Main agent actions on linked work
- Tests:
src/core/dispatch/orchestratorSourceFold.test.ts
For status, steer, and stop, the main agent addresses one existing Ship unit by the durable (main thread, act id) link. The requester's resolved identity and current thread are trusted context; model text cannot pick another task or requester. Those linked-work actions do not start a worker or expose its private brief; typed start is a separate path. The shared task-link and observation schemas are independent of the store implementations so Workers can consume them without host filesystem dependencies.
- Code:
src/core/coordinator/mainTaskLink.ts,src/core/coordinator/mainWorkObservation.ts,src/core/coordinator/unitSeedReceipt.ts,src/core/coordinator/mainActions.ts,src/core/coordinator/instanceStore.ts,src/core/coordinator/contract.ts,src/core/planeService.ts,src/core/authz/policy.ts,src/tools/mainWork.ts,src/tools/toolsets.ts,src/tools/runnableTool.ts,src/tools/commands.ts,src/core/dispatch/conversationCommand.ts,src/core/dispatch/orchestratorDecisions.ts,src/core/dispatch/commandRun.ts,src/core/commandConfirmations.ts,src/core/dispatcher.ts,src/core/dispatch/runLoop.ts,src/core/dispatch/run.ts,src/core/resumeLaunch.ts,src/core/runLedger/types.ts,src/agents/registry.ts,deploy/cloudflare-memory/worker.ts - Tests:
src/core/dispatch/conversationCommand.test.ts,src/core/dispatch/orchestratorDecisions.test.ts,src/core/runRecord.test.ts,src/core/coordinator/mainWorkObservation.test.ts,src/core/coordinator/unitSeedReceipt.test.ts,src/core/coordinator/mainActions.test.ts,src/core/authz/policy.test.ts,src/tools/mainWork.test.ts,src/tools/toolsets.test.ts,src/core/dispatch/run.test.ts,src/core/dispatcher.test.ts,src/core/dispatch/runLoop.test.ts,src/core/resumeLaunch.test.ts,src/agents/registry.test.ts,deploy/cloudflare-memory/runLedger.test.ts
| Criterion | Proof |
|---|---|
The orchestrator's existing private work_start, work_steer and work_stop calls share the command decision writer. The owned run saves the conversation, requester, delivered source message, tool name, and hashes of the target, input and context before starting or changing work. A failed acknowledgement holds the effect. The source must remain current through asynchronous preparation and immediately before the first mutation. A newer delivered turn or withdrawn authority at that boundary prevents the old call from acting. An admitted mutation keeps the existing completion rules. These selections share the command count and byte limits. Selection does not prove permission, delivery or effect completion. External live replies, unit-thread replies and explicit steer commands still require migration. | [unit] src/core/dispatcher.test.ts::dispatch::uses the configured pilot repository for a later main-run fix…, ::records commands and linked-worker changes in one original conversation history; src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::holds a worker start when a newer requester turn arrives during the decision save; src/tools/mainWork.test.ts::main work tools::holds…, ::binds…, ::a newer source during the real plane's final lookup leaves the original work running; src/core/dispatch/orchestratorDecisions.test.ts::orchestrator decision records::uses one history and identity space for commands and worker decisions; [gap] Full source migration remains in the shared message handling plan. |
| A requester-bound orchestrator command uses the shared action context and registry binder without a second routing model. Before execution or a confirmation offer, its owned run state records the conversation, requester, exact source-message ID and text hash, command arguments hash and source-context hash. Missing storage, acknowledgement failure, owner mismatch, malformed retained decisions or capacity exhaustion hold the command. Source access is checked again after the acknowledgement and at execution. This selection record is not proof of authorization, command completion or safe replay of an external effect. Command history retains its original orchestrator link after completion. The initial run limit is 256 decisions and 256 KiB of decision metadata. | [unit] src/core/dispatch/conversationCommand.test.ts::conversation command receipts::saves the orchestrator and source decision before applying a command, ::holds a command when its decision cannot be saved, ::rechecks source access after the decision acknowledgement, ::preserves command arguments named reason and intent, ::preserves raw parameters for the registry's schema transformations, ::refuses an unknown option before it can be hidden by a positional argument; src/core/dispatch/orchestratorDecisions.test.ts::orchestrator decision records::*; src/core/dispatcher.test.ts::dispatch::records the main command owner and source before the native mutation, ::holds main command effects without tracked decision storage; src/core/runRecord.test.ts::retains the command's orchestrator and source link after record serialization |
| A main conversation can select a registered command through its typed tool schema. Trusted code binds the latest requester turn, invokes the existing command registry, and keeps the existing authorization and confirmation rules. Resetting personal runtime settings completes in the same conversation regardless of the legacy operator routing setting. | [unit] src/core/dispatcher.test.ts::dispatch::resets personal settings through a main conversation command tool… |
| A main conversation can select a registered command through its typed tool schema. Trusted code binds the latest requester turn, invokes the existing command registry, and keeps the existing authorization and confirmation rules. Resetting personal runtime settings completes in the same conversation regardless of the legacy operator routing setting. Verified browser and delegated personal MCP conversations use the same original actor/thread/source tuple and fresh destination gate; a Slack-only verifier is never assumed. | [unit] src/core/dispatcher.test.ts::dispatch::resets personal settings through a main conversation command tool…; ::executes a private %s conversation command with its original requester and withholds reusable command data |
Each registered command invocation in one requester turn retains its own native result. Distinct calls to the same command do not share a persistence identity; exact replay of one call retains its original identity. Initial and deferred results remain bound to that invocation. Saved approvals retain their existing confirmation:<offer-id> identities and one-pending-offer-per-thread rule. | [unit] src/core/dispatcher.test.ts::dispatch::persists both same-command results from one requester turn; src/core/dispatch/conversationCommand.test.ts::conversation command receipts::* |
| A main command that requires confirmation offers it without running; only the requester can confirm it. A revoked private audience cancels the offer. Browser and delegated MCP commands also recheck the same original private destination after asynchronous command admission; loss of proof leaves settings unchanged. An earlier turn cannot authorize a new command, and confirming a channel reset cannot grant channel write access. | [unit] src/core/dispatcher.test.ts::dispatch::offers a channel reset and executes it only after its requester confirms; ::keeps channel settings when the conversation requester lacks config write access; ::refuses a conversation command copied from an earlier requester turn; ::cancels a conversation command offer when its private audience changes; ::executes a private %s conversation command with its original requester and withholds reusable command data |
| The main agent discovers a command's schema on demand and submits named parameters to the registered handler. The native result reaches the requester directly. Its model receives only invocation, submission, refusal, or approval-pending metadata, so command data without a durable source-read receipt does not enter its reusable model log. A deferred submission does not prove effect completion. | [unit] src/core/dispatcher.test.ts::dispatch::delivers command data to the requester without putting it in the model's reusable log |
| A deferred command posts its initial native result and lets the main agent answer while settlement remains pending. Settlement posts through the existing deferred-result path under provenance and audience checks, even after a newer verified requester follow-up. | [unit] src/core/dispatcher.test.ts::dispatch::answers the main conversation while a submitted command is still settling; ::delivers an admitted command completion after a newer requester follow-up |
| Requester access is checked again immediately before the registered handler after asynchronous command admission. Revocation during approval-class resolution leaves settings unchanged. | [unit] src/core/dispatcher.test.ts::dispatch::keeps settings unchanged when requester access changes during command admission |
| A nested inline command records its own outcome without replacing the main run's trace or channel ownership. | [unit] src/core/dispatcher.test.ts::dispatch::keeps the main run trace and channel ownership after an inline command |
Requester status reads a sanitized briefProof from the durable unit after a fresh direct-audience check. It names the original act and unit, schema, cause state, evidence counts/availability and acceptance presence; completeness means structural validation only. Legacy and missing briefs, or corrupt brief contents with intact identity, report unknown completeness, and unavailable storage or audience verification returns unavailable. No source body, URL, query, result, cause text or acceptance detail is projected. Source provenance stays unverified; source details require a future adapter-backed fresh access check. The narrow status read validates the surrounding unit while retaining its brief as unknown data; normal unit reads and effects keep strict brief decoding. Shared diagnostics retain their existing redaction. | [unit] src/core/coordinator/mainActions.test.ts::main task actions::projects durable typed proof without source bodies or acceptance text; ::marks legacy missing and corrupt briefs unknown without regenerating evidence; ::classifies a corrupt brief through the HTTP store without relaxing ordinary unit decoding; ::withholds proof when the fresh audience check is missing fails or is revoked; src/tools/mainWork.test.ts::main work tools::a channel flip during status read withholds private facts |
| Status reads the exact linked unit and its unchanged claimed branch for the requester in the current main thread. It returns a small status, branch and pull request projection without the brief, private thread or report. An independently verified seed acknowledgment may name its child run and coding or review role. Missing, stale, foreign and cross-thread links reveal no task facts. | [unit] src/core/coordinator/mainActions.test.ts::main task actions::status projects only the requester's linked unit without private context; ::a foreign or cross-thread caller cannot read the link; ::a changed unit branch revokes status, steer, and stop for the claimed task |
| Steering writes a bounded event under a stable action id to that unit and nudges its Workflow. An orchestrator acting for the bound requester writes the requester's sender id so an idle unit recognizes the wake. Repeating the same action appends no second event; reusing its id with different words refuses. A failed nudge leaves the event queued for the coordinator's next read. A stopped, ended, recovered, or mismatched unit receives no event; the durable append rechecks the exact claimed task and active state atomically with the event write, including a racing stop or branch change, and returns the stored event for bounded replay verification. A later whole-row update cannot rewrite a claimed brief. | [unit] src/core/coordinator/mainActions.test.ts::main task actions::steer persists one event and a replay cannot change its words; ::a main-agent steer wakes as the bound requester; ::a failed nudge leaves the event queued and offers reconciliation to its original owner; ::terminal units refuse a steer and a later brief rewrite cannot change the binding; ::a mismatched stored brief refuses a steer before writing an event; ::a stop racing the append leaves no steer on a terminal unit; ::a branch change racing the append leaves no steer on the former claimed unit; ::a persisted steer is nudged without reading the unit's full event history; [worker] deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's unit events (record 0051's reply-as-event rule)::guards a main-task steer against a stop in the same append transaction; ::atomically refuses main-task steer and stop after the claimed branch changes |
| A requester can stop their own linked work without an operator grant. The narrow policy row checks self identity, and the stop mark atomically rechecks the claimed task before ending any run. The model tool forwards that binding through its plane wrapper, including when the branch changes after lookup. Its answer reports a partial stop when the runner mark or any required run stop failed. A stop targets only child runs tagged with that instance, even when the main-agent run shares a one-unit thread. A foreign, view-as or non-person caller changes nothing. | [unit] src/core/coordinator/mainActions.test.ts::main task actions::stop preserves exact ownership and reports the plane outcome; ::a branch change racing the stop leaves the former claimed instance running; ::a failed child stop reports partial even when the runner mark succeeded; src/tools/mainWork.test.ts::main work tools::a branch change during the work_stop tool cannot stop the former claimed unit; src/core/planeService.test.ts::plane stop — the runner_stop move (record 0064; issue 1924)::stops only children of the linked instance when a one-unit plan shares the main thread; src/core/authz/policy.test.ts::main task stop authorization::admits only the requester without an operator grant; src/core/planeService.test.ts::plane stop — the runner_stop move (record 0064; issue 1924)::terminates the instance and ends its live children in one move: the stop mark first, the hosted parent hard-stopped, then every live child in a unit thread — never the parent twice |
The orchestrator's work_status, work_steer, and work_stop tools bind the current resolved requester and thread before reading or changing linked work. Model input may name only the stable act id and steer words; a provider call id combined with its run id makes one steer replay idempotent. Missing capabilities, invalid input, unknown work, and partial stop have actionable answers. The linked-work tools and their results can enter the run log, so the ledger claim and model call expose them only when an adapter-issued direct-audience stamp passes a fresh check for an unshared Slack IM. A Slack Connect D-channel, shared/private channel, group DM, web chat, relay, failed recheck, or uncertain audience omits the capability or returns a named refusal. Ledger promotion keeps the relaying app, bound credential, and direct-audience stamp for restart checks. A resumed run withholds linked-work actions because its consumed follow-up sender cannot be reconstructed reliably; the requester can regain control with a fresh verified direct-DM request. The toolset has no generic run start or spawn tool. Registered command writes use the requester-bound registry path above. | [unit] src/tools/mainWork.test.ts::main work tools::*; src/tools/toolsets.test.ts::the toolset table::orchestrator: source reads and private linked-work controls; ::a shared conversation's model tool list omits linked-work calls while a private one retains them; src/core/dispatch/run.test.ts::claimRun — the ledger claim once the prompt exists::persists linked-work tool definitions only for a direct requester Slack DM; ::does not record linked-work tools when the direct audience fails a fresh claim-time check; ::keeps the relaying app and bound credential identities on the promoted row for restart; ::keeps a verified direct audience stamp on the promoted row for safe rechecks after restart; src/core/resumeLaunch.test.ts::the pure pieces::resumeMessage…; src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::offers linked-work tools to the model only in a direct requester Slack DM; src/agents/registry.test.ts::orchestrator agent (record 0070 — the plane's chat preset)::its instructions call linked-work tools for status, steer and stop without asking for commands; [unit] src/channels/slack/directAudience.test.ts::Slack direct audience verification::accepts only a fresh unshared DM with this requester; src/channels/slack.test.ts::SlackIO direct audience::revalidates its exact requester DM before private output. |
| A live DM run withdraws its linked-work authority before it passes a follow-up from another sender, an app, a bound credential, or another run to the model. Revocation closes admission to new steer and stop effects immediately, waits for any already admitted effect to settle, then passes the follow-up. It stays withdrawn for the rest of that run, including a tool call, audience check, unit lookup, or plane-service lookup already in flight. Steer and stop recheck live authority immediately before their mutation. A later direct requester message may start a new authorized run. The plane's recognized provider-reissue control is not a requester message and does not withdraw authority. | [unit] src/tools/mainWork.test.ts::main work tools::refuses linked-work reads and effects during an untrusted follow-up turn; ::a deferred steer cannot cross a relayed follow-up; ::a delayed unit lookup cannot steer after authority is revoked; ::a delayed unit lookup cannot stop after authority is revoked; ::a delayed plane lookup cannot stop after authority is revoked; ::closes new effects before waiting for an in-flight effect to settle; ::a relayed follow-up closes the gate while a durable steer is in flight; src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::withdraws linked-work authority before a relayed follow-up reaches the model; ::a delayed direct-audience check cannot restore a revoked run; ::waits for an admitted stop before a revoked private follow-up reaches the model; ::a plane provider-reissue control does not withdraw linked-work authority |
| A work status read verifies the direct audience again after the store read, before any private fields return to the model. A channel that becomes shared during the read returns a named refusal. Final Slack publication has its own audience guard. | [unit] src/tools/mainWork.test.ts::main work tools::a channel flip during status read withholds private facts; [unit] src/core/dispatch/reply.test.ts::deliverAnswer — the answer reaches the thread::rechecks a stamped private run without a Slack context binding before its reply. |
| The main agent starts and binds one private worker from a typed, requester-authorized brief. Live delivery of later steering to its coding child and progress relay to the main thread remain pending. | [unit] Typed start and durable binding: main-agent-start.md; [gap] Private child live delivery and main-thread progress relay still need integration proof. This service queues input for the existing coordinator fold. |
| Current reads bind the canonical unit and state digest to exact call/result bytes and recheck state after awaited lookups; an immutable historical status is distinct from a current observation. | [unit] src/core/coordinator/mainWorkObservation.test.ts::current work observations::* |
| Seed receipts validate exact binding and saved bytes and keep coding/review roles distinct; acknowledgements do not prove provider execution. | [unit] src/core/coordinator/unitSeedReceipt.test.ts::unit seed receipt schema::* |
| Criterion | Proof |
|---|---|
| A failed Workflow nudge retains the admitted steer and offers reconciliation for the original unit through the existing durable outbox. | [unit] src/core/coordinator/mainActions.test.ts::*::a failed nudge leaves the event queued and offers reconciliation to its original owner |
| A private current-history command does not read or change an earlier requester target. It still records its current owner/source decision, rechecks source access and delivers only native result metadata; ordinary commands retain requester checkpoint behavior. | [unit] src/core/dispatch/conversationCommand.test.ts::conversation command receipts::keeps private command preparation off retained requester checkpoints while ordinary commands still update them |
Recorded source-evidence folds
The existing orchestrator decision history accepts a version 2 source-fold record for source evidence or source facts. It keeps the original orchestrator owner, the source receipt, a run or unit target hash, and input/context hashes. It cannot carry requester identity, command fields or worker controls from the evidence. Version 1 requester commands and worker selections retain their current format. Both kinds use the same identity space, serialized writer, storage acknowledgment and count/byte limits.
A source receipt and target hash are references, not authentication or permission. A future caller must read the canonical source and original decision, verify the target and current context/access, and qualify the internal delivery before use. This change enables no caller, routing, delivery, consumption or cleanup. Older readers hold the new variant; compatible readers must precede any writer.
| Criterion | Proof |
|---|---|
| Source evidence and source facts keep their type and exact run/unit target reference through recovery. Identical replay uses the existing record; changed context or target conflicts. | [unit] src/core/dispatch/orchestratorSourceFold.test.ts::orchestrator source fold decisions::retains source evidence and its exact destination reference through history recovery; ::keeps requester commands and source folds in one immutable identity space; src/core/runRecord.test.ts::full records validate source folds across JSON without requester conversion |
| Foreign owners, malformed receipts, unsupported schemas and injected requester/action fields stay held. | [unit] src/core/dispatch/orchestratorSourceFold.test.ts::orchestrator source fold decisions::holds malformed references and injected requester or action fields |
| An unknown save prevents later admissions; stored input retains its original receipt across the awaited write. Existing requester decisions remain readable. | [unit] src/core/dispatch/orchestratorSourceFold.test.ts::orchestrator source fold decisions::keeps an unknown save held and captures the original receipt before awaiting storage; src/core/dispatch/orchestratorDecisions.test.ts::orchestrator decision records::* |
| Canonical source/context/access checks authenticate an orchestrator decision before typed internal delivery and exact native consumption. | [gap] No source-fold caller, processor or producer is enabled by this record format. |