suemo 0.1.7 → 0.1.9
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/README.md +21 -3
- package/package.json +5 -3
- package/skills/suemo/SKILL.md +2 -2
- package/skills/suemo/references/agents-snippet.md +1 -1
- package/skills/suemo/references/cli-reference.md +1 -1
- package/skills/suemo/references/core-workflow.md +1 -1
- package/skills/suemo/references/manual-test-plan.md +1 -1
- package/skills/suemo/references/mcp-reference.md +1 -1
- package/skills/suemo/references/schema-retention-longevity.md +1 -1
- package/skills/suemo/references/sync-local-vps.md +1 -1
- package/src/AGENTS.md +1 -1
- package/src/cli/commands/health.ts +2 -2
- package/src/cli/commands/recall.ts +29 -1
- package/src/cli/commands/serve.ts +12 -0
- package/src/cognitive/consolidate.ts +6 -1
- package/src/cognitive/health.ts +25 -3
- package/src/db/client.ts +1 -1
- package/src/db/preflight.ts +56 -27
- package/src/db/schema.surql +7 -0
- package/src/mcp/dispatch.ts +31 -5
- package/src/mcp/server.ts +14 -1
- package/src/mcp/stdio.ts +33 -5
- package/src/memory/episode.ts +153 -7
- package/src/memory/fsrs.ts +237 -0
- package/src/memory/read.ts +191 -23
- package/src/memory/write.ts +46 -22
- package/src/opencode/plugin.ts +138 -17
- package/src/types.ts +9 -2
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,25 @@
|
|
|
11
11
|
|
|
12
12
|
suemo gives AI agents a memory that survives across sessions, models, and runtimes. Write observations from a Telegram bot, query them from OpenCode, consolidate overnight — all agents share one source of truth in SurrealDB.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Specifications:
|
|
15
|
+
|
|
16
|
+
- Index: **[`specs/SPEC.md`](./specs/SPEC.md)**
|
|
17
|
+
- Technical/runtime contract: **[`specs/SPEC-technical.md`](./specs/SPEC-technical.md)**
|
|
18
|
+
- Psychological/critique framing: **[`specs/SPEC-psychological.md`](./specs/SPEC-psychological.md)**
|
|
19
|
+
|
|
20
|
+
## For professional critique authors (longevity-focused)
|
|
21
|
+
|
|
22
|
+
If you're designing high-quality external critiques for suemo, evaluate it as **persistent coding-agent memory infrastructure**, not as a general AGI cognition claim.
|
|
23
|
+
|
|
24
|
+
Use this framing:
|
|
25
|
+
|
|
26
|
+
1. **Psychological intent** — what memory quality should hold over time?
|
|
27
|
+
2. **Technical mechanism** — what exact table/field/tool enforces that quality?
|
|
28
|
+
3. **Operational evidence** — what logs/tests/metrics prove it under drift and contradiction?
|
|
29
|
+
|
|
30
|
+
Questions are most useful when they stress long-horizon behavior (30–180 day operation), contradiction handling, consolidation quality, retrieval interference, and scope/session isolation.
|
|
31
|
+
|
|
32
|
+
For rigorous critique language and category framing, start from **[`specs/SPEC-psychological.md`](./specs/SPEC-psychological.md)**.
|
|
15
33
|
|
|
16
34
|
---
|
|
17
35
|
|
|
@@ -466,7 +484,7 @@ Current behavior: only write paths invoked with `sessionId` append to `memory_id
|
|
|
466
484
|
- CLI: `suemo believe "..." --session <sessionId>`
|
|
467
485
|
- MCP: `believe({ content, sessionId, ... })`
|
|
468
486
|
|
|
469
|
-
See `SPEC.md` for full normative semantics and hardening targets.
|
|
487
|
+
See `specs/SPEC-technical.md` for full normative semantics and hardening targets.
|
|
470
488
|
|
|
471
489
|
### Scope and longevity notes
|
|
472
490
|
|
|
@@ -526,7 +544,7 @@ These scenarios intentionally exercise all memory kinds and all relation kinds,
|
|
|
526
544
|
- **Runtime** — [Bun](https://bun.sh)
|
|
527
545
|
- **Language** — TypeScript (strict, `exactOptionalPropertyTypes`)
|
|
528
546
|
- **Database** — [SurrealDB](https://surrealdb.com) v3.0+ (SurrealKV)
|
|
529
|
-
- **SDK** — [surrealdb.js](https://github.com/surrealdb/surrealdb.js) v2 + `@surrealdb/node`
|
|
547
|
+
- **SDK** — [surrealdb.js](https://github.com/surrealdb/surrealdb.js) v2 + `@mdrv/surreal-node` (fork of `@surrealdb/node`)
|
|
530
548
|
- **CLI** — [crust](https://github.com/chenxin-yan/crust) (`@crustjs/core`)
|
|
531
549
|
- **HTTP / MCP** — [Elysia](https://elysiajs.com) v1.4
|
|
532
550
|
- **Validation** — [Zod](https://zod.dev) v4
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suemo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Umar Alfarouk",
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
"scripts": {
|
|
38
38
|
"dev": "bun run src/cli/index.ts",
|
|
39
39
|
"start": "bun run src/cli/index.ts",
|
|
40
|
+
"test": "bun test",
|
|
40
41
|
"ssot:check": "bun run scripts/ssot.ts check",
|
|
41
42
|
"ssot:sync": "bun run scripts/ssot.ts sync",
|
|
42
43
|
"fmt": "dprint fmt",
|
|
43
|
-
"check": "bun run fmt && bun tsc --noEmit && bun run ssot:check",
|
|
44
|
+
"check": "bun run fmt && bun tsc --noEmit && bun test && bun run ssot:check",
|
|
44
45
|
"sync": "bun run ssot:sync"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
@@ -50,12 +51,13 @@
|
|
|
50
51
|
"@crustjs/style": "^0.0.6",
|
|
51
52
|
"@logtape/file": "^2.0.4",
|
|
52
53
|
"@logtape/logtape": "^2.0.4",
|
|
53
|
-
"@
|
|
54
|
+
"@mdrv/surreal-node": "latest",
|
|
54
55
|
"elysia": "^1.4.28",
|
|
55
56
|
"surrealdb": "^2.0.3",
|
|
56
57
|
"zod": "^4.3.6"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
60
|
+
"@opencode-ai/plugin": "^1.3.0",
|
|
59
61
|
"@types/bun": "^1.3.11",
|
|
60
62
|
"typescript": "^5.9.3"
|
|
61
63
|
}
|
package/skills/suemo/SKILL.md
CHANGED
|
@@ -3,14 +3,14 @@ name: suemo
|
|
|
3
3
|
description: OpenCode-focused persistent memory workflow for suemo with CLI/MCP parity and versioned references.
|
|
4
4
|
license: GPL-3.0-only
|
|
5
5
|
compatibility: opencode
|
|
6
|
-
version: 0.1.
|
|
6
|
+
version: 0.1.9
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# suemo skill
|
|
10
10
|
|
|
11
11
|
Use suemo to persist technical context across sessions with minimal, project-scoped memory.
|
|
12
12
|
|
|
13
|
-
## Strict defaults (v0.1.
|
|
13
|
+
## Strict defaults (v0.1.8)
|
|
14
14
|
|
|
15
15
|
- Always run pre-checks before implementation:
|
|
16
16
|
- `goal_list({ scope })`
|
package/src/AGENTS.md
CHANGED
|
@@ -127,4 +127,4 @@ Load docs once per session unless requirements changed.
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
_Version: 0.1.
|
|
130
|
+
_Version: 0.1.9 | Updated: 2026-03-24 | Summary: stricter schema + explicit completion contract_
|
|
@@ -29,7 +29,7 @@ const reportCmd = health.sub('report')
|
|
|
29
29
|
let db: Surreal | undefined
|
|
30
30
|
try {
|
|
31
31
|
db = await connect(config.surreal)
|
|
32
|
-
const report = await healthReport(db)
|
|
32
|
+
const report = await healthReport(db, { embeddingProvider: config.embedding.provider })
|
|
33
33
|
if (outputMode === 'json') {
|
|
34
34
|
printCliJson(report, flags)
|
|
35
35
|
} else {
|
|
@@ -129,7 +129,7 @@ export const healthCmd = health
|
|
|
129
129
|
let db: Surreal | undefined
|
|
130
130
|
try {
|
|
131
131
|
db = await connect(config.surreal)
|
|
132
|
-
const report = await healthReport(db)
|
|
132
|
+
const report = await healthReport(db, { embeddingProvider: config.embedding.provider })
|
|
133
133
|
if (outputMode === 'json') {
|
|
134
134
|
printCliJson(report, flags)
|
|
135
135
|
} else {
|
|
@@ -10,6 +10,9 @@ export const recallCmd = app.sub('recall')
|
|
|
10
10
|
.meta({ description: 'Fetch a single node + its neighbours (ticks FSRS)' })
|
|
11
11
|
.args([{ name: 'nodeId', type: 'string', required: true }])
|
|
12
12
|
.flags({
|
|
13
|
+
rating: { type: 'number', short: 'r', description: 'FSRS rating (1=Again,2=Hard,3=Good,4=Easy)' },
|
|
14
|
+
at: { type: 'string', description: 'ISO datetime override for deterministic replay/testing' },
|
|
15
|
+
'dry-run': { type: 'boolean', description: 'Compute FSRS schedule without writing', default: false },
|
|
13
16
|
json: { type: 'boolean', description: 'Output full JSON' },
|
|
14
17
|
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
15
18
|
})
|
|
@@ -25,13 +28,38 @@ export const recallCmd = app.sub('recall')
|
|
|
25
28
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
26
29
|
const db = await connect(config.surreal)
|
|
27
30
|
try {
|
|
28
|
-
const
|
|
31
|
+
const rating = flags.rating === undefined ? undefined : Number(flags.rating)
|
|
32
|
+
if (rating !== undefined && ![1, 2, 3, 4].includes(rating)) {
|
|
33
|
+
throw new Error(`Invalid --rating value: ${flags.rating}. Expected one of: 1,2,3,4`)
|
|
34
|
+
}
|
|
35
|
+
if (flags.at) {
|
|
36
|
+
const atDate = new Date(String(flags.at))
|
|
37
|
+
if (Number.isNaN(atDate.getTime())) {
|
|
38
|
+
throw new Error(`Invalid --at datetime: ${flags.at}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await recall(db, args.nodeId, {
|
|
43
|
+
...(rating !== undefined ? { rating: rating as 1 | 2 | 3 | 4 } : {}),
|
|
44
|
+
...(flags.at ? { at: String(flags.at) } : {}),
|
|
45
|
+
dryRun: Boolean(flags['dry-run']),
|
|
46
|
+
})
|
|
47
|
+
log.debug('recall command completed', {
|
|
48
|
+
nodeId: args.nodeId,
|
|
49
|
+
rating: result.fsrs.rating,
|
|
50
|
+
nextReview: result.fsrs.nextReview,
|
|
51
|
+
stateAfter: result.fsrs.stateAfter,
|
|
52
|
+
dryRun: result.fsrs.dryRun,
|
|
53
|
+
})
|
|
29
54
|
if (outputMode === 'json') {
|
|
30
55
|
printCliJson(result, flags)
|
|
31
56
|
} else {
|
|
32
57
|
console.log(`[${result.node.kind}] ${result.node.id}`)
|
|
33
58
|
console.log(` ${result.node.content.slice(0, 120)}`)
|
|
34
59
|
console.log(` neighbors: ${result.neighbors.length}`)
|
|
60
|
+
console.log(` fsrs: rating=${result.fsrs.rating} interval=${result.fsrs.intervalDays}d`)
|
|
61
|
+
console.log(` next_review: ${result.fsrs.nextReview} (${result.fsrs.stateAfter})`)
|
|
62
|
+
if (result.fsrs.dryRun) console.log(' [dry-run] no FSRS state written')
|
|
35
63
|
}
|
|
36
64
|
} finally {
|
|
37
65
|
await disconnect()
|
|
@@ -192,12 +192,24 @@ export const serveCmd = app.sub('serve')
|
|
|
192
192
|
}
|
|
193
193
|
if (flags.stdio) {
|
|
194
194
|
log.debug('Starting MCP stdio transport')
|
|
195
|
+
log.debug('Serve transport resolved', {
|
|
196
|
+
transport: 'stdio',
|
|
197
|
+
embeddingProvider: config.embedding.provider,
|
|
198
|
+
autoSyncEnabled: Boolean(sync?.auto.enabled),
|
|
199
|
+
})
|
|
195
200
|
await runServerWithDevRetry({ stdio: true, config })
|
|
196
201
|
return
|
|
197
202
|
}
|
|
198
203
|
if (flags.port) config.mcp.port = flags.port
|
|
199
204
|
if (flags.host) config.mcp.host = flags.host
|
|
200
205
|
log.debug('Starting MCP HTTP transport', { host: config.mcp.host, port: config.mcp.port })
|
|
206
|
+
log.debug('Serve transport resolved', {
|
|
207
|
+
transport: 'http',
|
|
208
|
+
host: config.mcp.host,
|
|
209
|
+
port: config.mcp.port,
|
|
210
|
+
embeddingProvider: config.embedding.provider,
|
|
211
|
+
autoSyncEnabled: Boolean(sync?.auto.enabled),
|
|
212
|
+
})
|
|
201
213
|
await runServerWithDevRetry({ stdio: false, config })
|
|
202
214
|
// Server runs indefinitely — no disconnect
|
|
203
215
|
})
|
|
@@ -168,7 +168,12 @@ async function runNREM(
|
|
|
168
168
|
consolidated_into: NONE,
|
|
169
169
|
fsrs_stability: NONE,
|
|
170
170
|
fsrs_difficulty: NONE,
|
|
171
|
-
fsrs_next_review: NONE
|
|
171
|
+
fsrs_next_review: NONE,
|
|
172
|
+
fsrs_state: NONE,
|
|
173
|
+
fsrs_last_review: NONE,
|
|
174
|
+
fsrs_reps: NONE,
|
|
175
|
+
fsrs_lapses: NONE,
|
|
176
|
+
fsrs_last_grade: NONE
|
|
172
177
|
}
|
|
173
178
|
`,
|
|
174
179
|
{
|
package/src/cognitive/health.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EmbeddingProvider } from '@/src/config.ts'
|
|
1
2
|
import { checkCompatibility } from '@/src/db/preflight.ts'
|
|
2
3
|
import { getLogger } from '@/src/logger.ts'
|
|
3
4
|
import type { ConsolidationRun, HealthReport, SuemoStats } from '@/src/types.ts'
|
|
@@ -5,8 +6,14 @@ import type { Surreal } from 'surrealdb'
|
|
|
5
6
|
|
|
6
7
|
const log = getLogger(['suemo', 'cognitive', 'health'])
|
|
7
8
|
|
|
8
|
-
export async function healthReport(
|
|
9
|
-
|
|
9
|
+
export async function healthReport(
|
|
10
|
+
db: Surreal,
|
|
11
|
+
options: { embeddingProvider?: EmbeddingProvider['provider'] } = {},
|
|
12
|
+
): Promise<HealthReport> {
|
|
13
|
+
log.debug('healthReport()', {
|
|
14
|
+
embeddingProvider: options.embeddingProvider ?? null,
|
|
15
|
+
context: 'health:report',
|
|
16
|
+
})
|
|
10
17
|
|
|
11
18
|
const [
|
|
12
19
|
totalResult,
|
|
@@ -61,9 +68,24 @@ export async function healthReport(db: Surreal): Promise<HealthReport> {
|
|
|
61
68
|
'SELECT * FROM consolidation_run ORDER BY started_at DESC LIMIT 1',
|
|
62
69
|
),
|
|
63
70
|
// compat check (non-blocking)
|
|
64
|
-
checkCompatibility(db
|
|
71
|
+
checkCompatibility(db, {
|
|
72
|
+
...(options.embeddingProvider ? { embeddingProvider: options.embeddingProvider } : {}),
|
|
73
|
+
context: 'health:report',
|
|
74
|
+
}),
|
|
65
75
|
])
|
|
66
76
|
|
|
77
|
+
log.debug('healthReport() query batches complete', {
|
|
78
|
+
totalRows: totalResult[0]?.length ?? 0,
|
|
79
|
+
activeRows: activeResult[0]?.length ?? 0,
|
|
80
|
+
consolidatedRows: consolidatedResult[0]?.length ?? 0,
|
|
81
|
+
byKindRows: byKindResult[0]?.length ?? 0,
|
|
82
|
+
byScopeRows: byScopeResult[0]?.length ?? 0,
|
|
83
|
+
relationRows: relationCountResult[0]?.length ?? 0,
|
|
84
|
+
goalsRows: activeGoalsResult[0]?.length ?? 0,
|
|
85
|
+
fsrsDueRows: frssDueResult[0]?.length ?? 0,
|
|
86
|
+
compatOk: compatResult.ok,
|
|
87
|
+
})
|
|
88
|
+
|
|
67
89
|
const byKind: Record<string, number> = {}
|
|
68
90
|
for (const row of byKindResult[0] ?? []) byKind[row.kind] = row.count
|
|
69
91
|
|
package/src/db/client.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// createNodeEngines() patches in the Node.js WebSocket implementation.
|
|
4
4
|
import type { SurrealTarget } from '@/src/config.ts'
|
|
5
5
|
import { getLogger } from '@/src/logger.ts'
|
|
6
|
-
import { createNodeEngines } from '@
|
|
6
|
+
import { createNodeEngines } from '@mdrv/surreal-node'
|
|
7
7
|
import { createRemoteEngines, Surreal } from 'surrealdb'
|
|
8
8
|
|
|
9
9
|
const log = getLogger(['suemo', 'db', 'client'])
|
package/src/db/preflight.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface CompatibilityOptions {
|
|
|
18
18
|
* Keep true for runtime paths that execute vector/embed queries.
|
|
19
19
|
*/
|
|
20
20
|
requireEmbedding?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Embedding provider profile for this runtime.
|
|
23
|
+
* When set, requireEmbedding defaults to true only for surrealml.
|
|
24
|
+
*/
|
|
25
|
+
embeddingProvider?: 'surrealml' | 'openai-compatible' | 'stub'
|
|
21
26
|
/**
|
|
22
27
|
* Human-readable label for logging this preflight execution.
|
|
23
28
|
*/
|
|
@@ -35,10 +40,12 @@ export async function checkCompatibility(
|
|
|
35
40
|
let surrealkv = false
|
|
36
41
|
let retention_ok = false
|
|
37
42
|
let embedding = false
|
|
38
|
-
const
|
|
43
|
+
const embeddingProvider = options.embeddingProvider
|
|
44
|
+
const requireEmbedding = options.requireEmbedding
|
|
45
|
+
?? (embeddingProvider ? embeddingProvider === 'surrealml' : true)
|
|
39
46
|
const context = options.context ?? 'default'
|
|
40
47
|
|
|
41
|
-
log.info('Running preflight compatibility checks', { requireEmbedding, context })
|
|
48
|
+
log.info('Running preflight compatibility checks', { requireEmbedding, embeddingProvider, context })
|
|
42
49
|
|
|
43
50
|
// ── Check 1: version string ───────────────────────────────────────────────
|
|
44
51
|
try {
|
|
@@ -126,31 +133,41 @@ export async function checkCompatibility(
|
|
|
126
133
|
// ── Check 4: fn::embed() resolves ────────────────────────────────────────
|
|
127
134
|
// We don't actually embed anything — we just check that the function exists.
|
|
128
135
|
// An "Unknown function" error means embedding runtime is unavailable.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
if (!requireEmbedding && embeddingProvider && embeddingProvider !== 'surrealml') {
|
|
137
|
+
embedding = false
|
|
138
|
+
log.info('Skipping fn::embed preflight check for non-surrealml provider', {
|
|
139
|
+
context,
|
|
140
|
+
embeddingProvider,
|
|
141
|
+
})
|
|
142
|
+
} else {
|
|
143
|
+
try {
|
|
144
|
+
// fn::embed requires the ML module and a configured model.
|
|
145
|
+
// If it throws "No embedding model configured" that's acceptable — the
|
|
146
|
+
// function exists. If it throws "Unknown function 'fn::embed'" → not available.
|
|
147
|
+
await db.query(`RETURN fn::embed("suemo preflight test")`)
|
|
148
|
+
embedding = true
|
|
149
|
+
log.debug('Embedding function probe passed')
|
|
150
|
+
} catch (e: unknown) {
|
|
151
|
+
const msg = String(e).toLowerCase()
|
|
152
|
+
if (msg.includes('unknown function') || msg.includes('fn::embed')) {
|
|
153
|
+
embedding = false
|
|
154
|
+
if (requireEmbedding) {
|
|
155
|
+
errors.push(
|
|
156
|
+
'fn::embed() is not available in this SurrealDB database. Import/configure a SurrealML embedding model for this namespace/database, then retry.',
|
|
157
|
+
)
|
|
158
|
+
} else {
|
|
159
|
+
log.warn('Embedding function unavailable; continuing due to non-strict preflight mode', {
|
|
160
|
+
context,
|
|
161
|
+
error: String(e),
|
|
162
|
+
})
|
|
163
|
+
}
|
|
144
164
|
} else {
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
// Other error (e.g. no model configured but function exists) — treat as available
|
|
166
|
+
embedding = true
|
|
167
|
+
log.debug('fn::embed() exists but returned an error (model may not be configured yet)', {
|
|
147
168
|
error: String(e),
|
|
148
169
|
})
|
|
149
170
|
}
|
|
150
|
-
} else {
|
|
151
|
-
// Other error (e.g. no model configured but function exists) — treat as available
|
|
152
|
-
embedding = true
|
|
153
|
-
log.debug('fn::embed() exists but returned an error (model may not be configured yet)', { error: String(e) })
|
|
154
171
|
}
|
|
155
172
|
}
|
|
156
173
|
|
|
@@ -158,9 +175,18 @@ export async function checkCompatibility(
|
|
|
158
175
|
const embedSkipped = !requireEmbedding && !embedding
|
|
159
176
|
|
|
160
177
|
if (ok) {
|
|
161
|
-
log.info('All preflight checks passed', {
|
|
178
|
+
log.info('All preflight checks passed', {
|
|
179
|
+
surrealVersion,
|
|
180
|
+
surrealkv,
|
|
181
|
+
retention_ok,
|
|
182
|
+
embedding,
|
|
183
|
+
embeddingProvider,
|
|
184
|
+
})
|
|
162
185
|
if (embedSkipped) {
|
|
163
|
-
log.info('fn::embed preflight check skipped due to non-surrealml embedding profile', {
|
|
186
|
+
log.info('fn::embed preflight check skipped due to non-surrealml embedding profile', {
|
|
187
|
+
context,
|
|
188
|
+
embeddingProvider,
|
|
189
|
+
})
|
|
164
190
|
}
|
|
165
191
|
} else {
|
|
166
192
|
log.error('Preflight checks failed', { errors })
|
|
@@ -172,8 +198,11 @@ export async function checkCompatibility(
|
|
|
172
198
|
/**
|
|
173
199
|
* Hard-exit variant. Call in CLI commands; throw in MCP server startup.
|
|
174
200
|
*/
|
|
175
|
-
export async function requireCompatibility(
|
|
176
|
-
|
|
201
|
+
export async function requireCompatibility(
|
|
202
|
+
db: Surreal,
|
|
203
|
+
options: CompatibilityOptions = {},
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
const result = await checkCompatibility(db, options)
|
|
177
206
|
if (!result.ok) {
|
|
178
207
|
const details = result.errors.map((err) => ` ✗ ${err}`).join('\n')
|
|
179
208
|
throw new Error(`\n[suemo] Compatibility check failed:\n\n${details}\n\nFix the issues above and retry.\n`)
|
package/src/db/schema.surql
CHANGED
|
@@ -38,6 +38,13 @@ DEFINE FIELD OVERWRITE consolidated_into ON memory
|
|
|
38
38
|
DEFINE FIELD OVERWRITE fsrs_stability ON memory TYPE option<float>;
|
|
39
39
|
DEFINE FIELD OVERWRITE fsrs_difficulty ON memory TYPE option<float>;
|
|
40
40
|
DEFINE FIELD OVERWRITE fsrs_next_review ON memory TYPE option<datetime>;
|
|
41
|
+
DEFINE FIELD OVERWRITE fsrs_state ON memory TYPE option<string>
|
|
42
|
+
ASSERT $value = NONE OR $value INSIDE ['new','learning','review','relearning'];
|
|
43
|
+
DEFINE FIELD OVERWRITE fsrs_last_review ON memory TYPE option<datetime>;
|
|
44
|
+
DEFINE FIELD OVERWRITE fsrs_reps ON memory TYPE option<int>;
|
|
45
|
+
DEFINE FIELD OVERWRITE fsrs_lapses ON memory TYPE option<int>;
|
|
46
|
+
DEFINE FIELD OVERWRITE fsrs_last_grade ON memory TYPE option<int>
|
|
47
|
+
ASSERT $value = NONE OR $value INSIDE [1,2,3,4];
|
|
41
48
|
|
|
42
49
|
-- Vector index: HNSW (keep syntax compatible with current SurrealDB target)
|
|
43
50
|
DEFINE INDEX OVERWRITE idx_memory_embedding
|
package/src/mcp/dispatch.ts
CHANGED
|
@@ -110,8 +110,23 @@ export async function handleToolCall(
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
case 'recall': {
|
|
113
|
-
const parsed = z.object({
|
|
114
|
-
|
|
113
|
+
const parsed = z.object({
|
|
114
|
+
nodeId: z.string(),
|
|
115
|
+
rating: z.number().int().min(1).max(4).optional(),
|
|
116
|
+
at: z.string().optional(),
|
|
117
|
+
dryRun: z.boolean().optional(),
|
|
118
|
+
}).parse(params)
|
|
119
|
+
log.debug('Dispatch recall tool', {
|
|
120
|
+
nodeId: parsed.nodeId,
|
|
121
|
+
rating: parsed.rating ?? 3,
|
|
122
|
+
hasAt: parsed.at !== undefined,
|
|
123
|
+
dryRun: parsed.dryRun ?? false,
|
|
124
|
+
})
|
|
125
|
+
return recall(db, parsed.nodeId, {
|
|
126
|
+
...(parsed.rating !== undefined ? { rating: parsed.rating as 1 | 2 | 3 | 4 } : {}),
|
|
127
|
+
...(parsed.at !== undefined ? { at: parsed.at } : {}),
|
|
128
|
+
...(parsed.dryRun !== undefined ? { dryRun: parsed.dryRun } : {}),
|
|
129
|
+
})
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
case 'wander': {
|
|
@@ -143,9 +158,20 @@ export async function handleToolCall(
|
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
case 'context': {
|
|
146
|
-
const parsed = z.object({
|
|
161
|
+
const parsed = z.object({
|
|
162
|
+
scope: z.string().optional(),
|
|
163
|
+
sessionId: z.string().optional(),
|
|
164
|
+
limit: z.number().optional(),
|
|
165
|
+
}).parse(params)
|
|
166
|
+
const resolvedScope = parsed.scope?.trim() || inferredScope
|
|
167
|
+
log.debug('Dispatch context tool', {
|
|
168
|
+
scope: resolvedScope,
|
|
169
|
+
sessionId: parsed.sessionId ?? null,
|
|
170
|
+
limit: parsed.limit ?? null,
|
|
171
|
+
})
|
|
147
172
|
return context(db, {
|
|
148
|
-
scope:
|
|
173
|
+
scope: resolvedScope,
|
|
174
|
+
...(parsed.sessionId !== undefined ? { sessionId: parsed.sessionId } : {}),
|
|
149
175
|
...(parsed.limit !== undefined ? { limit: parsed.limit } : {}),
|
|
150
176
|
})
|
|
151
177
|
}
|
|
@@ -205,7 +231,7 @@ export async function handleToolCall(
|
|
|
205
231
|
}
|
|
206
232
|
|
|
207
233
|
case 'health':
|
|
208
|
-
return healthReport(db)
|
|
234
|
+
return healthReport(db, { embeddingProvider: config.embedding.provider })
|
|
209
235
|
|
|
210
236
|
case 'stats':
|
|
211
237
|
return suemoStats(db)
|
package/src/mcp/server.ts
CHANGED
|
@@ -127,7 +127,14 @@ export async function startMcpServer(config: SuemoConfig): Promise<void> {
|
|
|
127
127
|
projectDir: config.main?.projectDir ?? '.ua',
|
|
128
128
|
})
|
|
129
129
|
const db = await connect(config.surreal)
|
|
130
|
-
|
|
130
|
+
log.debug('Running MCP HTTP compatibility preflight', {
|
|
131
|
+
embeddingProvider: config.embedding.provider,
|
|
132
|
+
context: 'mcp:http-startup',
|
|
133
|
+
})
|
|
134
|
+
await requireCompatibility(db, {
|
|
135
|
+
embeddingProvider: config.embedding.provider,
|
|
136
|
+
context: 'mcp:http-startup',
|
|
137
|
+
})
|
|
131
138
|
await runSchema(db)
|
|
132
139
|
const autoSync = createAutoSyncRunner(db, config)
|
|
133
140
|
autoSync.start()
|
|
@@ -163,7 +170,13 @@ export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
|
163
170
|
const autoSync = createAutoSyncRunner(db, config)
|
|
164
171
|
autoSync.start()
|
|
165
172
|
try {
|
|
173
|
+
log.debug('Running MCP stdio compatibility preflight', {
|
|
174
|
+
embeddingProvider: config.embedding.provider,
|
|
175
|
+
requireEmbedding: config.embedding.provider === 'surrealml',
|
|
176
|
+
context: 'mcp:stdio-startup',
|
|
177
|
+
})
|
|
166
178
|
const compat = await checkCompatibility(db, {
|
|
179
|
+
embeddingProvider: config.embedding.provider,
|
|
167
180
|
requireEmbedding: config.embedding.provider === 'surrealml',
|
|
168
181
|
context: 'mcp:stdio-startup',
|
|
169
182
|
})
|