@theronap/cortex-mcp 0.9.137 → 0.9.138
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/lib/edge_extract.mjs +79 -5
- package/package.json +1 -1
package/lib/edge_extract.mjs
CHANGED
|
@@ -216,22 +216,96 @@ export function extractSession(transcript) {
|
|
|
216
216
|
'\n\n--- SESSION ---\n' + text.slice(0, 12000) + '\n--- END ---'
|
|
217
217
|
// Single-flight: if another session already has a summarizer running, skip (caller ships the tail;
|
|
218
218
|
// server summarizes). Prevents the concurrent-`claude -p` stampede that deadlocks the OAuth refresh.
|
|
219
|
-
if (!acquireSummaryLock()) return
|
|
219
|
+
if (!acquireSummaryLock()) return edgeSkip('busy', 'another session is summarizing; the tail ships instead')
|
|
220
220
|
try {
|
|
221
221
|
const r = spawnSync(
|
|
222
222
|
'claude',
|
|
223
223
|
['--print', '--model', process.env.CORTEX_SUMMARY_MODEL ?? 'claude-haiku-4-5', prompt],
|
|
224
224
|
{ env: edgeSafeEnv(process.env, { CORTEX_SUMMARIZING: '1' }), encoding: 'utf8', timeout: summaryTimeoutMs(), maxBuffer: 4 * 1024 * 1024 },
|
|
225
225
|
)
|
|
226
|
-
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
return
|
|
226
|
+
const why = classifyEdgeFailure(r)
|
|
227
|
+
if (why) return edgeSkip(why.reason, why.detail)
|
|
228
|
+
const parsed = parseEdgeJson(r.stdout.trim())
|
|
229
|
+
if (!parsed) return edgeSkip('unparseable', 'the model returned output that is not the expected JSON')
|
|
230
|
+
return parsed
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return edgeSkip('threw', e instanceof Error ? e.message : String(e))
|
|
230
233
|
} finally {
|
|
231
234
|
releaseSummaryLock()
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
// ── WHY THE EXTRACTION DID NOT RUN ───────────────────────────────────────────────────────────────
|
|
239
|
+
//
|
|
240
|
+
// Every failure path here returns null, and the caller correctly treats null as "ship the transcript
|
|
241
|
+
// tail and let the server summarize". That fallback is right. What was wrong is that FIVE different
|
|
242
|
+
// failures were indistinguishable, and one of them never fixes itself.
|
|
243
|
+
//
|
|
244
|
+
// 🔴 THE EVIDENCE. Found 2026-09-08: `claude --print` had been exiting 1 with
|
|
245
|
+
// "Failed to authenticate: OAuth session expired and could not be refreshed" — so edge extraction
|
|
246
|
+
// returned null for EVERY session, on every seat with an expired token. Nothing said so anywhere.
|
|
247
|
+
// It was invisible because the fallback works: 51 of 51 `claude-code` records over the preceding ten
|
|
248
|
+
// days carried a summary, because the SERVER wrote them. A green outcome over a dead mechanism.
|
|
249
|
+
//
|
|
250
|
+
// Two things were silently untrue for however long that lasted:
|
|
251
|
+
// 1. this module's own header — "the cloud then receives only the derived digest, never the raw
|
|
252
|
+
// transcript" — since the fallback ships the (redacted) tail instead;
|
|
253
|
+
// 2. anything downstream of the extraction, including ADR-0055's page proposals, which cannot
|
|
254
|
+
// exist if the call that would make them never returns.
|
|
255
|
+
//
|
|
256
|
+
// A busy lock and an expired credential are not the same event: the first resolves itself on the
|
|
257
|
+
// next session, the second needs a human to run `claude setup-token` and will otherwise never
|
|
258
|
+
// recover. Collapsing them into one silent `return null` is the same defect family as
|
|
259
|
+
// claimRecordForTriage's "probably in the future" — a message that names one cause for many.
|
|
260
|
+
function edgeSkip(reason, detail) {
|
|
261
|
+
// stderr, not stdout: stdout of a Stop hook is not read, and anything written there would land in
|
|
262
|
+
// the transcript of the NEXT capture. Prefixed so `cortex doctor` and a log grep can find it.
|
|
263
|
+
process.stderr.write(`cortex: edge extraction SKIPPED [${reason}] ${detail}\n`)
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* PURE (unit-tested): classify a finished `claude --print` result, or null when it succeeded.
|
|
269
|
+
*
|
|
270
|
+
* Exported so the failure taxonomy is testable without spawning anything — the auth case in
|
|
271
|
+
* particular could not otherwise be covered, and it is the one that matters most.
|
|
272
|
+
*/
|
|
273
|
+
export function classifyEdgeFailure(r) {
|
|
274
|
+
if (!r) return { reason: 'no-result', detail: 'spawn returned nothing' }
|
|
275
|
+
// spawn itself failed — almost always ENOENT, i.e. `claude` is not on the hook's PATH. A Stop hook
|
|
276
|
+
// runs with a login shell's PATH, which is not always the interactive one.
|
|
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 }
|
|
281
|
+
}
|
|
282
|
+
// timeout: spawnSync kills the child, so status is null and signal is set.
|
|
283
|
+
if (r.signal) return { reason: 'timeout', detail: `killed by ${r.signal} after ${summaryTimeoutMs()}ms` }
|
|
284
|
+
|
|
285
|
+
const out = `${r.stdout ?? ''}\n${r.stderr ?? ''}`
|
|
286
|
+
// ⚠ THE AUTH MESSAGE ARRIVES ON **STDOUT**, WITH EXIT 1 — not on stderr, which is where a reader
|
|
287
|
+
// would look for it. Verified against the live failure 2026-09-08:
|
|
288
|
+
// status 1, stderr "", stdout "Failed to authenticate: OAuth session expired and could not be
|
|
289
|
+
// refreshed". Matching only stderr would classify this as a plain non-zero exit and lose the one
|
|
290
|
+
// fact that tells a human what to do.
|
|
291
|
+
if (/oauth|authenticat|not logged in|401|unauthorized/i.test(out)) {
|
|
292
|
+
return {
|
|
293
|
+
reason: 'auth-expired',
|
|
294
|
+
detail: 'the subscription login for headless `claude --print` is dead. Run `claude setup-token`, '
|
|
295
|
+
+ 'then set CLAUDE_CODE_OAUTH_TOKEN in ~/.claude/settings.json (env). '
|
|
296
|
+
+ 'Until then EVERY session ships its transcript tail to the server instead of a local digest, '
|
|
297
|
+
+ 'and no page proposals are made.',
|
|
298
|
+
}
|
|
299
|
+
}
|
|
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
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function firstLine(s) {
|
|
306
|
+
return String(s ?? '').split('\n').map((l) => l.trim()).filter(Boolean)[0] ?? '(no output)'
|
|
307
|
+
}
|
|
308
|
+
|
|
235
309
|
// Tolerant JSON extraction: the model may wrap output in prose / ```json fences, so grab the first
|
|
236
310
|
// balanced-looking {...} span. Arrays pass through untouched — the SERVER validates them.
|
|
237
311
|
export function parseEdgeJson(out) {
|
package/package.json
CHANGED