suemo 0.1.7 → 0.1.8

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 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
- Canonical behavioral specification: **[`SPEC.md`](./SPEC.md)**.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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": {
@@ -56,6 +57,7 @@
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
  }
@@ -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.7
6
+ version: 0.1.8
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.7)
13
+ ## Strict defaults (v0.1.8)
14
14
 
15
15
  - Always run pre-checks before implementation:
16
16
  - `goal_list({ scope })`
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agents-snippet
3
3
  description: AGENTS.md snippet optimized for suemo skill discovery and usage.
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  ---
6
6
 
7
7
  # AGENTS.md snippet
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: cli-reference
3
3
  description: CLI command reference for suemo v0.0.6 including skill access.
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  ---
6
6
 
7
7
  # CLI reference
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: core-workflow
3
3
  description: Canonical suemo operating loop for OpenCode agents.
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  ---
6
6
 
7
7
  # Core workflow
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: manual-test-plan
3
3
  description: Comprehensive manual test matrix for suemo features and commands.
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  ---
6
6
 
7
7
  # Manual test plan
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: mcp-reference
3
3
  description: MCP tool reference for suemo v0.0.6.
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  ---
6
6
 
7
7
  # MCP tools
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: schema-retention-longevity
3
3
  description: Long-term schema and retention design expectations for suemo.
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  ---
6
6
 
7
7
  # Schema + retention longevity
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: sync-local-vps
3
3
  description: Two-way sync manual scenario for local and VPS SurrealDB.
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  ---
6
6
 
7
7
  # Local ↔ VPS sync scenario
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.7 | Updated: 2026-03-23 | Summary: stricter schema + explicit completion contract_
130
+ _Version: 0.1.8 | 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 result = await recall(db, args.nodeId)
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
  {
@@ -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(db: Surreal): Promise<HealthReport> {
9
- log.debug('healthReport()')
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
@@ -2,9 +2,9 @@
2
2
  // createRemoteEngines() handles ws://, wss://, http://, https:// connections.
3
3
  // createNodeEngines() patches in the Node.js WebSocket implementation.
4
4
  import type { SurrealTarget } from '@/src/config.ts'
5
+ import { createSurrealEngines, toSurrealConnectionError } from '@/src/db/engines.ts'
5
6
  import { getLogger } from '@/src/logger.ts'
6
- import { createNodeEngines } from '@surrealdb/node'
7
- import { createRemoteEngines, Surreal } from 'surrealdb'
7
+ import { Surreal } from 'surrealdb'
8
8
 
9
9
  const log = getLogger(['suemo', 'db', 'client'])
10
10
 
@@ -19,30 +19,33 @@ export async function connect(target: SurrealTarget): Promise<Surreal> {
19
19
  db: target.database,
20
20
  })
21
21
 
22
- // v2 SDK: engines are registered at construction time, not at connect.
23
- // Both remote (ws/wss/http) and Node.js engines are needed for Bun.
24
- const db = new Surreal({
25
- engines: {
26
- ...createRemoteEngines(),
27
- ...createNodeEngines(),
28
- },
29
- })
30
-
31
- // v2 SDK: namespace, database, and authentication can all be passed inline
32
- // to connect(). The `authentication` callback is invoked on connect and on
33
- // any automatic reconnection/token-refresh cycle.
34
- await db.connect(target.url, {
35
- namespace: target.namespace,
36
- database: target.database,
37
- authentication: () => ({
38
- username: target.auth.user,
39
- password: target.auth.pass,
40
- }),
41
- })
42
-
43
- _db = db
44
- log.debug('Connected')
45
- return db
22
+ try {
23
+ const engines = await createSurrealEngines({
24
+ url: target.url,
25
+ context: 'db:connect',
26
+ })
27
+
28
+ // v2 SDK: engines are registered at construction time, not at connect.
29
+ const db = new Surreal({ engines })
30
+
31
+ // v2 SDK: namespace, database, and authentication can all be passed inline
32
+ // to connect(). The `authentication` callback is invoked on connect and on
33
+ // any automatic reconnection/token-refresh cycle.
34
+ await db.connect(target.url, {
35
+ namespace: target.namespace,
36
+ database: target.database,
37
+ authentication: () => ({
38
+ username: target.auth.user,
39
+ password: target.auth.pass,
40
+ }),
41
+ })
42
+
43
+ _db = db
44
+ log.debug('Connected')
45
+ return db
46
+ } catch (error) {
47
+ throw toSurrealConnectionError(error, 'db:connect', target.url)
48
+ }
46
49
  }
47
50
 
48
51
  export function getDb(): Surreal {
@@ -0,0 +1,113 @@
1
+ import { getLogger } from '@/src/logger.ts'
2
+ import { createRemoteEngines } from 'surrealdb'
3
+
4
+ type SurrealEngines = ReturnType<typeof createRemoteEngines>
5
+
6
+ const log = getLogger(['suemo', 'db', 'engines'])
7
+
8
+ export interface CreateSurrealEnginesOptions {
9
+ url: string
10
+ context: string
11
+ loadNodeEngines?: () => Promise<SurrealEngines>
12
+ }
13
+
14
+ const REMOTE_SCHEME_RE = /^(https?|wss?):\/\//i
15
+
16
+ export function isRemoteSurrealUrl(url: string): boolean {
17
+ return REMOTE_SCHEME_RE.test(url)
18
+ }
19
+
20
+ function isLikelyArm64NativeIssue(message: string): boolean {
21
+ return message.includes('_ZTVSt12bad_weak_ptr')
22
+ || message.includes('surrealdb-node.linux-arm64-gnu.node')
23
+ || message.includes('surrealdb-node.linux-arm64-musl.node')
24
+ || message.includes('Cannot find native binding')
25
+ || message.includes('@surrealdb/node')
26
+ }
27
+
28
+ function buildArm64NativeBuildGuidance(): string {
29
+ return [
30
+ '@surrealdb/node native binding failed on linux arm64.',
31
+ 'Automatic install of @surrealdb/node-linux-* is not attempted by suemo because npm package availability/versioning is inconsistent across arm64 variants.',
32
+ 'Recommended workaround: build native binding locally and replace the bundled binary:',
33
+ ' git clone https://github.com/surrealdb/surrealdb.js',
34
+ ' cd surrealdb.js/packages/node',
35
+ ' bun add -g @napi-rs/cli',
36
+ ' bunx napi build --platform --release',
37
+ ' cp surrealdb-node.linux-arm64-gnu.node ~/.bun/install/global/node_modules/@surrealdb/node/dist/',
38
+ 'If your environment is musl-based, replace the destination file accordingly.',
39
+ 'Also consider filing/upvoting an upstream issue for arm64 portability.',
40
+ ].join('\n')
41
+ }
42
+
43
+ function wrapArm64NativeError(error: unknown, context: string, url: string): Error {
44
+ const base = error instanceof Error ? error.message : String(error)
45
+ const details = [
46
+ `SurrealDB native engine initialization failed (${context}) for target: ${url}`,
47
+ base,
48
+ ]
49
+
50
+ if (process.platform === 'linux' && process.arch === 'arm64') {
51
+ details.push('', buildArm64NativeBuildGuidance())
52
+ }
53
+
54
+ return new Error(details.join('\n'))
55
+ }
56
+
57
+ async function defaultLoadNodeEngines(): Promise<SurrealEngines> {
58
+ const mod = await import('@surrealdb/node')
59
+ if (typeof mod.createNodeEngines !== 'function') {
60
+ throw new Error('@surrealdb/node loaded without createNodeEngines export')
61
+ }
62
+ const engines = mod.createNodeEngines()
63
+ if (!engines || typeof engines !== 'object') {
64
+ throw new Error('@surrealdb/node createNodeEngines() returned invalid value')
65
+ }
66
+ return engines as SurrealEngines
67
+ }
68
+
69
+ export async function createSurrealEngines(options: CreateSurrealEnginesOptions): Promise<SurrealEngines> {
70
+ const remoteEngines = createRemoteEngines() as SurrealEngines
71
+ const loadNodeEngines = options.loadNodeEngines ?? defaultLoadNodeEngines
72
+
73
+ try {
74
+ const nodeEngines = await loadNodeEngines()
75
+ log.debug('Loaded @surrealdb/node engines', {
76
+ context: options.context,
77
+ url: options.url,
78
+ nodeEngineCount: Object.keys(nodeEngines).length,
79
+ })
80
+ return { ...remoteEngines, ...nodeEngines }
81
+ } catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error)
83
+ const remoteUrl = isRemoteSurrealUrl(options.url)
84
+
85
+ log.warning('Unable to initialize @surrealdb/node engines', {
86
+ context: options.context,
87
+ url: options.url,
88
+ platform: process.platform,
89
+ arch: process.arch,
90
+ remoteUrl,
91
+ error: message,
92
+ })
93
+
94
+ if (!remoteUrl || isLikelyArm64NativeIssue(message)) {
95
+ throw wrapArm64NativeError(error, options.context, options.url)
96
+ }
97
+
98
+ log.info('Falling back to remote-only engines', {
99
+ context: options.context,
100
+ url: options.url,
101
+ })
102
+ return remoteEngines
103
+ }
104
+ }
105
+
106
+ export function toSurrealConnectionError(error: unknown, context: string, url: string): Error {
107
+ const message = error instanceof Error ? error.message : String(error)
108
+ if (isLikelyArm64NativeIssue(message)) {
109
+ return wrapArm64NativeError(error, context, url)
110
+ }
111
+ if (error instanceof Error) return error
112
+ return new Error(String(error))
113
+ }
@@ -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 requireEmbedding = options.requireEmbedding ?? true
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
- try {
130
- // fn::embed requires the ML module and a configured model.
131
- // If it throws "No embedding model configured" that's acceptable — the
132
- // function exists. If it throws "Unknown function 'fn::embed'" → not available.
133
- await db.query(`RETURN fn::embed("suemo preflight test")`)
134
- embedding = true
135
- log.debug('Embedding function probe passed')
136
- } catch (e: unknown) {
137
- const msg = String(e).toLowerCase()
138
- if (msg.includes('unknown function') || msg.includes('fn::embed')) {
139
- embedding = false
140
- if (requireEmbedding) {
141
- errors.push(
142
- 'fn::embed() is not available in this SurrealDB database. Import/configure a SurrealML embedding model for this namespace/database, then retry.',
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
- log.warn('Embedding function unavailable; continuing due to non-strict preflight mode', {
146
- context,
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', { surrealVersion, surrealkv, retention_ok, embedding })
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', { context })
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(db: Surreal): Promise<void> {
176
- const result = await checkCompatibility(db)
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`)
@@ -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