A ship pipeline is a live run for its whole life, so every channel that can open a thread runs it and every surface is a projection of one record
The ask. Decide (the maintainer, before the plan is written): a ship pipeline's parent run stays live from the hand-off to the pipeline's end and carries everything the runner says as events; the preflight admits any channel whose request handle can open a thread; the web adapter gains a handle rebuilt from a thread key. Reject a message store for the web. Written for an engineer who knows the dispatcher, the run ledger and the plan runner and has not read the web-chat record. Success criteria:
- A person types an
agent:shiptask in a web conversation and watches the pipeline there, with the same rounds, card content and unit threads a Slack thread gets, refreshing the page at any point without losing a line. - Slack changes in no visible way.
- HTTP
/ingressand the MCP tool still refuse ship, with a reason that is true. - No new kind of stored thing: the web stores nothing of its own and a conversation remains the runs of one thread.
TL;DR
The web chat refuses agent:ship with "this adapter is single-shot", which is false: a web run outlives its request, takes steers and reads history, and the check is a channel-name allowlist written before the browser became a channel. The real gap is that the runner speaks into threads at four moments with no run live to record it (a unit thread's lead, the card redraw, a unit's ending report, the plan summary), which Slack absorbs because Slack is also a message store and the web cannot because a conversation is the runs of its thread. The bet: the pipeline's parent run, which today ends the moment the runner takes the hand-off and is rewritten at the end, stays live for the whole pipeline, hosted by whichever bot generation is up with the heartbeat every run already has, so those posts become events on a run and the web renders them as it renders every run. It costs a hosted marker at the run's birth, a host key on the ledger so the parent never occupies the thread its children need, a host-only write path whose refusal the runner's driver learns one release earlier, hand-off and reclaim rules for a row with no process, and a web handle rebuilt from a thread key. Doing nothing leaves ship Slack-only and leaves every web conversation with a pipeline turn that says "handed off" for two hours and then rewrites itself.
Amended 2026-09-18, while proposed, after the reconciliation read against record 0057. Three points. (1) Vocabulary: 0057 fixes operator as the model turn that binds a chat input, so the person who stops an orphaned pipeline is the maintainer here and in the plan; nothing in this record's design names an operator. (2) The host claim is released by every exit that hands nothing off: a request refused after the claim, including a preflight question that ends a plan bind (0057's unit nine), finishes its run as today and leaves no host-keyed row, so a question never locks a thread out of ship until a reclaim. (3) Under 0057 the agent:ship line becomes a plan bind and the host claim moves to the bind's executor; the live receipts this record's plan types as agent:ship are rerun once 0057 deletes the directive syntax. Sequencing agreed with 0057: this record's units one to three land first; 0057's unit nine follows this record's units three and five. 0057 takes its own dated amendment in return: occupancy is owned by this record's host key, a unit's report is this record's ship_unit event projected by the thread session, and working sessions register by the run's metadata thread key.
Today at 78a2ef9e
The delta from what a reader of the specs expects; the survey is in the appendix.
- The ship channel rule is a name check.
shipPreflightrefuses any channel id not startingslack:orcli:and calls the rest "single-shot". The spawn path refuses the live request's handle by capability: noopenThread,spawn_unsupported. The HTTP, MCP and web request handles all lack it today. - The parent run ends at the hand-off and is rewritten at the end. The
agent:shiprequest's run finishescompletedonce the runner has taken it; the runner'sfinishroute later writes a record under the same id from the instance and its unit rows (parentRunRecord). The parent is claimed on the ledger without a seed, and run-history item 39 excludes a ship pipeline from the hand-off by name, so a deploy tombstones it. - Thread occupancy has four readers, and admission is the only one that releases early.
ThreadAdmissionholds the in-process slot until the dispatch returns. The ledger'slive_runstable hasthread_key UNIQUE, and a run whose claim is refused runs untracked: no session log, no durable inbox, no hand-off. The coordinator'sspawnroute lists active runs by thread key and answersbusy. The dispatcher'sthreadsElsewheremap, rebuilt each sweep from the ledger's rows, refuses a coordinator spawn and steers a person's message into an inbox. A live parent in the requesting thread trips the last three. - The runner rebuilds a channel handle from a bare thread key, and only Slack answers.
threadIoForknowsslack:, handshttp:andmcp:the null channel, and returns nothing forweb:. The runner asks for this handle in five places. - Writes are fenced and the driver's retry set is closed. A ledger
appendfrom a non-owning generation is refused. The runner's driver re-asks a step only for four named refusals whose body carriesat, twelve times two minutes apart, and throws on any otherok: false. The driver is bundled into the bot's own Worker script beside the container class, so the two deploy together in one release.
The shape
A ship pipeline becomes a run like every other run: born at the hand-off, live for its whole life, finished by the runner's finish. It has no process of its own. The bot hosts it: the generation that took the hand-off keeps the registry row and the ledger claim it already made, heartbeats it as it heartbeats every run, and appends the runner's posts to it as events. The parent is marked hosted at birth. On the ledger its row is claimed under a host key, the thread key with a #host suffix, while the row's metadata names the thread itself: the ledger's own occupancy checks read the key column and never see the parent, everything that lists, files or renders a run reads the metadata and finds it under its conversation, and the two in-process readers that decide occupancy from a view skip the marker. At SIGTERM the hosting generation hands the row off like a resumable run; the next generation's reclaim re-hosts it instead of closing it. The card in Slack and the turn on the web are then two renderings of one record. The channel rule becomes what the spawn path already applies to a request: a channel runs ship when the handle its request arrived on can open a thread. The web gets that handle, and a second one rebuilt from a key for the runner.
The closest known shape is a Temporal parent workflow whose history holds each child's completion, the UI a projection of that history; the one difference is that our parent has no worker, so its liveness is a heartbeat the bot pays on its behalf and hands to the next generation at a deploy.
One trace: a two-unit plan from a web conversation, refreshed mid-round, the bot redeployed once
The requester's session is access:s; the conversation is web:s:c9. The bot is at generation A.
- The person sends
agent:ship in <owner>/<repo>: plan <path> units Ua Ub. The ship branch creates runr1in the registry withmeta.hosted: trueand its label, claims the ledger underweb:s:c9#hostwith the same metadata, and calls the preflight with the request handle's capability (canOpenThread: true); the channel check passes. Had another pipeline been hosted in this thread, the claim would answerthread-liveon the host key and the branch would refuse by name: one pipeline per thread. - The hand-off writes instance
i7withrunId: r1and asks the shim, then sets the row's statehosting: i7and publishesrun_meta { agent: ship, instanceId: i7 }, the fact the unit listing keys on. The dispatch returns and the admission slot is released as today; the branch's finish,finishingand seal are skipped. The web answers202withr1's view path; the page mounts the turn with a live token, and because the turn carrieshostedthe composer readssend. - The runner's
unit-startfor Ua asks A for the handle ofweb:s:c9. The web handle is rebuilt from the key alone (the session sub is in the key; the actor is the row'suserId). ItsopenThread(lead)mints conversationc9-u1under the same sub and returns the key and a bound handle. A hostsr1, so it publishesship_unit { unit: Ua, state: started, threadKey: web:s:c9-u1, lead }; the write-through mirrors it to the ledger in seq order. - The coding child for Ua dispatches into
web:s:c9-u1as a coordinator child, carrying the instance tag as today and noparentRunId. Its ledger claim is onweb:s:c9-u1, no collision; a one-unit task's child would claimweb:s:c9while the parent holdsweb:s:c9#host, no collision either. - A release lands. A receives SIGTERM. The hosted row counts as resumable, so A marks it
handoff, the drain does not wait on it, and the abandonment pass writes no tombstone for it. - B boots and reclaims. The reclaim is one ledger transaction that moves every taken row's
ownerGento B; a generation racing it finds the row already B's with a live lease and skips it.r1has no transcript, so today's rule would close it "no transcript stored". The new rule runs first: a row whosestate.hostingis set isrehost. B recreates the registry row withr1's id, metadata, label and the ledger's events as a replay (a fresh token comes with it), adopts the ledger row, subscribes the write-through and starts the heartbeat. Nothing about it enters the elsewhere map:rehostis its own outcome kind, and a foreign hosted row's key column reads#host, which no message's thread can match. - The person refreshes
/threads/c9. The seed listsr1live with B's token; the turn paintsr1's events,ship_unit Ua startedwith its thread link among them. The composer still readssend. - The review child approves Ua and the runner's
unit-endlands on B. B hostsr1, so it publishesship_unit { unit: Ua, state: merge_ready, report }. The route also replies the report into the unit thread, for Slack's sake; on the web that reply resolves and is logged undeliverable, as the null channel's is. - The person opens
/threads/c9-u1. Its runs carry the instance tagi7, so the seed reads the instance'srunId,r1, and listsr1'sship_unitevents that nameweb:s:c9-u1, drawn as turns of the parent linked tor1: the report sits where Slack's message would. - Had
unit-endlanded on a generation that does not hostr1(a rollout overlap), it would answer409 { ok: false, error: "not_host", at }. The driver countsnot_hostamong its passing conditions and asks again under its policy, twelve tries two minutes apart, a 24-minute window against a re-host gap of at most 60 seconds. - Ub runs the same way.
finishlands on B: B publishes theanswer(the plan summary), finishes the registry row and writes the one record through the ledger,ship_roundandship_unitevents in seq order, its thread the metadata's. NoparentRunRecordis assembled.
The property: the pipeline's every visible word is an event on a run that was live the whole time and occupied no thread, so no surface needs a store of its own and no refresh or redeploy loses a line or strips a child of its ledger row.
The difficulty map
- The hosted parent (section below): the host key against the occupancy readers and the column readers that must switch to metadata, the driver's passing condition a release ahead, hand-off, reclaim and stop. A reader this record missed is where it breaks.
- The web handle and the sibling conversation (section below): a handle from a key, a conversation nobody has sent into yet, the parent's word drawn in a child's conversation.
- The channel rule in the preflight (one paragraph).
- Spec rows across six living specs and one record note, and a new run event variant (most work, lowest risk).
The hosted parent run
Constraint. A run's liveness has two meanings today and they coincide at the end of every dispatch: the admission slot, released when the dispatch returns, and the registry row with its ledger claim, heartbeat every 10 s, lease 30 s, reclaimed by the next generation when the lease lapses or the phase is handoff. The parent must be live in the second sense for one to two hours (a pipeline runs 27 minutes to two hours in this month's worklog; a coding child's ask is 90 minutes) while occupying no thread, because a one-unit task's children dispatch into the requesting thread itself (record 0055 item 3), and the ledger enforces one live run per thread with a unique index: a child refused there is not told busy, it runs untracked. The parent has no process, no seed and no transcript, so the hand-off skips it by rule, the drain waits on it for 15 minutes and tombstones it, and the reclaim closes it. The ledger fences writes to the owning generation, and the driver kills an instance on any refusal it does not know.
Design. Eight changes, each in code that exists.
- The marker and the host key. The ship branch creates the registry row with
meta.hosted: trueand the label, and claims the ledger row under the host key with the same metadata (the branch calls the ledger'sopenitself, before any reservation, so the ledger key and the message's thread key part there and nowhere else).hostedandlabelare threaded through the registry's meta and summary, the ledger's meta and both view builders, so the hosting generation's own row and a foreign row both carry them. One helper mints and recognises the suffix and enforces the ledger's 256-character key cap; nothing else knows it. After the hand-off the branch setsstate.hosting: <instanceId>and publishesrun_meta { instanceId }.openlearns to answer with the same discriminated shapereservealready uses (tracked, fenced, untracked with its reason), so the branch refuses the request by name onthread-liveand still hands off untracked on an outage, as today. - The readers. On the ledger, the claim and the elsewhere map read the key column and miss the parent by construction. Four readers that file or address a run read the column today and switch to the metadata: the reclaim's record assembly, the interrupted-run notice (the key is copied at the reclaim's
closedoutcome), the resume launcher's message and its handle.liveOnThreadin the coordinator's spawn route reads views, so it filters!r.hosted. The web's turn seed carrieshosted; the page keeps the turn live and excludes hosted turns from the composer's mode and stop control.ThreadAdmissionneeds nothing. - The hand-off and the drain. The write-through's tracked run learns
hostedfromopenand reportsresumablefor it; the hand-off marking, the drain count and the abandonment pass all read that property today, so they follow. Run-history item 39's "never a ship pipeline" goes. - The reclaim. Before the transcript rule, a row with
state.hostingisrehost:registry.createwith the id, metadata, label and the events as a replay;adopt, the existing seedless take-over that starts the heartbeat; a fresh write-through subscription. One store read guards it: a run id the store already holds finished means the runner'sfinishlanded in the plain store because the ledger refused it, and the row isabandoned, the fenced delete-without-record the ledger already has. A row claimed under the host key whose hand-off never happened (a crash between steps 1 and 2 of the trace) carries nostate.hosting, falls to the transcript rule and closes "no transcript stored", filed under its conversation because the record assembly now reads the metadata. - The host-only write. The runner's
unit-start,round,unit-endandfinishroutes call one bot-sidehostPublish(runId, events): when this generation's registry holds the run,registry.publish, which assignsseqand is mirrored to the ledger; otherwise409 { ok: false, error: "not_host", at }.unit-startpublishesship_unitwith statestarted, the thread key and the lead;roundpublishes the round'sship_roundand aship_unitstate;unit-endpublishesship_unitwith the ending and the report;finishpublishes theanswer, finishes the registry row and seals the record through the ledger with the metadata's thread.parentRunRecordis deleted.drawCardkeeps redrawing the Slack card from the unit rows; the card and the events are two renderings of the same rows. - The driver, one release ahead. The driver's passing-condition set gains
not_host, one line. The driver ships inside the bot's Worker script, so this lands in a release before the bot starts answeringnot_host; the plan's first unit is that line alone. ship_unit. A new run event variant: unit, state (started, a round's outcome, an ending kind), thread key, lead or report, pull request when known. The run page's model draws it as a step with the report as detail; the friction analyzer's type filter learns the name.- Stop. A hosted run is not stoppable. The refusal sits in the registry's stop, which the run page's token route and
stopRunboth reach, and instopRun's ledger branch for a foreign row, soruns stop, the token route and the tokenless route all answer409 hostedpointing at the units; the run page's seed makesstopUrloptional and draws no control without it. A pipeline is stopped as it is today, by stopping the child in flight, whose ending the runner reads. A runner-side cancel is an open question below.
Invariants.
- A pipeline has exactly one parent record for its life, under the id the hand-off minted;
finishseals it and no second record is assembled. - A hosted run occupies no thread on any read: admission's slot, the ledger's claim, the coordinator's
liveOnThread, the dispatcher'sthreadsElsewhere, the web composer. - Every record, notice and rebuilt handle derives a run's thread from the row's metadata, never from the ledger's key column.
- The parent is live in exactly one generation's registry, or on the ledger under a dead generation for at most one lease plus one sweep (60 s), or in
handoffbetween the two. - Every runner post a Slack thread shows is an event on the parent; a write to it lands only through the hosting generation, and every other generation answers
not_host.
Failure modes. A fifth occupancy reader, or a fifth column reader, exists that this record missed: the plan's first tests are a one-unit task's child claiming its ledger row through the coordinator's spawn route with a hosted parent live, asserting the child is tracked, and a hosted row closed by the reclaim filing under the conversation. The store is unreadable at reclaim: the row is re-hosted anyway, and a finish that already landed is caught by the runner's retry, which finds a finished registry row and answers idempotently. The driver change misses its release: a rollout overlap during a pipeline kills the instance on the first not_host, which is why it ships alone and first. A hosted parent ever gains a seed (record 0057's session design): the ledger's session registration files the session under the claim key, so the session row and the record would disagree; the helper's metadata rule extends to that registration before any seeded hosted run exists.
The alternative it beat. The runner owns the parent's ledger row itself and no bot hosts it. Killed by the lease and the token: the runner's steps are minutes to ninety minutes apart while a child runs, the lease is 30 s, so a Workflow would have to wake every 10 s for two hours to keep a row the bot keeps for free; and a row nobody's registry holds has no token, so the web's turn paints frozen.
The web handle and the sibling conversation
Constraint. The runner asks for a handle with a thread key and a user id, no request, no session cookie. A web conversation is the runs of its thread, so a thread with no run yet has no row anywhere. A unit's ending report is posted into the unit's thread on Slack, where the web has no run to carry it. And a child may not name its parent run: lineage is mechanical on a thread's newest run, so a parentRunId on a coordinator child would make every person's reply in a unit thread a lineage child of the hosted parent and steer it into an inbox nothing reads.
Design. threadIoFor learns web:: the key carries the session sub, so the handle is a WebIO whose history reads the thread's runs as access:<sub>. Its openThread(lead) mints a conversation id under the same sub with the adapter's own generator and returns the key and a bound handle; the caller records the lead on the run it acts for, which for the runner is the ship_unit event. A rebuilt web handle's out-of-run reply resolves and logs with undeliverable set, as the null channel's does. Children keep carrying the instance tag and nothing more. The conversation seed gains one read, a new runs-service method over the instance store the service already holds for the unit page: when a conversation's runs carry an instance tag, the seed reads the instance's runId and lists that parent's ship_unit events that name this thread key, drawn as turns of the parent linked to its run. The sibling becomes a rail row when its first run registers, as any thread does; before that it is a link on the parent's turn.
Invariants. A sibling conversation is in the requester's own lane and readable by exactly the people who may read its runs. Opening a thread writes nothing to the run store. A web handle never posts a message of its own. A coordinator child carries no parentRunId.
Failure modes. The requester's session is revoked mid-pipeline: the handle still rebuilds (it needs the sub, not the session) and the children run as the person, which is what a Slack requester who logs out gets today. A spawn from a plain agent loses its lead text on the web: the child's own run carries the request, as on the CLI.
The channel rule in the preflight
The preflight takes a channel id and answers before the handle is consulted. It gains one input, canOpenThread, derived by the ship branch from the request handle it already holds. HTTP and MCP request handles lack it and are refused with the spawn's reason, "the channel cannot open a thread of its own"; the rebuilt null channel's openThread is irrelevant because the check runs on the request. The refusal text stops calling the web single-shot. The CLI's handle has openThread and keeps passing.
Why not X
Why not leave the parent finished and let the runner append to the finished record? A finished record is written once and sealed; appending afterwards is the rewrite-at-the-end this record removes, spread over two hours, and a finished run has no token, so the web's turn paints frozen. Liveness is also the true state of a pipeline.
Why not store bot messages, so the web is a message log like Slack? Parity already holds for everything a run says, and the orphans are four posts that are facts about the pipeline. A message store would hold each run's answer twice, add a retention schedule, and reverse record 0043's one-store decision, which names it as the change to avoid.
Why not make each runner post its own tiny run in the thread? A card redraw is one message edited over two hours; as runs it is a turn per redraw and a card per redraw on Slack, the noise record 0055 removed. Each such run would also claim the thread on the ledger for its life, colliding with the child there.
Why not one conversation for the whole plan, no siblings? Record 0055 rejected one thread per plan on the session key and record 0057 removes that key; neither has decided the topology. This record keeps today's topology so the web matches Slack and takes no position on merging the threads.
Boundaries
Not here: gate refusals as inline records (record 0043's own fallback; the same refusal vanishes on refresh today and deserves its own unit), ship over HTTP or MCP (still refused; a job-shaped ingress stays agent-ship's listed gap), thread-per-unit versus thread-per-plan, record 0057's session design, and a runner-side cancel. Migration: none; a pipeline in flight at the release has a finished parent, as today, and its rows are untouched.
What would change our mind
- Assumption: the readers named in fact 3 and in the design are all of them. Test, before the hosted-parent unit: grep every reader of
live_runs.thread_key, everythreadKeyread off a ledger row, and everylistRunswithstatus: "active"or a thread-key compare; then the two first tests above. - Assumption: the run page's model draws
ship_unitevents as steps without a new view model. Test: a fixture parent with threeship_unitevents throughcreateRunPageModel. - Assumption: every runner route that writes tolerates
not_hostonce the driver knows it. Test: the driver suite with a409 not_hostanswer carryingaton each of the four routes. - Unknown: how many pipelines a generation hosts at once. Measured by
runs list --agent ship --status activeover a week; the heartbeat is one call per 10 s per hosted run, the cost every live coding run already pays. - Reversibility: the ship branch's finish,
finishingand seal come back as one block;rehostis one classification before the transcript rule; the web handle is onecaseinthreadIoFor;parentRunRecordreturns from history.
Rollout
Four pull requests in this order, the first in a release of its own: (0) the driver learns not_host; (1) the hosted parent (the marker, the host key and its helper, the four column readers switched, liveOnThread's filter, open's shape, the tracked run's hosted, rehost, hostPublish, ship_unit, finish sealing the run, stop refused in the registry) with the two tests first; (2) the web handle, openThread, the seed's parent read, the composer's hosted exclusion, the preflight's capability input; (3) the spec rows and the web-chat record's note. The live receipt is a two-unit plan from a web conversation with one bot deploy during round 0, refreshed after the deploy, with the unit's report visible in the unit conversation.
Open questions
| Question | Owner | Resolves it | Needed before |
|---|---|---|---|
| Should unit-start's thread choice key on unit count rather than "generated plan", so a person-provided one-unit plan runs in the asking thread? | the maintainer | one line in the plan's ask | the plan |
| Does a pipeline need a stop of its own (a runner cancel the hosting generation forwards), or is stopping the child in flight enough, as on Slack today? | the maintainer | a week of runs stop attempts on hosted rows answered 409 hosted | after PR 1 |
Validation criteria bind in the plan; every row would be [gap] here.
Spec rows that change with the code
- agent-ship: item 1 and its validation row (the channel rule by capability, the refusal's reason); item 16 (the hand-off keeps the run live and hosted, one pipeline per thread); item 17 (the unit listing reads the parent's events); the HTTP/MCP gap bullet under the roadmap.
- thread-admission: item 1 (a hosted run occupies no thread); item 6 (the web is the fourth channel that opens a thread).
- run-history: item 29 (one live run per thread: the host key and its helper); item 39 ("never a ship pipeline" goes); items 36 and 38 (
rehost; records file by metadata); item 33 (finishseals the hosted row); the record shape'sship_unitevent; item 41 (hostedandlabelon a foreign row). - http-ingress: item 9 (the driver's passing conditions gain
not_host). - live-view: item 17 (hosted rows on the index, no stop control,
stopUrloptional); item 28 (the unit page reads the parent's events, not a record assembled at finish). - web-chat: item 2 (the seed's parent read; the composer ignores a hosted turn); item 11 (the adapter gains a key-built handle and
openThread). - Record 0043 takes a dated note of the same; its model stands.
Appendix: the survey
| Fact | Proof at 78a2ef9e |
|---|---|
The ship channel check is a slack:/cli: prefix test with a "single-shot" reason; the preflight is called with the request handle in scope | src/core/ship/preflight.ts lines 154 to 166; src/core/dispatch/ship.ts lines 165 to 181 |
The spawn refuses the request's handle by capability when it lacks openThread | src/core/dispatch/spawn.ts lines 275 to 280 |
The hand-off run finishes completed, takes finishing and seals; its claim carries no seed; the ship fork runs before reserveRun and calls open itself; open answers undefined for fenced, route-missing, exhausted and thread-live alike, and both callers treat it as untracked | src/core/dispatch/ship.ts lines 287 to 296, 431 to 503; src/core/dispatcher.ts lines 707, 924; src/core/runLedger/writeThrough.ts lines 565 to 578, 966 to 976, 1004 to 1019; src/core/dispatch/run.ts lines 286 to 296 |
The runner's finish writes a record under instance.runId from the instance and unit rows, synthesizing run_meta { instanceId } | src/channels/adminCoordinator.ts lines 1553 to 1611 |
live_runs.thread_key is UNIQUE; the claim refuses thread-live; a refused reserve leaves the run untracked; parseClaim caps the key at 256 characters and stores meta verbatim | deploy/cloudflare-memory/worker.ts lines 1487, 1671 to 1696, 1737, 3605 to 3650; src/core/runLedger/decisions.ts lines 11 to 28; src/core/dispatch/provision.ts line 669; src/core/dispatch/run.ts line 294 |
Views take a run's thread from the metadata; the reclaim's record assembly, the reclaim's closed outcome (read by the interrupted notice), the resume message and the resume launcher's handle take it from the column; the ledger's session registration files by the claim key | src/core/runsService.ts lines 414 to 443, 446 to 472; src/core/dispatch/record.ts line 201; src/core/boot.ts line 235; src/index.ts lines 1244, 1266; src/core/resumeLaunch.ts line 65; deploy/cloudflare-memory/worker.ts lines 1680 to 1690 |
The coordinator's spawn route answers busy for any active run on the unit's thread, matching the view's thread; a task's unit thread is the requesting thread; children carry the instance tag and no parentRunId | src/channels/adminCoordinator.ts lines 379 to 387, 451 to 465, 492 to 502, 622 to 632, 1081 to 1087 |
threadsElsewhere is rebuilt each sweep from ledger rows keyed by the row's thread column; lineage is mechanical on a thread's newest run's parentRunId | src/index.ts line 1226; src/core/boot.ts lines 255 to 266; src/core/dispatch/lineage.ts lines 51 to 63, 116 |
Only resumable tracked runs are handed off; the drain count and the abandonment pass read the same property; the tracked run's constructor takes no meta | src/core/runLedger/writeThrough.ts lines 645 to 674, 1065 to 1069; src/index.ts line 1386; src/core/dispatch/record.ts lines 247 to 265 |
append is fenced to the owner; the driver reads any status with at as an answer, retries four named refusals 12 times 2 min apart inside step.do, and throws on any other; the driver is bundled into the bot's Worker script | src/core/runLedger/decisions.ts lines 54 to 57; src/core/coordinator/driver.ts lines 67, 121 to 153, 228, 380 to 391; deploy/cloudflare/worker.ts lines 60, 167; src/deploy/plan.ts lines 212 to 214 |
registry.create takes an id and a replay and mints a fresh token; publish assigns seq; RunMeta is set at create only; LiveRunMeta has no label and ledgerView omits it | src/core/runRegistry.ts lines 63 to 73, 207 to 247, 298 to 310; src/core/runLedger/types.ts lines 46 to 108 |
adopt takes over a reclaimed row without a seed and starts the heartbeat; the reclaim is one transaction per sweep; finish writes the record and deletes the live row in one transaction; land falls back to the plain store when the ledger refuses, leaving the live row; abandon deletes a row without a record | src/core/runLedger/writeThrough.ts lines 726 to 733, 1019 to 1032; deploy/cloudflare-memory/worker.ts lines 1888 to 1930, 1941 to 1984; src/core/runLedger/ledger.ts line 104 |
Three stop paths: the token route calls the registry directly; the tokenless route and runs stop hold a view and call stopRun, which reads none; the run page's stopUrl is a required seed field | src/channels/liveView.ts lines 706, 759, 803 to 812; src/core/runsService.ts lines 877 to 900; src/core/commands/runs.ts lines 300 to 301; src/channels/webSeed.ts line 102 |
The web composer derives its mode from the live turn client-side; the turn seed is the run view plus fields; the runs service holds the instance store and a child's view carries parentInstanceId | web/src/pages/HomePage.vue lines 64 to 93; src/channels/webSeed.ts line 200; src/core/runsService.ts lines 406, 435, 473 |
| A coding child's ask is 90 minutes; a pipeline lives 27 minutes to two hours | src/core/budgets.ts line 104; the month's worklog (outside the repository) |
Sources
Records 0043, 0055, 0057, 0019, 0031, 0034; specs agent-ship, thread-admission, run-history, live-view, web-chat, http-ingress.
Amended 2026-09-19: built and live; two web rows stay open on the tracker
All nine units of the plan are merged and running in production (the host key, the hosted parent and the stop rules since 1.252.0; the web handle, the count-keyed thread choice and the conversation seed since 1.253.0), so the status flips to implemented. The record was written so that a pipeline typed into a web conversation runs exactly as one typed into Slack, with one run record projected by every surface. The re-evaluation against that reasoning: the Slack surface is unchanged through a parent that lived from hand-off to finish across three unit threads and a bot generation; a pipeline typed into the web chat is admitted by capability and its runner's events land inside the hosted parent after its card closes; the runs index lists hosted parents beside their children without the stalled label a parent that makes no tool calls used to earn. Two validation rows are open on the tracker rather than done: the unit report rendered in a web unit conversation, and the parent's word linking to the live parent with its token, both waiting for a web-started unit that runs a child. One selection defect surfaced by the web receipt run is filed on the tracker: a re-issued plan drops a unit the runner itself merged earlier from its units panel and summary, while a unit a person merged is reported as already landed.
Amended 2026-09-29: machine ingress has job threads
The success criterion above that HTTP and MCP refuse Ship is superseded. Their request handles now open logical job threads under the request thread key. A coordinator unit supplies its instance and unit as an idempotency key, so a retry or bot restart resolves the same child key; an unrelated child gets a unique key. The hosted parent and its child runs remain the durable record, read through run history. HTTP and MCP share the same dispatch lifecycle; MCP may return the run handle before completion. The re-evaluation is that the original capability rule still governs admission, while durable run records replace a transport message store for these machine channels. Authorization remains in the common dispatcher and command registry.