@theronap/agnoclast-mcp 0.9.166 → 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 +49 -20
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -291,40 +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
- // ⚠ NAME THE EXACT CALL, INCLUDING THE ARRAY. This line used to read "Then `intake_claim` and
321
- // `intake_materialize`" straight after a list of ids, which reads as `intake_claim <id>`. It is not:
322
- // the parameter is `intakeItemIds`, an ARRAY, and on 2026-09-22 following this very sentence took a
323
- // lease on ten unrelated units. The singular is accepted now, but an offer must still state the call
324
- // that acts on the thing being offered — a reader should not have to consult a schema to use a nudge.
325
- `— 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.` +
326
- more + who +
327
- `\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
328
327
  }
329
328
 
330
329
  function renderNudge(payload) {
@@ -2852,11 +2851,41 @@ function renderNudge(payload) {
2852
2851
  title: 'Decline a nudged record — it is not this session\'s business',
2853
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.',
2854
2853
  inputSchema: {
2855
- 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)'),
2856
2861
  reason: z.string().describe('WHY it is not yours — recorded, and the only thing that can later answer "why did nobody take this"'),
2857
2862
  },
2858
2863
  },
2859
- 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
+ }
2860
2889
  let res
2861
2890
  try {
2862
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.166",
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": {