Skip to content

The session log — a thread-and-agent conversation outlives its runs ​

Record 0035 decides that a run's transcript lives in its session's log: one Durable Object per session — a thread and an agent, the pair record 0034 defines — holding the transcript rows of every run of the session in order, each run a range of it, kept when a run finishes, indexed for search, and dropped only when its last kept run leaves retention. This spec covers the store half of that record (its rollout (1)): the object and its routes, the run's range on its ledger row and its record, the write path under the ledger's fence, the resume read, the tail read a follow-up will seed from, the byte policy, pi's compaction entry as a row, and the sweep. The seed a follow-up starts from — the log's tail within the seed budget, the gap marker after a broken run, the lines since, the request — is item 9; the recall and notes tools, the notepad in the prompt and the steer after a compaction are item 10 (the record's rollout (2), whole). The ledger this extends is run-history.md items 28–37; the harness whose compactions it stores is harness-pi.md item 8.

  • Code: src/core/testing/slackReceipts.ts (shared receipt data fixtures without tool runtime imports), src/core/testing/slackSources.ts (bot capability fixtures and compatible receipt exports), src/core/coordinator/reportAdmission.ts, src/core/coordinator/reportContext.ts, src/core/dispatch/commandRun.ts (command output capture), src/core/dispatch/sessionIdentity.ts, src/core/dispatch/handoffBrief.ts (focused child source projection), src/core/runLedger/threadSession.ts, src/core/runLedger/contextRetention.ts, src/core/runLedger/sessionRangePins.ts, src/core/runLedger/checkpointState.ts, src/core/references/receipts.ts (canonical session ownership), src/core/references/sourceReceipt.ts, src/core/references/sourceResultContext.ts, src/core/references/contextDependencies.ts (whole-context provenance), src/core/references/contextCheckpoint.ts (canonical ordinary-history grouping), src/core/runsService.ts (item 11: searchSession, SessionSearchView, SessionSearchHit), src/core/commands/runs.ts (item 11: runs search), src/core/dispatch/thread.ts (the one read of a thread's newest runs and what it yields — readThread, continuable, stickyAgentOf, previousRunOf, refusedRequestsOf, runsSince), src/core/dispatch/seed.ts (the session seed — sessionSeed, sessionSeedFor, SEED_BUDGET_TOKENS, REFUSED_REQUEST_STAND_IN), src/core/dispatch/threadArtifacts.ts (item 9: the thread's artifacts since the agent's previous run — threadArtifactsFor, threadArtifactsBlock, runArtifacts, carriesArtifacts, tldrOf, THREAD_ARTIFACTS_MAX_RUNS, THREAD_ARTIFACTS_BUDGET_TOKENS), src/tools/session.ts (recall, notes, SESSION_TOOLS, SessionCapability, sessionCapabilityFor), src/tools/toolsets.ts (the session tools in workspace, research and orchestration toolsets), src/agents/registry.ts (NOTEPAD, the prompts' notepad paragraph), src/core/dispatch/provision.ts (sessionNotesBlock, PromptContext.session, PromptContext.threadArtifacts), src/core/reviewRound.ts (makeSystemComposer's notes and artifacts blocks), src/core/harness/pi/harness.ts (compactionSteer, PiHarnessRun.notepad), src/core/runEvents.ts (the notes event), src/channels/runTimeline.ts (the notes row), src/core/runLedger/sessionLog.ts (the pure rules — sessionKey, item 13's threadSessionKey, workingSessionKey, connectorRowId, shipUnitRowId, storedTurnRow, silentOfStoredRow, foldedOfStoredRow, migrationOrder, migrationRowId, requestIndex, rowKind, textOfStoredRow, droppedToolResultRow, planSessionTrim, tailCut, sessionsToDrop, DEFAULT_SESSION_LOG_MAX_BYTES), src/core/runRecord.ts (RunSession, isRunSession, SESSION_KEY_PATTERN, RunRecord.session, RetentionPolicy.sessionLogMaxBytes), src/core/runLedger/types.ts (LiveRunMeta.session, CompactionEntry, TranscriptTurn), src/core/runLedger/transcript.ts (compaction rows, assembleTranscript's base and compactions), src/core/runLedger/ledger.ts (the seam's session methods), src/core/runLedger/inMemory.ts (InMemoryRunLedger.sessions), src/core/runLedgerWorker.ts (the /runs/session/* client, the budget on the owner claim), src/core/runLedger/writeThrough.ts (the claim order, the range, broken, the stamp on the finish record), src/core/runLedger/resume.ts (transcriptSource, ResumePlan.compactions), src/core/boot.ts (the completeness read by source), src/core/dispatch/admission.ts (an adopted row's session), src/core/dispatch/record.ts (assembleRunRecord's session), src/core/dispatch/runLoop.ts (the compactions handed to the harness), src/core/runLedger/stepReport.ts (StepReport.compaction), src/core/harness/pi/bridge.ts (BridgeObservation.compaction), src/core/harness/pi/mirror.ts (PiMirror.onCompaction, piSessionFile's compaction entries), src/core/harness/pi/harness.ts (PiHarnessResume.compactions), src/core/runStore.ts and src/config/validate.ts (runHistory.sessionLogMaxBytes), src/core/dispatch/operator.ts (item 13: operatorThreadTail), src/core/unitRuns.ts (item 13: unitSessionKeys), deploy/cloudflare-memory/worker.ts (SessionLogDO, the /runs/session/* routes, item 13's appendKeyed and /runs/session/append, RunHistoryDO's sessions registry, runs.session_key, the sweep's drop), deploy/cloudflare-memory/wrangler.template.jsonc (SESSION_LOGS binding, migration v9).
  • Tests: src/core/dispatch/handoffBrief.test.ts, src/core/coordinator/reportContext.test.ts, src/core/dispatch/commandRun.test.ts, deploy/cloudflare-memory/sessionUnification.test.ts, src/core/dispatch/sessionIdentity.test.ts, src/core/references/sourceResultContext.test.ts, src/core/references/contextDependencies.test.ts, src/core/runLedger/threadSession.test.ts, src/core/runLedger/contextRetention.test.ts, src/core/references/receipts.test.ts, src/core/dispatch/operator.test.ts (item 13: the operator's tail), src/core/unitRuns.test.ts (item 13: the unit page's keys), src/core/runsService.test.ts and src/core/commands/runs.test.ts (item 11), web/src/pages/unitPage.test.ts and src/core/untrusted.test.ts (item 11: the page's search box and the fence it takes off), src/core/dispatch/thread.test.ts, src/core/dispatch/seed.test.ts, src/core/dispatch/threadArtifacts.test.ts (item 9: the artifacts block), src/tools/session.test.ts, src/tools/github.test.ts and src/tools/skills.test.ts (the toolsets), src/agents/registry.test.ts (the notepad paragraph), src/core/dispatch/provision.test.ts (the notes block), src/core/reviewRound.test.ts (its place), src/core/harness/pi/harness.test.ts (the compaction steer), src/channels/runTimeline.test.ts (the notes row), src/core/dispatch/resolve.test.ts (the sticky agent by transcript), src/core/dispatcher.test.ts (a follow-up seeds from its session), src/channels/slack.test.ts (a history turn's time), src/core/commands/runs.test.ts (--thread), src/core/runLedger/sessionLog.test.ts, src/core/runRecord.test.ts (isRunRecord — the session field, clampRetentionPolicy — the session log's byte policy), src/core/runLedger/transcript.test.ts (the session log's rows), src/core/runLedger/inMemory.test.ts, src/core/runLedgerWorker.test.ts, src/core/runLedger/writeThrough.test.ts (the session log — a run is a range of it), src/core/runLedger/resume.test.ts, deploy/cloudflare-memory/sessionLog.test.ts (inside workerd against the real SQLite object, FTS5 included), deploy/cloudflare-memory/runLedger.test.ts (the sessions registry and the sweep's drop of a session log), src/core/harness/pi/bridge.test.ts, src/core/harness/pi/mirror.test.ts, src/core/harness/pi/harness.test.ts.
  • Docs: record 0035 (the design, its trace and its open questions), record 0034 (the session definition), run-history.md (the ledger, items 28–37 and 53), harness-pi.md (the mirror, item 8), config/config.example.yaml (runHistory.sessionLogMaxBytes).

Behavior ​

  1. One object per session. Ordinary native runs retain their thread-and-agent lane. A server-attested authoritative private input uses private:<original run id>:@context-v1, without legacy/shared fallback: the public thread, original ingress, unit and confirmation owners stay unchanged. Its log is retained run evidence, not future model context; tools and captures read fresh authorized private projection, with no saved notes or file catalogue. SessionLogDO (SESSION_LOGS binding, migration v9) is named sessionKey(threadKey, agent) = <threadKey>:<agent> (a run with no resolved agent keys on -), matching SESSION_KEY_PATTERN; two agents in one thread, or one agent in two threads, never share an object. The name is stable for the life of the thread — every run of the session derives the same key and finds the same log. Its tables: owner(run_id, gen) — the live writer; meta — the byte budget; turns(id INTEGER PRIMARY KEY, idx, part, kind, bytes, trimmed, json, text, UNIQUE(idx, part)) — one row per content part exactly as run_messages stores them (run-history item 32), plus the row's kind, its UTF-8 size and the text the index sees; turns_fts, an external-content FTS5 table over turns(id, text) (item 8); attachments(ref, media_type, data, bytes) — base64 over ATTACHMENT_REF_BYTES stored once and referenced, as the transcript object does; and notepad(text, updated_at), one row, the session's notes as the notes tool last wrote them (item 10). The routes — POST /runs/session/tail {key}, owner {key, runId, gen, maxBytes}, write {key, gen, rows, attachments}, read {key, from, to?}, read-tail {key, maxBytes}, clear-owner {key, runId, gen}, search {key, query, limit}, notepad {key}, notepad/write {key, gen, text} — sit among the ledger routes: the same bearer, the same body fences (write shares /runs/put's 2 MiB), a malformed key, generation, index or row is 400 before any object call. RunTranscriptDO and its routes stay: a row claimed before this log existed still resumes from and finishes on its own object, and no new run writes there. Its deleted_classes migration waits until no live row lacks session and at least seven days have passed since the release that stopped writing it — never a release count alone.
  2. A run is a range of its session's log. At the claim of a run with a conversation of its own (OpenRunRequest.seed; a ship pipeline has none and no session), the write-through reads the log's tail, claims the history row with meta.session = { key, seedFrom: tail, request: tail + requestIndex(seed), range: { from: tail } }, and THEN takes the log's owner (/runs/session/owner) — the history claim is the admission, so a refused claim never steals a live run's log; the three requests are one claim and any failure retries them all. The seed's turns are appended as rows seedFrom..seedFrom+n-1 under the same INSERT OR REPLACE on (idx, part) (a new generation overwrites a zombie's late row); the request is the seed's last user turn (requestIndex, as splitSeed reads it). A thread's first run of an agent starts at 0; the next run of the same agent starts where the last left off, and its own messages still count from 0 — every row of a run is at seedFrom + localIndex, so the step records' turnIndex and the completeness rule are unchanged. Every write of the run names its log — the seed, the seed record's (item 35: a step with no turns, whose rows write is the fence probe before the record), and every step — under the run's generation fence, the assistant turn before its tools run, as item 35 says; a write from another generation is 409 fenced, one before any owner 409 unknown-run. A run with a session owns no transcript object of its own, so a write of it that names no log is refused unknown-run and detaches the run; the reference ledger (InMemoryRunLedger) owns a per-run object only for a claim without a session — a row from before the log existed — so the same miss is refused in a test as it is live. Finish clears nothing: the rows stay for the thread's next run; the owner is released (clear-owner, fenced to the owner, best-effort) so a zombie of a finished run is refused; the record carries session with the range closed at the last turn written — { key, seedFrom, request, range: { from, to } }, validated by isRunRecord, to absent when the run wrote no turn. A detach (a refused or twice-failed write; item 35) keeps today's behavior and marks the record's range broken — on the record only, since the live row's writes are what failed; a record without session (older, or a run without a conversation) validates and reads exactly as before. An adopted row (a resume) continues appending at its indices and closes its range at finish; a reclaim re-owns the row's log as it re-owns a transcript object. A message with no parts is not a turn: it would write no row and still spend an index, and the next reclaim would read that hole as an incomplete transcript — so the mirror reports nothing for it and its index stays, and the bridge hands the mirror no message for the assistant message pi settles a failed model call with (harness-pi.md item 8: the failure is a note, never a turn).
  3. A resume reads the range. A run marked authoritativeHistory holds its original state and workspace: saved provider history cannot prove erased or revoked private bytes are absent, so neither restart nor in-process provider replacement rebuilds or reattaches it. The ordinary native path below is unchanged. transcriptSource(meta) says where a reclaimed run's rows are: its session log from seedFrom (readSession(key, seedFrom), the rows re-based so the conversation counts from 0) for a row with session, the run's own transcript object for one without; the boot's completeness verdict reads accordingly and planResume is unchanged, the system prompt still the live row's (item 29). The plan carries the transcript's compactions (item 6) to the harness.
  4. The tail read. read-tail {key, maxBytes} answers the newest whole turns whose bytes fit the budget, oldest first, with from — the index they start at; a turn is every part of one idx, so no tool call is parted from its result; when even the newest turn is over the budget, no rows and the tail index. The seed budget, and the cut before a user text turn, are the dispatcher's in rollout (2); the object's rule is the byte cut at a turn boundary.
  5. The byte policy. RetentionPolicy.sessionLogMaxBytes (default 200 MiB, clamped to [16 MiB, 2 GiB] like every policy field; runHistory.sessionLogMaxBytes in the config, validated as maxBytes is; ignored by a Worker that predates it) bounds one object's rows and attachments — maxBytes bounds the run records alone. The client sends the clamped budget on every owner claim and the object stores it; after each write, while the total exceeds it, the object replaces the oldest un-replaced tool-result rows (planSessionTrim, oldest first) with a marker (droppedToolResultRow: the same role and call id so the conversation stays valid, the error flag, the dropped size, and that the run record keeps the first 8,000 characters), updating the index; user and assistant text is never dropped, so a log whose text alone exceeds the budget keeps it all. The total counts the attachments' bytes too, and the policy can reclaim them: a candidate's bytes include the attachments only that row references (attachmentRefsOf, a part's own reference and a tool result's nested ones), and once the marker replaces the row those attachments are deleted — one another row still shows stays, as does the user's own image. The reference ledger (InMemoryRunLedger) holds a session to the budget its claimSession carries by the same plan, so the two implementations agree. The sessions registry row (item 7) carries the object's byte count, refreshed at finish.
  6. pi's compaction entry is a row. pi's compaction_end carries result.summary, firstKeptEntryId, tokensBefore and estimatedTokensAfter; the bridge hands the entry to the harness (BridgeObservation.compaction) and the mirror writes it as its own step report — the results pending since the last assistant turn as the user turn they would have been, then the entry as the row after them (StepReport.compaction, a TranscriptTurn with compaction in place of message, stored as { compaction: { summary, tokensBefore?, firstKeptEntryId? } } at its own index), nothing in flight. The row counts as one turn. assembleTranscript keeps the conversation as ChatMessages and reports each entry in compactions as { before, entry, keptBefore? }; piSessionFile renders it as pi's compaction entry between the messages it sat between, on the same branch. pi keeps the entries from firstKeptEntryId to the compaction and everything after it: with keptBefore known that id names the kept message's entry; without it pi's own id — which names no entry in a rebuilt file — leaves the window the summary and the turns after it. keptFrom (the log index of the entry pi kept first) is in the row's shape and rendered when present, and nothing produces it yet: pi's message events carry no entry id, so the mirror has no map from firstKeptEntryId to a turn. A compaction whose summary pi's own model call could not write — refused under the provider's usage policy, over its cap — carries the bot's pointer summary instead (harness-pi.md item 7: why, and that every dropped turn is behind recall) and is a row like any other.
  7. The registry and the sweep. RunHistoryDO keeps sessions(key, thread_key, agent, last_finished_at, bytes), upserted at a claim naming a session and at every record write naming one, and runs.session_key, the session a finished record was a range of (null for one without). When scheduled cleanup is enabled, the 6-hourly sweep, inside its transaction, trims the runs and lists the sessions no remaining run row names; after the transaction, each drop awaits the session object, so a claim or a finish can land between two drops and the list can go stale; the decision is therefore taken per key on what the object re-reads right before that key's drop — SELECT 1 FROM runs WHERE session_key = ? and SELECT 1 FROM live_runs WHERE thread_key = ? — through the pure rule sessionsToDrop, which takes exactly those two facts per candidate and answers drop, kept-run or thread-live. A candidate a fresh record or a live run has overtaken is skipped and keeps its registry row; any live run on the thread blocks every session of that thread, whichever agent it runs. A dropped session goes through the object's drop(): the owner row first, so a write racing the drop is refused, then every row, index entry, attachment, the notepad and the budget; the registry row goes once the object is empty. A session with a kept run, or a live thread, stays whole. Scheduled cleanup is paused for the MVP; existing session logs stay stored until the bounded sweep is implemented.
  8. The full-text index. turns_fts is an external-content FTS5 table over turns(id, text), so a row is stored once. A replaced row's old text is removed with the FTS delete command before the new row is written and indexed — the table learns of a replacement only when told — and the byte policy's markers are indexed the same way. What a row indexes (textOfStoredRow): a text part's text; a tool result's text, string or parts, so a failing test's name in a report is findable; a tool call's name and arguments, so a command is findable; a compaction row's summary; an image, a document, a thinking block or an unreadable row, nothing. SessionLogDO.search(query, limit) answers the matching rows in relevance order — FTS5's bm25 rank, the order the memory store's search uses, the newest first among equals — each with its turn, part, role (roleOfStoredRow), kind and indexed text, over the memory store's MATCH expression (ftsMatchExpr: every word quoted, at most 24, OR-joined); the search route (item 1) answers them with the gap markers (item 9) whose turn lies between the oldest and the newest hit (gapsBetween), and recall (item 10) reads it.
  9. A follow-up seeds from its session (record 0034 "The seed", record 0035 "The seed"; src/core/dispatch/thread.ts, src/core/dispatch/seed.ts, the messages stage of dispatch()). For a reply in an existing thread — a message with thread history; a thread starter, a spawn, a coordinator's child, a resume and a restart read nothing — the dispatcher reads the thread's newest runs once (readThread: one listRuns by thread key, THREAD_READ_LIMIT newest, live and finished, under no visibility predicate) and derives from that page the lineage (agent-conductor.md item 10), the sticky agent by transcript (routing-and-config.md item 3) and the previous run of the agent the request resolved to. The seed source is then exactly one of four, and the record's seed names it (run-history.md item 52): a resume continues its own plan; a spawned child starts from its parent's text turns (parent); a follow-up whose thread has a log of its resolved agent (sessionKey(threadKey, agent), one with rows) starts from that log (session) — every preset, since every preset runs on pi; anything else — a thread's first run of an agent, a directive naming an agent the thread has no log of — starts from the channel's history (channel). The session seed (sessionSeed, pure; sessionSeedFor reads the ledger) is, in order: the log's newest whole turns within the seed budget — SEED_BUDGET_TOKENS = 60,000 at four characters a token, SEED_BUDGET_BYTES on the object's tail read (item 4) — beginning after the newest compaction row (item 6; that entry's summary is carried for the system prompt and is never a row) and cut forward to the first user turn that carries text, with the results that turn carries for calls made before the cut dropped and noted (pi answers a tool batch and a steer in one user turn, and compacts between the batch's calls and its results), so the conversation opens as providers require and no tool call is parted from its result; thinking blocks dropped from the tail's assistant turns, an assistant turn of nothing but thinking keeping one text part so every row keeps its place; a tail that ends on calls in flight answered with one error tool_result per call saying the run ended before the result landed, as a resume settles them; the requests the provider refused left out — every finished run of the agent in the page whose record names its session and says failure: policy_refusal (run-history.md item 57; refusedRequestsOf) names its request row, and that row's words leave the tail, since the same words are refused again on every later request that carries them: the row keeps its place and its role as one text part (REFUSED_REQUEST_STAND_IN), because the seed reuses the log's rows by index and a row dropped would move every row after it and have the write-through write the tail again as new rows; the log keeps the words for recall; a note counts the requests left out; a refused row before the cut, or one that is not a user turn, is nothing to leave out; a gap marker as a user turn when the previous run's record says broken; the channel's lines since — the user turns whose time (HistoryItem.at, epoch ms; the Slack adapter sets it from the message ts) is after the previous run's finishedAt, the bot's own posts left out because the run's final reply is the tail's last assistant turn — and the request as the last user turn. Nothing is merged across the tail's end: every message after it is a new row. The rows the seed reuses ride the ledger's open request (OpenRunRequest.seed.log = { from, turns }): the first turns messages are the log's rows from from, so the claim sets the row's seedFrom to from and its range.from to the tail (item 2 as amended: seedFrom ≤ range.from), the seed write appends only the messages after them, and every row of the run is still at seedFrom + localIndex — the resume's read from seedFrom (item 3) is the conversation the model saw, the tail's thinking blocks aside (the seed drops them, the log keeps them). Named rows that do not end at the tail (the log moved under the seed) are written whole as new rows from the tail, with one warning. What wins over the budget: a newest turn alone over it, or a tail with no user text turn, yields the lines since and the request with a seed note; a broken predecessor the marker and the tail past it; an unknown previous end (no finished run of the agent in the page) the tail and the request with a note; a log that cannot be read — no route, a failed request — the channel, seed: channel, and a note naming the log. Every note is a run_note of kind seed on the record before the first turn; the run itself is never changed by one. runs list --thread <key> lists one thread's runs newest first through the same store filter, live and finished. Budgeted general source continuations: a bare continue in the same conversation following an unanswered general run's budget close reads its recorded input only after verifying its budget note, continuation answer and original requester. If that input is itself continue, it walks older runs (paging the thread when needed), verifying each same-requester budget close until it reaches the original question; an unreadable question or unverifiable chain fails closed. It seeds a fresh channel conversation from that ask and the current reply, never from the old session's tool results, notepad or a stale source response (no durable grant or revision label). A different requester is refused; the follow-up's tools still resolve against its requester. This exception to session reuse keeps the question without preserving source evidence across a changed grant.

The thread's artifacts since (record 0034 "The session", as amended; src/core/dispatch/threadArtifacts.ts): a follow-up — seeded from its session or, in a thread that has runs, from the channel — is also handed, as one block of its system prompt right after the notes block (item 10) and before the config block and the agent's prompt with its REVIEW TARGET, the typed artifacts of the thread's finished runs newer than the agent's previous run (runsSince: the runs before that run on the page, oldest first; every finished run when the agent has none to continue there; a live run has no record yet; another agent's run and a coordinator's child count alike, since both are runs of the thread). For each run whose view names one (carriesArtifacts: a verdict, dispositions, a handoff, a pull request or a review post — a run naming none costs no store read) the record is read whole through the runs service (getRun with its events, the read the coordinator's readRunFacts makes) and rendered as data in one order (runArtifacts): the pull request opened or edited (owner/repo#n read off its GitHub URL, the URL, the head branch), the review verdict with its findings by id (formatFinding), the dispositions by finding id with their notes (formatDisposition), the handoff's deviations, follow-ups and unproven criteria (handoffLines), the TL;DR of the description the run last submitted for that pull request (the last pr_description event; a description whose pull request never opened describes nothing a reader can open and is left out), and the review post's outcome — what a carried artifact lacks said out loud (findings: none recorded, nothing to hand off). The block is bounded as the tail is (item 4): the newest whole runs whose rendered bytes fit THREAD_ARTIFACTS_BUDGET_TOKENS (6,000 at four characters a token, a tenth of the seed budget), at most THREAD_ARTIFACTS_MAX_RUNS (6, of a page of THREAD_READ_LIMIT), oldest first in the text, a cut said in the block (cut to the newest N of M runs) and, when even the newest run's block is over the budget, that run named and none rendered; the heading names the window — since the agent's previous run, or every run of the thread. The block rides the prompt, never the conversation, so the seed's four sources and the record's seed are what they were; the record's seed notes say what rode (thread artifacts: N runs since the previous run ride the prompt (ids), a cut, a record that could not be read and was left out). Nothing since, or no run carrying an artifact, is no block and no note. The ship runner's own carry (agent-ship.md items 5 and 7) is untouched — it matches the dispositions to the round's finding ids and names the dropped ones — and a runner's child, dispatched with no thread read, gets no block. For the main orchestrator pilot, other agents' artifacts have no proven source audience and do not enter its prompt. Its own verified session still seeds the follow-up; the main agent checks a specialist result afresh before using it (orchestration-plane.md item 12). Specialist follow-ups continue to receive the artifacts described above.

Continuation checkpoint and empty-tail recovery (items 4, 6 and 9). A tail read also returns checkpoint: { index, entry } for the newest compaction in that session, independently of the selected rows. A follow-up carries this summary even when its row falls outside the tail. If the ordinary seed reuses no turns, exceeds the combined history budget, or excludes the previous request, sessionSeedFor reads the newest finished run's exact request row on that agent and session, preserves its actor and refused-request substitution, and copies a bounded view containing that request and recent exchanges. The view drops tool results whose calls were cut and settles a trailing call with the existing unknown-outcome result. Notes, summary and reused or copied history fit the seed byte budget; unavailable or oversized protected request context refuses continuation. A refusal emits one conversation_context_refused JSON line with its reason, previous run id, request index and measured bytes against the bound; it contains no request text. A copy appends new rows instead of claiming contiguous reuse. A missing summary and an omitted range are named; the prompt directs recall before relying on omitted details or repeating unresolved work. History sizing excludes top-level and tool-result media payloads, which retain their existing attachment limits. A complete continuation checkpoint excludes earlier rows from the copied suffix. Cumulative source obligations and current admission checks remain in force. Record 0109 explains this exception to ordinary tail reuse.

  1. recall, notes and the notepad (record 0035 "Recall" and "The notepad"; src/tools/session.ts). Two tools (SESSION_TOOLS in src/tools/toolsets.ts) join the toolsets of the presets that have a workspace (full, readonly, explore), workspace-less research (web), general (assistant), conductor, and the workspace-less orchestrator, whose long-lived plane thread outlives any one run's context (orchestration-plane.md item 12), relayed to pi like every other bot tool (harness-pi.md item 7). The capability (SessionCapability, sessionCapabilityFor): the dispatcher builds it right after the ledger claim from the row's place in the log (LedgerRun.session) over the write-through's session reads and its notepad write under this generation (readSession, searchSession, readNotepad, writeNotepad), and hands it to the run loop, the tool context and the pi harness; a run without a session — untracked, a ship pipeline, a process with no ledger — has none, and the tools say they are not available. Both read under the requester's runs:read: the same point read on the run itself get_run_status makes (authorization.md item 5); a read the requester may not make is an empty result, never an error. recall { query, limit? } searches the caller's own session log through item 8's search — limit 5 by default, 50 at most — and answers { hits: [{ turn, role, snippet }], gaps?, note? }: the hits in relevance order, each turn's log index, its role and a one-line snippet of at most 300 characters; gaps names the gap markers the hits straddle; note says when no turn matches (and points at the notepad) and, in a first run (seedFrom 0), that the log begins with it. recall { turn } reads that turn whole (readSession(key, turn, turn)): { turn, role, content } with every part, under the relay's tool-result cap; a turn the log lacks is named. A call with neither is refused by name. notes { text } replaces the notepad whole — one document per session, at most NOTEPAD_MAX_BYTES (8,192 UTF-8 bytes; over it the tool refuses naming the size and writes nothing) — through the object's notepad/write route under the owner's fence (unknown-run before an owner, fenced for another generation, 400 over the size), says the size it saved, and publishes the whole text, redacted like every narrative event, as a notes event on the record (RunEvent, a narrative event like answer; the record's last one is the final text; the run page draws it as a Notes block of its own — 📝 Notes for this thread under the clock of the write, the Markdown rendered by the one renderer — never as the ⏱ line a lifecycle note gets, and since the notepad is replaced whole on every write the newest write stands open and an earlier one folds as superseded, live-view.md); a fenced write says the notes were not saved. notes {} reads the notepad back, or says it is empty. The tool's call and its result are turns in the log like any other, so the notepad's history is the log's. recall knows the thread's files when its catalogue reader is admitted (record 0033; execution.md item 20): for automatic-intake toolsets on a deployment with an artifact store, the capability carries the thread's catalogue read (SessionCapability.assets, readThreadAssets fresh on each call, so a file this run just received or attached is in it) and this run's workspace paths (workspacePathOf, WorkspaceFiles); a hit whose turn names one of the files — an attachments line, an attach_file call — carries files: each file's name, size, type, the run that received or produced it, its key and where it is for this run (whereIs: in this workspace at its attachments/ path when this run pulled it, on its run's page for a produced file, no longer in the store when its retention passed, or that the store could not be asked); the join is by name at a word boundary (filesNamedIn: 2-clip.mp4 in an attachments line names clip.mp4, myclip.mp4 does not), under the name the person saw and the spelling the workspace gives it (safeBasename); the catalogue is read only when some hit carries an attachments line or an attach_file call, never for prose alone. recall { assets: true } lists the catalogue outright in that shape instead of searching — an empty one says no run of the thread received or produced a file, a run without an admitted catalogue reader says the catalogue is not available to this run, a denied read is an empty list — and a call with none of query, turn or assets is refused by name. Tool-based source intake: a declared slack_context capability selects tool intake (sourceIntakeFor) even when binding fails. The main orchestrator run receives no prior-run file catalogue in its prompt or SessionCapability.assets, even with an artifact store. recall { assets: true } reports that unavailability; file-bearing search hits receive no catalogue-derived files enrichment. Discover message and file IDs through bounded slack_context thread or link reads, then read the file against its exact authorized message; each accepted read persists its source receipt before exposing content (slack-context-tools.md). Missing binding never restores the catalogue. Direct requester attachments keep their existing input path. The notepad's three read points: (a) the system prompt at every run start — sessionSeedFor reads it beside the tail and the dispatcher hands it, with the newest compaction's summary, to composePrompt, which renders sessionNotesBlock right after the memory block ("YOUR NOTES FOR THIS THREAD …", then "SUMMARY OF THE EARLIER CONVERSATION …", then "FILES OF THIS THREAD …" — the thread's files one line each as describeAsset writes them, for automatic-intake runs in a thread with files and an artifact store, seeded or not (execution.md item 20) — each only when present; makeSystemComposer's notes block); a notepad that cannot be read is a seed note and the run starts without it; (b) a steer after every compaction_end that carried a summary, at pi's next turn boundary (compactionSteer: the notes as they stand, or that they are empty and how to keep them, and that recall reaches every earlier turn) — never during the write-up, after a hard stop, or while a re-attach catches up on history (harness-pi.md item 6); the steer is advisory, so a notepad read that fails there (a ledger blip, a Worker without the route) costs it its notes and never the run — the steer says the notes could not be read just now, and a harness_error note says why; a resumed run keeps its row's prompt, so notes written after it started reach a resumed pi by the steer alone; (c) notes {} on demand. The prompt says what belongs there: workspace prompts (coding and review, both variants, explore) and workspace-less research, general, conductor and orchestrator prompts carry the NOTEPAD paragraph verbatim (src/agents/registry.ts) — decisions and their reasons, the names of things found, what is not yet proven, written when decided and not only at the end, as a Markdown document a person reads on the run page: a ## heading per section (Done, In progress, Next, Facts; an empty section left out), one bullet per item, one line per bullet, never one paragraph; the notes tool's description names the same shape — and that recall reaches every earlier turn; These prompts also carry PHASE_CHECKPOINTS: keep the goal, decisions, rejected approaches, completed work, unproven criteria and next step. Where run_check is available, its recorded outcome and checkpoint acknowledgment remain separate from model-reported progress. Without recorded-check capability, notes cite original source evidence and never label unavailable proof as verified. The Ship pipeline has no model prompt and does not promise these tools.

  2. A person searches one session's log (record 0035, "Recall"; RunsService.searchSession in src/core/runsService.ts, runs search in src/core/commands/runs.ts). The read recall makes (item 10) is a route for people too: runs search <session key> <words…> [--limit n] — GET /api/runs.search?session=…&query=… on the dashboard's surface, the same on MCP and the CLI — searches one session's index (item 8), one key per query since the index is per session and no cross-session index exists, and answers { session, hits: [{ turn, role, snippet, runId, gap }], gaps }: the hits in relevance order, each turn's log index, its role, a one-line snippet of at most SNIPPET_CHARS (300) characters (snippetOf, the tool's line too), the run of the session whose recorded range (item 2) holds the turn — how a hit links into that run's timeline — and, on a hit past a gap marker the hits straddle (item 9), that marker's turn as gap, with every straddled marker in gaps; limit is 10 by default and at most SEARCH_MAX_HITS (50), the object's cap, recall's and the route's alike. Under the reader's runs:read predicate (run-visibility.md item 9): ordinary sessions list the original thread's runs of the named agent; a - agent sentinel selects only runs without a resolved agent, including legacy and context keys; unit working sessions page that agent's visible runs across transport threads. Only an exact stored session key or an admitted live session grants the read, including :@context-v1 keys. A registry-held live run must match its ledger identity (thread, agent, requester, channel, repository, channel visibility and coordinator association, including the original registry summary's typed unit and attempt) before the ledger's session is used. A persisted ordinary run from before session metadata existed may authorize only the original legacy key; it cannot authorize a fresh context or a unit working key. Listings use bounded pages and continue through older history. Private worker runs remain excluded. A reader admitted to none of the session's runs — like a session nobody ran, a key that is not one, or a process without a ledger — is answered empty, one answer, the log never asked. Snippets are stored free text and leave the machine surfaces wrapped as untrusted content (wrapUntrusted, src/core/untrusted.ts — the fence's three markers in one node-free module); the command has no chat surface. The unit page's search box (live-view.md item 28; web/green/pages/Thread.vue) is this read for a person: one session at a time — the coding agent's or the review agent's, picked in the box, never both — through GET /api/runs.search, each hit listing its turn, who spoke, the snippet with the fence taken off (unwrapUntrusted — a page renders text as text) and the round and thread of the run whose range holds it, a link that opens that run's fold on the page at the step the turn lives in: once the fold has read the run's record it draws that one step with the run page's own step block, at the run page's own anchor (span-<the turn's span>, else call-<its first call>), scrolls to it and links to the same step on the run's page. The turn finds its step through the rows the record's tool_call and tool_result events name (logIndex, run-history.md item 53; createRunPageModel().landing): a named row is its event's step, and a row no event names — a steer's text, a compaction entry, an assistant turn that made no call — is the step that wrote the newest named row before it; the request row (session.request) and the reply's (the range's last row, when the run replied) are named, not drawn; a record from before the field names no rows, lands nowhere, and the fold opens as it always did — the round-level link stands. ?open=<run id>&turn=<n> is the shareable form. A hit past a gap marker says it lies past a compaction gap at that turn.

  3. A row knows its author (record 0057). A user turn a person authored stores its author on the row: StoredPart.actor, the platform-namespaced id of the person (slack:U…), in the row's JSON beside role and part. A machine turn, a compaction row and a row written before the field carry none; actorOfStoredRow reads a row's actor, undefined for those and for an unreadable row. The seed carries the authors: SessionSeed.actors names each seed message's author by index — the channel lines since the previous run from HistoryItem.user, the request row from the requester (request.actor; the dispatcher hands msg.userId) — and rides OpenRunRequest.seed.actors into the write-through's seed write, offset by the rows the seed reuses, as TranscriptTurn.actor through turnRows, in the reference ledger and the Worker client alike. A thread's first run — no session, the channel's history as the seed — is authored the same way: buildConversation carries each history line's HistoryItem.user and the request the requester's id beside the provider turns, through the same merges the turns take — a merged turn keeps its author only when every merged line is that one person's, since the stored row carries all of their words. assembleTranscript never surfaces it: the provider message is identical with or without an actor on its rows.

  4. The thread session and the working sessions (record 0057; the one-door plan's memory unit). Two key families join item 1's <threadKey>:<agent> under the same pattern and the same object. The thread session is keyed by the thread alone — threadSessionKey(threadKey) = <threadKey>:@thread, collision-free since no agent id starts with @ — the one log the operator reads and writes: every connector event the intake gate admits, every child's report, every question and answer is a turn in it, and a reply the gate withholds as silent is appended as the person's turn marked silent (storedTurnRow, silentOfStoredRow). A working session is keyed workingSessionKey(instance, unit, lane) = <instance>:<unit>:<lane> (coding | review): verified coding rounds of a unit continue one log and verified review rounds the other; a rejected execution-source context uses the isolated log described below. A re-issue of the same plan continues the prior instance's verified lanes — the key strips the re-issue's attempt suffix (planInstanceId's -<attempt>). The keyed append (/runs/session/append {key, rowId, rows}; appendKeyed on the object, appendSession through the seam and the write-through) lands the parts of one turn at the log's tail under a row id — turns.row_id, held by a partial unique index — idempotently: a row id the log has seen appends nothing (appended: false), so a fold that reads the same event twice yields one row; two appends keep their arrival order, each landing at the tail inside one transaction; no owner fence, since a thread session has no one owning run; and item 5's trim marker keeps the row id of a tool result it replaces, so a replayed migration or fold appends nothing even after a trim. A fold of a hosted parent's ship_unit event appends under that event's identity (shipUnitRowId: unit, state, seq) and its rows are marked folded (foldedOfStoredRow), what operatorTail keeps whole ahead of older turns; a connector turn appends under its message id, with its edit timestamp when edited (connectorRowId), so an edited message appends a second row and a re-delivered unedited one nothing. The operator's tail reads the thread session first (operatorThreadTail); a thread whose session is empty falls back to the thread's per-agent logs in the order of their runs, read-only, until the migration lands. The migration is replayable and rewrites no old key: an old thread's <thread>:<agent> logs are read into the thread session in the order of their runs' start times (migrationOrder), each run's rows in its session range in index order and rows outside any run's range after the runs that precede them in their own log, each row appended under the stable id migrationRowId(key, idx) so a replayed migration appends nothing twice; the old keys stay read-only for recall. The unit page's search keys derive from the working keys (unitSessionKeys): <instance>:<unit>:coding and <instance>:<unit>:review, a run of a lane that names its own session (a row written before the re-key) still winning. Deletion and retention: a thread session and a working session are session objects like any other — the sweep of item 7 drops one only when no kept run names it and no live run holds its thread, and they follow the run store's retention; no command deletes a thread session directly today — a deliberate gap, stated here until an operator-facing delete is decided. New sessions use the context epoch described below. Dispatcher intake, operator questions, and completed execution answers append through the atomic shared-log seam; claim and seed reads share the admitted working key. The Worker registers logical threads and retains referenced contexts. Generic child answers also enter their canonically linked parent conversation. Hosted coordinator reports use the same append seam. Raw report text without complete producer dependencies remains unknown and is omitted from proved conversation context; its separate committed status row can be admitted independently.

The execution seed captures transcript and notepad bytes before re-reading the canonical session's complete source metadata. An unavailable metadata read leaves provenance unknown; it does not substitute the earlier snapshot. This ordering also applies when a canonical unit lane overrides the thread-and-agent key. Coordinator publication freezes full and selected report bytes in the shared log before delivery. Freeform report rows remain unknown; a separate immutable status row carries committed machine fields with current canonical unit and audience checks.

Validation criteria ​

CriterionEvidence
The notes tool states that stored notes are unavailable in an authoritative source view, for both reads and writes. It does not report an owner-generation conflict for this restriction.[unit] src/tools/session.test.ts::sessionCapabilityFor — the capability the dispatcher builds for a run with a session::uses only fresh authoritative projection for tools and child capture without stored notes assets or recall ranges
Guarded custody protection advances the receiver's durable pin revision on every positive dependency, including same-range reacquisition. Pruning reads that revision before canonical holder selection and applies only if the receiver still matches it; a delayed stale plan cannot erase successor protection. Distinct revision/protect/conditional-prune RPC names refuse unknown or older receivers without unconditional fallback, and guarded logs decline legacy unconditional pruning. Ordinary unguarded context pins keep their pruning behavior; existing pin mutations also advance the revision.[unit] src/core/runLedger/workspaceDurability.test.ts::canonical scratch custody disposition::refuses stale pin plans and legacy pruning after same-range custody reacquisition; ::keeps ordinary pin pruning while reset or unreadable guarded revision stays held; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::retains both custody pins through actual maintenance before and after finish; ::advances same-range protection and atomically declines stale or legacy pin pruning; ::refuses a delayed prune when successor allocation protection arrives after holder selection; ::holds unknown or old receivers without unconditional fallback and preserves guarded data after reset; [gap] Mixed-version deployment and downgrade safety require a delivery gate before original allocation contracts are enabled.
The write-through requests normalization only for an acknowledged initial seed, does not adopt a refused checkpoint, and preserves the same committed receipt through later notes, source leaves, finish and restart[unit] src/core/runLedger/writeThrough.test.ts::open — claim and seed::normalizes only the committed initial seed and keeps failed attempts out of local state; src/core/runLedger/writeThrough.test.ts::open — claim and seed::keeps one committed checkpoint through later source changes, finish and facade restart
A later detached write marks the run range broken without invalidating its earlier committed checkpoint; archive and cold read verify the sealed prefix, while a new checkpoint still requires an intact range[unit] src/core/runLedger/writeThrough.test.ts::open — claim and seed::archives a committed checkpoint with a broken range after a later step detaches; src/core/references/contextCheckpoint.test.ts::ordinary context checkpoint::validates a committed prefix when a later write breaks the run range; deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::commits an exact ordinary checkpoint, archives it after a broken tail, and rejects stale input seals
Abandoning a run updates range pins only for session keys that named it as a holder or source; a run with no pins makes no session-log calls[unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — alarm retention of live events::abandon syncs only range pins touched by the abandoned run; deploy/cloudflare-memory/runLedger.test.ts::run ledger — alarm retention of live events::abandon without range pins does not sweep other session logs
Ordinary continuations replace covered run origins only with a committed checkpoint sealing the exact seed, notes, source closure and at most 128 recent identity aliases; aliases do not pin archives. Restart preserves the receipt, unknown sources remain unknown, and foreign authority remains explicit[unit] src/core/references/contextCheckpoint.test.ts::ordinary context checkpoint::*; [unit] src/core/runRecord.test.ts::retained context checkpoints bind the exact canonical run and remain private; [unit] src/core/dispatch/record.test.ts::durable source archive assembly::archives only the committed context checkpoint
Public repository result coverage binds the exact final bytes before storage; changed bytes cannot reuse the original receipt[unit] src/core/runLedger/inMemory.test.ts::public repository result receipts::*; [unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::hash-verifies repository results before committing their source coverage…
13: Coordinator reports enter the canonical unit thread before publication. Unit settlements first admit the exact report proposal hash with their canonical CAS, including idle, segment and recovery boundaries; only the winner can freeze raw bytes. A retry uses the original full report and rendered reply under the original delivery identity. Raw coordinator prose has unknown provenance, which a replay cannot upgrade to source-free context. A missing durable append withholds publication.[unit] src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::freezes coordinator report text before publication and replays the original delivery; src/core/coordinator/reportContext.test.ts::coordinator report snapshots::*
An internal keyed read returns the exact stored report after later turns; missing, trimmed, or malformed entries cannot substitute new bytes on retry[unit] src/core/runLedger/inMemory.test.ts::keyed session entry read::*; [unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::reads one frozen keyed entry without substituting transcript tail or malformed rows; [unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::reads the exact keyed report after later appends and returns no bytes for a missing or trimmed entry
Retained units pin canonical capsule sources through terminal updates. A legacy ending refuses failed-create replacement and preserves its sources; a missing source prevents admission atomically[unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::pins a pending unit's live source through finish, a legacy ending and refused owner replacement; [unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::refuses a missing unit source atomically before storing its capsule
Shared appends reject a divergent embedded context, preserve immutable replay proof through trimming, and retain exact external source archives without recursive roots; superseded ordinary checkpoint roots retire under the bounded history policy[unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::*; deploy/cloudflare-memory/sessionLog.test.ts::session log object — the keyed append (session-log item 13)::*; src/core/runLedger/inMemory.test.ts::keyed append context and immutable replay::*
13: Worker appends preserve exact per-row context across replay; changing a row's dependencies is refused and unknown command output does not overwrite neighboring known row proofs[unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::preserves each explicit row context through replay with an unknown row between known rows
13: a runless registry command saves its complete output and explicit unknown dependencies before returning; replay is idempotent and failed persistence prevents publication[unit] src/core/dispatch/commandRun.test.ts::runChatCommand — the machinery moved from the fast path::durably stores a runless command's exact output with explicit unknown dependencies before returning
13: approval offers and confirmed command results occupy separate turns; confirmed repository commands publish their stored outcome, and a second click cannot execute again[unit] src/core/dispatch/confirm.test.ts::consumeAndRun — the stored input runs once, as the requester, through the typed path (record 0044)::stores the approval offer and confirmed result as separate turns before returning the outcome, ::publishes the confirmed…
13: explicit per-row contexts align with messages across compaction, while absent or conflicting metadata grants no source proof[unit] src/core/runLedger/transcript.test.ts::explicit context metadata on shared conversation rows::*
13: an operator question and requester reply survive a new dispatcher facade without starting an execution run; the shared log preserves both turns and their complete dependency envelope[unit] src/core/dispatcher.test.ts::durable operator conversation without an agent run::keeps a question and its reply across a new dispatcher facade
13: the production Worker registers logical thread/working sessions, validates the durable source owner, commits context metadata atomically with keyed turns, and keeps existing source records/logs until their final live or retained holder retires[unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::*
13: unit working keys use the admitted coordinator unit identity and lane, persist that typed association across restart, preserve already claimed session keys, and never derive unit authority from an idempotency string[unit] src/core/dispatch/sessionIdentity.test.ts::run session identity comes from the admitted unit::*
13: source call announcements do not taint consumed context; copied seed results require original dependencies, newly exposed step results require the current writer's original action, exact mirror retries are idempotent, and uncovered source or orphan results make context unknown before persistence[unit] src/core/references/sourceResultContext.test.ts::source result context at the persisted row boundary::*
13: whole-context metadata unions original run identities, repository-access names, Slack and MCP dependency leaves across handoffs without relabeling audiences; absence remains unknown, unknown/revoked states cannot heal, equivocation and bounded overflow are explicit, and frozen snapshots bind revision and canonical digest[unit] src/core/references/contextDependencies.test.ts::whole-context dependencies::*
13: retained or live children pin only existing source runs explicitly named by their bounded handoff manifests, including frozen logs, note snapshots, source reads and artifact producers; pins end with their holders and do not resurrect deleted sources or recursively preserve unrelated ancestors[unit] src/core/runLedger/contextRetention.test.ts::context retention — bounded references follow their live holders::*
13: source metadata on canonical unit lanes is bound to the durable run's exact session, requester and thread; a failed owner lookup cannot fall back to a matching legacy key prefix, and the client identifies the registry used to check ownership[unit] src/core/references/receipts.test.ts::source metadata belongs to the persisted session owner::*, src/core/runLedger/inMemory.test.ts::resident attachment claims::accepts source metadata for the claimed unit lane and refuses another requester or session, src/core/runLedgerWorker.test.ts::WorkerRunLedger::requires explicit source metadata acknowledgment from the memory worker
13: connector and report turns atomically retain dependency metadata before their rows, preserve actor, silent and folded metadata, and reject changed bytes under an existing event identity; a bounded legacy migration interleaves run ranges, replays safely after a late finish, excludes canonical unit lanes and projects only independently verified ingress text, treating absent source metadata as unknown[unit] src/core/runLedger/threadSession.test.ts::thread session — durable turns and legacy context::*, src/core/runLedgerWorker.test.ts::WorkerRunLedger::requires the thread append's atomic context acknowledgment from the memory worker
13: copied working notes are saved under the new lane's exact owner before the model starts and never overwrite an established lane's notes; a child's structured source reference survives finish[unit] src/core/runLedger/writeThrough.test.ts::the session log — a run is a range of it::an explicit working lane continues across rounds without borrowing the ordinary agent log, ::retains a child's structured source reference in the finish record across adoption
13: a unit lane selected by the dispatcher is used for both seed reads and fenced claim writes; reused rows retain their indices across coding rounds and ordinary runs keep their own working log[unit] src/core/runLedger/writeThrough.test.ts::the session log — a run is a range of it::an explicit working lane continues across rounds without borrowing the ordinary agent log, src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::reads an explicit working lane and ignores another session's refused request indices
13: the first run of a new unit lane may copy a verified legacy lane, including its notes and trusted source metadata, but never reuse indices from the other key; once the new lane has rows the old one is not read[unit] src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::copies a legacy working seed once without reusing its row indices in the new lane
13: the thread session's key is the thread alone and collision-free with the per-agent keys; a unit's lanes continue one working session across rounds and a re-issue's attempt suffix is stripped[unit] src/core/runLedger/sessionLog.test.ts::threadSessionKey and workingSessionKey — the thread session and the working lanes (item 13)::*
13: an original coordinator instance uses explicit attempt zero in its admitted child and finished record; only reissue attempts strip the instance suffix[unit] src/core/runRecord.test.ts::isRunRecord — the coordinator fields (parentInstanceId, idempotencyKey)::accepts the original coordinator attempt zero and rejects invalid attempt identities; src/core/dispatch/sessionIdentity.test.ts::run session identity comes from the admitted unit::keeps an original instance name intact when its explicit attempt is zero
13: an edited message's row id differs from its original's and a re-delivered unedited one's does not; a fold's row id is the ship_unit event's identity[unit] src/core/runLedger/sessionLog.test.ts::connectorRowId and shipUnitRowId — the keyed append's identities (item 13)::*
13: a silent reply is stored as the person's turn marked silent, a fold marked folded, an ordinary turn neither[unit] src/core/runLedger/sessionLog.test.ts::storedTurnRow — a thread-session row and its marks (item 13)::*
13: the migration orders two agents' logs by their runs' start times, each run's rows in index order, rows outside any range after the runs before them, under stable row ids[unit] src/core/runLedger/sessionLog.test.ts::migrationOrder — old per-agent logs read once into the thread session (item 13)::*
13: through the write-through, a fold that reads the same ship_unit event twice yields one row; two connector turns keep the object's order and an edit appends a second row; a silent reply reads back as the person's turn marked silent[unit] src/core/runLedger/writeThrough.test.ts::appendSession — the thread session's idempotent keyed append (session-log item 13)::*
13: the object's keyed append is idempotent on the row id with no owner claimed — across a byte-policy trim of the row — keeps arrival order, and refuses a malformed row id or empty rows[unit] deploy/cloudflare-memory/sessionLog.test.ts::session log object — the keyed append (session-log item 13)::*
13: the operator's tail reads the thread session first and falls back to the per-agent logs only when it is empty[unit] src/core/dispatch/operator.test.ts::operatorThreadTail reads the thread session first (session-log item 13)::*
13: the unit page's search derives <instance>:<unit>:coding and <instance>:<unit>:review, the attempt suffix stripped[unit] src/core/unitRuns.test.ts::unitSessionKeys — the unit page's working-session keys (session-log item 13)::*, web/src/pages/unitPage.test.ts::the session search derives the unit's working keys <instance>:<unit>:coding and <instance>:<unit>:review when no run of a lane names its session (session-log item 13)
12: actorOfStoredRow — a row with an actor answers it; an old row without the field, a compaction row and an unreadable row answer none[unit] src/core/runLedger/sessionLog.test.ts::actorOfStoredRow — the author's id, absent for bot turns and old rows::*
12: turnRows stores the actor in the row's JSON, a machine turn and a compaction row store none, and assembleTranscript strips it from the provider messages, reporting it on the aligned actors array instead (absent when no row names one) — the verifier's author selection reads it (routing-and-config.md item 25)[unit] src/core/runLedger/transcript.test.ts::actor — the row's author, stored in the JSON, absent for bot turns::*
12: the seed's actor map — the channel lines since carry each author's id, the request row the requester's, a machine turn none, and a seed with no author carries no map[unit] src/core/dispatch/seed.test.ts::sessionSeed — actors on channel lines and the request row::*
12: the write-through writes the seed's actors into the stored rows, offset by the rows the seed reuses[unit] src/core/runLedger/writeThrough.test.ts::actor — rows carry the author's id through the write-through::*
12: the Worker client's seed write carries each turn's actor into the stored JSON[unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::a seed turn's actor rides the stored row's JSON…
12: the channel seed's authors — buildConversation parallels the provider turns with each turn's author, keeps a merged turn's author only when one person wrote every merged line, and carries none when no turn has one[unit] src/core/dispatch/messages.test.ts::buildConversation — the channel seed's authors::*
12: end to end through dispatch() — a first run's channel seed writes authored rows: each channel line its author's actor id, the request row the requester's, machine rows none, and a merged row of mixed authors none[unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::a first run's channel seed writes authored rows…
1: the key is <threadKey>:<agent> (a dash for no agent) and matches the pattern; two threads or two agents never share one[unit] src/core/runLedger/sessionLog.test.ts::sessionKey — one object per thread and agent::*
1: the object — an empty log's tail is 0; unknown-run before an owner; the owner writes and another generation is fenced; a range read in (idx, part) order carries only the attachments its rows reference and is empty past the tail; the owner moves with a later claim, clears itself and no one else; the notepad row exists and is empty; a malformed key, generation, row or index is 400[unit] deploy/cloudflare-memory/sessionLog.test.ts::session log object — the owner fence and the rows::*
2: RunSession accepts an open range, a closed one and broken, and refuses a bad key, a negative or fractional index, a request or range before the seed, an end before its start; the record validates with or without it; the request is the seed's last user turn[unit] src/core/runLedger/sessionLog.test.ts::isRunSession — the range a row and a record carry::*, ::requestIndex — the request is the seed's last user turn::*, src/core/runRecord.test.ts::isRunRecord — the session field::*
2: a finished source run with no written turn has an empty handoff range ending at from - 1; a live open range still needs its committed checkpoint, and a broken range remains unavailable[unit] src/core/dispatch/handoffValidation.test.ts::canonical child handoff validation::accepts a finished source with no written turn and refuses an unproved live range
2: the write-through — a first run starts the log at 0 with the row's session, its seed record lands under the same key so the run is tracked and resumable with no per-run object owned, and its finish closes the range and clears nothing; the next run appends at the tail and its steps land at seedFrom + their index; two agents keep two logs and a ship pipeline has no session; a detach marks the record broken; an adopted run continues its session and an older row its own object; a compaction rides as the row after the step's turns[unit] src/core/runLedger/writeThrough.test.ts::the session log — a run is a range of it::*
2: the client — a claim posts only the history claim; the session routes' bodies, seed and step writes into the log, the range read re-based, the tail read, release; finish releases the owner best-effort and clears nothing; reclaim re-owns a session log or a transcript object by the row; the configured byte budget rides every owner claim, clamped[unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::claim posts the run under the store key with the bearer and nothing else…, ::WorkerRunLedger::the session routes…, ::WorkerRunLedger::append with no events posts nothing; finish clears nothing…, ::buildRunLedger — the client for the configured history (run-history item 35)::carries the configured session log byte budget…
2: a message with no parts is not a turn — the mirror reports no step and takes no index for it and the results pending ride the next turn; the bridge hands the mirror no message for a failed model call, including an aborted message with partial text; a re-attach that catches up on one mirrors its first step right after the transcript it resumed from, no turn without parts[unit] src/core/harness/pi/mirror.test.ts::PiMirror — step records as the runner writes them::an assistant message with no parts is not a turn…, src/core/harness/pi/bridge.test.ts::turns, narration and the answer — the loop's rules::a model call that failed is a provider error and no message…, ::turns, narration and the answer — the loop's rules::keeps local aborts and unknown terminal results outside the provider-failure types, src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::re-attaches to a pi still running…
2: the reference ledger runs the session protocol — tail, unknown-run before the owner, fenced writes, the range and tail reads, finish releasing the owner and keeping the rows, a reclaim re-owning the log[unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::session log: the tail of an empty log is 0…
2, 6: rows read from a log index re-base to a conversation from 0; a compaction row occupies an index, joins no message, names the message it precedes and its kept message when keptFrom is in range[unit] src/core/runLedger/transcript.test.ts::the session log's rows — a base index, and compaction rows between turns::*
3: a row with a session reads the log from its seedFrom, one without reads its own object; the plan carries the compaction rows[unit] src/core/runLedger/resume.test.ts::planResume::transcriptSource: a row with a session reads the log from its seedFrom…, ::planResume::the plan carries the transcript's compaction rows…
4: the tail cut walks newest first and returns the oldest turn that still fits, whole turns only, nothing when the newest alone exceeds; the object answers the newest whole turns oldest first with from, and nothing at the tail when none fits[unit] src/core/runLedger/sessionLog.test.ts::tailCut — the newest turns within a byte budget, whole turns only::*, deploy/cloudflare-memory/sessionLog.test.ts::session log object — the tail read a follow-up seeds from::*
5: the policy field defaults to 200 MiB and clamps into [16 MiB, 2 GiB]; the trim plan takes the oldest tool results until the excess is covered and never text; the marker keeps role, call id and error flag and names the drop; a row's references are its own and its nested parts'; the object replaces the oldest results first, keeps every text, indexes the marker, clamps the budget on the owner route; a trimmed result's sole attachments go with it so the log converges, one another row still shows stays; the reference ledger holds a session to the claim's budget by the same rule[unit] src/core/runRecord.test.ts::clampRetentionPolicy — the session log's byte policy::*, src/core/runLedger/sessionLog.test.ts::planSessionTrim — the byte policy drops the oldest tool results first::*, ::droppedToolResultRow — the marker that replaces a dropped tool result::*, ::attachmentRefsOf — the attachments a stored row references::*, ::the default byte policy::*, deploy/cloudflare-memory/sessionLog.test.ts::session log object — the byte policy and the drop::*, src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::session log byte policy: over the budget carried on the claim…
6: the bridge hands the entry with the summary, the size and pi's kept id; the mirror writes it as its own step after the pending results and the next turn lands after it; piSessionFile renders it on the branch, naming the kept message's entry when known and pi's id otherwise; a restarted pi's session carries the entries where they sat[unit] src/core/harness/pi/bridge.test.ts::the notes — compaction, harness errors, dialogs, the unknown::a compaction is a \compacted` note…, src/core/harness/pi/mirror.test.ts::PiMirror — step records as the runner writes them::a compaction is its own step report…, ::PiMirror — step records as the runner writes them::a compaction with nothing pending…, ::piSessionFile — a compaction entry where pi wrote it:😗, src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::the explicit replacement protocol preserves compaction entries at their original positions`
7: the drop decision, per candidate on the two facts re-read at its drop, takes a session only when no kept run names it and no live run holds its thread and says which fact kept the rest; a claim registers the session and a finish stamps session_key and refreshes the finish time and bytes; the sweep drops the emptied session's object owner-first and its registry row, and leaves a kept session and a live thread's sessions whole; a run gone live or a record landed between the candidate list and the drop keeps the session and its registry row[unit] src/core/runLedger/sessionLog.test.ts::sessionsToDrop — the sweep's decision, per candidate on what it re-reads at the drop::*, deploy/cloudflare-memory/runLedger.test.ts::the sessions registry and the sweep's drop of a session log::*
8: a replaced row is stored once and the index follows it — the old text gone, the new one found; each row kind indexes its text and nothing else[unit] deploy/cloudflare-memory/sessionLog.test.ts::session log object — the owner fence and the rows::a replaced row (a new generation overwriting a zombie's late row) is stored once and the index follows it…, src/core/runLedger/sessionLog.test.ts::rowKind and textOfStoredRow — what the index sees::*
9: the seed budget is 60,000 tokens at four characters a token; the seed reuses the tail's rows as they are, then the user lines after the previous run's end (the bot's lines, the lines that run saw and a line with no clock left out), then the request, and names the rows reused; the tail is cut forward to the first user text turn and that turn's results for calls before the cut are dropped with a note; thinking is dropped and every row keeps its place; calls in flight are settled with a note as new rows; the tail begins after the newest compaction row and carries its summary for the prompt; a broken predecessor gets the gap marker; an unknown previous end, a newest turn over the budget and a tail with no user text turn each yield the request with a note; an empty log is no session; the request keeps its attachments[unit] src/core/dispatch/seed.test.ts::sessionSeed — the tail, the lines since, the request::*
9: a request a run's record marks refused under the provider's policy is left out of the tail — its row keeps its place with the stand-in, the rows around it stay, the log range is exact and a note counts it; two are both left out and counted; a row before the cut, a row that is not a user turn and a row named twice count for nothing, and after a compaction row the request is found by its log index; no refused request leaves the seed byte-identical; sessionSeedFor reads the refused rows off the thread's page by the agent[unit] src/core/dispatch/seed.test.ts::sessionSeed — the tail, the lines since, the request::leaves out the request a run's record says the provider refused under its usage policy…, ::two refused requests are both left out and counted…, ::no refused request leaves the seed byte-identical, src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::the requests the thread's records mark as refused under the provider's policy are read off the page…
9: refusedRequestsOf names the request row of every finished run of the agent whose record says failure: policy_refusal and names its session, and nothing for a run failed otherwise, another agent's, a live one or one without a session[unit] src/core/dispatch/thread.test.ts::refusedRequestsOf — the requests the provider refused under its usage policy::*
9: the thread is read once — one page of its newest runs, live and finished, under no visibility predicate — and a failed read is no thread with one warning; the sticky agent by transcript is the newest run's agent when it finished with a session log, whatever its preset, and nothing for a live or sessionless newest run; the previous run of an agent is its newest finished run with a session, past a live run and another agent's, with its end and whether its log is broken[unit] src/core/dispatch/thread.test.ts::readThread — one page of the thread's newest runs::*, ::stickyAgentOf — the thread's agent by transcript::*, ::previousRunOf — the agent's previous finished run::*
9: a seed that reuses the log's tail appends only what is new — seedFrom the cut, range.from the tail, the reused rows not written twice, a step and the record counting from the cut, the resume read from seedFrom the conversation the model saw; named rows that do not end at the tail are written whole as new rows with one warning[unit] src/core/runLedger/writeThrough.test.ts::the session log — a run is a range of it::a seed that reuses the log's tail appends only what is new…, ::a seed whose named rows do not end at the log's tail is written whole as new rows…
9: the sticky agent by transcript joins the user turns' model and effort, a directive wins over it, and an agent: token in the history alone establishes nothing[unit] src/core/dispatch/resolve.test.ts::resolveRun — the (agent, model, effort) triple::the thread's sticky agent by transcript joins the user turns' model and effort; a directive still wins over it, ::a follow-up without directives runs on the agent the thread established — the caller's read of the thread's transcript; an agent: token in the history alone establishes nothing
9: each Slack history turn carries its time in epoch ms off the message ts; a ts that does not parse leaves the item without one[unit] src/channels/slack.test.ts::SlackIO.history — thread reuse and concurrent attachment downloads::uses the thread the handler already fetched…, ::a message whose ts does not parse carries no time; the item is kept
9: runs list --thread <key> lists one thread's runs, live and finished, newest first, and nothing from another thread[unit] src/core/commands/runs.test.ts::runs.list::\--thread` lists one thread's runs, live and finished, newest first, and nothing from another thread`
9: the original requester continuing a budget-ended general source lookup in the same thread seeds only the original recorded question plus the continuation reply, never stale tool results; another requester cannot continue it[unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::a budget-ended source answer continues in the same conversation for its requester without replaying the cut source or another sender's access, ::a second budget-ended continuation keeps the original question instead of seeding continue twice, ::recovers the original question when several budgeted continuations exceed the thread page, ::refuses a repeated continuation when its original question is unavailable
9: end to end through dispatch() — a follow-up in a thread whose newest run is a coding run on pi continues coding without a directive (agentSource: sticky, no router call) and seeds from the session log: the pi harness is handed the log's tail, the line written since and the request, the record says seed: session with seedFrom inside the earlier run's range and range.from at the tail, its context events are the tail's text turns; a log that cannot be read seeds from the channel with a seed note naming it[unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::*
9: the runs since — the finished runs before the agent's previous continuable run on the page, oldest first, another agent's run and a coordinator's child alike, a live run and the previous run itself left out; every finished run when the agent has none there; nothing newer is an empty list[unit] src/core/dispatch/thread.test.ts::runsSince — the finished runs newer than the agent's previous run (session-log item 9)::*
9: the artifacts block — the cap is six runs within 6,000 tokens; the last description's TL;DR; every artifact kind rendered as data in one order and what a carried artifact lacks said out loud; a record carrying none renders nothing and a view carrying none is never read; the heading names the window and the runs render oldest first with their ids; no records is no block; the run cap keeps the newest and says how many were cut; the byte budget keeps the newest whole runs and names the newest run when it alone is over[unit] src/core/dispatch/threadArtifacts.test.ts::threadArtifacts — the thread's typed artifacts since the previous run::*
9: the read — a coding follow-up reads the runs newer than its previous run, the runner's review child included, with their events, and hands the block and one seed note naming them; an agent with no previous run reads every finished run carrying artifacts, oldest first, a live run and a run carrying none never read; nothing since is no block and no note; a record that cannot be read is left out with a note and the rest still ride; a cut is in the note[unit] src/core/dispatch/threadArtifacts.test.ts::threadArtifactsFor — the records read off the thread's page::*
9: the block sits right after the notes block and before the config block and the REVIEW TARGET block, follows memory without notes, and is absent when none was handed[unit] src/core/dispatch/provision.test.ts::composePrompt — the system prompt for the first turn::the thread's artifacts block (session-log item 9) sits right after the notes block…, src/core/reviewRound.test.ts::makeSystemComposer (head-pinned composition with an explicit AgentDef)::the thread's artifacts block follows the notes block and precedes the config block…
9: end to end through dispatch() — a coding follow-up is handed the verdict a review run recorded since its previous run, the findings by id in the prompt after its notes and never in the conversation, the record's seed unchanged and its seed note naming the run; a review follow-up is handed the coding run's dispositions by finding id with the pull request it edited, before its REVIEW TARGET[unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::a coding follow-up is handed the verdict a review run recorded in the thread since its previous run…, ::a review follow-up in a thread whose coding run recorded dispositions is handed them by finding id…
8, 10: the object's search answers hits in relevance order with turn, part, role, kind and text and the gap markers the hits straddle; the notepad is written whole under the owner's fence, read back with its time, unknown-run before an owner, fenced for another generation, 400 over the size; a wordless query, a bad limit and a textless write are 400[unit] deploy/cloudflare-memory/sessionLog.test.ts::session log object — the search recall reads and the notepad notes writes::*
10: the reference ledger's search scores the query's words, orders by score then newest, names the gaps between hits, and answers nothing for a wordless query or an unknown log; its notepad is written under the owner's fence and read back[unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::the search answers hits in relevance order with turn, role and kind…
10: the client posts the search, notepad read and notepad write routes with their bodies and answers what the Worker sent, nothing when it sent none, a fenced write as a result[unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::the search and notepad routes…
10: the run knows its place in the log and the write-through reaches the log's range read, search, notepad read and notepad write under its generation[unit] src/core/runLedger/writeThrough.test.ts::the session log — a run is a range of it::a thread's first run of an agent starts the log at 0…
10: recall searches in relevance order and answers turn, role and a one-line snippet with the gaps straddled; the default limit is five, no hit says so and points at the notepad, a first run is told the log begins with it; a turn is read whole by its index and a missing one named; a denied read is empty, no capability says so, a call with none of query, turn or assets is refused by name; a hit whose turn names one of the thread's files carries the file's key and where it is for this run and a hit naming none carries nothing — the join by name at a word boundary and under the workspace's spelling, the catalogue read only when a hit carries an attachments line or an attach_file call and the run has an admitted catalogue reader; assets: true lists the thread's files with their keys and where each is, an empty catalogue and an unavailable catalogue reader each say so, a denied read is an empty list; recall is side-effect free and notes is not; notes replaces the notepad whole, says the size, publishes the notes event redacted, refuses over 8 KiB naming the size, reads back or says empty, and a fenced write says not saved; the capability wraps the row's session over the write-through, carries the catalogue read and the workspace paths only when handed them, and is absent for a run without a session[unit] src/tools/session.test.ts::recall — the caller's own session log::*, ::notes — the agent's notepad for this thread::*, ::sessionCapabilityFor — the capability the dispatcher builds for a run with a session::*
10: dynamic reads of the current session's turns, search hits and notes admit the latest canonical dependencies after reading bytes and before exposure; denied or failed admission hides only that read, and conversation capture fails explicitly instead of supplying an empty replacement[unit] src/tools/session.test.ts::sessionCapabilityFor — the capability the dispatcher builds for a run with a session::admits dynamic own-session bytes after reading and before exposing them, ::withholds dynamic own-session bytes when dependency admission fails or throws
10: a focused parent brief retains original request, admitted user corrections, structured attachments and notes; assistant/tool exploration stays in original recall; protected text over its UTF-8 budget is refused whole and attachments retain their own limits[unit] src/core/dispatch/handoffBrief.test.ts::focused parent brief::*
10: the session tools are in full, readonly, explore, web, assistant, conductor and orchestrator. Research, general and conductor children recall exact omitted parent evidence inside the frozen source range and save their own notes, with parent source dependencies retained. A conductor child still cannot spawn another child. Workspace and read-child prompts explain durable notes and recall; Ship does not promise these tools[unit] src/tools/github.test.ts::*, src/tools/skills.test.ts::*, src/agents/registry.test.ts::the workspace prompts name the image toolchain::workspace and read-child prompts explain durable notes and recall; Ship does not promise those tools, src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::a %s child's model receives a focused parent brief and recalls omitted evidence within the frozen source range; both records retain seed provenance, src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::\harness: { conductor: pi }` runs the conductor on pi as a child of the bot: coding is refused, a %s child receives a focused parent brief and recalls exact omitted evidence, await_runs returns the child's reply, and both records retain source provenance`
10: the prompt carries the notes, the compaction's summary and the admitted automatic-intake catalogue's files (one line each, after the other two) as one block after the memory block and before the config block, each only when present; nothing without any of the three[unit] src/core/dispatch/provision.test.ts::composePrompt — the system prompt for the first turn::a follow-up seeded from its session carries its notes and the compaction's summary…, src/core/reviewRound.test.ts::makeSystemComposer (head-pinned composition with an explicit AgentDef)::the notes block follows the memory block and precedes the config block…
10: a main run with declared slack_context intake receives no prior-run file catalogue in its initial prompt or recall { assets: true }, even with an artifact store; source reads belong to the bound tool and its receipt contract[unit] src/core/dispatcher.test.ts::dispatch::excludes prior-run file catalogues from main source intake; source-read proofs in slack-context-tools.md
10: the seed read brings the notepad back for the prompt, never as a row; an empty one is none; a notepad read that fails is a note and the seed stands; an empty log reads none[unit] src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::*
10: after every compaction that carried a summary pi is steered with the notepad as it stands and the reach recall gives; without a notepad it is told the notes are empty and how to keep them; a failed compaction steers nothing; a notepad read that fails costs the steer its notes and never the run, with a harness_error note[unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::after every compaction pi is steered with the notepad…, ::a notepad read that fails at compaction time costs the steer its notes, never the run…
10: a notes event validates as a narrative event and renders as a note row of kind notes; the friction analyzer reads it as narrative[unit] src/channels/runTimeline.test.ts::createRunTimeline — artifact::a notes event is a note row carrying the notepad's text under the kind \notes``
10: the page keeps the note row's kind, so the notepad is told apart from a lifecycle note; the notepad renders as a Notes block with its Markdown under the write's clock, never as a ⏱ line, the newest write open and an earlier one folded as superseded, while a run note stays the ⏱ line[unit] web/src/lib/runPageModel.test.ts::notes and stops::a notes event is a note row of kind \notes` carrying the notepad whole; a run note carries its own kind, web/src/pages/runPage.test.ts::RunPage — history mode::a notes event renders as a Notes block with its markdown — not a ⏱ line — and only the newest write stands open`
10: workspace and read-child prompts ask for the notepad's shape — a ## heading per section (Done, In progress, Next, Facts), one bullet per item, one line per bullet, never one paragraph[unit] src/agents/registry.test.ts::the workspace prompts name the image toolchain::workspace and read-child prompts explain durable notes and recall; Ship does not promise those tools
10: end to end through dispatch() — the follow-up's prompt carries the notes the previous run kept, the pi harness can read the notepad for the steer, and the two session tools are relayed[unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::a follow-up in a thread whose newest run is a coding run on pi…
11: ordinary legacy and context keys, including unresolved-agent sentinel keys, unit working keys across transport threads, and matching admitted live keys search under the reader predicate; older pages are searched, conflicting live identities (including typed coordinator unit/attempt) and private worker runs remain excluded, and pre-session records grant only the original ordinary key[unit] src/core/runsService.test.ts::RunsService.searchSession — a person's search over one session's log::*
11: the search answers the matching turns in relevance order with role, snippet, the run whose range holds each and the marker a hit lies past, limit capping them; a reader outside the predicate, a session nobody ran, a key that is not one and a process without a ledger are one empty answer with the log never asked[unit] src/core/runsService.test.ts::RunsService.searchSession — a person's search over one session's log::*
11: runs search <session> <words…> on the machine surfaces — hits with their run, snippets wrapped as untrusted, --limit, no hits outside the predicate, a malformed key naming session, no chat surface[unit] src/core/commands/runs.test.ts::runs unit / runs children / runs search — the unit is the reading unit::runs search answers the matching turns…
11: a snippet is one line of at most 300 characters, cut with an ellipsis[unit] src/core/runLedger/sessionLog.test.ts::snippetOf — a hit's text as one line::*
11: the unit page's search box asks the route for one session at a time and lists the hits with their turn, role, the unfenced snippet and the round and thread of their run, opens a hit's run in place, names the gap a hit lies past, and says so for a hit no run holds, for no hits and for a refused search; the fence comes off a wrapped snippet and leaves an unwrapped string as it came[unit] web/src/pages/unitPage.test.ts::UnitPage — the unit is the reading unit (item 28)::the search box asks the route for one session at a time…, web/src/pages/unitPage.test.ts::UnitPage — the unit is the reading unit (item 28)::no hits and a refused search each say so in place…, src/core/untrusted.test.ts::unwrapUntrusted — the body of a fence, for a surface that renders text as text::*
11: a hit lands on the step its turn lives in — the fold reads the record once and draws that one step with the run page's block at the run page's anchor, scrolled to and linked on the run's page; a turn no event names lands on the step that wrote the row before it; the request and the reply are named, not drawn; a record from before the field lands nowhere and the fold opens as before; ?open=&turn= lands on first paint; the model's rule over stamped rows, a quiet call's step and a record without stamps[unit] web/src/pages/unitPage.test.ts::UnitPage — the unit is the reading unit (item 28)::a hit lands on the step its turn lives in…, web/src/lib/runPageModel.test.ts::landing — the step a session-log turn lives in (session-log item 11)::*
11, live: a search over a real unit's coding session finds a turn and names its run[agent] (human-gated.) After a ship on the deployed bot, runs search <unit thread key>:coding <a word the coding run used> from the CLI or GET /api/runs.search?session=…&query=… — expect a hit naming the coding run's id, whose page shows that turn.

Trusted source metadata ​

The thread's shared log and a run's working log share the ledger interface and have different writers. Connector events and published reports enter @thread through appendThreadTurn, keyed by their durable event identity. Working sessions retain run/generation fences and exact transcript indices. Unit claims and seed reads use the same canonical lane key; ordinary conversations retain their agent working logs. A new claim stamps RunSession.threadSession, so a replayable legacy migration can distinguish old rows from connector/report events already projected into the shared log. Migration reports the original keys and omitted turn/part counts. Legacy logs contribute only independently verified ingress text whose original row, actor and bytes match. Actor stamps and absent metadata do not prove text safe; unproven and derived source content stays behind authorized references with explicit omission counts. A copied legacy working seed preserves its actors, notes and trusted source metadata, but reuses no indices from the old key. Initial notes are saved under the new owner's fence before its model starts.

Bounded handoff references name the source records and session objects required by a child, including note snapshots, source-read archives and artifact producers. Ordinary retention selects the active history first; only those retained holders and live holders pin their explicitly named existing sources. Pinned records do not become new roots, and missing or explicitly deleted sources are never recreated. The core retention rules live in src/core/runLedger/contextRetention.ts; the Worker registry applies those rules through its durable owner, thread registry and reference index. Workerd tests cover holder retirement, explicit source deletion, and expiry; deployment remains a separate release step.

Slack source dependencies live in the session object's existing metadata, separately from model-authored rows and summaries. For source-bearing sessions, transcript, attachment and notepad writes require both the owning run ID and generation; a delayed earlier run in the same process is fenced. The fenced source write must acknowledge that metadata was saved before a source read returns content. Source readback must retain the known binding and every known context dependency both before and after the run-state acknowledgement; external revocation in either window withholds delivery, even when the previously merged envelope or context was unknown. Deliberate revocation recording and unrevoked unknown bookkeeping keep their existing acknowledgements without manufacturing known provenance. Concurrent source saves within one run execute in order across read, merge, write, readback and state acknowledgement, so each caller retains the earlier dependency union. A queued save freezes its caller evidence and never retries a prior uncertain write. A fresh known source cannot gain delivery acknowledgement from a revoked or mismatched source envelope, or revoked context. Revocation recording remains supported. Tail reads include it even when every transcript row exceeds the tail budget. A monotonic flag from typed external-source tool calls survives the same cuts and keeps those sources out of prompt replay. Legacy sessions without metadata remain unknown; transcript content cannot establish completeness. The source receipt contract and exact-ID revalidation proofs are in Slack context tools.

Receipt persistence through cuts is separate from prompt replay: the final main-agent audience gate still refuses compacted or truncated Slack-only replay. See the explicit prompt replay limit.

Whole-context dependencies occupy the session object's existing source metadata. The bounded envelope retains original run identities, repository-access names, Slack audience bindings and MCP run/action/response references through derived context; repository access is revalidated against current public status or the current grant; union is monotone and absence means unknown. A frozen handoff snapshot carries its envelope and canonical digest; the latest source envelope must still include it, and each leaf is independently revalidated before use. Overflow or conflicting evidence leaves an explicit unknown state, never a clean context.

CriterionProof
4: the newest compaction remains readable outside the selected tail[unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::keeps the newest compaction checkpoint readable when it falls outside the tail, deploy/cloudflare-memory/sessionLog.test.ts::requester target checkpoint independent of session transcript::returns the newest compaction independently of a tail that cannot hold it, src/core/runLedgerWorker.test.ts::WorkerRunLedger::the session routes: tail, owner with the configured byte budget, seed and step writes into the log at their indices, a range read re-based to the range, the tail read, release
9: recovery notices name only the prior context actually loaded, say when no saved summary is available, and show the same omitted ranges as the model prompt, excluding the recovered request[unit] src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::reports only the prior context actually loaded when no saved summary is available, ::recovers legacy context without a summary and preserves refused-request substitution, ::reads the log's tail under the budget by the thread-and-agent key, then the notepad, which rides the seed for the prompt and never as a row
9: omitted-range telemetry binds numeric ranges to the source session, including a copied legacy log, excludes source text and is absent for a complete view[unit] src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::logs omitted ranges by source session without source text and emits no omission event for a complete view
9: empty-tail continuation preserves the exact request, actor, summary, notes and complete recent exchange; legacy recovery names gaps and retains policy refusal substitution; unavailable or oversized protected context refuses continuation[unit] src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::continues a long run from its exact request, independent checkpoint, notes and complete recent exchange, ::recovers legacy context without a summary and preserves refused-request substitution, ::refuses continuation when the canonical request cannot be recovered or protected context exceeds the budget, ::retains original and tool-result image payloads under their attachment limits instead of the text-history cap
Publication validation names its failed structural check without retaining source content. Unknown or changing context, missing execution ownership, missing source receipts, incomplete transcripts and mismatched result bytes remain distinct refusals; proved execution stays allowed.[unit] src/core/dispatch/contextAccess.test.ts::publication of admitted context::names unknown, changing and unavailable context separately; src/core/dispatch/contextAccess.test.ts::message-bound context access::distinguishes an absent execution controller from a missing live owner; ::checks source access, receipts, session and bytes…

Configured session persistence must acknowledge appends before a reply becomes visible; an operational failure never behaves like the explicit no-ledger implementation. Registry command output is stored exactly, including runless commands and settled follow-ups, with explicit unknown dependencies until its producer has complete source metadata. A confirmed command's result uses the consumed approval's identity, separate from the original message's approval offer; its settled follow-up keeps that same identity with a distinct suffix. Each shared row carries its own immutable envelope; operator recall omits only unproved rows and keeps unrelated proved rows. The cumulative envelope stays conservative for notes and compaction.

Fresh contexts use a stable :@context-v1 suffix on the existing session key, including the shared thread log. Older logs remain immutable and reachable by their original run bindings. Reattached runs retain their exact stored keys; a new request only copies old content after its complete original dependencies are revalidated, otherwise it starts clean and reports the omitted context. The epoch is a session identity, not a reset of unknown or revoked metadata.

When a new request cannot reuse a session with run-bound GitHub receipts, it writes to a separate log under the same canonical scope plus :run:<original-run-id>. This prevents the rejected log's cumulative receipts and notepad from entering the fresh run. The old log remains unchanged, while the new run can save notes and recall its own transcript. Later requests use the latest recorded log in their agent/unit scope when its context can be verified; a full recent page without that scope requires the existing bounded complete history read before choosing a key. Resume, attaching restart and request-only restart keep the original run's exact recorded key. Request-only recovery takes it from the owned carry or a verified original record; recovery that cannot prove the key holds rather than returning to a rejected execution log. Legacy unkeyed recovery keeps its existing canonical path when that path carries no foreign execution receipts.

| 9, 10: the seed reads transcript, then notepad, then fresh cumulative metadata from the same canonical key; the notepad remains prompt context rather than a transcript row | [unit] src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::reads the log's tail under the budget by the thread-and-agent key, then the notepad, which rides the seed for the prompt and never as a row, ::reads an explicit working lane and ignores another session's refused request indices, ::captures the cumulative dependency envelope after reading mutable notes | | A fresh request cannot inherit rejected execution receipts or notes through cumulative session metadata; the old log remains intact, the fresh log supports notes/recall, verified follow-ups find the recorded scope beyond the recent page, and every recovery preserves its exact key or holds missing/foreign proof. | [unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::starts a fresh review without importing an earlier run's execution receipts or notes; ::continues a verified session after an earlier review moved to a fresh log; ::keeps the isolated session on a request-only restart…; ::holds a request-only recovery when the original session cannot be proved…; src/core/dispatch/sessionIdentity.test.ts::run session identity comes from the admitted unit::preserves the recorded isolated review log on restart; src/core/dispatch/reattach.test.ts::prepareRestartTurn: the request runs again as its own dispatch::carries raw original baseline and target across same-ID restart without hiding malformed evidence or an owned earlier push |

Retained context roots and keyed append integrity ​

When the Worker refuses a thread append, its client retains the observed HTTP status, ok, contextSaved, appended and a closed refusal reason. Absent and invalid reply fields remain distinct. The thread append error carries only these observations. It contains no report text, session or row key, arbitrary server reason or exception body. An observed appended field does not prove a committed row or authorize a retry. The existing success and context acknowledgment predicates remain in force.

Each refused reply emits one storage.session.append.refused JSON event at the client boundary. It carries the same closed observations, the frozen request digest and the existing generated request ID when available. It includes no raw thread or row key. Success emits no refusal event; a failed log sink cannot change the original result or request count.

CriterionProof
Refused Worker replies retain closed status and acknowledgment observations through the actual thread append call, without source bytes or an inferred result. Ordinary success and transport failures preserve their behavior and submit once.[unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::keeps closed thread append observations: $label; src/core/runLedgerWorker.test.ts::WorkerRunLedger::keeps successful thread append and transport failure behavior
A refused reply emits one closed request-bound event even when its caller drops the error. Successful appends stay quiet, and a throwing log sink preserves the refusal and single request.[unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::logs one closed request-bound append refusal even when its caller drops the error; src/core/runLedgerWorker.test.ts::WorkerRunLedger::keeps the refused append result when its structured log sink throws
A keyed report can commit before shared-session registration refuses a missing canonical source. The HTTP response retains appended: true with context-source-unavailable; keyed readback proves the stored report and its original context remain. Exact replay adds no second row and does not repair the source. A matching live source or retained archive header can permit registration even when no full archive is readable; registration alone proves no terminal or execution authority.[worker] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::preserves the committed report when source registration sees %s history

The append metadata and any embedded row context must match exactly. An absent request envelope is unknown; a row cannot upgrade it. Each accepted row identity stores a SHA-256 digest of its original parts and normalized context. Byte trimming preserves that digest. A changed payload or dependency set is refused; a historical row without an immutable digest cannot attest a replay.

The existing context_refs index supports canonical run, shared-session, and unit holders. A shared log remains a root for its own conversation lifetime; each append retains exact external source references before the API acknowledges persistence, while replacing superseded ordinary checkpoint roots. A retained original unit's immutable context capsule pins its sources from admission, including while its source run is live and after the unit ends, until the canonical unit is removed. Thus retained units can intentionally keep source archives beyond ordinary run retention. Pinned source records do not become new roots. Explicit source deletion remains authoritative, and neither a retry nor a reference recreates it. Missing source/index persistence produces an explicit gap without a success acknowledgment; retries reuse the same row identity.

readSessionEntry(key, rowId) / POST /runs/session/entry is an internal keyed read over existing session rows. It returns exact stored parts by immutable event identity, or no entry when absent or byte-trimmed. Report retries reuse these bytes instead of recomputing text; this read does not authorize source disclosure.

A canonical ordinary-context checkpoint may replace equivalent original-run access origins only after the current owner’s exact admitted seed, system, notepad, source revision and source authority are verified at storage. Its sealed alias window contains at most 128 ordinary identities, newest first, with each original checkpoint hash. These aliases prove identity without retaining the original archives. Ordinary archives follow the configured age, count and byte limits; the current checkpoint retains its own exact seed and external dependencies. Shared logs replace superseded ordinary checkpoint roots. Prepared or stale receipts grant no access. External dependencies and frozen child/unit source ranges remain independently pinned and unchanged.

Readers normalize a row through a currently authorized checkpoint before checking the resulting complete dependency closure. Ordinary rows outside its alias window may be omitted; an old row cannot fill a missing proof from matching identity text. The current sealed summary and notes continue into the next run. Automatic archive expiry preserves the bounded alias seal until its holder expires; explicit source deletion revokes aliases too. Saved memory remains usable only while its canonical source proof is retained; ordinary archive expiry does not create a separate memory retention root.

Checkpoint normalization, recovery reads, archival and initial unit-seed acknowledgment keep their input gate while they await source proof. A source refusal or unexpected fault leaves the callback as data and rejects the caller after the gate closes. This preserves the source hold and serial order without resetting the shared object for a rejected operation. The existing gate deadline remains unchanged.

Each failed callback attempts one checkpoint.gate_refused diagnostic after the gate closes. It names the operation, run and stored owner generation observed inside the gate; an absent live owner is null. Error class labels and the message are fixed. Exception prose, stacks and causes stay out of the diagnostic. A diagnostic sink failure cannot replace the original error.

CriterionProof
A source controller saving an earlier snapshot preserves the committed ordinary-origin normalization and every newly acquired external source leaf.[unit] src/core/runLedger/writeThrough.test.ts::open — claim and seed::keeps covered origins normalized when a source controller saves its earlier snapshot
A checkpoint requires the live generation and an acknowledged seed; missing or stale proof cannot change the stored context[unit] src/core/runLedger/inMemory.test.ts::ordinary context checkpoints at the storage boundary::requires a live owner and never normalizes an uncommitted seed
A refused SessionLog checkpoint leaves RunHistory's input gate without an exception, preserves its pending receipt and source hold, and can recover after the source becomes available[unit] deploy/cloudflare-memory/sessionUnification.test.ts::checkpoint input gate failures::keeps a real pending-source refusal outside the %s gate and preserves recovery
An unreadable transcript cannot acknowledge a checkpoint or unit seed; an unexpected storage fault still rejects its caller and permits recovery without an escaped gate callback[unit] deploy/cloudflare-memory/sessionUnification.test.ts::checkpoint input gate failures::rejects an unreadable transcript outside the %s gate without acknowledging it; ::rejects an unexpected storage fault outside the gate and keeps the original checkpoint recoverable
A competing state write waits for remote checkpoint proof, then cannot replace the canonical receipt with an invalid one[unit] deploy/cloudflare-memory/sessionUnification.test.ts::checkpoint input gate failures::serializes a competing state write until the remote checkpoint returns
Refusal diagnostics attribute all four paths without copying exception text; the original error object and recovery survive an unavailable diagnostic sink[unit] deploy/cloudflare-memory/sessionUnification.test.ts::checkpoint input gate failures::*
At least 384 ordinary continuations keep a sealed window of at most 128 identities, preserve order and original marker hashes across reconstruction, and never pin archives merely because they are aliases[unit] src/core/references/contextCheckpoint.test.ts::ordinary context checkpoint::rolls ordinary identity evidence after 128 runs without growing a restored checkpoint; [unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::bounds 384 ordinary continuations across retention and reconstruction while frozen units keep their originals
Controller work-read receipts retain exact tool/call/input/result hashes in the producing conversation's private full record; malformed or foreign receipts are refused and public summaries omit private work IDs[unit] src/core/runRecord.test.ts::keeps work-read receipts private and bound to their producing conversation; [unit] deploy/cloudflare-memory/runs.test.ts::retained source archive storage::keeps exact work-read evidence out of summaries and rejects corrupt private evidence; [unit] src/core/runsService.test.ts::RunsService.getRun::never exposes internal source archives or handoff context through any persisted projection
A unit input receipt requires the current owner, acknowledged initial step and exact retained transcript/system hashes. Its seed range becomes immutable before acknowledgment; the original receipt and work reads survive adoption and finish without appearing in public summaries. This proves persisted input, not provider execution.[unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::acknowledges only exact committed unit input and keeps its private receipt immutable through adoption and finish; [unit] src/core/runLedger/writeThrough.test.ts::open — claim and seed::archives acknowledged unit input and exact work reads from live state at the real finish seam
An admitted unit protects the exact frozen source range and its attachments from later byte trimming and mutation until its canonical holder is removed; an already trimmed range cannot be admitted[unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::keeps a queued unit's frozen tool results until its canonical unit is removed
CriterionProof
Acknowledged original promotion admits the deterministic session seed projection while retaining and authenticating raw rows. Empty-tail, over-budget and copied-legacy seeds with zero reused turns confirm fresh input at the observed tail; stale pointers keep the existing fresh-input fallback. Foreign projections remain held.[unit] src/core/runLedger/writeThrough.test.ts::original reservation promotion custody::confirms supported session projection while retaining the raw original: %s
Concurrent source saves acknowledge each successful caller only after retaining both exact dependency sets in the session and run state; revoked context, mismatched bindings and foreign owners remain unavailable.[unit] deploy/cloudflare-memory/runLedger.test.ts::concurrent source persistence::*
CriterionEvidence
New handoffs from an orchestrator root retain its run, requester, channel and thread as an explicit owner link. Nested workers keep that original link through saved handoffs and restart. A standalone specialist or an unlinked inherited context does not acquire an owner link.[unit] src/tools/session.test.ts::inherited session context::keeps the original orchestrator owner…; ::does not invent an orchestrator owner…
A claimed orchestrator owner must match the canonical source agent and original source identity. Foreign owner fields and a specialist posing as an orchestrator are refused. Older unlinked context remains readable without gaining an owner claim.[unit] src/core/dispatch/handoffValidation.test.ts::canonical child handoff validation::checks the claimed orchestrator owner…; ::refuses a nested handoff…

Bounded internal Memory parts ​

Internal chunks stay in the existing session object. References participate in pins, shared-reference checks, trim, owner-close orphan collection and object retention drop. Tail budgets charge hydrated logical bytes. A maxBytes: 1 metadata-only read keeps sources and context without claiming model history. Successful ordinary READ projects legacy rows and attachments; unknown or corrupt required bytes are unavailable, not blank text.

Legacy-only search keeps its original FTS5 ranking. Once a log includes internal large parts, bounded whitespace-safe cells use the same FTS5 tokenizer and retained query expression. A logical part ranks by its best matching cell’s bm25 score, then by newest turn/part; it appears once. This is bounded-cell relevance, not a whole-document bm25 claim. An unindexable long token or incomplete required index is explicitly unavailable. No JavaScript token approximation supplies a false match or no-match.

CriterionProof
Partial uploads stay private until the complete row; owner, immutable-content and pinned-row refusals preserve the original payload. Exact owner closure removes only unreferenced chunks; retention drop clears the same object[unit] deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::retains partial chunk uploads until owner closure and preserves published pinned bytes under refusal
Actual FTS Unicode matching, tail/cell boundary text and mixed bounded-cell relevance remain truthful; metadata-only reads preserve the complete version-3 dependency leaf and source stamping[unit] deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::uses actual FTS Unicode matching and bounded-cell relevance while metadata-only source reads stay usable
Byte-policy trimming projects the canonical tool-result marker and removes only its unshared internal chunks[unit] deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::trims an oversized internal tool result and its unshared chunks through the existing byte policy

Internal-codec emission requires an exact compatible Memory and bot-reader deployment receipt. Memory rollback must retain codec projection, reference accounting, search and cleanup while any internal row or orphan remains. Legacy bot/CLI success DTO compatibility does not authorize rolling Memory back to an unaware build; typed required-history failure guards are also part of the bot minimum. This source change grants no deployment or activation.