@theronap/agnoclast-mcp 0.9.173 → 0.9.175
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/diagnose.mjs +23 -1
- package/lib/server.mjs +26 -8
- package/package.json +1 -1
package/lib/diagnose.mjs
CHANGED
|
@@ -542,6 +542,26 @@ export function outageReport(outcome, { tool, requestId = null, version = CLIENT
|
|
|
542
542
|
return null
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
// ⚠ "COULD NOT CONNECT" IS ALSO WHAT A LAPTOP WITH NO NETWORK LOOKS LIKE. The first real report (2026-09-25)
|
|
546
|
+
// was ×12 `connect` outages from one Mac between 09:34 and 11:39 — hooks running during DarkWake /
|
|
547
|
+
// Maintenance Sleep with the network half up, while production was healthy. So a thrown connect/timeout
|
|
548
|
+
// is only filed when the INTERNET is reachable and Agnoclast is not. The probe hits a neutral anycast host
|
|
549
|
+
// with its own short budget; if that fails too, the fault is local and nothing is filed. A response of any
|
|
550
|
+
// kind (infra 4xx/5xx, 503) proves the network worked, so those paths never probe.
|
|
551
|
+
const REACHABILITY_URL = 'https://1.1.1.1/cdn-cgi/trace'
|
|
552
|
+
const REACHABILITY_TIMEOUT_MS = 2500
|
|
553
|
+
let reachabilityProbe = async () => {
|
|
554
|
+
try {
|
|
555
|
+
const r = await fetch(REACHABILITY_URL, { method: 'GET', signal: AbortSignal.timeout(REACHABILITY_TIMEOUT_MS) })
|
|
556
|
+
try { await r.body?.cancel() } catch { /* nothing to drain */ }
|
|
557
|
+
return true
|
|
558
|
+
} catch {
|
|
559
|
+
return false
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/** Tests: replace the internet-reachability probe. Returns the previous one. */
|
|
563
|
+
export function setReachabilityProbe(fn) { const prev = reachabilityProbe; reachabilityProbe = fn; return prev }
|
|
564
|
+
|
|
545
565
|
/** Auto-filing is off under `bun test` unless the test points the spool somewhere (never the real home). */
|
|
546
566
|
function autofileEnabled(env = process.env) {
|
|
547
567
|
if (env.CORTEX_AUTOFILE === '0') return false
|
|
@@ -601,7 +621,9 @@ export async function fetchCortex(url, opts = {}, policy = {}) {
|
|
|
601
621
|
} catch (e) {
|
|
602
622
|
if (hook && e?.retryClass) {
|
|
603
623
|
const r = outageReport({ thrownClass: e.retryClass }, { tool: toolForUrl(url) })
|
|
604
|
-
|
|
624
|
+
// Awaited, not fire-and-forget: hook processes are short-lived and would exit before a detached
|
|
625
|
+
// probe answered. Bounded by REACHABILITY_TIMEOUT_MS, and only on a path that is already failing.
|
|
626
|
+
try { if (r && await reachabilityProbe()) spoolReport(r) } catch { /* filing must never change the error */ }
|
|
605
627
|
}
|
|
606
628
|
throw e
|
|
607
629
|
}
|
package/lib/server.mjs
CHANGED
|
@@ -231,8 +231,25 @@ export async function runServer(version) {
|
|
|
231
231
|
const body = await res.text()
|
|
232
232
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
233
233
|
}
|
|
234
|
-
const { context, brainRefs } = await res.json()
|
|
234
|
+
const { context, brainRefs, settling } = await res.json()
|
|
235
|
+
// ⭐ THE SETTLING FEED, APPENDED TO THE SESSION-START BLOCK — the thing it has never carried.
|
|
236
|
+
//
|
|
237
|
+
// Everything else in that block is PERSON-scoped, so /api/mcp-context never needed to know which
|
|
238
|
+
// session was asking and never read the `x-cortex-session-key` header it has been receiving on
|
|
239
|
+
// every request since fetchCortex started sending it. The settling feed is the only inherently
|
|
240
|
+
// SESSION-scoped signal in the block, which is why it was structurally excluded rather than
|
|
241
|
+
// forgotten — and why a NEW SESSION has never once been told which arrivals are asking it a
|
|
242
|
+
// question. That is the resume case: start a session, and the offers are waiting.
|
|
243
|
+
//
|
|
244
|
+
// ⚠ NOT CACHED, and the distinction matters. The block is cached for 5 minutes; an OFFER is
|
|
245
|
+
// live state that another session can claim out from under this one. Baking it into the cached
|
|
246
|
+
// string would serve an offer already taken, or hide one that just arrived — and a feed whose
|
|
247
|
+
// whole contract is "this persists until you answer it" cannot afford to be five minutes wrong.
|
|
248
|
+
// So the cache keeps the person-scoped context exactly as before, and the session-scoped feed is
|
|
249
|
+
// appended only on a FRESH fetch. Session start and resume are always fresh fetches, which is
|
|
250
|
+
// the case this exists for; mid-session cache hits are covered by the tools.
|
|
235
251
|
cache = { text: context, ts: now, repo }
|
|
252
|
+
const withSettling = (context ?? '') + renderSettling({ settling })
|
|
236
253
|
// T11: stash the digest node refs surfaced this fetch so capture.mjs can forward them as
|
|
237
254
|
// hydrated_from at the session's ingest (feedback-loop guard). Keyed by cwd so the matching
|
|
238
255
|
// session picks them up. Best-effort + inert when brainRefs is empty (digest flag off).
|
|
@@ -244,7 +261,8 @@ export async function runServer(version) {
|
|
|
244
261
|
writeFileSync(join(dir, `${key}.json`), JSON.stringify({ refs: brainRefs, ts: now }))
|
|
245
262
|
}
|
|
246
263
|
} catch { /* best-effort — never break context serving */ }
|
|
247
|
-
|
|
264
|
+
// The fresh block carries the offers; the cached one deliberately does not (see above).
|
|
265
|
+
return withSettling
|
|
248
266
|
}
|
|
249
267
|
|
|
250
268
|
const server = new McpServer({ name: 'cortex', version })
|
|
@@ -373,7 +391,7 @@ function renderNudge(payload) {
|
|
|
373
391
|
// so a session doing pipeline work never saw it — measured 2026-09-24, eighteen hours with a
|
|
374
392
|
// stale watermark while three arrivals landed unannounced. This is the tool an agent calls at
|
|
375
393
|
// session start and on every topic shift, which is when "mail arrived" is worth knowing.
|
|
376
|
-
return { content: [{ type: 'text', text: (ctx?.context ?? '') + renderArrival(ctx) }] }
|
|
394
|
+
return { content: [{ type: 'text', text: (ctx?.context ?? '') + renderArrival(ctx) + renderSettling(ctx) }] }
|
|
377
395
|
},
|
|
378
396
|
)
|
|
379
397
|
|
|
@@ -870,7 +888,7 @@ function renderNudge(payload) {
|
|
|
870
888
|
// so a session doing pipeline work never saw it — measured 2026-09-24, eighteen hours with a
|
|
871
889
|
// stale watermark while three arrivals landed unannounced. This is the tool an agent calls at
|
|
872
890
|
// session start and on every topic shift, which is when "mail arrived" is worth knowing.
|
|
873
|
-
return { content: [{ type: 'text', text: (ctx?.context ?? '') + renderArrival(ctx) }] }
|
|
891
|
+
return { content: [{ type: 'text', text: (ctx?.context ?? '') + renderArrival(ctx) + renderSettling(ctx) }] }
|
|
874
892
|
},
|
|
875
893
|
)
|
|
876
894
|
|
|
@@ -2170,7 +2188,7 @@ function renderNudge(payload) {
|
|
|
2170
2188
|
if (!out) return toolError('The server returned no body.')
|
|
2171
2189
|
|
|
2172
2190
|
if (!out.total) {
|
|
2173
|
-
return { content: [{ type: 'text', text: 'Nothing staged — every arrival has reached a brain.' }] }
|
|
2191
|
+
return { content: [{ type: 'text', text: 'Nothing staged — every arrival has reached a brain.' + renderSettling(out) }] }
|
|
2174
2192
|
}
|
|
2175
2193
|
const lines = [`${out.total} staged arrival(s) — in NO brain yet:`, '']
|
|
2176
2194
|
for (const a of out.arrivals ?? []) {
|
|
@@ -2219,7 +2237,7 @@ function renderNudge(payload) {
|
|
|
2219
2237
|
lines.push('', 'Place one with place_staged_record (staged_id + pages). The pages decide the brain, which decides who can read it.')
|
|
2220
2238
|
lines.push('No page fits? `author` the page it needs, then place onto it. Junk? `discard_staged` (reversible, dry-run by default).')
|
|
2221
2239
|
lines.push('A default brain is NOT the answer for content — one standing rule cannot decide disclosure for mail nobody has read.')
|
|
2222
|
-
return { content: [{ type: 'text', text: withAskNotice(lines.join('\n')) }] }
|
|
2240
|
+
return { content: [{ type: 'text', text: withAskNotice(lines.join('\n')) + renderSettling(out) }] }
|
|
2223
2241
|
},
|
|
2224
2242
|
)
|
|
2225
2243
|
|
|
@@ -4276,7 +4294,7 @@ function renderNudge(payload) {
|
|
|
4276
4294
|
// ⚠ THE DELTA STILL SHIPS ON THE EMPTY PATH. "Nothing waiting for a home" and "nothing has
|
|
4277
4295
|
// arrived" are different facts, and an empty digest is exactly when a silent counter reads
|
|
4278
4296
|
// as all-clear.
|
|
4279
|
-
return { content: [{ type: 'text', text: 'Nothing waiting for a home.' + renderArrival(out) }] }
|
|
4297
|
+
return { content: [{ type: 'text', text: 'Nothing waiting for a home.' + renderArrival(out) + renderSettling(out) }] }
|
|
4280
4298
|
}
|
|
4281
4299
|
const lines = records.map((r) => {
|
|
4282
4300
|
const homes = r.currentHomes?.length ? r.currentHomes.join(', ') : 'nothing'
|
|
@@ -4286,7 +4304,7 @@ function renderNudge(payload) {
|
|
|
4286
4304
|
return {
|
|
4287
4305
|
content: [{
|
|
4288
4306
|
type: 'text',
|
|
4289
|
-
text: `${records.length} record(s) waiting for a home:\n\n${lines.join('\n\n')}\n\nRecognize any as your own work? claim_record it now, then route_record once you know where it belongs.` + renderArrival(out),
|
|
4307
|
+
text: `${records.length} record(s) waiting for a home:\n\n${lines.join('\n\n')}\n\nRecognize any as your own work? claim_record it now, then route_record once you know where it belongs.` + renderArrival(out) + renderSettling(out),
|
|
4290
4308
|
}],
|
|
4291
4309
|
}
|
|
4292
4310
|
},
|
package/package.json
CHANGED