@theronap/agnoclast-mcp 0.9.165 → 0.9.167

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.
Files changed (2) hide show
  1. package/lib/server.mjs +65 -16
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -291,35 +291,39 @@ function renderSettling(payload) {
291
291
  if (!offers.length) return ''
292
292
  const total = Number(feed?.total ?? offers.length)
293
293
 
294
+ // ⚠ THE THREE STAGES READ DIFFERENTLY BECAUSE THE RIGHT RESPONSE DIFFERS. A `matched` arrival is
295
+ // probably yours. An `orphan` reached you because it reached NOBODY — you are not the obvious
296
+ // owner, you are the only one looking, and the burden of proof is lower for leaving it. Rendering
297
+ // all three as "matched to what you have open" would be a claim the feed cannot support, and an
298
+ // agent that catches a nudge overstating itself stops trusting the rest of it.
294
299
  const lines = offers.map((x) => {
295
- const why = x.via?.length ? ` — matches ${x.via.map((v) => `\`${v}\``).join(', ')}` : ''
296
- // Looks without claims is evidence about the UNIT, not about the sessions that walked away.
300
+ const why = x.via_reason === 'matched' && x.via?.length
301
+ ? ` — matches ${x.via.map((v) => `\`${v}\``).join(', ')}`
302
+ : x.via_reason === 'orphan'
303
+ ? ' — ⚠ NO session of yours matches this; it is here because it reached nobody'
304
+ : ' — every session that matched it declined, so it is back to everyone'
297
305
  const looked = x.looks > 0 ? ` [looked at ${x.looks}x, still unclaimed]` : ''
298
306
  return ` - ${x.source} · ${x.hoursAgo}h ago · ${x.intakeItemId}${why}${looked}`
299
307
  })
300
308
 
301
- // ⚠ SAY THE TRUE TOTAL, NOT THE PAGE SIZE. Measured 2026-09-22: one identifier matched 113 settling
302
- // units against a cap of 12. A block that says "here are 12" when there are 113 is claiming
303
- // certainty it does not have — and renders identically every turn, which teaches the reader that
304
- // it never changes and can be skipped.
305
309
  const more = total > offers.length
306
310
  ? `\n— Showing ${offers.length} of ${total}. \`intake_changes\` lists the rest; this is a sample, not the queue.`
307
311
  : ''
308
312
 
309
- // ⚠ CONTENTION IS STATED, exactly as the record nudge states `k`. Measured the same day: THREE live
310
- // sessions each matched the SAME 113 units on one identifier. Silent about that, each would claim
311
- // the same units and learn about the race from a failed lease.
312
313
  const maxHolders = offers.reduce((m, x) => Math.max(m, Number(x.holders ?? 1)), 1)
313
314
  const who = maxHolders > 1
314
315
  ? `\n— ⚠ ${maxHolders} live sessions of yours hold a matching identifier, so these are offered to all of them. \`intake_claim\` takes a lease — claim before you work, not after, or two sessions judge the same arrival.`
315
- : `\n— No other live session of yours holds a matching identifier, so nobody else is looking at these.`
316
+ : ''
316
317
 
317
318
  return `\n\n⚡ ARRIVALS SETTLING — yours alone, and no one else can see these yet:\n${lines.join('\n')}\n` +
318
319
  `— These are unjudged ARRIVALS, not records. Since automatic materialization was deleted, nothing files them for you.\n` +
319
320
  `— \`intake_look <id>\` reads the full body without claiming it — looking is free, non-exclusive and logged, so look rather than guess from the headline.\n` +
320
- `— Then \`intake_claim\` and \`intake_materialize\` into the brain and pages it belongs to, or \`intake_discard\`, or \`intake_defer\` with a question when only the owner can decide.` +
321
- more + who +
322
- `\n— If none are yours, leave them: they stay settling for your other sessions, and become everyone's business once the window closes.`
321
+ `— Then \`intake_claim {intakeItemIds: ["<id>"]}\` to take a lease on THAT unit (the array is required; without an id you claim the head of the queue instead), then \`intake_materialize\` into the brain and pages it belongs to — or \`intake_discard\`, or \`intake_defer\` with a question when only the owner can decide.\n` +
322
+ // ⚠ THE DECLINE IS NAMED HERE BECAUSE IT DID NOT EXIST BEFORE 0.9.167. An offer with no
323
+ // available "no" is re-offered on every tool call until the window closes, which is how an
324
+ // agent learns to skip the block entirely.
325
+ `— NOT yours? \`not_mine {intake_item_id: "<id>", reason: "..."}\` — per session, not a suppression: it keeps going to your other matching sessions, and once they have all declined it comes back to everyone.` +
326
+ more + who
323
327
  }
324
328
 
325
329
  function renderNudge(payload) {
@@ -982,6 +986,17 @@ function renderNudge(payload) {
982
986
  claimKind: z.enum(['relevance', 'cleanup']).optional().describe('default relevance'),
983
987
  limit: z.number().optional().describe('max items (default 10). Ignored when intakeItemIds is given.'),
984
988
  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.'),
989
+ // 🔴 SINGULAR ACCEPTED, AND NOT AS A CONVENIENCE. Every sibling verb takes `intakeItemId`:
990
+ // intake_look, intake_materialize, intake_release, intake_defer, intake_discard. This one
991
+ // alone was plural, so the natural singular was NOT IN THE SCHEMA and was silently dropped
992
+ // — and the fallback is not an error, it is claiming the head of the queue. On 2026-09-22 a
993
+ // session offered one unit by the settling block called `intake_claim {intakeItemId}` and
994
+ // took a 30-minute lease on TEN unrelated units belonging to other sessions' work. A typo
995
+ // that becomes a side effect is worse than a typo that fails.
996
+ intakeItemId: z.string().optional().describe(
997
+ 'a single unit to claim — the same thing as intakeItemIds with one element. Accepted because ' +
998
+ 'every other intake verb spells it this way.',
999
+ ),
985
1000
  intakeItemIds: z.array(z.string()).optional().describe(
986
1001
  'claim these specific units instead of the head of the queue (max 50). Without it you get FIFO, ' +
987
1002
  'so reaching one known unit means claiming everything ahead of it. Ids come from intake_changes ' +
@@ -993,7 +1008,11 @@ function renderNudge(payload) {
993
1008
  ),
994
1009
  },
995
1010
  },
996
- async ({ claimKind, limit, intakeItemIds, leaseMinutes }) => {
1011
+ async ({ claimKind, limit, intakeItemId, intakeItemIds, leaseMinutes }) => {
1012
+ // Merge the two spellings before anything else looks at them. Passing both is not an error —
1013
+ // the union is what the caller plainly meant.
1014
+ const ids = [...new Set([...(intakeItemIds ?? []), ...(intakeItemId ? [intakeItemId] : [])])]
1015
+ intakeItemIds = ids.length ? ids : undefined
997
1016
  const res = await fetchCortex(`${BASE}/api/intake/claim`, {
998
1017
  method: 'POST',
999
1018
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
@@ -2832,11 +2851,41 @@ function renderNudge(payload) {
2832
2851
  title: 'Decline a nudged record — it is not this session\'s business',
2833
2852
  description: 'Say a nudged record is NOT your business. Use it when the ⚡ ARRIVED block offered you a record and you have looked and it does not belong to what you are doing. ⚠ THIS IS NOT A CLAIM AND NOT A SUPPRESSION: the record stays unclaimed, stays on the general timeline, and stays offerable to any other session holding a matching identifier. You are saying "not MY business", never "nobody\'s business", and no other session is affected. Declining is a COMPLETE and expected answer — the cascade offers on the evidence of what you have open, which is a good guess and not a fact, and a wrong claim costs more than a decline. Without this the same nudge returns on your next tool call and the disagreement is recorded nowhere.',
2834
2853
  inputSchema: {
2835
- record_id: z.string().describe('the record id from the ⚡ ARRIVED block'),
2854
+ record_id: z.string().optional().describe('the record id from the ⚡ ARRIVED block'),
2855
+ // ⚠ THE TWO BLOCKS OFFER DIFFERENT KINDS OF THING, and this verb now answers both. ⚡ ARRIVED
2856
+ // offers unclaimed RECORDS; ⚡ ARRIVALS SETTLING offers intake ARRIVALS that are not records
2857
+ // and may never become one. Until 0.9.167 the settling block had no decline at all, so a unit
2858
+ // a session had judged irrelevant was re-offered on every tool call — pilot B's 22-hour
2859
+ // re-offer defect (§11), recreated by the fix for the other half of the same problem.
2860
+ intake_item_id: z.string().optional().describe('the ARRIVAL id from the ⚡ ARRIVALS SETTLING block (not a record id)'),
2836
2861
  reason: z.string().describe('WHY it is not yours — recorded, and the only thing that can later answer "why did nobody take this"'),
2837
2862
  },
2838
2863
  },
2839
- async ({ record_id, reason }) => {
2864
+ async ({ record_id, intake_item_id, reason }) => {
2865
+ // ⚠ REFUSE BOTH AND NEITHER EXPLICITLY. Tonight's other defect was a parameter accepted by the
2866
+ // caller, dropped by the schema, and replaced by a default that did something else entirely.
2867
+ // An ambiguous decline must fail loudly, never pick one.
2868
+ if (record_id && intake_item_id) {
2869
+ return toolError('Pass record_id OR intake_item_id, not both — they are different kinds of thing and I will not guess which you meant.')
2870
+ }
2871
+ if (!record_id && !intake_item_id) {
2872
+ return toolError('Pass record_id (from ⚡ ARRIVED) or intake_item_id (from ⚡ ARRIVALS SETTLING).')
2873
+ }
2874
+ if (intake_item_id) {
2875
+ let r
2876
+ try {
2877
+ r = await fetchCortex(`${BASE}/api/settling/reject`, {
2878
+ method: 'POST',
2879
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2880
+ body: JSON.stringify({ intakeItemId: intake_item_id, reason }),
2881
+ })
2882
+ } catch (e) {
2883
+ return toolError(`Could not decline ${intake_item_id}: ${e.message}`)
2884
+ }
2885
+ const o = await r.json().catch(() => null)
2886
+ if (!r.ok) return toolError(`Could not decline ${intake_item_id}: ${o?.error ?? r.status}`)
2887
+ return { content: [{ type: 'text', text: `Noted — arrival ${intake_item_id} will not be offered to THIS session again. It stays pending and still goes to your other matching sessions; once they have all declined it, it comes back to everyone as unclaimed.` }] }
2888
+ }
2840
2889
  let res
2841
2890
  try {
2842
2891
  res = await fetchCortex(`${BASE}/api/nudge/reject`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.165",
3
+ "version": "0.9.167",
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": {