suemo 0.0.4 → 0.0.6

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.
@@ -109,9 +109,12 @@ export async function checkCompatibility(
109
109
  + 'Start SurrealDB with: SURREAL_DATASTORE_RETENTION=90d surreal start surrealkv://path/to/data',
110
110
  )
111
111
  } else {
112
- // Unexpected error — still flag but don't assume retention is wrong
113
- retention_ok = true
114
- log.debug('Retention probe returned unexpected error (may be fine)', { error: String(e) })
112
+ retention_ok = false
113
+ errors.push(
114
+ `Retention probe failed unexpectedly: ${String(e)}. `
115
+ + 'Verify server capabilities and retention env (SURREAL_DATASTORE_VERSIONED=true and SURREAL_DATASTORE_RETENTION=90d).',
116
+ )
117
+ log.debug('Retention probe returned unexpected error', { error: String(e) })
115
118
  }
116
119
  } finally {
117
120
  try {
@@ -172,11 +175,7 @@ export async function checkCompatibility(
172
175
  export async function requireCompatibility(db: Surreal): Promise<void> {
173
176
  const result = await checkCompatibility(db)
174
177
  if (!result.ok) {
175
- console.error('\n[suemo] Compatibility check failed:\n')
176
- for (const err of result.errors) {
177
- console.error(` ✗ ${err}`)
178
- }
179
- console.error('\nFix the issues above and retry.\n')
180
- process.exit(1)
178
+ const details = result.errors.map((err) => ` ✗ ${err}`).join('\n')
179
+ throw new Error(`\n[suemo] Compatibility check failed:\n\n${details}\n\nFix the issues above and retry.\n`)
181
180
  }
182
181
  }
@@ -70,6 +70,9 @@ DEFINE INDEX OVERWRITE idx_memory_salience
70
70
  DEFINE INDEX OVERWRITE idx_memory_topic_key
71
71
  ON memory FIELDS topic_key;
72
72
 
73
+ DEFINE INDEX OVERWRITE idx_memory_updated_at
74
+ ON memory FIELDS updated_at;
75
+
73
76
  -- ── relates_to ───────────────────────────────────────────────────────────────
74
77
  DEFINE TABLE OVERWRITE relates_to SCHEMAFULL
75
78
  TYPE RELATION IN memory OUT memory;
@@ -83,14 +86,28 @@ DEFINE FIELD OVERWRITE valid_until ON relates_to TYPE option<datetime> DEFAULT N
83
86
  DEFINE FIELD OVERWRITE created_at ON relates_to TYPE datetime DEFAULT time::now();
84
87
  DEFINE FIELD OVERWRITE updated_at ON relates_to TYPE datetime DEFAULT time::now();
85
88
 
89
+ DEFINE INDEX OVERWRITE idx_relates_to_updated_at
90
+ ON relates_to FIELDS updated_at;
91
+
86
92
  -- ── episode ───────────────────────────────────────────────────────────────────
87
93
  DEFINE TABLE OVERWRITE episode SCHEMAFULL;
88
94
 
89
95
  DEFINE FIELD OVERWRITE session_id ON episode TYPE string;
90
96
  DEFINE FIELD OVERWRITE started_at ON episode TYPE datetime DEFAULT time::now();
91
97
  DEFINE FIELD OVERWRITE ended_at ON episode TYPE option<datetime> DEFAULT NONE;
98
+ DEFINE FIELD OVERWRITE updated_at ON episode TYPE datetime DEFAULT time::now();
92
99
  DEFINE FIELD OVERWRITE summary ON episode TYPE option<string>;
93
100
  DEFINE FIELD OVERWRITE context ON episode TYPE option<object> FLEXIBLE DEFAULT NONE;
101
+ DEFINE FIELD OVERWRITE goal ON episode TYPE option<string> DEFAULT NONE;
102
+ DEFINE FIELD OVERWRITE discoveries ON episode TYPE option<array<string>> DEFAULT NONE;
103
+ DEFINE FIELD OVERWRITE accomplished ON episode TYPE option<array<string>> DEFAULT NONE;
104
+ DEFINE FIELD OVERWRITE files_changed ON episode TYPE option<array<string>> DEFAULT NONE;
105
+
106
+ DEFINE INDEX OVERWRITE idx_episode_session_time
107
+ ON episode FIELDS session_id, ended_at, started_at;
108
+
109
+ DEFINE INDEX OVERWRITE idx_episode_updated_at
110
+ ON episode FIELDS updated_at;
94
111
 
95
112
  -- REFERENCES: bidirectional array of memory links
96
113
  DEFINE FIELD OVERWRITE memory_ids ON episode
package/src/index.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  // src/index.ts — public API surface
2
- export { defineConfig, loadConfig, resolveSyncConfig } from './config.ts'
2
+ export { defineConfig, inferProjectIdentity, inferProjectScope, loadConfig, resolveSyncConfig } from './config.ts'
3
3
  export type {
4
4
  AuthConfig,
5
5
  ConsolidationConfig,
6
6
  EmbeddingProvider,
7
7
  LLMConfig,
8
+ MainConfig,
8
9
  McpConfig,
10
+ ProjectIdentity,
9
11
  ResolvedSyncAutoConfig,
10
12
  ResolvedSyncConfig,
11
13
  RetrievalConfig,
package/src/logger.ts CHANGED
@@ -10,10 +10,26 @@ export type LogLevel = 'debug' | 'info' | 'warning' | 'error' | 'fatal'
10
10
 
11
11
  // Different formatters for different purposes
12
12
  const prettySink: Sink = getConsoleSink({
13
+ console: {
14
+ debug: console.error,
15
+ info: console.error,
16
+ log: console.error,
17
+ warn: console.error,
18
+ error: console.error,
19
+ trace: console.error,
20
+ } as Console,
13
21
  formatter: record => `${record.level}: ${String(record.message)}\n`,
14
22
  })
15
23
 
16
24
  const jsonSink: Sink = getConsoleSink({
25
+ console: {
26
+ debug: console.error,
27
+ info: console.error,
28
+ log: console.error,
29
+ warn: console.error,
30
+ error: console.error,
31
+ trace: console.error,
32
+ } as Console,
17
33
  formatter: record =>
18
34
  JSON.stringify({
19
35
  level: record.level,
@@ -26,12 +42,16 @@ const jsonSink: Sink = getConsoleSink({
26
42
  export async function initLogger(options: {
27
43
  level: LogLevel
28
44
  logFile?: string // if set, also write to file
45
+ quiet?: boolean
29
46
  }): Promise<void> {
30
47
  const sinks: Record<string, ReturnType<typeof getConsoleSink>> = {
31
48
  pretty: prettySink,
32
49
  json: jsonSink,
33
50
  }
34
51
 
52
+ const noopSink: Sink = () => {}
53
+ sinks['noop'] = noopSink as ReturnType<typeof getConsoleSink>
54
+
35
55
  if (options.logFile) {
36
56
  sinks['file'] = getFileSink(options.logFile)
37
57
  }
@@ -48,7 +68,9 @@ export async function initLogger(options: {
48
68
  {
49
69
  category: ['suemo'],
50
70
  lowestLevel: options.level,
51
- sinks: Object.keys(sinks).filter(item => item !== 'json'),
71
+ sinks: options.quiet
72
+ ? ['noop']
73
+ : Object.keys(sinks).filter(item => item !== 'json' && item !== 'noop'),
52
74
  },
53
75
  ],
54
76
  })
@@ -2,12 +2,22 @@ import type { Surreal } from 'surrealdb'
2
2
  import { z } from 'zod'
3
3
  import { consolidate } from '../cognitive/consolidate.ts'
4
4
  import { healthReport, suemoStats, vitals } from '../cognitive/health.ts'
5
+ import { inferProjectScope } from '../config.ts'
5
6
  import type { SuemoConfig } from '../config.ts'
6
7
  import { goalList, goalResolve, goalSet } from '../goal.ts'
7
8
  import { getLogger } from '../logger.ts'
8
9
  import { episodeEnd, episodeStart, getSessionContext, setSessionContext } from '../memory/episode.ts'
9
- import { query, recall, timeline, wander } from '../memory/read.ts'
10
- import { believe, capturePrompt, invalidate, observe, upsertByKey } from '../memory/write.ts'
10
+ import { context, query, recall, timeline, wander } from '../memory/read.ts'
11
+ import {
12
+ believe,
13
+ capturePrompt,
14
+ invalidate,
15
+ observe,
16
+ suggestTopicKey,
17
+ updateMemoryById,
18
+ upsertByKey,
19
+ } from '../memory/write.ts'
20
+ import { listSkillReferences, readSkillDoc, readSkillReference } from '../skill/catalog.ts'
11
21
  import { ObserveInputSchema, QueryInputSchema } from '../types.ts'
12
22
 
13
23
  const log = getLogger(['suemo', 'mcp', 'dispatch'])
@@ -20,7 +30,10 @@ const MUTATING_TOOLS = new Set([
20
30
  'goal_resolve',
21
31
  'upsert_by_key',
22
32
  'capture_prompt',
33
+ 'episode_start',
34
+ 'episode_end',
23
35
  'session_context_set',
36
+ 'update',
24
37
  ])
25
38
 
26
39
  interface DispatchOptions {
@@ -35,6 +48,7 @@ export async function handleToolCall(
35
48
  opts: DispatchOptions = {},
36
49
  ): Promise<unknown> {
37
50
  log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
51
+ const inferredScope = inferProjectScope(process.cwd(), config)
38
52
 
39
53
  const maybeTriggerMutationSync = (): void => {
40
54
  if (!MUTATING_TOOLS.has(method) || !opts.onMutation) return
@@ -45,7 +59,11 @@ export async function handleToolCall(
45
59
 
46
60
  switch (method) {
47
61
  case 'observe': {
48
- const result = await observe(db, ObserveInputSchema.parse(params), config)
62
+ const parsed = ObserveInputSchema.parse(params)
63
+ const result = await observe(db, {
64
+ ...parsed,
65
+ scope: parsed.scope?.trim() || inferredScope,
66
+ }, config)
49
67
  maybeTriggerMutationSync()
50
68
  return result
51
69
  }
@@ -58,7 +76,10 @@ export async function handleToolCall(
58
76
  confidence: z.number().optional(),
59
77
  })
60
78
  .parse(params)
61
- const result = await believe(db, parsed, config)
79
+ const result = await believe(db, {
80
+ ...parsed,
81
+ scope: parsed.scope?.trim() || inferredScope,
82
+ }, config)
62
83
  maybeTriggerMutationSync()
63
84
  return result
64
85
  }
@@ -70,8 +91,13 @@ export async function handleToolCall(
70
91
  return { ok: true }
71
92
  }
72
93
 
73
- case 'query':
74
- return query(db, QueryInputSchema.parse(params), config)
94
+ case 'query': {
95
+ const parsed = QueryInputSchema.parse(params)
96
+ return query(db, {
97
+ ...parsed,
98
+ scope: parsed.scope?.trim() || inferredScope,
99
+ }, config)
100
+ }
75
101
 
76
102
  case 'recall': {
77
103
  const parsed = z.object({ nodeId: z.string() }).parse(params)
@@ -85,7 +111,7 @@ export async function handleToolCall(
85
111
  return wander(db, {
86
112
  ...(parsed.anchor ? { anchor: parsed.anchor } : {}),
87
113
  ...(parsed.hops ? { hops: parsed.hops } : {}),
88
- ...(parsed.scope ? { scope: parsed.scope } : {}),
114
+ scope: parsed.scope?.trim() || inferredScope,
89
115
  })
90
116
  }
91
117
 
@@ -99,21 +125,46 @@ export async function handleToolCall(
99
125
  })
100
126
  .parse(params)
101
127
  return timeline(db, {
102
- ...(parsed.scope ? { scope: parsed.scope } : {}),
128
+ scope: parsed.scope?.trim() || inferredScope,
103
129
  ...(parsed.from ? { from: parsed.from } : {}),
104
130
  ...(parsed.until ? { until: parsed.until } : {}),
105
131
  ...(parsed.limit ? { limit: parsed.limit } : {}),
106
132
  })
107
133
  }
108
134
 
135
+ case 'context': {
136
+ const parsed = z.object({ scope: z.string().optional(), limit: z.number().optional() }).parse(params)
137
+ return context(db, {
138
+ scope: parsed.scope?.trim() || inferredScope,
139
+ ...(parsed.limit !== undefined ? { limit: parsed.limit } : {}),
140
+ })
141
+ }
142
+
109
143
  case 'episode_start': {
110
144
  const parsed = z.object({ sessionId: z.string() }).parse(params)
111
- return episodeStart(db, parsed.sessionId)
145
+ const result = await episodeStart(db, parsed.sessionId)
146
+ maybeTriggerMutationSync()
147
+ return result
112
148
  }
113
149
 
114
150
  case 'episode_end': {
115
- const parsed = z.object({ sessionId: z.string(), summary: z.string().optional() }).parse(params)
116
- return episodeEnd(db, parsed.sessionId, parsed.summary)
151
+ const parsed = z.object({
152
+ sessionId: z.string(),
153
+ summary: z.string().optional(),
154
+ goal: z.string().optional(),
155
+ discoveries: z.array(z.string()).optional(),
156
+ accomplished: z.array(z.string()).optional(),
157
+ files_changed: z.array(z.string()).optional(),
158
+ }).parse(params)
159
+ const result = await episodeEnd(db, parsed.sessionId, parsed.summary, {
160
+ ...(parsed.summary !== undefined ? { summary: parsed.summary } : {}),
161
+ ...(parsed.goal !== undefined ? { goal: parsed.goal } : {}),
162
+ ...(parsed.discoveries !== undefined ? { discoveries: parsed.discoveries } : {}),
163
+ ...(parsed.accomplished !== undefined ? { accomplished: parsed.accomplished } : {}),
164
+ ...(parsed.files_changed !== undefined ? { files_changed: parsed.files_changed } : {}),
165
+ })
166
+ maybeTriggerMutationSync()
167
+ return result
117
168
  }
118
169
 
119
170
  case 'goal_set': {
@@ -121,7 +172,7 @@ export async function handleToolCall(
121
172
  .object({ content: z.string(), scope: z.string().optional(), tags: z.array(z.string()).optional() })
122
173
  .parse(params)
123
174
  const result = await goalSet(db, parsed.content, config, {
124
- ...(parsed.scope ? { scope: parsed.scope } : {}),
175
+ scope: parsed.scope?.trim() || inferredScope,
125
176
  tags: parsed.tags ?? [],
126
177
  })
127
178
  maybeTriggerMutationSync()
@@ -138,7 +189,7 @@ export async function handleToolCall(
138
189
  case 'goal_list': {
139
190
  const parsed = z.object({ scope: z.string().optional(), includeResolved: z.boolean().optional() }).parse(params)
140
191
  return goalList(db, {
141
- ...(parsed.scope ? { scope: parsed.scope } : {}),
192
+ scope: parsed.scope?.trim() || inferredScope,
142
193
  includeResolved: parsed.includeResolved ?? false,
143
194
  })
144
195
  }
@@ -153,13 +204,14 @@ export async function handleToolCall(
153
204
  return vitals(db)
154
205
 
155
206
  case 'consolidate': {
156
- const parsed = z.object({ nremOnly: z.boolean().optional() }).parse(params)
207
+ const parsed = z.object({ nremOnly: z.boolean().optional(), scope: z.string().optional() }).parse(params)
157
208
  return consolidate(db, {
158
209
  nremOnly: parsed.nremOnly ?? false,
159
210
  nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
160
211
  remRelationThreshold: config.consolidation.remRelationThreshold,
161
212
  llm: config.consolidation.llm,
162
213
  embedding: config.embedding,
214
+ scope: parsed.scope?.trim() || inferredScope,
163
215
  })
164
216
  }
165
217
 
@@ -177,7 +229,7 @@ export async function handleToolCall(
177
229
  })
178
230
  .parse(params)
179
231
  const result = await upsertByKey(db, config, parsed.topicKey, parsed.content, {
180
- ...(parsed.scope ? { scope: parsed.scope } : {}),
232
+ scope: parsed.scope?.trim() || inferredScope,
181
233
  tags: parsed.tags ?? [],
182
234
  ...(parsed.source ? { source: parsed.source } : {}),
183
235
  ...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
@@ -198,13 +250,61 @@ export async function handleToolCall(
198
250
  })
199
251
  .parse(params)
200
252
  const result = await capturePrompt(db, config, parsed.prompt, parsed.derivedIds ?? [], {
201
- ...(parsed.scope ? { scope: parsed.scope } : {}),
253
+ scope: parsed.scope?.trim() || inferredScope,
202
254
  ...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
203
255
  })
204
256
  maybeTriggerMutationSync()
205
257
  return result
206
258
  }
207
259
 
260
+ case 'update': {
261
+ const parsed = z
262
+ .object({
263
+ nodeId: z.string(),
264
+ content: z.string().optional(),
265
+ kind: z.enum(['observation', 'belief', 'question', 'hypothesis', 'goal']).optional(),
266
+ tags: z.array(z.string()).optional(),
267
+ scope: z.string().nullable().optional(),
268
+ source: z.string().nullable().optional(),
269
+ confidence: z.number().optional(),
270
+ })
271
+ .parse(params)
272
+
273
+ const result = await updateMemoryById(db, config, parsed.nodeId, {
274
+ ...(parsed.content !== undefined ? { content: parsed.content } : {}),
275
+ ...(parsed.kind !== undefined ? { kind: parsed.kind } : {}),
276
+ ...(parsed.tags !== undefined ? { tags: parsed.tags } : {}),
277
+ ...(parsed.scope !== undefined ? { scope: parsed.scope ?? null } : {}),
278
+ ...(parsed.source !== undefined ? { source: parsed.source ?? null } : {}),
279
+ ...(parsed.confidence !== undefined ? { confidence: parsed.confidence } : {}),
280
+ })
281
+ maybeTriggerMutationSync()
282
+ return result
283
+ }
284
+
285
+ case 'suggest_topic_key': {
286
+ const parsed = z.object({ input: z.string() }).parse(params)
287
+ return { topicKey: suggestTopicKey(parsed.input) }
288
+ }
289
+
290
+ case 'skill': {
291
+ const parsed = z.object({ reference: z.string().optional() }).parse(params)
292
+ const reference = parsed.reference?.trim()
293
+ if (!reference) {
294
+ const references = listSkillReferences()
295
+ return {
296
+ name: 'suemo',
297
+ content: readSkillDoc(),
298
+ references,
299
+ }
300
+ }
301
+ const resolved = readSkillReference(reference)
302
+ if (!resolved) {
303
+ throw new Error(`Unknown skill reference: ${reference}`)
304
+ }
305
+ return { name: resolved.name, content: resolved.content }
306
+ }
307
+
208
308
  case 'session_context_get': {
209
309
  const parsed = z.object({ sessionId: z.string() }).parse(params)
210
310
  return getSessionContext(db, parsed.sessionId)
package/src/mcp/server.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/mcp/server.ts
2
2
  import { Elysia } from 'elysia'
3
- import { resolveSyncConfig, type SuemoConfig } from '../config.ts'
3
+ import { inferProjectScope, resolveSyncConfig, type SuemoConfig } from '../config.ts'
4
4
  import { connect, disconnect } from '../db/client.ts'
5
5
  import { checkCompatibility, requireCompatibility } from '../db/preflight.ts'
6
6
  import { runSchema } from '../db/schema.ts'
@@ -37,6 +37,7 @@ function createAutoSyncRunner(
37
37
  let running = false
38
38
  let queued = false
39
39
  let lastRunAt = 0
40
+ let lastAttemptAt = 0
40
41
 
41
42
  const runAutoSync = async ({ reason, force = false }: AutoSyncOptions): Promise<void> => {
42
43
  if (!resolvedSync.auto.enabled) return
@@ -44,7 +45,8 @@ function createAutoSyncRunner(
44
45
 
45
46
  const now = Date.now()
46
47
  const minIntervalMs = resolvedSync.auto.minWriteIntervalSeconds * 1000
47
- if (!force && reason === 'write' && lastRunAt > 0 && now - lastRunAt < minIntervalMs) {
48
+ const throttleFrom = Math.max(lastRunAt, lastAttemptAt)
49
+ if (!force && reason === 'write' && throttleFrom > 0 && now - throttleFrom < minIntervalMs) {
48
50
  return
49
51
  }
50
52
 
@@ -60,6 +62,7 @@ function createAutoSyncRunner(
60
62
  }
61
63
 
62
64
  running = true
65
+ lastAttemptAt = Date.now()
63
66
  try {
64
67
  const result = await syncTo(db, target, {
65
68
  direction: resolvedSync.auto.direction,
@@ -78,9 +81,11 @@ function createAutoSyncRunner(
78
81
  running = false
79
82
  if (queued) {
80
83
  queued = false
81
- queueMicrotask(() => {
82
- void runAutoSync({ reason: 'write', force: true })
83
- })
84
+ const nowAfterRun = Date.now()
85
+ const minDelay = Math.max(0, minIntervalMs - (nowAfterRun - Math.max(lastRunAt, lastAttemptAt)))
86
+ setTimeout(() => {
87
+ void runAutoSync({ reason: 'write' })
88
+ }, minDelay)
84
89
  }
85
90
  }
86
91
  }
@@ -116,6 +121,11 @@ function createAutoSyncRunner(
116
121
  }
117
122
 
118
123
  export async function startMcpServer(config: SuemoConfig): Promise<void> {
124
+ const inferredScope = inferProjectScope(process.cwd(), config)
125
+ log.info('Resolved default project scope', {
126
+ scope: inferredScope,
127
+ projectDir: config.main?.projectDir ?? '.ua',
128
+ })
119
129
  const db = await connect(config.surreal)
120
130
  await requireCompatibility(db)
121
131
  await runSchema(db)
@@ -144,6 +154,11 @@ export async function startMcpServer(config: SuemoConfig): Promise<void> {
144
154
  }
145
155
 
146
156
  export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
157
+ const inferredScope = inferProjectScope(process.cwd(), config)
158
+ log.info('Resolved default project scope', {
159
+ scope: inferredScope,
160
+ projectDir: config.main?.projectDir ?? '.ua',
161
+ })
147
162
  const db = await connect(config.surreal)
148
163
  const autoSync = createAutoSyncRunner(db, config)
149
164
  autoSync.start()
@@ -153,12 +168,8 @@ export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
153
168
  context: 'mcp:stdio-startup',
154
169
  })
155
170
  if (!compat.ok) {
156
- console.error('\n[suemo] Compatibility check failed:\n')
157
- for (const err of compat.errors) {
158
- console.error(` ✗ ${err}`)
159
- }
160
- console.error('\nFix the issues above and retry.\n')
161
- process.exit(1)
171
+ const details = compat.errors.map((err) => ` ✗ ${err}`).join('\n')
172
+ throw new Error(`\n[suemo] Compatibility check failed:\n\n${details}\n\nFix the issues above and retry.\n`)
162
173
  }
163
174
  await runSchema(db)
164
175
  await runStdioServer(db, config, { onMutation: autoSync.onWrite })
package/src/mcp/stdio.ts CHANGED
@@ -121,6 +121,17 @@ const MCP_TOOLS: McpToolDefinition[] = [
121
121
  },
122
122
  },
123
123
  },
124
+ {
125
+ name: 'context',
126
+ description: 'Recover recent session context by scope',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ scope: { type: 'string' },
131
+ limit: { type: 'number' },
132
+ },
133
+ },
134
+ },
124
135
  {
125
136
  name: 'episode_start',
126
137
  description: 'Start an episode for a session ID',
@@ -138,6 +149,10 @@ const MCP_TOOLS: McpToolDefinition[] = [
138
149
  properties: {
139
150
  sessionId: { type: 'string' },
140
151
  summary: { type: 'string' },
152
+ goal: { type: 'string' },
153
+ discoveries: { type: 'array', items: { type: 'string' } },
154
+ accomplished: { type: 'array', items: { type: 'string' } },
155
+ files_changed: { type: 'array', items: { type: 'string' } },
141
156
  },
142
157
  required: ['sessionId'],
143
158
  },
@@ -195,7 +210,10 @@ const MCP_TOOLS: McpToolDefinition[] = [
195
210
  description: 'Trigger memory consolidation',
196
211
  inputSchema: {
197
212
  type: 'object',
198
- properties: { nremOnly: { type: 'boolean' } },
213
+ properties: {
214
+ nremOnly: { type: 'boolean' },
215
+ scope: { type: 'string' },
216
+ },
199
217
  },
200
218
  },
201
219
  {
@@ -216,6 +234,44 @@ const MCP_TOOLS: McpToolDefinition[] = [
216
234
  required: ['topicKey', 'content'],
217
235
  },
218
236
  },
237
+ {
238
+ name: 'update',
239
+ description: 'Update an existing memory node by ID',
240
+ inputSchema: {
241
+ type: 'object',
242
+ properties: {
243
+ nodeId: { type: 'string' },
244
+ content: { type: 'string' },
245
+ kind: { type: 'string', enum: ['observation', 'belief', 'question', 'hypothesis', 'goal'] },
246
+ tags: { type: 'array', items: { type: 'string' } },
247
+ scope: { type: ['string', 'null'] },
248
+ source: { type: ['string', 'null'] },
249
+ confidence: { type: 'number' },
250
+ },
251
+ required: ['nodeId'],
252
+ },
253
+ },
254
+ {
255
+ name: 'suggest_topic_key',
256
+ description: 'Suggest a deterministic canonical topic key from free text',
257
+ inputSchema: {
258
+ type: 'object',
259
+ properties: {
260
+ input: { type: 'string' },
261
+ },
262
+ required: ['input'],
263
+ },
264
+ },
265
+ {
266
+ name: 'skill',
267
+ description: 'Get suemo skill doc or a specific versioned reference',
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {
271
+ reference: { type: 'string' },
272
+ },
273
+ },
274
+ },
219
275
  {
220
276
  name: 'capture_prompt',
221
277
  description: 'Capture a raw prompt and link derived observations',
@@ -1,7 +1,7 @@
1
1
  import type { Surreal } from 'surrealdb'
2
2
  import { incrementWriteStats } from '../cognitive/health.ts'
3
3
  import { getLogger } from '../logger.ts'
4
- import type { Episode } from '../types.ts'
4
+ import { type Episode, type EpisodeSummaryFields, EpisodeSummaryFieldsSchema } from '../types.ts'
5
5
 
6
6
  const log = getLogger(['suemo', 'memory', 'episode'])
7
7
 
@@ -17,6 +17,10 @@ export async function episodeStart(
17
17
  ended_at: NONE,
18
18
  summary: NONE,
19
19
  context: NONE,
20
+ goal: NONE,
21
+ discoveries: NONE,
22
+ accomplished: NONE,
23
+ files_changed: NONE,
20
24
  memory_ids: []
21
25
  }
22
26
  `,
@@ -30,18 +34,42 @@ export async function episodeEnd(
30
34
  db: Surreal,
31
35
  sessionId: string,
32
36
  summary?: string,
37
+ structured?: EpisodeSummaryFields,
33
38
  ): Promise<Episode> {
34
39
  log.info('episodeEnd()', { sessionId })
40
+ const fields = structured ? EpisodeSummaryFieldsSchema.parse(structured) : undefined
41
+ const summaryText = summary ?? fields?.summary
35
42
  const result = await db.query<[Episode[]]>(
36
43
  `
37
44
  UPDATE episode
38
45
  SET ended_at = time::now(),
39
- summary = $summary
46
+ updated_at = time::now(),
47
+ summary = $summary,
48
+ goal = $goal,
49
+ discoveries = $discoveries,
50
+ accomplished = $accomplished,
51
+ files_changed = $filesChanged,
52
+ context = $context
40
53
  WHERE session_id = $sessionId
41
54
  AND ended_at = NONE
42
55
  RETURN AFTER
43
56
  `,
44
- { sessionId, summary: summary ?? null },
57
+ {
58
+ sessionId,
59
+ summary: summaryText ?? null,
60
+ goal: fields?.goal ?? null,
61
+ discoveries: fields?.discoveries ?? null,
62
+ accomplished: fields?.accomplished ?? null,
63
+ filesChanged: fields?.files_changed ?? null,
64
+ context: fields
65
+ ? {
66
+ ...(fields.goal !== undefined ? { goal: fields.goal } : {}),
67
+ ...(fields.discoveries !== undefined ? { discoveries: fields.discoveries } : {}),
68
+ ...(fields.accomplished !== undefined ? { accomplished: fields.accomplished } : {}),
69
+ ...(fields.files_changed !== undefined ? { files_changed: fields.files_changed } : {}),
70
+ }
71
+ : null,
72
+ },
45
73
  )
46
74
  const episode = result[0]?.[0]
47
75
  if (!episode) throw new Error(`No open episode found for session: ${sessionId}`)
@@ -63,6 +91,7 @@ export async function attachToEpisode(
63
91
  `
64
92
  UPDATE episode
65
93
  SET memory_ids += [$memoryId]
94
+ , updated_at = time::now()
66
95
  WHERE session_id = $sessionId AND ended_at = NONE
67
96
  `,
68
97
  { sessionId, memoryId },
@@ -124,7 +153,8 @@ export async function setSessionContext(
124
153
  `
125
154
  UPDATE episode
126
155
  SET summary = $summary,
127
- context = $context
156
+ context = $context,
157
+ updated_at = time::now()
128
158
  WHERE session_id = $sessionId AND ended_at = NONE
129
159
  RETURN AFTER
130
160
  `,
@@ -139,7 +169,8 @@ export async function setSessionContext(
139
169
  const result = await db.query<[Episode[]]>(
140
170
  `
141
171
  UPDATE episode
142
- SET summary = $summary
172
+ SET summary = $summary,
173
+ updated_at = time::now()
143
174
  WHERE session_id = $sessionId AND ended_at = NONE
144
175
  RETURN AFTER
145
176
  `,
@@ -153,7 +184,8 @@ export async function setSessionContext(
153
184
  const result = await db.query<[Episode[]]>(
154
185
  `
155
186
  UPDATE episode
156
- SET context = $context
187
+ SET context = $context,
188
+ updated_at = time::now()
157
189
  WHERE session_id = $sessionId AND ended_at = NONE
158
190
  RETURN AFTER
159
191
  `,