instar 1.3.556 → 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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +33 -6
- 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/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/SessionManager.d.ts +27 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +75 -2
- package/dist/core/SessionManager.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/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 +64 -64
- package/upgrades/1.3.557.md +61 -0
- package/upgrades/1.3.558.md +75 -0
- package/upgrades/side-effects/age-kill-transcript-aware-resume.md +115 -0
- package/upgrades/side-effects/mm-pool-seamlessness-devgate.md +149 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- **The 5 `multiMachine.seamlessness` coherence flags** — `ws3OneVoice` (one-voice
|
|
9
|
+
election), `ws13Reconcile` (ownership reconcile), `ws41DurableAck` (durable cross-machine
|
|
10
|
+
acknowledgement), `ws43RoleGuard` (role-guard-at-spawn), `ws43JournalLease` (journal-lease
|
|
11
|
+
cutover) — **moved from a hardcoded `false` in `ConfigDefaults` to the developmentAgent
|
|
12
|
+
gate (live-on-dev / dark-fleet)**, mirroring the already-merged `ws44PoolLinks` /
|
|
13
|
+
`ws44PoolCache` precedent.
|
|
14
|
+
- **Driver — operator directive (topic 13481, 2026-06-13):** "Nothing should ship dark for
|
|
15
|
+
development agents. Everything fully functional so it actually gets tested and doesn't
|
|
16
|
+
just rot." This is PR2 of that directive; PR1 (#1151) moved the 7 cross-machine memory
|
|
17
|
+
stores.
|
|
18
|
+
- **How it resolves now:** each flag's `ConfigDefaults` block OMITS the literal so
|
|
19
|
+
`resolveDevAgentGate` decides — LIVE on a development agent, DARK on the fleet. Every
|
|
20
|
+
consumer read-site (the SpeakerElection, the OwnershipReconciler, the durable remote-ack
|
|
21
|
+
routes, the scheduler role-guard, the journal-lease cutover gate, and the heartbeat
|
|
22
|
+
capability advert) was routed through that one gate, and `ws43JournalLeaseDryRun` is
|
|
23
|
+
computed COHERENTLY with its gate (a dev agent runs the cutover genuinely live, the fleet
|
|
24
|
+
stays in the safe dry-run posture).
|
|
25
|
+
- **The session-pool master switch is deliberately HELD.** `multiMachine.sessionPool`
|
|
26
|
+
(the active-active master plus its inbound-queue and hold-for-stability sub-flags) was NOT
|
|
27
|
+
moved. It carries a second, structurally-enforced rollout gate (`sessionPool.stage`,
|
|
28
|
+
advanced only through an end-to-end-test-gated ladder); flipping only the dev gate would
|
|
29
|
+
leave the pool inert, and forcing the stage would bypass that deliberate cutover
|
|
30
|
+
discipline. It is surfaced for an operator decision instead (advance the stage on the dev
|
|
31
|
+
mesh if active-active pooling should be dogfooded live).
|
|
32
|
+
- **Fleet + single-machine agents are an unchanged no-op** — every layer resolves dark
|
|
33
|
+
exactly as before, and each is a strict no-op without a second online machine. An
|
|
34
|
+
operator's explicit per-flag setting remains the documented force-dark (false) /
|
|
35
|
+
fleet-flip (true) override.
|
|
36
|
+
|
|
37
|
+
## What to Tell Your User
|
|
38
|
+
|
|
39
|
+
Nothing changes for fleet agents — these multi-machine coherence features stay off for
|
|
40
|
+
everyone except development agents. On a development agent that runs across two of your
|
|
41
|
+
machines, the coordination layers that keep the two machines behaving as one agent (one
|
|
42
|
+
voice per conversation, no double-replies, durable acknowledgements that survive a briefly
|
|
43
|
+
offline machine, and a guard that keeps state-writing jobs on the right machine) now run
|
|
44
|
+
live, so they finally get real-world testing before reaching everyone. There is no
|
|
45
|
+
user-facing action and no behavior change on a normal install or any single-machine agent.
|
|
46
|
+
The active-active session pool itself stays off pending a separate operator decision, because
|
|
47
|
+
it has its own staged rollout that should advance deliberately.
|
|
48
|
+
|
|
49
|
+
## Summary of New Capabilities
|
|
50
|
+
|
|
51
|
+
- The five cross-machine coherence layers — single outbound voice, ownership reconcile,
|
|
52
|
+
durable cross-machine acknowledgement, the spawn-time role-guard, and the job journal-lease
|
|
53
|
+
cutover — now run LIVE on development agents (dark on the fleet) so they are actually
|
|
54
|
+
exercised across the operator's two machines instead of shipping dark everywhere.
|
|
55
|
+
|
|
56
|
+
## Evidence
|
|
57
|
+
|
|
58
|
+
- `tests/unit/seamlessness-dev-gate-wiring.test.ts` (13): each flag resolves live-on-dev /
|
|
59
|
+
dark-on-fleet through the gate; the dryRun coherence holds; a no-missed-consumer
|
|
60
|
+
source-string check fails CI if any consumer reverts to the raw read; the registry
|
|
61
|
+
coherence check confirms the 5 flags are dev-gated and the 3 session-pool flags stay held.
|
|
62
|
+
- `tests/unit/PostUpdateMigrator-seamlessnessDevGate.test.ts` (9): the migrator strips the
|
|
63
|
+
default-shaped values, leaves operator-touched config alone, and is idempotent.
|
|
64
|
+
- `tests/e2e/attention-remote-ack-alive.test.ts`: two new dev-gate cases prove the durable
|
|
65
|
+
remote-ack route goes ALIVE on a dev agent with the flag omitted and stays a 503 on the
|
|
66
|
+
fleet — the gate flips the real route, not just a registry entry.
|
|
67
|
+
- `tests/unit/lint-dev-agent-dark-gate.test.ts`: the EXPECTED attribution map line numbers
|
|
68
|
+
were recomputed; the 3 session-pool flags remain as held hardcoded defaults.
|
|
69
|
+
|
|
70
|
+
## Migration
|
|
71
|
+
|
|
72
|
+
- `PostUpdateMigrator.migrateConfigSeamlessnessDevGate` strips a default-shaped `false` per
|
|
73
|
+
flag on update (and the paired `ws43JournalLeaseDryRun:true` only alongside a
|
|
74
|
+
default-shaped `ws43JournalLease:false`) so the gate resolves on already-deployed dev
|
|
75
|
+
agents. An operator-set value is left entirely alone — reach is not authority. Idempotent.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Side-Effects Review — Age-Kill Transcript-Awareness
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `age-kill-transcript-aware-resume`
|
|
4
|
+
**Date:** `2026-06-14`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** spec-convergence panel (6 internal reviewers + Standards-Conformance gate; external non-Claude pass degraded — see convergence report)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The session age-limit reaper (`SessionManager.monitorTick`, the `elapsed > limit` block) decides
|
|
11
|
+
whether an over-age session is "truly idle" and therefore age-kill-eligible. Before this change
|
|
12
|
+
that decision used only two signals — the tmux pane shows an idle prompt, and there is no
|
|
13
|
+
non-baseline child process — both BLIND to MCP/tool work (the Playwright MCP server runs out of
|
|
14
|
+
the pane's process tree; bash tool calls are short-lived). On 2026-06-13 an actively-working
|
|
15
|
+
session was terminal-killed because, between tool calls, it looked idle. This change adds a third
|
|
16
|
+
signal: `isTranscriptRecentlyActive(session, 120_000)` — true when the session's framework
|
|
17
|
+
transcript (JSONL) was modified within the last 2 minutes. The idle decision is extracted into a
|
|
18
|
+
pure, exported `isAgeGateTrulyIdle(idleAtPrompt, hasActiveProcs, transcriptActive)` so the exact
|
|
19
|
+
incident is reproducible at the decision boundary in a unit test. Files: `src/core/SessionManager.ts`
|
|
20
|
+
(constant `AGE_GATE_TRANSCRIPT_ACTIVE_MS`, the `isAgeGateTrulyIdle` function, the
|
|
21
|
+
`isTranscriptRecentlyActive` method, the import of `resolveFrameworkTranscriptPath`, and the
|
|
22
|
+
age-gate wiring); tests in `tests/unit/session-timeout-activity-aware.test.ts` and
|
|
23
|
+
`tests/unit/session-manager-behavioral.test.ts`.
|
|
24
|
+
|
|
25
|
+
## Decision-point inventory
|
|
26
|
+
|
|
27
|
+
- `SessionManager.monitorTick age-gate (ageGateTrulyIdle)` — **modify** — adds a transcript-activity
|
|
28
|
+
defer condition; the kill now requires idle-pane AND no-child-proc AND transcript-not-recently-active.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
35
|
+
|
|
36
|
+
In reap terms, "over-block" = wrongly DEFERRING a kill. A genuinely-finished session whose
|
|
37
|
+
transcript was touched <2 min before the check is held off for up to one 2-minute window. This is
|
|
38
|
+
harmless: the idle-detection block immediately below the age gate still reaps it once its
|
|
39
|
+
transcript goes quiet. No session is kept alive indefinitely on a stale signal — only real,
|
|
40
|
+
ongoing transcript writes (which require real model activity) sustain the defer.
|
|
41
|
+
|
|
42
|
+
## 2. Under-block
|
|
43
|
+
|
|
44
|
+
**What failure modes does this still miss?**
|
|
45
|
+
|
|
46
|
+
The change ONLY adds a defer condition; it removes no kill. A session with NO resolvable transcript
|
|
47
|
+
(an unknown framework, or pi-cli which has no resolver case) gets `isTranscriptRecentlyActive ===
|
|
48
|
+
false` and the age gate falls back to EXACTLY today's pane/procs verdict — so such a session is no
|
|
49
|
+
better protected than before (safe degradation, no NEW under-block). The deliberate non-coverage:
|
|
50
|
+
the age gate does not try to be a hard resource ceiling for runaway-but-active sessions — that is
|
|
51
|
+
owned by SessionWatchdog (stuck-process), the context-wedge sentinel, and the CPU/memory-pressure
|
|
52
|
+
reaper, all unchanged.
|
|
53
|
+
|
|
54
|
+
## 3. Level-of-abstraction fit
|
|
55
|
+
|
|
56
|
+
**Is this at the right layer?**
|
|
57
|
+
|
|
58
|
+
Yes. The SessionReaper (the pressure/idle reap path) ALREADY layers transcript-growth awareness
|
|
59
|
+
(`probeTranscript`/`transcriptDelta`). This change brings the parallel age-kill path to parity at
|
|
60
|
+
the SAME layer using the SAME core resolver (`resolveFrameworkTranscriptPath` from
|
|
61
|
+
`core/FrameworkSessionStore`). It is a cheap detector feeding an existing defer, not a new
|
|
62
|
+
authority and not a re-implementation of an existing primitive. It deliberately does NOT depend on
|
|
63
|
+
`monitoring/transcriptProber` (that would invert core→monitoring layering); it uses the core
|
|
64
|
+
resolver directly.
|
|
65
|
+
|
|
66
|
+
## 4. Signal vs authority compliance
|
|
67
|
+
|
|
68
|
+
**Does this hold blocking authority with brittle logic, or produce a signal that feeds a smart gate?**
|
|
69
|
+
|
|
70
|
+
It produces a SIGNAL that ADDS a defer (keep) condition to an existing authority. It grants no new
|
|
71
|
+
blocking authority and moves strictly in the safe direction (fewer kills of active sessions). The
|
|
72
|
+
probe is brittle (a single statSync) but its failure mode is bounded by the fail-to-`false`
|
|
73
|
+
contract: a probe error never keeps a session alive — it only declines to add the new defer
|
|
74
|
+
reason, falling through to the existing pane/procs verdict. (Ref: `docs/signal-vs-authority.md`.)
|
|
75
|
+
|
|
76
|
+
## 5. Interactions
|
|
77
|
+
|
|
78
|
+
**Does it shadow another check, get shadowed, double-fire, or race with adjacent cleanup?**
|
|
79
|
+
|
|
80
|
+
Consistent with the SessionReaper's existing transcript gate (same resolver, same intent). The
|
|
81
|
+
single-point mtime check vs the SessionReaper's cross-tick delta is an intentional, benign
|
|
82
|
+
asymmetry (the SessionReaper requires repeated positive-idle confirmations before it reaps, so the
|
|
83
|
+
two never produce a contradictory kill on the same tick). The idle-detection block below the age
|
|
84
|
+
gate still owns genuinely-idle reaping (the held off path falls through, no `continue`). No new
|
|
85
|
+
kill/revive loop: the change does NOT touch `WorkEvidence`/`ReapGuard.evaluate()`/`workEvidence()`,
|
|
86
|
+
so the documented evaluate-vs-workEvidence disagreement class (2026-06-13 13-session loop) is
|
|
87
|
+
untouched. Adversarial review confirmed loop-safety. The `overAgeButActiveLogged` set already
|
|
88
|
+
prevents log spam on the held off path.
|
|
89
|
+
|
|
90
|
+
## 6. External surfaces
|
|
91
|
+
|
|
92
|
+
**Does it change anything visible to other agents/users/systems? Timing/state dependence?**
|
|
93
|
+
|
|
94
|
+
No API route, no config key, no hook, no template, no dashboard surface. The only externally
|
|
95
|
+
observable effect: a long-running, actively-working session is no longer terminal-killed at ~5h —
|
|
96
|
+
which is the intended, operator-requested behavior. The new constant is code-level (not a config
|
|
97
|
+
surface). The change reads only a local file's mtime; it depends on the framework runtime writing
|
|
98
|
+
its transcript on activity (true for claude-code, codex-cli, gemini-cli; pi-cli safely degrades).
|
|
99
|
+
|
|
100
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
101
|
+
|
|
102
|
+
**Machine-local BY DESIGN.** Each machine's `SessionManager` reaps only its OWN sessions and reads
|
|
103
|
+
only its OWN local transcripts. There is no shared/replicated/proxied state: the decision is a
|
|
104
|
+
per-session, per-machine read of a local file. No cross-machine surface is introduced or needed —
|
|
105
|
+
a reap decision is inherently local to the machine running the session. No user-facing notice is
|
|
106
|
+
added (so no one-voice gating concern); no durable cross-machine state (so no topic-transfer
|
|
107
|
+
strand); no generated URL.
|
|
108
|
+
|
|
109
|
+
## 8. Rollback cost
|
|
110
|
+
|
|
111
|
+
Revert the PR — a single-file behavior change plus its tests; no data migration, no agent-state
|
|
112
|
+
repair, no config rollback. As an interim dial without a revert, setting
|
|
113
|
+
`AGE_GATE_TRANSCRIPT_ACTIVE_MS` to 0 makes the new defer condition inert (reverts to the prior
|
|
114
|
+
two-signal behavior). Ships with the dist on update (no PostUpdateMigrator entry needed — the
|
|
115
|
+
change is internal runtime behavior, not an agent-installed file).
|
|
@@ -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).
|