Skip to content

feat(orchestration): a native chat can be a dispatch worker, like a terminal agent - #22972

Merged
brennanb2025 merged 15 commits into
mainfrom
brennanb2025/d6-chat-worker
Oct 7, 2026
Merged

brennanb2025 merged 15 commits into
mainfrom
brennanb2025/d6-chat-worker

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1089 $\color{#cf222e}{\Huge{\mathbf{−}}}$​27 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1062
Prod 40 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​929 $\color{#cf222e}{\Huge{\mathbf{−}}}$​145 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​784

The problem

Orca's orchestration lets one agent (the coordinator) hand a task to another agent (a worker), follow its progress, and collect its result. A worker could be an agent running in a terminal pane, or a native-chat worker that Orca started itself, but not a native chat that already exists (Orca's built-in chat view for Claude and Codex). Asking for one was refused. dispatch --to orca_session_id:<id> answered session_chat_not_dispatchable ("a chat can't receive a dispatch yet"), and worker-start --terminal orca_session_id:<id> failed with a misleading terminal_handle_stale. So a coordinator could not hand work to a chat that already had the right context; it had to start a fresh worker instead.

ELI5

Any native chat can now be given a task the way a terminal agent is. The coordinator uses the same two commands and names the chat by the ID every chat agent is already shown (orca_session_id:<id>) instead of a terminal handle. The chat gets the task the way a message you type gets there: if it is idle, the task starts a turn; if it is busy, the task waits in the chat's queue as a normal card, which you can see, pause or delete like your own. The task is labelled with who sent it ("Message from ", and "From " on a waiting card), like other agents' messages in a chat. The chat reports back with the same worker_done command a terminal worker uses, and the coordinator lists, shows, reads, stops and abandons it with the same commands. Stopping or abandoning it never closes your chat.

What this changes for you

Situation Before After
A coordinator gives an existing chat a task (dispatch --to orca_session_id:<id> --inject, or worker-start --terminal orca_session_id:<id>) Refused Accepted. The chat gets the task and reports with worker_done, like a terminal worker
The chat is idle — The task starts a turn at once. worker-start answers ready
The chat is busy with your own turn — Your turn finishes untouched. The task waits as a card in the chat's queue and is sent as its own turn after yours. worker-start answers outcome_unknown and says the task is waiting as a queued card, the same answer a terminal start gives when it can't see the agent begin
You press Stop, or Orca restarts, while the task card waits — The card is paused, like your own queued messages, until you send a message or press Resume. You can also delete it
worker-list / worker-show on the chat worker — live while the chat is open (also when it is resting and its next message would start it), exited once you close it, unverifiable when this machine can't tell (for example, a chat on another host)
worker-read — Reads the chat's current conversation, following /clear
worker-stop / worker-abandon — The task is stopped or abandoned; your chat stays open and keeps whatever it is doing, as a terminal reused with --terminal does
You close the chat while it has a task — The task fails as "closed by operator", the same as closing a terminal worker's pane
Mail the coordinator sends to the worker (dispatch:<id>) — Reaches the chat's "you have new messages" notice, as for a terminal worker
A chat on another machine (SSH, WSL, a paired host) — Refused with the existing host-boundary error, as mail to it is

What Changed

  • The task goes through the chat's own send. It uses sendAgentTurn with queueing on, exactly like the "you have new messages" notice and like a message you send with queueing on. There is no separate task store, hold, retry or readiness gate. The message records who it is from (body.from) with a new task kind: the sender, Run, task and Dispatch. The chat then names the coordinator the way it names any agent's message. A native-chat worker that Orca starts itself gets the same label on its task.
  • One send per Dispatch. The send's operation ID is derived from the Dispatch, so a retried request replays the first answer instead of queueing the task twice.
  • worker-start reads the send's outcome with existing states only. Accepted means ready. Queued or not yet started means outcome_unknown; the task has been handed over and is not resent. Refused or rejected means failed. A chat's Dispatch records the chat by its Orca session ID (the ID it had before any /clear) and has no pane or process, so a chat worker's own sub-dispatches count toward the nesting limit.
  • Liveness is read from the chat's records, the same rule the mail lane uses to decide whether a chat can be reached. Only a chat you closed reads exited; missing evidence reads unverifiable, never exited.
  • Closing a chat settles its Dispatch through the same path a closed terminal pane uses, once the close is proven. A close that is rolled back (for example a worktree delete Orca refuses) leaves the task alone.
  • Refusals. These cover an unknown chat, a provider's own session ID, a chat on another host, a closed chat, a chat in a different worktree than the worker, and a chat handing a task to itself with --inject; each is refused before anything is created. A chat that already has an active task is refused too. dispatch refuses it up front; worker-start fails it the same way it fails a terminal that already has one, after recording the new task as failed. worker-start --on <host> --terminal orca_session_id: is refused at home, before any remote call.
  • dispatch --inject to a chat keeps the connection alive while the chat's agent starts. A chat at rest can take longer than the socket's idle limit to accept, which used to read as "Restart Orca". Only chat targets get this; terminal --inject is unchanged.
  • Coordinator mail that was held when the chat's agent stopped is pointed at again on the chat's next idle moment, including in the session a /clear continued it in, as for a terminal worker.
  • The command name in the task text for any native-chat worker is the one its mail notices use (orca-dev in a dev build), instead of always orca.

Why

Before this change, a chat could not be a worker at all. An earlier version of this PR gave the task its own store, its own delivery lane and a readiness check, and live testing showed what that costs: a second "is the chat free?" rule beside the queue's own drifted from it, and a task could be merged into a turn the user had just started. Sending the task through the chat's existing send and queue removes that class of bug, because there is only one rule. It is also the common pattern for one agent messaging another: the message uses the same send a person's does, and the queued row records who sent it.

Where a terminal worker lives with an edge, the chat lives with the same one instead of gaining an extra rule.

Differences from a terminal worker

  • Intended: a busy chat queues the task instead of making worker-start wait for it to go idle. The queue already protects your turn, and the task card is visible and deletable.
  • Intended: stopping or abandoning a task does not withdraw a task card that is still waiting. It is your card to keep or delete. If it later runs, the chat's worker_done for the ended task is refused. A terminal handed a task with --inject while busy behaves the same way.
  • Intended: deleting a waiting task card leaves the task open until the coordinator abandons it. The coordinator gets no signal, as when you clear input typed into a terminal worker. A deletion is your action, and it never settles orchestration bookkeeping for you.
  • Intended: after an Orca restart, a waiting task card stays paused until you act. This matches your own queued messages and mail notices. A terminal's own input queue would send it.
  • Intended: a chat on another host is refused, as mail to it is, until reach to it can be proven.
  • Temporary: a coordinator on a paired remote client keeps its own 60-second timeout without keepalives. A very slow chat start can read as a timeout there even though the task was delivered. terminal.send with an agent prompt has the same gap today.

Compatibility

  • Mixed versions. The sender travels on the message to clients, like any agent message. An older client or host that doesn't know the task kind keeps the sender and drops only the task details, so it still shows "From ", and opening the sender looks it up by its address. The one removed error code, session_chat_not_dispatchable, has no remaining reader.
  • SSH / WSL / remote. A chat whose session runs on another host is refused. Loss of contact never reads as exited.

Linked Issue

Part of the native-chat orchestration work (no separate issue).

Visual Proof

After the sender label landed (re-QA at 8e88b50):

A waiting task card names its coordinator

q2-queued-card

The task turn names its coordinator

q2-task-label

A task from a chat coordinator, named by that chat's title; clicking the name opens it

q1-w1-label

dispatch --inject from a terminal coordinator

q3-w3-label

Earlier full QA (3effc5f, before the sender label):

Live QA on a separate Mac at 3effc5f (the final head adds only wording and a main merge). It ran in a hidden window with Claude native chats, and the screenshots were taken over the DevTools protocol.

An idle chat takes its task as one turn and answers it

s1-w1-task-turn

The coordinator chat gets a ready receipt and the worker's worker_done

s1-c-receipt

The chat is busy with the person's own sleep 60; the task waits as a queued card

s2-queued-card

The person's turn finished with its own answer; the task then ran as its own turn

s2-after

The person pressed Stop: the queue pauses with Resume, and the task card is not sent

s7-paused-card

The person deleted the task card

s7-card-deleted

dispatch --inject into a chat at rest: the task arrives with no connection error

s3-at-rest

The coordinator's mail reaches the chat worker as a "you have new messages" turn

s3-w3-after

After worker-stop the chat keeps running its command; nothing is closed or interrupted

s4-after-stop

After abandon the chat is still usable; its late worker_done is refused

s4-usable

Closing the worker chat (its Dispatch fails as closed by operator)

s5-after-close

Regression check: a terminal worker still starts and reports as before

r-terminal-worker-2

Testing

  • New suites run the real chat host, queue, RPC server and database, with only the agent process faked: structured-chat-dispatch-worker.test.ts, structured-chat-dispatch-worker-lifecycle.test.ts, structured-chat-worker-start.test.ts, orchestration/chat-assignee.test.ts, shared/agent-session-message-source.test.ts, plus long-poll and close-suite cases.
  • They cover:
    • an idle chat taking its task;
    • a busy chat holding it as one card;
    • a retry queueing nothing new;
    • self-dispatch, other-host, wrong-worktree and remote --on refusals;
    • the coordinator's mail reaching the chat;
    • the nesting limit;
    • stop and abandon leaving the chat open;
    • a closed chat failing its Dispatch, and a rolled-back close not failing it;
    • a /clear successor reporting as the chat;
    • the chat's worker_done admitted and two forged senders refused;
    • the downgrade decode.
  • Each key behaviour's test was checked to fail with its production line reverted.
  • New tests also cover the sender on the task and on a native-chat worker Orca starts, held coordinator mail pointed at again after a /clear, and opening a task's sender.
  • On the final head, targeted suites pass locally (the chat, mail, sender, /clear and close suites). The node, CLI and web typechecks pass, and oxlint is clean. CI is green.

Notes

Deferred, not in this PR:

  • A /clear'd worker that Orca started being read and stopped through the session that continues it. That is a separate fix for those workers.
  • terminal read taking any chat's ID.
  • Showing a chat worker's working/blocked activity in worker-list (it reads unknown; workers Orca starts in a native chat have the same gap).
  • Receipt fields that still say "terminal" for a chat in places (the placement effect's kind, and the "already has an active dispatch" wording).
  • Guide text telling agents that a chat can be a worker.

@brennanb2025
brennanb2025 force-pushed the brennanb2025/d5-self-address branch from cad7778 to 486afc2 Compare September 26, 2026 01:40

@pullfrog pullfrog 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.

Important

Two concrete issues: the chat dispatch preamble now persists the plaintext dispatch capability in a check-readable mailbox, and the mail lane treats any dispatch-type message in any structured mailbox as a preamble. Both are fixable in the lane; details inline.

Reviewed changes

  • Chat as a Dispatch assignee. session_chat_not_dispatchable is gone; resolveDispatchAssigneeParty now returns a chat party, so dispatch --to session:<chat> and worker-start --terminal session:<chat> are accepted. Rows carry assignee_handle=session:<root>, assignee_orca_session_id=<root>, null pane/process, and the preamble is owed as a dispatch-type message in the Dispatch mailbox rather than typed into a pane.
  • Session-proven authority. verifyDispatchCapability gained a host-verified, canonicalized orcaSessionId branch, and assertChatAssigneeReachable reuses the mail reach rules (other-host / closed / unknown / worker-identity-lost) on both write paths.
  • /clear lineage. New executingSessionId/lineageHeadSessionId route structured-worker authority, observation, worker-read/stop/archive, agent status and terminal read through the live successor; observeStructuredWorker is split into observeStructuredSession.
  • Caller cap. settleWithinCallerCap returns the outcome_unknown receipt inside a native-session caller's shell-tool limit while finish() keeps starting the worker.
  • terminal read session:<id> for a chat or structured worker, served from the live session's journal.
Technical details
# Capability persisted in a check-readable mailbox

## Affected sites
- `src/main/runtime/orchestration/dispatch-preamble-turn.ts:27` — stores the full preamble (incl. `--dispatch-capability dcap_…`) as the `dispatch:<id>` message body.
- `src/main/runtime/rpc/methods/orchestration/messaging/check-methods.ts:73` — `getActiveDispatchForIdentity('dispatch:<id>')` looks up `assignee_handle = 'dispatch:<id>'`, which never matches a chat row (`session:<root>`), so the ownership fence is skipped.
- `src/main/runtime/rpc/methods/orchestration/messaging/check-direct.ts:24` — reads the mailbox with no fence; `exposeReadMessages` (`mailbox-message-receipt.ts:37`) does not redact.

## Required outcome
A `dispatch:<id>` mailbox read must not disclose the dispatch capability that `verifyDispatchCapability` checks with `timingSafeEqual`.

## Suggested approach (optional)
Reuse the existing `[dispatch capability redacted]` redaction (`src/main/runtime/orchestration/worker-transcript-payload.ts:19`) on the direct-mailbox read path, as the transcript paths already do. The lane reads `preamble.body` from the DB directly, so redacting the `check` receipt does not affect the intended turn delivery.

## Open questions for the human
The module docs treat the session id as "not a credential" (same-host, same user), so if reading another dispatch's capability is considered in-scope, this is a real weakening of the only control that gates `worker_done` settlement; if cross-agent spoofing is explicitly out of scope, it is still a defense-in-depth regression from the PTY/structured paths, which redact it.
Technical details
# Any `dispatch`-type message is treated as a preamble

## Affected sites
- `src/main/runtime/orchestration/structured-mailbox-pointer-delivery.ts:225` — the preamble branch keys only on `message.type === 'dispatch'` for every mailbox the structured lane owns (`dispatch:<id>`, `run:<id>`, direct `session:<id>`).
- `src/main/runtime/orchestration/types.ts:3` / `src/cli/specs/orchestration.ts:75` — `dispatch` is a documented, user-sendable `--type`.

## Required outcome
Only the Dispatch's actual preamble is delivered as a raw turn and marked read+delivered; a user-sent `dispatch`-type message in a `run:` or direct `session:` mailbox keeps the normal pointer/`check` path.

## Suggested approach (optional)
At minimum scope the branch to `mailboxHandle.startsWith('dispatch:')`. Stronger: identify the preamble row itself (e.g. record its message id on the Dispatch) so a user `--type dispatch` sent into a Dispatch mailbox is still pointed rather than silently marked read.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/runtime/orchestration/dispatch-preamble-turn.ts Outdated
Comment thread src/main/runtime/orchestration/structured-mailbox-pointer-delivery.ts Outdated

@pullfrog pullfrog 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.

ℹ️ No new issues in the gap-fix commits; one minor liveness-read inconsistency inline.

Reviewed changes

  • Fleet liveness from the session host. The federated fleet overlay was extracted into a shared fleet-execution-host-verdict.ts and applied to local structured rows too, so worker-list and worker-show project a chat's or structured worker's liveness from the session host instead of unverifiable / missing_status; worker-list installs the lazy session host when a structured row is present.
  • Settled Dispatch no longer leaks its preamble. routeUnreadDispatchMailboxToRunMailbox now excludes dispatch-type rows, so a Dispatch that settles while its owed preamble is still unread cannot hand that preamble to the coordinator.
  • One --terminal receipt. decideWorkerStartMode returns a single mode: "reused" receipt (reused_terminal, mode-neutral detail) for a terminal handle and a session: address alike, and the host probe/downgrade is skipped for it.
  • Remote caller cap. worker-start --on from a session caller returns the outcome_unknown receipt inside the caller's shell-tool cap while the attach continues (settleWithinCallerCap, federatedInProgressReceipt).
  • Shared session predicate. isStructuredSessionAddress and observeStructuredAssignee collapse the "minted worker or chat" checks used by the runtime and worker observation.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts Outdated
@brennanb2025
brennanb2025 force-pushed the brennanb2025/d6-chat-worker branch from 40ba689 to 95eb26b Compare September 26, 2026 07:17

@pullfrog pullfrog 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.

✅ No new issues found. The two prior findings are addressed by the new commits; the earlier worker-show liveness thread remains open and unchanged.

Reviewed changes

  • Host-minted preamble identity. The owed chat preamble is now identified by a dpreamble_<dispatchId> id only the host mints (dispatch-preamble-identity.ts), so no --type or body can forge it; the lane delivers only that row as a turn, and every SQL mail reader (getUnreadMessages, getAllMessages, getInbox, getAllMessagesForHandle, getOrCreateMailboxDelivery, the settled-Dispatch reroute) hides it through one SQL form of the rule. This closes the check-readable-capability and dispatch-type-overload findings.
  • Typed lineage failure contract. resolveExecutingSession returns here/other-host/unrecorded; executingSessionId throws the typed host-boundary refusal for another host, and an unreadable record store throws session_caller_not_live rather than falling back to the pre-clear session. structuredWorkerSessionId no longer swallows.
  • Durable starting receipt. A session caller's capped worker-start (local and --on) returns state: starting with nextCommands: [worker-show], no failedStage/lastError; the CLI exits 0 on starting and 1 on outcome_unknown/failed.
  • Fleet activity. worker-list and worker-show now pass agentStatus into projectWorkerFleet, so a live structured or chat row carries its activity from getAgentStatusForHandle.
  • Shared wording and typed select. Coordinator-self/other-worktree refusals share one wording for a terminal and a chat; selectMessageRows is the one typed select over messages.
  • Twin-run parity test. orchestration-assignee-parity.test.ts diffs the agent-visible results of one coordinator script against a terminal and a chat assignee.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 0/5

[High risk] Extends dispatch system to support chat as a worker type.

The PR does not appear safe to merge while the in-doubt turn gate and previously reported blocking issues remain.

Findings

  1. P1 In-doubt turns appear idle ▶
  2. P1 Security Chat history lacks read authorization ▶
  3. P1 Security Worker read exposes earlier turns ▶
  4. P1 One worker breaks fleet listing ▶
  5. P1 Unreachable chat aborts group send ▶
  6. P1 Existing preamble tables lack columns ▶

Summary

The PR lets an existing native chat serve as a dispatch worker, delivers its preamble through the structured-session lane, and extends worker observation and reading across cleared-session successors.

  • The latest gate change can allow a new instruction into a turn whose acknowledgement is still in doubt.

Reviews (12) · Last reviewed commit: "fix(orchestration): the gate holds for a..."

Comment on lines +123 to +136
function structuredSessionReadTarget(handle: string, db: OrchestrationDb | null): string | null {
const party = parseOrcaSessionAddress(handle) ? resolveOrchestrationParty(handle, db) : null
const workerHandle = party ? party.terminalHandle : handle
if (workerHandle !== null) {
const worker = resolveStructuredWorkerAuthority(workerHandle, db)?.identity
return worker ? structuredWorkerSessionId(worker) : null
}
const store = readAgentSessionRecordStore()
const executing =
party?.orcaSessionId && store ? resolveExecutingSession(store, party.orcaSessionId) : null
if (executing?.kind === 'other-host') {
throw otherHostSessionRefusal(party?.orcaSessionId ?? handle)
}
return executing?.kind === 'here' ? executing.sessionId : null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Chat history lacks read authorization

When an agent or client knows another chat’s session:<id> address, terminal read now resolves that address and returns the chat’s journal. The terminal.read handler passes the address through without checking whether the caller may read that chat, so earlier user messages from an unrelated conversation can be exposed.

How this was verified: The terminal-read path resolves a session address and reads its journal without a caller-to-session authorization check.

Comment on lines +112 to +115
const address =
db.getWorkerDispatch(dispatchId)?.agent_terminal_handle ??
db.getDispatchContextById(dispatchId)?.assignee_handle
const chat = parseOrcaSessionAddress(address)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Worker read exposes earlier turns

When an existing chat is assigned a dispatch, worker-read selects that chat’s session journal rather than output produced for the dispatch. A coordinator reading task output can therefore receive up to 200 earlier conversation items, including unrelated user turns.

How this was verified: The chat-assignee path passes the chat session to a journal-tail read that does not filter by dispatch.

): Promise<void> {
for (const worker of projected) {
const handle = durable.get(worker.dispatchId)?.agentTerminalHandle
const observation = handle ? observeStructuredAssignee(handle, host.db) : null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 One worker breaks fleet listing

If one structured assignee’s record store cannot be read, or its executing /clear successor is recorded on another host, observeStructuredAssignee throws here. Because the page projection does not catch that error per worker, the entire worker-list request fails and hides unrelated healthy workers instead of showing the affected assignee as unverifiable.

@pullfrog pullfrog 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.

ℹ️ No new issues in this incremental delta. The earlier worker-show liveness thread remains open and was not touched by these commits.

Reviewed changes

  • Narrowed lineage failure contract. resolveExecutingSession now converts only store.getRecord failures into the typed session_caller_not_live refusal, so a bug in the walk surfaces as itself rather than a false not-live verdict.
  • Async session-read adaptation. projectWorkerFleet and applyStructuredSessionVerdicts await the now-async getAgentStatusForHandle; projectFleetWorker(Page) moved into worker-list-projection.ts and every caller (worker-control, federated-worker-show, worker-list, worker-observation) awaits it. The PR's test session hosts and the --terminal reused receipt expectation were updated to match.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 force-pushed the brennanb2025/d5-self-address branch from 106af5e to 079aa5d Compare September 29, 2026 06:33
@brennanb2025
brennanb2025 force-pushed the brennanb2025/d6-chat-worker branch from 5197f00 to 56562b4 Compare September 29, 2026 06:45

@pullfrog pullfrog 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.

ℹ️ No new issues in this incremental delta. It is a test-only commit; the earlier worker-show liveness thread remains open and was not touched by it.

Reviewed changes

  • Provider-death hold regression test. orchestration-chat-assignee.test.ts now pins that a chat whose latest send never ran because its provider exited gets neither its owed preamble nor a peer pointer across repeated idle edges, leaves the preamble row unread, and receives it exactly once after a later send runs.
  • Test-host fixtures. The chat/parity/cleared-session test hosts return submissions (the AgentJournalSnapshot field the pointer gate reads), and agent-facing-parity.test.ts's readGateFacts supplies latestSendHoldsMail: false. No production code changed.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog 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.

ℹ️ No new issues in this incremental delta — both commits are test-only, and the rewritten regression test faithfully models the host operation ledger.

Reviewed changes

The delta since the prior pullfrog review is two test commits (no production code changed).

  • Rewrote the dead-provider restart regression. orchestration-chat-assignee.test.ts now fakes the host's operation ledger (a recorded clientOperationId replays its verdict and starts nothing) and counts real provider starts. It pins one start for the preamble, none across three idle edges, the preamble left unread, and exactly one delivery marked read once a person's later turn has run.
  • Trimmed the real-host preamble test to its suite's line budget. structured-chat-coordinator-mail.test.ts loses blank lines and inlines the capability extraction and the check call.
  • Dropped a stale import. structured-mailbox-pointer-delivery.test.ts removes the unused formatMessagePointer import and reformats two setSubmissions calls.

The rewritten test drives the real decideStructuredPointerAttempt reuse path (structured-pointer-operation-id.ts) through readPointerGateFacts/snapshot.submissions; removing the single-operation-id rule makes it red (the recorded ablation shows a second provider start), so it is not theatre. The open worker-observation.ts:81 liveness thread is untouched by this delta and remains open.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 changed the base branch from brennanb2025/d5-self-address to main October 2, 2026 00:48
@brennanb2025
brennanb2025 force-pushed the brennanb2025/d6-chat-worker branch from 1b6be5a to 7ece5ed Compare October 2, 2026 00:48
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 8514bd13-ebfe-4e75-8d8a-888799b2956b

📥 Commits

Reviewing files that changed from the base of the PR and between 0e06d91 and b7d6b53.

📒 Files selected for processing (8)
  • src/main/runtime/orchestration/dispatch-preamble-turn.test.ts
  • src/main/runtime/orchestration/dispatch-preamble-turn.ts
  • src/main/runtime/orchestration/structured-mailbox-pointer-host.ts
  • src/main/runtime/orchestration/structured-session-pointer-delivery.test.ts
  • src/main/runtime/orchestration/structured-session-pointer-delivery.ts
  • src/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.ts
  • src/main/runtime/rpc/orchestration-chat-assignee.test.ts
  • src/main/runtime/structured-chat-coordinator-mail.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

The changes add support for Orca session addresses as chat Dispatch assignees. They add persistent Dispatch preamble turns and deliver those turns through structured mailbox handling. Worker authority, readiness, lifecycle, messaging, terminal reads, and fleet projections now resolve chat assignees and sessions continued through /clear. New tests cover chat-assignee workflows, preamble delivery states, worker parity, session lineage, and worker-list output.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to b7d6b

The incremental changes look sound, but two earlier concerns remain open. After a restart, worker-show can disagree with worker-list for a structured worker. Lineage resolution errors may also propagate to callers that expected a null result. Both are bounded, and the merge-readiness risk is low.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to b7d6b

Chat sessions can now execute delegated tasks. The inspected targeting and ownership checks remain in place, but whether an already-started task submission is prevented from taking effect after cancellation was not established.

Retained concerns

  • Medium · security · inferred: Dispatch cleanup prevents future preamble discovery, but does not visibly revoke an already-claimed asynchronous submission. If the underlying chat host does not independently enforce Dispatch termination before acceptance, canceled or settled task instructions could still start in the existing chat. The final enforcement boundary remains unverified.
Security review details

Security Blast Radius

  • inferred — The demonstrated added exposure is coordinator-supplied task text entering an eligible existing chat as a user-role turn. Inspected routing confines it to reachable local session lineage and applies worktree checks on worker-start; broader tenant, environment, credential, or tool-authority exposure was not established.

Security Findings and Attack Paths

  • inferred — The unresolved path requires a valid chat Dispatch and termination concurrent with asynchronous submission. If acceptance ignores the ended Dispatch, its task text could still initiate work in the chat. Ordinary cleanup and initial ownership checks are counterevidence, but do not resolve this interleaving; no successful unauthorized acceptance was demonstrated.

Trust Boundaries and Controls

  • observed — The host wrapper forwards session runtime fencing and operation identity rather than an explicit Dispatch-liveness assertion. Mutation admission checks fingerprints, session availability, durable operation admission, and replay; its preparation step can await before execution. Complete caller authorization and Dispatch revocation enforcement were not established.

Resilience and Maintainability Implications

  • observed — Uncertain submission remains distinguishable from provider acceptance, and worker-start retains lifecycle authority without falsely declaring the worker ready. This supports recovery while keeping uncertainty visible to callers.

Hardening Proposals

  • proposed — If not already guaranteed by the underlying host, bind preamble acceptance to Dispatch liveness and define cancellation semantics for pending submissions. A deterministic termination-during-admission test would establish that cleanup cannot leave executable stale instructions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.99% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 169 functions across 71 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: native chats can serve as Dispatch workers like terminal agents.
Description check ✅ Passed The description is detailed and covers the problem, user-facing changes, rationale, visual proof, testing, compatibility, and deferred work. The Linked Issue section says no separate issue exists; the…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@pullfrog pullfrog 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.

ℹ️ No new issues in this delta. It is a rebase onto current main plus a test-only fixture extraction; the previously flagged worker-observation liveness-read note remains open and is unchanged by these commits.

Reviewed changes

  • Rebased onto current main. The branch was retargeted from brennanb2025/d5-self-address to main and the 13 feature commits squashed into 2 on 02790c53e8. Two main changes forced production adaptation: the dispatch capability is gone (no more dcap_/capability_hash; prepareStartingWorkerAuthority returns void, preamble delivery no longer takes a capability, and failed-start-dispatch-identity.ts guards on process_incarnation IS NULL), and the session address was renamed session: → orca_session_id: with "orchestration address" → "Orca session ID" wording throughout.
  • Replaced the address-visibility helpers. agentVisibleOrchestrationAddress/withAgentVisibleAddresses are gone; orcaSessionIdOrHandle in orchestration-party.ts is now the one way a preamble names a party (session by its Orca session ID, else its handle).
  • Extracted the real structured-chat host harness. structured-chat-coordinator-mail.test.ts's inline host, database, runtime and dispatcher setup moved to structured-chat-real-host-test-fixture.ts unchanged, freeing the suite's line budget for the chat-assignee dispatch case.

Verification on this head: pnpm tc:node clean, changed-code quality gate clean (0 findings / 59 files), and the chat-assignee, cleared-session, coordinator-mail, parity, pointer-delivery, start-mode, authority and agent-facing-parity suites pass (124 tests).

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog 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.

ℹ️ No new issues in this test-only commit. It strengthens the existing chat-assignee rejection coverage; the previously flagged worker-observation liveness note remains open and is untouched here.

Reviewed changes

  • Pinned the exact rejection of a foreign worker_done. The existing "another chat's worker_done is rejected" case now asserts the sender_not_assignee code and the received handle orca_session_id:<id> reason, and that the Dispatch stays dispatched.
  • Added an impersonation case. A second chat that names the assignee's Orca session ID as its sender is refused consumer_fenced (effectsApplied: false), proving the host fences on the caller's own resolved session, not the declared handle. The suite passes (23 tests) on this head.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai 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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: c2d9830c-2bd1-4cfc-b7aa-af96c6fb614c

📥 Commits

Reviewing files that changed from the base of the PR and between 02790c5 and 1ed4499.

📒 Files selected for processing (61)
  • src/cli/handlers/orchestration/worker-list-session-address-row.test.ts
  • src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts
  • src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts
  • src/main/runtime/orca-runtime-resolve-terminal-pane.ts
  • src/main/runtime/orca-runtime-stop-requested-pty-ids.ts
  • src/main/runtime/orchestration/agent-facing-parity.test.ts
  • src/main/runtime/orchestration/db/dispatch-context/dispatch-context-store.ts
  • src/main/runtime/orchestration/db/messages/direct-mailbox-routing.ts
  • src/main/runtime/orchestration/db/messages/message-inbox.ts
  • src/main/runtime/orchestration/db/messages/role-mailbox-delivery.ts
  • src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-authority.ts
  • src/main/runtime/orchestration/db/worker-terminal/failed-start-dispatch-identity.ts
  • src/main/runtime/orchestration/db/worker-terminal/worker-terminal-resource-store.ts
  • src/main/runtime/orchestration/dispatch-assignee-orca-session-id.ts
  • src/main/runtime/orchestration/dispatch-preamble-identity.ts
  • src/main/runtime/orchestration/dispatch-preamble-turn.ts
  • src/main/runtime/orchestration/mailbox-delivery-target.ts
  • src/main/runtime/orchestration/orchestration-party.ts
  • src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts
  • src/main/runtime/orchestration/structured-mailbox-pointer-delivery.ts
  • src/main/runtime/orchestration/structured-session-lineage.ts
  • src/main/runtime/orchestration/structured-session-mail-address.ts
  • src/main/runtime/orchestration/structured-session-mail-target.ts
  • src/main/runtime/orchestration/structured-worker-group-addressing.ts
  • src/main/runtime/rpc/methods/orchestration-structured-worker-lifecycle.ts
  • src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-start-mode.ts
  • src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts
  • src/main/runtime/rpc/methods/orchestration/federation/federated-fleet-snapshot.ts
  • src/main/runtime/rpc/methods/orchestration/federation/federated-worker-show.ts
  • src/main/runtime/rpc/methods/orchestration/messaging/send-group.ts
  • src/main/runtime/rpc/methods/orchestration/messaging/session-recipient.ts
  • src/main/runtime/rpc/methods/orchestration/runs/dispatch-methods.ts
  • src/main/runtime/rpc/methods/orchestration/worker/agent-status-producer-census.test.ts
  • src/main/runtime/rpc/methods/orchestration/worker/deliver-worker-dispatch-preamble.ts
  • src/main/runtime/rpc/methods/orchestration/worker/explicit-worker-terminal-validation.ts
  • src/main/runtime/rpc/methods/orchestration/worker/fleet-execution-host-verdict.ts
  • src/main/runtime/rpc/methods/orchestration/worker/fleet-status-observed-identity.test.ts
  • src/main/runtime/rpc/methods/orchestration/worker/fleet-status-terminal-identity.test.ts
  • src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts
  • src/main/runtime/rpc/methods/orchestration/worker/worker-control.ts
  • src/main/runtime/rpc/methods/orchestration/worker/worker-list-method.ts
  • src/main/runtime/rpc/methods/orchestration/worker/worker-list-projection.ts
  • src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts
  • src/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.ts
  • src/main/runtime/rpc/orchestration-assignee-parity.test.ts
  • src/main/runtime/rpc/orchestration-chat-assignee.test.ts
  • src/main/runtime/rpc/orchestration-party-addressing.test.ts
  • src/main/runtime/rpc/orchestration-session-caller.test.ts
  • src/main/runtime/structured-agent-session-close.ts
  • src/main/runtime/structured-chat-coordinator-mail.test.ts
  • src/main/runtime/structured-chat-real-host-test-fixture.ts
  • src/main/runtime/structured-session-worktree-teardown.ts
  • src/main/runtime/structured-worker-authority.test.ts
  • src/main/runtime/structured-worker-authority.ts
  • src/main/runtime/structured-worker-cleared-session.test.ts
  • src/main/runtime/structured-worker-identity.ts
  • src/main/runtime/structured-worker-terminal-read.ts
  • src/shared/orchestration-session-caller-codes.ts
  • src/shared/structured-agent-session-agent-status.ts
  • tests/e2e/structured-native-chat-routing-authority.unit.test.ts
💤 Files with no reviewable changes (2)
  • src/main/runtime/rpc/orchestration-session-caller.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment on lines +137 to +156
export async function projectFleetWorkerPage(
runtime: OrcaRuntimeService,
db: OrchestrationDb,
dispatchId: string
): Promise<Awaited<ReturnType<typeof projectWorkerFleet>> | null> {
const rows = db.listWorkerTerminalResources({ dispatchIds: [dispatchId], limit: 1 })
if (rows.length === 0) {
return null
}
const now = Date.now()
return projectWorkerFleet({
db,
agentStatus: (handle) => runtime.getAgentStatusForHandle(handle),
rows,
attentionFacts: db.getWorkerAttentionFactsForDispatches([dispatchId], now),
statuses: runtime.getOrchestrationFleetAgentStatusSnapshot(),
limit: 1,
now
})
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Install the session host before projectFleetWorkerPage projects a structured row.

worker-list calls ensureSessionHostForStructuredRows before projectWorkerFleet. projectFleetWorkerPage does not. This helper serves worker-show, worker-read, the context-only show path, and the federated show path. After a runtime restart, the session host may not be installed yet. In that state, observeStructuredAssignee reads no host, so the worker-show projection can disagree with the worker-list projection for the same structured worker. inspectWorkerTerminal installs the host only in its chat branch. Its minted structworker_ branch does not install it. Call ensureSessionHostForStructuredRows inside this helper so that every caller reads the same host.

Proposed fix
   if (rows.length === 0) {
     return null
   }
+  await ensureSessionHostForStructuredRows(runtime, rows)
   const now = Date.now()

Import ensureSessionHostForStructuredRows from ./fleet-execution-host-verdict.

Comment on lines +109 to +110
const sessionId = structuredWorkerSessionId(identity)
const record = readStructuredAgentSessionRecord(sessionId)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP -C3 '\bresolveStructuredWorkerAuthority\s*\(|\bobserveStructuredAssignee\s*\(|\blistAddressableStructuredWorkers\s*\(' src/main --type=ts -g '!**/*.test.ts'
rg -nP -C8 'function applyStructuredSessionVerdicts' src/main --type=ts

Repository: stablyai/orca

Length of output: 16209


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- changed file ---'
cat -n src/main/runtime/structured-worker-authority.ts | sed -n '1,230p'
printf '%s\n' '--- diff for changed file ---'
git diff 02790c53e8836ea1c28dda1ea400aa25a2dd5067 1ed44998dbc224ed8a7ff9c2af6ec9e2a632fd39 -- src/main/runtime/structured-worker-authority.ts
printf '%s\n' '--- executingSessionId and record helpers ---'
rg -n -C8 '\b(executingSessionId|readStructuredAgentSessionRecord|structuredWorkerSessionId)\b' src/main --type=ts
printf '%s\n' '--- listed callers and enclosing flows ---'
cat -n src/main/runtime/orchestration/structured-worker-group-addressing.ts | sed -n '1,130p'
cat -n src/main/runtime/rpc/methods/orchestration/worker/worker-list-projection.ts | sed -n '88,125p'
cat -n src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts | sed -n '60,100p'
cat -n src/main/runtime/rpc/methods/orchestration/messaging/send-group.ts | sed -n '135,180p'

Repository: stablyai/orca

Length of output: 42212


🏁 Script executed:

#!/bin/bash
set -e
cat -n src/main/runtime/structured-worker-authority.ts | sed -n '1,230p'
printf '%s\n' '--- bound helpers ---'
rg -n -C8 '\b(executingSessionId|readStructuredAgentSessionRecord|structuredWorkerSessionId)\b' src/main --type=ts
printf '%s\n' '--- group/list flows ---'
cat -n src/main/runtime/orchestration/structured-worker-group-addressing.ts | sed -n '1,130p'
cat -n src/main/runtime/rpc/methods/orchestration/worker/worker-list-projection.ts | sed -n '88,125p'
cat -n src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts | sed -n '60,100p'
cat -n src/main/runtime/rpc/methods/orchestration/messaging/send-group.ts | sed -n '135,180p'

Repository: stablyai/orca

Length of output: 42070


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- lineage implementation ---'
rg -n -C18 'export function (executingSessionId|resolveExecutingSession)' src/main/runtime/orchestration/structured-session-lineage.ts
printf '%s\n' '--- direct mailbox target and delivery ---'
rg -n -C12 'resolveStructuredWorkerDirectMailboxTarget|deliverForHandle|resolveStructuredTarget' src/main --type=ts
printf '%s\n' '--- predicate callers ---'
rg -n -C8 'isLiveStructuredAgent|resolveTerminalIdentity|Boolean\(resolveStructuredWorkerAuthority' src/main --type=ts -g '!**/*.test.ts'

Repository: stablyai/orca

Length of output: 42304


Keep read-only structured-worker lookups non-throwing.

executingSessionId throws when the session record cannot be read or when another host owns the session. The call at resolveStructuredWorkerAuthority runs before readStructuredAgentSessionRecord can catch store errors. The same unguarded lookup runs in group enumeration and assignee observation. One affected row can abort worker-list, group fan-out, or direct mailbox resolution.

Catch lineage refusals at read-only boundaries. Preserve typed refusals for action paths.

Suggested fix
-  const sessionId = structuredWorkerSessionId(identity)
+  let sessionId: string
+  try {
+    sessionId = structuredWorkerSessionId(identity)
+  } catch {
+    return null
+  }
   const record = readStructuredAgentSessionRecord(sessionId)

Apply the same policy to listAddressableStructuredWorkers by skipping a row when session resolution fails, and to observeStructuredAssignee by returning null. observeStructuredWorker should return an unverifiable observation when its session cannot be resolved.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sessionId = structuredWorkerSessionId(identity)
const record = readStructuredAgentSessionRecord(sessionId)
let sessionId: string
try {
sessionId = structuredWorkerSessionId(identity)
} catch {
return null
}
const record = readStructuredAgentSessionRecord(sessionId)

@brennanb2025
brennanb2025 force-pushed the brennanb2025/d6-chat-worker branch from 1ed4499 to a6a9bb2 Compare October 2, 2026 01:43
Comment thread src/main/runtime/orchestration/dispatch-preamble-turn.ts Outdated
Comment on lines +105 to +109
function chatAssigneeSessionId(address: string): string | null {
const assignee = parseOrcaSessionAddress(address)
? resolveStructuredAssignee(address, null)
: null
return assignee?.kind === 'chat' ? assignee.sessionId : null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Unreachable chat aborts group send

If an active chat assignee’s /clear successor is recorded on another host, this identity lookup throws a host-boundary refusal. One such recipient aborts the entire @all, @claude, or @codex send before mail is written for reachable workers, rather than being skipped with a recipient warning.

@pullfrog pullfrog 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.

Important

One correctness gap in the new chat-preamble mechanism: after a process restart, an owed preamble whose send the journal already recorded as unknown is re-sent under a fresh operation id, so the chat can receive the dispatch instructions a second time. A second, narrower issue: settleDispatchPreambleTurn treats a row dropped because its Dispatch ended as "withdrawn". Details inline.

Reviewed changes

This run re-reviews the delta since the prior pullfrog review at 1ed44998db (8 commits, 27 files net).

  • The owed chat preamble is now its own row. dispatch_preamble_turns (owed/sending/in_doubt/delivered) replaces the host-minted messages row; dispatch-preamble-identity.ts and every NOT_OWED_DISPATCH_PREAMBLE_SQL mail filter are gone. The lane sends the row first, as its own body, under the operation ledger, and mail waits behind it.
  • A busy chat's start waits like a terminal's. settleDispatchPreambleTurn replaces waitForDispatchPreambleTurn: past the budget the turn is withdrawn and the start fails with nothing delivered, while an in-flight send is waited out and an unresolved one is reported in_doubt rather than failed.
  • A chat at rest reads live. observeChatAssignee promotes a released-lease chat to live while its tab is listed, and reports exited only once the tab is gone.
  • worker-list activity from the agent-status store. applyStructuredSessionVerdicts reads the session's activity from getOrchestrationFleetAgentStatusSnapshot by the session pane key instead of opening a journal.
  • Wording and CLI parity. A chat's own-dispatch and active-dispatch refusals name the Orca session ID rather than a terminal, and a structured session's preamble names this runtime's own CLI.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/runtime/orchestration/structured-mailbox-pointer-delivery.ts Outdated
Comment thread src/main/runtime/orchestration/dispatch-preamble-turn.ts Outdated
@brennanb2025
brennanb2025 force-pushed the brennanb2025/d6-chat-worker branch from a6a9bb2 to c9ce151 Compare October 2, 2026 03:21
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready for a human look (not merged)

The problem. A coordinator agent could hand a task to a terminal agent but not to an agent in a native chat: dispatch --to <chat> and worker-start --terminal <chat> were refused. Separately, on main, a native-chat worker that ran /clear was read, shown and stopped through its old, closed session. Live testing of this PR also found a third bug already on main: Orca treated a chat as idle for the few seconds between a user sending a message and the provider echoing it, so orchestration mail sent in that gap was merged into the user's turn and the user's own answer could be lost.

What changes for you. An agent in a native chat can now be a worker exactly like a terminal agent, named by its orca_session_id:<id>. It gets its task as a turn, reports with worker_done, and is listed, shown, read and stopped with the same commands; stopping or releasing it never closes your chat. A /clear'd worker is followed to the session that continues it. Mail to a chat now waits out a message you have just sent it. Full before/after table and behaviour changes are in the description.

Since the last review, the PR was rebased onto current main and reworked:

  • Rebased onto main and the session: → orca_session_id: rename (fix(orchestration): call the Orca session ID orca_session_id everywhere agents see it #24230). The worker-start wait cap and the dispatch capability check were dropped because main removed both on purpose; a chat worker's reports are accepted only from that chat (tested, including a forged report from another chat).
  • The task text a chat receives now lives in its own table (one row per task), sent by the existing mail-delivery path under an ID tied to the task, instead of being a hidden row in the mail table that every mail reader had to filter.
  • A busy chat's worker-start now behaves like a busy terminal's: it waits up to --timeout-ms for the chat to be free and attaches nothing until then; past the budget it fails with the real reason.
  • worker-list / worker-show take a chat worker's activity from the agent status store instead of opening every chat's history; one at-rest rule means a resting worker that is still held reads live, not exited with "recover".
  • The shared "can this chat take a turn now" check counts a sent-but-unechoed message as busy (fixes the main bug above for mail too).
  • The /clear lookup fix is its own first commit and could land alone.

Fixed during review (all with tests that fail without the fix): a resting chat worker reading exited with a "recover" hint (P1); a task never delivered behind an unacknowledged check; a failed busy start leaving the chat fenced from its own mail; a worker-start that ignored --timeout-ms and gave false text; per-row history reads in worker-list; two /clear lookup gaps (keep-alive and release archive); a minted worker regressing to "recover" after every restart; the task being merged into the user's running turn (P1, found live); three regressions in the first version of that fix; wording that called a chat a "Terminal"; dev builds naming the wrong CLI.

Deferred (not blocking):

  • A worker Orca started whose turn an app restart cut off reads live, and nothing re-sends that turn (on main worker-list already suggested no action).
  • A chat waiting on your answer to a question waits out --timeout-ms rather than failing at once like a terminal's startup prompt (intended: the question is yours to answer).
  • Pre-existing on main, worth follow-ups: plain orca orchestration check, which the "you have new messages" notice tells agents to run, prints no message bodies, so an agent can miss what a peer asked (seen live 4 times); /clear is refused while a chat runs check --wait.
  • dispatch --dry-run to an unreachable chat is refused; a chat on another host is refused with mail wording; chats in WSL/SSH workspaces cannot be workers yet.

Verified:

  • 3 review rounds plus 2 narrow rounds, an architecture review, and the readiness checklist (pass, no P0–P2 left).
  • tc:node passes at the final commit b7d6b53ab5a; tc:cli passed at c9ce1514591 (no CLI change since). 4,331 orchestration/RPC tests pass; all lint and code-quality checks are clean.
  • CI on b7d6b53ab5a: all 22 checks pass (17 skipped by path).
  • Live QA on Windows (8 rounds, Codex driving hidden dev builds; Claude chats in the app). On the final commit: a busy-chat start in the first seconds after a user message failed correctly 2 of 2 with nothing delivered, and mail waited out the user's turn. Screenshots are in the description.

Not verified: macOS live (the Mac test machine was unreachable), Linux, SSH remotes, a Codex chat as the worker, a packaged build, the 30-minute idle rest end to end. The stopped-chat, /clear and stop/release cases ran live on an earlier commit of this PR, not the final one.

Your calls: whether to split the /clear fix (first commit) into its own PR, and whether the two worker-show/worker-stop behaviour changes for workers Orca starts at rest are what you want.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Status update: two restart fixes after the bot reviews (408e4a8d796)

Triaging the automated review comments found one real bug and one small inconsistency, both now fixed with tests that fail on the previous commit:

  • A chat could get its task twice after an Orca restart. If Orca restarted while a chat's task send was unconfirmed, the task went out again under a new ID; a Codex chat always hit this. The task's send ID now lives on its own row and survives a restart, so the unconfirmed send is replayed under the same ID and never starts a second task. Main's rule that a later turn gets a new ID is unchanged.
  • The first worker-show after a restart contradicted itself for a worker Orca started (unverifiable next to live). The session host is now set up before either kind of worker is read.

The other bot comments were already fixed by later commits or did not hold up against the code.

Checks: tc:node, 4,333 orchestration/RPC tests, and all lint checks pass locally on 408e4a8d796. In CI, static analysis (and verify, which reports it) fails on three anti-slop errors in tests/e2e/helpers/terminal-pty-write-spy.unit.test.ts. That file came from main in #24556 and is not touched by this PR; CI lints this branch merged with current main, so any PR fails that step until main fixes it. The same lint is clean on this PR's own commit.

Still not merged; the open calls in the comment above (splitting out the /clear fix, and the at-rest worker-show/worker-stop changes for workers Orca starts) are unchanged.

@pullfrog pullfrog 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.

✅ No new issues found. This delta fixes both previously open threads, and the two open risks tracked in prior reviews are now retired.

Reviewed changes

This run re-reviews the single commit since the prior pullfrog review at b7d6b53 (4 files).

  • An in-doubt preamble replays under its own durable id across a restart. resolveStructuredPointerOperation now passes mintedByThisProcess: args.ledger !== undefined || stored?.operation_id === args.sentByThisProcess. A preamble always supplies a ledger (its id lives on the dispatch_preamble_turns row), so after a restart a journal-recorded unknown resolves to reuse and the host replays that verdict rather than minting a fresh instruction turn. A later accepted turn still re-mints, matching the mail path's rule. The shared decideStructuredPointerAttempt contract is unchanged — only the caller's input — so the direct unit tests still pin the mail semantics.
  • worker-show installs the session host before it reads. inspectWorkerTerminal now calls ensureStructuredAgentSessionHost() once at the top for any session-address handle (isStructuredSessionAddress), before both the minted-worker and chat branches. A first read after a restart can no longer return unverifiable from the observation while the fleet projection (which installs the host itself) reports live.
  • Tests. A new restart case in structured-mailbox-pointer-delivery.test.ts drives a fresh lane over the same DB and asserts the send replays under the first operation id (it would go red under the old logic). A new case in structured-worker-cleared-session.test.ts nulls the host ref and asserts inspectWorkerTerminal and projectFleetWorker agree on live.

Both prior open threads (the restart double-delivery in structured-mailbox-pointer-delivery.ts, and the worker-show liveness divergence in worker-observation.ts) are addressed by this commit and have been resolved.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…source

A chat worker's task is held in the chat's queue like any agent message, so
the card needs to say which Dispatch it is from: the sender, run, task and
Dispatch ids. An older build reads an unknown kind as the person's card and
sends it as written.
… agent

`dispatch --to orca_session_id:<id>` and `worker-start --terminal
orca_session_id:<id>` now accept a chat on this host instead of refusing it.

- The chat is refused only where mail to it would be: unknown, a provider id,
  another host, or closed. A chat can't be its own coordinator's worker.
- Its task goes through sendAgentTurn as a queued send, as mail notices do:
  an idle chat starts a turn, a busy one holds a card naming the Dispatch.
  The operation id is derived from the Dispatch, so a resend replays.
- worker-start reads that outcome as the terminal path reads its write; a
  card held behind a running turn is handed over with its start unobserved.
- The Dispatch names the chat by its /clear root, with no pane or process,
  and records its Orca session id, so its own sub-dispatches nest under it.
- Mail to dispatch:<id> reaches the chat; worker-show/list/read read the
  chat's session records; stop and abandon never close the chat; closing the
  chat fails its Dispatch as closing a terminal does.
- A dispatch preamble for a structured session names the CLI the structured
  mail lane names.
… /clear

At rest is live, a closed chat has exited, and another host or nothing to
read is unverifiable. A /clear keeps the chat's Dispatch, and the session
that continues the chat reports as it.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/d6-chat-worker branch from e7f5efa to f581b74 Compare October 6, 2026 03:51
- dispatch --inject is a keepalive-backed wait, so a chat that takes a while
  to accept its task no longer reads as a dead runtime at the 30 s idle cut.
- An injected task whose delivery is unknown keeps its Dispatch open, as an
  unknown worker-start does, instead of failing it while the chat may run it.
- A busy chat's worker-start receipt says the task waits as a card in the
  chat's queue, and that worker-abandon does not remove that card.
- A close that puts the chat's tab back no longer fails its Dispatch: the
  closed-chat settle runs after the close's outcome, not at its hide.
- worker-start adopting a chat says it gave the task to the chat, instead of
  claiming it started a terminal agent; the mode value is unchanged.
- Test: a chat whose agent has not taken its task reads outcome_unknown.
…close settles

The rollback suite pinned the hide's arguments; it now expects the deferred
notice, and that the notice is sent once, after a rolled-back tab is back.
…ifiable, not exited

A /clear successor this host has no record of is missing evidence, not proof
the chat is gone. Only a closed chat reads exited, so only a closed chat
settles its Dispatch; a lost successor reads unverifiable with that reason.
- A chat's close notice settles only that chat's Dispatch. A scan of every
  chat Dispatch read another chat whose close was still in flight (its tab
  hidden, maybe to be put back) as closed and failed its Dispatch.
- Only a dispatch --inject into a chat is a long poll; a terminal inject
  writes and returns, and keeps its short-RPC slot.
- Receipt wording: worker-start says it gave the task to the chat, and a
  queued task is sent when the chat's queue reaches it.
Conflict in structured-agent-session-runtime.ts: kept the host's
onSessionTabHidden dep beside main's new agents registry argument to
modelCatalogHostDeps.
- worker-start's mode sentence for a chat states the placement, which is
  true whether the task is delivered, queued or refused.
- Comments and a test title no longer claim a close notice re-derives every
  chat worker, or that a queued task waits for the current turn.
Brings main's max-lines fix for protocol-version.ts (#25839).
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status (rebuilt branch, head df9df23)

The problem. A coordinator agent could hand a task to a terminal agent but not to an existing native chat. dispatch --to orca_session_id:<id> was refused, and worker-start --terminal orca_session_id:<id> failed with a misleading "stale terminal" error.

What changes for you. A native chat can now be a worker. Its task arrives the way your own message does: an idle chat starts a turn, and a busy chat holds the task as a normal queued card after your turn. The chat reports with worker_done like a terminal worker, and the coordinator lists, shows, reads, stops and abandons it with the same commands. Stop and abandon never close your chat. Closing the chat fails its task, as closing a terminal worker does.

What happened in review. This branch replaces the earlier version of this PR with a fresh build on current main. The earlier version had its own task store, delivery lane and readiness check. The task now goes through the chat's existing send and queue, so there is no second "is the chat free?" rule to drift from the queue's own.

  • Fixed in review:
    • dispatch --inject into a chat that had to start up could show "Restart Orca" though the task was delivered. The connection is now kept alive for chat targets only.
    • A delivered task whose acknowledgement never came marked the Dispatch failed. It now stays open.
    • A close that was rolled back (for example a refused worktree delete), or another chat closing at the same time, could fail a live chat's task. Only a proven close of that chat fails it now.
    • A chat whose /clear successor this machine has no record of read as exited. It now reads unverifiable.
    • A terminal --inject briefly took a long-poll slot.
    • Receipt sentences that were false for a chat ("Started a terminal agent worker…", "no agent was started").
    • Two test gaps.
  • Deferred:
    • A chat worker's working/blocked activity in worker-list (it reads unknown; workers Orca starts in a native chat have the same gap).
    • Receipt fields that still say "terminal" for a chat.
    • A coordinator on a paired remote client keeps its own 60-second timeout without keepalives (same gap as terminal.send with an agent prompt).
    • Guide text telling agents a chat can be a worker.
    • terminal read taking any chat's ID.
    • A /clear'd worker that Orca started being followed to its continuing session. That is a separate fix.
  • The readiness check on the final diff found no P0, P1 or P2 issues.

What was verified.

  • CI is green on df9df23.
  • Locally, 125 targeted test files (1070 tests) pass. They run the real chat host, queue, RPC server and database with only the agent process faked. Each key behaviour's test fails with its production line reverted.
  • Live QA on a separate Mac (3effc5f, Claude native chats, hidden window) passed every scenario:
    • an idle chat takes the task;
    • a busy chat keeps the person's own answer and runs the task as its next turn;
    • Stop pauses the task card, and the person can delete it;
    • --inject into a resting chat works;
    • stop and abandon leave the chat running and usable;
    • closing the chat fails its task;
    • four refusals;
    • a terminal worker still works.

What was not verified. SSH, WSL and paired hosts (covered by tests only), Windows and Linux, a restart while a task card waits, worker-read and /clear on a chat worker live (tests only), and closing a tab by clicking it (the store's close action was used).

Open for a product decision before this is marked ready:

  • The task shows in the chat as a message bubble on your side holding the whole worker instructions, and the waiting card starts "You are working inside Orca…". Nothing yet says which coordinator or task sent it. Mail notices look the same today; showing the sender is the planned follow-up.
  • Those instructions still tell the agent "Do not exit the shell. Your terminal stays available…". That text is unchanged from main, which already sends it to native-chat workers Orca starts.
  • A waiting task card has the normal Steer button, so one click folds the task into your running turn.
  • Deleting a waiting task card leaves the task open until the coordinator abandons it, as clearing typed input does for a terminal worker.

…message sender and the /clear'd worker

- The task's sender rides on the message body (body.from), as every agent
  message does now: a 'task' kind with the coordinator named by the shared
  sender builder. The minted worker's preamble carries it too.
- The minted worker's observation reads the session running it (its /clear
  successor), as worker-show did on main.
- A chat assignee's worker-read reads its whole /clear lineage, as a
  worker's does; mail reach's unverifiable lineage reads unverifiable.
Conflicts in the structured host install: kept the closed-chat settle hook
beside main's new attention delivery.
…start's own ids

The minted worker's preamble names who its task is from with the run, task
and Dispatch ids worker-start already holds, so it reads no Dispatch row.
A chat worker's coordinator mail lands in its Dispatch's mailbox, but a
chat's idle edge re-derived the Dispatch mailbox only for a party with a
terminal handle. A pointer lost with the provider then waited for new mail
after a restart or a /clear. The re-derivation now looks the Dispatch up by
the party's address, which names a worker by its handle and a chat by its
root.
…s no mail

Opening an agent message's sender looked up the mail it carried, which only a
mail notice has; with the task kind in the union that read no longer typed.
A task's coordinator is found by its address alone.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Status update: integrated with the sender label and the /clear fix (head 05a439c)

The open product question from the last status comment was how the task looks in the chat. The plan was to land the change that shows who an agent's message is from first. That change and the fix that follows a /clear'd worker to its continuing session have both merged, and this PR is now built on them.

What changes for you since the last update

  • The task in the chat is labelled with who sent it: "Message from " on the turn, and "From " on a waiting card. Clicking the name opens the coordinator. A native-chat worker that Orca starts itself gets the same label.
  • If a chat worker's agent stopped while coordinator mail was being pointed at it, that mail is now pointed at again on the chat's next idle moment, including after a /clear.

Fixed in this round

  • Held coordinator mail for a chat worker was never pointed at again after a /clear. The cause was in this PR.
  • A type error from adding the task label kind to the sender-opening code. The web typecheck now passes.
  • A /clear'd worker Orca started now reads its running session in worker-show, matching the merged /clear fix.

Decided

  • The Steer button on a waiting task card stays.
  • The worker instructions' terminal wording ("Do not exit the shell…") moves to a separate small follow-up.

Still deferred

  • A chat coordinator is named by its tab title. That is the general sender-naming rule, which mail notices share.
  • The full worker instructions are still shown in the labelled task turn, and the waiting card's preview starts with them.
  • The other items listed in the PR body's Notes.

Verified

  • CI on this head.
  • Targeted suites locally: chat, mail, sender, /clear and close.
  • The node, CLI and web typechecks.
  • A re-QA on a separate Mac at 8e88b50, before the last two small fixes, with screenshots in the body. It covered an idle chat, a busy chat (the card says "From qa-coord", and your own reply stays intact), dispatch --inject from a terminal coordinator, and a terminal-worker regression check.

Not verified

  • The mobile typecheck and tests ran only in CI, not locally. This PR changes no mobile file.
  • SSH, WSL and paired hosts, Windows and Linux, and a restart while a task card waits.

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