instar 1.3.557 → 1.3.559
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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +52 -7
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +50 -27
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/AutonomousSessions.d.ts +12 -0
- package/dist/core/AutonomousSessions.d.ts.map +1 -1
- package/dist/core/AutonomousSessions.js +23 -0
- package/dist/core/AutonomousSessions.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +22 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +75 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +48 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/monitoring/ReapNotifier.d.ts +19 -0
- package/dist/monitoring/ReapNotifier.d.ts.map +1 -1
- package/dist/monitoring/ReapNotifier.js +65 -3
- package/dist/monitoring/ReapNotifier.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +7 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/upgrades/1.3.558.md +75 -0
- package/upgrades/1.3.559.md +48 -0
- package/upgrades/side-effects/honest-session-recycle.md +140 -0
- package/upgrades/side-effects/mm-pool-seamlessness-devgate.md +149 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Reap notices no longer dress a routine **session recycle** up as a death. A long
|
|
9
|
+
autonomous run periodically hits its per-session lifetime cap, at which point the
|
|
10
|
+
old terminal process is replaced by a fresh one that continues the run — but the
|
|
11
|
+
user used to see `🪦 Your session was shut down — it reached its maximum allowed
|
|
12
|
+
runtime`, even while the run's own clock still showed many hours remaining. Now,
|
|
13
|
+
when an `age-limit` reap targets a session whose topic has an ACTIVE autonomous
|
|
14
|
+
run, ReapNotifier emits honest continuation copy instead:
|
|
15
|
+
`🔄 Your session was recycled at its per-session lifetime cap — your autonomous
|
|
16
|
+
run has 11h 42m left, so no work was lost. I'll pick it back up on my next turn —
|
|
17
|
+
send a message if I stay quiet.`
|
|
18
|
+
|
|
19
|
+
It never goes silent: any read error, or a run that is genuinely over, falls back
|
|
20
|
+
to the normal loud "shut down" notice. Only the age-limit-recycle-of-an-active-run
|
|
21
|
+
case is reworded; a stuck-session death is still surfaced loudly even mid-run.
|
|
22
|
+
|
|
23
|
+
## What to Tell Your User
|
|
24
|
+
|
|
25
|
+
If you run long autonomous sessions and were alarmed by repeated "your session was
|
|
26
|
+
shut down — reached its maximum allowed runtime" messages: those were usually a
|
|
27
|
+
routine recycle, not a failure — your work was never lost and the run kept going.
|
|
28
|
+
You'll now see an honest "🔄 recycled, your run has Xh left, no work lost" notice
|
|
29
|
+
for that case instead of the tombstone. A genuine stop still shows the loud notice.
|
|
30
|
+
|
|
31
|
+
## Summary of New Capabilities
|
|
32
|
+
|
|
33
|
+
- Honest reap-notice wording for an age-limit recycle of a still-active autonomous
|
|
34
|
+
run (continuation, not death), with the run's real remaining time.
|
|
35
|
+
- New shared helper `autonomousRunRemainingForTopic` (one source of truth for "is
|
|
36
|
+
this topic's run in-flight, and how long is left?").
|
|
37
|
+
|
|
38
|
+
## Evidence
|
|
39
|
+
|
|
40
|
+
- `tests/unit/reap-notifier.test.ts` — honest-recycle block: recycle copy,
|
|
41
|
+
never-contradicts-clock, no-run→death, only-age-limit, throws→fail-loud,
|
|
42
|
+
over-window→death.
|
|
43
|
+
- `tests/unit/AutonomousSessions.test.ts` — `autonomousRunRemainingForTopic`:
|
|
44
|
+
in-flight remaining, numeric/string topic, over-window null, no-run/paused/
|
|
45
|
+
missing-duration null.
|
|
46
|
+
- `tests/integration/honest-session-recycle-wiring.test.ts` — real helper composed
|
|
47
|
+
with real notifier (dep non-null, real computed remaining).
|
|
48
|
+
- `npx tsc --noEmit` clean; 51/51 unit + 2/2 integration green.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Side-Effects Review — Honest Session Recycle
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `honest-session-recycle`
|
|
4
|
+
**Date:** `2026-06-14`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent — CONCUR (2 non-blocking concerns, both resolved)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
When a session is reaped at its per-session lifetime cap (`reason: 'age-limit'`)
|
|
11
|
+
but its Telegram topic still has an ACTIVE autonomous run, the recycle is a
|
|
12
|
+
continuation (the run respawns), not a death. ReapNotifier now stamps that fact
|
|
13
|
+
at ingest and emits honest "🔄 recycled at its lifetime cap — your autonomous run
|
|
14
|
+
has Xh left, resuming; no work was lost" copy instead of the false "🪦 reached its
|
|
15
|
+
maximum allowed runtime" gravestone. Files: `src/monitoring/ReapNotifier.ts`
|
|
16
|
+
(new optional dep `autonomousRunActiveFor`, ingest stamp, four render branches,
|
|
17
|
+
two pure helpers), `src/commands/server.ts` (wires the dep via a new
|
|
18
|
+
`autonomousRunRemainingForTopic`), `src/core/AutonomousSessions.ts` (the extracted
|
|
19
|
+
run-remaining helper). No SessionManager / kill-chokepoint change.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
- `ReapNotifier.onReaped` ingest — **modify** — stamps `autonomousRunActive` when
|
|
24
|
+
an `age-limit` reap has an active run; affects only NOTICE WORDING, never
|
|
25
|
+
whether a notice is sent.
|
|
26
|
+
- `ReapNotifier` render (single/aggregate/unbound/legacy) — **modify** — branch the
|
|
27
|
+
per-event copy on the stamp.
|
|
28
|
+
- The reap/respawn behavior itself — **pass-through** — unchanged.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
No block/allow surface — this change only chooses notice WORDING. It can never
|
|
35
|
+
suppress a notice: the only effect of the recycle branch is gentler copy; the
|
|
36
|
+
notice is always still sent. Over-block not applicable.
|
|
37
|
+
|
|
38
|
+
## 2. Under-block
|
|
39
|
+
|
|
40
|
+
No block/allow surface. The nearest analogue is "could a real death be softened
|
|
41
|
+
into a recycle?" — covered: the stamp requires `reason === 'age-limit'` AND an
|
|
42
|
+
active run with remaining > 0. A stuck-session death (`idle-zombie`,
|
|
43
|
+
`watchdog-stuck`, etc.) is never softened even mid-run (unit-tested). A run past
|
|
44
|
+
its window returns null → death copy stands.
|
|
45
|
+
|
|
46
|
+
## 3. Level-of-abstraction fit
|
|
47
|
+
|
|
48
|
+
Correct layer. The wording decision lives in ReapNotifier (the existing single
|
|
49
|
+
listener that already owns "is this a disappearance worth a notice?"). The
|
|
50
|
+
autonomous-state lookup lives at the server wiring layer (which has the state) and
|
|
51
|
+
is computed by one shared helper `autonomousRunRemainingForTopic` in
|
|
52
|
+
AutonomousSessions.ts — the same module that owns the run-window data — so the
|
|
53
|
+
recycle copy and any future caller agree on "is this run in-flight, how long
|
|
54
|
+
left?". SessionManager's kill chokepoint is deliberately NOT touched (lowest blast
|
|
55
|
+
radius).
|
|
56
|
+
|
|
57
|
+
## 4. Signal vs authority compliance
|
|
58
|
+
|
|
59
|
+
**Reference:** docs/signal-vs-authority.md
|
|
60
|
+
|
|
61
|
+
- [x] No — this change has no block/allow surface. It is pure presentation: a
|
|
62
|
+
signal (the autonomous-run fact) consumed to choose honest wording. It holds no
|
|
63
|
+
authority over reaping, respawning, or whether a notice fires. Fails toward the
|
|
64
|
+
loud legacy copy on any error.
|
|
65
|
+
|
|
66
|
+
## 5. Interactions
|
|
67
|
+
|
|
68
|
+
- **Shadowing:** none — the branch is inside the existing notice formatter; it
|
|
69
|
+
does not run before/after any other check.
|
|
70
|
+
- **Double-fire:** none — still exactly one `sessionReaped` → one notice per the
|
|
71
|
+
existing coalescing; this only changes the text.
|
|
72
|
+
- **Races:** the autonomous-run state is read at INGEST (synchronously when the
|
|
73
|
+
event arrives), not at the SUMMARY release (which can be ~30 min later), so the
|
|
74
|
+
"X left" figure reflects reap time and can't drift to a stale/negative value at
|
|
75
|
+
render. The reported `remainingSeconds` is a point-in-time snapshot, by design.
|
|
76
|
+
- **Feedback loops:** none — reads autonomous state, writes only notice text.
|
|
77
|
+
|
|
78
|
+
## 6. External surfaces
|
|
79
|
+
|
|
80
|
+
- Telegram: the user-visible reap notice text changes for the active-run recycle
|
|
81
|
+
case (🔄 + "no work was lost") — this is the intended fix. All other reap
|
|
82
|
+
notices are byte-identical.
|
|
83
|
+
- No other-agent, GitHub, Cloudflare, or persistent-state surface. No new route.
|
|
84
|
+
- **Operator surface (Mobile-Complete):** no operator-facing actions added — this
|
|
85
|
+
is an outbound notice wording change only. Not applicable.
|
|
86
|
+
|
|
87
|
+
## 6b. Operator-surface quality
|
|
88
|
+
|
|
89
|
+
No operator surface — not applicable. No `dashboard/*` or approval/grant/secret
|
|
90
|
+
form is touched.
|
|
91
|
+
|
|
92
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
93
|
+
|
|
94
|
+
**machine-local BY DESIGN.** Reap notices are emitted by the machine that owns
|
|
95
|
+
and reaps the session; the autonomous run state read (`config.stateDir`) is that
|
|
96
|
+
same machine's. A recycle on machine A is narrated by machine A in the topic — the
|
|
97
|
+
correct single voice for that event (the reaping machine is authoritative about
|
|
98
|
+
its own reap). No durable state is created (notice text only), so nothing strands
|
|
99
|
+
on topic transfer. No URLs generated. One-voice: the notice rides the existing
|
|
100
|
+
ReapNotifier coalescing + reap-notice drain, which already own single-emission.
|
|
101
|
+
|
|
102
|
+
## 8. Rollback cost
|
|
103
|
+
|
|
104
|
+
Pure code change — revert the three files and ship a patch. No persistent state,
|
|
105
|
+
no migration, no agent-state repair. During any rollback window the only
|
|
106
|
+
regression is the notice reverting to the old (alarming-but-not-harmful) wording.
|
|
107
|
+
No user data affected. The feature is also inert by construction when no
|
|
108
|
+
autonomous run is active (the common case) — it only ever activates on an
|
|
109
|
+
age-limit recycle of a live run.
|
|
110
|
+
|
|
111
|
+
## Conclusion
|
|
112
|
+
|
|
113
|
+
The review produced one design refinement during the build: the run-remaining
|
|
114
|
+
computation was extracted from an inline server.ts block into a single tested
|
|
115
|
+
helper (`autonomousRunRemainingForTopic`) so the run clock has one source of
|
|
116
|
+
truth. The change is wording-only with a fail-loud default and full both-sides
|
|
117
|
+
test coverage (recycle vs death; active / over-window / paused / no-run / missing
|
|
118
|
+
duration / throws). Clear to ship pending the high-risk second-pass concurrence.
|
|
119
|
+
|
|
120
|
+
## Second-pass review (if required)
|
|
121
|
+
|
|
122
|
+
**Reviewer:** independent reviewer subagent (high-risk: reap-notifier / session-lifecycle)
|
|
123
|
+
**Independent read of the artifact: CONCUR** (verified: fail-open is airtight across three layers; cannot silence a notice; terminal-death mislabeling is prevented by the `age-limit` + `remaining>0` guard; run-state read at ingest not render; pure presentation, no blocking authority).
|
|
124
|
+
|
|
125
|
+
Two non-blocking concerns raised — **both resolved before commit:**
|
|
126
|
+
|
|
127
|
+
- **Test-tier coverage.** Resolved: added `tests/integration/honest-session-recycle-wiring.test.ts`, which composes the REAL `autonomousRunRemainingForTopic` helper with the REAL ReapNotifier over a temp stateDir (the dep is non-null and delegates to the real run-window read) — the wiring-integrity tier. Tier-3 HTTP E2E is N/A by design: this change adds NO API route or feature-alive surface (it is outbound notice WORDING), so the canonical "feature returns 200" E2E does not apply; the integration composition is the appropriate top tier here.
|
|
128
|
+
- **Copy over-promised self-resume.** Resolved: respawn is message-triggered today (spec F1), so the copy was corrected to "no work was lost. I'll pick it back up on my next turn — send a message if I stay quiet" (single) / "it resumes on its next turn" (legacy) — no "resumes automatically" claim.
|
|
129
|
+
|
|
130
|
+
## Evidence pointers
|
|
131
|
+
|
|
132
|
+
- `tests/unit/reap-notifier.test.ts` — honest-recycle describe block (6 tests):
|
|
133
|
+
recycle copy, never-contradicts-clock, no-run-death, only-age-limit, throws-fails-safe, over-window-death.
|
|
134
|
+
- `tests/unit/AutonomousSessions.test.ts` — `autonomousRunRemainingForTopic`
|
|
135
|
+
describe block (4 tests): in-flight remaining, numeric/string topic, over-window
|
|
136
|
+
null, no-run/paused/missing-duration null.
|
|
137
|
+
- `tests/integration/honest-session-recycle-wiring.test.ts` — real helper composed
|
|
138
|
+
with real notifier (2 tests): in-flight run → honest recycle copy with real
|
|
139
|
+
computed remaining; no run file → terminal death copy.
|
|
140
|
+
- `npx tsc --noEmit` clean; 51/51 unit + 2/2 integration tests green.
|
|
@@ -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).
|