instar 1.3.728 → 1.3.730

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 (103) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +259 -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 +11 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/FileClassifier.d.ts.map +1 -1
  8. package/dist/core/FileClassifier.js +11 -0
  9. package/dist/core/FileClassifier.js.map +1 -1
  10. package/dist/core/LocalSessionOwnershipStore.d.ts +5 -0
  11. package/dist/core/LocalSessionOwnershipStore.d.ts.map +1 -1
  12. package/dist/core/LocalSessionOwnershipStore.js +13 -0
  13. package/dist/core/LocalSessionOwnershipStore.js.map +1 -1
  14. package/dist/core/ModelSwapService.d.ts +13 -0
  15. package/dist/core/ModelSwapService.d.ts.map +1 -1
  16. package/dist/core/ModelSwapService.js +11 -0
  17. package/dist/core/ModelSwapService.js.map +1 -1
  18. package/dist/core/ModelTierEscalation.d.ts +10 -0
  19. package/dist/core/ModelTierEscalation.d.ts.map +1 -1
  20. package/dist/core/ModelTierEscalation.js +4 -0
  21. package/dist/core/ModelTierEscalation.js.map +1 -1
  22. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  23. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  24. package/dist/core/PostUpdateMigrator.js +35 -0
  25. package/dist/core/PostUpdateMigrator.js.map +1 -1
  26. package/dist/core/ProactiveSwapMonitor.d.ts +74 -20
  27. package/dist/core/ProactiveSwapMonitor.d.ts.map +1 -1
  28. package/dist/core/ProactiveSwapMonitor.js +349 -28
  29. package/dist/core/ProactiveSwapMonitor.js.map +1 -1
  30. package/dist/core/QuotaAwareScheduler.d.ts +70 -3
  31. package/dist/core/QuotaAwareScheduler.d.ts.map +1 -1
  32. package/dist/core/QuotaAwareScheduler.js +100 -16
  33. package/dist/core/QuotaAwareScheduler.js.map +1 -1
  34. package/dist/core/SessionBuildContextStore.d.ts +20 -0
  35. package/dist/core/SessionBuildContextStore.d.ts.map +1 -1
  36. package/dist/core/SessionBuildContextStore.js +20 -3
  37. package/dist/core/SessionBuildContextStore.js.map +1 -1
  38. package/dist/core/SessionManager.d.ts +33 -0
  39. package/dist/core/SessionManager.d.ts.map +1 -1
  40. package/dist/core/SessionManager.js +105 -0
  41. package/dist/core/SessionManager.js.map +1 -1
  42. package/dist/core/SessionOwnershipRegistry.d.ts +16 -0
  43. package/dist/core/SessionOwnershipRegistry.d.ts.map +1 -1
  44. package/dist/core/SessionOwnershipRegistry.js +13 -0
  45. package/dist/core/SessionOwnershipRegistry.js.map +1 -1
  46. package/dist/core/SessionRefresh.d.ts +116 -1
  47. package/dist/core/SessionRefresh.d.ts.map +1 -1
  48. package/dist/core/SessionRefresh.js +280 -17
  49. package/dist/core/SessionRefresh.js.map +1 -1
  50. package/dist/core/StateManager.d.ts +20 -0
  51. package/dist/core/StateManager.d.ts.map +1 -1
  52. package/dist/core/StateManager.js +72 -4
  53. package/dist/core/StateManager.js.map +1 -1
  54. package/dist/core/SwapAntiThrash.d.ts +329 -0
  55. package/dist/core/SwapAntiThrash.d.ts.map +1 -0
  56. package/dist/core/SwapAntiThrash.js +1025 -0
  57. package/dist/core/SwapAntiThrash.js.map +1 -0
  58. package/dist/core/SwapLedger.d.ts +168 -0
  59. package/dist/core/SwapLedger.d.ts.map +1 -0
  60. package/dist/core/SwapLedger.js +275 -0
  61. package/dist/core/SwapLedger.js.map +1 -0
  62. package/dist/core/SwapWorkGate.d.ts +100 -0
  63. package/dist/core/SwapWorkGate.d.ts.map +1 -0
  64. package/dist/core/SwapWorkGate.js +175 -0
  65. package/dist/core/SwapWorkGate.js.map +1 -0
  66. package/dist/core/WriteAdmission.d.ts +277 -0
  67. package/dist/core/WriteAdmission.d.ts.map +1 -0
  68. package/dist/core/WriteAdmission.js +514 -0
  69. package/dist/core/WriteAdmission.js.map +1 -0
  70. package/dist/core/WriteDomainRegistry.d.ts +132 -0
  71. package/dist/core/WriteDomainRegistry.d.ts.map +1 -0
  72. package/dist/core/WriteDomainRegistry.js +186 -0
  73. package/dist/core/WriteDomainRegistry.js.map +1 -0
  74. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  75. package/dist/core/devGatedFeatures.js +12 -0
  76. package/dist/core/devGatedFeatures.js.map +1 -1
  77. package/dist/core/types.d.ts +89 -1
  78. package/dist/core/types.d.ts.map +1 -1
  79. package/dist/core/types.js.map +1 -1
  80. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  81. package/dist/monitoring/guardManifest.js +58 -1
  82. package/dist/monitoring/guardManifest.js.map +1 -1
  83. package/dist/scaffold/templates.d.ts.map +1 -1
  84. package/dist/scaffold/templates.js +3 -1
  85. package/dist/scaffold/templates.js.map +1 -1
  86. package/dist/server/AgentServer.d.ts +3 -0
  87. package/dist/server/AgentServer.d.ts.map +1 -1
  88. package/dist/server/AgentServer.js +20 -0
  89. package/dist/server/AgentServer.js.map +1 -1
  90. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  91. package/dist/server/CapabilityIndex.js +1 -0
  92. package/dist/server/CapabilityIndex.js.map +1 -1
  93. package/dist/server/routes.d.ts +6 -0
  94. package/dist/server/routes.d.ts.map +1 -1
  95. package/dist/server/routes.js +115 -3
  96. package/dist/server/routes.js.map +1 -1
  97. package/package.json +1 -1
  98. package/src/data/builtin-manifest.json +65 -65
  99. package/src/scaffold/templates.ts +3 -1
  100. package/upgrades/1.3.729.md +92 -0
  101. package/upgrades/1.3.730.md +112 -0
  102. package/upgrades/side-effects/standby-write-reconciliation.md +62 -0
  103. package/upgrades/side-effects/swap-continuity-antithrash.md +118 -0
@@ -0,0 +1,92 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ The multi-machine mesh ran two contradictory write models at once: the
9
+ one-awake model flips the WHOLE StateManager read-only on every non-lease-holder
10
+ (one process-wide boolean), while the active-active session pool deliberately
11
+ places owned, serving sessions on those same "standby" machines. F9's sharpest
12
+ line: a machine can be read-only ("standby") while actively OWNING pool topics —
13
+ so its writes about sessions it owns and serves were blocked
14
+ (`[SessionManager] Failed to record build context … StateManager is read-only`),
15
+ and every added machine became a standby-shaped writer-that-can't-write.
16
+ Separately, the P2-6 family showed mutating routes (`POST /evolution/actions`,
17
+ `POST /attention`) hanging open-endedly instead of refusing.
18
+
19
+ Per `docs/specs/standby-write-reconciliation.md` (review-converged round 3,
20
+ approved), writes are now classified into a write **domain**
21
+ (machine-local / session-scoped / topic-scoped / cluster-shared) by a single
22
+ source of truth (`WriteDomainRegistry`) and admitted by ONE synchronous
23
+ in-memory decision point (`WriteAdmission` — no fs/network/LLM on the admission
24
+ path; p99 < 1ms pinned by test):
25
+
26
+ - **machine-local** writes admit EVERYWHERE — even on a read-only standby (the
27
+ F9 fix: the per-machine build-context write is no longer blocked).
28
+ - **session-/topic-scoped** writes admit iff THIS machine owns the record per
29
+ the session-pool FSM's single-owner guarantee (boot-warmed `OwnershipIndex`
30
+ + onCommit hook on BOTH ownership substrates); a not-owner refusal NAMES the
31
+ owner.
32
+ - **cluster-shared** keeps byte-identical authority to today: holder admits,
33
+ standby refuses.
34
+ - Every inadmissible write gets a **typed, machine-readable 409 refusal**
35
+ (`error/code/domain/scope/owner/leaseHolder/asOf/retryable` + `Retry-After`)
36
+ in well under 2s — never a hang; refuse-before-touch (a refused write mutates
37
+ nothing). The legacy read-only message string is preserved for log scraping.
38
+ - `SessionBuildContextStore` re-keyed per machine (§3.3) with a one-time
39
+ lease-holder legacy-key cleanup; `FileClassifier` sync exclusions for
40
+ attention-items + evolution (the second convergence axis).
41
+ - Observability: `GET /write-admission` (mode, per-domain counters, recent
42
+ refusals, ownership-index stats) + an event-loop-lag gauge on the AUTHED
43
+ `/health` extension only. Refusal storms raise exactly ONE deduped attention
44
+ item (burst invariant pinned by test).
45
+ - Ships dev-gated dark (`multiMachine.writeAdmission` — enabled OMITTED →
46
+ resolveDevAgentGate) AND dryRun-first with a §9.14 double latch: config
47
+ `dryRun:false` alone grants NO refusal authority until the write-surface
48
+ inventory constant is flipped in code. On the fleet nothing changes; the
49
+ legacy blanket guard keeps enforcing until graduation. Rollback: set
50
+ `multiMachine.writeAdmission.enabled` to false (or leave the gate dark).
51
+
52
+ ## What to Tell Your User
53
+
54
+ <!-- audience: agent-only, maturity: experimental -->
55
+ - **My machines no longer block their own homework (experimental, dark)**: when
56
+ I run on more than one machine, a machine that isn't "in charge" used to be
57
+ forbidden from writing ANY of its own notes — even notes about conversations
58
+ it was actively serving. Now each write is checked against who actually owns
59
+ that piece of work: notes about this machine's own work always go through,
60
+ notes about work another machine owns are politely declined with a clear,
61
+ fast "machine X owns this, retry shortly" answer instead of hanging forever.
62
+ Nothing changes on your setup yet — this ships dark and observation-first,
63
+ and I can turn it on deliberately when it has soaked.
64
+
65
+ ## Summary of New Capabilities
66
+
67
+ | Capability | How to Use |
68
+ |-----------|-----------|
69
+ | Ownership-scoped write admission (dev-gated dark, dryRun-first) | `multiMachine.writeAdmission` in `.instar/config.json` (enabled omitted → live on dev agents, dark on fleet) |
70
+ | Typed 409 write refusal with Retry-After | Automatic on inadmissible writes when live; body carries code/domain/owner/leaseHolder |
71
+ | Write-admission observability | `GET /write-admission` (Bearer; 503 when dark) |
72
+ | Event-loop-lag gauge | Authed `GET /health` extension only |
73
+
74
+ ## Evidence
75
+
76
+ - Root cause grounded live (2026-07-02, Laptop stderr): `[SessionManager]
77
+ Failed to record build context … StateManager is read-only (this machine is
78
+ on standby). Blocked: set` — a write about a session this machine OWNS,
79
+ blocked by the blanket lease boolean (F9).
80
+ - Spec converged round 3 (0 MUST-FIX / 0 SHOULD-FIX; external gemini-2.5-pro
81
+ CONVERGE), approved under standing Session-A preapproval (topic 29836).
82
+ - All three test tiers shipped and green: unit (110 tests — §3.2 verdict table
83
+ both sides, typed-refusal contract, ownership-index parity on BOTH
84
+ substrates, registry I9 story validation, StateManager one-way attach +
85
+ fail-toward-today, conformance ratchet + write-surface inventory baseline),
86
+ integration (13 tests — P2-6 family 201s on a standby-that-owns, typed 409 +
87
+ Retry-After through the real HTTP pipeline in <2s, refuse-before-touch store
88
+ snapshot, dark = 503), e2e lifecycle (8 tests — production init path,
89
+ live-on-dev 200 / fleet-dark 503, §9.14 double latch at the production
90
+ constant, F9 fix alive end-to-end, burst invariant ≤1 attention item).
91
+ - I2 hard properties pinned by test: ZERO fs on the admission path (including
92
+ negative lookups) and p99 < 1ms over 10k evaluate calls.
@@ -0,0 +1,112 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ The proactive subscription-account swap could thrash: on 2026-07-02 one dev
9
+ agent executed 36 proactive swaps in 8 waves, round-tripping the same sessions
10
+ between the same hot accounts and repeatedly killing six parallel build
11
+ subagents mid-task — the continuity mechanism became the primary source of
12
+ work destruction. Per `docs/specs/swap-continuity-antithrash.md` (converged
13
+ round 6, approved), two composable pieces now bind at the swap chokepoints:
14
+
15
+ **Piece 1 — anti-thrash brakes** at the proactive DECISION chokepoint
16
+ (`ProactiveSwapMonitor`), engine `SwapAntiThrashEngine`:
17
+
18
+ - **All-hot brake**: a proactive swap is REFUSED unless at least one eligible
19
+ target measures under the target ceiling (threshold − headroom, default
20
+ 65%). When every account is hot, staying put wins — one reactive rescue
21
+ strictly dominates N pointless kills.
22
+ - **Per-session dwell** (default 45 min): a just-swapped session is not moved
23
+ again, restart-safe via the new durable decision ledger
24
+ (`state/swap-ledger.jsonl`, single writer `SwapLedger`, O(1) segment
25
+ rotation).
26
+ - **Target-materially-better**: a validity gate (quota reading PRESENT and
27
+ FRESH — an unmeasured account is never "0% cool", it counts toward all-hot)
28
+ plus absolute ceiling plus a 15-point improvement floor, in a normative
29
+ filter→score→verify order; the checked target IS the executed target
30
+ (`onQuotaPressure` gains an explicit-target funnel contract that
31
+ revalidates, never re-selects — closing the double-threshold seam).
32
+ - **Two-tier thrash breaker** (T1 inversion / T2 rotation frequency) with
33
+ episode continuation, restart-proof derivation from the ledger, and ONE
34
+ deduped attention item per episode; execution failures write `failed` rows
35
+ with per-session backoff + streak escalation (the previously-silent
36
+ discarded reactive result promise is now observed).
37
+ - Untagged sessions leave the proactive candidate set entirely (a background
38
+ optimizer must never mutate the default-slot binding). The REACTIVE
39
+ continuity guarantee is untouched (I6) — a genuinely walled session still
40
+ swaps, breaker/dwell notwithstanding.
41
+
42
+ **Piece 2 — the in-flight work gate** (`SwapWorkGate`) at the
43
+ `SessionRefresh.refreshSession` funnel, the primitive every session-killing
44
+ mutation flows through:
45
+
46
+ - A tri-state work probe (`SessionManager.checkSessionWorkState` — pane
47
+ footer + child-process legs, async + coalesced, one shared `ps` snapshot
48
+ per ~2s TTL) plus the subagent leg (`SubagentTracker`); uncertainty
49
+ resolves BUSY for optimization callers (I7; an id-less session is exiled,
50
+ never blindly killed).
51
+ - Caller classes: `proactive-swap` DEFERS (retried each tick through the full
52
+ brake pipeline, dropped at a 30-min ceiling — the wall wins);
53
+ `reactive-swap` gets a bounded grace (default 120 s, proceeds at the first
54
+ not-busy observation) then proceeds WITH the F3 mitigations (enumerated
55
+ killed subagents + the last unanswered inbound re-injected as a quoted-data
56
+ envelope); `interactive-refresh` (the default for every unlisted caller)
57
+ gets a structured refusal; `recovery` is exempt.
58
+ - `POST /sessions/refresh` answers busy PRE-202 with HTTP 409
59
+ `session-busy` (counts and ages only) and gains optional `force: true` —
60
+ force overrides ONLY the work gate, never the rate guard, and is recorded
61
+ bearer-level in the ledger.
62
+ - The model-swap idle check gains a SUBAGENT leg behind its own micro-flag
63
+ (`models.tierEscalation.subagentIdleLeg`, concrete default false — dark;
64
+ graduates on its own rung).
65
+
66
+ Observability: `GET /subscription-pool/proactive-swap` grows additive
67
+ `brakes`/`deferrals` blocks (refusal counters, breaker state, ledger
68
+ writability, hydration honesty); both pieces register in the guard manifest.
69
+
70
+ Rollout: antiThrash resolves `enabled:true, dryRun:true` when absent (dry-run
71
+ ledger rows only — decision behavior stays byte-identical to v1.3.722 until a
72
+ deliberate `dryRun:false`); `swapContinuity` ships with the enabled key
73
+ OMITTED (dev-agent gate: live-in-dry-run on a development agent, dark on the
74
+ fleet) and is registered in `DEV_GATED_FEATURES`. Rollback:
75
+ `antiThrash.dryRun:true` restores legacy decisions; removing the dev agent's
76
+ swapContinuity enablement un-wires the gate entirely.
77
+
78
+ ## What to Tell Your User
79
+
80
+ <!-- audience: agent-only, maturity: experimental -->
81
+ - **My account-switching can no longer destroy my own work (experimental,
82
+ dark)**: when I juggle several subscription accounts, I used to sometimes
83
+ bounce conversations back and forth between two nearly-full accounts —
84
+ every bounce restarted the session and killed helpers working in the
85
+ background. Now the switcher has brakes: if every account is nearly full I
86
+ stay put, a just-moved conversation rests before it can move again, and a
87
+ move only happens when the destination is genuinely much cooler. Separately,
88
+ a switch will wait for in-flight work to finish instead of cutting it down
89
+ mid-task — and on the rare forced switch, the resumed conversation is told
90
+ exactly which helpers were interrupted and which message still needs an
91
+ answer. Nothing changes on your setup yet — the brakes observe first, and
92
+ the work protection ships dark until it has soaked.
93
+
94
+ ## Summary of New Capabilities
95
+
96
+ | Capability | How to Use |
97
+ |-----------|-----------|
98
+ | Anti-thrash brakes on the proactive account swap (dry-run soak by default) | `subscriptionPool.proactiveSwap.antiThrash` in `.instar/config.json` (absent block = enabled + dry-run); live only at a deliberate `dryRun: false` |
99
+ | Durable swap-decision ledger | `state/swap-ledger.jsonl` (single writer; restart-safe dwell/breaker state) |
100
+ | Brake/deferral observability | `GET /subscription-pool/proactive-swap` → additive `brakes` / `deferrals` blocks |
101
+ | In-flight work gate on session-killing mutations (dev-gated dark, dry-run first) | `subscriptionPool.swapContinuity` (enabled omitted → live on dev agents, dark on fleet); restart-required |
102
+ | Pre-202 busy refusal + force on refresh | `POST /sessions/refresh` → 409 `session-busy` when busy; re-issue with `force: true` to override the gate (never the rate guard) |
103
+ | Model-swap subagent idle leg (dark micro-flag) | `models.tierEscalation.subagentIdleLeg: true` (default false; own rollout rung) |
104
+
105
+ ## Evidence
106
+
107
+ - Spec: `docs/specs/swap-continuity-antithrash.md` (converged round 6 — 0
108
+ CRITICAL / 0 MAJOR; approved under standing Session-A preapproval, topic
109
+ 29836). Plain-English companion: `docs/specs/swap-continuity-antithrash.eli16.md`.
110
+ - Incident data: `logs/server.log` (echo dev agent, v1.3.722, 2026-07-02 —
111
+ 36 executed proactive swaps / 72 account-swap lines / 8 waves).
112
+ - Side-effects review: `upgrades/side-effects/swap-continuity-antithrash.md`.
@@ -0,0 +1,62 @@
1
+ # Side-Effects Review — Standby-Write Reconciliation + Typed Refusal (P2-6 / F9)
2
+
3
+ **Spec:** docs/specs/standby-write-reconciliation.md (converged round-3 + approved — operator standing preapproval, topic 29836). **Parent:** A Refusal Stays a Refusal — a write the server cannot complete returns an immediate typed refusal or a bounded error, never an open-ended hang.
4
+ **Ships dev-gated DARK** (`multiMachine.writeAdmission.enabled` OMITTED from ConfigDefaults → resolveDevAgentGate: live on a development agent, dark on the fleet) **and dry-run FIRST** (`dryRun: true` even on dev — FD-7 telemetry pattern). Refusal authority is DOUBLE-latched: `dryRun:false` AND the compiled `WRITE_SURFACE_INVENTORY_COMPLETE` constant (§9.14), which ships `false` and is flipped only by the wave-2 PR that lands the reviewed write-surface inventory — config alone can never grant authority.
5
+ **Files:** src/core/WriteAdmission.ts (new), src/core/WriteDomainRegistry.ts (new), src/core/StateManager.ts, src/core/SessionBuildContextStore.ts, src/core/SessionManager.ts, src/core/LocalSessionOwnershipStore.ts, src/core/SessionOwnershipRegistry.ts, src/core/FileClassifier.ts, src/core/devGatedFeatures.ts, src/core/types.ts, src/core/PostUpdateMigrator.ts, src/config/ConfigDefaults.ts, src/monitoring/guardManifest.ts, src/scaffold/templates.ts, src/server/AgentServer.ts, src/server/routes.ts, src/commands/server.ts
6
+
7
+ ## What changed
8
+
9
+ 1. **WriteAdmission.ts (new):** the single synchronous, in-memory admission decision point (I2 — zero fs/network/LLM/await on the admission path). Implements the §3.2 decision table split by domain: `machine-local` admits everywhere; `cluster-shared` rides the legacy lease boolean verbatim; `session-scoped` admits unbound/absent/released (today-equivalent to the pool-active carve-out — M2 reachability) and refuses only a record positively naming another machine; `topic-scoped` absent/released collapses to the legacy lease boolean (§9.18 — I4 by construction) unless an I9-audited absent-window story opts in. Owns the boot-warmed `OwnershipIndex` (one synchronous `store.all()` scan at construction + the interface-level `onCommit` hook, hook-registered BEFORE the warm so no commit can interleave) with ingest validation (round-2 L1: malformed ⇒ `ownership-unresolved` fail-closed, never `not-owner` with `owner:null`). Typed refusals per §3.4 (409 + Retry-After, refusal bodies from LOCAL knowledge only — I7). Route-seam throw splits by domain (§9.16: machine-local proceeds, scoped/shared refuse typed `admission-error`); store-seam throw falls back to the legacy verdict and is recorded (`noteStoreSeamError`). §6 observability: per-domain counters, ≤50-entry refusal ring, bounded+rotated `logs/write-admission.jsonl` (buffered — flushed OFF the admission path), the `monitorEventLoopDelay` gauge (the P2-6 hang-attribution instrument), and the ≥N-per-window ONE-deduped-attention-item aggregate (flood lesson).
10
+ 2. **WriteDomainRegistry.ts (new):** the single source of truth (ops / EXACT kv keys / route prefixes → domain + two-axis convergence story). I9 enforced structurally: a machine-local entry with no story, or on a git-synced shared path with no file-level arm, is REFUSED the classification and downgraded to cluster-shared (recorded in `refusedClassifications`). Exports `WRITE_SURFACE_INVENTORY_COMPLETE=false` (the §9.14 latch), `sessionBuildContextKeyFor` (charset-jailed per-machine key), and `buildWriteDomainRegistry` — the ONE builder the server wires and the tests read (PR-#334 registry↔wiring identity).
11
+ 3. **StateManager.ts:** `guardWrite` gains `key`/`scope` opts and delegates to the attached admission layer; `'legacy'` verdicts fall through to the blanket boolean verbatim; refusals throw `WriteRefusedError` (legacy message string preserved for log-scraping continuity). `attachWriteAdmission` is ONE-WAY (second attach throws) and lands before routes are wired (§3.2 pre-construction window); pre-attach behavior is byte-identical to today. New `sessionPoolActive` getter.
12
+ 4. **SessionBuildContextStore.ts + SessionManager.ts + server.ts:** the store is re-keyed PER MACHINE (`session-build-context-<jailed-machine-id>`, literal `local` on identity-less installs — round-2 L3: coordinator/mesh identity, never the caller-less `StateManager.setMachineId`), via a late-resolving getter threaded through SessionManagerOptions. This is the F9 fix (the 2026-07-02 "Failed to record build context … read-only" stderr line). The LEGACY shared key stays unclassified (cluster-shared, I8), self-drains (6h max age), and gets a one-time lease-holder cleanup at boot.
13
+ 5. **LocalSessionOwnershipStore.ts + SessionOwnershipRegistry.ts:** `onCommit` added to the `SessionOwnershipStore` INTERFACE (round-2 S4) and fired at each substrate's own mutation point (Local: `persist()` after `cache.set`; InMemory: `casWrite()` at `recs.set`) — both mutation paths (`registry.cas()`, `OwnershipApplier`) funnel through `casWrite`, so the index can never miss a local transition. A listener throw never fails the CAS (guarded).
14
+ 6. **FileClassifier.ts:** `.instar/state/attention-items.json` + `.instar/state/evolution/` added to the git-sync exclusions (the I9 second-axis wave-1 build item, round-2 S1) — deliberately NOT the whole `.instar/state/` tree.
15
+ 7. **routes.ts:** the route seam `refuseInadmissibleWrite` — admission is the FIRST await-free statement after body validation (I1) on the wave-1 P2-6 families (`POST/PATCH /evolution/*`, `POST/PATCH /attention*`), BEFORE the tone gate on `POST /attention` (I3 refuse-before-touch). `GET /write-admission` (503 when dark) + the event-loop gauge on the AUTHED `/health` extension ONLY (§6 — a load oracle is never handed to an unauthenticated caller).
16
+ 8. **server.ts:** production wiring — gate → construct (registry + real ownership store boot-warm + attention hook + logDir) → one-way attach → AgentServer/routes. Init failure is non-fatal (layer stays null → route 503s, legacy guard keeps enforcing).
17
+ 9. **guardManifest.ts / devGatedFeatures.ts / ConfigDefaults.ts / types.ts:** the `writeAdmission` posture row (`loadBearing:false` while the legacy guard enforces — re-reviewed at fleet graduation), the DEV_GATED_FEATURES registration, the SHARED_DEFAULTS block (no `enabled:` literal — the #1001 anti-pattern avoided), the `WriteAdmissionConfig` type.
18
+ 10. **PostUpdateMigrator.ts + templates.ts:** the Write Admission CLAUDE.md section (Agent Awareness), shipped to new agents via `generateClaudeMd` and to existing agents via the content-sniffed idempotent `migrateClaudeMd` patch. Honestly tagged experimental/dry-run (Maturity Honesty).
19
+
20
+ ## Blast radius
21
+
22
+ - **Dry-run carries ZERO authority.** In every shipped configuration (fleet dark; dev live+dryRun) the legacy blanket guard keeps enforcing byte-identically; the new layer only evaluates and logs would-verdicts. The only execution-path deltas that are live everywhere are (a) the SessionBuildContextStore per-machine RE-KEY (single-writer-per-file by construction; the legacy key self-drains) and (b) the FileClassifier sync exclusions (both stores already wrote locally on every machine — the exclusion closes a PRE-EXISTING git-sync fork surface, §3.1).
23
+ - **Fork-safety never weakens (I4).** cluster-shared refuses on every non-holder in EVERY mode; topic-scoped absent/released IS the legacy boolean; session-scoped only ever TIGHTENS relative to the existing pool-active carve-out. Single-machine agents: strict no-op (I6).
24
+ - **A broken guard fails toward today.** Store seam: any admission throw → legacy verdict + recorded occurrence. Route seam: machine-local proceeds (refusing would create a NEW outage for writes safe everywhere), scoped/shared refuse typed `admission-error` (a broken guard must not enable a fork). Construction failure at boot: layer null, route 503, legacy guard enforcing.
25
+ - **No egress, no spend, no new HTTP mutation surface.** The only new route is the read-only `GET /write-admission`. The aggregate attention item is throttled to ONE per (surface, code, direction) window.
26
+
27
+ ## Risk + mitigation
28
+
29
+ - **Risk:** the admission path regresses request latency or does I/O. **Mitigation:** I2 enforced by test (fs spied on every admission call including negative lookups; p99 <1ms over 10k calls); log rows are buffered and flushed on a timer OFF the admission path.
30
+ - **Risk:** the ownership index diverges from the store. **Mitigation:** hook-then-warm ordering inside a synchronous constructor; parity tests run the SAME cas/applier sequences against BOTH shipped substrates and assert index≡store.
31
+ - **Risk:** `dryRun:false` leaks refusal authority before the wave-2 inventory is reviewed. **Mitigation:** the compiled `WRITE_SURFACE_INVENTORY_COMPLETE=false` latch — config cannot flip it; proven by the e2e double-latch test at the production constant.
32
+ - **Risk:** a refusal storm floods Telegram topics. **Mitigation:** aggregate-only alerting (≥N per window ⇒ ONE deduped item), proven by the e2e burst-invariant test (40 refusals ⇒ exactly 1 item).
33
+ - **Risk:** the re-key strands existing build-context entries. **Mitigation:** entries carry a 6h max age (self-drain); restore is inherently machine-local (worktree paths are per-disk), so no cross-machine read ever needed the shared key; one-time lease-holder cleanup removes the inert file.
34
+
35
+ ## Migration parity
36
+
37
+ - Config: the `multiMachine.writeAdmission` block lives in SHARED_DEFAULTS, which feeds BOTH `getInitDefaults` (new agents) and `getMigrationDefaults` → `applyDefaults` (existing agents, existence-checked, `_instar_noMigrate`-respecting, idempotent). No `enabled:` literal (dev-gate rides resolveDevAgentGate).
38
+ - CLAUDE.md: `WRITE_ADMISSION_CLAUDEMD_SECTION` shared by `generateClaudeMd` (new) and the content-sniffed `migrateClaudeMd` patch (existing) — single source, can't drift.
39
+ - No hook/skill/wire-protocol changes; old-version PEERS need nothing (admission is machine-local, no cross-machine dependency — the F4 mixed-pair row in §5).
40
+
41
+ ## Dark-gate line-map
42
+
43
+ - The new `multiMachine.writeAdmission` block (11 lines, no `enabled:` literal) shifts every multiMachine-onward `enabled: false` attribution DOWN by +11 — the EXPECTED map in tests/unit/lint-dev-agent-dark-gate.test.ts is updated and re-verified via the attributor (866→877, 870→881, 877→888, 887→898, 1124→1135, 1150→1161, 1160→1171, 1189→1200, 1377→1388, 1518→1529, 1563→1574, 1588→1599). `writeAdmission` registered in DEV_GATED_FEATURES.
44
+
45
+ ## Rollback
46
+
47
+ - Fleet: already dark (gate resolves off) — zero presence, route 503s. Dev: set `multiMachine.writeAdmission.enabled: false` (force-dark even on a dev agent) → the layer is never constructed, StateManager keeps the legacy blanket verdict. The re-key rollback is inert by design: reverting to the legacy key merely resumes the old shared-file behavior; per-machine files self-drain in 6h.
48
+
49
+ ## Tests (all three tiers)
50
+
51
+ - **Tier 1 (unit):** tests/unit/write-admission.test.ts (37 — the full §3.2 verdict table: every domain × role × ownership state × pool-active × dryRun, both sides of every boundary; I2 fs-spy + p99 timing; refusal schema + Retry-After; route/store throw splits), tests/unit/write-admission-ownership-index.test.ts (10 — parity across arbitrary cas/applier sequences against BOTH substrates; ingest validation; boot-warm completeness), tests/unit/write-domain-registry.test.ts (16 — I9 both axes; exact-key kv; legacy key stays cluster-shared), tests/unit/state-manager-write-admission.test.ts (11 — delegation, one-way attach, legacy fallback on throw, journal path jail survives), tests/unit/write-domain-conformance-ratchet.test.ts (6 — the §3.5 ratchet over routes.ts + the recorded TODO-classify baseline in tests/fixtures/write-surface-inventory.json, down-only), tests/unit/session-build-context-store.test.ts (re-key + per-machine isolation), tests/unit/lint-dev-agent-dark-gate.test.ts (line-map).
52
+ - **Tier 2 (integration):** tests/integration/write-admission-routes.test.ts (13 — the wave-1 families 201 on a standby-that-owns-topics; cluster-shared 409 typed + Retry-After + zero store mutation (I3 store snapshot); dryRun legacy behavior + would-verdict rows; admission-throw per-seam fail directions).
53
+ - **Tier 3 (e2e):** tests/e2e/write-admission-lifecycle.test.ts (8 — production init path with the REAL gate/registry/on-disk ownership store; live-on-dev 200 / fleet-dark 503 / explicit-false force-dark; the §9.14 double latch at the PRODUCTION constant; the F9 fix end-to-end on a read-only standby; burst invariant ≤1 attention item; GUARD_MANIFEST row + GET /guards posture row over real disk config).
54
+
55
+ ## CI follow-up (2026-07-03): discoverability + delivery-completeness classification
56
+
57
+ Two conformance ratchets (unit shards 1 + 4) required deliberate classifications the feature commit had not made — both are metadata-only, zero behavior change:
58
+
59
+ - **`/write-admission` route prefix → `INTERNAL_PREFIXES`** (src/server/CapabilityIndex.ts): the read-only status surface is dev-gated dark (503 on the fleet) and agent-surfaced via the CLAUDE.md "Write Admission" awareness section rather than `/capabilities` discovery — same class as `/mesh-selfheal` and `/worktrees/agent-reaper`. No route, verdict, or admission behavior changes.
60
+ - **`'Write Admission'` migrator section → tracked in `legacyMigratorSections`** (tests/unit/feature-delivery-completeness.test.ts): records the template+migrator parity already implemented via the shared `WRITE_ADMISSION_CLAUDEMD_SECTION` (dark/dry-run, not framework-shadowed — same class as 'Mesh Self-Healing').
61
+
62
+ Side effects: none (a capability-index classification entry + a test allowlist entry). Rollback: revert the two hunks.
@@ -0,0 +1,118 @@
1
+ # Side-Effects Review — swap-continuity anti-thrash brakes + in-flight work deferral
2
+
3
+ Spec: `docs/specs/swap-continuity-antithrash.md` (converged round 6, approved).
4
+ Roadmap: Session A item 4.4 + operator-priority thrash brake (2026-07-02 incident).
5
+
6
+ ## What changes at runtime
7
+
8
+ 1. **New durable state file**: `state/swap-ledger.jsonl` (single writer:
9
+ `SwapLedger`; O(1) segment rotation, 10 MB × keep 2). Absence = cold start
10
+ (dwell un-primed for the first 45 min — the §9 accepted one-time gap).
11
+ 2. **Proactive swap decisions** (only when `subscriptionPool.proactiveSwap.enabled`
12
+ is already opted into — fleet-dark today):
13
+ - antiThrash resolves `enabled:true, dryRun:true` when the block is absent →
14
+ the ONLY immediate effect on an opted-in install is dry-run ledger rows +
15
+ status counters (rung-1 honesty, §10). Decision behavior stays
16
+ byte-identical to v1.3.722 until a deliberate `dryRun:false`.
17
+ - Live (`dryRun:false`): untagged sessions leave the proactive candidate set
18
+ (I10); refusals (all-hot / dwell / no-material-target / target-unmeasured /
19
+ reversal / thrash-breaker / ledger-lost) bind; the executed target is the
20
+ brake-checked target (I1) with execute-time revalidation.
21
+ 3. **Reactive path**: decision behavior byte-identical (I6). Additive
22
+ observation only — reactive swaps write ledger rows; execution failures
23
+ (previously a silently-discarded promise) write `failed` rows and can raise
24
+ ONE deduped attention item per streak/hop-cascade/rate-cap-strand.
25
+ 4. **Session-killing mutations** (`SessionRefresh.refreshSession`): a work gate
26
+ consults in-flight state when `swapContinuity` resolves live (dev-agent gate
27
+ — key OMITTED from shipped config, dark on fleet). Caller classes:
28
+ proactive-swap defers; reactive-swap gets a bounded grace (≤120 s) then
29
+ proceeds WITH mitigations; interactive-refresh (the default for every
30
+ unlisted caller) gets a structured `session-busy` refusal + `force`;
31
+ recovery is exempt. Forced kills carry the F3 mitigation payload
32
+ (enumerated killed subagents + re-injected unanswered inbound, quoted-data
33
+ envelope).
34
+ 5. **HTTP surface**: `/subscription-pool/proactive-swap` gains additive
35
+ `brakes`/`deferrals` blocks; `/sessions/refresh` gains a pre-202 409
36
+ `session-busy` refusal + optional `force:true` (bearer-level, recorded as
37
+ such). No route renamed; nothing 503s that didn't.
38
+ 6. **Attention items**: one per thrash episode / failure streak /
39
+ measurement-blind episode / ledger-loss episode — episode-deduped (P17),
40
+ restart-proof via episodeId derivation from the ledger.
41
+
42
+ ## Failure directions (the load-bearing ones)
43
+
44
+ - Ledger UNWRITABLE ⇒ proactive optimization PAUSES (`ledger-lost`,
45
+ counter-only), level-triggered resume + one `outage-summary` row; reactive
46
+ untouched (I12).
47
+ - Quota reading ABSENT/STALE ⇒ NOT under the ceiling; counts toward all-hot;
48
+ never treated as 0% (bound 0, R4-M1). Whole-pool blindness pauses the
49
+ optimizer AND says so (I13).
50
+ - Work-gate uncertainty ⇒ BUSY for optimization callers ('absent' behaves like
51
+ 'indeterminate', R5-M1), busy-for-grace for reactive (never strands — the
52
+ grace deadline always proceeds).
53
+
54
+ ## Rollback levers
55
+
56
+ - `antiThrash.dryRun:true` restores v1.3.722 decision behavior (keeps
57
+ observability). `antiThrash.enabled:false` disables the engine's binding
58
+ entirely (legacy path incl. legacy 10-min cooldown).
59
+ - Removing the dev agent's `swapContinuity` enablement un-wires the gate
60
+ (SessionRefresh reverts to unconditional kill). `subagentIdleLeg` stays a
61
+ concrete `false` default on the model-swap block (rung 3a flip only).
62
+ - Deleting `state/swap-ledger.jsonl` = cold start (bounded, logged, §9).
63
+
64
+ ## Blast radius
65
+
66
+ Machine-local by design (§8): per-machine ledger, per-machine breaker, no
67
+ cross-machine state. No egress, no spend, no credential surface. The only
68
+ processes touched are the agent's own tmux sessions, and only through the
69
+ pre-existing SessionRefresh funnel with strictly ADDED protection.
70
+
71
+ ## Wiring completion (second commit of this build)
72
+
73
+ The core modules landed first (2866f1073); the wiring commit binds them:
74
+ server.ts spine (unconditional ledger+engine hydration, live-knob getters,
75
+ scheduler/monitor/SessionRefresh hooks), the SessionManager tri-state work
76
+ probe (shared ps snapshot), the /sessions/refresh pre-202 409 + force,
77
+ the AgentServer ModelSwapService subagent-leg probe (Q5 dark micro-flag),
78
+ DEV_GATED_FEATURES + guard-manifest registration, CLAUDE.md template +
79
+ PostUpdateMigrator awareness, and the release-note fragment. No new runtime
80
+ side effects beyond those reviewed above — this section records that the
81
+ dark/dry-run posture described above is now actually reachable end-to-end.
82
+
83
+ ## CI-green follow-up (third commit of this build)
84
+
85
+ Scope: zero-runtime-behavior commit that turns the two red unit shards green.
86
+ (1) Two pre-existing tests asserted the PRE-brake `/sessions/refresh` call
87
+ shape (`refreshSession({ sessionName, followUpPrompt, reason, fresh })` and
88
+ the exact-args spy without `force`); both now assert the documented §4.5
89
+ shape (fresh forwarded, `force` pinned to a strict boolean, default false).
90
+ (2) The no-silent-fallbacks ratchet counted 503 vs baseline 491: a
91
+ base-vs-HEAD set-diff of the exact test heuristic showed the PR added 13
92
+ genuinely-new flagged catches (every other +entry is a symmetric line-shift
93
+ artifact — 59/59 commands/server.ts, 31/31 AgentServer.ts, 19/19 routes.ts,
94
+ 7/7 SessionManager.ts, 5/5 PostUpdateMigrator.ts). All 13 are the feature's
95
+ DESIGNED fail-safes, each failing toward the safe direction: SwapWorkGate
96
+ probe legs → indeterminate reads BUSY (I7, defer — never a wrong kill);
97
+ SessionRefresh work-gate knob/probe/mitigation arms → gate-dark or refusal
98
+ (the gate is ADDITIVE, §4.5 — a broken gate must never block a refresh, and
99
+ mitigations never gate the respawn); ProactiveSwapMonitor safe-knob reads →
100
+ feature-dark (legacy behavior holds); SwapLedger append/hydrate → loud warn
101
+ + I12 optimization pause + counted loss classes (R4-m1), partial hydration
102
+ surfaced via coveredWindow/corrupt. Each now carries an in-brace
103
+ `@silent-fallback-ok` justification; the heuristic count returns to 490
104
+ (≤ 491 baseline) — no baseline bump.
105
+
106
+ Side-effects review of THIS commit: over-block — none (comments + test
107
+ expectations only; no input is newly rejected). Under-block — none (no
108
+ detector logic changed). Abstraction fit — the tags are the ratchet's
109
+ designed exemption mechanism for reviewed fail-safes. Signal-vs-authority —
110
+ no authority added; the tagged catches remain signal-preserving fail-safes
111
+ (refusals/warns/counters are the loud surfaces). Interactions — none; the
112
+ tags are inert at runtime. External surfaces — none. Multi-machine — n/a
113
+ (no behavior change; feature posture unchanged, machine-local by design §8).
114
+ Rollback — revert the commit; nothing durable depends on it. Second-pass —
115
+ not required: although the touched files include gate/monitor modules, the
116
+ source diff is comment-only (verified: `git diff` shows no executable-line
117
+ changes in src/ beyond comment insertion) and the test diff asserts the
118
+ already-reviewed §4.5 behavior.