# Recovery journal and checkpoints Recovery records observations; it never replays effects, executes verification, changes ownership, waives evidence or accepts tasks. The store uses its current reconnectable SQLite connection and synchronous transactions. Integrated child send/stop persists an intent before host invocation. Host acceptance is persisted before subsequent activity queries. A repeated request_key (or the same trusted turn/step/rootCallId/callId when no key is supplied) returns the original operation without invoking the host. A conflicting payload under the same key is rejected. Callers must reuse a key to check an ambiguous retry, not generate a replacement key. A tools activation binds the first recovery journal it observes, including a journal present before the first control. Missing, unreadable, closed or replaced journals fail before host dispatch with E_CONTROL_JOURNAL_UNAVAILABLE; an explicit request_key also requires a journal on a new activation. This error proves only that the current call did not dispatch: an earlier same-key effect can remain unknown. Preserve the original retry_key; never generate a new key to bypass the failure. Early unavailable replies include operation.retry_key only when a supplied key passes the recovery text rules or trusted coordinates reconstruct its stable key. They never allocate a fresh UUID to represent an unknown historical key. With neither source, retain any previously returned key separately; the unavailable response cannot recover it. Restoring the original journal returns its pending/replayed operation without another effect. A replacement journal object requires trusted tools reactivation after the controller verifies the original persistent database; the activation never guesses that a fresh database has no earlier effects. Mounted beginControl/finishControl methods must return synchronous durable envelopes. A new intent returns only operation_id, invoke:true, replayed:false, durable:true and status:"pending"; a replay returns the full bound operation with invoke:false. Only a validated new intent permits host invocation. Null, undefined, incomplete envelopes, non-boolean invoke, promises and thenables fail before dispatch with E_CONTROL_JOURNAL_UNAVAILABLE and preserve the computed retry_key without claiming durability. Already-started native and cross-realm promises have their rejections consumed without awaiting or accepting their eventual values. Finish replies must identify the original operation and match the requested outcome and trusted caller/run/action/target. An invalid or unreadable finish reply returns E_CONTROL_OUTCOME_UNKNOWN with the last confirmed intent and original key; the database may already contain an outcome, so inspect that original record before deciding its state. Malformed replies never provide send/stop acceptance receipts or a new-key retry suggestion. Only genuinely detached legacy activations that have never observed a journal and supply no explicit request_key retain their control capability. They explicitly report durable:false, durability:"unavailable" and have no replay protection. A successful ctx.get lookup is authoritative, including normal undefined; a reflective fallback is used only by legacy contexts without get. Explicit null for the published service or recovery journal, and any real primary lookup exception, fail closed. Failed service resolution is not evidence of detached mode. Reconnecting the same TaskforceRecovery object to its original SQLite database preserves the binding; journal identity does not authenticate same-UID database edits. The operation contains trusted direct caller, child, optional verified run/task, captured task generation, process instance, action, timestamps and outcome. Message content, stop reason, raw errors and arbitrary provider error codes are never journaled. A payload digest checks replay consistency and is excluded from diagnostics. Every integrated post-invocation error returns the original durable operation and retry_key with fixed no-replay guidance, even when the host supplies a retry hint. Existing native diagnostic codes/text remain available; ambiguous E_STORE_BUSY is classified E_CONTROL_OUTCOME_UNKNOWN. Raw diagnostics never enter the journal/export. Pending or unknown does not prove delivery failure. A crash after host acceptance and before outcome persistence can leave pending forever; explicit human investigation is required. Opening a second connection or restarting never marks another process abandoned. Missing live agents are unknown. Explicit idle is an instantaneous observation. Stop acceptance and idle never establish process-tree quiescence; tree_quiescent remains null. ## Product tools - task_checkpoint(task_id, summary, next_action?) stores up to 4096/2048 UTF-8 bytes of model-authored recovery context, current owner and generation. Only the same-run root or actual owner can write it. It is not evidence and never restores authority. - task_board(view="timeline", task_id?, limit?, cursor?) returns descending task transitions with a continuation cursor. - task_board(view="recovery", task_id?, limit?, operation_cursor?, checkpoint_cursor?, lifecycle_cursor?) returns controls, checkpoints and scoped lifecycle observations, unresolved verification count, integrity counts and observation gaps. Use continuation.operations/checkpoints/lifecycle for each independent section. - task_child_send/task_child_stop accept optional request_key, bounded to 256 UTF-8 bytes. Keys share the caller's namespace across actions; matching replay never authorizes a second effect. A broken root ancestry does not disable direct-child control. Unattributed operations are visible only through a trusted direct-caller scope, including the recovery view's fallback. They never become visible in every run or get guessed into the caller's root. ## Host interfaces TaskforceRecovery(store) exports beginControl(input, authority), finishControl(input, authority), checkpoint(input, runId, trustedActor), recordLifecycle(input, authority), timeline(input, runId), inspect(input, runId, directCaller?), diagnosticBundle(input, runId). Construct with new; TaskforceStore exposes its instance as store.recovery. authority.sessionId/runId/isRoot are supplied separately from model input. RECOVERY_DDL is applied inside the existing store migration transaction. SQLite triggers append task_event rows atomically with state/owner/generation changes. These rows deliberately record actor_session:null and source:"database_change": owner is not actor, and a direct SQL update does not prove a host identity. Existing task mutation facts retain trusted actor attribution where applicable; checkpoints and control records have explicit trusted actors. Older tasks receive kind:"baseline", history_complete:false. Task history and host lifecycle observations have separate sources; neither claims earlier unavailable history is complete. Adoption carries only NULL audit attached to originally NULL tasks in the same transaction, refuses already-assigned recovery audit, and preserves legacy adoption result keys. Mismatched recovery rows are hidden, counted, and block store acceptance through existing integrity checks. Detached control records without task attribution remain direct-caller scoped. ## Scoped lifecycle observations The tools plugin installs listeners within its Taskforce preset scope. The observer requires both registered preset membership and the existing trusted scope-membership check before reading sessions or the store. Standard, switched-away, and standalone contexts do not acquire observation authority. Context-owned listeners participate in normal reload/disposal. A session_event source means the recorder observed the typed record in the current host session and persisted its safe projection into SQLite. It does not prove the host session backend itself completed a flush; the upstream checkpoint policy owns that boundary. Supported middleware records request/prepared, request/failed and agent/pre-step, preserving exact middleware results/errors. Real agent/error and agent/disposed hooks record only finite coordinates and fixed error codes. Hooks scan at most the latest 256 session records for typed turn/start and turn/end records, deduplicated by trusted session ID and sequence. Terminal reason kinds completed, aborted, blocked, error, max-tokens, interrupted and forked are allowlisted (the host union is extensible, so other values stay null). Interrupted and forked can be synthetic history closures, not live loop outcomes; nested abort reasons, provider descriptions, messages, prompts and raw exceptions are excluded. Missing coordinates, truncated windows, conflicting replay and write failures become explicit observation gaps. A recording failure never changes a model request's result. Lifecycle rows identify the observed session and explicit source (host_hook or session_event); they do not assert a human actor. They include process instance, observed Node/package versions, and a host package version only when native package metadata resolves it. Unknown versions remain null. Broken ancestry records direct-caller-only observations; no root run is guessed. The run recovery view includes lifecycle for the whole run even when task_id filters task-related sections. Disposal means a disposal hook was observed, not crash, inactivity, or process-tree quiescence. A terminal session record describes its own turn and does not prove descendants stopped. Earlier history, host entrypoint, exit status and supervisor evidence can remain unavailable. Native CI checks real disposal on supported installed hosts; its seeded turn/end record validates the wire shape and collection path, not a production crash or an executed model turn. ## Limits and evidence Recovery pages default to 25 rows, accept 1–100 and fit 16384 UTF-8 bytes including metadata and the product tool's success envelope. Sections expose truncation and continuation. Projection accounts for JSON escaping and reserves one row in every populated section: oversized context is replaced by null with context_omitted:true; other oversized text is null with explicit omitted_fields. Numeric row IDs and pagination remain reachable. Omitted identity is unknown, not proof of an unassigned owner. The stored context is unchanged. Embedded NUL input is rejected because native SQLite text retrieval cannot reliably round-trip it. IDs/keys cap at 256 bytes; payload hashes are 64 hex characters. Checkpoint summaries are included only in authorized recovery views. diagnosticBundle excludes them and allowlists metadata: IDs, counts, timestamps, statuses and the small fixed error-code set. It excludes prompts, reasoning, messages, commands, environment, raw errors, paths, logs, request keys and payload hashes. No export uploads anything. The journal is an API boundary, not protection against same-UID database modification. No production crash cause is inferred from synthetic tests. The recorder collects supported scoped lifecycle observations from the host. A production diagnosis still requires the actual incident's durable records and any missing exit/supervisor evidence. Source contracts: both [DSH rc.2](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/packages/core/session/src/types.ts#L201) and [alpha.2](https://github.com/deepseek-ai/deepseek-harness/blob/d743267388641bc76f17c45ce8b4c231aed1d32c/packages/core/session/src/types.ts#L201) define the same terminal kinds. Native installed-package tests separately validate the actual observation path.