context-handoff
Absolute context-band signals, session-terminal mutation deny (D1–D3), and the operator CLI for write/resume/bypass/repair/unlock — F3 continuity handoff.
context-handoff
ADLC phase: P4 continuity (F3) · Library + CLI: absolute context bands, session-terminal mutation deny (D1–D3), and operator handoff commands.
What it defends against
Long sessions rot: depth, transcript size, and context-window fill climb past the
point where the agent still holds the plot. context-handoff freezes the
absolute band thresholds and the session-terminal deny contracts that
later harness adapters and build-gate will share — so no local copy of the
numbers can drift, and a deny written once cannot silently clear when a marker
file disappears.
Slice 2 adds the operator/host CLI (adlc handoff …) that adapters and
humans call to write finals, resume/consume denies, bypass, repair binds, and
unlock. Harness adapters and build-gate migration land in later slices.
Absolute bands
Signals are OR-joined across available kinds (pct, depth, bytes). Missing
kinds are ignored; present-but-malformed or out-of-domain values fail closed as
hard. Floor is never subtracted from band comparators (no floor-delta bands).
| Band | pct | depth | bytes |
|---|---|---|---|
| warn | ≥ 50 | ≥ 20 | ≥ 128 KiB |
| handoff | ≥ 60 | ≥ 30 | ≥ 192 KiB |
| hard | ≥ 80 | ≥ 40 | ≥ 256 KiB |
Advisory nags (nagSuppression) may be suppressed near hard so deny/handoff
owns the signal; that path never affects mutation deny.
Deny lifecycle (D1–D3)
| Code | Meaning |
|---|---|
| D1 | Deny active for the denier session — mutations blocked. |
| D2 | Deny sticky across re-entry for that session until a verified consume. |
| D3 | Other open denies in the store also block mutations for non-denier sessions. |
After a verified other-session consume, that record leaves D3 (status=consumed);
the denier remains D2. Marker vanish / store-unavailable paths fail closed unless
an unbound operator bypass clears the recoverable codes.
CLI (adlc handoff)
Dry-run by default. Pass --write to mutate. Mutating commands require
ADLC_MANIFEST_KEY and append durable evidence to .adlc/manifest.jsonl —
missing key exits 1 (never silent success).
adlc handoff write --session <id> [--ticket <id>] [--host <h>] [--content-hash <h>] [--write] [--json]
adlc handoff resume --session <consumer> --deny-session <denier> [--write] [--json]
adlc handoff bypass --session <id> [--unbound-reason <text>] [--write] [--json]
adlc handoff repair --session <id> --ticket <id> --content-hash <h> [--write] [--json]
adlc handoff unlock --session <id> --pid <n> --started-at <iso> --host <h> --nonce <n> [--write] [--json]| Command | Role |
|---|---|
| write | Final checkpoint + ensure deny marker / .adlc/.deny-store |
| resume | Other-session consume (same-session exits 2); writes resume-auth cache |
| bypass | Bound (session only) vs unbound (--unbound-reason) one-shot grant |
| repair | Privileged host bind of an existing open deny ticket_id+content_hash |
| unlock | Reclaim lock only when the PID is dead, the lock is this host's, and all fields match |
repair never creates a deny: a missing or already-consumed marker exits 1,
so the command that relaxes a deny cannot arm one. resume makes the manifest
entry durable before it flips the record (outcome: authorized attests the
authorization, not the persisted consume), and rolls the resume-auth cache back
if anything fails — a failed resume leaves the deny open. The bypass grant
printed on stdout is scoped to the calling adapter invocation; the durable
proof is the context-handoff-bypass manifest entry. An explicitly empty
--unbound-reason exits 1 rather than handing back the bound grant that cannot
clear what the operator asked to override.
write is the refresh path, so it rebinds an open marker onto the final it
writes; a refresh that would unbind one (dropping --ticket) or refresh a
consumed one exits 1 instead of leaving a final and a deny that disagree. Every
command that mutates a marker — write, resume, repair — runs under that
session's .lock and exits 2 when a live session on this host holds it, so one
deny authorizes exactly one successor. If evidence cannot be appended, the
files written in that run are rolled back: un-evidenced state is un-auditable
state. A hard-killed process leaves its lock behind for unlock to reclaim.
Artifact paths (under the repo root; --dir names the .adlc directory, and
its final path segment must be .adlc so artifacts and evidence stay together):
- Final:
.adlc/handoffs/finals/<session_id>.json - Deny:
.adlc/handoffs/denies/<session_id>.json+.adlc/.deny-store - Resume-auth:
.adlc/handoffs/<session_id>.resume-auth.json - Lock:
.adlc/handoffs/<session_id>.lock
Library usage
import { WARN_PCT, HANDOFF_PCT, HARD_PCT } from '@adlc/context-handoff/lib/thresholds.mjs';
import { evaluateBands, nagSuppression } from '@adlc/context-handoff/lib/bands.mjs';
import { evaluateMutationGate } from '@adlc/context-handoff/lib/mutation-gate.mjs';
import { ensureDenyMarker, loadDenyRecords } from '@adlc/context-handoff/lib/deny-marker.mjs';
const bands = evaluateBands({ pct: 65, depth: 12 });
// → { warn: true, handoff: true, hard: false }node --test packages/context-handoff/test/*.test.mjsBinding design:
docs/specs/context-rot-handoff.md
and the package README.