@theronap/agnoclast-mcp 0.9.195 → 0.9.196

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 +30 -6
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -555,15 +555,20 @@ function renderNudge(payload) {
555
555
  summary: z.string().describe('a summary that stands ON ITS OWN. It must not mention, allude to, or imply that other segments exist — "the rest of this session covered personal projects" leaks in prose exactly what splitting was meant to contain'),
556
556
  title: z.string().optional(),
557
557
  project: z.string().optional(),
558
+ pages: z.array(z.string()).optional().describe('pages IN THAT BRAIN this half belongs on — each a page `ref:` as read_page shows it'),
558
559
  })).optional().describe('SPLIT the log, one entry per brain. Use whenever a session touched work belonging to different brains: a session is a container of time, not a topic, and every single-brain answer is wrong — filing it all in the org brain exposes personal work to colleagues, filing it all in the personal one denies the org its record, and summarizing half silently drops the other half. Max ONE segment per brain (they share this session\'s dedupe key, so two aimed at the same brain would overwrite each other). When a chunk is ambiguous, put it in the MORE PRIVATE brain — a misfile there is private, a misfile the other way is visible to everyone in the org.'),
559
560
  project: z.string().optional().describe('project key/name this session worked in'),
560
561
  title: z.string().optional().describe('short title for the session'),
562
+ pages: z.array(z.string()).optional().describe('pages this log belongs on — each a page `ref:` as read_page shows it (the project it was about, the decision it bears on). The pages this session WROTE are attached automatically; name the ones it was ABOUT but did not edit. REQUIRED IN EFFECT when you log on behalf of a DIFFERENT session (an explicit sessionId): only a session filing its own log is let through without them. The single-brain form; with `segments`, put pages on each segment'),
561
563
  sessionId: z.string().optional().describe('the Claude Code session id — DEFAULTS to the current session, so omit it unless you are logging on behalf of another one. It is shared by every segment and the only thing pairing them, and it is also the join to the pages this session wrote (page_revisions.session_key); without it the record is unattributable forever, since the fallback dedupe key is a timestamp. Deliberately NOT a link: segments never reference each other, so a reader cleared for one brain cannot tell the others exist, while you can join on it across brains'),
562
564
  brain: z.string().optional().describe('which brain to record this session in (name or org id, one of your own). REQUIRED IN EFFECT for a multi-brain member: session-class sources route only by an explicit brain or a sole membership, so omitting it stages the log instead of recording it.'),
563
565
  privacy: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('tier this record AT WRITE TIME. A record is normally read by whoever can read the page it is filed under. Pass "confidential" when the summary names confidential work (a candidate evaluation, a security finding) so it is yours alone from the start — safer than letting it land page-visible and locking it after, which leaves it readable in between. Pass "scoped" to lock it to you and your managers. OMIT it for ordinary work: the record then follows its page, which is what makes it visible to the people working on that page with you.'),
564
566
  },
565
567
  },
566
- async ({ summary, segments, project, title, sessionId, brain, privacy }) => {
568
+ async ({ summary, segments, project, title, sessionId, brain, privacy, pages }) => {
569
+ // read_page prints `ref: <uuid>`; a caller who pastes the label with it must not get a 400.
570
+ const pageRefs = (list) => (Array.isArray(list) ? list : [])
571
+ .map((r) => String(r).trim().replace(/^ref:\s*/i, '')).filter(Boolean)
567
572
  // Default the session id from the environment when the caller omits it.
568
573
  //
569
574
  // `sessionId` is optional and the model routinely does not pass it, so the record lands with a
@@ -588,8 +593,8 @@ function renderNudge(payload) {
588
593
  // A session is a container of TIME, not a topic (ADR-0029 step 4). Normalize to a list of
589
594
  // segments; the single-brain call is just a one-segment list.
590
595
  const list = Array.isArray(segments) && segments.length
591
- ? segments.map((s) => ({ brain: s.brain, summary: s.summary, title: s.title ?? title, project: s.project ?? project }))
592
- : (summary ? [{ brain, summary, title, project }] : null)
596
+ ? segments.map((s) => ({ brain: s.brain, summary: s.summary, title: s.title ?? title, project: s.project ?? project, pages: pageRefs(s.pages) }))
597
+ : (summary ? [{ brain, summary, title, project, pages: pageRefs(pages) }] : null)
593
598
  if (!list) {
594
599
  return toolError('Pass `summary` (single brain) or a non-empty `segments` array (one entry per brain).')
595
600
  }
@@ -722,6 +727,12 @@ function renderNudge(payload) {
722
727
  body: JSON.stringify({
723
728
  intakeItemId: j.intakeItemId,
724
729
  orgId,
730
+ // Pages the caller named. The pages this session WROTE need no naming: the server
731
+ // attaches those by session key. Sent only when given, so a plain close-out keeps
732
+ // the exact body it always had.
733
+ ...(seg.pages.length
734
+ ? { documentIds: seg.pages, attachmentMeta: seg.pages.map(() => ({ origin: 'session' })) }
735
+ : {}),
725
736
  record: {
726
737
  title: seg.title,
727
738
  summary: seg.summary,
@@ -732,16 +743,29 @@ function renderNudge(payload) {
732
743
  })
733
744
  } catch (e) {
734
745
  failures++
735
- lines.push(`✗ ${where}: intake ${j.intakeItemId} NOT materialized — ${e.message}`)
746
+ lines.push(`✗ ${where}: the curated log was NOT written (intake ${j.intakeItemId}) — ${e.message}`)
736
747
  continue
737
748
  }
738
749
  const mj = await m.json().catch(() => ({}))
739
750
  if (!m.ok || !mj.recordId) {
740
751
  failures++
741
- lines.push(`✗ ${where}: intake ${j.intakeItemId} NOT materialized — ${mj.error ?? m.status}`)
752
+ // 🔴 THE OLD WORDING ("not materialized") WAS A LIE HALF THE TIME (2026-10-02). When this session's unit is
753
+ // already a record — filed mid-session — the unit IS materialized; what failed is writing
754
+ // the curated summary onto it. Say what did not happen, and what would make it happen.
755
+ //
756
+ // The other-homes refusal (ADR-0051) is answerable from here: name a page. A server that
757
+ // exempts a session's own close-out never sends it for one; it still does when logging on
758
+ // behalf of ANOTHER session, which is a judgment about someone else's work.
759
+ const why = mj.error === 'other_homes_unchecked'
760
+ ? 'the server asks what pages this log belongs on. Re-run with pages:["<page ref>", …] — the project it was about, the decision it bears on (read_page prints each page\'s ref)'
761
+ : (mj.error ?? m.status)
762
+ lines.push(`✗ ${where}: the curated log was NOT written (intake ${j.intakeItemId}) — ${why}`)
742
763
  continue
743
764
  }
744
- lines.push(`✓ ${where}: record ${mj.recordId}`)
765
+ // `duplicate` with `updated: false` is ingest saying this session's unit had already been
766
+ // acted on — so the record existed and this call replaced its title and summary.
767
+ const amended = j.duplicate === true && j.updated === false
768
+ lines.push(`✓ ${where}: record ${mj.recordId}${amended ? ' (replaced the summary on this session\'s existing record)' : ''}`)
745
769
  continue
746
770
  }
747
771
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.195",
3
+ "version": "0.9.196",
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": {