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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VetoedKillBackoff — a per-session veto-backoff ledger for the SessionManager
|
|
3
|
+
* kill branches (the age-gate AND the bound-idle zombie killer).
|
|
4
|
+
*
|
|
5
|
+
* SessionManager.monitorTick runs every 5 seconds. When it decides a session is
|
|
6
|
+
* killable (past its max age, or idle-at-prompt past its bound-idle threshold) it
|
|
7
|
+
* requests a kill via terminateSession → ReapGuard. If the §P2 KEEP-guard vetoes
|
|
8
|
+
* that kill (a recent user message, an open commitment, a live subagent, …), the
|
|
9
|
+
* session correctly survives — but a naive caller re-requests the SAME kill every
|
|
10
|
+
* 5 seconds forever (720 attempts/hour/session → the 2026-06-05 17,503-line log
|
|
11
|
+
* flood, and the 2026-07-03 132MB reap-log idle-zombie hot-spin).
|
|
12
|
+
*
|
|
13
|
+
* This ledger makes the kill branch RESPECT the guard's verdict: after a veto it
|
|
14
|
+
* suppresses re-requests for `backoffMs` (so a kept session is re-checked on a slow
|
|
15
|
+
* cadence, not every tick). It changes only how OFTEN a vetoed kill is retried —
|
|
16
|
+
* never WHICH sessions are killed (the KEEP-guard remains the sole authority). A
|
|
17
|
+
* genuinely-idle-abandoned session has no keep-reason, so its first request returns
|
|
18
|
+
* terminated:true and it dies exactly as before.
|
|
19
|
+
*
|
|
20
|
+
* Generalized from the shipped `AgeKillBackoff` (#863): a superset that adds a
|
|
21
|
+
* reason-KEY-aware stale-reprieve (a changed protection re-checks now, doesn't wait
|
|
22
|
+
* out the window), an episode counter (feeds the P19 breaker), and a once-per-episode
|
|
23
|
+
* log gate (`recordVeto` returns `firstOfEpisode`) — all folded into the ledger value
|
|
24
|
+
* so no parallel Set is needed. The class is not persisted; there is no on-disk value
|
|
25
|
+
* to migrate.
|
|
26
|
+
*
|
|
27
|
+
* Pure logic, injectable clock, bounded memory — unit-testable in isolation.
|
|
28
|
+
*/
|
|
29
|
+
export interface VetoedKillBackoffOptions {
|
|
30
|
+
/** Suppress kill re-requests for this long after a veto. 0 = no cooldown window
|
|
31
|
+
* (still tracks episodes + log-once + breaker; NOT a disable — the disable path
|
|
32
|
+
* is never constructing the instance). */
|
|
33
|
+
backoffMs: number;
|
|
34
|
+
/** Hard cap on distinct sessions tracked (memory bound; oldest-evicted). */
|
|
35
|
+
maxTracked: number;
|
|
36
|
+
}
|
|
37
|
+
export declare const DEFAULT_VETOED_KILL_BACKOFF: VetoedKillBackoffOptions;
|
|
38
|
+
/** Back-compat aliases for the shipped age-gate name. */
|
|
39
|
+
export type AgeKillBackoffOptions = VetoedKillBackoffOptions;
|
|
40
|
+
export declare const DEFAULT_AGE_KILL_BACKOFF: VetoedKillBackoffOptions;
|
|
41
|
+
/**
|
|
42
|
+
* Map a ReapGuard blockedReason result (or a bare reason string) to the STABLE
|
|
43
|
+
* ledger key. The `reason` field ReapGuard returns is already a stable enum key
|
|
44
|
+
* (`open-commitment`, `recent-user-message`, …), NOT free-form/interpolated text,
|
|
45
|
+
* so it is safe to use verbatim as the ledger key (R3-2). The human-readable string
|
|
46
|
+
* (if any interpolation ever existed) is used only for WARN/attention wording; only
|
|
47
|
+
* this stable key crosses into the ledger.
|
|
48
|
+
*
|
|
49
|
+
* Returns null when there is no usable reason string — the "unkeyable, fail-open"
|
|
50
|
+
* case: the caller re-evaluates this tick rather than minting a fabricated key.
|
|
51
|
+
*/
|
|
52
|
+
export declare function normalizeReasonKey(blocked: {
|
|
53
|
+
reason?: string;
|
|
54
|
+
} | string | null | undefined): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Every keep-reason `ReapGuard.blockedReason()` can currently return (source of
|
|
57
|
+
* truth: src/core/ReapGuard.ts evaluate()). Used by the exhaustive normalization
|
|
58
|
+
* test — each maps to its OWN distinct key, and no two materially-different keep
|
|
59
|
+
* reasons collapse onto one another.
|
|
60
|
+
*/
|
|
61
|
+
export declare const KNOWN_REAP_KEEP_REASONS: readonly ["protected", "spawn-grace", "recovery-in-flight", "pending-injection", "relay-lease", "recent-user-message", "open-commitment", "active-subagent", "structural-long-work", "active-process", "process-uninspectable", "main-process-active", "guard-error"];
|
|
62
|
+
/**
|
|
63
|
+
* The subset of skip reasons whose PERSISTENCE is a genuine "this session is STUCK
|
|
64
|
+
* and should be investigated" signal — i.e. the ones the Fix A′ P19 breaker may
|
|
65
|
+
* escalate on (spec §Fix A′: "likely a stuck open-commitment or a resume-loop").
|
|
66
|
+
*
|
|
67
|
+
* DELIBERATELY EXCLUDED (second-pass review, point 9 — the multi-machine standby
|
|
68
|
+
* escalation gap the converged spec did not cover):
|
|
69
|
+
* - Authority/CAS skips `terminateSession` returns BEFORE the keep-guard runs —
|
|
70
|
+
* `not-lease-holder` (a STANDBY machine returns this every tick for every idle
|
|
71
|
+
* session — escalating it as "vetoed from cleanup" is false), `in-flight`,
|
|
72
|
+
* `not-found`, `already-completed`/`already-killed`/`already-*`. These are not
|
|
73
|
+
* keep-reasons at all.
|
|
74
|
+
* - Intentional / transient keep-reasons that are NOT a stuck anomaly:
|
|
75
|
+
* `protected` (operator-configured permanent keep), `spawn-grace` (startup),
|
|
76
|
+
* `recovery-in-flight` (a bounded recovery window), `guard-error` (a transient
|
|
77
|
+
* inspect failure that resolves itself).
|
|
78
|
+
*
|
|
79
|
+
* A skip reason OUTSIDE this set still COOLS DOWN (recordVeto → the flood-stop
|
|
80
|
+
* benefit applies universally, incl. the standby `not-lease-holder` flood) — it
|
|
81
|
+
* simply never raises the "stuck session" attention item.
|
|
82
|
+
*/
|
|
83
|
+
export declare const IDLE_ZOMBIE_ESCALATION_REASONS: ReadonlySet<string>;
|
|
84
|
+
export declare class VetoedKillBackoff {
|
|
85
|
+
private readonly backoffMs;
|
|
86
|
+
private readonly maxTracked;
|
|
87
|
+
/** sessionId -> ledger entry (insertion-ordered for oldest-eviction). */
|
|
88
|
+
private readonly entries;
|
|
89
|
+
constructor(opts?: Partial<VetoedKillBackoffOptions>);
|
|
90
|
+
/**
|
|
91
|
+
* Whether the kill branch may request a kill for this session right now.
|
|
92
|
+
*
|
|
93
|
+
* - backoffMs === 0 → always true (no cooldown gate).
|
|
94
|
+
* - no entry → true (never vetoed / already cleared).
|
|
95
|
+
* - STALE-REPRIEVE (R3-2): a stored key AND a supplied key that DIFFER means the
|
|
96
|
+
* protection changed → invalidate the entry and allow one fresh evaluation now.
|
|
97
|
+
* - else → true once `nowMs >= entry.until`.
|
|
98
|
+
*
|
|
99
|
+
* Back-compat: `reasonKey` omitted (age-gate 2-arg callsites) ⇒ treated as null
|
|
100
|
+
* ⇒ no stale-reprieve, today's behavior.
|
|
101
|
+
*/
|
|
102
|
+
shouldRequest(sessionId: string, nowMs: number, reasonKey?: string | null): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Record that the guard KEPT this session (kill vetoed) — back off re-requests.
|
|
105
|
+
* Returns `firstOfEpisode`: true exactly once per (session, reasonKey) episode so
|
|
106
|
+
* the caller logs the WARN once, not every tick.
|
|
107
|
+
*
|
|
108
|
+
* With backoffMs === 0 (no cooldown) the entry is STILL recorded — episode
|
|
109
|
+
* counting, the once-per-episode log gate, and the P19 breaker all still work
|
|
110
|
+
* (R4-5: cooldownMs:0 is enabled-but-no-cooldown, NOT a disable).
|
|
111
|
+
*/
|
|
112
|
+
recordVeto(sessionId: string, nowMs: number, reasonKey?: string | null): boolean;
|
|
113
|
+
/** Consecutive veto episodes recorded for this session (0 if none). */
|
|
114
|
+
episodeCount(sessionId: string): number;
|
|
115
|
+
/** The session was actually killed — drop its state (episode ends). */
|
|
116
|
+
recordKilled(sessionId: string): void;
|
|
117
|
+
/** Session is gone (cleanup on removal). */
|
|
118
|
+
clear(sessionId: string): void;
|
|
119
|
+
/** A session's state changed materially (e.g. it resumed work) — drop the
|
|
120
|
+
* back-off so it is re-evaluated at the next tick rather than staying suppressed. */
|
|
121
|
+
reset(sessionId: string): void;
|
|
122
|
+
/** Test/inspection seam — ms remaining in the back-off window (0 if not
|
|
123
|
+
* suppressed). Reads `.until`, never the whole object (L1). */
|
|
124
|
+
remainingMs(sessionId: string, nowMs: number): number;
|
|
125
|
+
/** Test/inspection seam. */
|
|
126
|
+
get trackedCount(): number;
|
|
127
|
+
/** Drop the oldest entry while over the cap (insertion-ordered Map). */
|
|
128
|
+
private evictIfNeeded;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=VetoedKillBackoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VetoedKillBackoff.d.ts","sourceRoot":"","sources":["../../src/core/VetoedKillBackoff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,wBAAwB;IACvC;;+CAE2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,2BAA2B,EAAE,wBAGzC,CAAC;AAEF,yDAAyD;AACzD,MAAM,MAAM,qBAAqB,GAAG,wBAAwB,CAAC;AAC7D,eAAO,MAAM,wBAAwB,EAAE,wBAAsD,CAAC;AAwB9F;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvD,MAAM,GAAG,IAAI,CAIf;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,uQAc1B,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,8BAA8B,EAAE,WAAW,CAAC,MAAM,CAU7D,CAAC;AAEH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;gBAE/C,IAAI,GAAE,OAAO,CAAC,wBAAwB,CAAM;IAOxD;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAYnF;;;;;;;;OAQG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IA4BhF,uEAAuE;IACvE,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIvC,uEAAuE;IACvE,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrC,4CAA4C;IAC5C,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI9B;0FACsF;IACtF,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI9B;oEACgE;IAChE,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAKrD,4BAA4B;IAC5B,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,wEAAwE;IACxE,OAAO,CAAC,aAAa;CAOtB"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VetoedKillBackoff — a per-session veto-backoff ledger for the SessionManager
|
|
3
|
+
* kill branches (the age-gate AND the bound-idle zombie killer).
|
|
4
|
+
*
|
|
5
|
+
* SessionManager.monitorTick runs every 5 seconds. When it decides a session is
|
|
6
|
+
* killable (past its max age, or idle-at-prompt past its bound-idle threshold) it
|
|
7
|
+
* requests a kill via terminateSession → ReapGuard. If the §P2 KEEP-guard vetoes
|
|
8
|
+
* that kill (a recent user message, an open commitment, a live subagent, …), the
|
|
9
|
+
* session correctly survives — but a naive caller re-requests the SAME kill every
|
|
10
|
+
* 5 seconds forever (720 attempts/hour/session → the 2026-06-05 17,503-line log
|
|
11
|
+
* flood, and the 2026-07-03 132MB reap-log idle-zombie hot-spin).
|
|
12
|
+
*
|
|
13
|
+
* This ledger makes the kill branch RESPECT the guard's verdict: after a veto it
|
|
14
|
+
* suppresses re-requests for `backoffMs` (so a kept session is re-checked on a slow
|
|
15
|
+
* cadence, not every tick). It changes only how OFTEN a vetoed kill is retried —
|
|
16
|
+
* never WHICH sessions are killed (the KEEP-guard remains the sole authority). A
|
|
17
|
+
* genuinely-idle-abandoned session has no keep-reason, so its first request returns
|
|
18
|
+
* terminated:true and it dies exactly as before.
|
|
19
|
+
*
|
|
20
|
+
* Generalized from the shipped `AgeKillBackoff` (#863): a superset that adds a
|
|
21
|
+
* reason-KEY-aware stale-reprieve (a changed protection re-checks now, doesn't wait
|
|
22
|
+
* out the window), an episode counter (feeds the P19 breaker), and a once-per-episode
|
|
23
|
+
* log gate (`recordVeto` returns `firstOfEpisode`) — all folded into the ledger value
|
|
24
|
+
* so no parallel Set is needed. The class is not persisted; there is no on-disk value
|
|
25
|
+
* to migrate.
|
|
26
|
+
*
|
|
27
|
+
* Pure logic, injectable clock, bounded memory — unit-testable in isolation.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_VETOED_KILL_BACKOFF = {
|
|
30
|
+
backoffMs: 10 * 60 * 1000, // 10 minutes → 6 attempts/hr (was 720/hr)
|
|
31
|
+
maxTracked: 1024,
|
|
32
|
+
};
|
|
33
|
+
export const DEFAULT_AGE_KILL_BACKOFF = DEFAULT_VETOED_KILL_BACKOFF;
|
|
34
|
+
function coerceNonNegInt(v, fallback) {
|
|
35
|
+
const n = typeof v === 'number' ? v : NaN;
|
|
36
|
+
if (!Number.isFinite(n) || n < 0)
|
|
37
|
+
return fallback;
|
|
38
|
+
return Math.floor(n);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Map a ReapGuard blockedReason result (or a bare reason string) to the STABLE
|
|
42
|
+
* ledger key. The `reason` field ReapGuard returns is already a stable enum key
|
|
43
|
+
* (`open-commitment`, `recent-user-message`, …), NOT free-form/interpolated text,
|
|
44
|
+
* so it is safe to use verbatim as the ledger key (R3-2). The human-readable string
|
|
45
|
+
* (if any interpolation ever existed) is used only for WARN/attention wording; only
|
|
46
|
+
* this stable key crosses into the ledger.
|
|
47
|
+
*
|
|
48
|
+
* Returns null when there is no usable reason string — the "unkeyable, fail-open"
|
|
49
|
+
* case: the caller re-evaluates this tick rather than minting a fabricated key.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeReasonKey(blocked) {
|
|
52
|
+
if (!blocked)
|
|
53
|
+
return null;
|
|
54
|
+
const reason = typeof blocked === 'string' ? blocked : blocked.reason;
|
|
55
|
+
return typeof reason === 'string' && reason.length > 0 ? reason : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Every keep-reason `ReapGuard.blockedReason()` can currently return (source of
|
|
59
|
+
* truth: src/core/ReapGuard.ts evaluate()). Used by the exhaustive normalization
|
|
60
|
+
* test — each maps to its OWN distinct key, and no two materially-different keep
|
|
61
|
+
* reasons collapse onto one another.
|
|
62
|
+
*/
|
|
63
|
+
export const KNOWN_REAP_KEEP_REASONS = [
|
|
64
|
+
'protected',
|
|
65
|
+
'spawn-grace',
|
|
66
|
+
'recovery-in-flight',
|
|
67
|
+
'pending-injection',
|
|
68
|
+
'relay-lease',
|
|
69
|
+
'recent-user-message',
|
|
70
|
+
'open-commitment',
|
|
71
|
+
'active-subagent',
|
|
72
|
+
'structural-long-work',
|
|
73
|
+
'active-process',
|
|
74
|
+
'process-uninspectable',
|
|
75
|
+
'main-process-active',
|
|
76
|
+
'guard-error',
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* The subset of skip reasons whose PERSISTENCE is a genuine "this session is STUCK
|
|
80
|
+
* and should be investigated" signal — i.e. the ones the Fix A′ P19 breaker may
|
|
81
|
+
* escalate on (spec §Fix A′: "likely a stuck open-commitment or a resume-loop").
|
|
82
|
+
*
|
|
83
|
+
* DELIBERATELY EXCLUDED (second-pass review, point 9 — the multi-machine standby
|
|
84
|
+
* escalation gap the converged spec did not cover):
|
|
85
|
+
* - Authority/CAS skips `terminateSession` returns BEFORE the keep-guard runs —
|
|
86
|
+
* `not-lease-holder` (a STANDBY machine returns this every tick for every idle
|
|
87
|
+
* session — escalating it as "vetoed from cleanup" is false), `in-flight`,
|
|
88
|
+
* `not-found`, `already-completed`/`already-killed`/`already-*`. These are not
|
|
89
|
+
* keep-reasons at all.
|
|
90
|
+
* - Intentional / transient keep-reasons that are NOT a stuck anomaly:
|
|
91
|
+
* `protected` (operator-configured permanent keep), `spawn-grace` (startup),
|
|
92
|
+
* `recovery-in-flight` (a bounded recovery window), `guard-error` (a transient
|
|
93
|
+
* inspect failure that resolves itself).
|
|
94
|
+
*
|
|
95
|
+
* A skip reason OUTSIDE this set still COOLS DOWN (recordVeto → the flood-stop
|
|
96
|
+
* benefit applies universally, incl. the standby `not-lease-holder` flood) — it
|
|
97
|
+
* simply never raises the "stuck session" attention item.
|
|
98
|
+
*/
|
|
99
|
+
export const IDLE_ZOMBIE_ESCALATION_REASONS = new Set([
|
|
100
|
+
'open-commitment',
|
|
101
|
+
'recent-user-message',
|
|
102
|
+
'active-subagent',
|
|
103
|
+
'structural-long-work',
|
|
104
|
+
'active-process',
|
|
105
|
+
'main-process-active',
|
|
106
|
+
'process-uninspectable',
|
|
107
|
+
'pending-injection',
|
|
108
|
+
'relay-lease',
|
|
109
|
+
]);
|
|
110
|
+
export class VetoedKillBackoff {
|
|
111
|
+
backoffMs;
|
|
112
|
+
maxTracked;
|
|
113
|
+
/** sessionId -> ledger entry (insertion-ordered for oldest-eviction). */
|
|
114
|
+
entries = new Map();
|
|
115
|
+
constructor(opts = {}) {
|
|
116
|
+
const d = DEFAULT_VETOED_KILL_BACKOFF;
|
|
117
|
+
// backoffMs may be 0 (no cooldown window) — only a negative/NaN falls back.
|
|
118
|
+
this.backoffMs = coerceNonNegInt(opts.backoffMs, d.backoffMs);
|
|
119
|
+
this.maxTracked = Math.max(1, coerceNonNegInt(opts.maxTracked, d.maxTracked));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether the kill branch may request a kill for this session right now.
|
|
123
|
+
*
|
|
124
|
+
* - backoffMs === 0 → always true (no cooldown gate).
|
|
125
|
+
* - no entry → true (never vetoed / already cleared).
|
|
126
|
+
* - STALE-REPRIEVE (R3-2): a stored key AND a supplied key that DIFFER means the
|
|
127
|
+
* protection changed → invalidate the entry and allow one fresh evaluation now.
|
|
128
|
+
* - else → true once `nowMs >= entry.until`.
|
|
129
|
+
*
|
|
130
|
+
* Back-compat: `reasonKey` omitted (age-gate 2-arg callsites) ⇒ treated as null
|
|
131
|
+
* ⇒ no stale-reprieve, today's behavior.
|
|
132
|
+
*/
|
|
133
|
+
shouldRequest(sessionId, nowMs, reasonKey) {
|
|
134
|
+
if (this.backoffMs === 0)
|
|
135
|
+
return true;
|
|
136
|
+
const entry = this.entries.get(sessionId);
|
|
137
|
+
if (!entry)
|
|
138
|
+
return true;
|
|
139
|
+
if (entry.reasonKey != null && reasonKey != null && reasonKey !== entry.reasonKey) {
|
|
140
|
+
// Protection changed — re-check now rather than waiting out the window.
|
|
141
|
+
this.entries.delete(sessionId);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return nowMs >= entry.until;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Record that the guard KEPT this session (kill vetoed) — back off re-requests.
|
|
148
|
+
* Returns `firstOfEpisode`: true exactly once per (session, reasonKey) episode so
|
|
149
|
+
* the caller logs the WARN once, not every tick.
|
|
150
|
+
*
|
|
151
|
+
* With backoffMs === 0 (no cooldown) the entry is STILL recorded — episode
|
|
152
|
+
* counting, the once-per-episode log gate, and the P19 breaker all still work
|
|
153
|
+
* (R4-5: cooldownMs:0 is enabled-but-no-cooldown, NOT a disable).
|
|
154
|
+
*/
|
|
155
|
+
recordVeto(sessionId, nowMs, reasonKey) {
|
|
156
|
+
const key = reasonKey ?? null;
|
|
157
|
+
const existing = this.entries.get(sessionId);
|
|
158
|
+
let firstOfEpisode;
|
|
159
|
+
if (!existing || existing.reasonKey !== key) {
|
|
160
|
+
// New episode (no entry, or the reason changed) — fresh entry, log once.
|
|
161
|
+
this.entries.set(sessionId, {
|
|
162
|
+
until: nowMs + this.backoffMs,
|
|
163
|
+
reasonKey: key,
|
|
164
|
+
logged: true,
|
|
165
|
+
episodeCount: 1,
|
|
166
|
+
});
|
|
167
|
+
firstOfEpisode = true;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Same reason, existing episode — extend the window + count the episode.
|
|
171
|
+
existing.until = nowMs + this.backoffMs;
|
|
172
|
+
existing.episodeCount++;
|
|
173
|
+
if (existing.logged) {
|
|
174
|
+
firstOfEpisode = false;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
existing.logged = true;
|
|
178
|
+
firstOfEpisode = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.evictIfNeeded();
|
|
182
|
+
return firstOfEpisode;
|
|
183
|
+
}
|
|
184
|
+
/** Consecutive veto episodes recorded for this session (0 if none). */
|
|
185
|
+
episodeCount(sessionId) {
|
|
186
|
+
return this.entries.get(sessionId)?.episodeCount ?? 0;
|
|
187
|
+
}
|
|
188
|
+
/** The session was actually killed — drop its state (episode ends). */
|
|
189
|
+
recordKilled(sessionId) {
|
|
190
|
+
this.entries.delete(sessionId);
|
|
191
|
+
}
|
|
192
|
+
/** Session is gone (cleanup on removal). */
|
|
193
|
+
clear(sessionId) {
|
|
194
|
+
this.entries.delete(sessionId);
|
|
195
|
+
}
|
|
196
|
+
/** A session's state changed materially (e.g. it resumed work) — drop the
|
|
197
|
+
* back-off so it is re-evaluated at the next tick rather than staying suppressed. */
|
|
198
|
+
reset(sessionId) {
|
|
199
|
+
this.entries.delete(sessionId);
|
|
200
|
+
}
|
|
201
|
+
/** Test/inspection seam — ms remaining in the back-off window (0 if not
|
|
202
|
+
* suppressed). Reads `.until`, never the whole object (L1). */
|
|
203
|
+
remainingMs(sessionId, nowMs) {
|
|
204
|
+
const entry = this.entries.get(sessionId);
|
|
205
|
+
return entry != null && entry.until > nowMs ? entry.until - nowMs : 0;
|
|
206
|
+
}
|
|
207
|
+
/** Test/inspection seam. */
|
|
208
|
+
get trackedCount() {
|
|
209
|
+
return this.entries.size;
|
|
210
|
+
}
|
|
211
|
+
/** Drop the oldest entry while over the cap (insertion-ordered Map). */
|
|
212
|
+
evictIfNeeded() {
|
|
213
|
+
while (this.entries.size > this.maxTracked) {
|
|
214
|
+
const oldest = this.entries.keys().next().value;
|
|
215
|
+
if (oldest === undefined)
|
|
216
|
+
break;
|
|
217
|
+
this.entries.delete(oldest);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=VetoedKillBackoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VetoedKillBackoff.js","sourceRoot":"","sources":["../../src/core/VetoedKillBackoff.ts"],"names":[],"mappings":"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"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4509,6 +4509,24 @@ export interface MonitoringConfig {
|
|
|
4509
4509
|
idleThrottleSettleGate?: {
|
|
4510
4510
|
enabled?: boolean;
|
|
4511
4511
|
};
|
|
4512
|
+
/**
|
|
4513
|
+
* Session-respawn-thrash Fix A (docs/specs/session-respawn-thrash-elimination.md):
|
|
4514
|
+
* the veto-backoff ledger for the SessionManager bound-idle zombie killer. When a
|
|
4515
|
+
* session is idle-at-prompt past its bound-idle threshold but the ReapGuard
|
|
4516
|
+
* permanently vetoes the kill (open-commitment / recent-user-message), the killer
|
|
4517
|
+
* used to re-fire terminateSession every 5s forever (the 132MB reap-log hot-spin).
|
|
4518
|
+
* This ledger backs off re-attempts to one per `cooldownMs`, logs once per veto
|
|
4519
|
+
* episode, and raises ONE attention item after `escalateAfterEpisodes` (P19
|
|
4520
|
+
* breaker). `enabled` resolves via the developmentAgent dark-feature gate
|
|
4521
|
+
* (live-on-dev, dark-fleet); `cooldownMs: 0` is enabled-but-no-cooldown (NOT a
|
|
4522
|
+
* disable — the disable path is `enabled: false`, which never constructs the
|
|
4523
|
+
* ledger). Machine-local by design (never replicated).
|
|
4524
|
+
*/
|
|
4525
|
+
idleKillVetoBackoff?: {
|
|
4526
|
+
enabled?: boolean;
|
|
4527
|
+
cooldownMs?: number;
|
|
4528
|
+
escalateAfterEpisodes?: number;
|
|
4529
|
+
};
|
|
4512
4530
|
/**
|
|
4513
4531
|
* honest-session-state-surfaces Finding (b): lift the Tier-3 honest
|
|
4514
4532
|
* stuck-state classification into PresenceProxy Tier 1 / Tier 2 standby —
|