# JIT plan — nested-session inertness + deferred checkpoint compaction (→ omp-spt 0.7.0)

> Closes the two PARITY rows left `unverified` after 0.6.0 (rows 49 and 56) and fixes the two
> defects the audit of those rows uncovered. Both were established from OMP source (the harness —
> in scope to read; spt-core source is not) and field-confirmed on hertz 2026-09-06.
>
> **Finding 1 — nested activation.** OMP subagents (`task` tool) run in-process on a fresh
> `AgentSession` and **re-run every prepared extension factory** with a fresh `ExtensionAPI`
> (`sdk.ts` `bindPreparedExtensions`; `task/executor.ts` forwards `session.preparedExtensions`
> unless the subagent restricts its tool list; runner initialised in `print` mode with
> `hasUI: false`). No event or context field marks a subagent. Our `session_start` guard only
> checked the env-derived id, so the nested copy ran `spt api bind hertz --set-session-id
> <subagent-sid>`. Field: one `[omp-spt hertz] omp-spt could not bind hertz` error line per
> subagent, `spt bind exit 1: ER_HOSTED_PROBE:no-row`; hertz stayed ONLINE only because core's
> hosted probe refused an unknown session id.
>
> **Finding 2 — in-tool compaction deadlock.** `spt_checkpoint` awaited `ctx.compact()` from
> inside its own `execute`. OMP's `session.compact()` first `abort()`s the active run and awaits
> `agent.waitForIdle()` = the running prompt promise; the agent loop awaits `tool.execute`
> unconditionally (`agent-loop.ts` `executeToolCalls`). The tool waits for compaction, compaction
> waits for the loop, the loop waits for the tool: the "Compacting context… (esc to cancel)"
> loader hangs until Esc, which surfaces as `SPT checkpoint failed` and no wake. hertz: NEVER-RUN,
> so it never bit in the field. Unit tests passed because the fake `ctx.compact` resolved.
>
> **Field-verified on 0.6.0 (kept):** the long-command nudge line (35 s foreground `sleep`) and
> the `[omp-spt hertz]` log prefix. OMP auto-backgrounds foreground bash at
> `bash.autoBackground.thresholdMs` (default 60 s) and backgrounds early on an incoming steer, so
> the nudge's window is a completed foreground run of 30–60 s.

## Design

### A. One activation per process (REQ-HAZARD-NESTED-ACTIVATION, hazard #15; PARITY row 49)
- `createOmpSpt()` owns a `primaryInstance` slot shared by every `ompSpt(pi)` binding it
  produces (OMP rebinds the same module's default export per session, so the slot is process
  scope in the field; each test harness calls `createOmpSpt` afresh and can share one factory
  across two fake `pi`s on purpose).
- The first `session_start` claims the slot. Any later binding whose `session_start` finds the
  slot taken is a **nested session**: it logs one debug line, never binds, never spawns a
  listener, never publishes state, registers nothing that reaches core, and its
  `session_shutdown` is a no-op. `activateEndpoint` refuses in a nested copy too.
- Deliveries therefore surface only in the primary conversation — row 49 is satisfied by
  construction, not by a subagent marker OMP does not offer.

### B. Deferred compaction (REQ-HAZARD-INTOOL-COMPACT, hazard #16; REQ-PARITY-CHECKPOINT)
- `spt_checkpoint` validates as before (activated, `live_agent`), then **arms** the checkpoint
  and returns at once: "armed; OMP compacts when this turn ends; end the turn now". Arming twice
  in one turn is refused.
- At `completeTurn` with a checkpoint armed the endpoint publishes **busy** (it is about to lose
  its context) and schedules `runArmedCheckpoint` on a 0 ms timer. The runner returns if a new
  turn already started (`completeTurn` re-schedules), else awaits `ctx.compact(...)` from
  **outside** any tool, then queues the wake exactly as before (`nextTurn` + `triggerTurn`).
- Failure after arming cannot be reported through the tool result any more, so it is reported as
  a next-turn message (`omp-spt-checkpoint-failed`, attribution user, triggers a turn): commune
  saved, context NOT reset, no wake queued. Error log + notify as well.

### C. Deliveries during a checkpoint (REQ-CHECKPOINT-DELIVERY-HOLD; PARITY row 56)
- From arming until the wake is queued (or the failure notice), `submitListenerItem` keeps every
  inbound in custody unsubmitted — the same seam the usage-limit hold uses — and the delivery
  watchdog pauses. A stub submitted into the dying context would be summarised away.
- Release = resubmit: the held stubs go out right after the wake is queued; OMP orders the wake
  turn and the delivery turns itself (queued steers survive compaction natively —
  `session-maintenance.ts` `compact()` finally re-drains them after reconnect).
- Messages the model already consumed before calling the tool are the commune's business, as in
  claude-spt.

## Tasks
1. `traceable-reqs.toml`: add `REQ-HAZARD-NESTED-ACTIVATION`, `REQ-HAZARD-INTOOL-COMPACT`,
   `REQ-CHECKPOINT-DELIVERY-HOLD` (doc + impl + unit).
2. Extension: primary-instance guard; armed checkpoint state machine; hold seam; status rail
   ` · checkpoint pending`; brief line.
3. Tests: rewrite `testNativeCheckpointTool` (tool returns without compacting; compaction after
   `agent_end`; wake after compaction; failure → next-turn notice); add
   `testNestedSessionCopyStaysInert`, `testCheckpointHoldsDeliveriesUntilWake`.
4. Skill `commune/SKILL.md` step 3: the tool arms; end the turn; the wake follows compaction.
5. Docs: KNOWN-HAZARDS #15/#16; PARITY rows 49/55/56 (+ baseline); harness-contract Extension
   lifecycle + Failure behavior; CHANGELOG 0.7.0; SPT-CORE-FINDINGS note (core's hosted-probe
   refusal is the accidental guard today).
6. Gate: `node tests/omp-extension.mjs`, `sh ci/run-gates.sh`, `traceable-reqs check`; release
   0.7.0 per `docs/RELEASE-RUNBOOK.md`; `spt adapter update omp-spt`; restart hertz; field-run
   one real `spt_checkpoint` on hertz (first ever) with a peer message sent mid-checkpoint.
