@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 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 body = null
72
- if (isJson) { try { body = JSON.parse(bodyText) } catch { /* not json after all */ } }
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
- const res = await fetch(url, opts)
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 ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.40",
3
+ "version": "0.9.41",
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": {