instar 1.3.531 → 1.3.533

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,28 @@
1
+ # Stale-commitment kill→revive loop — Plain-English Overview
2
+
3
+ > The one-line version: a session that finished its work but had an old, untouched promise still on the books was being killed (correctly) and then revived (incorrectly) over and over, forever. This stops the revival half from firing on a stale promise.
4
+
5
+ ## The problem in one breath
6
+
7
+ Today 13 idle sessions across 6 different topics were age-killed and then brought back to life, again and again, in a loop. Every single one was idle — its turn was finished, nothing was running — and the *only* reason the system revived it was that a commitment ("I'll keep you posted") was still registered on that topic. Many of those commitments were long-since done; they just were never marked delivered.
8
+
9
+ ## What already exists
10
+
11
+ The reaper has one shared "is it safe to end this session?" guard with two halves:
12
+
13
+ - **The kill half** (`evaluate()`) already knows a commitment goes *stale*. If the topic has had no user message for 8 hours, the commitment is treated as abandoned and no longer keeps the session alive — so an idle session with only a stale promise is correctly reaped. (This staleness rule was added back on 2026-06-06.)
14
+ - **The revive half** (`workEvidence()`) decides whether a killed session had real interrupted *work* worth bringing back. It tags the session with evidence like "a build was running" or "a sub-agent was live" — and, until now, "an open commitment exists."
15
+
16
+ ## What this changes
17
+
18
+ One line. The revive half now applies the **same 8-hour staleness gate** the kill half already uses. So an open commitment only counts as "interrupted work worth reviving" while the topic has had a user message within 8 hours.
19
+
20
+ The result: the two halves finally agree. A stale commitment neither keeps a session alive *nor* revives it. A *fresh* commitment keeps the session alive in the first place, so it never needs reviving. The loop's engine — kill (stale) then revive (commitment) — is gone.
21
+
22
+ ## Why this is safe
23
+
24
+ It can only ever revive **less**, never more. A session that was genuinely doing work when it died still carries its real work signals (a live build, an active sub-agent, a pending message, a running process) — those are untouched, and a busy session is never age-killed in the first place. The only revivals removed are exactly the harmful ones: an idle session resurrected to "finish" a promise that was already done. Promises still get their own follow-through from the commitment system (the beacon and the overdue-commitment job) — that's the right owner for a promise, not a respawned session burning a fresh turn each time.
25
+
26
+ ## What you'll notice
27
+
28
+ Fewer "🪦 your session was shut down — a restart is queued" messages on topics where nothing was actually unfinished. The looping topics (this one included) settle down. Nothing else changes.
@@ -0,0 +1,82 @@
1
+ # Side-Effects Review — ReapGuard stale-commitment revival loop
2
+
3
+ **Version / slug:** `reapguard-stale-commitment-revival-loop`
4
+ **Date:** `2026-06-13`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `general-purpose reviewer subagent (lifecycle change — required)`
7
+
8
+ ## Summary of the change
9
+
10
+ `ReapGuard` (the shared, stateless "is it safe to reap this session?" guard) has two methods that read an open commitment differently. `evaluate()` (the KILL decision) only keeps a session alive on an open commitment while the topic has had a user message within `staleCommitmentWindowMs` (default 8h); past that the commitment is "abandoned" and the idle session is reaped (staleness gate added 2026-06-06). `workEvidence()` (the RESUME-eligibility decision) emitted `open-commitment` evidence for **any** active commitment, with **no** staleness gate. So the same guard killed an idle session (commitment stale ⇒ reap) and immediately tagged it resume-eligible (commitment exists ⇒ revive), looping forever.
11
+
12
+ Evidence (live `logs/reap-log.jsonl`, 2026-06-13): 13 age-limit reaps with `midWork=true`, **all** carrying solely `workEvidence=[open-commitment]`, across 6 topics (multi-channel-support, Resource Limitation Mitigation, instar evolution, Subscription & Auth, instar-exo, Topic UX). The resume queue showed repeated `reason=age-limit` respawn entries, several doubled per topic.
13
+
14
+ The fix: one line in `ReapGuard.workEvidence()` — the `open-commitment` probe now applies the identical predicate `evaluate()` uses (`protectOpenCommitments && activeCommitmentForTopic && recentUserMessage(staleCommitmentWindowMs)`). Files touched: `src/core/ReapGuard.ts` (the probe + a comment), `tests/unit/work-evidence.test.ts` (a new regression `describe` block).
15
+
16
+ ## Decision-point inventory
17
+
18
+ - `ReapGuard.workEvidence()` open-commitment probe (`src/core/ReapGuard.ts`) — **modify** — narrows when `open-commitment` is emitted as work evidence to match the KEEP guard's staleness horizon. This is a SIGNAL producer (work evidence consumed by the resume queue / reap-notify), not a blocking authority.
19
+
20
+ ---
21
+
22
+ ## 1. Over-block
23
+
24
+ **What legitimate inputs does this change reject that it shouldn't?**
25
+
26
+ This change narrows a SIGNAL (work evidence), not a block/allow gate. The "over-block" analogue is: does it now suppress `open-commitment` evidence for a session that genuinely had interrupted work? No. To be age-killed at all, a session must be idle-at-prompt with no live processes (the age-gate's `ageGateTrulyIdle` check) — a genuinely-busy session is deferred, never killed. And by the time `workEvidence()` runs as the fallback, `blockedReason()` has already returned null, meaning the commitment was already judged stale (no user message in 8h). A session with real interrupted work carries OTHER evidence (`structural-long-work`, `active-subagent`, `pending-injection`, `active-process`, `main-process-active`) which are untouched. A commitment touched within 8h still emits `open-commitment` (covered by the "FRESH commitment" test). No legitimate revival is lost.
27
+
28
+ ## 2. Under-block
29
+
30
+ **What failure modes does this still miss?**
31
+
32
+ The change does not, by itself, drain the 279 already-`pending` commitments that fuel the loop — but it makes them HARMLESS for revival (a stale one no longer revives). A separate, careful runtime hygiene pass to mark genuinely-delivered commitments is tracked: <!-- tracked: topic-18423 --> (operator-visible follow-up on the live machine; not a code deferral). It also does not change the age-limit reaper killing idle sessions — that is correct behavior and out of this fix's scope by design (the loop was the *revival* half, not the kill).
33
+
34
+ ## 3. Level-of-abstraction fit
35
+
36
+ **Is this at the right layer?**
37
+
38
+ Yes. `ReapGuard` is the single shared guard both the reaper and the terminate authority consult; aligning its two methods at that one place fixes every killer that uses the `workEvidence()` fallback (the age-limit path does). Putting the staleness logic anywhere downstream (resume queue, reap-notify) would patch one consumer and leave the evidence itself wrong for the others. The correct owner of *promise* follow-through (the commitment beacon / overdue-commitment job) is a different subsystem and is intentionally left to do its job — this fix stops the resume queue from double-covering it on a stale signal.
39
+
40
+ ## 4. Signal vs authority compliance
41
+
42
+ **Does this hold blocking authority with brittle logic, or produce a signal that feeds a smart gate?** (ref: `docs/signal-vs-authority.md`)
43
+
44
+ Compliant. `workEvidence()` is a pure SIGNAL producer — observe-only evidence collection. It holds no blocking authority (the KILL authority is `evaluate()`/`blockedReason()`, unchanged). This change makes the existing signal MORE accurate by removing a false-positive; it adds no new brittle check and no new authority.
45
+
46
+ ## 5. Interactions
47
+
48
+ **Does it shadow another check, get shadowed, double-fire, race with cleanup?**
49
+
50
+ The two `ReapGuard` methods now use one identical predicate, removing the contradiction that WAS the bug. No race: `workEvidence()` is only collected after `blockedReason()` returned null (a non-null keep returns early at terminateSession before evidence is gathered), so the two never disagree on the same live state. Downstream consumers behave correctly: dropping the sole `open-commitment` signal flips `isMidWork`→false and `evidenceEligible`→false, which correctly prevents (a) `ResumeQueue` enqueue (gated on `evidenceEligible`), (b) the server's live enqueue (gated on `midWork===true`), and (c) boot-reconciliation re-enqueue from the reap-log (also `midWork===true`). The reaper's own idle path supplies authoritative `workEvidence:[]` and is unaffected (it was never the loop's source).
51
+
52
+ ## 6. External surfaces
53
+
54
+ **Does it change anything visible to other agents/users/systems?**
55
+
56
+ User-visible: fewer "your session was shut down — a restart is queued" notices on topics with no genuinely-unfinished work; the looping topics settle. Reap-log entries for these reaps now record `midWork:false` (honest — they were idle reaps, not interrupted work). No agent-to-agent or cross-system surface. No new timing/runtime dependency — it reuses the same in-memory `recentUserMessage` dep `evaluate()` already calls.
57
+
58
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
59
+
60
+ **Machine-local BY DESIGN.** `ReapGuard` evaluates the LOCAL machine's sessions against that machine's local topic-state deps (topic binding, recent-message, active-commitment). Session reaping is inherently a per-machine operation — a machine reaps its own sessions — so there is no replication path to add and none is wanted. The commitment registry that backs `activeCommitmentForTopic` follows its own (separate) cross-machine story; this change reads it identically to how `evaluate()` already does, so it introduces no new multi-machine assumption. No user-facing notice voice, durable-state-on-transfer, or generated-URL concern applies.
61
+
62
+ ## 8. Rollback cost
63
+
64
+ Cheap. Pure code change, no migration, no persisted-state shape change. Back-out = revert the one-line probe to its prior form (`upgrades/eli16/...` + git revert) and ship a patch. The reap-log entries already written are inert historical records. Config knob `staleCommitmentWindowMs: Infinity` independently restores the always-protect behavior for BOTH methods consistently (now that they share the predicate) without a code change, if a site wants the old revival behavior back immediately.
65
+
66
+ ---
67
+
68
+ ## Second-pass review
69
+
70
+ **Reviewer:** independent general-purpose reviewer subagent (required: change touches session lifecycle kill/recovery + a "guard").
71
+
72
+ **VERDICT: Concur with the review.**
73
+
74
+ The reviewer independently audited `ReapGuard.evaluate()`/`workEvidence()`, the `SessionManager.terminateSession` evidence path (lines 825–925), and the new tests, pressure-testing all five risk dimensions:
75
+
76
+ - **A (over-block):** could not construct a genuinely-interrupted session left un-revived; busy sessions never reach the age-kill, and real work carries other evidence.
77
+ - **B (race/consistency):** the two methods now use the identical predicate triple; the benign `recent-user-message`/`open-commitment` independence in `workEvidence()` cannot resurrect the loop (for a stale session both windows are false; workEvidence is recomputed from the same live deps regardless of WHY evaluate cleared).
78
+ - **C (killer-supplied evidence):** grepped every killer supplying explicit `opts.workEvidence` — only the idle-reaper (authoritative `[]`), the operator force-resume (`['active-build-or-autonomous-run']`), and tests. The age-limit path supplies none → uses the patched fallback. The fix reaches the path that actually fired.
79
+ - **D (config edges):** `protectOpenCommitments:false` and `staleCommitmentWindowMs:Infinity` now behave consistently across both methods.
80
+ - **E (downstream):** correctly prevents ResumeQueue enqueue, server live enqueue, AND boot-reconciliation re-enqueue (all `midWork===true`/`evidenceEligible` gated); ReapNotifier reports the idle reap honestly.
81
+
82
+ No concern raised; no design iteration required.
@@ -0,0 +1,92 @@
1
+ # Side-Effects Review — WS5.1 (subscription-pool pool-scope visibility)
2
+
3
+ ## Summary
4
+
5
+ Adds a read-side, additive `scope=pool` branch to `GET /subscription-pool` — the operator's
6
+ ONE view of "how much quota is left across ALL my machines / accounts." It mirrors the merged
7
+ WS4.1 `GET /sessions?scope=pool` fan-out exactly: fan out to every online mesh peer's PLAIN
8
+ `/subscription-pool` (no recursion), tag each account with the machine that holds it, and merge
9
+ into a dark-peer-tolerant object with a classified `pool.failed` list. No replication, no PII
10
+ machinery, no HLC, no new config flag. The placement tie-breaker half is DEFERRED (CMT-1416).
11
+
12
+ ## Files changed
13
+
14
+ - **`src/server/routes.ts`** — the `GET /subscription-pool` handler gains an
15
+ `if (req.query.scope === 'pool')` branch (registered BEFORE the plain return). Self accounts
16
+ tagged `remote:false` + machine identity; a `Promise.all` fan-out over `resolvePeerUrls()`
17
+ fetches each peer's PLAIN route carrying THIS machine's Bearer with a 5s timeout; remote
18
+ accounts tagged `remote:true` + machineId/machineNickname; a non-OK/down/slow/unauth peer
19
+ pushes a NORMALIZED `pool.failed` row (`unauthorized`/`error`/`timeout`/`unreachable`). The
20
+ new peer-fetch catch is tagged `// @silent-fallback-ok`. The handler signature changed from
21
+ sync `(_req, res)` to `async (req, res)` (the fan-out awaits). The plain (no-scope) path is
22
+ byte-identical.
23
+ - **`src/scaffold/templates.ts`** — a "Quota across ALL my machines (pool-scope read)" bullet
24
+ on the Subscription Pool section in `generateClaudeMd()` (new agents).
25
+ - **`src/core/PostUpdateMigrator.ts`** — the same bullet added to the section-install template
26
+ AND an idempotent, content-sniffed additive-bullet patcher (`!content.includes('Quota across
27
+ ALL my machines')`, anchored on the "poll all now" line) for existing agents that already
28
+ carry the section. Mirrors the existing proactive-swap bullet patcher.
29
+ - **Tests**: `tests/unit/subscription-pool-scope.test.ts` (merge/tag/classify + 3 adversarial
30
+ lenses), `tests/integration/subscription-pool-scope.test.ts` (real peer + 401 peer + dead
31
+ port), `tests/e2e/subscription-pool-scope-lifecycle.test.ts` (feature-is-alive on the
32
+ production AgentServer path).
33
+ - **Spec + ELI16 + release fragment**: `docs/specs/ws51-subscription-pool-scope.md` +
34
+ `.eli16.md` + `upgrades/next/ws51-subscription-pool-scope.md`.
35
+
36
+ ## Signal vs. Authority
37
+
38
+ This surface is a pure READ — it reports state, it never mutates or gates anything. The
39
+ fan-out carries THIS machine's own Bearer (its own authority, the same as the sessions route),
40
+ never a caller-supplied token (auth-boundary lens, tested). A peer that is down/slow/unauth is
41
+ the DESIGNED tolerant degrade reported up-stack in `pool.failed` (no-silent-fallbacks: the
42
+ catch is tagged `@silent-fallback-ok`); it never blocks, never 500s, never silently omits a
43
+ machine. The placement DECISION is untouched (the tie-breaker is deferred), so nothing in this
44
+ slice can move a live session.
45
+
46
+ ## Framework generality
47
+
48
+ The capability is framework-agnostic — it reads the subscription pool registry + the mesh peer
49
+ set, neither of which is Claude-specific. A Codex/Gemini agent running a multi-account pool
50
+ gets the same pool-scope view. The CLAUDE.md awareness rides the existing Subscription Pool
51
+ featureSection whose shadow markers already mirror it to AGENTS.md / GEMINI.md (the section
52
+ heading is unchanged, so Codex/Gemini parity is preserved with no marker edit).
53
+
54
+ ## Operator surface quality
55
+
56
+ The operator never types a curl. The conversational trigger is "how much quota is left across
57
+ ALL my machines?" — the CLAUDE.md bullet (new + existing agents) teaches the agent to reach for
58
+ `GET /subscription-pool?scope=pool` and to explain a dark peer honestly (a classified
59
+ `pool.failed` row, never a silent omission). The response is plain JSON the agent narrates; the
60
+ normalized failure reasons (`timeout`/`unreachable`/`unauthorized`/`error`) are
61
+ operator-legible and carry no internal URL or token.
62
+
63
+ ## Blast radius / risk
64
+
65
+ - **Additive route-branch, no config flag.** The dark-gate line-map is UNCHANGED (verified
66
+ 16/16 green without editing the EXPECTED map) — no ConfigDefaults enabled-path was touched.
67
+ - **Back-compat preserved.** The plain `GET /subscription-pool` (no `scope`) returns the exact
68
+ `{ enabled, count, accounts }` shape as before — verified by the existing
69
+ `subscription-pool-routes` + `subscription-pool` unit suites staying green and an integration
70
+ assertion that the plain route has no `pool`/`scope` keys.
71
+ - **Single-machine = strict no-op superset.** No peers / no `resolvePeerUrls` → the self-only
72
+ view tagged `scope:'pool'` with empty `pool.failed`. An unwired pool → `enabled:false`,
73
+ `accounts:[]`, still `scope:'pool'`.
74
+ - **No amplification.** The peer fetch hits the PLAIN route (no `scope=pool`), `Promise.all` is
75
+ bounded by the live peer set, each fetch has its own 5s timeout — no fan-out storm on an
76
+ N-machine pool.
77
+
78
+ ## Deferred / tracked
79
+
80
+ - The placement tie-breaker (prefer the machine with more account-pool headroom on an
81
+ otherwise-equal tie) is DEFERRED as `<!-- tracked: CMT-1416 -->`. `MachineCapacity` carries
82
+ `quotaState.blocked` only — no per-account remaining%; plumbing aggregate account-pool
83
+ headroom through the capacity heartbeat is larger than a clean small slice. The pool-scope
84
+ READ ships alone. WS5.2 (account follow-me) / WS5.3 (escalation rides the topic) are separate
85
+ surfaces.
86
+
87
+ ## Rollback
88
+
89
+ Revert the single `feat` commit. The change is purely additive (a new query-branch + two
90
+ awareness bullets); reverting restores the prior sync handler and the prior CLAUDE.md template
91
+ with no migration cleanup needed (the additive migrator patcher is content-sniffed and simply
92
+ stops running once the source no longer carries the bullet template).