instar 1.3.557 → 1.3.558

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.
@@ -0,0 +1,149 @@
1
+ # Side-Effects — multiMachine.seamlessness coherence flags → dev-gated (topic 13481), sessionPool master HELD
2
+
3
+ **Change (PR2 of the no-dark-on-dev multi-machine directive):** Re-gate the 5
4
+ `multiMachine.seamlessness` coherence flags — `ws3OneVoice` (one-voice election),
5
+ `ws13Reconcile` (ownership reconcile), `ws41DurableAck` (durable cross-machine /ack),
6
+ `ws43RoleGuard` (role-guard-at-spawn), `ws43JournalLease` (journal-lease cutover) — from
7
+ hardcoded `false` in `ConfigDefaults.ts` to the developmentAgent gate (live-on-dev /
8
+ dark-fleet), mirroring the merged `ws44PoolLinks` / `ws44PoolCache` precedent. The
9
+ `ws43JournalLeaseDryRun` sub-knob is computed COHERENTLY with its gate (dev → live/false,
10
+ fleet → dry-run/true). **The `multiMachine.sessionPool.*` master switch and its
11
+ `inboundQueue` / `holdForStability` sub-flags are deliberately HELD** (see #7).
12
+
13
+ **Driver:** Operator directive (Justin, topic 13481, 2026-06-13): "NOTHING should ship
14
+ dark on development agents. Everything fully functional so it actually gets tested and
15
+ doesn't just rot." PR1 (#1151) moved the 7 stateSync memory stores; PR2 is the remaining
16
+ dark-on-dev multi-machine seamlessness layers.
17
+
18
+ **Spec:** `docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md` (the converged + approved spec the
19
+ seamlessness flags were built against) + `docs/specs/multi-machine-replicated-store-
20
+ foundation.md` (sessionPool context). This is a follow-up gating fix to already-merged
21
+ features, not a new feature.
22
+
23
+ ## What was wrong
24
+
25
+ The 5 seamlessness flags shipped with a literal `false` in `ConfigDefaults.ts` (e.g.
26
+ `ws3OneVoice: false`). A written literal force-darks even a development agent (the #1001
27
+ anti-pattern), so the coherence layers never ran ANYWHERE — they could never be dogfooded
28
+ on Echo / the Mini and never graduated toward the fleet. ws44PoolLinks/ws44PoolCache had
29
+ already been corrected to the omitted-gate pattern; these five lagged behind.
30
+
31
+ ## What changed
32
+
33
+ - `src/config/ConfigDefaults.ts` — OMIT the hardcoded `false` for all 5 flags (each carries
34
+ a ws44-style "DELIBERATELY OMITTED" comment) so `resolveDevAgentGate` decides at runtime.
35
+ `ws43JournalLeaseDryRun` is ALSO omitted (computed coherently at the consumer). `ws13DryRun`
36
+ stays a plain hardcoded default `true` (it is the in-component "log intended CAS without
37
+ performing it" rung, NOT the dev-gate). The sessionPool block is UNTOUCHED.
38
+ - `src/commands/server.ts` — route each consumer read through `resolveDevAgentGate(...)`:
39
+ - `ws3OneVoice`: `SpeakerElection.enabled` → `resolveDevAgentGate(ws3Cfg().ws3OneVoice, config)`.
40
+ - `ws13Reconcile`: `OwnershipReconciler.enabled` → gated; `dryRun` stays the plain `ws13DryRun !== false` read.
41
+ - `ws43RoleGuard`: `scheduler.setRoleGuard().enabled` → gated.
42
+ - `ws43JournalLease`: `setJournalLeaseCutover().enabled` → gated; `dryRun` →
43
+ `cfg?.ws43JournalLeaseDryRun ?? !resolveDevAgentGate(undefined, config)` (coherent).
44
+ - the heartbeat `seamlessnessFlags` advert resolves journal-lease via the gate, not the raw read.
45
+ - `src/server/routes.ts` — `ws41DurableAckEnabled()` reads through
46
+ `resolveDevAgentGate(..., ctx.config)`, so the route + precedence guard agree.
47
+ - `src/core/devGatedFeatures.ts` — ADD all 5 flags as `DEV_GATED_FEATURES` entries
48
+ (by-name, each with a non-destructive/no-egress justification). A header comment records
49
+ WHY the sessionPool master + sub-flags are NOT moved here (the StageAdvancer stage-gate).
50
+ - `src/core/PostUpdateMigrator.ts` — NEW `migrateConfigSeamlessnessDevGate(config)`: strips
51
+ a default-shaped `false` per flag (and the paired `ws43JournalLeaseDryRun:true` only
52
+ alongside a default-shaped `ws43JournalLease:false`) so the gate resolves on update. An
53
+ operator-set value (explicit `true`, a divergent dryRun) is left ENTIRELY alone. Idempotent.
54
+ Wired into the migrate path with `upgraded`/`skipped` reporting.
55
+ - `tests/unit/lint-dev-agent-dark-gate.test.ts` — EXPECTED attribution map line numbers
56
+ recomputed via the attributor (the 5 removed literals + comment reflow shift sessionPool
57
+ 786→809 and cartographer 1100→1123). The 3 sessionPool flags STAY in the map (held,
58
+ hardcoded false). No paths added/removed (the seamlessness flags were never `enabled:`-named).
59
+
60
+ ## Decision-point inventory (Phase-4)
61
+
62
+ ### 1. Over-block
63
+ A gate that resolved live when it shouldn't. Not possible: `resolveDevAgentGate` resolves
64
+ true ONLY when `developmentAgent: true` (or an explicit `true`), so the fleet is byte-for-byte
65
+ unchanged. Each layer is additionally a strict single-machine no-op (the election never
66
+ engages below 2 online machines; the role-guard never fires when this machine holds the lease;
67
+ the journal-lease cutover requires ≥2 flag-coherent peers).
68
+
69
+ ### 2. Under-block
70
+ A consumer left on the raw `=== true` read would keep the feature DARK on a dev agent even
71
+ though ConfigDefaults omits it (the PR1 "still-dark-on-dev = incomplete" failure). Closed by
72
+ grepping ALL consumer sites (5 flags + the heartbeat advert + the dryRun coherence formula)
73
+ and routing every one through the gate, then pinning it with a source-string no-missed-consumer
74
+ wiring test (`tests/unit/seamlessness-dev-gate-wiring.test.ts`, section B) that fails CI if a
75
+ consumer reverts to the raw read.
76
+
77
+ ### 3. Level-of-abstraction fit
78
+ The gate is resolved at the SAME construction/route boundary as the ws44 precedent — one
79
+ funnel (`resolveDevAgentGate`) reads the raw config value and the agent's `developmentAgent`
80
+ flag. No new abstraction; the consumers' downstream `enabled`-true semantics are unchanged.
81
+
82
+ ### 4. Signal vs authority compliance
83
+ Each layer runs in the SAFE direction and none gains authority from going live-on-dev:
84
+ ws3OneVoice only WITHHOLDS a duplicate send (never fabricates one); ws13Reconcile runs in its
85
+ own dry-run (logs intended CAS, performs none); ws43RoleGuard can only REFUSE a spawn (never
86
+ wrongly spawn); ws41DurableAck persists an intent bound to the AUTHENTICATED operator and the
87
+ owner REVALIDATES at apply time; ws43JournalLease coordinates job claims over durable state
88
+ and the cutover gate guarantees the two mechanisms are never both live for a job set.
89
+
90
+ ### 5. Interactions
91
+ - ws43JournalLease ↔ ws43JournalLeaseDryRun: the two MUST resolve coherently or a dev agent
92
+ would run live-but-dry-run (logged, never exercised) — handled by the `?? !resolveDevAgentGate`
93
+ formula at the consumer AND the migration's paired strip.
94
+ - The heartbeat `seamlessnessFlags.ws43JournalLease` advert (what peers read to decide
95
+ coherence) now mirrors the resolved enabled+not-dry-run, so a dev agent honestly advertises
96
+ the capability to a peer.
97
+ - sessionPool HELD: because the seamlessness flags have NO stage-gate, they are independent of
98
+ the sessionPool master — flipping them live-on-dev does not activate the (held) session pool.
99
+
100
+ ### 6. External surfaces
101
+ None new. The five features' routes/behaviors already existed; this only changes WHICH agents
102
+ resolve them live. Replication/coordination is between the operator's OWN machines — no
103
+ external egress, no third-party spend, no new API.
104
+
105
+ ### 7. Rollback cost & multi-machine posture (the heart)
106
+ - **Rollback:** trivial and per-flag — set an explicit `multiMachine.seamlessness.<flag>: false`
107
+ in `.instar/config.json` to force-dark even a dev agent; an explicit `true` is the fleet-flip.
108
+ No data migration to undo (these are coordination layers, not stores). The 3 sessionPool flags
109
+ need no rollback — they were never flipped.
110
+ - **Multi-machine posture:** these layers EXIST to make the operator's two machines behave as
111
+ one agent. Live-on-dev means Echo (laptop) + the Mac Mini now genuinely exercise one-voice
112
+ election, ownership reconcile, durable cross-machine acks, the spawn-time role-guard, and the
113
+ journal-lease cutover across the real two-machine mesh — the only way they get tested before
114
+ fleet. A single-machine dev agent is a strict no-op for all five (no peers).
115
+ - **sessionPool HELD — the deliberate exception:** `multiMachine.sessionPool.{enabled,
116
+ inboundQueue.enabled, holdForStability.enabled}` were NOT moved to dev-gated. They share a
117
+ SECOND, structurally-enforced gate: `sessionPool.stage` is **StageAdvancer-write-only** (a
118
+ module-private `STAGE_WRITE_TOKEN`, rejected by `stageWriteGuard.ts` for any other writer),
119
+ defaults to `'dark'`, and advances only through an E2E-gated rollout ladder
120
+ (`dark → shadow → live-transfer → rebalance`). The activation expression EVERYWHERE is
121
+ `enabled && stage !== 'dark'` (server.ts:1990, 1998, 14351, 15611; boot-sweep gate 7019-7024).
122
+ So: (a) dev-gating `enabled` alone leaves the pool INERT on a dev agent (`stage` is still
123
+ dark) — a hollow move that fails the directive's "actually gets tested"; and (b) forcing
124
+ `stage` past dark in ConfigDefaults would BYPASS the deliberate cutover discipline the
125
+ StageAdvancer exists to enforce — not a clean, safe-by-default, reversible change. This is a
126
+ genuine collision between the "nothing dark on dev" directive and an explicit structural
127
+ safety invariant, so it is surfaced to the operator rather than forced. **Operator decision
128
+ needed:** advance `sessionPool.stage` on the dev mesh via the StageAdvancer ladder (gated on
129
+ green E2E) if active-active pooling should be dogfooded live; until then it stays dark.
130
+
131
+ ## Migration parity
132
+
133
+ `applyDefaults` is add-missing-only deep-merge, so a new agent gets the omitted-flag shape via
134
+ `init`. Existing agents that received the old `false` literals would keep them (explicit values
135
+ are not overwritten) and stay dark even on a dev agent — so `migrateConfigSeamlessnessDevGate`
136
+ strips the exact default-shaped `false` (and paired dryRun) on update. An operator's hand-edited
137
+ value is never touched (reach is not authority). Idempotent (a second run strips nothing).
138
+
139
+ ## Tests
140
+
141
+ Unit: `seamlessness-dev-gate-wiring` (13 — resolution + no-missed-consumer source-seam +
142
+ registry coherence), `PostUpdateMigrator-seamlessnessDevGate` (9), `lint-dev-agent-dark-gate`
143
+ (line-map recomputed), `devGatedFeatures-wiring` (74), `feature-delivery-completeness` (99),
144
+ `SpeakerElection` / `OwnershipReconciler` / `ws3-one-voice-wiring`. Integration/E2E: the
145
+ existing `scheduler-role-guard`, `scheduler-journal-lease-cutover`, `attention-remote-ack`
146
+ suites stay green; `attention-remote-ack-alive` E2E gains two dev-gate cases (flag OMITTED +
147
+ developmentAgent:true → route ALIVE; fleet → 503) proving the gate flips the route, not just
148
+ the registry. Typecheck clean (`tsc --noEmit` exit 0); full lint suite clean. The full suite is
149
+ left to CI (the authority).