greprag 5.26.0 → 5.28.0
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/corpus/index.js +6 -2
- package/dist/commands/corpus/index.js.map +1 -1
- package/dist/commands/corpus/upload.js +39 -1
- package/dist/commands/corpus/upload.js.map +1 -1
- package/dist/commands/inbox-watch-supervisor.js +113 -0
- package/dist/commands/inbox-watch-supervisor.js.map +1 -1
- package/dist/commands/inbox-watch.d.ts +5 -0
- package/dist/commands/inbox-watch.js +81 -5
- package/dist/commands/inbox-watch.js.map +1 -1
- package/dist/commands/watcher-registry.d.ts +59 -0
- package/dist/commands/watcher-registry.js +287 -0
- package/dist/commands/watcher-registry.js.map +1 -0
- package/dist/hook.js +48 -33
- package/dist/hook.js.map +1 -1
- package/dist/index.js +21 -5
- package/dist/index.js.map +1 -1
- package/dist/session-id.d.ts +12 -2
- package/dist/session-id.js +13 -4
- package/dist/session-id.js.map +1 -1
- package/package.json +1 -1
- package/skill/greprag/SKILL.md +1 -1
- package/skill/greprag/docs/inbox-watch.md +6 -4
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Local watcher process registry + severed-ancestry orphan reaper.
|
|
3
|
+
*
|
|
4
|
+
* The architectural correction the whole monitor-resilience saga circled but
|
|
5
|
+
* never landed (see adr/monitor-resilience.md 2026-05-28 "severed-ancestry =
|
|
6
|
+
* the disarm signal" + the 2026-06-04 entry): a watcher's liveness lives at the
|
|
7
|
+
* LOCAL process layer, not in a cloud registry. The consumer of a watcher is
|
|
8
|
+
* the Claude Code Monitor task on THIS machine; the server can't see whether
|
|
9
|
+
* anyone is still consuming the stream, so a server lease counts an orphan
|
|
10
|
+
* (consumer dead, process still respawning) as "armed". Two local signals fix
|
|
11
|
+
* that:
|
|
12
|
+
*
|
|
13
|
+
* 1. **pidfile** (`~/.greprag/watchers/<short>.json`) — written by the
|
|
14
|
+
* supervisor on start, removed on terminal exit. `isLocallyArmed` reads it
|
|
15
|
+
* to gate re-arming with zero cloud dependency and zero ghost-lease lag.
|
|
16
|
+
* Paired with EPIPE-terminal in the watcher (a watcher whose consumer pipe
|
|
17
|
+
* breaks exits and removes its own pidfile), a live pidfile means a live,
|
|
18
|
+
* *consumed* watcher.
|
|
19
|
+
*
|
|
20
|
+
* 2. **severed-ancestry reap** — `reapOrphanWatchers` snapshots the process
|
|
21
|
+
* table and kills any `inbox watch` process whose ancestry no longer
|
|
22
|
+
* reaches a live `claude.exe`. Backstop for the case EPIPE can't catch (a
|
|
23
|
+
* consumer hard-killed without closing the pipe). SAFE BY CONSTRUCTION: the
|
|
24
|
+
* kill set is intersected with "cmdline contains `inbox watch`", so it can
|
|
25
|
+
* only ever terminate greprag watchers — never claude.exe, an editor, or an
|
|
26
|
+
* unrelated node server. A bug can at worst kill a watcher that then
|
|
27
|
+
* re-arms; it can never touch the operator's work.
|
|
28
|
+
*
|
|
29
|
+
* adr: adr/monitor-resilience.md */
|
|
30
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
33
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
34
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
35
|
+
}
|
|
36
|
+
Object.defineProperty(o, k2, desc);
|
|
37
|
+
}) : (function(o, m, k, k2) {
|
|
38
|
+
if (k2 === undefined) k2 = k;
|
|
39
|
+
o[k2] = m[k];
|
|
40
|
+
}));
|
|
41
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
42
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
43
|
+
}) : function(o, v) {
|
|
44
|
+
o["default"] = v;
|
|
45
|
+
});
|
|
46
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
47
|
+
var ownKeys = function(o) {
|
|
48
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
49
|
+
var ar = [];
|
|
50
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
51
|
+
return ar;
|
|
52
|
+
};
|
|
53
|
+
return ownKeys(o);
|
|
54
|
+
};
|
|
55
|
+
return function (mod) {
|
|
56
|
+
if (mod && mod.__esModule) return mod;
|
|
57
|
+
var result = {};
|
|
58
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
59
|
+
__setModuleDefault(result, mod);
|
|
60
|
+
return result;
|
|
61
|
+
};
|
|
62
|
+
})();
|
|
63
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
|
+
exports.writeWatcherPidfile = writeWatcherPidfile;
|
|
65
|
+
exports.removeWatcherPidfile = removeWatcherPidfile;
|
|
66
|
+
exports.isLocallyArmed = isLocallyArmed;
|
|
67
|
+
exports.reapOrphanWatchers = reapOrphanWatchers;
|
|
68
|
+
const fs = __importStar(require("fs"));
|
|
69
|
+
const path = __importStar(require("path"));
|
|
70
|
+
const child_process_1 = require("child_process");
|
|
71
|
+
const WATCHER_DIRNAME = 'watchers';
|
|
72
|
+
// Matches a greprag watcher's command line in every launch shape: the npm shim
|
|
73
|
+
// and the old bash while-loop wrapper (`greprag inbox watch …`), and the
|
|
74
|
+
// supervisor's CreateProcess re-invocation (`node …\dist\index.js inbox watch
|
|
75
|
+
// …`). Anchored on the INVOKED BINARY (`greprag` or `index.js`) immediately
|
|
76
|
+
// followed by the `inbox watch` args — NOT a bare `inbox watch` substring, which
|
|
77
|
+
// would also match an unrelated shell that merely mentions the phrase (a grep, a
|
|
78
|
+
// diagnostic, this very reaper). This is the SAFETY GUARD: only a process whose
|
|
79
|
+
// command line is an actual watcher invocation is ever a kill candidate, so the
|
|
80
|
+
// reaper cannot touch claude.exe, an editor, or an incidental shell.
|
|
81
|
+
const WATCHER_CMD_RE = /(?:index\.js|greprag)["'\s]+inbox\s+watch\b/i;
|
|
82
|
+
const CLAUDE_PROC_RE = /^claude(\.exe)?$/i;
|
|
83
|
+
function grepragHome() {
|
|
84
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
85
|
+
return home ? path.join(home, '.greprag') : null;
|
|
86
|
+
}
|
|
87
|
+
function watchersDir() {
|
|
88
|
+
const h = grepragHome();
|
|
89
|
+
return h ? path.join(h, WATCHER_DIRNAME) : null;
|
|
90
|
+
}
|
|
91
|
+
/** Write the watcher pidfile (supervisor PID), keyed by the 8-hex session short.
|
|
92
|
+
* Best-effort: a failed write only means `isLocallyArmed` falls back to
|
|
93
|
+
* re-arming, never a crash. */
|
|
94
|
+
function writeWatcherPidfile(short, pid) {
|
|
95
|
+
try {
|
|
96
|
+
const dir = watchersDir();
|
|
97
|
+
if (!dir)
|
|
98
|
+
return;
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
const rec = { short, pid, startedAt: Date.now() };
|
|
101
|
+
fs.writeFileSync(path.join(dir, `${short}.json`), JSON.stringify(rec));
|
|
102
|
+
}
|
|
103
|
+
catch { /* best-effort */ }
|
|
104
|
+
}
|
|
105
|
+
function removeWatcherPidfile(short) {
|
|
106
|
+
try {
|
|
107
|
+
const dir = watchersDir();
|
|
108
|
+
if (!dir)
|
|
109
|
+
return;
|
|
110
|
+
fs.rmSync(path.join(dir, `${short}.json`), { force: true });
|
|
111
|
+
}
|
|
112
|
+
catch { /* best-effort */ }
|
|
113
|
+
}
|
|
114
|
+
function readWatcherPidfile(short) {
|
|
115
|
+
try {
|
|
116
|
+
const dir = watchersDir();
|
|
117
|
+
if (!dir)
|
|
118
|
+
return null;
|
|
119
|
+
const raw = fs.readFileSync(path.join(dir, `${short}.json`), 'utf-8');
|
|
120
|
+
const rec = JSON.parse(raw);
|
|
121
|
+
if (rec && typeof rec.pid === 'number' && rec.short === short)
|
|
122
|
+
return rec;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** True iff `pid` is a live process. `process.kill(pid, 0)` sends no signal —
|
|
130
|
+
* it only probes existence. EPERM means the process exists but is owned by
|
|
131
|
+
* another user (still alive); ESRCH means gone. */
|
|
132
|
+
function pidAlive(pid) {
|
|
133
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
134
|
+
return false;
|
|
135
|
+
try {
|
|
136
|
+
process.kill(pid, 0);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
return e?.code === 'EPERM';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Local-first arm check: is there a live watcher PROCESS for this session on
|
|
144
|
+
* THIS machine? This is ground truth for "armed" — the consumer (the Monitor
|
|
145
|
+
* task) is local, so a live local pidfile means a live, consumed watcher.
|
|
146
|
+
* Replaces the server `isSessionArmed` check, which counts orphans (consumer
|
|
147
|
+
* dead, socket still open) as armed and so both (a) suppresses re-arm when the
|
|
148
|
+
* real watcher is gone and (b) lets re-arm stack new watchers on undead
|
|
149
|
+
* orphans. A dead-PID pidfile is swept here so the next turn re-arms. */
|
|
150
|
+
function isLocallyArmed(short) {
|
|
151
|
+
const rec = readWatcherPidfile(short);
|
|
152
|
+
if (!rec)
|
|
153
|
+
return false;
|
|
154
|
+
if (!pidAlive(rec.pid)) {
|
|
155
|
+
removeWatcherPidfile(short);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
/** Snapshot (pid, ppid, name, cmdline) for every process. Windows via one CIM
|
|
161
|
+
* call (a single PowerShell invocation — avoids the per-PID `wmic` calls that
|
|
162
|
+
* hung historically, see ADR 2026-05-26); POSIX via `ps`. Best-effort: any
|
|
163
|
+
* failure returns [] and the reaper becomes a no-op. */
|
|
164
|
+
function snapshotProcs() {
|
|
165
|
+
try {
|
|
166
|
+
if (process.platform === 'win32') {
|
|
167
|
+
const script = 'Get-CimInstance Win32_Process | ' +
|
|
168
|
+
'Select-Object ProcessId,ParentProcessId,Name,CommandLine | ' +
|
|
169
|
+
'ConvertTo-Json -Compress';
|
|
170
|
+
const out = (0, child_process_1.execFileSync)('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { timeout: 12_000, maxBuffer: 64 * 1024 * 1024, windowsHide: true }).toString();
|
|
171
|
+
const parsed = JSON.parse(out);
|
|
172
|
+
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
173
|
+
return arr
|
|
174
|
+
.map((p) => ({
|
|
175
|
+
pid: Number(p.ProcessId),
|
|
176
|
+
ppid: Number(p.ParentProcessId),
|
|
177
|
+
name: String(p.Name || ''),
|
|
178
|
+
cmd: String(p.CommandLine || ''),
|
|
179
|
+
}))
|
|
180
|
+
.filter(p => Number.isFinite(p.pid) && p.pid > 0);
|
|
181
|
+
}
|
|
182
|
+
// POSIX fallback.
|
|
183
|
+
const out = (0, child_process_1.execFileSync)('ps', ['-eo', 'pid=,ppid=,comm=,args='], {
|
|
184
|
+
timeout: 12_000, maxBuffer: 64 * 1024 * 1024,
|
|
185
|
+
}).toString();
|
|
186
|
+
const rows = [];
|
|
187
|
+
for (const line of out.split('\n')) {
|
|
188
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
189
|
+
if (!m)
|
|
190
|
+
continue;
|
|
191
|
+
rows.push({ pid: Number(m[1]), ppid: Number(m[2]), name: m[3], cmd: m[4] });
|
|
192
|
+
}
|
|
193
|
+
return rows;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Walk `start`'s parent chain in the snapshot; true iff a live `claude.exe`
|
|
200
|
+
* appears as an ancestor. A healthy watcher is launched (via the Monitor task)
|
|
201
|
+
* under the live claude.exe, so its chain reaches it; an orphan's owning
|
|
202
|
+
* claude.exe died on reload, so its chain is severed (a parent PID absent from
|
|
203
|
+
* the live snapshot) before any claude.exe. Depth- and cycle-guarded. */
|
|
204
|
+
function hasLiveClaudeAncestor(start, byPid) {
|
|
205
|
+
let cur = start;
|
|
206
|
+
const seen = new Set();
|
|
207
|
+
for (let depth = 0; cur && depth < 32; depth++) {
|
|
208
|
+
if (seen.has(cur.pid))
|
|
209
|
+
break; // cycle guard
|
|
210
|
+
seen.add(cur.pid);
|
|
211
|
+
if (CLAUDE_PROC_RE.test(cur.name))
|
|
212
|
+
return true; // start/ancestor IS claude
|
|
213
|
+
const parent = byPid.get(cur.ppid);
|
|
214
|
+
if (!parent)
|
|
215
|
+
return false; // severed — parent not in the live table
|
|
216
|
+
cur = parent;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
/** Kill every `inbox watch` process whose ancestry no longer reaches a live
|
|
221
|
+
* claude.exe (severed = orphan), plus sweep dead-PID pidfiles. Returns a
|
|
222
|
+
* summary. Best-effort and bounded: a snapshot failure yields a zero result;
|
|
223
|
+
* the kill set is guaranteed (by `WATCHER_CMD_RE`) to contain only greprag
|
|
224
|
+
* watchers. Intended to run at SessionStart, before this session arms — at that
|
|
225
|
+
* moment every live watcher belongs to a PRIOR incarnation, so reaping is the
|
|
226
|
+
* common path, not the exception. */
|
|
227
|
+
function reapOrphanWatchers() {
|
|
228
|
+
sweepDeadPidfiles();
|
|
229
|
+
const procs = snapshotProcs();
|
|
230
|
+
if (procs.length === 0)
|
|
231
|
+
return { scanned: 0, orphans: 0, killed: [] };
|
|
232
|
+
const byPid = new Map();
|
|
233
|
+
for (const p of procs)
|
|
234
|
+
byPid.set(p.pid, p);
|
|
235
|
+
// Candidate set — the SAFETY GUARD. Only these are ever eligible to be killed.
|
|
236
|
+
const watchers = procs.filter(p => WATCHER_CMD_RE.test(p.cmd));
|
|
237
|
+
const severed = watchers.filter(p => !hasLiveClaudeAncestor(p, byPid));
|
|
238
|
+
const killed = [];
|
|
239
|
+
for (const p of severed) {
|
|
240
|
+
if (killProcessTree(p.pid))
|
|
241
|
+
killed.push(p.pid);
|
|
242
|
+
}
|
|
243
|
+
return { scanned: watchers.length, orphans: severed.length, killed };
|
|
244
|
+
}
|
|
245
|
+
/** Force-kill a process and its descendants. `taskkill /T` reaps the tree in one
|
|
246
|
+
* shot so a supervisor can't respawn its child in the race window. */
|
|
247
|
+
function killProcessTree(pid) {
|
|
248
|
+
try {
|
|
249
|
+
if (process.platform === 'win32') {
|
|
250
|
+
(0, child_process_1.execFileSync)('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { timeout: 5_000, stdio: 'ignore', windowsHide: true });
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
try {
|
|
254
|
+
process.kill(pid, 'SIGKILL');
|
|
255
|
+
}
|
|
256
|
+
catch { /* already gone */ }
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Remove pidfiles whose recorded PID is dead. Keeps `isLocallyArmed` honest
|
|
265
|
+
* even when a watcher died without removing its own file (hard kill / crash). */
|
|
266
|
+
function sweepDeadPidfiles() {
|
|
267
|
+
try {
|
|
268
|
+
const dir = watchersDir();
|
|
269
|
+
if (!dir || !fs.existsSync(dir))
|
|
270
|
+
return;
|
|
271
|
+
for (const f of fs.readdirSync(dir)) {
|
|
272
|
+
if (!f.endsWith('.json'))
|
|
273
|
+
continue;
|
|
274
|
+
try {
|
|
275
|
+
const rec = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
276
|
+
if (!rec || typeof rec.pid !== 'number' || !pidAlive(rec.pid)) {
|
|
277
|
+
fs.rmSync(path.join(dir, f), { force: true });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
fs.rmSync(path.join(dir, f), { force: true }); // unparseable → drop
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch { /* best-effort */ }
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=watcher-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher-registry.js","sourceRoot":"","sources":["../../src/commands/watcher-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/hook.js
CHANGED
|
@@ -53,6 +53,7 @@ const turn_provenance_1 = require("./turn-provenance");
|
|
|
53
53
|
const codex_steering_1 = require("./codex-steering");
|
|
54
54
|
const front_desk_mail_1 = require("./front-desk-mail");
|
|
55
55
|
const email_pull_1 = require("./email-pull");
|
|
56
|
+
const watcher_registry_1 = require("./commands/watcher-registry");
|
|
56
57
|
const API_URL_DEFAULT = 'https://api.greprag.com';
|
|
57
58
|
const MAX_FIELD_CHARS = 500_000; // safety cap per text field
|
|
58
59
|
// ---------- Env + config ---------------------------------------------------
|
|
@@ -996,6 +997,20 @@ function writeRecapOutput(text, mode) {
|
|
|
996
997
|
* Storage and display are both UTC. The agent can compute local time itself
|
|
997
998
|
* if needed — a server-side UTC display avoids straddle-day confusion. */
|
|
998
999
|
async function recap(input, mode = 'plain') {
|
|
1000
|
+
// SessionStart orphan reap (best-effort, side-effect only — never writes to
|
|
1001
|
+
// stdout, which carries this hook's additionalContext JSON). At session start
|
|
1002
|
+
// every live watcher belongs to a PRIOR incarnation; this kills any whose
|
|
1003
|
+
// consumer is gone (severed ancestry) and sweeps stale pidfiles, so the orphan
|
|
1004
|
+
// pile that OOM-crashed the desktop cannot accumulate across reloads. Bounded
|
|
1005
|
+
// (~one process snapshot) and guaranteed to touch only `inbox watch`
|
|
1006
|
+
// processes. adr: adr/monitor-resilience.md
|
|
1007
|
+
try {
|
|
1008
|
+
const reaped = (0, watcher_registry_1.reapOrphanWatchers)();
|
|
1009
|
+
if (reaped.killed.length > 0) {
|
|
1010
|
+
process.stderr.write(`[greprag] reaped ${reaped.killed.length} orphan watcher(s) [${reaped.killed.join(', ')}]\n`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
catch { /* reaping is best-effort — never block session start */ }
|
|
999
1014
|
const cwd = input.cwd || process.cwd();
|
|
1000
1015
|
const cfg = getConfig(cwd);
|
|
1001
1016
|
if (!cfg.enabled || !cfg.apiKey)
|
|
@@ -1124,27 +1139,17 @@ async function recap(input, mode = 'plain') {
|
|
|
1124
1139
|
}
|
|
1125
1140
|
writeRecapOutput(parts.join('\n') + '\n', mode);
|
|
1126
1141
|
}
|
|
1127
|
-
/**
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
});
|
|
1139
|
-
if (!res.ok)
|
|
1140
|
-
return null;
|
|
1141
|
-
const data = await res.json();
|
|
1142
|
-
return (data.watchers ?? []).some(w => (0, session_id_1.truncateSessionId)(w.session_id) === short);
|
|
1143
|
-
}
|
|
1144
|
-
catch {
|
|
1145
|
-
return null;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1142
|
+
/** Arm-state detection moved LOCAL (2026-06-04). The former `isSessionArmed`
|
|
1143
|
+
* asked the server's watcher registry "does this session have a live socket?"
|
|
1144
|
+
* and its docstring claimed that was "ground truth, not a stale PID". It was
|
|
1145
|
+
* the opposite: an ORPHAN (consumer dead, SSE socket still open and respawning)
|
|
1146
|
+
* keeps its socket, so it never falls off the registry — it reads as armed.
|
|
1147
|
+
* That single false premise drove the whole failure: a fresh ghost lease
|
|
1148
|
+
* suppressed re-arm (the live session went unwatched — "it keeps dying"), and
|
|
1149
|
+
* an expired one let re-arm stack a new watcher on the undead orphan (the
|
|
1150
|
+
* pile-up that OOM-crashed the desktop). Liveness is a LOCAL-process fact now —
|
|
1151
|
+
* see `isLocallyArmed` in commands/watcher-registry.ts, gated on a supervisor
|
|
1152
|
+
* pidfile this machine actually owns. adr: adr/monitor-resilience.md */
|
|
1148
1153
|
/** UserPromptSubmit — fires on every user prompt. History:
|
|
1149
1154
|
*
|
|
1150
1155
|
* v1: surfaced a project-wide unread inbox COUNT. Removed in v5.6.1 — the count
|
|
@@ -1152,8 +1157,10 @@ async function isSessionArmed(apiUrl, apiKey, short) {
|
|
|
1152
1157
|
* v2: turn-2 fallback arm injection — blind (fired on turn 2 regardless, with a
|
|
1153
1158
|
* conditional "if already armed, ignore" hedge because it couldn't see state).
|
|
1154
1159
|
*
|
|
1155
|
-
* v3
|
|
1156
|
-
*
|
|
1160
|
+
* v3: DETECTION-GATED arm injection, server-registry version (retired 2026-06-04
|
|
1161
|
+
* — the registry counted orphans as armed; see isSessionArmed's gravestone).
|
|
1162
|
+
* v4 (current): DETECTION-GATED on LOCAL state. Every turn, check whether THIS
|
|
1163
|
+
* session has a live watcher PROCESS on this machine (isLocallyArmed, pidfile):
|
|
1157
1164
|
* armed → silent (never nag once armed)
|
|
1158
1165
|
* not armed → inject the LOUD arm directive (buildArmDirective). Unconditional:
|
|
1159
1166
|
* we only reach here when the server CONFIRMS no watcher, so there
|
|
@@ -1199,9 +1206,17 @@ async function notify(input, source = 'claude-code') {
|
|
|
1199
1206
|
writeAdditionalContext('UserPromptSubmit', context);
|
|
1200
1207
|
return;
|
|
1201
1208
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1209
|
+
// Local-first arm detection. Ground truth is a live watcher PROCESS on THIS
|
|
1210
|
+
// machine — its consumer (the Monitor task) is local, so a live supervisor
|
|
1211
|
+
// pidfile means a live, consumed watcher. This replaces the server
|
|
1212
|
+
// `isSessionArmed` registry check, whose ghost-lease flaw made an orphan
|
|
1213
|
+
// (consumer dead, socket still open) read as "armed" — which both suppressed
|
|
1214
|
+
// re-arm when the real watcher was gone AND, once the lease expired, let
|
|
1215
|
+
// re-arm stack a new watcher on the undead orphan. With EPIPE-terminal + the
|
|
1216
|
+
// SessionStart reaper sweeping stale pidfiles, "no live pidfile" reliably means
|
|
1217
|
+
// "needs arming", lag-free and with no cloud round-trip. adr: adr/monitor-resilience.md
|
|
1218
|
+
if ((0, watcher_registry_1.isLocallyArmed)(short))
|
|
1219
|
+
return; // live local watcher → silent
|
|
1205
1220
|
writeAdditionalContext('UserPromptSubmit', (0, session_id_1.buildArmDirective)(short, (0, session_id_1.readIdentityAlias)()));
|
|
1206
1221
|
}
|
|
1207
1222
|
/** "You've got mail" turn hook (Chip B) — wire as a UserPromptSubmit hook.
|
|
@@ -1259,9 +1274,9 @@ async function maybeAutoSaveAttachments(cwd, apiUrl, apiKey) {
|
|
|
1259
1274
|
return (0, email_pull_1.buildAutoSaveSummary)(saved, dir);
|
|
1260
1275
|
}
|
|
1261
1276
|
/** UserPromptSubmit PROBE / manual diagnostic — prints a per-turn readout of
|
|
1262
|
-
* whether THIS session has a live watcher (the same
|
|
1263
|
-
* `notify` gates on). Kept as a separate subcommand for visibility/
|
|
1264
|
-
* wire it as a second UserPromptSubmit hook to see arm state
|
|
1277
|
+
* whether THIS session has a live watcher (the same isLocallyArmed pidfile
|
|
1278
|
+
* signal `notify` gates on). Kept as a separate subcommand for visibility/
|
|
1279
|
+
* debugging: wire it as a second UserPromptSubmit hook to see arm state.
|
|
1265
1280
|
* adr: adr/session-id-awareness.md */
|
|
1266
1281
|
async function armCheck(input) {
|
|
1267
1282
|
const cfg = getConfig(input.cwd || process.cwd());
|
|
@@ -1270,10 +1285,10 @@ async function armCheck(input) {
|
|
|
1270
1285
|
const short = (0, session_id_1.truncateSessionId)(input.session_id);
|
|
1271
1286
|
if (!short)
|
|
1272
1287
|
return;
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1288
|
+
// Local ground truth — the same signal `notify` gates on (live supervisor
|
|
1289
|
+
// pidfile on this machine), not the ghost-prone cloud registry.
|
|
1290
|
+
const armed = (0, watcher_registry_1.isLocallyArmed)(short);
|
|
1291
|
+
const verdict = `ARM-CHECK (probe): session ${short} ${armed ? 'IS armed — live local watcher detected.' : 'is NOT armed — no live local watcher detected.'}`;
|
|
1277
1292
|
process.stdout.write(JSON.stringify({
|
|
1278
1293
|
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: verdict },
|
|
1279
1294
|
}) + '\n');
|