@theronap/agnoclast-mcp 0.9.212 → 0.9.214
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 +9 -2
- package/lib/doctor.mjs +1 -1
- package/lib/intake_batch_render.mjs +26 -0
- package/lib/server.mjs +46 -2
- package/package.json +1 -1
package/lib/diagnose.mjs
CHANGED
|
@@ -679,7 +679,14 @@ export async function fetchCortex(url, opts = {}, policy = {}) {
|
|
|
679
679
|
|
|
680
680
|
// Live health probe: hit /api/mcp-context with the token and classify the result.
|
|
681
681
|
// Returns { ok, status, projectCount?, requestId?, diagnosis? }.
|
|
682
|
-
|
|
682
|
+
//
|
|
683
|
+
// `probe: true` (the SessionStart status line) asks the server for the token check and the capture
|
|
684
|
+
// notice ONLY, not the context. That hook fires at the same moment as `snapshot-context`, which fetches
|
|
685
|
+
// the full context, so without it every session start assembled the context twice, concurrently, on a
|
|
686
|
+
// max:1 pool: a ~1.6 s start measured ~5 s in prod (2026-10-06). doctor/setup/install keep the full
|
|
687
|
+
// fetch on purpose; they exist to prove the context itself comes back. An older server ignores the
|
|
688
|
+
// parameter and returns the full context, which this still parses, so the two can ship in either order.
|
|
689
|
+
export async function checkToken(token, base, { probe = false } = {}) {
|
|
683
690
|
if (!token) {
|
|
684
691
|
return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
|
|
685
692
|
message: 'No CORTEX_TOKEN found. Run: npx -y @theronap/cortex-mcp login (opens your browser; or `setup <token>` with a token from the console → Connect your AI)' } }
|
|
@@ -689,7 +696,7 @@ export async function checkToken(token, base) {
|
|
|
689
696
|
message: `Token "${String(token).slice(0, 8)}…" is not a valid Agnoclast token (expected a UUID). ` +
|
|
690
697
|
`Re-run setup with the token from the console.` } }
|
|
691
698
|
}
|
|
692
|
-
const url = `${(base ?? CANONICAL_BASE).replace(/\/$/, '')}/api/mcp-context`
|
|
699
|
+
const url = `${(base ?? CANONICAL_BASE).replace(/\/$/, '')}/api/mcp-context${probe ? '?probe=1' : ''}`
|
|
693
700
|
let res
|
|
694
701
|
try {
|
|
695
702
|
res = await fetchCortex(url, { headers: { Authorization: `Bearer ${token}` } })
|
package/lib/doctor.mjs
CHANGED
|
@@ -71,7 +71,7 @@ async function statusLine(out, { capturesStuck = false } = {}) {
|
|
|
71
71
|
return
|
|
72
72
|
}
|
|
73
73
|
try {
|
|
74
|
-
const r = await checkToken(token, base)
|
|
74
|
+
const r = await checkToken(token, base, { probe: true })
|
|
75
75
|
if (r.ok) {
|
|
76
76
|
// ⚠ THE NOTICE REPLACES THE HAPPY LINE RATHER THAN FOLLOWING IT.
|
|
77
77
|
// "connected — sessions on this machine are captured to your org" was printed truthfully to
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Rendering for ADR-0076 §4.5 — a conversation's texts, decided together and filed one record each.
|
|
2
|
+
// Kept out of server.mjs so it can be tested on its output, not on its source text.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A batch materialization, per id. ⚠ PARTIAL FAILURE IS REPORTED PER ID, never summarised as success — a
|
|
6
|
+
* batch that half landed is the one outcome an agent must not mistake for done (same rule as
|
|
7
|
+
* place_staged_record's batch).
|
|
8
|
+
*/
|
|
9
|
+
export function renderMaterializeBatch(results) {
|
|
10
|
+
const good = results.filter((r) => r.ok)
|
|
11
|
+
const bad = results.filter((r) => !r.ok)
|
|
12
|
+
return `Batch of ${results.length}: ${good.length} filed, ${bad.length} failed — one record each.\n` +
|
|
13
|
+
results.map((r) => ` ${r.ok ? '✓' : '✗'} ${r.id} — ${r.detail}`).join('\n') +
|
|
14
|
+
(bad.length ? `\n\n⚠ ${bad.length} did NOT land. They are still in the pile; fix the cause and file those ids again (a unit must be claimed first).` : '')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One settling offer that stands for a whole text conversation (the server collapsed its waiting texts into
|
|
19
|
+
* it), or null when the offer is a single arrival and renders the ordinary way.
|
|
20
|
+
*/
|
|
21
|
+
export function renderConversationOffer(x, why = '', looked = '') {
|
|
22
|
+
if (!x?.conversation || !(x.conversation.count > 1)) return null
|
|
23
|
+
const ids = x.conversation.ids.slice(0, 12).join(', ') + (x.conversation.ids.length > 12 ? ', …' : '')
|
|
24
|
+
return ` - 📱 ${x.conversation.count} texts in one conversation · newest ${x.hoursAgo}h ago · ${ids}${why}${looked}\n` +
|
|
25
|
+
' decide them together: `intake_look` the newest, `intake_claim` them all, then file them in one `intake_materialize` (intakeItemIds) — one record each'
|
|
26
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { spawnObligationWorker } from './obligations_worker.mjs'
|
|
|
24
24
|
import { renderObligations, renderPageObligations, tierReads, who } from './obligations_render.mjs'
|
|
25
25
|
import { renderRemovalQuestion, renderRemoved } from './member_remove_render.mjs'
|
|
26
26
|
import { renderSettings } from './settings_render.mjs'
|
|
27
|
+
import { renderConversationOffer, renderMaterializeBatch } from './intake_batch_render.mjs'
|
|
27
28
|
import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
|
|
28
29
|
|
|
29
30
|
// PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
|
|
@@ -428,6 +429,9 @@ function renderSettling(payload) {
|
|
|
428
429
|
? ' — ⚠ NO session of yours matches this; it is here because it reached nobody'
|
|
429
430
|
: ' — every session that matched it declined, so it is back to everyone'
|
|
430
431
|
const looked = x.looks > 0 ? ` [looked at ${x.looks}x, still unclaimed]` : ''
|
|
432
|
+
// ADR-0076 §4.5: a conversation's waiting texts arrive one per message and are offered as ONE line.
|
|
433
|
+
const conversationLine = renderConversationOffer(x, why, looked)
|
|
434
|
+
if (conversationLine) return conversationLine
|
|
431
435
|
return ` - ${x.source} · ${x.hoursAgo}h ago · ${x.intakeItemId}${why}${looked}`
|
|
432
436
|
})
|
|
433
437
|
|
|
@@ -1230,6 +1234,12 @@ function renderNudge(payload) {
|
|
|
1230
1234
|
'⚠ ADR-0077: a GitHub commit or PR does not finish until every changed path has an answer. If a changed file\'s deepest folder (or, in a grab-bag folder like web/lib/engine/ or db/supabase/, the file itself) has no claim and no decline, the call is REFUSED with the open questions; answer each in folderDecisions and call again.',
|
|
1231
1235
|
inputSchema: {
|
|
1232
1236
|
intakeItemId: z.string().describe('intake item uuid'),
|
|
1237
|
+
intakeItemIds: z.array(z.string()).optional().describe(
|
|
1238
|
+
'MORE units to file exactly the same way, in one call — one record EACH, never merged (ADR-0076 §4.5). ' +
|
|
1239
|
+
'For a conversation\'s waiting texts that you decided together: claim them all with `intake_claim`, then pass ' +
|
|
1240
|
+
'the first as intakeItemId and the rest here. title/summary/dedupeKey are ignored in a batch (each unit keeps its ' +
|
|
1241
|
+
'own), and a preview covers intakeItemId only. Each id is reported separately; a failure never hides behind a success.',
|
|
1242
|
+
),
|
|
1233
1243
|
orgId: z.string().describe('destination brain org uuid'),
|
|
1234
1244
|
documentIds: z.array(z.string()).optional().describe('additional pages in orgId — each a page `ref:` exactly as read_page shows it, or a brain_documents id (deterministic homes are merged automatically)'),
|
|
1235
1245
|
title: z.string().optional(),
|
|
@@ -1266,6 +1276,38 @@ function renderNudge(payload) {
|
|
|
1266
1276
|
},
|
|
1267
1277
|
},
|
|
1268
1278
|
async (args) => {
|
|
1279
|
+
// ADR-0076 §4.5 — A BATCH: one decision, N records. Theron, 2026-10-05: a conversation's waiting texts
|
|
1280
|
+
// are looked up once and handled together, but "it shouldn't file all of them as one record". The
|
|
1281
|
+
// loop lives here, as it does for place_staged_record's batch: the route stays one unit per call.
|
|
1282
|
+
const batch = [...new Set([args.intakeItemId, ...(args.intakeItemIds ?? [])])].slice(0, 50)
|
|
1283
|
+
if (batch.length > 1 && args.mode !== 'preview') {
|
|
1284
|
+
const results = []
|
|
1285
|
+
for (const id of batch) {
|
|
1286
|
+
try {
|
|
1287
|
+
const r = await fetchCortex(`${BASE}${intakeMaterializePath(undefined)}`, {
|
|
1288
|
+
method: 'POST',
|
|
1289
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1290
|
+
body: JSON.stringify({
|
|
1291
|
+
intakeItemId: id,
|
|
1292
|
+
orgId: args.orgId,
|
|
1293
|
+
documentIds: args.documentIds ?? [],
|
|
1294
|
+
record: { source: args.source, record_type: args.recordType },
|
|
1295
|
+
attachmentMeta: (args.documentIds ?? []).map(() => ({ origin: args.origin ?? 'llm' })),
|
|
1296
|
+
...(args.governingPage ? { governingDocumentId: args.governingPage } : {}),
|
|
1297
|
+
...(args.searchedForOtherHomes ? { searchedForOtherHomes: true } : {}),
|
|
1298
|
+
// One placement is a decision about ONE conversation: the server refuses any id whose
|
|
1299
|
+
// batch differs from the first id's, so a stray id is never filed onto these pages.
|
|
1300
|
+
sameBatchAs: batch[0],
|
|
1301
|
+
}),
|
|
1302
|
+
})
|
|
1303
|
+
const o = await r.json().catch(() => null)
|
|
1304
|
+
results.push({ id, ok: r.ok, detail: r.ok ? (o?.recordId ?? 'filed') : (o?.error ?? `HTTP ${r.status}`) })
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
results.push({ id, ok: false, detail: e.message })
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return { content: [{ type: 'text', text: renderMaterializeBatch(results) }] }
|
|
1310
|
+
}
|
|
1269
1311
|
// A preview goes to its OWN route: a server too old to know it answers 404, where the materialize
|
|
1270
1312
|
// route would ignore a `mode` it does not know and publish.
|
|
1271
1313
|
const preview = args.mode === 'preview'
|
|
@@ -1312,8 +1354,10 @@ function renderNudge(payload) {
|
|
|
1312
1354
|
}
|
|
1313
1355
|
// Same redirect-not-dispatch rule as place_staged_record. A staged arrival needs no claim and
|
|
1314
1356
|
// no unseal, so the fix is a different verb rather than a retry of this one.
|
|
1315
|
-
|
|
1316
|
-
|
|
1357
|
+
// ⚠ `args.intakeItemId`, not a bare `intakeItemId`: this handler never declared one, so a 404 here
|
|
1358
|
+
// threw a ReferenceError instead of saying which verb to use (found 2026-10-05 adding batches).
|
|
1359
|
+
if (res.status === 404 && (await pileHolding(args.intakeItemId)) === 'staged') {
|
|
1360
|
+
return toolError(`${args.intakeItemId} is a STAGED arrival, not an intake unit — \`intake_materialize\` cannot act on it. Use \`place_staged_record\` with the pages it belongs to; staged arrivals take no lease, so there is nothing to claim or release. (\`staged_look\` reads it first, and carries the brain-ladder proposal.)`)
|
|
1317
1361
|
}
|
|
1318
1362
|
return toolError(body)
|
|
1319
1363
|
}
|
package/package.json
CHANGED