@theronap/cortex-mcp 0.9.138 → 0.9.140
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/bin/cortex-mcp.mjs +30 -0
- package/lib/edge_extract.mjs +25 -7
- package/package.json +1 -1
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -28,6 +28,36 @@ const VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.u
|
|
|
28
28
|
const cmd = process.argv[2]
|
|
29
29
|
const rest = process.argv.slice(3)
|
|
30
30
|
|
|
31
|
+
// ── The summarizer subprocess runs NO context hooks ──────────────────────────────────────────────
|
|
32
|
+
//
|
|
33
|
+
// Edge extraction shells out to `claude --print` with CORTEX_SUMMARIZING=1. That headless session
|
|
34
|
+
// fires the SAME hook chain as a real one, and on a wired seat that is five SessionStart commands
|
|
35
|
+
// plus hydrate on UserPromptSubmit — four of which make network calls to Agnoclast.
|
|
36
|
+
//
|
|
37
|
+
// `capture` (Stop) has guarded on this env var since the recursion fix. Nothing else did, so every
|
|
38
|
+
// summarizer call still paid for snapshot-context + status + skills + hydrate before the model saw
|
|
39
|
+
// a single token of the prompt. Two consequences, both measured live 2026-09-08:
|
|
40
|
+
//
|
|
41
|
+
// 1. LATENCY. Extraction ran 45-68s against a 45s timeout and failed as often as it succeeded.
|
|
42
|
+
// A one-off `claude --print 'say ok'` on the same credential answers in about a second.
|
|
43
|
+
// 2. POLLUTED OUTPUT. `status` prints the pending-records block to stdout, so the summarizer
|
|
44
|
+
// answered THAT before the prompt: its stdout began "You have 17 intake units in the cleanup
|
|
45
|
+
// queue ... Want me to help with either?" ahead of the JSON. The extractor asks for "ONLY
|
|
46
|
+
// minified JSON"; a hook prepending prose to the model's input is how that instruction gets
|
|
47
|
+
// overridden by the system itself.
|
|
48
|
+
//
|
|
49
|
+
// The guard is here, at the dispatcher, rather than in each subcommand: it is one rule about what a
|
|
50
|
+
// summarizer subprocess IS, and putting it in five places is how the sixth gets forgotten. Exit 0
|
|
51
|
+
// and silent — a hook that errors is noise, and there is genuinely nothing to do.
|
|
52
|
+
//
|
|
53
|
+
// ⚠ NOT guarded: `capture` (has its own, with a diagnostic), and anything a human might run by hand
|
|
54
|
+
// while this var happens to be set. Only the ambient context commands are listed.
|
|
55
|
+
const CONTEXT_HOOK_COMMANDS = new Set(['snapshot-context', 'status', 'hydrate', 'skills'])
|
|
56
|
+
if (process.env.CORTEX_SUMMARIZING && CONTEXT_HOOK_COMMANDS.has(cmd)) {
|
|
57
|
+
process.stderr.write(`cortex: ${cmd} skipped — summarizer subprocess (CORTEX_SUMMARIZING)\n`)
|
|
58
|
+
process.exit(0)
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
if (cmd === '--version' || cmd === '-v') {
|
|
32
62
|
process.stdout.write(`cortex-mcp ${VERSION}\n`)
|
|
33
63
|
process.exit(0)
|
package/lib/edge_extract.mjs
CHANGED
|
@@ -275,13 +275,33 @@ export function classifyEdgeFailure(r) {
|
|
|
275
275
|
// spawn itself failed — almost always ENOENT, i.e. `claude` is not on the hook's PATH. A Stop hook
|
|
276
276
|
// runs with a login shell's PATH, which is not always the interactive one.
|
|
277
277
|
if (r.error) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
if (r.error.code === 'ENOENT') {
|
|
279
|
+
return { reason: 'no-claude', detail: '`claude` is not on PATH for this hook; edge extraction cannot run here' }
|
|
280
|
+
}
|
|
281
|
+
// ⚠ A spawnSync TIMEOUT surfaces here, as error.code ETIMEDOUT — not only as a signal. Observed
|
|
282
|
+
// live 2026-09-08: `[spawn-failed] spawnSync claude ETIMEDOUT` at 47.3s against a 45s limit,
|
|
283
|
+
// which reads as "the spawn broke" when the truth is "it ran and was too slow". Checking the
|
|
284
|
+
// signal alone missed it, so both are checked; this classifier's own first outing found this.
|
|
285
|
+
if (r.error.code === 'ETIMEDOUT') {
|
|
286
|
+
return { reason: 'timeout', detail: `exceeded ${summaryTimeoutMs()}ms` }
|
|
287
|
+
}
|
|
288
|
+
return { reason: 'spawn-failed', detail: r.error.message }
|
|
281
289
|
}
|
|
282
|
-
//
|
|
290
|
+
// The other shape of the same event: spawnSync killed the child, so status is null and signal set.
|
|
283
291
|
if (r.signal) return { reason: 'timeout', detail: `killed by ${r.signal} after ${summaryTimeoutMs()}ms` }
|
|
284
292
|
|
|
293
|
+
// 🔴 EVERYTHING BELOW APPLIES ONLY TO A FAILED CALL. Exit 0 means the model answered; its answer
|
|
294
|
+
// is CONTENT, not a status report, and must never be scanned for failure keywords.
|
|
295
|
+
//
|
|
296
|
+
// This ordering was wrong on the first version and produced a false `auth-expired` on a call that
|
|
297
|
+
// had returned status 0 — because the session being summarized was itself about fixing an OAuth
|
|
298
|
+
// bug, so the model's own summary contained the word. A working extraction was thrown away for
|
|
299
|
+
// discussing the wrong subject. Any session about auth, 401s or logins would have hit it.
|
|
300
|
+
//
|
|
301
|
+
// The general form of the mistake: matching a symptom string across a channel that carries both
|
|
302
|
+
// diagnostics AND payload. The exit status is the only trustworthy separator, so it goes first.
|
|
303
|
+
if (r.status === 0) return r.stdout ? null : { reason: 'no-output', detail: 'claude exited 0 with empty stdout' }
|
|
304
|
+
|
|
285
305
|
const out = `${r.stdout ?? ''}\n${r.stderr ?? ''}`
|
|
286
306
|
// ⚠ THE AUTH MESSAGE ARRIVES ON **STDOUT**, WITH EXIT 1 — not on stderr, which is where a reader
|
|
287
307
|
// would look for it. Verified against the live failure 2026-09-08:
|
|
@@ -297,9 +317,7 @@ export function classifyEdgeFailure(r) {
|
|
|
297
317
|
+ 'and no page proposals are made.',
|
|
298
318
|
}
|
|
299
319
|
}
|
|
300
|
-
|
|
301
|
-
if (!r.stdout) return { reason: 'no-output', detail: 'claude exited 0 with empty stdout' }
|
|
302
|
-
return null
|
|
320
|
+
return { reason: 'exit', detail: `claude exited ${r.status}: ${firstLine(out)}` }
|
|
303
321
|
}
|
|
304
322
|
|
|
305
323
|
function firstLine(s) {
|
package/package.json
CHANGED