Repository navigation
feat(orchestration): a native chat can be a dispatch worker, like a terminal agent - #22972
Conversation
cad7778 to
486afc2
Compare
There was a problem hiding this comment.
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_dispatchableis gone;resolveDispatchAssigneePartynow returns a chat party, sodispatch --to session:<chat>andworker-start --terminal session:<chat>are accepted. Rows carryassignee_handle=session:<root>,assignee_orca_session_id=<root>, null pane/process, and the preamble is owed as adispatch-type message in the Dispatch mailbox rather than typed into a pane. - Session-proven authority.
verifyDispatchCapabilitygained a host-verified, canonicalizedorcaSessionIdbranch, andassertChatAssigneeReachablereuses the mail reach rules (other-host / closed / unknown / worker-identity-lost) on both write paths. /clearlineage. NewexecutingSessionId/lineageHeadSessionIdroute structured-worker authority, observation, worker-read/stop/archive, agent status andterminal readthrough the live successor;observeStructuredWorkeris split intoobserveStructuredSession.- Caller cap.
settleWithinCallerCapreturns theoutcome_unknownreceipt inside a native-session caller's shell-tool limit whilefinish()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.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ 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.tsand applied to local structured rows too, soworker-listand worker-show project a chat's or structured worker's liveness from the session host instead ofunverifiable / missing_status;worker-listinstalls the lazy session host when a structured row is present. - Settled Dispatch no longer leaks its preamble.
routeUnreadDispatchMailboxToRunMailboxnow excludesdispatch-type rows, so a Dispatch that settles while its owed preamble is still unread cannot hand that preamble to the coordinator. - One
--terminalreceipt.decideWorkerStartModereturns a singlemode: "reused"receipt (reused_terminal, mode-neutral detail) for a terminal handle and asession:address alike, and the host probe/downgrade is skipped for it. - Remote caller cap.
worker-start --onfrom a session caller returns theoutcome_unknownreceipt inside the caller's shell-tool cap while the attach continues (settleWithinCallerCap,federatedInProgressReceipt). - Shared session predicate.
isStructuredSessionAddressandobserveStructuredAssigneecollapse the "minted worker or chat" checks used by the runtime and worker observation.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
40ba689 to
95eb26b
Compare
There was a problem hiding this comment.
✅ No new issues found. The two prior findings are addressed by the new commits; the earlier
worker-showliveness 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--typeor 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 anddispatch-type-overload findings. - Typed lineage failure contract.
resolveExecutingSessionreturnshere/other-host/unrecorded;executingSessionIdthrows the typed host-boundary refusal for another host, and an unreadable record store throwssession_caller_not_liverather than falling back to the pre-clear session.structuredWorkerSessionIdno longer swallows. - Durable
startingreceipt. A session caller's cappedworker-start(local and--on) returnsstate: startingwithnextCommands: [worker-show], nofailedStage/lastError; the CLI exits 0 onstartingand 1 onoutcome_unknown/failed. - Fleet activity.
worker-listand worker-show now passagentStatusintoprojectWorkerFleet, so a live structured or chat row carries its activity fromgetAgentStatusForHandle. - Shared wording and typed select. Coordinator-self/other-worktree refusals share one wording for a terminal and a chat;
selectMessageRowsis the one typed select overmessages. - Twin-run parity test.
orchestration-assignee-parity.test.tsdiffs the agent-visible results of one coordinator script against a terminal and a chat assignee.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
486afc2 to
106af5e
Compare
95eb26b to
5197f00
Compare
|
| 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 |
There was a problem hiding this comment.
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.
| const address = | ||
| db.getWorkerDispatch(dispatchId)?.agent_terminal_handle ?? | ||
| db.getDispatchContextById(dispatchId)?.assignee_handle | ||
| const chat = parseOrcaSessionAddress(address) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
ℹ️ No new issues in this incremental delta. The earlier
worker-showliveness thread remains open and was not touched by these commits.
Reviewed changes
- Narrowed lineage failure contract.
resolveExecutingSessionnow converts onlystore.getRecordfailures into the typedsession_caller_not_liverefusal, so a bug in the walk surfaces as itself rather than a false not-live verdict. - Async session-read adaptation.
projectWorkerFleetandapplyStructuredSessionVerdictsawait the now-asyncgetAgentStatusForHandle;projectFleetWorker(Page)moved intoworker-list-projection.tsand every caller (worker-control,federated-worker-show,worker-list,worker-observation) awaits it. The PR's test session hosts and the--terminalreusedreceipt expectation were updated to match.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
106af5e to
079aa5d
Compare
5197f00 to
56562b4
Compare
There was a problem hiding this comment.
ℹ️ No new issues in this incremental delta. It is a test-only commit; the earlier
worker-showliveness thread remains open and was not touched by it.
Reviewed changes
- Provider-death hold regression test.
orchestration-chat-assignee.test.tsnow 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(theAgentJournalSnapshotfield the pointer gate reads), andagent-facing-parity.test.ts'sreadGateFactssupplieslatestSendHoldsMail: false. No production code changed.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
079aa5d to
7300d9f
Compare
56562b4 to
1b6be5a
Compare
There was a problem hiding this comment.
ℹ️ 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.tsnow fakes the host's operation ledger (a recordedclientOperationIdreplays 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.tsloses blank lines and inlines the capability extraction and thecheckcall. - Dropped a stale import.
structured-mailbox-pointer-delivery.test.tsremoves the unusedformatMessagePointerimport and reformats twosetSubmissionscalls.
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.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
1b6be5a to
7ece5ed
Compare
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (8)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review. 📝 WalkthroughWalkthroughThe 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 Priority: ➖ Normal Merge Risk: 🔵 Low · up to 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 ReviewSecurity architecture risk: 🟡 Moderate · up to 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
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
ℹ️ No new issues in this delta. It is a rebase onto current
mainplus a test-only fixture extraction; the previously flaggedworker-observationliveness-read note remains open and is unchanged by these commits.
Reviewed changes
- Rebased onto current
main. The branch was retargeted frombrennanb2025/d5-self-addresstomainand the 13 feature commits squashed into 2 on02790c53e8. Two main changes forced production adaptation: the dispatch capability is gone (no moredcap_/capability_hash;prepareStartingWorkerAuthorityreturnsvoid, preamble delivery no longer takes a capability, andfailed-start-dispatch-identity.tsguards onprocess_incarnation IS NULL), and the session address was renamedsession:→orca_session_id:with "orchestration address" → "Orca session ID" wording throughout. - Replaced the address-visibility helpers.
agentVisibleOrchestrationAddress/withAgentVisibleAddressesare gone;orcaSessionIdOrHandleinorchestration-party.tsis 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 tostructured-chat-real-host-test-fixture.tsunchanged, 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).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ No new issues in this test-only commit. It strengthens the existing chat-assignee rejection coverage; the previously flagged
worker-observationliveness 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 thesender_not_assigneecode and thereceived handle orca_session_id:<id>reason, and that the Dispatch staysdispatched. - 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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: c2d9830c-2bd1-4cfc-b7aa-af96c6fb614c
📒 Files selected for processing (61)
src/cli/handlers/orchestration/worker-list-session-address-row.test.tssrc/main/runtime/orca-runtime-get-pty-record-for-pane-key.tssrc/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.tssrc/main/runtime/orca-runtime-resolve-terminal-pane.tssrc/main/runtime/orca-runtime-stop-requested-pty-ids.tssrc/main/runtime/orchestration/agent-facing-parity.test.tssrc/main/runtime/orchestration/db/dispatch-context/dispatch-context-store.tssrc/main/runtime/orchestration/db/messages/direct-mailbox-routing.tssrc/main/runtime/orchestration/db/messages/message-inbox.tssrc/main/runtime/orchestration/db/messages/role-mailbox-delivery.tssrc/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-authority.tssrc/main/runtime/orchestration/db/worker-terminal/failed-start-dispatch-identity.tssrc/main/runtime/orchestration/db/worker-terminal/worker-terminal-resource-store.tssrc/main/runtime/orchestration/dispatch-assignee-orca-session-id.tssrc/main/runtime/orchestration/dispatch-preamble-identity.tssrc/main/runtime/orchestration/dispatch-preamble-turn.tssrc/main/runtime/orchestration/mailbox-delivery-target.tssrc/main/runtime/orchestration/orchestration-party.tssrc/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.tssrc/main/runtime/orchestration/structured-mailbox-pointer-delivery.tssrc/main/runtime/orchestration/structured-session-lineage.tssrc/main/runtime/orchestration/structured-session-mail-address.tssrc/main/runtime/orchestration/structured-session-mail-target.tssrc/main/runtime/orchestration/structured-worker-group-addressing.tssrc/main/runtime/rpc/methods/orchestration-structured-worker-lifecycle.tssrc/main/runtime/rpc/methods/orchestration-worker-start-mode.test.tssrc/main/runtime/rpc/methods/orchestration-worker-start-mode.tssrc/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.tssrc/main/runtime/rpc/methods/orchestration/federation/federated-fleet-snapshot.tssrc/main/runtime/rpc/methods/orchestration/federation/federated-worker-show.tssrc/main/runtime/rpc/methods/orchestration/messaging/send-group.tssrc/main/runtime/rpc/methods/orchestration/messaging/session-recipient.tssrc/main/runtime/rpc/methods/orchestration/runs/dispatch-methods.tssrc/main/runtime/rpc/methods/orchestration/worker/agent-status-producer-census.test.tssrc/main/runtime/rpc/methods/orchestration/worker/deliver-worker-dispatch-preamble.tssrc/main/runtime/rpc/methods/orchestration/worker/explicit-worker-terminal-validation.tssrc/main/runtime/rpc/methods/orchestration/worker/fleet-execution-host-verdict.tssrc/main/runtime/rpc/methods/orchestration/worker/fleet-status-observed-identity.test.tssrc/main/runtime/rpc/methods/orchestration/worker/fleet-status-terminal-identity.test.tssrc/main/runtime/rpc/methods/orchestration/worker/local-worker-start.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-control.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-list-method.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-list-projection.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-observation.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.tssrc/main/runtime/rpc/orchestration-assignee-parity.test.tssrc/main/runtime/rpc/orchestration-chat-assignee.test.tssrc/main/runtime/rpc/orchestration-party-addressing.test.tssrc/main/runtime/rpc/orchestration-session-caller.test.tssrc/main/runtime/structured-agent-session-close.tssrc/main/runtime/structured-chat-coordinator-mail.test.tssrc/main/runtime/structured-chat-real-host-test-fixture.tssrc/main/runtime/structured-session-worktree-teardown.tssrc/main/runtime/structured-worker-authority.test.tssrc/main/runtime/structured-worker-authority.tssrc/main/runtime/structured-worker-cleared-session.test.tssrc/main/runtime/structured-worker-identity.tssrc/main/runtime/structured-worker-terminal-read.tssrc/shared/orchestration-session-caller-codes.tssrc/shared/structured-agent-session-agent-status.tstests/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.
| 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 | ||
| }) | ||
| } |
There was a problem hiding this comment.
🎯 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.
| const sessionId = structuredWorkerSessionId(identity) | ||
| const record = readStructuredAgentSessionRecord(sessionId) |
There was a problem hiding this comment.
🩺 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=tsRepository: 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.
| const sessionId = structuredWorkerSessionId(identity) | |
| const record = readStructuredAgentSessionRecord(sessionId) | |
| let sessionId: string | |
| try { | |
| sessionId = structuredWorkerSessionId(identity) | |
| } catch { | |
| return null | |
| } | |
| const record = readStructuredAgentSessionRecord(sessionId) |
1ed4499 to
a6a9bb2
Compare
| function chatAssigneeSessionId(address: string): string | null { | ||
| const assignee = parseOrcaSessionAddress(address) | ||
| ? resolveStructuredAssignee(address, null) | ||
| : null | ||
| return assignee?.kind === 'chat' ? assignee.sessionId : null |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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-mintedmessagesrow;dispatch-preamble-identity.tsand everyNOT_OWED_DISPATCH_PREAMBLE_SQLmail 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.
settleDispatchPreambleTurnreplaceswaitForDispatchPreambleTurn: 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 reportedin_doubtrather than failed. - A chat at rest reads live.
observeChatAssigneepromotes a released-lease chat tolivewhile its tab is listed, and reportsexitedonly once the tab is gone. worker-listactivity from the agent-status store.applyStructuredSessionVerdictsreads the session's activity fromgetOrchestrationFleetAgentStatusSnapshotby 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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
a6a9bb2 to
c9ce151
Compare
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: What changes for you. An agent in a native chat can now be a worker exactly like a terminal agent, named by its Since the last review, the PR was rebased onto current main and reworked:
Fixed during review (all with tests that fail without the fix): a resting chat worker reading Deferred (not blocking):
Verified:
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, Your calls: whether to split the |
Status update: two restart fixes after the bot reviews (
|
There was a problem hiding this comment.
✅ 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.
resolveStructuredPointerOperationnow passesmintedByThisProcess: args.ledger !== undefined || stored?.operation_id === args.sentByThisProcess. A preamble always supplies a ledger (its id lives on thedispatch_preamble_turnsrow), so after a restart a journal-recordedunknownresolves toreuseand 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 shareddecideStructuredPointerAttemptcontract 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.
inspectWorkerTerminalnow callsensureStructuredAgentSessionHost()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 returnunverifiablefrom the observation while the fleet projection (which installs the host itself) reportslive. - Tests. A new restart case in
structured-mailbox-pointer-delivery.test.tsdrives 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 instructured-worker-cleared-session.test.tsnulls the host ref and assertsinspectWorkerTerminalandprojectFleetWorkeragree onlive.
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.
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.
e7f5efa to
f581b74
Compare
- 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).
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. 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 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.
What was verified.
What was not verified. SSH, WSL and paired hosts (covered by tests only), Windows and Linux, a restart while a task card waits, Open for a product decision before this is marked ready:
|
…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.
Status update: integrated with the sender label and the
|

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>answeredsession_chat_not_dispatchable("a chat can't receive a dispatch yet"), andworker-start --terminal orca_session_id:<id>failed with a misleadingterminal_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 sameworker_donecommand 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
dispatch --to orca_session_id:<id> --inject, orworker-start --terminal orca_session_id:<id>)worker_done, like a terminal workerworker-startanswersreadyworker-startanswersoutcome_unknownand says the task is waiting as a queued card, the same answer a terminal start gives when it can't see the agent beginworker-list/worker-showon the chat workerlivewhile the chat is open (also when it is resting and its next message would start it),exitedonce you close it,unverifiablewhen this machine can't tell (for example, a chat on another host)worker-read/clearworker-stop/worker-abandon--terminaldoesdispatch:<id>)What Changed
sendAgentTurnwith 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 newtaskkind: 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.worker-startreads the send's outcome with existing states only. Accepted meansready. Queued or not yet started meansoutcome_unknown; the task has been handed over and is not resent. Refused or rejected meansfailed. 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.exited; missing evidence readsunverifiable, neverexited.--inject; each is refused before anything is created. A chat that already has an active task is refused too.dispatchrefuses it up front;worker-startfails 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 --injectto 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--injectis unchanged./clearcontinued it in, as for a terminal worker.orca-devin a dev build), instead of alwaysorca.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
worker-startwait for it to go idle. The queue already protects your turn, and the task card is visible and deletable.worker_donefor the ended task is refused. A terminal handed a task with--injectwhile busy behaves the same way.terminal.sendwith an agent prompt has the same gap today.Compatibility
taskkind 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.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
The task turn names its coordinator
A task from a chat coordinator, named by that chat's title; clicking the name opens it
dispatch --injectfrom a terminal coordinatorEarlier 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
The coordinator chat gets a
readyreceipt and the worker'sworker_doneThe chat is busy with the person's own
sleep 60; the task waits as a queued cardThe person's turn finished with its own answer; the task then ran as its own turn
The person pressed Stop: the queue pauses with Resume, and the task card is not sent
The person deleted the task card
dispatch --injectinto a chat at rest: the task arrives with no connection errorThe coordinator's mail reaches the chat worker as a "you have new messages" turn
After
worker-stopthe chat keeps running its command; nothing is closed or interruptedAfter abandon the chat is still usable; its late
worker_doneis refusedClosing the worker chat (its Dispatch fails as closed by operator)
Regression check: a terminal worker still starts and reports as before
Testing
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.--onrefusals;/clearsuccessor reporting as the chat;worker_doneadmitted and two forged senders refused;/clear, and opening a task's sender./clearand close suites). The node, CLI and web typechecks pass, and oxlint is clean. CI is green.Notes
Deferred, not in this PR:
/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 readtaking any chat's ID.worker-list(it readsunknown; workers Orca starts in a native chat have the same gap).