@j0hanz/cortex-mcp 1.6.0 → 1.7.0

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.
@@ -1,5 +1,4 @@
1
1
  export interface EngineContext {
2
2
  readonly sessionId: string;
3
- readonly abortSignal?: AbortSignal;
4
3
  }
5
4
  export declare function runWithContext<T>(ctx: EngineContext, fn: () => T): T;
@@ -26,6 +26,12 @@ interface EngineEvents {
26
26
  sessionId: string;
27
27
  level: ReasoningLevel;
28
28
  }];
29
+ 'session:completed': [{
30
+ sessionId: string;
31
+ }];
32
+ 'session:cancelled': [{
33
+ sessionId: string;
34
+ }];
29
35
  'session:expired': [{
30
36
  sessionId: string;
31
37
  }];
@@ -20,7 +20,7 @@ export async function reason(query, level, options) {
20
20
  const session = resolveSession(level, sessionId, query, targetThoughts);
21
21
  const config = getLevelConfig(session.level);
22
22
  const { totalThoughts } = session;
23
- return runWithContext({ sessionId: session.id, ...(abortSignal ? { abortSignal } : {}) }, () => withSessionLock(session.id, async () => {
23
+ return runWithContext({ sessionId: session.id }, () => withSessionLock(session.id, async () => {
24
24
  throwIfReasoningAborted(abortSignal);
25
25
  if (rollbackToStep !== undefined) {
26
26
  sessionStore.rollback(session.id, rollbackToStep);
@@ -53,7 +53,7 @@ export async function reason(query, level, options) {
53
53
  content: addedThought.content,
54
54
  });
55
55
  const updated = getSessionOrThrow(session.id);
56
- void emitBudgetExhaustedIfNeeded({
56
+ emitBudgetExhaustedIfNeeded({
57
57
  session: updated,
58
58
  tokenBudget: config.tokenBudget,
59
59
  generatedThoughts: addedThought.index + 1,
@@ -129,7 +129,7 @@ function resolveSession(level, sessionId, query, targetThoughts) {
129
129
  }
130
130
  const config = getLevelConfig(level);
131
131
  const totalThoughts = resolveThoughtCount(level, query, config, targetThoughts);
132
- const session = sessionStore.create(level, totalThoughts);
132
+ const session = sessionStore.create(level, totalThoughts, query);
133
133
  engineEvents.emit('session:created', {
134
134
  sessionId: session.id,
135
135
  level,
@@ -16,7 +16,7 @@ export declare class SessionStore {
16
16
  constructor(ttlMs?: number, maxSessions?: number, maxTotalTokens?: number);
17
17
  ensureCleanupTimer(): void;
18
18
  dispose(): void;
19
- create(level: ReasoningLevel, totalThoughts?: number): Readonly<Session>;
19
+ create(level: ReasoningLevel, totalThoughts?: number, query?: string): Readonly<Session>;
20
20
  get(id: string): Readonly<Session> | undefined;
21
21
  getSummary(id: string): Readonly<SessionSummary> | undefined;
22
22
  list(): Readonly<Session>[];
@@ -28,7 +28,7 @@ export declare class SessionStore {
28
28
  delete(id: string): boolean;
29
29
  addThought(sessionId: string, content: string, stepSummary?: string): Thought;
30
30
  rollback(sessionId: string, toIndex: number): void;
31
- reviseThought(sessionId: string, thoughtIndex: number, content: string): Thought;
31
+ reviseThought(sessionId: string, thoughtIndex: number, content: string, stepSummary?: string): Thought;
32
32
  markCompleted(sessionId: string): void;
33
33
  markCancelled(sessionId: string): void;
34
34
  private updateSessionStatus;
@@ -49,9 +49,9 @@ export declare class SessionStore {
49
49
  private snapshotThought;
50
50
  private snapshotSession;
51
51
  private snapshotSessionSummary;
52
- private emitSessionsListChanged;
53
- private emitSessionsResourceUpdated;
54
- private emitSessionsCollectionUpdated;
52
+ private emitListChanged;
53
+ private emitListResourceUpdated;
54
+ private emitCollectionUpdated;
55
55
  private emitSessionEvicted;
56
- private emitSessionResourcesUpdated;
56
+ private emitSessionResourceUpdated;
57
57
  }
@@ -1,11 +1,12 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { SessionNotFoundError } from '../lib/errors.js';
3
4
  import { getLevelConfig } from './config.js';
4
5
  import { engineEvents } from './events.js';
5
6
  export const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
6
7
  export const DEFAULT_MAX_SESSIONS = 100;
7
8
  export const DEFAULT_MAX_TOTAL_TOKENS = 500_000;
8
- const TOKEN_ESTIMATE_DIVISOR = 4;
9
+ const TOKEN_ESTIMATE_DIVISOR = 3.5;
9
10
  const MIN_SWEEP_INTERVAL_MS = 10;
10
11
  const MAX_SWEEP_INTERVAL_MS = 60_000;
11
12
  function estimateTokens(text) {
@@ -52,7 +53,7 @@ export class SessionStore {
52
53
  clearInterval(this.cleanupInterval);
53
54
  this.cleanupInterval = undefined;
54
55
  }
55
- create(level, totalThoughts) {
56
+ create(level, totalThoughts, query) {
56
57
  this.evictIfAtCapacity();
57
58
  const config = getLevelConfig(level);
58
59
  const now = Date.now();
@@ -66,12 +67,13 @@ export class SessionStore {
66
67
  tokensUsed: 0,
67
68
  createdAt: now,
68
69
  updatedAt: now,
70
+ ...(query !== undefined ? { query } : {}),
69
71
  };
70
72
  this.sessions.set(session.id, session);
71
73
  this.addToOrder(session.id);
72
74
  this.sortedSessionIdsCache = null;
73
- this.emitSessionsListChanged();
74
- this.emitSessionsResourceUpdated();
75
+ this.emitListChanged();
76
+ this.emitListResourceUpdated();
75
77
  return this.snapshotSession(session);
76
78
  }
77
79
  get(id) {
@@ -111,13 +113,13 @@ export class SessionStore {
111
113
  return false;
112
114
  }
113
115
  engineEvents.emit('session:deleted', { sessionId: id });
114
- this.emitSessionsCollectionUpdated();
116
+ this.emitCollectionUpdated();
115
117
  return true;
116
118
  }
117
119
  addThought(sessionId, content, stepSummary) {
118
120
  const session = this.sessions.get(sessionId);
119
121
  if (!session) {
120
- throw new Error(`Session not found: ${sessionId}`);
122
+ throw new SessionNotFoundError(sessionId);
121
123
  }
122
124
  const tokens = estimateTokens(content);
123
125
  this.evictForTokenHeadroom(tokens, sessionId);
@@ -137,7 +139,7 @@ export class SessionStore {
137
139
  rollback(sessionId, toIndex) {
138
140
  const session = this.sessions.get(sessionId);
139
141
  if (!session) {
140
- throw new Error(`Session not found: ${sessionId}`);
142
+ throw new SessionNotFoundError(sessionId);
141
143
  }
142
144
  // If toIndex is out of bounds or implies no change, return.
143
145
  // We keep thoughts up to and including toIndex.
@@ -154,10 +156,10 @@ export class SessionStore {
154
156
  this.totalTokens -= removedTokens;
155
157
  this.markSessionTouched(session);
156
158
  }
157
- reviseThought(sessionId, thoughtIndex, content) {
159
+ reviseThought(sessionId, thoughtIndex, content, stepSummary) {
158
160
  const session = this.sessions.get(sessionId);
159
161
  if (!session) {
160
- throw new Error(`Session not found: ${sessionId}`);
162
+ throw new SessionNotFoundError(sessionId);
161
163
  }
162
164
  const existing = session.thoughts[thoughtIndex];
163
165
  if (!existing) {
@@ -169,13 +171,14 @@ export class SessionStore {
169
171
  if (delta > 0) {
170
172
  this.evictForTokenHeadroom(delta, sessionId);
171
173
  }
174
+ const effectiveStepSummary = stepSummary ?? existing.stepSummary;
172
175
  const revised = {
173
176
  index: thoughtIndex,
174
177
  content,
175
178
  revision: existing.revision + 1,
176
179
  tokenCount: newTokens,
177
- ...(existing.stepSummary !== undefined
178
- ? { stepSummary: existing.stepSummary }
180
+ ...(effectiveStepSummary !== undefined
181
+ ? { stepSummary: effectiveStepSummary }
179
182
  : {}),
180
183
  };
181
184
  session.thoughts[thoughtIndex] = revised;
@@ -202,6 +205,12 @@ export class SessionStore {
202
205
  if (session?.status === 'active') {
203
206
  session.status = status;
204
207
  this.markSessionTouched(session);
208
+ if (status === 'completed') {
209
+ engineEvents.emit('session:completed', { sessionId });
210
+ }
211
+ else {
212
+ engineEvents.emit('session:cancelled', { sessionId });
213
+ }
205
214
  }
206
215
  }
207
216
  evictIfAtCapacity() {
@@ -254,7 +263,7 @@ export class SessionStore {
254
263
  currentId = nextId;
255
264
  }
256
265
  if (changed) {
257
- this.emitSessionsCollectionUpdated();
266
+ this.emitCollectionUpdated();
258
267
  }
259
268
  }
260
269
  buildSortedSessionIdsCache() {
@@ -362,7 +371,7 @@ export class SessionStore {
362
371
  session._cachedSummary = undefined;
363
372
  this.touchOrder(session.id);
364
373
  this.sortedSessionIdsCache = null;
365
- this.emitSessionResourcesUpdated(session.id);
374
+ this.emitSessionResourceUpdated(session.id);
366
375
  }
367
376
  getSessionIdsForIteration() {
368
377
  this.sortedSessionIdsCache ??= this.buildSortedSessionIdsCache();
@@ -405,6 +414,7 @@ export class SessionStore {
405
414
  tokensUsed: session.tokensUsed,
406
415
  createdAt: session.createdAt,
407
416
  updatedAt: session.updatedAt,
417
+ ...(session.query !== undefined ? { query: session.query } : {}),
408
418
  };
409
419
  Object.freeze(snapshot);
410
420
  Object.freeze(snapshot.thoughts);
@@ -430,27 +440,27 @@ export class SessionStore {
430
440
  session._cachedSummary = summary;
431
441
  return summary;
432
442
  }
433
- emitSessionsListChanged() {
443
+ emitListChanged() {
434
444
  engineEvents.emit('resources:changed', { uri: 'reasoning://sessions' });
435
445
  }
436
- emitSessionsResourceUpdated() {
446
+ emitListResourceUpdated() {
437
447
  engineEvents.emit('resource:updated', { uri: 'reasoning://sessions' });
438
448
  }
439
- emitSessionsCollectionUpdated() {
440
- this.emitSessionsListChanged();
441
- this.emitSessionsResourceUpdated();
449
+ emitCollectionUpdated() {
450
+ this.emitListChanged();
451
+ this.emitListResourceUpdated();
442
452
  }
443
453
  emitSessionEvicted(sessionId, reason) {
444
454
  engineEvents.emit('session:evicted', {
445
455
  sessionId,
446
456
  reason,
447
457
  });
448
- this.emitSessionsCollectionUpdated();
458
+ this.emitCollectionUpdated();
449
459
  }
450
- emitSessionResourcesUpdated(sessionId) {
460
+ emitSessionResourceUpdated(sessionId) {
451
461
  engineEvents.emit('resource:updated', {
452
462
  uri: `reasoning://sessions/${sessionId}`,
453
463
  });
454
- this.emitSessionsResourceUpdated();
464
+ this.emitListResourceUpdated();
455
465
  }
456
466
  }
@@ -79,7 +79,7 @@ const TOOL_CONTRACTS = [
79
79
  constraints: 'optional',
80
80
  },
81
81
  ],
82
- outputShape: '{ok, result: {sessionId, level, status, thoughts[], generatedThoughts, requestedThoughts, totalThoughts, remainingThoughts, tokenBudget, tokensUsed, ttlMs, expiresAt, createdAt, updatedAt, summary}}',
82
+ outputShape: '{ok, result: {sessionId, query?, level, status, thoughts[], generatedThoughts, requestedThoughts, totalThoughts, remainingThoughts, tokenBudget, tokensUsed, ttlMs, expiresAt, createdAt, updatedAt, summary}}',
83
83
  },
84
84
  ];
85
85
  export function getToolContracts() {
@@ -40,6 +40,7 @@ export interface Thought {
40
40
  }
41
41
  export interface Session extends SessionBase {
42
42
  readonly thoughts: readonly Thought[];
43
+ readonly query?: string;
43
44
  }
44
45
  export interface SessionSummary extends SessionBase {
45
46
  readonly generatedThoughts: number;
@@ -3,6 +3,7 @@ declare const ReasoningThinkSuccessSchema: z.ZodObject<{
3
3
  ok: z.ZodLiteral<true>;
4
4
  result: z.ZodObject<{
5
5
  sessionId: z.ZodString;
6
+ query: z.ZodOptional<z.ZodString>;
6
7
  level: z.ZodEnum<{
7
8
  basic: "basic";
8
9
  normal: "normal";
@@ -40,6 +41,7 @@ export declare const ReasoningThinkToolOutputSchema: z.ZodObject<{
40
41
  ok: z.ZodBoolean;
41
42
  result: z.ZodOptional<z.ZodObject<{
42
43
  sessionId: z.ZodString;
44
+ query: z.ZodOptional<z.ZodString>;
43
45
  level: z.ZodEnum<{
44
46
  basic: "basic";
45
47
  normal: "normal";
@@ -90,6 +92,7 @@ export declare const ReasoningThinkResultSchema: z.ZodUnion<readonly [z.ZodObjec
90
92
  ok: z.ZodLiteral<true>;
91
93
  result: z.ZodObject<{
92
94
  sessionId: z.ZodString;
95
+ query: z.ZodOptional<z.ZodString>;
93
96
  level: z.ZodEnum<{
94
97
  basic: "basic";
95
98
  normal: "normal";
@@ -19,6 +19,10 @@ const ReasoningThinkSuccessSchema = z.strictObject({
19
19
  ok: z.literal(true),
20
20
  result: z.strictObject({
21
21
  sessionId: z.string(),
22
+ query: z
23
+ .string()
24
+ .optional()
25
+ .describe('Original query text for this session.'),
22
26
  level: z.enum(REASONING_LEVELS),
23
27
  status: z.enum(SESSION_STATUSES),
24
28
  thoughts: z.array(ThoughtSchema),
package/dist/server.js CHANGED
@@ -121,9 +121,22 @@ function attachEngineEventHandlers(server) {
121
121
  process.stderr.write(`[cortex-mcp.server] Failed to log budget_exhausted: ${getErrorMessage(err)}\n`);
122
122
  });
123
123
  };
124
+ const onSessionLifecycle = (data) => {
125
+ void server.server.sendResourceListChanged().catch((err) => {
126
+ logNotificationFailure(RESOURCE_LIST_CHANGED_METHOD, err, {
127
+ sessionId: data.sessionId,
128
+ });
129
+ });
130
+ };
124
131
  engineEvents.on('resources:changed', onResourcesChanged);
125
132
  engineEvents.on('resource:updated', onResourceUpdated);
126
133
  engineEvents.on('thought:budget-exhausted', onBudgetExhausted);
134
+ engineEvents.on('session:created', onSessionLifecycle);
135
+ engineEvents.on('session:completed', onSessionLifecycle);
136
+ engineEvents.on('session:cancelled', onSessionLifecycle);
137
+ engineEvents.on('session:expired', onSessionLifecycle);
138
+ engineEvents.on('session:evicted', onSessionLifecycle);
139
+ engineEvents.on('session:deleted', onSessionLifecycle);
127
140
  let detached = false;
128
141
  return () => {
129
142
  if (detached) {
@@ -133,6 +146,12 @@ function attachEngineEventHandlers(server) {
133
146
  engineEvents.off('resources:changed', onResourcesChanged);
134
147
  engineEvents.off('resource:updated', onResourceUpdated);
135
148
  engineEvents.off('thought:budget-exhausted', onBudgetExhausted);
149
+ engineEvents.off('session:created', onSessionLifecycle);
150
+ engineEvents.off('session:completed', onSessionLifecycle);
151
+ engineEvents.off('session:cancelled', onSessionLifecycle);
152
+ engineEvents.off('session:expired', onSessionLifecycle);
153
+ engineEvents.off('session:evicted', onSessionLifecycle);
154
+ engineEvents.off('session:deleted', onSessionLifecycle);
136
155
  };
137
156
  }
138
157
  function installCloseCleanup(server, cleanup) {
@@ -27,7 +27,7 @@ function buildTraceResource(session) {
27
27
  }
28
28
  : session;
29
29
  return {
30
- uri: `file:///cortex/sessions/${session.id}/trace.md`,
30
+ uri: `reasoning://sessions/${session.id}/trace.md`,
31
31
  mimeType: 'text/markdown',
32
32
  text: formatThoughtsToMarkdown(sessionView),
33
33
  };
@@ -86,7 +86,7 @@ function isReasoningTaskExtra(value) {
86
86
  }
87
87
  return true;
88
88
  }
89
- function parseReasoningTaskExtra(rawExtra) {
89
+ function assertReasoningTaskExtra(rawExtra) {
90
90
  if (!isReasoningTaskExtra(rawExtra)) {
91
91
  throw new Error('Invalid task context in request handler.');
92
92
  }
@@ -241,6 +241,7 @@ function buildStructuredResult(session, generatedThoughts, targetThoughts) {
241
241
  ok: true,
242
242
  result: {
243
243
  sessionId: session.id,
244
+ ...(session.query !== undefined ? { query: session.query } : {}),
244
245
  level: session.level,
245
246
  status: session.status,
246
247
  thoughts: [...session.thoughts],
@@ -260,7 +261,7 @@ function buildStructuredResult(session, generatedThoughts, targetThoughts) {
260
261
  }
261
262
  function buildSummary(session, remainingThoughts) {
262
263
  if (session.status === 'completed') {
263
- return `Reasoning complete — ${String(session.thoughts.length)} thoughts at [${session.level}] level. Session ${session.id}.`;
264
+ return `Reasoning complete — ${String(session.thoughts.length)} thought${session.thoughts.length === 1 ? '' : 's'} at [${session.level}] level. Session ${session.id}.`;
264
265
  }
265
266
  if (session.status === 'cancelled') {
266
267
  return `Reasoning cancelled at thought ${String(session.thoughts.length)}/${String(session.totalThoughts)}. Session ${session.id}.`;
@@ -354,7 +355,7 @@ function createProgressHandler(args) {
354
355
  const message = formatProgressMessage({
355
356
  toolName: `꩜ ${TOOL_NAME}`,
356
357
  context: 'Thought',
357
- metadata: `[${String(displayProgress)}/${String(batchTotal)}]${summary ? ` ${summary}` : ''}`,
358
+ ...(summary ? { metadata: summary } : {}),
358
359
  ...(isTerminal ? { outcome: 'complete' } : {}),
359
360
  });
360
361
  await notifyProgress({
@@ -569,7 +570,7 @@ async function runReasoningTask(args) {
569
570
  }
570
571
  function getTaskId(extra) {
571
572
  if (typeof extra.taskId !== 'string' || extra.taskId.length === 0) {
572
- throw new Error('Task ID missing in request context.');
573
+ throw new InvalidRunModeArgsError('Task ID missing in request context.');
573
574
  }
574
575
  return extra.taskId;
575
576
  }
@@ -612,7 +613,7 @@ Protocol validation: malformed task metadata/arguments fail at request level bef
612
613
  throw new Error(`Invalid reasoning_think params: ${parseResult.error.message}`);
613
614
  }
614
615
  const params = parseResult.data;
615
- const extra = parseReasoningTaskExtra(rawExtra);
616
+ const extra = assertReasoningTaskExtra(rawExtra);
616
617
  const progressToken = extra._meta?.progressToken;
617
618
  if (!reasoningTaskLimiter.tryAcquire()) {
618
619
  throw new ServerBusyError();
@@ -649,11 +650,11 @@ Protocol validation: malformed task metadata/arguments fail at request level bef
649
650
  return { task };
650
651
  },
651
652
  getTask(_params, rawExtra) {
652
- const extra = parseReasoningTaskExtra(rawExtra);
653
+ const extra = assertReasoningTaskExtra(rawExtra);
653
654
  return extra.taskStore.getTask(getTaskId(extra));
654
655
  },
655
656
  async getTaskResult(_params, rawExtra) {
656
- const extra = parseReasoningTaskExtra(rawExtra);
657
+ const extra = assertReasoningTaskExtra(rawExtra);
657
658
  const result = await extra.taskStore.getTaskResult(getTaskId(extra));
658
659
  assertCallToolResult(result);
659
660
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/cortex-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "mcpName": "io.github.j0hanz/cortex-mcp",
5
5
  "author": "Johanz",
6
6
  "license": "MIT",