instar 1.3.61 → 1.3.63
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 +176 -10
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +36 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +45 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ReapGuard.d.ts +79 -0
- package/dist/core/ReapGuard.d.ts.map +1 -0
- package/dist/core/ReapGuard.js +114 -0
- package/dist/core/ReapGuard.js.map +1 -0
- package/dist/core/SessionLivenessOracle.d.ts +104 -0
- package/dist/core/SessionLivenessOracle.d.ts.map +1 -0
- package/dist/core/SessionLivenessOracle.js +201 -0
- package/dist/core/SessionLivenessOracle.js.map +1 -0
- package/dist/core/SessionManager.d.ts +90 -10
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +200 -40
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRefresh.d.ts +10 -0
- package/dist/core/SessionRefresh.d.ts.map +1 -1
- package/dist/core/SessionRefresh.js +12 -2
- package/dist/core/SessionRefresh.js.map +1 -1
- package/dist/core/types.d.ts +70 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/ContextWedgeSentinel.d.ts +118 -0
- package/dist/monitoring/ContextWedgeSentinel.d.ts.map +1 -0
- package/dist/monitoring/ContextWedgeSentinel.js +238 -0
- package/dist/monitoring/ContextWedgeSentinel.js.map +1 -0
- package/dist/monitoring/ReapLog.d.ts +51 -0
- package/dist/monitoring/ReapLog.d.ts.map +1 -0
- package/dist/monitoring/ReapLog.js +82 -0
- package/dist/monitoring/ReapLog.js.map +1 -0
- package/dist/monitoring/ReapNotifier.d.ts +66 -0
- package/dist/monitoring/ReapNotifier.d.ts.map +1 -0
- package/dist/monitoring/ReapNotifier.js +112 -0
- package/dist/monitoring/ReapNotifier.js.map +1 -0
- package/dist/monitoring/SentinelNotifier.d.ts +1 -1
- package/dist/monitoring/SentinelNotifier.d.ts.map +1 -1
- package/dist/monitoring/SentinelNotifier.js.map +1 -1
- package/dist/monitoring/SessionReaper.d.ts +4 -0
- package/dist/monitoring/SessionReaper.d.ts.map +1 -1
- package/dist/monitoring/SessionReaper.js +19 -42
- package/dist/monitoring/SessionReaper.js.map +1 -1
- package/dist/monitoring/StaleSessionBackstop.d.ts +89 -0
- package/dist/monitoring/StaleSessionBackstop.d.ts.map +1 -0
- package/dist/monitoring/StaleSessionBackstop.js +176 -0
- package/dist/monitoring/StaleSessionBackstop.js.map +1 -0
- package/dist/monitoring/sentinelWiring.d.ts +23 -0
- package/dist/monitoring/sentinelWiring.d.ts.map +1 -1
- package/dist/monitoring/sentinelWiring.js +36 -0
- package/dist/monitoring/sentinelWiring.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +5 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +33 -11
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +5 -0
- package/upgrades/1.3.62.md +87 -0
- package/upgrades/1.3.63.md +87 -0
- package/upgrades/side-effects/context-wedge-sentinel.md +69 -0
- package/upgrades/side-effects/unified-session-lifecycle-robustness.md +241 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReapGuard — the shared, stateless "is it safe to reap this session?" guard.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/unified-session-lifecycle-robustness.md §P2.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `SessionReaper.evaluate()` so that EVERY autonomous killer —
|
|
7
|
+
* not just the careful new reaper — consults the same positive-evidence KEEP
|
|
8
|
+
* checks before a session is ended. The single ReapAuthority
|
|
9
|
+
* (`SessionManager.terminateSession`) calls this guard; a non-null result means
|
|
10
|
+
* the session must be KEPT and the terminate is a no-op.
|
|
11
|
+
*
|
|
12
|
+
* Scope is the STATELESS guards only (deps-driven, no per-tick observation
|
|
13
|
+
* state): protected, spawn-grace, recovery-in-flight, pending-injection,
|
|
14
|
+
* relay-lease, recent-user-message, open-commitment, active-subagent,
|
|
15
|
+
* structural-long-work, active-process, main-process-uninspectable/active.
|
|
16
|
+
*
|
|
17
|
+
* The STATEFUL checks — transcript-growth (compares against the previous tick)
|
|
18
|
+
* and positive-idle proof (needs a captured pane frame) — stay inside
|
|
19
|
+
* `SessionReaper.evaluate()`, which calls this guard FIRST (preserving the exact
|
|
20
|
+
* keptBy ordering) and then layers its own per-instance checks.
|
|
21
|
+
*
|
|
22
|
+
* Ordering is identical to the original `evaluate()` so a refactor preserves
|
|
23
|
+
* every `keptBy` reason for the same input (asserted by the reaper's tests).
|
|
24
|
+
*/
|
|
25
|
+
import type { Session } from './types.js';
|
|
26
|
+
export type ReapConfidence = 'high' | 'low';
|
|
27
|
+
/** A KEEP verdict: the reason a session must not be reaped, + a confidence. */
|
|
28
|
+
export interface ReapKeepReason {
|
|
29
|
+
reason: string;
|
|
30
|
+
confidence: ReapConfidence;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Signal sources for the stateless guards. A subset of `SessionReaperDeps` so a
|
|
34
|
+
* single set of closures (built once in server.ts) can back both the reaper and
|
|
35
|
+
* the authority. Every "cannot inspect" path resolves to KEEP, never to reap.
|
|
36
|
+
*/
|
|
37
|
+
export interface ReapGuardDeps {
|
|
38
|
+
protectedSessions: () => string[];
|
|
39
|
+
isRecoveryActive: (session: Session) => boolean;
|
|
40
|
+
hasPendingInjection: (tmuxSession: string) => boolean;
|
|
41
|
+
isRelayLeaseActive: (sessionId: string) => boolean;
|
|
42
|
+
topicBinding: (tmuxSession: string) => number | null;
|
|
43
|
+
recentUserMessage: (topicId: number, withinMs: number) => boolean;
|
|
44
|
+
activeCommitmentForTopic: (topicId: number) => boolean;
|
|
45
|
+
activeSubagentCount: (claudeSessionId: string | undefined) => number;
|
|
46
|
+
buildOrAutonomousActive: (topicId: number | null) => boolean;
|
|
47
|
+
hasActiveProcesses: (tmuxSession: string) => boolean;
|
|
48
|
+
/** Optional main-process CPU/IO liveness. `undefined` ⇒ cannot inspect ⇒ KEEP. */
|
|
49
|
+
mainProcessActive?: (tmuxSession: string) => boolean | undefined;
|
|
50
|
+
now?: () => number;
|
|
51
|
+
}
|
|
52
|
+
export interface ReapGuardOptions {
|
|
53
|
+
/** Minimum session age before reap-eligibility (spawn grace). 0 disables. */
|
|
54
|
+
minAgeMs: number;
|
|
55
|
+
/** Window for "recent user message" on a bound topic. */
|
|
56
|
+
recentUserWindowMs: number;
|
|
57
|
+
/** Whether an open commitment on the bound topic blocks reaping. */
|
|
58
|
+
protectOpenCommitments: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare const DEFAULT_REAP_GUARD_OPTIONS: ReapGuardOptions;
|
|
61
|
+
export declare class ReapGuard {
|
|
62
|
+
private readonly deps;
|
|
63
|
+
private readonly opts;
|
|
64
|
+
private readonly now;
|
|
65
|
+
constructor(deps: ReapGuardDeps, opts?: Partial<ReapGuardOptions>);
|
|
66
|
+
/**
|
|
67
|
+
* Returns the reason this session must be KEPT (not reaped), or `null` if all
|
|
68
|
+
* stateless guards clear. Cheap-first ordering: in-memory checks before any
|
|
69
|
+
* subprocess fork (process tree / main-process probe) so a guarded session is
|
|
70
|
+
* rejected without paying for a capture.
|
|
71
|
+
*
|
|
72
|
+
* Never throws — a thrown signal source is caught and resolved to KEEP
|
|
73
|
+
* ('guard-error'), because we can never reason about a session we cannot
|
|
74
|
+
* inspect, and the safe answer is always "keep".
|
|
75
|
+
*/
|
|
76
|
+
blockedReason(session: Session): ReapKeepReason | null;
|
|
77
|
+
private evaluate;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=ReapGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReapGuard.d.ts","sourceRoot":"","sources":["../../src/core/ReapGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,CAAC;AAE5C,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,cAAc,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,iBAAiB,EAAE,MAAM,MAAM,EAAE,CAAC;IAClC,gBAAgB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IAChD,mBAAmB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;IACtD,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAClE,wBAAwB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACvD,mBAAmB,EAAE,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IACrE,uBAAuB,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC;IAC7D,kBAAkB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,CAAC;IACjE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED,eAAO,MAAM,0BAA0B,EAAE,gBAIxC,CAAC;AAEF,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC;IAMjE;;;;;;;;;OASG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI;IAQtD,OAAO,CAAC,QAAQ;CAkDjB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReapGuard — the shared, stateless "is it safe to reap this session?" guard.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/unified-session-lifecycle-robustness.md §P2.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `SessionReaper.evaluate()` so that EVERY autonomous killer —
|
|
7
|
+
* not just the careful new reaper — consults the same positive-evidence KEEP
|
|
8
|
+
* checks before a session is ended. The single ReapAuthority
|
|
9
|
+
* (`SessionManager.terminateSession`) calls this guard; a non-null result means
|
|
10
|
+
* the session must be KEPT and the terminate is a no-op.
|
|
11
|
+
*
|
|
12
|
+
* Scope is the STATELESS guards only (deps-driven, no per-tick observation
|
|
13
|
+
* state): protected, spawn-grace, recovery-in-flight, pending-injection,
|
|
14
|
+
* relay-lease, recent-user-message, open-commitment, active-subagent,
|
|
15
|
+
* structural-long-work, active-process, main-process-uninspectable/active.
|
|
16
|
+
*
|
|
17
|
+
* The STATEFUL checks — transcript-growth (compares against the previous tick)
|
|
18
|
+
* and positive-idle proof (needs a captured pane frame) — stay inside
|
|
19
|
+
* `SessionReaper.evaluate()`, which calls this guard FIRST (preserving the exact
|
|
20
|
+
* keptBy ordering) and then layers its own per-instance checks.
|
|
21
|
+
*
|
|
22
|
+
* Ordering is identical to the original `evaluate()` so a refactor preserves
|
|
23
|
+
* every `keptBy` reason for the same input (asserted by the reaper's tests).
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_REAP_GUARD_OPTIONS = {
|
|
26
|
+
minAgeMs: 30 * 60_000,
|
|
27
|
+
recentUserWindowMs: 30 * 60_000,
|
|
28
|
+
protectOpenCommitments: true,
|
|
29
|
+
};
|
|
30
|
+
export class ReapGuard {
|
|
31
|
+
deps;
|
|
32
|
+
opts;
|
|
33
|
+
now;
|
|
34
|
+
constructor(deps, opts) {
|
|
35
|
+
this.deps = deps;
|
|
36
|
+
this.opts = { ...DEFAULT_REAP_GUARD_OPTIONS, ...(opts ?? {}) };
|
|
37
|
+
this.now = deps.now ?? (() => Date.now());
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns the reason this session must be KEPT (not reaped), or `null` if all
|
|
41
|
+
* stateless guards clear. Cheap-first ordering: in-memory checks before any
|
|
42
|
+
* subprocess fork (process tree / main-process probe) so a guarded session is
|
|
43
|
+
* rejected without paying for a capture.
|
|
44
|
+
*
|
|
45
|
+
* Never throws — a thrown signal source is caught and resolved to KEEP
|
|
46
|
+
* ('guard-error'), because we can never reason about a session we cannot
|
|
47
|
+
* inspect, and the safe answer is always "keep".
|
|
48
|
+
*/
|
|
49
|
+
blockedReason(session) {
|
|
50
|
+
try {
|
|
51
|
+
return this.evaluate(session);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { reason: 'guard-error', confidence: 'low' };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
evaluate(session) {
|
|
58
|
+
const keep = (reason, confidence = 'high') => ({
|
|
59
|
+
reason,
|
|
60
|
+
confidence,
|
|
61
|
+
});
|
|
62
|
+
// ── Cheap, in-memory guards first (no subprocess fork) ──
|
|
63
|
+
// A. Protected set.
|
|
64
|
+
if (this.deps.protectedSessions().includes(session.tmuxSession))
|
|
65
|
+
return keep('protected');
|
|
66
|
+
// M. Spawn grace — a freshly-spawned session that hasn't had time to produce
|
|
67
|
+
// output yet must not be reaped. Skipped when minAgeMs is 0.
|
|
68
|
+
if (this.opts.minAgeMs > 0 && session.startedAt) {
|
|
69
|
+
const ageMs = this.now() - Date.parse(session.startedAt);
|
|
70
|
+
if (!(ageMs >= this.opts.minAgeMs))
|
|
71
|
+
return keep('spawn-grace');
|
|
72
|
+
}
|
|
73
|
+
// G. Recovery in flight — a kill-to-respawn is mid-flight; never race it.
|
|
74
|
+
if (this.deps.isRecoveryActive(session))
|
|
75
|
+
return keep('recovery-in-flight');
|
|
76
|
+
// H. Pending injection (a message is waiting to be delivered to the session).
|
|
77
|
+
if (this.deps.hasPendingInjection(session.tmuxSession))
|
|
78
|
+
return keep('pending-injection');
|
|
79
|
+
// Relay lease (an agent-to-agent relay holds this session).
|
|
80
|
+
if (this.deps.isRelayLeaseActive(session.id))
|
|
81
|
+
return keep('relay-lease');
|
|
82
|
+
const topicId = this.deps.topicBinding(session.tmuxSession);
|
|
83
|
+
// I. Recent user interaction on the bound topic.
|
|
84
|
+
if (topicId != null && this.deps.recentUserMessage(topicId, this.opts.recentUserWindowMs)) {
|
|
85
|
+
return keep('recent-user-message');
|
|
86
|
+
}
|
|
87
|
+
// J. Open commitment on the bound topic.
|
|
88
|
+
if (this.opts.protectOpenCommitments && topicId != null && this.deps.activeCommitmentForTopic(topicId)) {
|
|
89
|
+
return keep('open-commitment');
|
|
90
|
+
}
|
|
91
|
+
// K. Active subagent spawned by this session.
|
|
92
|
+
if (this.deps.activeSubagentCount(session.claudeSessionId) > 0)
|
|
93
|
+
return keep('active-subagent');
|
|
94
|
+
// L. Structural long-work (build / autonomous) on the topic/project.
|
|
95
|
+
if (this.deps.buildOrAutonomousActive(topicId))
|
|
96
|
+
return keep('structural-long-work');
|
|
97
|
+
// ── Activeness (positive-evidence) — may fork a subprocess ──
|
|
98
|
+
// C. Process tree: any non-baseline child ⇒ working.
|
|
99
|
+
if (this.deps.hasActiveProcesses(session.tmuxSession))
|
|
100
|
+
return keep('active-process');
|
|
101
|
+
// C(main). Main-process CPU/IO delta. undefined ⇒ cannot inspect ⇒ KEEP (low).
|
|
102
|
+
if (this.deps.mainProcessActive) {
|
|
103
|
+
const mp = this.deps.mainProcessActive(session.tmuxSession);
|
|
104
|
+
if (mp === undefined)
|
|
105
|
+
return keep('process-uninspectable', 'low');
|
|
106
|
+
if (mp === true)
|
|
107
|
+
return keep('main-process-active');
|
|
108
|
+
}
|
|
109
|
+
// All stateless guards clear. (The reaper layers transcript-growth +
|
|
110
|
+
// positive-idle after this; other killers proceed to terminate.)
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=ReapGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReapGuard.js","sourceRoot":"","sources":["../../src/core/ReapGuard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionLivenessOracle — the single, tri-state answer to "is this tmux session
|
|
3
|
+
* alive?" for every autonomous session killer.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/unified-session-lifecycle-robustness.md §P1.
|
|
6
|
+
*
|
|
7
|
+
* The 2026-05-27 incident: the boot purge probed `tmux has-session` with a 1s
|
|
8
|
+
* timeout inside a bare try/catch, so a *timeout* (tmux busy at boot) threw the
|
|
9
|
+
* same error as "session gone" — and live sessions were mass-purged. The oracle
|
|
10
|
+
* is the structural fix:
|
|
11
|
+
*
|
|
12
|
+
* - `dead` is returned ONLY on a definitive negative: the tmux server is
|
|
13
|
+
* reachable (a `list-sessions` succeeded) AND the session's exact canonical
|
|
14
|
+
* id is absent from that list. Never inferred from a prefix match, an
|
|
15
|
+
* unrecognized error string, or an exit code.
|
|
16
|
+
* - Anything else — timeout, server-unreachable, ENOENT/EPIPE, unknown failure
|
|
17
|
+
* — is `indeterminate`. The hard rule (enforced by every caller) is: NEVER
|
|
18
|
+
* transition a session to killed on `indeterminate`. Indeterminate means
|
|
19
|
+
* "ask again next tick," never "reap now."
|
|
20
|
+
*
|
|
21
|
+
* Performance contract (§P1): liveness for the whole set is resolved from ONE
|
|
22
|
+
* `tmux list-sessions` (no per-session fork), cached for a short TTL so multiple
|
|
23
|
+
* killers in one tick never re-probe. The list call is async (never
|
|
24
|
+
* execFileSync), retried once with backoff, and bounded by a total wall-clock
|
|
25
|
+
* cap so a slow tmux can never block boot — unresolved sessions are left
|
|
26
|
+
* `indeterminate` and finished by the first monitoring tick.
|
|
27
|
+
*/
|
|
28
|
+
export type Liveness = 'alive' | 'dead' | 'indeterminate';
|
|
29
|
+
export interface LivenessResult {
|
|
30
|
+
liveness: Liveness;
|
|
31
|
+
/** Diagnostic reason — for the reap-log / debugging, never user-facing prose. */
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
export interface SessionLivenessOracleConfig {
|
|
35
|
+
/** Per-attempt timeout for the `list-sessions` probe (ms). Must be > a floor. */
|
|
36
|
+
probeTimeoutMs: number;
|
|
37
|
+
/** Retries (in addition to the first attempt) on a transient/unreachable result. */
|
|
38
|
+
probeRetries: number;
|
|
39
|
+
/** Backoff between retries (ms). */
|
|
40
|
+
probeBackoffMs: number;
|
|
41
|
+
/** Total wall-clock cap across all attempts (ms). On cap → indeterminate. */
|
|
42
|
+
bootCapMs: number;
|
|
43
|
+
/** How long a successful list snapshot is trusted before a refresh (ms). */
|
|
44
|
+
cacheTtlMs: number;
|
|
45
|
+
}
|
|
46
|
+
export declare const DEFAULT_LIVENESS_CONFIG: SessionLivenessOracleConfig;
|
|
47
|
+
/** Floors enforced at startup validation (a 0ms timeout would re-create the death spiral). */
|
|
48
|
+
export declare const LIVENESS_FLOORS: {
|
|
49
|
+
readonly minProbeTimeoutMs: 1000;
|
|
50
|
+
readonly minBootCapMs: 2000;
|
|
51
|
+
};
|
|
52
|
+
type ExecFn = (file: string, args: string[], opts: {
|
|
53
|
+
timeout: number;
|
|
54
|
+
}) => Promise<{
|
|
55
|
+
stdout: string;
|
|
56
|
+
stderr: string;
|
|
57
|
+
}>;
|
|
58
|
+
export interface SessionLivenessOracleDeps {
|
|
59
|
+
tmuxPath: string;
|
|
60
|
+
/** promisified execFile — injected for testability. */
|
|
61
|
+
exec: ExecFn;
|
|
62
|
+
now?: () => number;
|
|
63
|
+
/** Optional sink for diagnostics. */
|
|
64
|
+
log?: (msg: string) => void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate a liveness config against the floors. Returns an array of human-readable
|
|
68
|
+
* problems (empty = valid). Called at startup so a nonsensical knob is rejected
|
|
69
|
+
* loudly rather than silently re-creating the boot-purge bug.
|
|
70
|
+
*/
|
|
71
|
+
export declare function validateLivenessConfig(cfg: Partial<SessionLivenessOracleConfig>): string[];
|
|
72
|
+
export declare class SessionLivenessOracle {
|
|
73
|
+
private readonly cfg;
|
|
74
|
+
private readonly deps;
|
|
75
|
+
private readonly now;
|
|
76
|
+
private snapshot;
|
|
77
|
+
private snapshotAt;
|
|
78
|
+
/** Coalesce concurrent refreshes so N killers in one tick share one tmux call. */
|
|
79
|
+
private inflight;
|
|
80
|
+
constructor(deps: SessionLivenessOracleDeps, cfg?: Partial<SessionLivenessOracleConfig>);
|
|
81
|
+
/**
|
|
82
|
+
* Liveness for a single session. Uses the cached list snapshot when fresh,
|
|
83
|
+
* otherwise refreshes (one shared tmux call). Never throws.
|
|
84
|
+
*/
|
|
85
|
+
probe(tmuxSession: string): Promise<LivenessResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Liveness for many sessions resolved from ONE snapshot. This is the boot-purge
|
|
88
|
+
* path: one `tmux list-sessions` for the whole tracked set, no per-session fork.
|
|
89
|
+
*/
|
|
90
|
+
probeAll(tmuxSessions: string[]): Promise<Map<string, LivenessResult>>;
|
|
91
|
+
/** Force the next probe to re-read tmux (used by tests / after a known change). */
|
|
92
|
+
invalidate(): void;
|
|
93
|
+
private classify;
|
|
94
|
+
private ensureSnapshot;
|
|
95
|
+
/**
|
|
96
|
+
* One `tmux list-sessions`, retried up to probeRetries on a transient/unreachable
|
|
97
|
+
* result, bounded by bootCapMs total. Returns an authoritative snapshot only when
|
|
98
|
+
* tmux answered definitively (success, or a clean "no server running").
|
|
99
|
+
*/
|
|
100
|
+
private refresh;
|
|
101
|
+
private listOnce;
|
|
102
|
+
}
|
|
103
|
+
export {};
|
|
104
|
+
//# sourceMappingURL=SessionLivenessOracle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionLivenessOracle.d.ts","sourceRoot":"","sources":["../../src/core/SessionLivenessOracle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,eAAe,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,uBAAuB,EAAE,2BAMrC,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,eAAe;;;CAGlB,CAAC;AAEX,KAAK,MAAM,GAAG,CACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,KACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,qCAAqC;IACrC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAWD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,CAAC,2BAA2B,CAAC,GAAG,MAAM,EAAE,CAkB1F;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,UAAU,CAAK;IACvB,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAsC;gBAE1C,IAAI,EAAE,yBAAyB,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC;IAMvF;;;OAGG;IACG,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAKzD;;;OAGG;IACG,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAO5E,mFAAmF;IACnF,UAAU,IAAI,IAAI;IAKlB,OAAO,CAAC,QAAQ;YAYF,cAAc;IAoB5B;;;;OAIG;YACW,OAAO;YAmBP,QAAQ;CA4BvB"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionLivenessOracle — the single, tri-state answer to "is this tmux session
|
|
3
|
+
* alive?" for every autonomous session killer.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/unified-session-lifecycle-robustness.md §P1.
|
|
6
|
+
*
|
|
7
|
+
* The 2026-05-27 incident: the boot purge probed `tmux has-session` with a 1s
|
|
8
|
+
* timeout inside a bare try/catch, so a *timeout* (tmux busy at boot) threw the
|
|
9
|
+
* same error as "session gone" — and live sessions were mass-purged. The oracle
|
|
10
|
+
* is the structural fix:
|
|
11
|
+
*
|
|
12
|
+
* - `dead` is returned ONLY on a definitive negative: the tmux server is
|
|
13
|
+
* reachable (a `list-sessions` succeeded) AND the session's exact canonical
|
|
14
|
+
* id is absent from that list. Never inferred from a prefix match, an
|
|
15
|
+
* unrecognized error string, or an exit code.
|
|
16
|
+
* - Anything else — timeout, server-unreachable, ENOENT/EPIPE, unknown failure
|
|
17
|
+
* — is `indeterminate`. The hard rule (enforced by every caller) is: NEVER
|
|
18
|
+
* transition a session to killed on `indeterminate`. Indeterminate means
|
|
19
|
+
* "ask again next tick," never "reap now."
|
|
20
|
+
*
|
|
21
|
+
* Performance contract (§P1): liveness for the whole set is resolved from ONE
|
|
22
|
+
* `tmux list-sessions` (no per-session fork), cached for a short TTL so multiple
|
|
23
|
+
* killers in one tick never re-probe. The list call is async (never
|
|
24
|
+
* execFileSync), retried once with backoff, and bounded by a total wall-clock
|
|
25
|
+
* cap so a slow tmux can never block boot — unresolved sessions are left
|
|
26
|
+
* `indeterminate` and finished by the first monitoring tick.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_LIVENESS_CONFIG = {
|
|
29
|
+
probeTimeoutMs: 5000,
|
|
30
|
+
probeRetries: 1,
|
|
31
|
+
probeBackoffMs: 250,
|
|
32
|
+
bootCapMs: 8000,
|
|
33
|
+
cacheTtlMs: 3000,
|
|
34
|
+
};
|
|
35
|
+
/** Floors enforced at startup validation (a 0ms timeout would re-create the death spiral). */
|
|
36
|
+
export const LIVENESS_FLOORS = {
|
|
37
|
+
minProbeTimeoutMs: 1000,
|
|
38
|
+
minBootCapMs: 2000,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Validate a liveness config against the floors. Returns an array of human-readable
|
|
42
|
+
* problems (empty = valid). Called at startup so a nonsensical knob is rejected
|
|
43
|
+
* loudly rather than silently re-creating the boot-purge bug.
|
|
44
|
+
*/
|
|
45
|
+
export function validateLivenessConfig(cfg) {
|
|
46
|
+
const problems = [];
|
|
47
|
+
if (cfg.probeTimeoutMs != null && cfg.probeTimeoutMs < LIVENESS_FLOORS.minProbeTimeoutMs) {
|
|
48
|
+
problems.push(`liveness.probeTimeoutMs (${cfg.probeTimeoutMs}) is below the floor ${LIVENESS_FLOORS.minProbeTimeoutMs}ms — ` +
|
|
49
|
+
`a too-short probe re-creates the 2026-05-27 false-purge (slow tmux read as dead).`);
|
|
50
|
+
}
|
|
51
|
+
if (cfg.probeRetries != null && cfg.probeRetries < 0) {
|
|
52
|
+
problems.push(`liveness.probeRetries (${cfg.probeRetries}) must be >= 0.`);
|
|
53
|
+
}
|
|
54
|
+
if (cfg.bootCapMs != null && cfg.bootCapMs < LIVENESS_FLOORS.minBootCapMs) {
|
|
55
|
+
problems.push(`liveness.bootCapMs (${cfg.bootCapMs}) is below the floor ${LIVENESS_FLOORS.minBootCapMs}ms.`);
|
|
56
|
+
}
|
|
57
|
+
if (cfg.cacheTtlMs != null && cfg.cacheTtlMs < 0) {
|
|
58
|
+
problems.push(`liveness.cacheTtlMs (${cfg.cacheTtlMs}) must be >= 0.`);
|
|
59
|
+
}
|
|
60
|
+
return problems;
|
|
61
|
+
}
|
|
62
|
+
export class SessionLivenessOracle {
|
|
63
|
+
cfg;
|
|
64
|
+
deps;
|
|
65
|
+
now;
|
|
66
|
+
snapshot = null;
|
|
67
|
+
snapshotAt = 0;
|
|
68
|
+
/** Coalesce concurrent refreshes so N killers in one tick share one tmux call. */
|
|
69
|
+
inflight = null;
|
|
70
|
+
constructor(deps, cfg) {
|
|
71
|
+
this.deps = deps;
|
|
72
|
+
this.cfg = { ...DEFAULT_LIVENESS_CONFIG, ...(cfg ?? {}) };
|
|
73
|
+
this.now = deps.now ?? (() => Date.now());
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Liveness for a single session. Uses the cached list snapshot when fresh,
|
|
77
|
+
* otherwise refreshes (one shared tmux call). Never throws.
|
|
78
|
+
*/
|
|
79
|
+
async probe(tmuxSession) {
|
|
80
|
+
const snap = await this.ensureSnapshot();
|
|
81
|
+
return this.classify(tmuxSession, snap);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Liveness for many sessions resolved from ONE snapshot. This is the boot-purge
|
|
85
|
+
* path: one `tmux list-sessions` for the whole tracked set, no per-session fork.
|
|
86
|
+
*/
|
|
87
|
+
async probeAll(tmuxSessions) {
|
|
88
|
+
const snap = await this.ensureSnapshot();
|
|
89
|
+
const out = new Map();
|
|
90
|
+
for (const name of tmuxSessions)
|
|
91
|
+
out.set(name, this.classify(name, snap));
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** Force the next probe to re-read tmux (used by tests / after a known change). */
|
|
95
|
+
invalidate() {
|
|
96
|
+
this.snapshot = null;
|
|
97
|
+
this.snapshotAt = 0;
|
|
98
|
+
}
|
|
99
|
+
classify(tmuxSession, snap) {
|
|
100
|
+
if (!snap.authoritative) {
|
|
101
|
+
// Could not get ground truth — slow/unreachable tmux. NEVER dead.
|
|
102
|
+
return { liveness: 'indeterminate', reason: snap.reason };
|
|
103
|
+
}
|
|
104
|
+
// Authoritative snapshot: exact-id membership is ground truth.
|
|
105
|
+
if (snap.names.has(tmuxSession)) {
|
|
106
|
+
return { liveness: 'alive', reason: 'present-in-list' };
|
|
107
|
+
}
|
|
108
|
+
return { liveness: 'dead', reason: 'absent-from-reachable-server' };
|
|
109
|
+
}
|
|
110
|
+
async ensureSnapshot() {
|
|
111
|
+
const fresh = this.snapshot && this.now() - this.snapshotAt < this.cfg.cacheTtlMs;
|
|
112
|
+
if (fresh)
|
|
113
|
+
return this.snapshot;
|
|
114
|
+
if (this.inflight)
|
|
115
|
+
return this.inflight;
|
|
116
|
+
this.inflight = this.refresh()
|
|
117
|
+
.then((snap) => {
|
|
118
|
+
// Only cache an authoritative snapshot; a transient failure must not be
|
|
119
|
+
// sticky (else one bad tick freezes liveness for the whole TTL).
|
|
120
|
+
if (snap.authoritative) {
|
|
121
|
+
this.snapshot = snap;
|
|
122
|
+
this.snapshotAt = this.now();
|
|
123
|
+
}
|
|
124
|
+
return snap;
|
|
125
|
+
})
|
|
126
|
+
.finally(() => {
|
|
127
|
+
this.inflight = null;
|
|
128
|
+
});
|
|
129
|
+
return this.inflight;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* One `tmux list-sessions`, retried up to probeRetries on a transient/unreachable
|
|
133
|
+
* result, bounded by bootCapMs total. Returns an authoritative snapshot only when
|
|
134
|
+
* tmux answered definitively (success, or a clean "no server running").
|
|
135
|
+
*/
|
|
136
|
+
async refresh() {
|
|
137
|
+
const deadline = this.now() + this.cfg.bootCapMs;
|
|
138
|
+
let lastReason = 'no-attempt';
|
|
139
|
+
for (let attempt = 0; attempt <= this.cfg.probeRetries; attempt++) {
|
|
140
|
+
if (attempt > 0) {
|
|
141
|
+
if (this.now() >= deadline) {
|
|
142
|
+
return { authoritative: false, names: new Set(), reason: `boot-cap-exceeded(${lastReason})` };
|
|
143
|
+
}
|
|
144
|
+
await delay(this.cfg.probeBackoffMs);
|
|
145
|
+
}
|
|
146
|
+
const remaining = Math.max(0, deadline - this.now());
|
|
147
|
+
const timeout = Math.min(this.cfg.probeTimeoutMs, remaining || this.cfg.probeTimeoutMs);
|
|
148
|
+
const res = await this.listOnce(timeout);
|
|
149
|
+
if (res.authoritative)
|
|
150
|
+
return res;
|
|
151
|
+
lastReason = res.reason;
|
|
152
|
+
}
|
|
153
|
+
return { authoritative: false, names: new Set(), reason: lastReason };
|
|
154
|
+
}
|
|
155
|
+
async listOnce(timeout) {
|
|
156
|
+
try {
|
|
157
|
+
const { stdout } = await this.deps.exec(this.deps.tmuxPath, ['list-sessions', '-F', '#{session_name}'], { timeout });
|
|
158
|
+
const names = new Set(stdout
|
|
159
|
+
.split('\n')
|
|
160
|
+
.map((s) => s.trim())
|
|
161
|
+
.filter(Boolean));
|
|
162
|
+
return { authoritative: true, names, reason: 'list-ok' };
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// tmux exits non-zero with "no server running on ..." when there is no
|
|
166
|
+
// server at all. That is an AUTHORITATIVE "no sessions exist" — but we
|
|
167
|
+
// treat it conservatively: only classify it authoritative-empty when the
|
|
168
|
+
// message is the recognized no-server string. Any other error (timeout,
|
|
169
|
+
// ENOENT, EPIPE, unknown) is non-authoritative → indeterminate.
|
|
170
|
+
const msg = errText(err);
|
|
171
|
+
if (/no server running/i.test(msg)) {
|
|
172
|
+
return { authoritative: true, names: new Set(), reason: 'no-server-running' };
|
|
173
|
+
}
|
|
174
|
+
if (isTimeout(err))
|
|
175
|
+
return { authoritative: false, names: new Set(), reason: 'probe-timeout' };
|
|
176
|
+
return { authoritative: false, names: new Set(), reason: `probe-error:${truncate(msg, 80)}` };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function delay(ms) {
|
|
181
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
182
|
+
}
|
|
183
|
+
function errText(err) {
|
|
184
|
+
if (err && typeof err === 'object') {
|
|
185
|
+
const e = err;
|
|
186
|
+
return (e.stderr || e.message || String(err)).toString();
|
|
187
|
+
}
|
|
188
|
+
return String(err);
|
|
189
|
+
}
|
|
190
|
+
function isTimeout(err) {
|
|
191
|
+
if (err && typeof err === 'object') {
|
|
192
|
+
const e = err;
|
|
193
|
+
// execFile timeout kills the child with SIGTERM and sets killed=true.
|
|
194
|
+
return e.killed === true || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT';
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
function truncate(s, n) {
|
|
199
|
+
return s.length > n ? s.slice(0, n) : s;
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=SessionLivenessOracle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionLivenessOracle.js","sourceRoot":"","sources":["../../src/core/SessionLivenessOracle.ts"],"names":[],"mappings":"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"}
|