@theronap/cortex-mcp 0.9.40 → 0.9.41
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/capture.mjs +4 -1
- package/lib/diagnose.mjs +15 -13
- package/package.json +1 -1
package/lib/capture.mjs
CHANGED
|
@@ -189,11 +189,14 @@ async function captureWork(stdinRaw) {
|
|
|
189
189
|
|
|
190
190
|
let res
|
|
191
191
|
try {
|
|
192
|
+
// Best-effort background ingest: bound it tight and retry once so a hanging/504-ing server
|
|
193
|
+
// makes a DETACHED worker self-terminate in ~20s instead of lingering on 3 unbounded attempts.
|
|
192
194
|
res = await fetchCortex(`${base}/api/ingest`, {
|
|
193
195
|
method: 'POST',
|
|
194
196
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
195
197
|
body: JSON.stringify(ingestBody),
|
|
196
|
-
|
|
198
|
+
timeoutMs: 10_000,
|
|
199
|
+
}, { retries: 1 })
|
|
197
200
|
} catch (e) {
|
|
198
201
|
// Never break a session — just report and move on.
|
|
199
202
|
process.stderr.write(`cortex: ${e.message}\n`)
|
package/lib/diagnose.mjs
CHANGED
|
@@ -68,20 +68,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
68
68
|
// 'app' → a real Cortex API error with a JSON message (retriable only if 5xx)
|
|
69
69
|
export function classify(status, contentType, bodyText, requestId) {
|
|
70
70
|
const isJson = (contentType ?? '').includes('application/json')
|
|
71
|
-
let
|
|
72
|
-
if (isJson) { try {
|
|
73
|
-
const appError = body?.error ?? null
|
|
71
|
+
let appError = null
|
|
72
|
+
if (isJson) { try { appError = JSON.parse(bodyText)?.error ?? null } catch { /* not json after all */ } }
|
|
74
73
|
const rid = requestId ? ` [request id: ${requestId}]` : ''
|
|
75
74
|
|
|
76
|
-
// Multi-brain: 409 no_active_brain means the caller belongs to >1 brain and hasn't chosen one. It is
|
|
77
|
-
// NOT an auth failure — surface the action (set_active_brain) directly, not a "Cortex API 409:" wrapper.
|
|
78
|
-
if (status === 409 && body?.code === 'no_active_brain') {
|
|
79
|
-
return {
|
|
80
|
-
kind: 'app', retriable: false,
|
|
81
|
-
message: `${appError ?? 'You belong to more than one brain — call set_active_brain to choose where your writes land.'} (see my_brains for your options)${rid}`,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
75
|
if (status === 401 || (isJson && appError === 'invalid token')) {
|
|
86
76
|
return {
|
|
87
77
|
kind: 'auth', retriable: false,
|
|
@@ -107,11 +97,23 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
107
97
|
// fetch with retry on TRANSIENT responses only (429, 5xx, and infra-style non-JSON 403).
|
|
108
98
|
// App-level 401/403-with-JSON are returned immediately (retrying won't change the verdict).
|
|
109
99
|
// Throws a clear network error if the host is unreachable after retries.
|
|
100
|
+
//
|
|
101
|
+
// PER-ATTEMPT TIMEOUT (added 2026-07-08): every attempt is bounded by an AbortSignal so a
|
|
102
|
+
// HANGING server (not a fast 5xx — an ingest endpoint that just never responds, observed this day)
|
|
103
|
+
// can't stall a hook indefinitely. Without it, `fetch` waits forever and the retry loop made it
|
|
104
|
+
// WORSE — 3 unbounded attempts stacked. Now worst case = (retries+1) * timeoutMs + backoff, and a
|
|
105
|
+
// timed-out attempt is treated as transient (retried, then surfaced as the reach error the callers
|
|
106
|
+
// already swallow). Tunable via CORTEX_HTTP_TIMEOUT_MS; per-call override via opts.timeoutMs.
|
|
110
107
|
export async function fetchCortex(url, opts = {}, { retries = 2, baseDelayMs = 400 } = {}) {
|
|
108
|
+
const { timeoutMs: optTimeout, signal: callerSignal, ...fetchOpts } = opts
|
|
109
|
+
const timeoutMs = optTimeout ?? (Number(process.env.CORTEX_HTTP_TIMEOUT_MS) || 15_000)
|
|
111
110
|
let lastErr
|
|
112
111
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
113
112
|
try {
|
|
114
|
-
|
|
113
|
+
// Fresh timeout signal per attempt; compose with any caller-supplied signal.
|
|
114
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
115
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal
|
|
116
|
+
const res = await fetch(url, { ...fetchOpts, signal })
|
|
115
117
|
const ct = res.headers.get('content-type') ?? ''
|
|
116
118
|
const transient =
|
|
117
119
|
res.status === 429 ||
|