Skip to content

Inbox: launch agents through one host interface (stack 4a) - #2402

Open
backnotprop wants to merge 41 commits into
feat/inbox-launch-scratchfrom
feat/inbox-launch-host
Open

backnotprop wants to merge 41 commits into
feat/inbox-launch-scratchfrom
feat/inbox-launch-host

Conversation

@backnotprop

@backnotprop backnotprop commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #2398 (feat/inbox-launch-scratch, now merged in at fb9a6d45f, which brings #2378 at 3e86bbbc5 with the per-session localAnswer fix). Review these commits:

  • Inbox launch: launch agents through one host interface (the refactor);
  • Inbox launch: hosts keyed by their own agent; drop test-only and unused exports (review fixes);
  • Inbox launch: the seam test covers a question, Stop's withdraw and Delete through the fake host; the fake's answer in Terminal is per session (review fixes, and the e2e flake).

The merge commits only bring the stack together.

What this does

This is a pure refactor (PR 4a in the launch plan). The launch engine now launches agents through one interface:

  • packages/server/inbox-launch-host.ts: LaunchHost, a plain TypeScript interface, and LaunchHostContext, what the engine gives a host. There is no registry, plugin system or config. The engine builds hosts from [claude, ...options.hosts?.(context)], keyed by each host's own agent.

  • packages/server/inbox-launch.ts (the engine) names no agent. These come from the host or from launch.agent:

    • the record's agent and to.host;
    • target/composeBlock, which loop over the hosts;
    • compose's per-agent scratch_trusted;
    • the refusal text.

    Availability, the 10 s sessions view and the permission transport are kept per host.

  • packages/server/inbox-launch-claude.ts: createClaudeLaunchHost. It holds Claude's:

    • availability and startOf;
    • trust (trusts, one member; where trust is accepted stays inside the Claude host);
    • the settings file;
    • claude --bg, the bridge-poll live, respawn, stop/rm/purge (purge returns skipped inside a repository);
    • the attach and trust commands.

    createClaudeHost is the CLI driver.

  • packages/server/inbox-launch-claude-permission.ts: Claude's transport. It holds:

    • the hook's long-poll route, the key and the leases;
    • readDue/settledAfter and launchPermissionHooks;
    • the person-only terms: claudePermissionTerms, claudeInBypass, CLAUDE_TERMINAL_ONLY, CLAUDE_HANDOFF, CLAUDE_HANDOFF_PLAIN, and the one CLAUDE_NAME.
  • packages/server/inbox-launch-permission.ts is now agent-neutral: createLaunchQuestions, openPermissionSince and fileTarget. A question's author host and its delivery come from launch.agent.

After this PR, adding Codex is one new host file and its test fake.

No behaviour change

  • createInboxLaunch and createLaunchPermissions keep their signatures. hosts is a new optional option, used only by tests.
  • The persisted record, the routes, the JSON and the UI strings are unchanged.
  • No unit assertion was edited. One file had its imports moved: inbox-launch-permission.test.ts. Three of its lines now call the shared openPermissionSince(store, threadId) in place of the removed test-only permissions.openSince(root). The expected values are the same.

A record whose agent has no host (after a downgrade):

  • it gets no run line;
  • the sweep skips it;
  • signature() shows -;
  • every /api/inbox/launch/:id/* route and the permission route answer 404 launch_not_found.

Left for PR 4, as HOST.md says:

  • the card's Claude wording and its Auto/Full choices;
  • settingsState, which the engine still reads from the Claude host.

Review fixes

  • hosts keyed by host.agent. The option now returns a list.
  • The seam test (inbox-launch-host.test.ts, 3 tests) now also checks these:
    • a question asked through the shared createLaunchQuestions lands with the fake's agent as its author (a mutation that hard-codes "claude-code" there fails it);
    • Stop calls the fake's stop, then its transport's withdrawAll, and the question is superseded;
    • Delete calls stop, remove and withdrawAll;
    • a scratch launch's Delete calls the fake's purge for its folder.
  • The e2e flake (inbox-launch-permission.spec.ts, mode read as auto instead of bypassPermissions):
    • The cause was the fake Claude's localAnswer. It was one global switch, so it answered every session still on a dialog, including one left from an earlier test, and that session's record could land first.
    • Fix: localAnswer is now the id of the session the person answers in claude attach, and only that session's dialog takes it. Every call in the spec names its thread's session (sessionOf).
    • The ask-rule test's workaround comment went with it. No retries were added.
  • Nits:
    • removed the test-only openSince and the unused context members (store, now);
    • removed the unused type exports (ClaudeLaunchHost, LaunchQuestions, LaunchPermissions);
    • one CLAUDE_NAME.

Merge of #2398 at 528f6e2ed (claudePurgeRefusal and the dry-run gate stay in the Claude host's purge; the deleting Map that makes a double-click Delete answer once stays in the engine), and earlier at dd72e65be (S5):

  • The compose block carries agents[i].scratch_trusted from each host's trusts.
  • The scratch_inode identity checks and the scratch root's repository refusal stay in the engine (they are agent-neutral).
  • Claude's purge guard (claudeRepositoryAbove) stays in the Claude host.

Correction: the earlier body named the wrong commit for the smoke lines. They come from f40a32aa5 / f6ab5fa3d. Both guard failures are now fixed upstream (#2376 at 2e37ea8b0, #2378 at 90844fbeb), and they pass here.

Finishing for merge

This section does change behaviour, for launched Claude Code sessions only. Launch stays off by default.

  • One note writer. launchNoteText (the sanitizer) and writeLaunchNote live in inbox-launch-host.ts. Codex, OpenCode and Pi use them.
  • Attachments. launchUploadsDir names the Inbox uploads folder. A Claude launch's settings file allows Read there, so reading the person's own attachment doesn't sit on a prompt. The rule's real effect is for the real-CLI check.
  • The login env.
    • The settings file now carries the login PATH.
    • respawn now gets the login env, as start does: --bg and respawn may be what starts Claude's supervisor, and a worker inherits the supervisor's env.
    • With no SHELL in the Inbox's env (launchd), the login read uses the account's shell.
    • When the login env can't reach claude, it's found through the install folders of the plain env.
  • The plugin check. available() looks in the login shell's CLAUDE_CONFIG_DIR, the folder a launched session uses.

Tests

Before (upstream 528f6e2ed) This PR
Unit: 14 launch files, one file at a time 323 tests 323 pass, no assertion edited
New inbox-launch-host.test.ts – 3 pass
Guards: never-touch-the-real-inbox, no-browser-sentinel, agents-md-size – 18 + 7 + 3 pass
E2E: inbox-launch-claude + -permission + -scratch 34 tests 34 pass (10 + 18 + 6)
  • The unit files are:

    • inbox-launch-{claude,env,permission,settings,scratch} and inbox-permission-answers;
    • core inbox-run-line, inbox-permission and inbox-scratch;
    • inbox-permission-hook, composeModel, sendTargets, shared/config and agent-cli-stubs.

    Their tests are the ones at 528f6e2ed; only the imports changed.

  • bun run typecheck is clean.

  • The apps were built in order (review, hook, opencode), and .local/plannotator was compiled.

  • The e2e runs used Bun 1.4.4, the e2e lock, a temp HOME and data dir, PLANNOTATOR_INBOX_CONNECT=0 and NO_BROWSER.

Line counts (engine + Claude + shared permission, plus the new files):

  • before: 1,864 (1,021 + 396 + 447);
  • after: 2,226 (1,005 + 536 + 237, plus inbox-launch-claude-permission.ts 306 and inbox-launch-host.ts 142);
  • plus the 233-line seam test.

backnotprop and others added 8 commits October 30, 2026 19:20
…nto feat/inbox-launch-host

# Conflicts:
#	docs/agent-guides/inbox-launch.md
#	docs/agent-guides/server-and-storage.md
#	packages/server/inbox-launch-claude.ts
…nto feat/inbox-launch-host

# Conflicts:
#	docs/agent-guides/inbox-launch.md
#	docs/agent-guides/server-and-storage.md
#	packages/server/inbox-launch.ts
The engine (inbox-launch.ts) now holds `{ "claude-code": claude }`, one
LaunchHost per agent (inbox-launch-host.ts), and names no agent itself:
the record's agent, to.host and a permission question's author come from
the host. Claude Code's CLI calls, settings file, trust, availability,
bridge liveness, respawn, stop/rm/purge and attach move into
createClaudeLaunchHost (inbox-launch-claude.ts); its hook route, key,
leases and person-only terms into inbox-launch-claude-permission.ts. The
shared questions (ask, answer, withdraw) stay agent-neutral in
inbox-launch-permission.ts.

No behaviour change: signatures of createInboxLaunch and
createLaunchPermissions kept (hosts is an optional, test-only addition),
the record, routes and UI strings unchanged. A record whose agent has no
host gets no line, no sweep and 404 launch_not_found.

New: inbox-launch-host.test.ts registers an in-memory fakeLaunchHost and
drives start, the run line, Stop and a reply's restart through it.

Co-Authored-By: Claude <noreply@anthropic.com>
…feat/inbox-launch-host

# Conflicts:
#	docs/agent-guides/inbox-launch.md
#	docs/agent-guides/server-and-storage.md
#	packages/server/inbox-launch-claude.ts
#	packages/server/inbox-launch-permission.ts
#	packages/server/inbox-launch.ts
…lete through the fake host; the fake's answer in Terminal is per session
…feat/inbox-launch-host

# Conflicts:
#	packages/server/inbox-launch-claude.ts
#	packages/server/inbox-launch.ts
backnotprop and others added 3 commits October 30, 2026 23:36
One shape for Codex (#2407), OpenCode (#2408) and Pi (#2412), so each can
merge this and drop its own variant. Claude Code uses none of the optional
seams; its behaviour and its words are unchanged.

- LaunchHostContext.loginEnv(): a fresh login-shell read (shared with one
  under way, never held), for a host about to spawn its own process.
- LaunchHost.close?(): the Inbox stops; the engine's stop() calls it on
  each host after it unsubscribes.
- LaunchHost.stoppedNote?(launch): per launch, the words after
  "<name> stopped." when the session stopped without the person's Stop
  (the stopped line's `cause`); none: today's line.
- A resume that throws is a failed restart ("It did not start again."),
  logged to start.log, never an unhandled rejection.
- The permission card names the agent from its question's author
  (agentNameOf); Claude Code's cards read as before.
- Merge-friendly lists: the engine's ownHosts, InboxLaunchAgent and
  OWN_FAKES one entry per line.
A host's `live` may read null as soon as its stop begins (OpenCode's does), so
the 1 s sweep's maybeRespawn could resume a session mid-Delete (a server
started into a session being deleted, then orphaned) or undo a Stop within a
second. The sweep now skips a launch in `stopping` or `deleting`, and a reply
still waiting when the person pressed Stop is held (`Memo.heldReply`): the
person's next reply starts the session again and takes it along.

Test: inbox-launch-host.test.ts, a slow Stop and a slow Delete with a reply
waiting (fails without this change).

Co-Authored-By: Claude <noreply@anthropic.com>
…attachments and plugin check

- launchNoteText and writeLaunchNote live in inbox-launch-host.ts, for the hosts that write thread notes.
- Read on the Inbox's uploads in the settings file: an attachment is read without a prompt.
- The login PATH goes in the settings file; a respawn runs with the login env, as a start does.
- With no SHELL in the Inbox's env the account's own shell is read, never plain sh.
- A start whose login env can't reach claude finds it in the install folders.
- The plugin check reads the Claude folder the login shell names.
# Conflicts:
#	packages/server/inbox-launch.ts
#	tests/agent-cli-stubs.test.ts
# Conflicts:
#	tests/agent-cli-stubs.test.ts
# Conflicts:
#	docs/agent-guides/inbox-launch.md
# Conflicts:
#	docs/agent-guides/inbox-launch.md
# Conflicts:
#	packages/server/inbox-launch-permission.ts
…aunch-host

# Conflicts:
#	packages/server/inbox-launch.ts
…and Delete) into feat/inbox-launch-host

# Conflicts:
#	packages/server/inbox-launch.ts
…x-launch-host

# Conflicts:
#	packages/server/inbox-launch.ts
…ner, guest stopped line) into feat/inbox-launch-host

# Conflicts:
#	packages/inbox/components/RunLine.tsx
#	packages/server/inbox-launch.ts

This branch has not been deployed

No deployments
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