@theronap/cortex-mcp 0.9.49 → 0.9.50
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.
- package/lib/diagnose.mjs +29 -3
- package/lib/server.mjs +41 -9
- package/package.json +1 -1
package/lib/diagnose.mjs
CHANGED
|
@@ -69,7 +69,19 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
69
69
|
export function classify(status, contentType, bodyText, requestId) {
|
|
70
70
|
const isJson = (contentType ?? '').includes('application/json')
|
|
71
71
|
let appError = null
|
|
72
|
-
|
|
72
|
+
let appHint = null
|
|
73
|
+
if (isJson) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(bodyText)
|
|
76
|
+
appError = parsed?.error ?? null
|
|
77
|
+
// `hint` carries the RECOVERY instruction for the errors an agent is meant to act on, not
|
|
78
|
+
// just report: 409 would_drop ("re-author including the dropped sections…") and 413 too_large
|
|
79
|
+
// ("split the section…"). It used to be dropped here — only `error` survived — so the agent
|
|
80
|
+
// saw a bare code like "would_drop_sections" and had no idea what to do with it. The routes
|
|
81
|
+
// were writing careful self-heal guidance that never reached anyone.
|
|
82
|
+
appHint = typeof parsed?.hint === 'string' && parsed.hint.trim() ? parsed.hint.trim() : null
|
|
83
|
+
} catch { /* not json after all */ }
|
|
84
|
+
}
|
|
73
85
|
const rid = requestId ? ` [request id: ${requestId}]` : ''
|
|
74
86
|
|
|
75
87
|
if (status === 401 || (isJson && appError === 'invalid token')) {
|
|
@@ -90,7 +102,7 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
90
102
|
}
|
|
91
103
|
return {
|
|
92
104
|
kind: 'app', retriable: status >= 500,
|
|
93
|
-
message: `Cortex API ${status}: ${appError ?? 'unknown error'}.${rid}`,
|
|
105
|
+
message: `Cortex API ${status}: ${appError ?? 'unknown error'}.${appHint ? ` ${appHint}` : ''}${rid}`,
|
|
94
106
|
}
|
|
95
107
|
}
|
|
96
108
|
|
|
@@ -104,8 +116,22 @@ export function classify(status, contentType, bodyText, requestId) {
|
|
|
104
116
|
// WORSE — 3 unbounded attempts stacked. Now worst case = (retries+1) * timeoutMs + backoff, and a
|
|
105
117
|
// timed-out attempt is treated as transient (retried, then surfaced as the reach error the callers
|
|
106
118
|
// already swallow). Tunable via CORTEX_HTTP_TIMEOUT_MS; per-call override via opts.timeoutMs.
|
|
119
|
+
// ADR-0020 Stage 2 — this process's session identity, stamped on every request so the server can
|
|
120
|
+
// resolve THIS session's write pointer instead of the person-wide one. Set once by the MCP server at
|
|
121
|
+
// startup (setSessionKey); left null in the one-shot hook processes (capture, context_log), which
|
|
122
|
+
// have no session of their own and correctly fall back to the account pointer.
|
|
123
|
+
//
|
|
124
|
+
// Injected HERE rather than at each call site because fetchCortex is the single chokepoint every
|
|
125
|
+
// caller already funnels through — ~30 call sites pass their own `headers` object, and a header this
|
|
126
|
+
// load-bearing must not depend on remembering it at each one.
|
|
127
|
+
let SESSION_KEY = null
|
|
128
|
+
export function setSessionKey(key) { SESSION_KEY = key || null }
|
|
129
|
+
|
|
107
130
|
export async function fetchCortex(url, opts = {}, { retries = 2, baseDelayMs = 400 } = {}) {
|
|
108
|
-
const { timeoutMs: optTimeout, signal: callerSignal, ...
|
|
131
|
+
const { timeoutMs: optTimeout, signal: callerSignal, ...rest } = opts
|
|
132
|
+
const fetchOpts = SESSION_KEY
|
|
133
|
+
? { ...rest, headers: { ...(rest.headers ?? {}), 'x-cortex-session-key': SESSION_KEY } }
|
|
134
|
+
: rest
|
|
109
135
|
const timeoutMs = optTimeout ?? (Number(process.env.CORTEX_HTTP_TIMEOUT_MS) || 15_000)
|
|
110
136
|
let lastErr
|
|
111
137
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
package/lib/server.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { writeFileSync, mkdirSync } from 'fs'
|
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
import { join } from 'path'
|
|
7
7
|
import { createHash, randomUUID } from 'crypto'
|
|
8
|
-
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
8
|
+
import { fetchCortex, classify, resolveBase, setSessionKey } from './diagnose.mjs'
|
|
9
9
|
import { runSendImessage } from './imessage_send.mjs'
|
|
10
10
|
import { formatGrepHits } from './grep_cli.mjs'
|
|
11
11
|
import { renderTriage } from './red_link_triage.mjs'
|
|
@@ -43,6 +43,10 @@ export async function runServer(version) {
|
|
|
43
43
|
// working dir identify this session; we heartbeat /api/session-ping while alive so the user can see
|
|
44
44
|
// all THEIR sessions via my_sessions. Self-only on the server; best-effort (never breaks serving).
|
|
45
45
|
const SESSION_KEY = randomUUID()
|
|
46
|
+
// ADR-0020 Stage 2: stamp this key on every outbound request (fetchCortex injects it) so writes
|
|
47
|
+
// resolve THIS session's brain pointer. Must be set BEFORE the first fetchCortex call below —
|
|
48
|
+
// otherwise the opening requests of a session would silently resolve by the account pointer.
|
|
49
|
+
setSessionKey(SESSION_KEY)
|
|
46
50
|
async function pingSession() {
|
|
47
51
|
try {
|
|
48
52
|
await fetchCortex(`${BASE}/api/session-ping`, {
|
|
@@ -646,10 +650,26 @@ export async function runServer(version) {
|
|
|
646
650
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
647
651
|
return { content: [{ type: 'text', text: `Could not list brains: ${d.message}` }] }
|
|
648
652
|
}
|
|
649
|
-
const { brains, activeIsExplicit } = await res.json()
|
|
653
|
+
const { brains, activeIsExplicit, activeSource, sessionOrgId, accountOrgId } = await res.json()
|
|
650
654
|
if (!brains?.length) return { content: [{ type: 'text', text: 'You have no brains.' }] }
|
|
651
|
-
|
|
652
|
-
|
|
655
|
+
// Show CONTENTS, not just the name (ADR-0020 §7 corollary): a brain named TTO that holds 4
|
|
656
|
+
// pages while the real TTO inventory sits in Personal reads as correct and is exactly
|
|
657
|
+
// backwards. The page count and a couple of titles make that visible at the moment of choosing.
|
|
658
|
+
const lines = brains.map((b) => {
|
|
659
|
+
const inv = b.pageCount === 0
|
|
660
|
+
? 'EMPTY'
|
|
661
|
+
: `${b.pageCount} page${b.pageCount === 1 ? '' : 's'}${b.sampleTitles?.length ? `: ${b.sampleTitles.slice(0, 2).join(', ')}` : ''}`
|
|
662
|
+
return `${b.isActive ? '▶' : ' '} ${b.name} (${b.role}${b.status !== 'active' ? `, ${b.status}` : ''}) — ${inv} [${b.orgId}]`
|
|
663
|
+
})
|
|
664
|
+
// Which layer is deciding, and what clearing it would fall back to — otherwise the pointer
|
|
665
|
+
// confusion simply reappears one level down.
|
|
666
|
+
const note = !activeIsExplicit
|
|
667
|
+
? '\n(active brain is the default — you have one brain; set_active_brain once you hold more.)'
|
|
668
|
+
: activeSource === 'session'
|
|
669
|
+
? `\n(this SESSION's override — your other sessions are unaffected${accountOrgId && accountOrgId !== sessionOrgId ? `; clearing it falls back to ${accountOrgId}` : ''}.)`
|
|
670
|
+
: activeSource === 'account'
|
|
671
|
+
? '\n(account-wide pointer — shared by every session that has not set its own.)'
|
|
672
|
+
: ''
|
|
653
673
|
return { content: [{ type: 'text', text: `Your brains (▶ = writes land here):\n${lines.join('\n')}${note}` }] }
|
|
654
674
|
},
|
|
655
675
|
)
|
|
@@ -658,16 +678,20 @@ export async function runServer(version) {
|
|
|
658
678
|
'set_active_brain',
|
|
659
679
|
{
|
|
660
680
|
title: 'Choose the brain your writes go to',
|
|
661
|
-
description:
|
|
662
|
-
inputSchema: {
|
|
681
|
+
description: "Point your writes (author / log_session / capture) at one of your brains, by its org id (from my_brains). Pass org_id = null to clear it. You can only select a brain you are a member of. SCOPE: 'session' (default) changes ONLY this session — other windows you have open keep writing where they were, which is almost always what you want. 'account' changes the sticky person-wide pointer, which redirects every other session that has not set its own.",
|
|
682
|
+
inputSchema: {
|
|
683
|
+
org_id: z.string().nullable().describe('the org id of the brain to write to (from my_brains), or null to clear the pointer'),
|
|
684
|
+
scope: z.enum(['session', 'account']).optional()
|
|
685
|
+
.describe("'session' (default) = this window only; 'account' = person-wide, affects your other open sessions too"),
|
|
686
|
+
},
|
|
663
687
|
},
|
|
664
|
-
async ({ org_id }) => {
|
|
688
|
+
async ({ org_id, scope }) => {
|
|
665
689
|
let res
|
|
666
690
|
try {
|
|
667
691
|
res = await fetchCortex(`${BASE}/api/brains`, {
|
|
668
692
|
method: 'PUT',
|
|
669
693
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
670
|
-
body: JSON.stringify({ orgId: org_id ?? null }),
|
|
694
|
+
body: JSON.stringify({ orgId: org_id ?? null, scope: scope ?? 'session' }),
|
|
671
695
|
})
|
|
672
696
|
} catch (e) {
|
|
673
697
|
return { content: [{ type: 'text', text: `Could not set active brain: ${e.message}` }] }
|
|
@@ -677,7 +701,15 @@ export async function runServer(version) {
|
|
|
677
701
|
return { content: [{ type: 'text', text: `Could not set active brain: ${d.message}` }] }
|
|
678
702
|
}
|
|
679
703
|
const r = await res.json()
|
|
680
|
-
|
|
704
|
+
// Always say WHICH scope changed. The default is session-only, so a caller expecting the old
|
|
705
|
+
// person-wide stickiness must be able to see that it did NOT change their other windows.
|
|
706
|
+
const where = r.scope === 'session' ? 'this session only' : 'ALL your sessions (person-wide)'
|
|
707
|
+
if (!r.activeOrgId) {
|
|
708
|
+
return { content: [{ type: 'text', text: r.scope === 'session'
|
|
709
|
+
? 'Cleared this session\'s brain override — writes fall back to your account pointer.'
|
|
710
|
+
: 'Cleared your account-wide active-brain pointer (writes fall back to your default brain).' }] }
|
|
711
|
+
}
|
|
712
|
+
return { content: [{ type: 'text', text: `Writes now land in brain ${r.activeOrgId} — ${where}.` }] }
|
|
681
713
|
},
|
|
682
714
|
)
|
|
683
715
|
|