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.
@@ -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).