@theronap/agnoclast-mcp 0.9.254 → 0.9.256

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.
@@ -0,0 +1,31 @@
1
+ // ONE INTAKE UNIT ID, TWO SPELLINGS (2026-10-22).
2
+ //
3
+ // `not_mine` took `intake_item_id` while `intake_release` (and every other intake verb) took
4
+ // `intakeItemId`. The schemas strip an unknown key, so the wrong spelling arrived as "missing" — one
5
+ // session made 607 failing calls on 10-21/10-22 guessing between the two. Every intake verb now takes
6
+ // BOTH spellings; when both are given they must agree, never silently pick one.
7
+
8
+ /**
9
+ * PURE. The intake unit id from either spelling, or an error string.
10
+ * `required: false` lets a verb that also takes another kind of id (not_mine's record_id) accept neither.
11
+ */
12
+ export function intakeIdFrom(args, { required = true } = {}) {
13
+ const camel = typeof args?.intakeItemId === 'string' ? args.intakeItemId.trim() : ''
14
+ const snake = typeof args?.intake_item_id === 'string' ? args.intake_item_id.trim() : ''
15
+ if (camel && snake && camel !== snake) {
16
+ return { error: `intakeItemId (${camel}) and intake_item_id (${snake}) disagree — pass one of them.` }
17
+ }
18
+ const id = camel || snake
19
+ if (!id && required) return { error: 'intakeItemId is required (intake_item_id is accepted too).' }
20
+ return { id: id || null }
21
+ }
22
+
23
+ /** intakeIdFrom for an expression-bodied handler: the id, or a thrown Error the SDK returns as a tool error. */
24
+ export function requireIntakeId(args) {
25
+ const r = intakeIdFrom(args)
26
+ if (r.error) throw new Error(r.error)
27
+ return r.id
28
+ }
29
+
30
+ /** The zod description for the alias field, so every verb says the same thing. */
31
+ export const INTAKE_ID_ALIAS_NOTE = 'same as intakeItemId — either spelling is accepted'
package/lib/server.mjs CHANGED
@@ -25,6 +25,7 @@ import { renderObligations, renderPageObligations, tierReads, who } from './obli
25
25
  import { renderLeaveQuestion, renderLeft, renderRemovalQuestion, renderRemoved } from './member_remove_render.mjs'
26
26
  import { renderSettings } from './settings_render.mjs'
27
27
  import { renderConversationOffer, renderMaterializeBatch } from './intake_batch_render.mjs'
28
+ import { intakeIdFrom, requireIntakeId, INTAKE_ID_ALIAS_NOTE } from './intake_id_alias.mjs'
28
29
  import { obligationsForUnits, renderFilingObligations, filingObligationsBrief, OBLIGATIONS_UNANSWERED_TEXT } from './filing_obligations.mjs'
29
30
  import { renderRecord, READ_RECORD_DEFAULT_CHARS, READ_RECORD_PARTS } from './record_render.mjs'
30
31
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
@@ -1204,9 +1205,14 @@ function renderNudge(payload) {
1204
1205
  title: 'Claim private intake items',
1205
1206
  description:
1206
1207
  'Claim a lease on private intake items (relevance or cleanup). Returns decrypted payloads for the lease holder only. Requires x-cortex-session-key (set automatically by this MCP server). Aged work never blocks a claim: the response reports cleanupDueCount and how far behind the oldest unit is, and servicing it is expected but always your call. Claiming is a commitment to process — hand back anything you will not finish with intake_release, or intake_defer if it is the owner\'s decision to make. ' +
1207
- 'YOU ARE THE JUDGE of every unit you claim (ADR-0062): nothing is published until you materialize, and materializing is when it becomes visible. Each unit carries a `proposal` — the brain its signals point to and, for that brain, the pages it would attach to, the page that would govern it, and the `tier` it would be born with — computed without writing anything. Decide the brain, the pages (confirm, drop or add), which page should govern (the page whose reasons for restricting its audience fit the content, not a person page), and obligations. Pages only tighten a tier; if a record should be LOOSER than `tierReason.baseline`, that is the owner\'s call — intake_defer with the question.',
1208
+ 'YOU ARE THE JUDGE of every unit you claim (ADR-0062): nothing is published until you materialize, and materializing is when it becomes visible. Each unit carries a `proposal` — the brain its signals point to and, for that brain, the pages it would attach to, the page that would govern it, and the `tier` it would be born with — computed without writing anything. Decide the brain, the pages (confirm, drop or add), which page should govern (the page whose reasons for restricting its audience fit the content, not a person page), and obligations. Pages only tighten a tier; if a record should be LOOSER than `tierReason.baseline`, that is the owner\'s call — intake_defer with the question. ' +
1209
+ 'view "compact" (trial, only where the server enables it — otherwise you get the full response): the judge rules, your brains, the lease and the cleanup count come ONCE per response in `rules`/`brains`, not on every unit; each unit carries its full content, only the proposed brain\'s pages as one line each, and up front what intake_materialize would refuse it for (`materialize.otherHomes`, `materialize.folderQuestions`). Read `rules` before judging.',
1208
1210
  inputSchema: {
1209
1211
  claimKind: z.enum(['relevance', 'cleanup']).optional().describe('default relevance'),
1212
+ view: z.enum(['full', 'compact']).optional().describe(
1213
+ 'default "full". "compact" sends the rules once per response instead of on every unit, and only the proposed ' +
1214
+ 'brain\'s placement. Honoured only where the server enables it; otherwise the response is the full one.',
1215
+ ),
1210
1216
  limit: z.number().optional().describe('max items (default 10). Ignored when intakeItemIds is given.'),
1211
1217
  source: z.string().optional().describe('claim only the oldest units of ONE source — e.g. "imessage", "meeting", "email", "github", "claude-code". Reaches texts without first claiming every older GitHub/session unit ahead of them. Ignored when intakeItemIds is given.'),
1212
1218
  leaseMinutes: z.number().optional().describe('lease length in minutes, 5-60 (default 30). Prefer intake_look for read-only inspection — a short lease is only for briefly exclusive work.'),
@@ -1221,6 +1227,7 @@ function renderNudge(payload) {
1221
1227
  'a single unit to claim — the same thing as intakeItemIds with one element. Accepted because ' +
1222
1228
  'every other intake verb spells it this way.',
1223
1229
  ),
1230
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1224
1231
  intakeItemIds: z.array(z.string()).optional().describe(
1225
1232
  'claim these specific units instead of the head of the queue (max 50). Without it you get FIFO, ' +
1226
1233
  'so reaching one known unit means claiming everything ahead of it. Ids come from intake_changes ' +
@@ -1232,9 +1239,11 @@ function renderNudge(payload) {
1232
1239
  ),
1233
1240
  },
1234
1241
  },
1235
- async ({ claimKind, limit, source, intakeItemId, intakeItemIds, leaseMinutes }) => {
1236
- // Merge the two spellings before anything else looks at them. Passing both is not an error —
1237
- // the union is what the caller plainly meant.
1242
+ async ({ claimKind, limit, source, intakeItemId, intakeItemIds, leaseMinutes, intake_item_id, view }) => {
1243
+ // Merge the spellings before anything else looks at them. Passing both is not an error —
1244
+ // the union is what the caller plainly meant. (intake_item_id: 2026-10-22, see intake_id_alias.mjs —
1245
+ // dropped by the schema it would have meant a FIFO claim of the head of the queue.)
1246
+ if (intake_item_id) intakeItemIds = [...(intakeItemIds ?? []), intake_item_id]
1238
1247
  const ids = [...new Set([...(intakeItemIds ?? []), ...(intakeItemId ? [intakeItemId] : [])])]
1239
1248
  intakeItemIds = ids.length ? ids : undefined
1240
1249
  const res = await fetchCortex(`${BASE}/api/intake/claim`, {
@@ -1250,6 +1259,8 @@ function renderNudge(payload) {
1250
1259
  // claim into a no-op.
1251
1260
  ...(intakeItemIds ? { intakeItemIds } : {}),
1252
1261
  ...(source ? { source } : {}),
1262
+ // Sent only when asked for: a default claim's request body is exactly what it was.
1263
+ ...(view === 'compact' ? { view } : {}),
1253
1264
  }),
1254
1265
  })
1255
1266
  if (!res.ok) {
@@ -1282,7 +1293,9 @@ function renderNudge(payload) {
1282
1293
  + `Nothing is blocked — run intake_claim with claimKind:"cleanup" when you reach a natural break.`
1283
1294
  }
1284
1295
 
1285
- return { content: [{ type: 'text', text: JSON.stringify(j, null, 2) + nudge }] }
1296
+ // The compact view is printed without indentation — it is what the view is for. The full one, as before.
1297
+ const text = j?.view === 'compact' ? JSON.stringify(j) : JSON.stringify(j, null, 2)
1298
+ return { content: [{ type: 'text', text: text + nudge }] }
1286
1299
  },
1287
1300
  )
1288
1301
 
@@ -1294,19 +1307,21 @@ function renderNudge(payload) {
1294
1307
  'Atomically publish a claimed intake item into one brain — the disclosure moment (ADR-0062). Deterministic identifier homes in that brain are always attached; documentIds may add further pages. The record is born at the strictest of its source baseline, what the producer asked for, and every attached page; a source floor (iMessage, claude-code) pins it. ' +
1295
1308
  'Name the governing page with governingPage (one of documentIds, never a person page) — otherwise the ranking picks it. ' +
1296
1309
  'Pass mode "preview" first when your choice differs from the claim\'s proposal: it returns the pages, the governing page and the tier this exact call would produce, and writes nothing. Never writes private intake into search/history before this call. ' +
1297
- '⚠ ADR-0051: a unit does not finish until you looked for its OTHER homes. A call whose documentIds add nothing beyond the deterministic homes it gets anyway is REFUSED once — name the pages this unit is about (the design it implements, the project, the person), or go look, or say you looked with searchedForOtherHomes. ' +
1310
+ '⚠ ADR-0051 — BEFORE you file, look for this unit\'s OTHER homes: the pages it is about beyond what an identifier matched (the design it implements, the project, the person, the decision it bears on). grep or read for them, then name them in documentIds. A call that names nothing beyond the deterministic homes still FILES, on those homes only, and says so — add pages later with route_record. ' +
1298
1311
  '⚠ A meeting or lecture TRANSCRIPT files with NOTES in `summary` (Theron, 2026-10-08): the gist, decisions, action items and dates, names spelled right. Without them the call is REFUSED; the transcript itself is kept on the record unchanged. ' +
1299
1312
  '⚠ 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.',
1300
1313
  inputSchema: {
1301
- intakeItemId: z.string().describe('intake item uuid'),
1314
+ intakeItemId: z.string().optional().describe('intake item uuid (required — intake_item_id is accepted too)'),
1315
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1302
1316
  intakeItemIds: z.array(z.string()).optional().describe(
1303
1317
  'MORE units to file exactly the same way, in one call — one record EACH, never merged (ADR-0076 §4.5). ' +
1304
- 'For a conversation\'s waiting texts that you decided together: claim them all with `intake_claim`, then pass ' +
1318
+ 'Any units you hold a live claim on may be batched, even from separate intake_claim calls — except that texts from ' +
1319
+ 'DIFFERENT text conversations never share a batch. Claim them with `intake_claim`, then pass ' +
1305
1320
  'the first as intakeItemId and the rest here. title/summary/dedupeKey are ignored in a batch (each unit keeps its ' +
1306
1321
  'own), and a preview covers intakeItemId only. Each id is reported separately; a failure never hides behind a success.',
1307
1322
  ),
1308
1323
  orgId: z.string().describe('destination brain org uuid'),
1309
- 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)'),
1324
+ documentIds: z.array(z.string()).optional().describe('additional pages in orgId — each a page TITLE exactly as read_page shows it, its `ref:`, or a brain_documents id (deterministic homes are merged automatically). A title that matches no page, or more than one, is refused with the candidates listed'),
1310
1325
  title: z.string().optional(),
1311
1326
  summary: z.string().optional().describe(
1312
1327
  'what future retrieval reads first. For a meeting or lecture TRANSCRIPT these are its NOTES, and they are required: ' +
@@ -1317,7 +1332,7 @@ function renderNudge(payload) {
1317
1332
  dedupeKey: z.string().optional(),
1318
1333
  origin: z.enum(['deterministic', 'llm', 'user', 'session']).optional(),
1319
1334
  governingPage: z.string().optional().describe(
1320
- 'the page that should GOVERN this record (ADR-0062) — a page `ref:` or document id, and one of documentIds. ' +
1335
+ 'the page that should GOVERN this record (ADR-0062) — a page title, `ref:` or document id, and one of documentIds. ' +
1321
1336
  'Choose the page whose reasons for restricting its audience fit the content; never a person page. It can only ' +
1322
1337
  'tighten the record, and the ranking will not move it. Omit to leave the choice to the ranking.',
1323
1338
  ),
@@ -1326,9 +1341,8 @@ function renderNudge(payload) {
1326
1341
  ),
1327
1342
  searchedForOtherHomes: z.boolean().optional().describe(
1328
1343
  'ADR-0051. "I looked for other homes and there are genuinely none." Only consulted when documentIds add no page ' +
1329
- 'beyond the deterministic homes. ⚠ DO NOT SEND THIS ON THE FIRST CALL AS A MATTER OF COURSE — it is the answer AFTER ' +
1330
- 'looking, not a way past the question. Measured 2026-09-25: 30 records filed through this verb in one afternoon with no ' +
1331
- 'judged page on any of them.',
1344
+ 'beyond the deterministic homes; it suppresses the "filed on its deterministic homes only" note. Send it only AFTER ' +
1345
+ 'looking — it is the answer to the question, not a way past it.',
1332
1346
  ),
1333
1347
  folderDecisions: z.array(z.object({
1334
1348
  identifier: z.string().describe('the open question\'s identifier, or a folder above it that its answerWith lists'),
@@ -1356,6 +1370,9 @@ function renderNudge(payload) {
1356
1370
  },
1357
1371
  },
1358
1372
  async (args) => {
1373
+ const unit = intakeIdFrom(args)
1374
+ if (unit.error) return toolError(unit.error)
1375
+ args = { ...args, intakeItemId: unit.id }
1359
1376
  // ADR-0076 §4.5 — A BATCH: one decision, N records. Theron, 2026-10-05: a conversation's waiting texts
1360
1377
  // are looked up once and handled together, but "it shouldn't file all of them as one record". The
1361
1378
  // loop lives here, as it does for place_staged_record's batch: the route stays one unit per call.
@@ -1380,8 +1397,8 @@ function renderNudge(payload) {
1380
1397
  attachmentMeta: (args.documentIds ?? []).map(() => ({ origin: args.origin ?? 'llm' })),
1381
1398
  ...(args.governingPage ? { governingDocumentId: args.governingPage } : {}),
1382
1399
  ...(args.searchedForOtherHomes ? { searchedForOtherHomes: true } : {}),
1383
- // One placement is a decision about ONE conversation: the server refuses any id whose
1384
- // batch differs from the first id's, so a stray id is never filed onto these pages.
1400
+ // The server refuses an id it cannot safely file with the first: a text from ANOTHER text
1401
+ // conversation, or a unit this session holds no live claim on (not_same_batch).
1385
1402
  sameBatchAs: batch[0],
1386
1403
  ...obligationsFor(id),
1387
1404
  }),
@@ -1389,8 +1406,11 @@ function renderNudge(payload) {
1389
1406
  const o = await r.json().catch(() => null)
1390
1407
  const why = o?.error === 'obligations_unanswered'
1391
1408
  ? 'not filed — a text needs `obligations` ([] when it asks nothing)'
1392
- : (o?.error ?? `HTTP ${r.status}`)
1393
- results.push({ id, ok: r.ok, detail: r.ok ? `${o?.recordId ?? 'filed'}${filingObligationsBrief(o?.obligations)}` : why })
1409
+ : o?.error === 'not_same_batch'
1410
+ ? `not filed — ${o?.hint ?? 'not in the same batch as the first id'}`
1411
+ : `${o?.error ?? `HTTP ${r.status}`}${o?.detail ? ` — ${o.detail}` : ''}`
1412
+ const homesNote = o?.otherHomes?.note ? ' (deterministic homes only — add pages with route_record)' : ''
1413
+ results.push({ id, ok: r.ok, detail: r.ok ? `${o?.recordId ?? 'filed'}${homesNote}${filingObligationsBrief(o?.obligations)}` : why })
1394
1414
  } catch (e) {
1395
1415
  results.push({ id, ok: false, detail: e.message })
1396
1416
  }
@@ -1484,6 +1504,8 @@ function renderNudge(payload) {
1484
1504
  // the ordinary case (observed 2026-09-03 on the first handoff record materialised). Silence is
1485
1505
  // correct here, and it also covers a server too old to send the field at all.
1486
1506
  const lines = []
1507
+ // ADR-0051, non-blocking since server 2026-10-22: filed, but on deterministic homes only.
1508
+ if (body?.otherHomes?.note) lines.push(`⚠ ${body.otherHomes.note}`)
1487
1509
  const governingLine = renderGoverningLine(body?.governing)
1488
1510
  if (governingLine) lines.push(governingLine)
1489
1511
  const sa = body?.sessionAttachments
@@ -1538,11 +1560,16 @@ function renderNudge(payload) {
1538
1560
  description:
1539
1561
  'The OTHER terminal outcome for a claimed intake unit: this is nothing, drop it. Use it for content that should never become a record — unsubscribe receipts, empty greetings, marketing blasts, a stranger\'s photo — instead of materializing junk into a brain because materialize was the only verb available. IRREVERSIBLE: the ciphertext and nonces are destroyed in the same transaction, and a content-free tombstone stops the connector re-delivering the unit. You must already hold the claim (discarding something you never read is refused), `reason` is required and is stored, and it is one item per call — a loop discarding a whole pile on one decision is a bulk job, not judgment.',
1540
1562
  inputSchema: {
1541
- intakeItemId: z.string().describe('intake item uuid from intake_claim'),
1563
+ intakeItemId: z.string().optional().describe('intake item uuid from intake_claim (required — intake_item_id is accepted too)'),
1564
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1542
1565
  reason: z.string().describe('why this is nothing — recorded on the claim, and the only surviving trace of the decision'),
1543
1566
  },
1544
1567
  },
1545
- async ({ intakeItemId, reason }) => {
1568
+ async (args) => {
1569
+ const { reason } = args
1570
+ const unit = intakeIdFrom(args)
1571
+ if (unit.error) return toolError(unit.error)
1572
+ const intakeItemId = unit.id
1546
1573
  let res
1547
1574
  try {
1548
1575
  res = await fetchCortex(`${BASE}/api/intake/discard`, {
@@ -1577,12 +1604,17 @@ function renderNudge(payload) {
1577
1604
  description:
1578
1605
  'The THIRD terminal outcome, and the right one whenever the honest answer is "this is not mine to decide." Use it when a claimed unit is someone else\'s private content, when the destination brain is a real judgment call rather than a lookup, or when publishing and destroying are both wrong — a stranger\'s message thread, a photo you cannot place, an email whose brain depends on context only the owner has. Destroys NOTHING: the unit stays encrypted and intact, its state becomes `awaiting_user`, and your `question` is what the owner actually sees. Prefer this over letting a lease lapse — a lapsed lease says nothing, increments `attempts`, and hands the identical dead end to the next session. Requires the claim, one item per call.',
1579
1606
  inputSchema: {
1580
- intakeItemId: z.string().describe('intake item uuid from intake_claim'),
1607
+ intakeItemId: z.string().optional().describe('intake item uuid from intake_claim (required — intake_item_id is accepted too)'),
1608
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1581
1609
  question: z.string().describe('what you need the owner to decide, in their words not yours — this is the entire message they get, so "which brain should this iMessage thread go to, if any?" beats "needs triage"'),
1582
1610
  options: z.array(z.string()).optional().describe('the concrete choices, when the decision is a pick rather than an open question — e.g. ["Personal","TTO","Discard — nothing to record"]. Stored on the question and rendered by intake_cleanup_status, so the owner can answer with a choice instead of prose.'),
1583
1611
  },
1584
1612
  },
1585
- async ({ intakeItemId, question, options }) => {
1613
+ async (args) => {
1614
+ const { question, options } = args
1615
+ const unit = intakeIdFrom(args)
1616
+ if (unit.error) return toolError(unit.error)
1617
+ const intakeItemId = unit.id
1586
1618
  let res
1587
1619
  try {
1588
1620
  res = await fetchCortex(`${BASE}/api/intake/defer`, {
@@ -1617,11 +1649,16 @@ function renderNudge(payload) {
1617
1649
  description:
1618
1650
  'Give a claimed unit back to the queue WITHOUT deciding anything. This is not a fourth outcome — materialize, discard and defer resolve a unit; release just ends your hold on it. Use it when you claimed more than you can act on, or when the unit turns out to belong to work you are not doing. Prefer it over letting the lease expire: a lapse and a crash are indistinguishable in the ledger, so silently timing out costs the pile the one signal that says a session looked and chose to pass. Use intake_defer instead when the unit needs the OWNER to decide — release puts it back in front of the next session, which will hit whatever wall you did.',
1619
1651
  inputSchema: {
1620
- intakeItemId: z.string().describe('intake item uuid from intake_claim'),
1652
+ intakeItemId: z.string().optional().describe('intake item uuid from intake_claim (required — intake_item_id is accepted too)'),
1653
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1621
1654
  reason: z.string().describe('why you are handing it back — "claimed too broadly", "not related to this session\'s work". Recorded on the claim, and the only thing distinguishing this from a lapsed lease'),
1622
1655
  },
1623
1656
  },
1624
- async ({ intakeItemId, reason }) => {
1657
+ async (args) => {
1658
+ const { reason } = args
1659
+ const unit = intakeIdFrom(args)
1660
+ if (unit.error) return toolError(unit.error)
1661
+ const intakeItemId = unit.id
1625
1662
  let res
1626
1663
  try {
1627
1664
  res = await fetchCortex(`${BASE}/api/intake/release`, {
@@ -1715,11 +1752,12 @@ function renderNudge(payload) {
1715
1752
  description:
1716
1753
  'Read a PENDING intake unit\'s FULL decrypted body AND its `proposal` — the brain its signals point to, the pages it would attach to, the page that would govern it, the tier it would be born with, and `unclaimedIdentifiers` — without taking a lease. A read RECEIPT, not a claim (ADR-0035). The proposal rides with the look because they answer one question together: what is this, and where would it go — so you can tell whether a unit is yours to judge BEFORE leasing it. Use it when the settling feed\'s headline + candidate identifiers cannot tell you whether the unit belongs to your current work; looking is non-exclusive (any number of sessions may inspect the same unit) and always logged, so look freely rather than guessing from the headline — diligence is subsidized here. After looking: claim it if it is yours to process, or simply move on — no release needed, you never held it. The response includes how many sessions have looked (`looks`/`distinctLookers`): several looks and no claim is a sign the unit needs the triage agent or the owner, not another look. Resolved units are refused — read those as records.',
1717
1754
  inputSchema: {
1718
- intakeItemId: z.string().describe('intake item uuid, from the settling feed (intake_changes) or intake_claim'),
1755
+ intakeItemId: z.string().optional().describe('intake item uuid, from the settling feed (intake_changes) or intake_claim (required — intake_item_id is accepted too)'),
1756
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1719
1757
  workingIdentifiers: z.array(z.string()).optional().describe('the identifiers your current work touches (repo:…, file:…, project:…) — stored on the receipt; feeds look→claim conversion analysis'),
1720
1758
  },
1721
1759
  },
1722
- async ({ intakeItemId, workingIdentifiers }) => renderIntakeLook(intakeItemId, workingIdentifiers ?? []),
1760
+ async ({ intakeItemId, intake_item_id, workingIdentifiers }) => renderIntakeLook(requireIntakeId({ intakeItemId, intake_item_id }), workingIdentifiers ?? []),
1723
1761
  )
1724
1762
 
1725
1763
  server.registerTool(
@@ -4069,10 +4107,24 @@ function renderNudge(payload) {
4069
4107
  // a session had judged irrelevant was re-offered on every tool call — pilot B's 22-hour
4070
4108
  // re-offer defect (§11), recreated by the fix for the other half of the same problem.
4071
4109
  intake_item_id: z.string().optional().describe('the ARRIVAL id from the ⚡ ARRIVALS SETTLING block (not a record id)'),
4110
+ // 2026-10-22: every other intake verb spells it intakeItemId; one session made 607 failing calls
4111
+ // guessing between the two. Both are accepted — see intake_id_alias.mjs.
4112
+ intakeItemId: z.string().optional().describe('same as intake_item_id — either spelling is accepted'),
4113
+ recordId: z.string().optional().describe('same as record_id — either spelling is accepted'),
4072
4114
  reason: z.string().describe('WHY it is not yours — recorded, and the only thing that can later answer "why did nobody take this"'),
4073
4115
  },
4074
4116
  },
4075
- async ({ record_id, intake_item_id, reason }) => {
4117
+ async (args) => {
4118
+ const { reason } = args
4119
+ const unit = intakeIdFrom(args, { required: false })
4120
+ if (unit.error) return toolError(unit.error)
4121
+ const intake_item_id = unit.id
4122
+ const recCamel = typeof args.recordId === 'string' ? args.recordId.trim() : ''
4123
+ const recSnake = typeof args.record_id === 'string' ? args.record_id.trim() : ''
4124
+ if (recCamel && recSnake && recCamel !== recSnake) {
4125
+ return toolError(`recordId (${recCamel}) and record_id (${recSnake}) disagree — pass one of them.`)
4126
+ }
4127
+ const record_id = recCamel || recSnake || null
4076
4128
  // ⚠ REFUSE BOTH AND NEITHER EXPLICITLY. Tonight's other defect was a parameter accepted by the
4077
4129
  // caller, dropped by the schema, and replaced by a default that did something else entirely.
4078
4130
  // An ambiguous decline must fail loudly, never pick one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.254",
3
+ "version": "0.9.256",
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": {