Skip to content

Latest commit

 

History

History
85 lines (49 loc) · 19 KB

handoff-runtime.md

File metadata and controls

85 lines (49 loc) · 19 KB

Experimental handoff runtime

Dry-run validates the original prepared target, not a newly resolved replacement. Claude keeps the original PID/generation check; Codex rechecks and compares the original home/writer context. Changed evidence refuses without native submission and never rewrites the prepared binding.

This guide describes the limited experimental handoff implementation shipped in Python v1.1.0 on 2026-10-09 for #181. The frozen Handoff v1 design and its synthetic fixture are unchanged. Publication does not complete every design acceptance gate or certify every platform or live-agent route.

Supported experimental route

Same-machine POSIX Claude inbox submission can use an explicitly initialized, user-owned ledger and a separate private receipt collector. The connected inbox PID, original process birth and registry endpoint must match the selected generation. Same-machine POSIX Codex supports correlation-only queue submission with no receipt capability and null target generation. Required delivered/acknowledged waits refuse before its queue invocation; best-effort ACK/observation requests report unsupported while retaining actual queued facts. No model/session is started. Single-destination POSIX source-streamed SSH supports correlation-only Claude/Codex submission as described below. Codex body-free injection observation, Windows bounded pipes, paired devices and SSH receipt bootstrap remain unsupported. Ordinary opt-out local/SSH behavior and the Codex wake version gate are unchanged.

session-peer handoff init --json
session-peer handoff prepare --to worker --message-file ./private-message.txt --json
session-peer send --to worker --message-file ./private-message.txt --correlation-id PREPARED_UUID --request-ack --wait-for acknowledged --wait-timeout 30 --json
session-peer handoff status --correlation-id PREPARED_UUID --json
session-peer handoff wait --correlation-id PREPARED_UUID --wait-for acknowledged --wait-timeout 30 --json
session-peer ack --receipt - --json
session-peer handoff confirm --receipt - --json

The message file must be a private, owner-only regular UTF-8 file. Use a prepared ID returned by the second command, not the literal placeholder. ACK/confirmation JSON enters only through private stdin. A cooperating recipient chooses whether to submit the delegated receipt under its normal permissions. Receipt authority is not approval to run other tools, send messages, wake a model or read a transcript. No automatic response is inferred.

Experimental handoff sending accepts named/positional message text, a private message file or bounded POSIX raw stdin. Stdin has one original effect cutoff, strict UTF-8 and limits of four million raw bytes/one million codepoints; a stalled pipe or SIGINT refuses before creating an intent, without an invented epoch or submission result. Receipt/confirmation stdin is a separate bounded API. Opt-in multi-host sending remains an implementation gate. Ordinary opt-out stdin and multi-host sending are unchanged. Native effect input uses the shared untrusted-peer body frame, with every external body line quoted; final character limits include that frame and any receipt authority.

Codex preparation records the canonical thread/home plus bounded private original home/writer resolution. Reusing a prepared ID with changed resolution refuses rather than rebinding it. This is not an atomic native incarnation or consumption proof. The ordinary live-writer and explicit inactive-home rules remain mandatory, and the original resolution is rechecked immediately before the single queue invocation. Its version probe and queue share the reviewed storage guard/environment normalizer from #268; effective native configuration layers/daemon guarantees remain that issue's open investigation boundary. The owned POSIX child has bounded raw metadata stdout/stderr and uses the original deadline/cleanup reserve. A complete matching native queue receipt may retain queueId/submitted facts after timeout or abnormal exit; absent, wrong-target, partial, invalid UTF-8 or overflow output remains unknown even at exit 0 and never retries. Diagnostic stderr never supplies native evidence. No app-server/body/transcript API is called.

Before minting authority, the runtime checks its exact protected script/interpreter and probes that script's ACK command. Private effect input names that exact isolated Python receipt-only handler and ledger, not an unverified session-peer from PATH; the raw capability remains on stdin. Receipt/confirmation input has one five-second EOF deadline and a 4096-byte limit. If storage fails after native submission, the output retains independently validated target/character/dry-run facts, omits unprovable handoff/ACK and forbids retry. With no explicit wait, independently known native submission keeps success/exit 0 plus a fixed history diagnostic; an explicit wait that cannot be verified fails with exit 1/130 without inventing a wait result.

For Codex correlation-only mode there is no pending explicit wait: SIGINT after a complete valid queue receipt retains success/exit 0 and wait not_requested. Partial evidence under SIGINT remains unknown/exit 130. Interrupted version preflight refuses/exit 130 before queue. Diagnostic stderr overflow is separate from stdout overflow: bounded stderr never erases a captured complete valid stdout receipt, while protocol stdout overflow is always unknown. Failed native journal snapshots use a closed fixed-reason profile; corrupt positive/secret fields fail as ledger_corrupt without being copied into query results. Malformed public correlation IDs are usage errors (exit 2) before message input; private receipt validation remains an operation failure.

Source-streamed SSH correlation-only route

Verified structured SSH-self addresses keep the local message limit after normalization, including prepared-ID reuse and dry-run. Actual SSH destinations still have the 90,000-character command-argument limit. The protected metadata child additionally bounds each encoded JSON input/output frame to 1 MiB, including UTF-8 encoding and JSON escapes; this can reject otherwise valid local-sized input before submission.

The sender uses one original per-destination budget for metadata, harmless target probing, durable intent, one native effect and cleanup. Its protected metadata child preserves From/Reply and structured SSH-self normalization without renewing detection timeouts. A consumed intent refuses before any new probe. Required remote delivered/acknowledged waits refuse before SSH submission; best-effort requests report unsupported and mint no capability, remote ledger or collector. The effect rechecks the original Claude generation or canonical Codex home/writer context. An explicit Codex generation request is unsupported, not silently removed. Status/wait never invokes SSH submission.

The probe records an old remote boot-bound monotonic anchor before replying. Only after strict response validation does the sender compute both remaining durations from one original clock sample. The remote effect uses that old anchor plus the remaining durations, including exactly five seconds within the total for cleanup; changed boot, future/expired anchor or malformed context refuses before native effect. No UTC or cross-host clock origins are compared. SSH shutdown does not prove cancellation of remote submission or consumption. Home mapping is the trusted remote handler's actual expanduser/resolve result, including owned symlink aliases; it is not a sender-verified inode, atomic incarnation or universal native storage guarantee.

The actual command must enter the closed private handler before config/source/SSH children start. Hidden options are not authentication: the receiver quotes every external body line once. Strict native profiles and request/target/home/context proofs preserve complete submission evidence after disconnect, timeout, diagnostic overflow or interruption, but never salvage an overflowing protocol stdout prefix. Connection-verified SSH identity and safe reply metadata remain separate from native facts. Private proofs, body, clock anchors and native context are not reflected into public outputs. Owned fake-SSH/source-stream/SQLite/queue fixtures verify this shipped slice, not actual remote hosts, native model consumption or installed reverse-SSH receipt bootstrap.

Read one request's status

session-peer handoff status --correlation-id PREPARED_UUID

Use an existing prepared ID. The human-readable view shows one snapshot from the local owner's ledger: known submission facts, injection observation, ACK assurance and whether it was late, the latest stored wait, retry prohibition and supported next actions. The ACK and wait are shown separately: a late ACK does not rewrite an expired wait. The latest stored wait is not a complete wait history. ACK lateness is relative to the first wait, which may differ from the latest stored wait. Expired or quarantined history shows receipt and injection details as unavailable; a pending ACK without a supported keep-waiting action does not establish that further waiting is available. The JSON field schema and exit-code meanings are retained: a successful query returns 0 and a failed query returns 1; query success is not proof of receipt.

Status does not submit or wake, initialize or repair a ledger, mint receipt authority, or change waits. Its read-only ledger path does not save changes; in-memory retention may still show a tombstone with details unavailable. Explicit quarantine after restore sets submission to unknown and retains terminal waits on disk; the view shows the latest wait only when a valid public snapshot can be formed. Known lifecycle limitation: quarantine clears ACK detail, so a retained satisfied wait cannot satisfy the existing public validator; status refuses with invalid_handoff (exit 1). It does not repair or rewrite that history. This remains a #181 lifecycle gate. Existing status logic may query an already-running collector for clock evidence to determine next actions, but does not start one. The view does not read transcripts or message bodies, or expose raw capabilities.

The collector clock query is also read-only. An older collector may reject that query, so keep_waiting may be absent from the available next actions; status does not fall back to a writing query or restart the collector. The ledger snapshot and optional collector clock observation are separate, not an atomic combined observation. After a wait fails with history_unavailable and the collector clock becomes available again, status now preserves known submission and that failed wait without offering keep_waiting. Other wait-creation semantics are unchanged. A tombstone's latest-wait detail is shown as unavailable due to expired history, not as evidence that no wait was requested.

Status remains a single local query. Version 1.3.0 adds bounded local watching below; Windows handoff and Codex ACK remain unsupported. See #296 for the implementation history.

Watch one request for a bounded period

session-peer handoff watch --correlation-id PREPARED_UUID --watch-timeout 30 --interval-ms 1000 --json

Watch polls only an existing local owner's POSIX ledger. The timeout accepts ASCII integers 1..60 seconds (default 30); the interval accepts ASCII integers 1000..5000 milliseconds (default 1000). It takes at most 61 samples and emits an initial snapshot only if the budget permits a valid public projection, then only changes to public metadata or historyPhase; it may emit no snapshots. It is not a lossless event stream. Known submission, ACK assurance, ACK lateness relative to the first wait, and the latest stored wait retain the status view's separate meanings. A local observation time in UTC is not ACK arrival time or ordering evidence. historyPhase is prepared, attempted, terminal, tombstone or quarantined; attempted means a durable attempt fence, not active execution.

JSON output is NDJSON using the common schemaVersion 1 envelope with watch-specific fields: operation is watch and event is snapshot or end. A snapshot nests the unchanged public handoff object; normal status JSON is unchanged. Deadline completion returns 0 and means only that watching ended, not that an ACK arrived or a wait succeeded. SIGINT returns 130; history or output failure returns 1. Missing/corrupt history or a changed ledgerEpoch stops the watch without inventing a handoff or epoch. The existing quarantined satisfied-wait refusal remains the #181 lifecycle gate described above.

Each output frame is limited to 16 KiB, total output to 256 KiB, with 2 KiB reserved for the end frame. For real-fd stdout, one owned isolated output-only Python helper receives only bounded public serialized bytes and never changes shared descriptor flags or reads ledger/native data. The parent enforces the original total deadline, including helper IPC, and checks it before new ledger read or validation stages. It reserves 100 ms within that total for final output and cleanup, so sampling may end earlier. The parent terminates and reaps its helper on timeout, SIGINT or error. Uninterruptible OS operations still prevent a hard-real-time guarantee. After a partial or unacknowledged write, no subsequent frame, including end, is sent; consumers must reject a truncated final NDJSON line. A helper completion reply proves OS write completion only, not recipient consumption. Watch does not read native bodies or transcripts or expose capabilities or payload/capability digests; public targetGeneration remains unchanged under its existing contract. It does not create intents, waits or receipts, submit, wake, initialize, repair or write the ledger. It may use the existing read-only clock query on an already-running local collector, never launch a collector or native message process. Host/device routing and Windows watching are unsupported; no Codex ACK support is added. The read-only ledger releases its shared lock after reading immutable snapshot bytes, before parsing and validation. Interrupted final output and cleanup are capped at 100 ms within the remaining total budget. Overall success requires a complete successful end frame and exit 0: cleanup may fail after that frame. A nonzero exit means overall failure, without promoting ACK, discarding independently valid submission facts or permitting retry.

Evidence, privacy and timing

A complete inbox write is submission, not consumption or turn completion. Only an authenticated correlated receipt, or a separate explicit operator attestation with provable ordering, produces acknowledged. An early receipt during native write/drain is saved as bounded unclassified evidence. It is classified only after independently known submission and original-clock ordering; classification cannot invent a receipt or backdate an ACK. A committed ACK survives collector restart; unused authority expires conservatively on restart. Duplicates still require original token-hash proof, including after expiry. Manual confirmation never rewrites an old wait.

Ordinary Python Claude fields remain absent rather than synthesized as false/null. Known native facts survive wait failure. Pending wait interruption records stopped and returns complete structured output with exit 130; querying the stopped intent succeeds with exit 0. No status, wait, ACK or confirmation operation submits a native message. Missing/corrupt/unknown history has a separate handoffQuery error without a fabricated epoch. No automatic resend or fallback is allowed.

The per-destination budget starts before setup: 30 seconds by default, ASCII integers 1..60 only. Exactly five seconds are reserved within the total for cleanup. Observation/effect ends at total minus five seconds; timeout diagnostics describe that cutoff. Budgets at or below five seconds refuse before effect. OS filesystem/identity operations and fsync cannot be forcibly cancelled by this Python implementation; this is not a universal hard-real-time latency guarantee.

Raw receipt capabilities occur only in authorized native message input and private IPC/stdin, never sender/collector ordinary outputs, argv, URI, environment or own journal. The journal stores hashes. Native recipient queue/history may retain the delegated token, and same-user readers may acquire it: token_possession is not independent model identity. No recipient transcript is read.

Storage and remaining gates

Clock investigation (2026-10-09): CPython 3.9.25's Darwin clock subtracts a process-local starting tick. The runtime therefore uses typed mach_absolute_time/mach_timebase_info nanoseconds on Darwin across sender, journal and collector, and native monotonic nanoseconds elsewhere. The inbox adapter receives only a translated remaining duration in its own clock domain, not persisted cross-process timestamps. Missing/invalid native clock proof refuses; earlier candidate records have a distinct clock-provider continuity namespace and are not silently reinterpreted. Tests compare real parent/child samples; macOS Python 3.9 acceptance is also checked by CI, not inferred from this Mac's Python 3.14 run.

The owner-only POSIX directory is ~/.local/share/session-peer/handoff. Atomic fsynced snapshots and a stable exclusive lock fence the first native attempt before effect; a crash after that fence remains unknown even if no write actually began. Reservation includes each intent's bounded future receipt/wait/native detail; the 10,000-intent and 32 MiB quotas include tombstones and reservations. Capacity never evicts effect fences. There are at most 64 distinct waits per intent.

Details compact to a target-bound tombstone after 30 days only under proven native boot/monotonic continuity; unavailable continuity conservatively retains detail and may exhaust quota sooner. Cross-boot retention needs further acceptance review. A known restored ledger must be quarantined with the explicit operator HandoffLedger.quarantine_restored() API before reuse; CLI restore/reconciliation tooling remains a gate. Archive/new initialization is an explicit operator action, never automatic repair. Unmarked rollback or cloned history cannot be detected reliably. A stale collector socket is not automatically removed or rebound. Collector lifetime is at most 24 hours; receipt TTL is 24 hours from the original durable intent commit and collector restart revokes unused authority. Lifecycle/expiry boundaries still require review against the complete design before expanding support.

Verification uses owned local Unix inboxes, actual peer-PID/process-birth checks, private collector/producer subprocesses, owned SQLite/fake Codex queue children, bounded raw stdin, strict JSON, concurrent startup, crash fencing, late/duplicate/expired receipt and SIGINT fixtures. Fake queue/version metadata is not a real Codex session or universal native-version certification. Tests do not prove real Claude model consumption, a native Codex observer, remote bootstrap or Windows cleanup. #181 stays open until those remaining acceptance gates are resolved; the frozen design fixture is not runtime certification.