@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.
@@ -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)
@@ -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
- return r.error.code === 'ENOENT'
279
- ? { reason: 'no-claude', detail: '`claude` is not on PATH for this hook; edge extraction cannot run here' }
280
- : { reason: 'spawn-failed', detail: r.error.message }
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
- // timeout: spawnSync kills the child, so status is null and signal is set.
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
- if (r.status !== 0) return { reason: 'exit', detail: `claude exited ${r.status}: ${firstLine(out)}` }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.138",
3
+ "version": "0.9.140",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {