instar 1.3.245 → 1.3.246

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +108 -2
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +13 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/SessionManager.d.ts +3 -0
  8. package/dist/core/SessionManager.d.ts.map +1 -1
  9. package/dist/core/SessionManager.js +3 -0
  10. package/dist/core/SessionManager.js.map +1 -1
  11. package/dist/core/frameworkInjectionProcesses.d.ts +41 -0
  12. package/dist/core/frameworkInjectionProcesses.d.ts.map +1 -0
  13. package/dist/core/frameworkInjectionProcesses.js +53 -0
  14. package/dist/core/frameworkInjectionProcesses.js.map +1 -0
  15. package/dist/core/frameworkSessionLaunch.d.ts +9 -0
  16. package/dist/core/frameworkSessionLaunch.d.ts.map +1 -1
  17. package/dist/core/frameworkSessionLaunch.js +7 -0
  18. package/dist/core/frameworkSessionLaunch.js.map +1 -1
  19. package/dist/core/types.d.ts +22 -0
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/core/types.js.map +1 -1
  22. package/dist/messaging/SpawnRequestManager.d.ts +13 -0
  23. package/dist/messaging/SpawnRequestManager.d.ts.map +1 -1
  24. package/dist/messaging/SpawnRequestManager.js +3 -0
  25. package/dist/messaging/SpawnRequestManager.js.map +1 -1
  26. package/dist/messaging/types.d.ts.map +1 -1
  27. package/dist/messaging/types.js +8 -1
  28. package/dist/messaging/types.js.map +1 -1
  29. package/dist/threadline/ThreadlineRouter.d.ts +55 -1
  30. package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
  31. package/dist/threadline/ThreadlineRouter.js +246 -6
  32. package/dist/threadline/ThreadlineRouter.js.map +1 -1
  33. package/dist/threadline/WarmSessionPool.d.ts +22 -0
  34. package/dist/threadline/WarmSessionPool.d.ts.map +1 -1
  35. package/dist/threadline/WarmSessionPool.js +38 -0
  36. package/dist/threadline/WarmSessionPool.js.map +1 -1
  37. package/package.json +1 -1
  38. package/scripts/instar-dev-precommit.js +42 -0
  39. package/src/data/builtin-manifest.json +3 -3
  40. package/upgrades/1.3.246.md +97 -0
  41. package/upgrades/side-effects/threadline-warm-session-a2a.md +146 -0
  42. package/upgrades/threadline-warm-session-a2a.eli16.md +63 -0
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-04T18:14:33.049Z",
5
- "instarVersion": "1.3.245",
4
+ "generatedAt": "2026-06-04T18:56:49.668Z",
5
+ "instarVersion": "1.3.246",
6
6
  "entryCount": 198,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -1506,7 +1506,7 @@
1506
1506
  "type": "subsystem",
1507
1507
  "domain": "sessions",
1508
1508
  "sourcePath": "src/core/SessionManager.ts",
1509
- "contentHash": "d01eb671ddb9e21b1aeef11fb0b79a63db7a088baf94352fce58a3f0a7c4fd60",
1509
+ "contentHash": "4d50cc3ebd4b430069d5957fbed44bdadd487c17386b680f0cafe66a33c381e2",
1510
1510
  "since": "2025-01-01"
1511
1511
  },
1512
1512
  "subsystem:auto-updater": {
@@ -0,0 +1,97 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Phase 2 of the Threadline agent-to-agent (A2A) continuity work: **warm sessions**.
9
+
10
+ The turn-based fix (#746) made a peer's spaced follow-up *resume* the same
11
+ conversation via `claude --resume`. But rapid-fire bursts (several messages in
12
+ seconds) still cold-spawned, because each A2A reply ran as a throwaway `-p`
13
+ session that answered once and exited — there was nothing alive to hand the next
14
+ message to — and a fresh thread could even hit the 30s per-peer spawn cooldown.
15
+
16
+ This change keeps the reply session **alive between messages** for a warm-enabled
17
+ peer and **injects** follow-ups into it:
18
+
19
+ - The first message from a verified, non-topic-bound peer spawns a **persistent
20
+ interactive** reply session (not a throwaway `-p`), carrying the full grounded
21
+ prompt as its first turn, and registers it in a new `WarmSessionPool` keyed by
22
+ threadId (caps: globalCap 3, perPeerCap 1, ttlMs 600000, trustFloor `verified`).
23
+ - The next message on that thread is **injected** into the same live session — no
24
+ cold-spawn, no cooldown — and refreshes its TTL.
25
+ - A periodic reap tick evicts on TTL/pressure; on eviction the **next message
26
+ falls back losslessly to the proven `--resume` path (#746)** — nothing is lost.
27
+
28
+ Three correctness/security fixes ride along, each independently load-bearing:
29
+
30
+ - **`claude.exe` allowlist fix** — live macOS Claude panes report
31
+ `pane_current_command = claude.exe`, which the inject allowlist
32
+ (`ALLOWED_INJECTION_PROCESSES`) did not contain, so A2A live-inject was
33
+ **dead-on-arrival on macOS**. Added `claude.exe`.
34
+ - **Grounding-on-inject** — injected follow-ups are now wrapped in the same
35
+ untrusted-data grounding preamble used on spawn/resume (they previously injected
36
+ the raw envelope body).
37
+ - **Per-thread isolation + peer-conflict guard** — each thread gets its own
38
+ private session (no shared listener, nothing to leak across peers), and
39
+ `WarmSessionPool.admit` throws `WarmSessionPeerConflictError` rather than letting
40
+ a different peer hijack an existing thread's warm session; a pre-spawn `peek`
41
+ check makes the conflict path fall back to a normal cold-spawn.
42
+
43
+ Dark-shipped behind `threadline.warmSessionA2A.enabled`, resolved via the
44
+ `developmentAgent` dev-gate (`enabled ?? !!config.developmentAgent`): **live on the
45
+ development agent, dark on the fleet**. With the flag absent and `developmentAgent`
46
+ false, behavior is byte-for-byte the current cold-spawn path.
47
+
48
+ **Framework-general (not Claude-specific).** The warm worker runs in the LOCAL
49
+ agent's framework (claude-code / codex-cli / gemini-cli / future), not hardcoded
50
+ Claude: the inject allowlist is now DERIVED from a per-framework process-name
51
+ registry (`Record<IntelligenceFramework, …>`), and the warm spawn passes the local
52
+ framework so the right CLI + resume mechanism applies. Enforced by compiler
53
+ exhaustiveness + `tests/unit/framework-agnosticism.test.ts` + a new `/instar-dev`
54
+ precommit gate that requires any launch/inject-abstraction change to state its
55
+ framework generality (standard: **Framework-Agnostic — and Framework-Optimizing**).
56
+
57
+ ## What to Tell Your User
58
+
59
+ When another agent and I go back and forth quickly, I now keep the same live
60
+ conversation open and type each new message into it — instead of starting a fresh,
61
+ memoryless reply every time. So a rapid exchange holds its thread end-to-end, not
62
+ just the slower turn-by-turn case. It's an internal robustness improvement that
63
+ ships active only on the development agent (dark on everyone else) and falls back
64
+ to the already-shipped resume path if a warm session is ever retired. Nothing to
65
+ turn on.
66
+
67
+ ## Summary of New Capabilities
68
+
69
+ | Capability | How to Use |
70
+ |-----------|-----------|
71
+ | Warm A2A sessions (keep-alive + inject) | Automatic for verified, non-topic-bound Threadline peers when `threadline.warmSessionA2A.enabled` is on (defaults to the `developmentAgent` flag). Rapid follow-ups inject into the live session instead of cold-spawning. |
72
+ | Tunable warm-session pool | `.instar/config.json` → `threadline.warmSessionA2A`: `globalCap` (3), `perPeerCap` (1), `ttlMs` (600000), `trustFloor` (`verified`). |
73
+ | Lossless eviction fallback | On TTL/pressure eviction, the next message resumes via the `--resume` path (#746) — no continuity lost. |
74
+
75
+ ## Evidence
76
+
77
+ **The `claude.exe` bug (reproduced in dev):** `ALLOWED_INJECTION_PROCESSES`
78
+ (`src/messaging/types.ts`) listed `claude` but not `claude.exe`. On macOS a live
79
+ Claude pane reports `pane_current_command = claude.exe`, so the inject guard
80
+ `isInjectableProcess()` returned **false for every live A2A reply session** —
81
+ agent-to-agent live-inject was dead-on-arrival on macOS.
82
+
83
+ - *Before:* the prior live Echo↔Dawn round-trip recorded `2× Spawned, 0× Resumed,
84
+ 1× spawn-denied (cooldown)` on a fresh thread — every follow-up cold-started; the
85
+ inject path was never reachable because the allowlist check rejected `claude.exe`.
86
+ - *After:* unit test asserts `claude.exe ∈ ALLOWED_INJECTION_PROCESSES`; the
87
+ integration test "2nd message on a thread with a live warm session → inject (no
88
+ 2nd spawn, no cooldown)" passes — the inject path is now reachable and exercised.
89
+
90
+ **Test evidence (re-run on top of current main, v1.3.243):** tsc `--noEmit` clean;
91
+ warm-session unit (105) + integration (5: incl. flag-on inject, flag-off
92
+ byte-identical cold-spawn, peer-conflict → cold fallback) + e2e (1: continuity then
93
+ evict → resume via #746) all green; esm / no-empty-catch / no-direct-destructive /
94
+ no-silent-fallbacks gates green (warm fail-opens annotated `@silent-fallback-ok`,
95
+ net silent-fallback count unchanged). The live Echo↔Dawn production round-trip
96
+ ("Resumed not Spawned" + rapid inject with no cooldown) is the post-deploy
97
+ standards-met gate, run by the orchestrator after merge.
@@ -0,0 +1,146 @@
1
+ # Side-Effects Review — Threadline Warm-Session A2A Integration
2
+
3
+ **Version / slug:** `threadline-warm-session-a2a`
4
+ **Date:** `2026-06-04`
5
+ **Author:** `Echo (instar dev agent)`
6
+ **Second-pass reviewer:** `not required (Tier 1 — additive + dark-gated)`
7
+
8
+ ## Summary of the change
9
+
10
+ Phase 2 of A2A continuity: keep a verified peer's reply session **alive** between
11
+ messages and **inject** follow-ups, instead of cold-spawning each time. The first
12
+ message spawns a persistent **interactive** session (not throwaway `-p`), registered
13
+ in a new `WarmSessionPool` keyed by threadId; subsequent messages inject into the
14
+ live session (no spawn, no 30s cooldown); a reap tick evicts on TTL/pressure; on
15
+ eviction the next message falls back losslessly to the shipped `--resume` path
16
+ (#746). Three correctness/security fixes ride along: `claude.exe` added to the
17
+ inject allowlist (live macOS Claude panes report `claude.exe`, so live-inject was
18
+ dead-on-arrival); grounding-on-inject (wrap injected body in the untrusted-data
19
+ preamble); per-thread isolation + `WarmSessionPeerConflictError` guard.
20
+
21
+ Dark-shipped behind `threadline.warmSessionA2A.enabled ?? !!config.developmentAgent`.
22
+ Files: `src/messaging/types.ts`, `src/threadline/WarmSessionPool.ts`,
23
+ `src/threadline/ThreadlineRouter.ts`, `src/commands/server.ts`,
24
+ `src/config/ConfigDefaults.ts`, `src/core/types.ts`,
25
+ `src/messaging/SpawnRequestManager.ts`, `src/core/SessionManager.ts`,
26
+ `src/core/frameworkSessionLaunch.ts` + unit/integration/e2e tests.
27
+
28
+ ## Decision-point inventory
29
+
30
+ - `ALLOWED_INJECTION_PROCESSES` (`src/messaging/types.ts`) — **modify (additive)** — add `claude.exe`. Widens which live panes accept an injected message; only matters for the A2A inject path (Telegram uses a different non-allowlisted path).
31
+ - `WarmSessionPool.admit` (`src/threadline/WarmSessionPool.ts`) — **new** — throws `WarmSessionPeerConflictError` if a different peer owns the thread; caps/TTL/LRU eviction.
32
+ - `ThreadlineRouter.tryInjectIntoLiveSession` (`src/threadline/ThreadlineRouter.ts`) — **modify** — wrap injected body in grounding preamble; `pool.touch` on success.
33
+ - `ThreadlineRouter.handleInboundMessage` new-spawn path — **modify (gated)** — when `preferWarmSession`, request an interactive keep-alive spawn + `pool.admit`; pre-spawn `peek` conflict → cold-spawn fallback.
34
+ - `server.ts` relay decision + `spawnSession` callback + reap tick — **modify (gated)** — resolve `warmEnabled`, construct pool (null when off), branch the spawn callback on `opts.interactive`, periodic `reapExpired` + `killWarmSessionByName`, `.unref()`, cleared on shutdown.
35
+ - `ConfigDefaults` / `ThreadlineConfig` — **new (additive)** — `warmSessionA2A` caps with no `enabled` (resolved via dev-gate).
36
+ - `SessionManager.spawnInteractiveSession` + `frameworkSessionLaunch` — **modify (additive)** — new `sessionId?` option → `--session-id` on the interactive claude launch (mutually exclusive with `--resume`).
37
+
38
+ ## 1. Over-block
39
+
40
+ The new `WarmSessionPeerConflictError` could *withhold* a warm session from a
41
+ legitimate same-thread message if peerId were computed inconsistently. Mitigation:
42
+ peerId is the same stable sender fingerprint used for the thread's trust/ownership
43
+ checks; same-peer refresh is explicitly tested as allowed. On any conflict the
44
+ message is **not dropped** — it falls back to a normal cold-spawn. So the worst
45
+ "over-block" outcome is "no warm speedup," never "message rejected." The trust
46
+ floor (`verified`) gates *warmth*, not *delivery*: a below-floor peer still gets the
47
+ unchanged cold-spawn reply.
48
+
49
+ ## 2. Under-block
50
+
51
+ `claude.exe` widens the inject allowlist — could an unintended process now be
52
+ injected into? No: inject is keyed on the pool's own `sessionName` (a session this
53
+ agent spawned + admitted), not a free scan of panes; the allowlist is a
54
+ defense-in-depth process-name check on top of an already-owned session handle. The
55
+ grounding-on-inject wrap closes the prior gap where a peer's raw body reached the
56
+ session without untrusted-data framing. Residual: a malicious *verified* peer can
57
+ still keep one warm session pinned (bounded by perPeerCap 1 + ttl 600s + globalCap
58
+ 3) — a resource bound, not a content-leak (per-thread isolation means nothing to
59
+ leak across peers).
60
+
61
+ ## 3. Level-of-abstraction fit
62
+
63
+ `WarmSessionPool` is a pure registry (no I/O); the router owns the
64
+ spawn/admit/inject/touch decisions; the server owns process construction + the kill
65
+ primitive (`killWarmSessionByName` resolves tmux-name → session-id → kill, because
66
+ `killSession` keys by session id). The interactive `sessionId` option lives at the
67
+ launch builder, mirroring the existing `--resume` plumbing. Correct layering — the
68
+ pool never touches tmux, the router never builds argv.
69
+
70
+ ## 4. Signal vs authority compliance
71
+
72
+ The trust floor is a **resource** control (who may keep a session warm), not a new
73
+ authority/approval surface. No gate is relaxed. Inbound trust gates are untouched.
74
+ Trust comparison uses an explicit `TRUST_ORDER` index (never string `>=`, which
75
+ would make `'verified' >= 'trusted'` true alphabetically) — guarded by a test.
76
+
77
+ ## 5. Interactions
78
+
79
+ - Builds on #746: on eviction, `get()` + `jsonlExists(uuid)` resume the thread via
80
+ `--resume` — the e2e test forces an evict and asserts the next message resumes
81
+ (entry intact). `onSessionComplete` demote/persist is unchanged.
82
+ - The interactive worker keeps project MCP (does **not** set `disableProjectMcp`),
83
+ matching the `-p` path, so `threadline_send` is available — the warm worker can
84
+ actually reply.
85
+ - Re-verified after rebasing onto current main (v1.3.243, which added #747 Telegram
86
+ delivery dedupe + #748 duplicate-response guard): tsc clean; warm unit (105) +
87
+ integration (5) + e2e (1) + esm/empty-catch/no-direct-destructive/no-silent-
88
+ fallbacks gates all green. No-silent-fallbacks net count unchanged (warm
89
+ fail-opens annotated `@silent-fallback-ok`).
90
+
91
+ ## 6. External surfaces
92
+
93
+ New config block `threadline.warmSessionA2A` (additive; `applyDefaults` deep-merges
94
+ nested objects — verified by test, no `PostUpdateMigrator` patch needed). No new
95
+ HTTP route. CLAUDE.md/template: warm sessions are internal robustness — no
96
+ agent-facing capability surface added. Migration-parity: config is additive +
97
+ existence-checked; dark by default on the fleet (developmentAgent gate).
98
+
99
+ ## 7. Rollback cost
100
+
101
+ Low. Flag-off (or `developmentAgent` false) is byte-for-byte the current cold-spawn
102
+ path — proven by flag-off tests. Full revert = the listed source files; no persisted
103
+ format change (the pool is in-memory only). `claude.exe` allowlist + grounding-on-
104
+ inject are independently safe to keep even if the warm path is reverted.
105
+
106
+ ## Framework generality
107
+
108
+ Per the **Framework-Agnostic — and Framework-Optimizing** standard, the warm path
109
+ must work for EVERY agentic framework (claude-code / codex-cli / gemini-cli /
110
+ future), not just Claude. Two parts were initially Claude-leaning and are now
111
+ routed through the framework abstraction:
112
+
113
+ - **Inject allowlist.** `ALLOWED_INJECTION_PROCESSES` was a hardcoded shells +
114
+ `claude`/`claude.exe` list — a non-Claude warm worker's pane process (`codex`,
115
+ `gemini`) would be refused. It is now DERIVED: `shells ∪ FRAMEWORK_INJECTION_
116
+ PROCESS_NAMES` (new `src/core/frameworkInjectionProcesses.ts`), a
117
+ `Record<IntelligenceFramework, …>` so the compiler forces an entry per framework.
118
+ - **Warm spawn framework.** The interactive keep-alive spawn now passes
119
+ `framework: _defaultFramework` (the local agent's framework) to
120
+ `spawnInteractiveSession`, so a codex/gemini agent's warm worker launches in its
121
+ own framework with its own MCP/permission flags. The deterministic `sessionId`
122
+ is mapped per-framework by `frameworkSessionLaunch` (claude pins `--session-id`;
123
+ codex/gemini ignore it and resume by their own mechanism) — not a Claude
124
+ assumption, it's the abstraction's job.
125
+
126
+ **Enforced (structure, not willpower):** (1) compiler exhaustiveness on the two
127
+ `Record<IntelligenceFramework, …>` maps; (2) `tests/unit/framework-agnosticism.test.ts`
128
+ fails if any framework's injection-process entry is empty, a launch builder is
129
+ missing, or the allowlist drifts from `shells ∪ registry`; (3) the `/instar-dev`
130
+ precommit gate (`assertFrameworkGenerality`) requires this very section for any
131
+ change to the launch/inject abstraction surface.
132
+
133
+ ## Conclusion
134
+
135
+ Low-risk, additive, dark-gated. Completes the rapid-fire A2A continuity case on top
136
+ of the shipped turn-based foundation, with per-thread isolation removing the shared-
137
+ listener leak concern entirely. Both sides of every boundary are tested (warm-on
138
+ inject vs flag-off cold-spawn; same-peer refresh vs peer-conflict fallback; evict →
139
+ resume). Live Echo↔Dawn production round-trip is the orchestrator's standards-met
140
+ gate (post-deploy).
141
+
142
+ ## Second-pass review (if required)
143
+
144
+ Not required — Tier 1 (additive, dark-gated, single subsystem). The subsystem-scale
145
+ shape (a new pool + lifecycle wiring) was handled via spec-converge + an ELI16
146
+ review surfaced to the operator before merge, per the A4 ceremony.
@@ -0,0 +1,63 @@
1
+ # Warm sessions: how rapid agent-to-agent threads stop cold-starting
2
+
3
+ ## The problem in one breath
4
+ When another agent and I have a back-and-forth, each of my replies used to run as
5
+ a **throwaway**: it answered once and exited. So when a second message arrived a
6
+ moment later, there was nothing alive to hand it to — I'd cold-start a brand-new,
7
+ memoryless reply. The turn-based fix I already shipped (#746) handles the *slow*
8
+ case by re-opening the same conversation with `--resume`. But *rapid-fire* (a few
9
+ messages in seconds) still cold-started, and on a fresh thread could even hit a
10
+ 30-second spawn cooldown.
11
+
12
+ ## A real bug I found while grounding this
13
+ While reading the code I found that even when a reply session *was* briefly alive,
14
+ my "type the next message into it" path was being **refused every single time**.
15
+ The safety check only allows a short list of program names, and it lists `claude`
16
+ — but on this Mac the live program actually shows up as **`claude.exe`**. So the
17
+ check never matched, and agent-to-agent live-typing has been **dead on arrival on
18
+ macOS**. One-line fix: allow `claude.exe`. (This is exactly why "ground before you
19
+ assert" matters — the feature looked wired but wasn't.)
20
+
21
+ ## The fix (reuses what's already shipped)
22
+ Keep the reply session **alive** between messages — exactly like my normal chat
23
+ sessions with you, which stay open and get messages typed into them:
24
+ - The first message from a peer spawns a **persistent** reply session (not a
25
+ throwaway), tracked in a small pool keyed by the thread.
26
+ - The next message is **typed into that same live session** (now that the
27
+ `claude.exe` fix lets it through) — no cold-start, no cooldown.
28
+ - The pool has tight caps (3 sessions total, 1 per peer) and a 10-minute timeout
29
+ so a chatty peer can't pin a pile of live sessions; when one is evicted, the
30
+ **next message just falls back to the proven `--resume` path** — nothing lost.
31
+
32
+ ## The nice surprise: the security question dissolved
33
+ An earlier draft worried about letting "verified" peers into a **single shared**
34
+ listener — one peer's content could mix into a session handling others. The new
35
+ design gives **each conversation its own private session**, so there's nothing
36
+ shared to leak. The trust gate stops being a secrecy control and becomes a simple
37
+ "who's allowed to keep a session warm" resource control. Injected follow-ups also
38
+ now get the same untrusted-data grounding wrapper that spawns/resumes already use.
39
+
40
+ ## Works for every framework — not just Claude (your note)
41
+
42
+ The first cut leaned Claude-specific in two spots, and both are now routed through
43
+ the framework abstraction so a Codex or Gemini agent gets the same warm sessions:
44
+ - The "is this process safe to type into?" allowlist used to be hardcoded to
45
+ `claude`/`claude.exe`. It's now **derived** from a per-framework registry
46
+ (`claude → claude/claude.exe`, `codex → codex`, `gemini → gemini`), typed so the
47
+ compiler refuses to add a framework without giving it a process name.
48
+ - The warm worker now launches in **the local agent's framework**, not always
49
+ Claude — so on a Codex agent it spins up a Codex session, with Codex's own
50
+ resume mechanism.
51
+
52
+ And — the part you actually asked for — this is enforced by the **review process**,
53
+ not by remembering: a CI test fails if any framework lacks coverage or the allowlist
54
+ drifts, and the `/instar-dev` precommit gate makes any change to the launch/inject
55
+ layer state, in writing, whether it works for Codex and Gemini. So "works for all
56
+ frameworks" is now a gate, not a good intention.
57
+
58
+ ## How it ships
59
+ Dark on the fleet, **live only on the development agent**, behind a flag, with the
60
+ proven resume path as the fallback if a warm session is ever evicted. With the flag
61
+ off, behavior is byte-for-byte the current cold-spawn path. Full unit + integration
62
+ + e2e tests cover both sides: warm inject when enabled, byte-identical cold-spawn
63
+ when disabled, peer-conflict → cold fallback, and evict → resume continuity.