Skip to content

feat(mailbox): channel-shaped envelope foundation (PR A) - #1444

Merged
namastex888 merged 1 commit into
devfrom
feat/channel-envelope-inbox-clean
Apr 28, 2026
Merged

namastex888 merged 1 commit into
devfrom
feat/channel-envelope-inbox-clean

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

Foundation PR for the CLI-native channels sprint. Extends the mailbox + native-inbox substrate to carry source and meta attribution alongside the message body, so future PRs (B–F) can route omni / system / external / peer-agent traffic through a single delivery substrate with structured provenance.

Adopts the semantics of Anthropic's Claude Code Channels (the <channel source="X" meta_k=v>body</channel> envelope) without adopting the MCP transport. Genie owns the source layer.

Replaces #1432 (closed) — that PR had stale snapshots and accumulated unrelated commits during the test-infra-blocked wait. This is a fresh single-commit rebase off current origin/dev with snapshot regen for 4.260428.7.

What lands

Schema (back-compat by default)

  • src/db/migrations/054_mailbox_source_meta.sql — source TEXT NOT NULL DEFAULT 'agent' + meta JSONB NOT NULL DEFAULT '{}' on mailbox. Idempotent (IF NOT EXISTS).

Types + plumbing

  • MailboxMessage interface gains source: string and meta: Record<string, unknown>.
  • mailbox.send(repoPath, from, to, body, opts?: { source?, meta? }) — opts optional; existing callers untouched.
  • NativeInboxMessage gains optional source and meta.

Channel envelope renderer

  • New src/lib/channel-envelope.ts — pure formatEnvelope + parseEnvelope. Default source='agent' returns plain body for back-compat; non-default sources render <channel source="X" from="Y" k="v">body</channel>.

Inbox-list rendering

  • genie inbox list prepends [<source>] for non-default sources in human render; JSON output exposes source and meta verbatim.

Test plan

  • bun test src/lib/__tests__/mailbox.test.ts src/lib/claude-native-teams.test.ts src/lib/channel-envelope.test.ts src/term-commands/agent/inbox.test.ts test/visual/tui-snapshot.test.tsx → 109/0/17 (all green incl. snapshots regen for 4.260428.7)
  • bun run typecheck clean
  • Pre-push hook (bun run check) passed cleanly — no --no-verify needed.

What this unblocks

  • PR B — codex UserPromptSubmit handler reads PG mailbox and returns pending messages as additionalContext.
  • PR C — migrate claude-code.ts:deliver (omni → claude) from tmux send-keys to writeNativeInbox with source='whatsapp'.
  • PR D — migrate claude-code.ts:injectNudge to writeNativeInbox with source='system'.
  • PR E — remove protocol-router.ts:injectToTmuxPane once metric confirms zero traffic.
  • PR F+ — first external channel adapter as a genie channel <kind> subcommand.

Design doc: .genie/brainstorms/codex-first-class-integration/DESIGN.md (landed via #1427).

🤖 Generated with Claude Code

Adopt the semantics of Anthropic's Claude Code Channels (structured
`<channel source="X" meta_k=v>body</channel>` envelope) without adopting
the MCP transport. Genie owns the source layer — `genie send`, omni →
whatsapp, system nudges, future external adapters all funnel through the
same mailbox + native-inbox pipeline, with `source` attribution telling
the receiving Claude where the message came from.

Why: PRs B–F (codex hook handler, omni → native migration, system-nudge
migration, injectToTmuxPane removal, external adapters) all build on
this foundation.

Changes:
- migration 054: ALTER TABLE mailbox ADD source TEXT DEFAULT 'agent',
  meta JSONB DEFAULT '{}'. Idempotent (IF NOT EXISTS).
- mailbox.send: opts arg `{ source?, meta? }`. Existing 4-arg callers
  unchanged. MailboxMessage carries source + meta on every row.
- channel-envelope.ts: pure formatEnvelope/parseEnvelope helpers.
  Plain-body passthrough for source='agent' (back-compat); structured
  tag for everything else. Round-trips meta values with attribute
  escaping.
- toNativeInboxMessage: when source != 'agent', wraps body in
  `<channel …>` and carries source/meta onto NativeInboxMessage.
  Default-source path is byte-identical to today's output.
- NativeInboxMessage: optional `source` and `meta` fields.
- genie agent inbox: `[<source>]` tag prepended to non-agent previews;
  JSON output surfaces source + meta verbatim per conversation.

Tests:
- src/lib/channel-envelope.test.ts: 12 cases covering round-trip,
  passthrough, escaping, malformed input.
- src/term-commands/agent/inbox.test.ts: 10 cases covering source tag
  rendering and JSON enrichment.
- src/lib/__tests__/mailbox.test.ts: extended with 4 cases for default
  source, explicit source=whatsapp round-trip, JSONB meta persistence,
  and back-compat for legacy 4-arg callers.
- src/lib/claude-native-teams.test.ts: extended with 2 cases for
  source/meta persistence + legacy-shape preservation.
- Existing fixtures in brief.test.ts, unified-log.test.ts,
  scheduler-daemon.test.ts updated with source/meta defaults.

Validation:
- bun run typecheck: clean
- bunx biome check .: only pre-existing complexity warnings in unrelated
  files
- bun test src/lib/__tests__/mailbox.test.ts src/lib/claude-native-teams.test.ts
  src/lib/channel-envelope.test.ts src/term-commands/agent/inbox.test.ts:
  92 pass, 0 fail

Out of scope (later PRs): codex UserPromptSubmit hook handler (PR B),
omni → native migration with source='whatsapp' (PR C), system-nudge
migration with source='system' (PR D), injectToTmuxPane removal (PR E),
external adapters (PR F+).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c03a4117-7f96-467a-8bf9-cffa55a4da70

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/channel-envelope-inbox-clean

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f3c1d79fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


const DEFAULT_SOURCE = 'agent';

const TAG_OPEN_RE = /^<channel\s+([^>]*)>([\s\S]*)<\/channel>\s*$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse channel attrs without breaking on > in values

parseEnvelope uses TAG_OPEN_RE with ([^>]*) for the attribute blob, so any attribute value containing > terminates the blob early and corrupts both parsed metadata and body. Since formatEnvelope does not escape >, a payload like meta: { reason: 'a > b' } produces an envelope that this parser misreads (e.g., body starts mid-attribute). This breaks the advertised format/parse round-trip for valid user text and can mis-route source/meta handling when such values appear.

Useful? React with 👍 / 👎.

Comment thread src/lib/mailbox.ts
Comment on lines +333 to +337
const meta: Record<string, string | number | boolean> = {};
for (const [key, value] of Object.entries(msg.meta ?? {})) {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
meta[key] = value;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve non-primitive meta when converting to native inbox

toNativeInboxMessage drops every non-primitive msg.meta entry before writing native.meta, even though the mailbox now accepts arbitrary JSON metadata. In the non-default-source path this silently loses nested metadata (objects/arrays) in engineer.json, so downstream JSON readers cannot round-trip the original attribution data. This is a data-loss regression for any channel integration that stores structured metadata.

Useful? React with 👍 / 👎.

@namastex888
namastex888 merged commit 06c5565 into dev Apr 28, 2026
11 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces source attribution and metadata to the mailbox system, allowing messages to carry origin information and associated data end-to-end. Key changes include a database migration adding source and meta columns, a new channel-envelope utility for formatting and parsing structured message tags, and updates to the mailbox API and CLI to support these fields while maintaining backward compatibility. Review feedback focuses on preventing data loss by allowing nested objects in the meta field of NativeInboxMessage and using more idiomatic destructuring in the envelope parser.

* attributes when the body is rendered into a `<channel …>` tag. Persisted
* verbatim so future readers can round-trip the data.
*/
meta?: Record<string, string | number | boolean>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The meta property on NativeInboxMessage is typed to only allow primitive values (string, number, boolean), but the underlying MailboxMessage and database schema support nested objects. This causes data loss in toNativeInboxMessage where non-primitive values are filtered out. To ensure metadata can be round-tripped correctly, this type should be Record<string, unknown>.

Suggested change
meta?: Record<string, string | number | boolean>;
meta?: Record<string, unknown>;

Comment thread src/lib/mailbox.ts
Comment on lines +333 to 349
const meta: Record<string, string | number | boolean> = {};
for (const [key, value] of Object.entries(msg.meta ?? {})) {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
meta[key] = value;
}
}

return {
from: msg.from,
text: msg.body,
text: formatEnvelope({ source, from: msg.from, meta: msg.meta, body: msg.body }),
summary,
timestamp: msg.createdAt,
color,
read: false,
source,
meta,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Following the recommended change to allow Record<string, unknown> for meta on NativeInboxMessage, this filtering logic is no longer necessary and currently causes data loss for nested objects. The meta from the original MailboxMessage should be passed through directly.

  return {
    from: msg.from,
    text: formatEnvelope({ source, from: msg.from, meta: msg.meta, body: msg.body }),
    summary,
    timestamp: msg.createdAt,
    color,
    read: false,
    source,
    meta: msg.meta,
  };

Comment on lines +92 to +98
const source = attrs.source ?? DEFAULT_SOURCE;
const from = attrs.from;
const meta: Record<string, string> = {};
for (const [key, value] of Object.entries(attrs)) {
if (key === 'source' || key === 'from') continue;
meta[key] = value;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code to separate source, from, and meta from the attrs object can be made more concise and idiomatic using object destructuring with a rest parameter.

Suggested change
const source = attrs.source ?? DEFAULT_SOURCE;
const from = attrs.from;
const meta: Record<string, string> = {};
for (const [key, value] of Object.entries(attrs)) {
if (key === 'source' || key === 'from') continue;
meta[key] = value;
}
const { source: parsedSource, from, ...meta } = attrs;
const source = parsedSource ?? DEFAULT_SOURCE;

@automagik-genie
automagik-genie deleted the feat/channel-envelope-inbox-clean branch September 25, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant