instar 1.3.749 → 1.3.751
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 +40 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AgeKillBackoff.d.ts +9 -48
- package/dist/core/AgeKillBackoff.d.ts.map +1 -1
- package/dist/core/AgeKillBackoff.js +8 -84
- package/dist/core/AgeKillBackoff.js.map +1 -1
- package/dist/core/MessageSentinel.d.ts.map +1 -1
- package/dist/core/MessageSentinel.js +11 -0
- package/dist/core/MessageSentinel.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +20 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +43 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +68 -32
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +159 -7
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/VetoedKillBackoff.d.ts +130 -0
- package/dist/core/VetoedKillBackoff.d.ts.map +1 -0
- package/dist/core/VetoedKillBackoff.js +221 -0
- package/dist/core/VetoedKillBackoff.js.map +1 -0
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/IncidentDedupe.d.ts +36 -0
- package/dist/monitoring/IncidentDedupe.d.ts.map +1 -0
- package/dist/monitoring/IncidentDedupe.js +38 -0
- package/dist/monitoring/IncidentDedupe.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +20 -20
- package/upgrades/1.3.750.md +30 -0
- package/upgrades/1.3.751.md +17 -0
- package/upgrades/side-effects/keyword-intent-decision-ratchet.md +67 -0
- package/upgrades/side-effects/session-respawn-thrash-elimination.md +75 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-04T05:23:37.602Z",
|
|
5
|
+
"instarVersion": "1.3.751",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"domain": "identity",
|
|
12
12
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
13
13
|
"installedPath": ".instar/hooks/instar/session-start.sh",
|
|
14
|
-
"contentHash": "
|
|
14
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
15
15
|
"since": "2025-01-01"
|
|
16
16
|
},
|
|
17
17
|
"hook:dangerous-command-guard": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"domain": "safety",
|
|
21
21
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
22
22
|
"installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
|
|
23
|
-
"contentHash": "
|
|
23
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
24
24
|
"since": "2025-01-01"
|
|
25
25
|
},
|
|
26
26
|
"hook:grounding-before-messaging": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"domain": "safety",
|
|
30
30
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
31
31
|
"installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
|
|
32
|
-
"contentHash": "
|
|
32
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
33
33
|
"since": "2025-01-01"
|
|
34
34
|
},
|
|
35
35
|
"hook:compaction-recovery": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"domain": "identity",
|
|
39
39
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
40
40
|
"installedPath": ".instar/hooks/instar/compaction-recovery.sh",
|
|
41
|
-
"contentHash": "
|
|
41
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
42
42
|
"since": "2025-01-01"
|
|
43
43
|
},
|
|
44
44
|
"hook:external-operation-gate": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"domain": "safety",
|
|
48
48
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
49
49
|
"installedPath": ".instar/hooks/instar/external-operation-gate.js",
|
|
50
|
-
"contentHash": "
|
|
50
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
51
51
|
"since": "2025-01-01"
|
|
52
52
|
},
|
|
53
53
|
"hook:deferral-detector": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"domain": "safety",
|
|
57
57
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
58
58
|
"installedPath": ".instar/hooks/instar/deferral-detector.js",
|
|
59
|
-
"contentHash": "
|
|
59
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
60
60
|
"since": "2025-01-01"
|
|
61
61
|
},
|
|
62
62
|
"hook:self-stop-guard": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"domain": "coherence",
|
|
66
66
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
67
67
|
"installedPath": ".instar/hooks/instar/self-stop-guard.js",
|
|
68
|
-
"contentHash": "
|
|
68
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
69
69
|
"since": "2025-01-01"
|
|
70
70
|
},
|
|
71
71
|
"hook:post-action-reflection": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"domain": "evolution",
|
|
75
75
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
76
76
|
"installedPath": ".instar/hooks/instar/post-action-reflection.js",
|
|
77
|
-
"contentHash": "
|
|
77
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
78
78
|
"since": "2025-01-01"
|
|
79
79
|
},
|
|
80
80
|
"hook:external-communication-guard": {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"domain": "safety",
|
|
84
84
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
85
85
|
"installedPath": ".instar/hooks/instar/external-communication-guard.js",
|
|
86
|
-
"contentHash": "
|
|
86
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
87
87
|
"since": "2025-01-01"
|
|
88
88
|
},
|
|
89
89
|
"hook:scope-coherence-collector": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"domain": "coherence",
|
|
93
93
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
94
94
|
"installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
|
|
95
|
-
"contentHash": "
|
|
95
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
96
96
|
"since": "2025-01-01"
|
|
97
97
|
},
|
|
98
98
|
"hook:scope-coherence-checkpoint": {
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"domain": "coherence",
|
|
102
102
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
103
103
|
"installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
|
|
104
|
-
"contentHash": "
|
|
104
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
105
105
|
"since": "2025-01-01"
|
|
106
106
|
},
|
|
107
107
|
"hook:free-text-guard": {
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"domain": "safety",
|
|
111
111
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
112
112
|
"installedPath": ".instar/hooks/instar/free-text-guard.sh",
|
|
113
|
-
"contentHash": "
|
|
113
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
114
114
|
"since": "2025-01-01"
|
|
115
115
|
},
|
|
116
116
|
"hook:claim-intercept": {
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"domain": "coherence",
|
|
120
120
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
121
121
|
"installedPath": ".instar/hooks/instar/claim-intercept.js",
|
|
122
|
-
"contentHash": "
|
|
122
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
123
123
|
"since": "2025-01-01"
|
|
124
124
|
},
|
|
125
125
|
"hook:claim-intercept-response": {
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"domain": "coherence",
|
|
129
129
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
130
130
|
"installedPath": ".instar/hooks/instar/claim-intercept-response.js",
|
|
131
|
-
"contentHash": "
|
|
131
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
132
132
|
"since": "2025-01-01"
|
|
133
133
|
},
|
|
134
134
|
"hook:stop-gate-router": {
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"domain": "safety",
|
|
138
138
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
139
139
|
"installedPath": ".instar/hooks/instar/stop-gate-router.js",
|
|
140
|
-
"contentHash": "
|
|
140
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
141
141
|
"since": "2025-01-01"
|
|
142
142
|
},
|
|
143
143
|
"hook:auto-approve-permissions": {
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"domain": "safety",
|
|
147
147
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
148
148
|
"installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
|
|
149
|
-
"contentHash": "
|
|
149
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
150
150
|
"since": "2025-01-01"
|
|
151
151
|
},
|
|
152
152
|
"job:health-check": {
|
|
@@ -1538,7 +1538,7 @@
|
|
|
1538
1538
|
"type": "subsystem",
|
|
1539
1539
|
"domain": "sessions",
|
|
1540
1540
|
"sourcePath": "src/core/SessionManager.ts",
|
|
1541
|
-
"contentHash": "
|
|
1541
|
+
"contentHash": "fa7570cfad6454faed57c41435475136946b7801a6e1d32b01d2b34fcea8b66f",
|
|
1542
1542
|
"since": "2025-01-01"
|
|
1543
1543
|
},
|
|
1544
1544
|
"subsystem:auto-updater": {
|
|
@@ -1562,7 +1562,7 @@
|
|
|
1562
1562
|
"type": "subsystem",
|
|
1563
1563
|
"domain": "updates",
|
|
1564
1564
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
1565
|
-
"contentHash": "
|
|
1565
|
+
"contentHash": "3a280757ac9c2c4a35cf63aa4ba2f4a954ee85be842715fd93a92aa0881325e3",
|
|
1566
1566
|
"since": "2025-01-01"
|
|
1567
1567
|
},
|
|
1568
1568
|
"subsystem:scheduler": {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Fixed a hot-spin loop in the idle-session cleaner. When a session sat idle past its bound-idle threshold but was PROTECTED by a keep-reason (an open commitment, a recent user message), the cleaner re-attempted the kill every 5 seconds forever — the ReapGuard vetoed it every time, but the idle clock was never reset, so it just re-fired. That single loop wrote thousands of "skipped" records a day and grew one agent's reap-log to 132MB, and it was the real driver behind the "my sessions keep swapping/restarting" symptom.
|
|
9
|
+
|
|
10
|
+
The shipped `AgeKillBackoff` P19 back-off primitive (which already fixed the *identical* loop one branch over — the age-gate) is now generalized into a shared `VetoedKillBackoff` and wired into the idle-zombie branch too. A vetoed idle-zombie kill now BACKS OFF (default 30-minute cooldown) instead of re-firing every tick, logs once per episode instead of every 5s, and — after 6 consecutive vetoed episodes on a genuinely-stuck session — raises exactly ONE flood-guarded attention item so the stuck session is surfaced rather than silently spun on.
|
|
11
|
+
|
|
12
|
+
**This never changes WHICH sessions are killed — only how OFTEN a kill the guard already vetoed is re-attempted.** A session with no keep-reason still dies on the first attempt. The KEEP-guard remains the sole authority.
|
|
13
|
+
|
|
14
|
+
Ships LIVE on development agents and DARK on the fleet (`monitoring.idleKillVetoBackoff`, `enabled` resolved by the dev-agent gate). Per-machine by design. `enabled: false` fully reverts to the prior per-tick behavior with no persisted state to unwind.
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
Nothing changes for you unless you were seeing a session "keep restarting/swapping" or a runaway audit log. On a development agent this fix runs live now: idle sessions that are protected (open commitment / recent message) are re-checked on a slow cadence instead of hammered every 5 seconds, so the reap-log stops flooding and the CPU churn behind it goes away. If a session is *genuinely* stuck (permanently vetoed for hours), you'll now get ONE plain heads-up on your attention queue naming it — instead of thousands of silent log writes. On the wider fleet it ships off until it's proven out on the dev agent.
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
- `monitoring.idleKillVetoBackoff` config knob — `cooldownMs` (default 30m), `escalateAfterEpisodes` (default 6). `enabled` is governed by the dev-agent gate (live on dev, dark on fleet); set it explicitly to force on/off. `cooldownMs: 0` is enabled-but-no-cooldown, not a disable.
|
|
23
|
+
- A P19 breaker that raises ONE flood-guarded HIGH attention item ("Session X is permanently vetoed from idle-zombie cleanup") after N consecutive veto episodes — surfacing a genuinely-stuck session instead of spinning silently.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
|
|
27
|
+
- New `tests/unit/vetoed-kill-backoff.test.ts` (20) + `tests/unit/session-manager-idle-veto-backoff.test.ts` (14) cover the value-shape migration, reason-key stale-reprieve, once-per-episode logging, the disabled contract, single-guard-eval, the map-leak eviction, `cooldownMs:0` semantics, and the breaker.
|
|
28
|
+
- Regression: `AgeKillBackoff.test.ts` (7) + `age-kill-backoff-integration.test.ts` (4) green (the age-gate behaves identically). 45/45 feature+regression; `tsc --noEmit` exit 0.
|
|
29
|
+
- Side-effects review + independent second-pass review: `upgrades/side-effects/session-respawn-thrash-elimination.md`.
|
|
30
|
+
- Spec: `docs/specs/session-respawn-thrash-elimination.md` (converged, cross-model reviewed) + `docs/specs/session-respawn-thrash-elimination.eli16.md`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Added a new CI ratchet, `tests/unit/keyword-intent-decision-ratchet.test.ts`, that enforces the constitutional standard **"Intelligence Infers, Keywords Only Guard"**: a natural-language keyword/phrase/regex list must never be the thing that DECIDES what a human meant (classify intent, gate/reroute/swallow a message). The ratchet walks `src/{core,monitoring,server,threadline,messaging}`, detects the anti-pattern, subtracts an explicit per-file allowlist reproduced from the 2026-07-03 audit (enum validators, security scrubbers, error/process-message classifiers, structured-output parsers, cosmetic selectors, quantity extractors, observe-only signal loggers), exempts declared safety floors carrying a new inline `@intent-safety-floor-ok` marker, and asserts the remaining offender count against a committed baseline of **6** (the audit's six findings — the number can only decrease). It ships in **report mode** (`ENFORCE = false`): it prints offenders every run but never fails CI on a net-new violation until the flag is deliberately flipped after a clean soak. `MessageSentinel` gains the `@intent-safety-floor-ok` marker (the standard's survivor #2 — the emergency-stop fast-path with an LLM stage behind it); no logic change.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
Nothing user-facing — this is an internal development guardrail (agent-only, preview maturity). It is the enforcement arm of the "Intelligence Infers, Keywords Only Guard" standard: it structurally prevents new code that dumbs a "what did the human mean?" decision down to keyword matching, the class of bug that once made the agent swallow the operator's "keep the work on the laptop" as a move command. If asked why a future PR fails this check: the change added a keyword list that decides natural-language intent — convert it to an LLM-with-context decision (the CoherenceGate / cheap-prefilter→LLM template) or justify it as one of the two survivors.
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
- `tests/unit/keyword-intent-decision-ratchet.test.ts` — report-mode ratchet detecting keyword-list intent decisions; baseline 6; sibling to `no-silent-fallbacks`.
|
|
17
|
+
- New inline marker convention `@intent-safety-floor-ok` (mirroring `@silent-fallback-ok`) for declaring a legitimate LLM-backed safety floor.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Side-Effects Review — Keyword-Intent Decision Ratchet (report-mode)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `keyword-intent-decision-ratchet`
|
|
4
|
+
**Date:** `2026-07-03`
|
|
5
|
+
**Author:** `Echo (instar-dev)`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `tests/unit/keyword-intent-decision-ratchet.test.ts`, a bespoke vitest ratchet (sibling to `no-silent-fallbacks`) enforcing the constitutional standard "Intelligence Infers, Keywords Only Guard". It walks `src/{core,monitoring,server,threadline,messaging}`, detects the anti-pattern (a natural-language keyword/phrase/regex list tested against a message/conversation/text variable to DECIDE what a human meant), subtracts an explicit per-file allowlist reproduced from the 2026-07-03 audit, exempts declared safety floors carrying a new `@intent-safety-floor-ok` marker, and asserts the remaining offender count against a committed baseline of `6`. Ships in report mode (`ENFORCE = false`): it prints offenders but does not fail CI on a net-new violation. The only behavioral/in-scope file touched is `src/core/MessageSentinel.ts`, which receives a ~15-line comment adding the `@intent-safety-floor-ok` marker (no logic change). Supporting docs (design spec, audit, standard doc) ship alongside.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `keyword-intent-decision detector (test-only)` — add — a static-analysis signature that flags files; it never runs at agent runtime and holds no runtime authority over any message.
|
|
15
|
+
- `MessageSentinel FAST_STOP/FAST_PAUSE fast-path` — pass-through — unchanged logic; only a documentation marker is added declaring it the standard's survivor #2 safety floor.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
|
|
21
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
22
|
+
|
|
23
|
+
None at runtime — this is a CI test, not a message gate. Its only "block" surface is failing CI. In report mode (`ENFORCE = false`) it cannot fail CI on a net-new violation at all; it only asserts the six known offenders are still detected (a detector-alive guard) and prints anything new. A CI-level over-block (flagging an innocent file) is prevented by the allowlist, which reproduces the audit's cleared set: enum validators, security scrubbers, error/process-message classifiers, structured-output parsers, cosmetic selectors, quantity extractors, and observe-only signal loggers. Verified on `JKHeadley/main`: the flagged-minus-allowlist set is exactly the six audit offenders — zero false positives.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Under-block
|
|
28
|
+
|
|
29
|
+
**What failure modes does this still miss?**
|
|
30
|
+
|
|
31
|
+
By design, it errs toward false negatives (the conservatism mandate — a noisy ratchet gets disabled). Known blind spots: (a) a NEW keyword-intent gate added INSIDE an already-allowlisted file is masked (the allowlist is keyed by file); each allowlist entry is documented by symbol so a reviewer can spot-check. (b) The detector's message-variable set is tight (`text|message|content|body|t|lower|normText|…`); a keyword gate written against an unusually-named variable would be missed. (c) Scope is the five source dirs only; `.instar/hooks/*` and other trees are out of scope (audit-cleared as agent-output). These are accepted per the standard's "miss a subtle one over flag an enum validator" guidance.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 3. Level-of-abstraction fit
|
|
36
|
+
|
|
37
|
+
**Is this at the right layer?**
|
|
38
|
+
|
|
39
|
+
Yes — it is a build-time static-analysis ratchet (low-level, cheap, deterministic), which is exactly the right layer for "no code should string-match to decide intent." It does NOT attempt to be a runtime authority (that would itself violate the standard it enforces). It mirrors the proven `no-silent-fallbacks` ratchet's shape. The runtime conversions of the six offenders (to LLM-with-context) are separate follow-up work the ratchet's baseline tracks; this change only installs the guard.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 4. Signal vs authority compliance
|
|
44
|
+
|
|
45
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
46
|
+
|
|
47
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
48
|
+
|
|
49
|
+
- [x] No — this change has no runtime block/allow surface. It is a CI test. Its detector is brittle-by-nature (regex signature) but holds NO authority over any user message or agent action; it only counts source-code patterns and, when enforcement is later enabled, gates the build against net-new regressions — the same authority every ratchet here holds. The MessageSentinel marker adds no logic.
|
|
50
|
+
|
|
51
|
+
[The ratchet is the enforcement arm of a standard whose whole point is that brittle keyword logic must not hold intent-decision authority. The ratchet itself scrupulously avoids doing so: it never inspects a live message, only source text.]
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 5. Interactions
|
|
56
|
+
|
|
57
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
58
|
+
|
|
59
|
+
- **Shadowing:** Sibling to the `no-silent-fallbacks` ratchet and the "an LLM gate must not string-match" guard; complementary, no shadowing. Runs as an ordinary unit test in the vitest suite.
|
|
60
|
+
- **The `@intent-safety-floor-ok` marker** is new and independent of `@silent-fallback-ok` (different detector); no collision.
|
|
61
|
+
- **MessageSentinel:** the marker is a comment inside the file, above `FAST_STOP_PATTERNS`. It does not alter emergency-stop behavior, imports, or exports.
|
|
62
|
+
- **Migration parity:** none required — this touches no agent-installed files (`.claude/settings.json`, `.instar/config.json`, CLAUDE.md template, hook scripts, built-in skills). It is repo-internal test + docs.
|
|
63
|
+
- **CI:** adds one fast unit test (~3ms). No new dependencies (node `fs`/`path` only).
|
|
64
|
+
|
|
65
|
+
## 6. Rollback
|
|
66
|
+
|
|
67
|
+
Deleting `tests/unit/keyword-intent-decision-ratchet.test.ts` and reverting the MessageSentinel comment fully removes the change with no state or data implications. The report-mode flag (`ENFORCE = false`) means even a mis-tuned detector cannot block CI before the deliberate enforcement flip.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Side-Effects Review — Session Respawn/Kill Thrash Elimination (veto-backoff for the idle-zombie hot-spin)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/session-respawn-thrash-elimination.md (converged 2026-07-03T22:47:58Z, 5 iterations, cross-model codex-cli:gpt-5.5; approved by Justin — explicit "yes please" to build, topic 30823, 2026-07-03).
|
|
4
|
+
**Parent principle:** P19 No Unbounded Loops — a kill the authority VETOES must back off, not re-fire on the next monitor tick.
|
|
5
|
+
**Scope:** Fix A (VetoedKillBackoff veto-backoff ledger, RC#1) + Fix A′ (P19 breaker) ONLY. Fix B (reap-log rotation — shipped separately in #1356), Fix C (post-transfer closeout), Fix D (reaper memory metric) are EXPLICITLY out of this PR (spec §Rollout).
|
|
6
|
+
**Ships:** dev-agent LIVE / fleet-DARK via `resolveDevAgentGate(monitoring.idleKillVetoBackoff.enabled, config)` — `enabled` omitted from the migrated default so the gate decides. Single instances are per-machine (machine-local by design).
|
|
7
|
+
**Files:** src/core/VetoedKillBackoff.ts (new), src/core/AgeKillBackoff.ts (→ back-compat shim), src/monitoring/IncidentDedupe.ts (new), src/core/SessionManager.ts, src/commands/server.ts, src/core/types.ts, src/core/PostUpdateMigrator.ts, tests/unit/vetoed-kill-backoff.test.ts (new), tests/unit/session-manager-idle-veto-backoff.test.ts (new), tests/unit/AgeKillBackoff.test.ts (one assertion updated for the generalized 0-cooldown semantics).
|
|
8
|
+
|
|
9
|
+
## What changed
|
|
10
|
+
|
|
11
|
+
1. **VetoedKillBackoff.ts (new):** generalizes the shipped `AgeKillBackoff` primitive. Per-entry value shape changes `number` → `{ until, reasonKey, logged, episodeCount }` (L1); adds reason-KEY-aware `shouldRequest(id, now, reasonKey?)` stale-reprieve (a changed protection re-checks now); `recordVeto` returns `firstOfEpisode` (once-per-episode log gate folded into the value, no parallel Set); `episodeCount(id)` feeds the breaker. Exports `normalizeReasonKey` (the ReapGuard `reason` field is already a stable enum, used verbatim as the key) + `KNOWN_REAP_KEEP_REASONS` (test source-of-truth). Pure logic, injectable clock, `maxTracked` oldest-eviction.
|
|
12
|
+
2. **AgeKillBackoff.ts (shim):** re-exports `VetoedKillBackoff as AgeKillBackoff` so the age-gate callsites + shipped tests keep compiling and behaving identically (2-arg `shouldRequest`/`recordVeto`, ignored return).
|
|
13
|
+
3. **SessionManager.ts:** a SECOND, independently-configured `idleKillBackoff?: VetoedKillBackoff` for the idle-zombie branch (the age-gate keeps its own instance). Constructed ONLY when `opts.idleKillVetoBackoff.enabled === true` (C1 disabled contract — otherwise `undefined` and never consulted). New private `computeIdleZombieReapVerdict` (guards once), `handleIdleZombie` (the enabled/disabled split), `maybeEscalateIdleZombieVeto` (Fix A′ breaker). `terminateSession` refactored: the public method is UNCHANGED and delegates to a new private `terminateSessionInternal(…, precomputedGuardVerdict?)`; the precompute is threaded ONLY by the idle-zombie branch. Map-leak `clear`/`reset` co-located at every `idlePromptSince` lifecycle exit.
|
|
14
|
+
4. **IncidentDedupe.ts (new):** the `IncidentDedupe` seam + `InProcessIncidentDedupe` (best-effort in-process coalescing + TTL, bounded `maxEntries`, injectable clock) per spec C4. The breaker depends on the seam, never on the transport.
|
|
15
|
+
5. **server.ts:** resolves `idleKillVetoBackoff` via the dev-agent gate at the SessionManager construction boundary (mirrors `idleThrottleSettleGate`); injects `InProcessIncidentDedupe`; routes the `idleZombieVetoEscalation` event to ONE flood-guarded `telegram.createAttentionItem` (HIGH, `sourceContext: idle-zombie-veto:<topic>:<reason>`), guarded by `if (!telegram)`.
|
|
16
|
+
6. **types.ts:** the `monitoring.idleKillVetoBackoff?` config schema.
|
|
17
|
+
7. **PostUpdateMigrator.ts:** `migrateConfigIdleKillVetoBackoffDefault` — existence-checked, idempotent, seeds ONLY the tuning knobs (`cooldownMs`, `escalateAfterEpisodes`); `enabled` DELIBERATELY OMITTED so the dev-agent gate governs it (enable-path integrity).
|
|
18
|
+
|
|
19
|
+
## Phase-4 review questions
|
|
20
|
+
|
|
21
|
+
**1. Over-block — what legitimate inputs does this reject that it shouldn't?**
|
|
22
|
+
None. The change never rejects a kill that would happen today. A session with NO keep-reason still returns `terminated:true` on the first attempt with no cooldown recorded (VetoedKillBackoff test 2; SessionManager test 2). The ONLY thing "held back" is the *re-attempt* of a kill the ReapGuard already vetoed — which the guard is going to veto again identically 5s later. No legitimate kill is delayed: an abandoned session (guard clears) dies on the first attempt exactly as before.
|
|
23
|
+
|
|
24
|
+
**2. Under-block — what failure modes does this still miss?**
|
|
25
|
+
By design it does not touch RC#2 (post-transfer closeout churn, ~1% of volume — deferred to Fix C with a defined shape) or the 132MB historical reap-log file (an archive runbook step at Mini activation; growth is stopped, the historical file is not shrunk by this PR). The escalation dedupe is best-effort (C4): across a restart or on a second machine a duplicate attention item is possible — acceptable, the goal is flood-avoidance not exactly-once. All three are explicitly scoped OUT in the spec, not silent gaps.
|
|
26
|
+
|
|
27
|
+
**3. Level-of-abstraction fit — right layer? smarter gate already exists?**
|
|
28
|
+
Yes, and it deliberately REUSES the existing gate rather than adding a parallel one. The age-gate one branch up already routes through `AgeKillBackoff` (#863); this generalizes that SAME primitive to the idle-zombie branch instead of hand-rolling a second `Map`. The alternative (a centralized kill-attempt scheduler / global reap-log rate-limiter) was considered and rejected in the spec (§C5): it would change the cadence of kill classes that work correctly today for no additional coverage of the actual root cause. The fix is branch-local, minimal blast radius.
|
|
29
|
+
|
|
30
|
+
**4. Signal vs authority compliance (docs/signal-vs-authority.md).**
|
|
31
|
+
COMPLIANT. The veto-backoff holds NO kill authority — it only decides how OFTEN a *vetoed* kill is re-attempted. `terminateSession` remains the sole enforcement boundary (ReapGuard is the sole authority). The precomputed verdict is a PRIVATE same-tick optimization confined to `terminateSessionInternal` (idle-zombie only); the public signature exposes NO `precomputedGuardVerdict` (R3-1/R4-2), so no external caller can inject an authoritative reason for another kill class. The breaker (Fix A′) is SIGNAL-ONLY: it emits an event → one flood-guarded attention item; it never blocks a kill, never spawns a topic, never mutates authority. The breaker depends on the `IncidentDedupe` SEAM, not on the transport (decoupled).
|
|
32
|
+
|
|
33
|
+
**5. Interactions — shadow / shadowed / double-fire / race?**
|
|
34
|
+
- Funnels through the single-writer `terminateSession(Internal)` path + the in-flight `terminating` lock, so it can never double-kill or double-emit with the SessionReaper (§3.6 preserved).
|
|
35
|
+
- Compaction-recovery veto (line ~2015) still short-circuits BEFORE the idle-zombie handler and now also `reset()`s the ledger so a recovered session is re-evaluated fresh — no stale suppression.
|
|
36
|
+
- The age-gate uses a SEPARATE VetoedKillBackoff instance with its own knob/cadence — a veto in one branch never suppresses the other.
|
|
37
|
+
- Reducing `reapBlocked` emissions (the reap-log skip write) for vetoed idle-zombie kills IS the intended effect (that emission was the flood); no other consumer depends on its per-tick frequency.
|
|
38
|
+
|
|
39
|
+
**6. External surfaces — visible to other agents/users/systems? timing/state dependent?**
|
|
40
|
+
- User-visible: at most ONE HIGH attention item per genuinely-stuck session per incident (down from thousands of silent reap-log writes) — routed through the existing flood guard, never a new topic. Reap-log `idle-zombie` skip writes drop from ~3,200/day to ≤48/day (the north-star metric).
|
|
41
|
+
- No new HTTP route, no new MeshRpc verb, no new cross-agent surface.
|
|
42
|
+
- Timing: the cooldown uses an injectable clock in the ledger; the SessionManager path reads `now` from the monitor tick. Deterministic given the tick.
|
|
43
|
+
|
|
44
|
+
**7. Multi-machine posture (Cross-Machine Coherence).**
|
|
45
|
+
**Machine-local BY DESIGN — `machine-local-justification: hardware-bound-resource`.** The cooldown state is a property of THIS machine's monitor loop against THIS machine's live tmux/session set (a hardware-bound resource). It is never replicated — replicating it would be actively wrong (machine A's window says nothing about machine B's session set, and a peer could suppress a legitimate local evaluation). Strict no-op on a single-machine agent. The escalation dedupe is likewise per-machine/per-process (C4 best-effort). This posture is documented in the spec's §Multi-machine posture.
|
|
46
|
+
|
|
47
|
+
**8. Rollback cost.**
|
|
48
|
+
Trivial + zero-migration. Set `monitoring.idleKillVetoBackoff.enabled: false` → per the C1 disabled contract the `idleKillBackoff` instance is not constructed and not consulted; the branch reverts to EXACT prior per-tick behavior. There is NO persisted state to unwind (the ledger is in-memory only). The age-gate's own `ageKillBackoffMinutes` knob is untouched (independent instance). Full code revert is additive/dark throughout.
|
|
49
|
+
|
|
50
|
+
## Enable-path integrity (the one real bug caught in review)
|
|
51
|
+
|
|
52
|
+
The spec's §Config JSON example shows `"enabled": false` in the block. Writing that literal into `migrateConfig` would FORCE-DARK the development agent too (`resolveDevAgentGate` resolves `explicit ?? !!developmentAgent` — an explicit `false` wins the `??`), defeating the spec's §Activation milestone-1 "soak LIVE on Echo first" plan. **Fixed:** `migrateConfigIdleKillVetoBackoffDefault` seeds ONLY the tuning knobs and OMITS `enabled`, mirroring the stateSync-stores + tmux-resilience precedent (`resolveStateSyncStores` doc, devAgentGate.ts:55). Verified by the migrateConfig test (`enabled` asserted absent) and `scripts/lint-dev-agent-dark-gate.js` clean.
|
|
53
|
+
|
|
54
|
+
## Age-gate regression check
|
|
55
|
+
|
|
56
|
+
The shared class changed the internal value shape and, at `backoffMs:0`, now RECORDS a bounded entry (the old `AgeKillBackoff` early-returned, recording nothing). The age-gate's kill cadence + authority are IDENTICAL: at `backoffMs:0` `shouldRequest` still returns true every tick (the `backoffMs === 0` short-circuit), and the age-gate ignores `recordVeto`'s new boolean return. The only observable difference is bounded memory tracking (capped at `maxTracked=1024`, oldest-evicted). One assertion in `AgeKillBackoff.test.ts` updated to reflect the generalized "no-cooldown, but tracks" semantics (R4-5); the age-kill integration test (12 requests/tick at `backoffMs:0`) passes unchanged.
|
|
57
|
+
|
|
58
|
+
## Tests
|
|
59
|
+
|
|
60
|
+
- `tests/unit/vetoed-kill-backoff.test.ts` (20) — value-shape/L1 (remainingMs reads `.until` mid-cooldown + expired→0), reason-key stale-reprieve (changed key invalidates; same/omitted key = today's behavior), `recordVeto` firstOfEpisode + episodeCount + new-reason-is-new-episode, lifecycle drops (recordKilled/clear/reset), `maxTracked` eviction, `cooldownMs:0` records-but-no-gate, EXHAUSTIVE `normalizeReasonKey` over every KNOWN_REAP_KEEP_REASONS + unknown-with-discriminator + unkeyable→null, 2-arg age-gate back-compat.
|
|
61
|
+
- `tests/unit/session-manager-idle-veto-backoff.test.ts` (14) — vetoed kill sets cooldown + no re-fire next tick; cleared kill still kills (authority unchanged); cooldown expiry ⇒ one fresh attempt; active-output reset; one WARN per episode; DISABLED contract (undefined ledger, attempt every tick, no gating, no breaker); single guard-eval (blockedReason once per tick); public `terminateSession` exposes no precompute option; `cooldownMs:0` enabled-but-no-cooldown; breaker emits exactly ONE escalation after N episodes (injected dedupe); migrateConfig default (enabled OMITTED) + operator-override-untouched + idempotent.
|
|
62
|
+
- Regression: `AgeKillBackoff.test.ts` (7) + `age-kill-backoff-integration.test.ts` (4) green. **45/45 in the feature+regression set; `tsc --noEmit` exit 0.**
|
|
63
|
+
- Two unrelated suite failures (`PostUpdateMigrator-mcpAutorefresh`, `autonomous-stop-hook-realcheck`) are ENVIRONMENTAL (mock-claude subprocess harness absent in this sandbox) — verified failing IDENTICALLY on the pristine baseline with all tracked changes stashed. Not caused by this change.
|
|
64
|
+
|
|
65
|
+
## Second-pass review
|
|
66
|
+
|
|
67
|
+
Session-lifecycle change → an independent reviewer subagent audited the diff across 10 dimensions and ran the tests (45/45 green at review time; tsc clean).
|
|
68
|
+
|
|
69
|
+
**Verdict: CONCERN RAISED (non-blocking) — point 9 (multi-machine standby breaker mis-attribution), now FIXED.** All other dimensions concurred: authority preserved (only retry cadence changes, ReapGuard remains sole authority, public signature exposes no precompute — points 1/3/12); disabled contract exact (point 2); single-guard-eval verified (point 3); map-leak bounded (point 4, one cosmetic gap noted — a `terminateSessionInternal` success by a *different* killer leaves an entry until `cleanupStaleSessions`; not a true leak — unique-per-spawn ids, `maxTracked` ceiling, and it matches the age-gate's own long-standing profile); enable-path integrity confirmed fixed (point 5); breaker signal-only + double-deduped (point 6 — `IncidentDedupe` TTL map AND `createAttentionItem` id-idempotency, stronger than the spec's best-effort claim); age-gate regression-free (point 7); reason keys strictly low-cardinality (point 8).
|
|
70
|
+
|
|
71
|
+
**Point 9 — the real finding, and the fix.** The idle-zombie kill is `origin:'autonomous'`, so on a STANDBY (non-lease-holder) machine `terminateSessionInternal` short-circuits at the lease gate with `skipped:'not-lease-holder'` BEFORE the keep-guard runs. The original code recorded that as a veto episode and, after N episodes, raised a HIGH attention item reading "permanently vetoed from idle-zombie cleanup (reason: not-lease-holder)" — semantically false, and since Echo is multi-machine (Mini+Laptop) the standby machine WOULD emit it. Same for `in-flight` / `protected` / `already-*`. The spec's "strict no-op on a single-machine agent" claim silently didn't cover the standby-machine escalation path.
|
|
72
|
+
|
|
73
|
+
**Resolution (surgical, preserves every benefit):** the COOLDOWN (`recordVeto`) still applies to EVERY skip reason — backing off `not-lease-holder` on a standby machine is itself a *bonus* flood-stop (it is a documented reap-log flood source). Only the BREAKER escalation is now gated on the new `IDLE_ZOMBIE_ESCALATION_REASONS` set (VetoedKillBackoff.ts) — the genuine stuck-session keep-reasons (open-commitment, recent-user-message, active-subagent, structural-long-work, active-process, main-process-active, process-uninspectable, pending-injection, relay-lease). Authority/CAS skips (`not-lease-holder`, `in-flight`, `not-found`, `already-*`) and intentional/transient keeps (`protected`, `spawn-grace`, `recovery-in-flight`, `guard-error`) cool down but NEVER raise the "stuck session" item. New test `session-manager-idle-veto-backoff.test.ts` — "a standby (not-lease-holder) skip cools down but NEVER escalates the breaker" (10 episodes past threshold → 0 escalations, ledger tracks → cooldown engaged). 46/46 green after the fix. The spec's §Fix A′ gained a clarifying note recording this gate.
|
|
74
|
+
|
|
75
|
+
The cosmetic point-4 gap (a `:1438` eviction) was left as-is by deliberate choice: it matches the shipped age-gate's identical profile, is bounded by `maxTracked`, and adding an eviction there touches the hot single-writer success path for zero functional gain — out of proportion to the benefit.
|