instar 1.2.41 → 1.2.42
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 +32 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +10 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/SessionManager.d.ts +5 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +7 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +35 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.d.ts +19 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.js +32 -0
- package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -1
- package/dist/monitoring/sentinelWiring.d.ts +82 -0
- package/dist/monitoring/sentinelWiring.d.ts.map +1 -0
- package/dist/monitoring/sentinelWiring.js +154 -0
- package/dist/monitoring/sentinelWiring.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.2.41.md +12 -17
- package/upgrades/1.2.42.md +34 -0
- package/upgrades/side-effects/silently-stopped-trio-wiring.md +185 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentinelWiring — constructs the runtime dependencies for the two
|
|
3
|
+
* "silently-stopped" sentinels (SocketDisconnectSentinel and
|
|
4
|
+
* ActiveWorkSilenceSentinel) from a SessionManager surface + config.
|
|
5
|
+
*
|
|
6
|
+
* Why this module exists: PR #334 merged both sentinels as standalone,
|
|
7
|
+
* unit-tested detectors but never instantiated them in the server. The
|
|
8
|
+
* release notes claimed "wired into server startup" — it wasn't true. This
|
|
9
|
+
* module is the missing wire-up, extracted from server.ts so the dependency
|
|
10
|
+
* construction is itself unit-testable (Testing Integrity Standard: every
|
|
11
|
+
* dependency-injected component needs wiring-integrity tests proving the deps
|
|
12
|
+
* are real functions that delegate to the underlying primitives, not nulls or
|
|
13
|
+
* silent no-ops).
|
|
14
|
+
*
|
|
15
|
+
* Signal-vs-authority: everything here is a detector or a bounded recovery
|
|
16
|
+
* primitive. `looksActivelyWorking` is a structural detector. All user-facing
|
|
17
|
+
* escalation routes through the existing MessagingToneGate via the `/attention`
|
|
18
|
+
* endpoint (category: degradation) — no new blocking authority is introduced.
|
|
19
|
+
*
|
|
20
|
+
* Spec: docs/specs/silently-stopped-trio.md
|
|
21
|
+
*/
|
|
22
|
+
import { getActivitySignal } from './frameworkActivitySignals.js';
|
|
23
|
+
export function makeAttentionPoster(opts) {
|
|
24
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
25
|
+
return async (item) => {
|
|
26
|
+
try {
|
|
27
|
+
const resp = await doFetch(`http://localhost:${opts.port}/attention`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
Authorization: `Bearer ${opts.authToken}`,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ category: 'degradation', priority: 'HIGH', ...item }),
|
|
34
|
+
});
|
|
35
|
+
return resp.status === 201;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Network/route failure — treat as not-delivered. The sentinel's notify
|
|
39
|
+
// wrapper swallows this; the bigger failure (silent stop) is still closed
|
|
40
|
+
// because recovery attempts are independent of the notice landing.
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const SOCKET_CAPTURE_LINES = 40;
|
|
46
|
+
const SILENCE_CAPTURE_LINES = 40;
|
|
47
|
+
export function buildSocketDisconnectDeps(opts) {
|
|
48
|
+
return {
|
|
49
|
+
getRecentOutput: (sessionName) => opts.sessions.captureOutput(sessionName, SOCKET_CAPTURE_LINES) ?? '',
|
|
50
|
+
resumeFn: async (sessionName) => {
|
|
51
|
+
if (!opts.sessions.isSessionAlive(sessionName))
|
|
52
|
+
return false;
|
|
53
|
+
// A socket drop leaves Claude Code waiting at a retry state; a bare Enter
|
|
54
|
+
// re-engages without interrupting in-flight work (unlike Ctrl+C, which
|
|
55
|
+
// would cancel a tool call that may still be running).
|
|
56
|
+
return opts.sessions.sendKey(sessionName, 'Enter');
|
|
57
|
+
},
|
|
58
|
+
notifyFn: async (sessionName, text) => {
|
|
59
|
+
await opts.notify({
|
|
60
|
+
id: `socket-disconnect:${sessionName}`,
|
|
61
|
+
title: `${friendly(sessionName)} connection issue`,
|
|
62
|
+
summary: text,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
listSessionNames: () => opts.sessions.listRunningSessions().map((s) => s.tmuxSession),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Detector: does this captured frame show the framework actively working
|
|
70
|
+
* (spinner, tool call, "esc to interrupt", "(running)") rather than sitting
|
|
71
|
+
* idle at its prompt? Used to filter the silence sentinel's candidate set so a
|
|
72
|
+
* session that is simply waiting for the user is never flagged as "stopped".
|
|
73
|
+
*/
|
|
74
|
+
export function looksActivelyWorking(output, framework) {
|
|
75
|
+
if (!output)
|
|
76
|
+
return false;
|
|
77
|
+
const sig = getActivitySignal(framework);
|
|
78
|
+
return (sig.toolCallOrSpinner.test(output) ||
|
|
79
|
+
sig.escapeToInterrupt.test(output) ||
|
|
80
|
+
sig.runningIndicator.test(output));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Tracks per-session output-change time so the silence sentinel can tell
|
|
84
|
+
* "frozen mid-task" from "still producing output". Only sessions whose most
|
|
85
|
+
* recent frame shows active-work signatures are surfaced as candidates; an
|
|
86
|
+
* idle-at-prompt session is marked `paused` so the sentinel skips it — it is
|
|
87
|
+
* not "actively working then stopped".
|
|
88
|
+
*/
|
|
89
|
+
export class OutputActivityTracker {
|
|
90
|
+
sessions;
|
|
91
|
+
now;
|
|
92
|
+
last = new Map();
|
|
93
|
+
constructor(sessions, now = Date.now) {
|
|
94
|
+
this.sessions = sessions;
|
|
95
|
+
this.now = now;
|
|
96
|
+
}
|
|
97
|
+
snapshot() {
|
|
98
|
+
const t = this.now();
|
|
99
|
+
const running = this.sessions.listRunningSessions();
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const s of running) {
|
|
103
|
+
seen.add(s.tmuxSession);
|
|
104
|
+
const output = this.sessions.captureOutput(s.tmuxSession, SILENCE_CAPTURE_LINES) ?? '';
|
|
105
|
+
const hash = cheapHash(output);
|
|
106
|
+
const prev = this.last.get(s.tmuxSession);
|
|
107
|
+
if (!prev || prev.hash !== hash) {
|
|
108
|
+
this.last.set(s.tmuxSession, { hash, at: t });
|
|
109
|
+
}
|
|
110
|
+
const lastOutputAt = this.last.get(s.tmuxSession).at;
|
|
111
|
+
const active = looksActivelyWorking(output, s.framework);
|
|
112
|
+
out.push({ sessionName: s.tmuxSession, lastOutputAt, paused: !active });
|
|
113
|
+
}
|
|
114
|
+
// Drop tracking for sessions that have ended so the map can't grow without bound.
|
|
115
|
+
for (const key of Array.from(this.last.keys())) {
|
|
116
|
+
if (!seen.has(key))
|
|
117
|
+
this.last.delete(key);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function buildActiveWorkSilenceDeps(opts) {
|
|
123
|
+
return {
|
|
124
|
+
listSessions: () => opts.tracker.snapshot(),
|
|
125
|
+
nudgeFn: async (sessionName) => {
|
|
126
|
+
if (!opts.sessions.isSessionAlive(sessionName))
|
|
127
|
+
return false;
|
|
128
|
+
return opts.sessions.sendKey(sessionName, 'Enter');
|
|
129
|
+
},
|
|
130
|
+
notifyFn: async (sessionName, text) => {
|
|
131
|
+
await opts.notify({
|
|
132
|
+
id: `active-silence:${sessionName}`,
|
|
133
|
+
title: `${friendly(sessionName)} went quiet`,
|
|
134
|
+
summary: text,
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** FNV-1a — enough to detect that captured output changed. Not security-sensitive. */
|
|
140
|
+
function cheapHash(s) {
|
|
141
|
+
let h = 0x811c9dc5;
|
|
142
|
+
for (let i = 0; i < s.length; i++) {
|
|
143
|
+
h ^= s.charCodeAt(i);
|
|
144
|
+
h = Math.imul(h, 0x01000193);
|
|
145
|
+
}
|
|
146
|
+
return (h >>> 0).toString(16);
|
|
147
|
+
}
|
|
148
|
+
function friendly(sessionName) {
|
|
149
|
+
return sessionName
|
|
150
|
+
.replace(/^ai\.instar\./, '')
|
|
151
|
+
.replace(/-server$/, '')
|
|
152
|
+
.replace(/-lifeline$/, '');
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=sentinelWiring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinelWiring.js","sourceRoot":"","sources":["../../src/monitoring/sentinelWiring.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-
|
|
5
|
-
"instarVersion": "1.2.
|
|
4
|
+
"generatedAt": "2026-05-23T03:29:25.734Z",
|
|
5
|
+
"instarVersion": "1.2.42",
|
|
6
6
|
"entryCount": 191,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -1448,7 +1448,7 @@
|
|
|
1448
1448
|
"type": "subsystem",
|
|
1449
1449
|
"domain": "sessions",
|
|
1450
1450
|
"sourcePath": "src/core/SessionManager.ts",
|
|
1451
|
-
"contentHash": "
|
|
1451
|
+
"contentHash": "caf5980117359821e0523c8358a0eea102d4b1c63f592d9dce8462411232cedd",
|
|
1452
1452
|
"since": "2025-01-01"
|
|
1453
1453
|
},
|
|
1454
1454
|
"subsystem:auto-updater": {
|
package/upgrades/1.2.41.md
CHANGED
|
@@ -4,36 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
## What Changed
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Completes the silently-stopped trio shipped in 1.2.41. That release announced two new watchdogs — SocketDisconnectSentinel and ActiveWorkSilenceSentinel — as "wired into server startup." They were not: both shipped as standalone, tested detector classes that the server never actually instantiated, so on every agent they were dead code. This release is the missing wire-up that turns them on.
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
**The gap.** The detectors were correct and unit-tested, but nothing constructed or started them. A grep made it obvious: the RateLimitSentinel they were modeled on is referenced from six files; both new sentinels were referenced from zero outside their own definitions. The release notes claimed a behavior that did not exist.
|
|
10
10
|
|
|
11
|
-
**
|
|
12
|
-
|
|
13
|
-
**C — ActiveWorkSilenceSentinel.** Independent of topic binding. Walks the SessionRegistry every 60s looking for "had output recently, hasn't for N minutes" (default 15 min). On match: one gentle nudge to wake the pane, 30-second verify, escalate via the tone-gated attention path if the session doesn't unstick. Covers the gsd-style sub-spawned worktree session pattern that slipped through SessionWatchdog (requires running child), SessionMonitor (only topic-bound), and PresenceProxy (requires user message).
|
|
14
|
-
|
|
15
|
-
All three sentinel/decision paths route user-facing alerts through the existing MessagingToneGate B12-B14 ruleset — no jargon, always ends in a yes/no CTA.
|
|
11
|
+
**The fix.** A new wiring module builds each sentinel's dependencies from the live session manager and starts them at server boot, behind default-on config switches. SocketDisconnectSentinel now self-drives a 15-second scan over every running session. ActiveWorkSilenceSentinel walks the session registry every 60 seconds and — importantly — only flags sessions whose latest screen shows active work (a spinner, a running tool, an "interrupt" hint), so a session simply waiting for you is never mistaken for a frozen one. All escalations route through the existing tone gate, so you only hear about something that genuinely needs a yes-or-no from you.
|
|
16
12
|
|
|
17
13
|
## What to Tell Your User
|
|
18
14
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
15
|
+
- The two silent-stop watchdogs announced last release are now actually running. Before this, they were installed but switched off, so a dropped connection or a frozen background session could still go unnoticed.
|
|
16
|
+
- You will not get noise from sessions that are just idle and waiting for you — only from sessions that were genuinely working and then froze, or that lost their connection.
|
|
17
|
+
- Both watchdogs can be turned off per-agent in config if you ever need to, and they are on by default.
|
|
22
18
|
|
|
23
19
|
## Summary of New Capabilities
|
|
24
20
|
|
|
25
21
|
| Capability | How to Use |
|
|
26
22
|
|-----------|-----------|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
| ActiveWorkSilenceSentinel | Wired into server startup; threshold configurable via config.monitoring.activeWorkSilenceSentinel.silenceThresholdMs |
|
|
23
|
+
| SocketDisconnectSentinel (live) | On by default; disable via config.monitoring.socketDisconnectSentinel.enabled=false |
|
|
24
|
+
| ActiveWorkSilenceSentinel (live) | On by default; tune config.monitoring.activeWorkSilenceSentinel.silenceThresholdMs; disable via .enabled=false |
|
|
30
25
|
|
|
31
26
|
## Evidence
|
|
32
27
|
|
|
33
|
-
- Spec: `docs/specs/silently-stopped-trio.md` (with ELI16 companion). Side-effects: `upgrades/side-effects/silently-stopped-trio.md`.
|
|
34
|
-
-
|
|
35
|
-
-
|
|
28
|
+
- Spec: `docs/specs/silently-stopped-trio.md` (with ELI16 companion). Side-effects: `upgrades/side-effects/silently-stopped-trio-wiring.md`.
|
|
29
|
+
- The gap, reproduced: a grep for either sentinel name across the source returned only its own definition and test files — no construction, no start call.
|
|
30
|
+
- Tests: 30 new tests — wiring-integrity and active-vs-idle semantics (24), self-driving scan loop (3), and an end-to-end integration test that drives both sentinels through the tone-gated attention path and asserts an idle session is never escalated (3).
|
|
36
31
|
|
|
37
32
|
## Rollback
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
Per-sentinel config kill switch (no release needed), or revert the wiring module plus the server-startup block and the SocketDisconnectSentinel start/stop additions. No persistent state to clean up.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
Completes the silently-stopped trio shipped in 1.2.41. That release announced two new watchdogs — SocketDisconnectSentinel and ActiveWorkSilenceSentinel — as "wired into server startup." They were not: both shipped as standalone, tested detector classes that the server never actually instantiated, so on every agent they were dead code. This release is the missing wire-up that turns them on.
|
|
8
|
+
|
|
9
|
+
**The gap.** The detectors were correct and unit-tested, but nothing constructed or started them. A grep made it obvious: the RateLimitSentinel they were modeled on is referenced from six files; both new sentinels were referenced from zero outside their own definitions. The release notes claimed a behavior that did not exist.
|
|
10
|
+
|
|
11
|
+
**The fix.** A new wiring module builds each sentinel's dependencies from the live session manager and starts them at server boot, behind default-on config switches. SocketDisconnectSentinel now self-drives a 15-second scan over every running session. ActiveWorkSilenceSentinel walks the session registry every 60 seconds and — importantly — only flags sessions whose latest screen shows active work (a spinner, a running tool, an "interrupt" hint), so a session simply waiting for you is never mistaken for a frozen one. All escalations route through the existing tone gate, so you only hear about something that genuinely needs a yes-or-no from you.
|
|
12
|
+
|
|
13
|
+
## What to Tell Your User
|
|
14
|
+
|
|
15
|
+
- The two silent-stop watchdogs announced last release are now actually running. Before this, they were installed but switched off, so a dropped connection or a frozen background session could still go unnoticed.
|
|
16
|
+
- You will not get noise from sessions that are just idle and waiting for you — only from sessions that were genuinely working and then froze, or that lost their connection.
|
|
17
|
+
- Both watchdogs can be turned off per-agent in config if you ever need to, and they are on by default.
|
|
18
|
+
|
|
19
|
+
## Summary of New Capabilities
|
|
20
|
+
|
|
21
|
+
| Capability | How to Use |
|
|
22
|
+
|-----------|-----------|
|
|
23
|
+
| SocketDisconnectSentinel (live) | On by default; disable via config.monitoring.socketDisconnectSentinel.enabled=false |
|
|
24
|
+
| ActiveWorkSilenceSentinel (live) | On by default; tune config.monitoring.activeWorkSilenceSentinel.silenceThresholdMs; disable via .enabled=false |
|
|
25
|
+
|
|
26
|
+
## Evidence
|
|
27
|
+
|
|
28
|
+
- Spec: `docs/specs/silently-stopped-trio.md` (with ELI16 companion). Side-effects: `upgrades/side-effects/silently-stopped-trio-wiring.md`.
|
|
29
|
+
- The gap, reproduced: a grep for either sentinel name across the source returned only its own definition and test files — no construction, no start call.
|
|
30
|
+
- Tests: 30 new tests — wiring-integrity and active-vs-idle semantics (24), self-driving scan loop (3), and an end-to-end integration test that drives both sentinels through the tone-gated attention path and asserts an idle session is never escalated (3).
|
|
31
|
+
|
|
32
|
+
## Rollback
|
|
33
|
+
|
|
34
|
+
Per-sentinel config kill switch (no release needed), or revert the wiring module plus the server-startup block and the SocketDisconnectSentinel start/stop additions. No persistent state to clean up.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Side-Effects Review — Silently-stopped trio: server wiring
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `silently-stopped-trio-wiring`
|
|
4
|
+
**Date:** 2026-05-23
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** [pending — high-risk: sentinels + server startup]
|
|
7
|
+
**Spec:** [docs/specs/silently-stopped-trio.md](../../docs/specs/silently-stopped-trio.md)
|
|
8
|
+
**ELI16:** [docs/specs/silently-stopped-trio.eli16.md](../../docs/specs/silently-stopped-trio.eli16.md)
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
PR #334 merged `SocketDisconnectSentinel` and `ActiveWorkSilenceSentinel` as
|
|
13
|
+
standalone, unit-tested detectors — but never instantiated them in the running
|
|
14
|
+
server. A grep proved it: `RateLimitSentinel` (the pattern they claimed to
|
|
15
|
+
mirror) is referenced from six files; both new sentinels were referenced from
|
|
16
|
+
zero outside their own definition + test files. The merged release notes and
|
|
17
|
+
side-effects artifact both stated "wired into server startup". That was false:
|
|
18
|
+
on every agent, after the auto-update, the sentinels would still never run.
|
|
19
|
+
|
|
20
|
+
This change is the missing wire-up. No detector logic changes; the detectors
|
|
21
|
+
were correct. What was missing was construction + lifecycle.
|
|
22
|
+
|
|
23
|
+
- **`src/monitoring/sentinelWiring.ts`** (new) — testable dependency builders:
|
|
24
|
+
`makeAttentionPoster` (posts escalations to the tone-gated `/attention`
|
|
25
|
+
route, returns true only on 201), `buildSocketDisconnectDeps`,
|
|
26
|
+
`buildActiveWorkSilenceDeps`, `OutputActivityTracker` (per-session
|
|
27
|
+
output-change timing), and `looksActivelyWorking` (active-vs-idle detector
|
|
28
|
+
built on the existing `frameworkActivitySignals`).
|
|
29
|
+
- **`src/monitoring/SocketDisconnectSentinel.ts`** — added an optional
|
|
30
|
+
`listSessionNames` dep plus `start()` / `stop()` / `tick()` so the sentinel
|
|
31
|
+
self-drives a 15s scan loop (mirrors `ActiveWorkSilenceSentinel`'s shape).
|
|
32
|
+
Existing event-driven API (`report` / `scanSession`) is unchanged.
|
|
33
|
+
- **`src/commands/server.ts`** — instantiates both sentinels from a
|
|
34
|
+
SessionManager surface + config, and starts them. Guarded by config
|
|
35
|
+
kill-switches, default-on.
|
|
36
|
+
- **`src/core/types.ts` + `src/config/ConfigDefaults.ts`** — config types +
|
|
37
|
+
default-on entries. Migration parity is automatic: `migrateConfig` applies
|
|
38
|
+
all `ConfigDefaults` via `applyDefaults`, so existing agents receive the
|
|
39
|
+
config keys on update.
|
|
40
|
+
|
|
41
|
+
## Decision-point inventory
|
|
42
|
+
|
|
43
|
+
- `looksActivelyWorking` — **add** — structural detector (regex on captured
|
|
44
|
+
frame via `frameworkActivitySignals`). No judgment, no authority.
|
|
45
|
+
- `OutputActivityTracker.snapshot` — **add** — deterministic change-detection
|
|
46
|
+
(FNV-1a hash compare) + active/idle classification. Marks idle-at-prompt
|
|
47
|
+
sessions `paused` so the silence sentinel skips them.
|
|
48
|
+
- `makeAttentionPoster` — **pass-through** — delegates the actual block/allow
|
|
49
|
+
decision to the existing `/attention` tone gate (B12-B14). Returns true only
|
|
50
|
+
on 201; a 422 block is the gate doing its job, not an error.
|
|
51
|
+
- `SocketDisconnectSentinel.start/tick` — **add** — bounded scan loop.
|
|
52
|
+
|
|
53
|
+
## 1. Over-block
|
|
54
|
+
|
|
55
|
+
- **Active-vs-idle false negative → no escalation.** If a frozen session's last
|
|
56
|
+
frame does NOT contain an active-work signature (spinner / "esc to interrupt"
|
|
57
|
+
/ tool-call / "(running)"), `OutputActivityTracker` marks it `paused` and the
|
|
58
|
+
silence sentinel skips it. This is deliberate: the alternative (flag every
|
|
59
|
+
static frame) would fire on every idle-at-prompt session waiting for the
|
|
60
|
+
user. Cost of this choice: a session that froze at a non-active-looking frame
|
|
61
|
+
is missed by sentinel C — but SocketDisconnectSentinel, SessionWatchdog, and
|
|
62
|
+
SessionMonitor still cover their shapes. The integration test asserts an
|
|
63
|
+
idle-prompt session is NOT escalated.
|
|
64
|
+
- **Socket regex over-broad** (unchanged from PR #334) — false-positive cost is
|
|
65
|
+
one bare Enter + a first-notice that the tone gate (B14) suppresses anyway.
|
|
66
|
+
|
|
67
|
+
## 2. Under-block
|
|
68
|
+
|
|
69
|
+
- **`looksActivelyWorking` pattern coverage.** The frame is classified using
|
|
70
|
+
the session's resolved framework (via `SessionManager.frameworkForSession`,
|
|
71
|
+
added in this PR — second-pass review caught that the prior draft read a
|
|
72
|
+
non-existent `sess.framework` field and silently fell back to claude-code for
|
|
73
|
+
every session, mis-classifying Codex). Coverage is now correct for both
|
|
74
|
+
claude-code and codex-cli. A framework outside that map (none exist today)
|
|
75
|
+
still defaults to claude-code patterns; the same `frameworkActivitySignals`
|
|
76
|
+
module backs StallTriageNurse, so adding a framework improves all consumers
|
|
77
|
+
centrally.
|
|
78
|
+
- **First-notice delivery.** The "lost its connection, trying to recover"
|
|
79
|
+
notice has no CTA, so the tone gate (B14) blocks it (422 → poster returns
|
|
80
|
+
false). This is intended — only the escalation (which carries a yes/no CTA)
|
|
81
|
+
reaches the user. The recovery loop runs regardless of whether any notice
|
|
82
|
+
lands.
|
|
83
|
+
|
|
84
|
+
## 3. Level-of-abstraction fit
|
|
85
|
+
|
|
86
|
+
- Wiring lives in `server.ts` alongside the RateLimitSentinel/CompactionSentinel
|
|
87
|
+
wire-up — the same layer, the same idiom (`await import` + construct + start).
|
|
88
|
+
- Dependency construction is extracted to `sentinelWiring.ts` specifically so it
|
|
89
|
+
is unit-testable in isolation (the bug this PR fixes was an *untested wiring
|
|
90
|
+
gap*; the fix ships the wiring-integrity tests the standard requires).
|
|
91
|
+
- `OutputActivityTracker` is the right owner of per-session output timing for
|
|
92
|
+
the topic-independent case — SessionMonitor's equivalent snapshots are keyed
|
|
93
|
+
by topicId and therefore blind to non-topic-bound sessions (the exact gap).
|
|
94
|
+
|
|
95
|
+
## 4. Signal vs authority compliance
|
|
96
|
+
|
|
97
|
+
**Reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
98
|
+
|
|
99
|
+
- [x] **No new blocking authority.** Every user-facing escalation is a signal
|
|
100
|
+
posted to the existing `/attention` tone gate, which holds the authority.
|
|
101
|
+
- `looksActivelyWorking` and `OutputActivityTracker` are detectors.
|
|
102
|
+
- `makeAttentionPoster` returns the gate's verdict (201 → delivered, 422 →
|
|
103
|
+
blocked); it never overrides the gate.
|
|
104
|
+
- The recovery primitives (bare Enter via `sendKey`) are bounded and
|
|
105
|
+
alive-gated.
|
|
106
|
+
|
|
107
|
+
## 5. Interactions
|
|
108
|
+
|
|
109
|
+
- **Double-fire with the live socket sentinel + silence sentinel.** A socket
|
|
110
|
+
drop (15s scan) fires far sooner than the 15-min silence threshold, and each
|
|
111
|
+
uses a distinct `/attention` id (`socket-disconnect:` vs `active-silence:`),
|
|
112
|
+
so they don't collide. Idempotency-by-id on `/attention` upserts rather than
|
|
113
|
+
duplicating.
|
|
114
|
+
- **Recovery nudge vs other injectors.** The bare Enter is alive-gated and does
|
|
115
|
+
not interrupt work (unlike Ctrl+C). It does not register with the
|
|
116
|
+
zombie-kill veto used by RateLimit/Compaction sentinels — these sentinels do
|
|
117
|
+
not kill sessions, so the veto is not relevant.
|
|
118
|
+
- **Tracker memory.** `OutputActivityTracker` prunes ended sessions every
|
|
119
|
+
snapshot, so the map cannot grow unbounded.
|
|
120
|
+
|
|
121
|
+
## 6. External surfaces
|
|
122
|
+
|
|
123
|
+
- **Telegram:** one new attention-item class per incident, tone-gated. Quiet by
|
|
124
|
+
default (first-notice suppressed; only CTA escalations surface).
|
|
125
|
+
- **Config:** two new `monitoring.*Sentinel` keys, default-on, applied to
|
|
126
|
+
existing agents via the standard `applyDefaults` migration path.
|
|
127
|
+
- **Persistent state:** none — both sentinels keep in-memory state only.
|
|
128
|
+
- **Timing:** socket scan 15s, silence walk 60s; each captures ~40 lines of
|
|
129
|
+
tmux output per running session per tick. Same order of cost as the existing
|
|
130
|
+
SessionMonitor poll.
|
|
131
|
+
|
|
132
|
+
## 7. Rollback cost
|
|
133
|
+
|
|
134
|
+
- Per-sentinel config kill-switch:
|
|
135
|
+
`monitoring.socketDisconnectSentinel.enabled=false` /
|
|
136
|
+
`monitoring.activeWorkSilenceSentinel.enabled=false` — no release needed.
|
|
137
|
+
- Code rollback: revert the `server.ts` wire-up block + `sentinelWiring.ts` +
|
|
138
|
+
the `SocketDisconnectSentinel` start/stop additions. No persistent state to
|
|
139
|
+
clean up.
|
|
140
|
+
|
|
141
|
+
## Conclusion
|
|
142
|
+
|
|
143
|
+
Makes the silently-stopped trio actually run, which the prior PR claimed but did
|
|
144
|
+
not do. Adds the wiring-integrity + semantic tests the Testing Integrity
|
|
145
|
+
Standard requires for dependency-injected components, plus an integration test
|
|
146
|
+
that drives both sentinels end-to-end through the tone-gated `/attention` path.
|
|
147
|
+
No new blocking authority; default-on with per-sentinel kill switches.
|
|
148
|
+
|
|
149
|
+
Clear to ship pending second-pass review.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Second-pass review (if required)
|
|
154
|
+
|
|
155
|
+
**Reviewer:** independent review subagent, 2026-05-23
|
|
156
|
+
|
|
157
|
+
**Verdict: Concur with the review**, with one substantive correctness finding
|
|
158
|
+
that has been fixed in this PR.
|
|
159
|
+
|
|
160
|
+
- Confirmed both sentinels are constructed AND `.start()`-ed, guarded by
|
|
161
|
+
default-on kill switches matching the RateLimitSentinel idiom. Not dead code.
|
|
162
|
+
- Signal-vs-authority holds: escalations route through `/attention`; the
|
|
163
|
+
detectors hold no blocking authority. The `=== 201` success check is correct.
|
|
164
|
+
- Intervals are unref'd; `OutputActivityTracker` prunes ended sessions (no Map
|
|
165
|
+
leak). Bare-Enter recovery is alive-gated and the right choice over Ctrl+C.
|
|
166
|
+
- **Finding (FIXED): framework was never populated.** The draft read a
|
|
167
|
+
non-existent `sess.framework` field, so `looksActivelyWorking` always used
|
|
168
|
+
claude-code patterns — a Codex session's spinner would be classed idle and
|
|
169
|
+
skipped. Resolution: added `SessionManager.frameworkForSession` (public
|
|
170
|
+
accessor over the existing per-session framework cache) and plumbed it into
|
|
171
|
+
the session surface; removed the `as`-cast. Codex coverage now works.
|
|
172
|
+
- Over-fire risk assessed low and acceptable: a slow-but-alive tool call
|
|
173
|
+
re-renders its spinner/token count → hash changes → `lastOutputAt` resets, so
|
|
174
|
+
it won't trip; idle-at-prompt is `paused`; a frozen non-active frame is
|
|
175
|
+
skipped by design and covered by the other watchdogs.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Evidence pointers
|
|
180
|
+
|
|
181
|
+
- Tests: `tests/unit/monitoring/sentinelWiring.test.ts` (24),
|
|
182
|
+
`tests/unit/monitoring/SocketDisconnectSentinel.test.ts` (+3 loop tests),
|
|
183
|
+
`tests/integration/silently-stopped-trio-wiring.test.ts` (3).
|
|
184
|
+
- The gap: `grep -rln SocketDisconnectSentinel src | grep -v 'SocketDisconnectSentinel.ts'`
|
|
185
|
+
returned nothing before this change.
|