@theronap/cortex-mcp 0.9.110 → 0.9.112

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 +41 -6
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -859,7 +859,35 @@ export async function runServer(version) {
859
859
  const body = await res.text()
860
860
  return toolError(body)
861
861
  }
862
- return { content: [{ type: 'text', text: JSON.stringify(await res.json(), null, 2) }] }
862
+ const body = await res.json()
863
+ // WHERE THE SESSION'S OWN WORK LANDED, in words rather than buried in the JSON dump.
864
+ //
865
+ // Three outcomes that look identical in raw JSON and mean completely different things:
866
+ // null the attach THREW — a real failure, and best-effort swallowed it
867
+ // attached: [] the session authored no page in this brain — the ~85% normal case
868
+ // attached: [...] filed under N pages
869
+ //
870
+ // Reporting only a count would make the first indistinguishable from the second, which is how
871
+ // a broken write hides inside a normal-looking result. `undefined` means the server has not
872
+ // shipped this field yet, so nothing is said rather than something wrong.
873
+ const lines = []
874
+ const sa = body?.sessionAttachments
875
+ if (sa === null) {
876
+ lines.push('⚠ Session-page attachment FAILED for this record — it was written, but not filed under the pages this session wrote. Re-run the reconciler.')
877
+ } else if (sa && Array.isArray(sa.attached)) {
878
+ if (sa.attached.length > 0) {
879
+ lines.push(`Filed under ${sa.attached.length} page(s) this session wrote.`)
880
+ } else {
881
+ lines.push('No pages filed: this session authored none in this brain (the normal case).')
882
+ }
883
+ if (sa.droppedCrossBrain > 0) {
884
+ lines.push(`${sa.droppedCrossBrain} page(s) this session wrote live in another brain and were skipped — a record belongs to one brain.`)
885
+ }
886
+ }
887
+ const text = lines.length
888
+ ? `${lines.join('\n')}\n\n${JSON.stringify(body, null, 2)}`
889
+ : JSON.stringify(body, null, 2)
890
+ return { content: [{ type: 'text', text }] }
863
891
  },
864
892
  )
865
893
 
@@ -1181,7 +1209,10 @@ export async function runServer(version) {
1181
1209
  // drift again.
1182
1210
  const secs = (t.sections ?? []).map((s) => renderSection(s, day)).join('\n\n')
1183
1211
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}]`
1184
- return [head, t.summary, secs].filter(Boolean).join('\n')
1212
+ // ADR-0030 step 3b (2026-08-24): the summary header is NO LONGER RENDERED. It duplicated the
1213
+ // `In short` section byte-for-byte on 6 of 6 pages sampled, so every read paid for the same
1214
+ // text twice. The column stays vestigial; this just stops serving it.
1215
+ return [head, secs].filter(Boolean).join('\n')
1185
1216
  })
1186
1217
  const brainTag = matches.length > 1 ? ` · brain: ${m.brain}` : ''
1187
1218
  return `# ${m.title ?? key} (authored page${brainTag})\n\n${blocks.join('\n\n---\n\n')}`
@@ -1336,7 +1367,11 @@ export async function runServer(version) {
1336
1367
  ? `\nversion: ${t.version}`
1337
1368
  : `\nversion: none (this variant predates content-hash tracking — base_version writes will always fail here; ask an admin about the ADR-0018 backfill)`
1338
1369
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${versionLine}]`
1339
- return [head, t.summary, secs].filter(Boolean).join('\n')
1370
+ // ADR-0030 step 3b (2026-08-24): the summary header is NO LONGER RENDERED here either.
1371
+ // ⚠ There were TWO of these — project_status and read_page — and the first patch fixed only
1372
+ // project_status, leaving the primary surface untouched. Same granularity miss this repo
1373
+ // keeps recording: a fix applied to the occurrence you happened to find first.
1374
+ return [head, secs].filter(Boolean).join('\n')
1340
1375
  })
1341
1376
  // REPAIR TOOL (2026-07-31). This footer used to say "re-author just those sections with
1342
1377
  // `author`" — which `author` cannot do: computeDroppedSections rejects a partial-section
@@ -1345,7 +1380,7 @@ export async function runServer(version) {
1345
1380
  // retype silently deleted a sentence, a [[link]] (a graph edge), a command list and the
1346
1381
  // word "today" from sections it was never meant to touch. This footer renders on EVERY
1347
1382
  // page read in the system, so it was the single widest surface pointing the wrong way.
1348
- let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — fix just that passage with \`edit_page\`: quote the wrong text as old_string and pass this page's \`version\` as base_version (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). If the save comes back stale the page changed under you — the conflict hands back what changed, so re-anchor from that instead of re-reading. Reading a stale page you can fix IS the trigger to fix it. Editing is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it. If what went false is the SUMMARY at the top rather than a section, use \`set_summary\` — \`edit_page\` cannot reach it, and worse, it carries the old summary FORWARD into every revision it writes, so fixing a section silently re-certifies a summary that already contradicts it. Use \`author\` only to CREATE a page or rewrite one wholesale: it re-emits every section, so untouched sections get retyped on the way through and drift.\n— Citing code? Use a SYMBOL and file (\`formConnections\` in \`web/app/api/ingest/route.ts\`), never a line number — line numbers drift with every commit above them. And cite only what you opened THIS session; re-emitting a reference you read on another page is how a stale claim gains a second source and starts looking corroborated.`
1383
+ let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — fix just that passage with \`edit_page\`: quote the wrong text as old_string and pass this page's \`version\` as base_version (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). If the save comes back stale the page changed under you — the conflict hands back what changed, so re-anchor from that instead of re-reading. Reading a stale page you can fix IS the trigger to fix it. Editing is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it. Use \`author\` only to CREATE a page or rewrite one wholesale: it re-emits every section, so untouched sections get retyped on the way through and drift.\n— Citing code? Use a SYMBOL and file (\`formConnections\` in \`web/app/api/ingest/route.ts\`), never a line number — line numbers drift with every commit above them. And cite only what you opened THIS session; re-emitting a reference you read on another page is how a stale claim gains a second source and starts looking corroborated.`
1349
1384
  // ADDRESSING (2026-08-04). The server accepts `ref` on every page/node route and its ambiguity
1350
1385
  // 409s hand refs back — but read_page never PRINTED one, so the only way to obtain a ref was to
1351
1386
  // trigger the error first. That made ID addressing reachable in principle and unusable in
@@ -1654,7 +1689,7 @@ export async function runServer(version) {
1654
1689
  'set_summary',
1655
1690
  {
1656
1691
  title: 'Rewrite a wiki page summary without touching its sections',
1657
- description: 'Replace a page\'s SUMMARY the one-sentence line at the top — in place, leaving every section untouched. Use this the moment you notice a summary that no longer matches the page: `edit_page` structurally CANNOT reach it (it edits section bodies; the summary is not one), and `author` reaches it only by retyping every section on the way through, which is how a 2026-07-30 write silently deleted a sentence and a [[link]] from a section nobody meant to touch. Correcting a body with `edit_page` actively carries the OLD summary forward into the new revision, so a stale summary does not decay quietly — each unrelated fix re-certifies it. Fixing it matters more than it looks: page retrieval FTS-matches on title + summary ALONE — the tsvector is built over title and summary, and section bodies are not in that index — so a stale summary decides whether the page is found by that path at all, and it is the first line every reader sees before a word of the body. Requires base_version (the `version` read_page prints) that is also how it finds the right brain, so it can never write to the wrong one. Recorded in page_history as a correction, and reversible with rollback_page.',
1692
+ description: 'DEPRECATED retired by ADR-0030 (decided 2026-08-24: no part of a page summarises the page). Do not call it. It still works so seats on older clients do not break, and it will be removed in a later release. This description previously claimed page retrieval matches on title + summary ALONE and that a stale summary decides whether a page is found that was true only of the digest branch deleted by ADR-0030 step 2, and on 2026-08-24 a session read it, believed it, and asserted the opposite of the truth to its operator. `grep` searches SECTION BODIES and always has. Put the content in a section.',
1658
1693
  inputSchema: {
1659
1694
  name: z.string().describe('the exact page name, as read_page shows it'),
1660
1695
  summary: z.string().describe('the new summary: one sentence saying what this is and where it stands. May contain [[links]]. Max 2,000 chars — detail belongs in a section.'),
@@ -3147,7 +3182,7 @@ export async function runServer(version) {
3147
3182
  inputSchema: {
3148
3183
  kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
3149
3184
  name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "Agnoclast" or "Theron Peterson"'),
3150
- summary: z.string().describe('one-sentence summary of what this is and its current state (may contain [[links]])'),
3185
+ summary: z.string().optional().describe('DEPRECATED and no longer required (ADR-0030, decided 2026-08-24: no part of a page summarises the page). Omit it. Accepted only so older callers keep working; the column is vestigial and nothing reads it. Put what the page is in its FIRST SECTION grep indexes section bodies, and never indexed this.'),
3151
3186
  sections: z.array(z.object({
3152
3187
  heading: z.string().describe('e.g. Overview, Current state, Decisions, Open threads, People'),
3153
3188
  body: z.string().describe('dense markdown WITH inline [[links]] where the prose references another node'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.110",
3
+ "version": "0.9.112",
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": {