suemo 0.0.1 → 0.0.3
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 +127 -27
- package/package.json +1 -1
- package/src/cli/commands/believe.ts +22 -12
- package/src/cli/commands/consolidate.ts +18 -11
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/export-import.ts +83 -47
- package/src/cli/commands/goal.ts +52 -27
- package/src/cli/commands/health.ts +53 -18
- package/src/cli/commands/init.ts +155 -75
- package/src/cli/commands/observe.ts +26 -13
- package/src/cli/commands/query.ts +23 -7
- package/src/cli/commands/recall.ts +12 -6
- package/src/cli/commands/serve.ts +25 -6
- package/src/cli/commands/sync.ts +44 -10
- package/src/cli/commands/timeline.ts +30 -18
- package/src/cli/commands/wander.ts +27 -16
- package/src/cli/index.ts +3 -4
- package/src/cli/shared.ts +34 -0
- package/src/cognitive/consolidate.ts +48 -19
- package/src/cognitive/contradiction.ts +19 -7
- package/src/cognitive/health.ts +61 -1
- package/src/config.template.ts +58 -0
- package/src/config.ts +124 -14
- package/src/db/preflight.ts +32 -6
- package/src/db/schema.surql +30 -9
- package/src/db/schema.ts +6 -3
- package/src/embedding/index.ts +52 -0
- package/src/embedding/openai-compatible.ts +43 -0
- package/src/goal.ts +3 -1
- package/src/index.ts +5 -1
- package/src/mcp/dispatch.ts +232 -0
- package/src/mcp/server.ts +150 -4
- package/src/mcp/stdio.ts +385 -0
- package/src/mcp/tools.ts +13 -90
- package/src/memory/episode.ts +92 -0
- package/src/memory/read.ts +76 -19
- package/src/memory/write.ts +253 -20
- package/src/sync.ts +310 -66
- package/src/types.ts +30 -5
- package/src/cli/commands/shared.ts +0 -20
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join, resolve } from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { getLogger } from './logger.ts'
|
|
4
5
|
|
|
5
6
|
// ── Config shape ──────────────────────────────────────────────────────────────
|
|
6
7
|
|
|
@@ -17,8 +18,9 @@ export interface SurrealTarget {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export type EmbeddingProvider =
|
|
21
|
+
| { provider: 'openai-compatible'; url: string; model: string; dimension: number; apiKey?: string }
|
|
20
22
|
| { provider: 'surreal'; dimension: number }
|
|
21
|
-
| { provider: 'stub'; dimension: number }
|
|
23
|
+
| { provider: 'stub'; dimension: number }
|
|
22
24
|
|
|
23
25
|
export interface LLMConfig {
|
|
24
26
|
url: string // OpenAI-compatible endpoint
|
|
@@ -49,9 +51,41 @@ export interface McpConfig {
|
|
|
49
51
|
host: string
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
export type SyncDirectionConfig = 'push' | 'pull' | 'both'
|
|
55
|
+
|
|
56
|
+
export interface SyncAutoConfig {
|
|
57
|
+
enabled?: boolean
|
|
58
|
+
intervalSeconds?: number
|
|
59
|
+
direction?: SyncDirectionConfig
|
|
60
|
+
remote?: string
|
|
61
|
+
onWrite?: boolean
|
|
62
|
+
minWriteIntervalSeconds?: number
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
export interface SyncConfig {
|
|
53
|
-
remote
|
|
54
|
-
|
|
66
|
+
/** Legacy single-remote config (still supported for migration safety). */
|
|
67
|
+
remote?: SurrealTarget
|
|
68
|
+
/** Named remotes for selectable sync targets. */
|
|
69
|
+
remotes?: Record<string, SurrealTarget>
|
|
70
|
+
/** Default key from `remotes` when no explicit remote is selected. */
|
|
71
|
+
defaultRemote?: string
|
|
72
|
+
/** Optional auto-sync behavior for long-running MCP servers. */
|
|
73
|
+
auto?: SyncAutoConfig
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ResolvedSyncAutoConfig {
|
|
77
|
+
enabled: boolean
|
|
78
|
+
intervalSeconds: number
|
|
79
|
+
direction: SyncDirectionConfig
|
|
80
|
+
remote: string
|
|
81
|
+
onWrite: boolean
|
|
82
|
+
minWriteIntervalSeconds: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ResolvedSyncConfig {
|
|
86
|
+
remotes: Record<string, SurrealTarget>
|
|
87
|
+
defaultRemote: string
|
|
88
|
+
auto: ResolvedSyncAutoConfig
|
|
55
89
|
}
|
|
56
90
|
|
|
57
91
|
export interface SuemoConfig {
|
|
@@ -69,6 +103,55 @@ export function defineConfig(config: SuemoConfig): SuemoConfig {
|
|
|
69
103
|
return config
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
export function resolveSyncConfig(config: SuemoConfig): ResolvedSyncConfig | null {
|
|
107
|
+
if (!config.sync) return null
|
|
108
|
+
|
|
109
|
+
const remotes: Record<string, SurrealTarget> = config.sync.remotes
|
|
110
|
+
? { ...config.sync.remotes }
|
|
111
|
+
: (config.sync.remote ? { default: config.sync.remote } : {})
|
|
112
|
+
|
|
113
|
+
const remoteNames = Object.keys(remotes)
|
|
114
|
+
if (remoteNames.length === 0) return null
|
|
115
|
+
|
|
116
|
+
const defaultRemote = config.sync.defaultRemote ?? remoteNames[0]!
|
|
117
|
+
if (!remotes[defaultRemote]) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`sync.defaultRemote \"${defaultRemote}\" does not exist in sync.remotes. Available: ${remoteNames.join(', ')}`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const auto = config.sync.auto ?? {}
|
|
124
|
+
const intervalSeconds = auto.intervalSeconds ?? 300
|
|
125
|
+
const minWriteIntervalSeconds = auto.minWriteIntervalSeconds ?? 30
|
|
126
|
+
|
|
127
|
+
if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
|
|
128
|
+
throw new Error('sync.auto.intervalSeconds must be a positive number')
|
|
129
|
+
}
|
|
130
|
+
if (!Number.isFinite(minWriteIntervalSeconds) || minWriteIntervalSeconds < 0) {
|
|
131
|
+
throw new Error('sync.auto.minWriteIntervalSeconds must be >= 0')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const autoRemote = auto.remote ?? defaultRemote
|
|
135
|
+
if (!remotes[autoRemote]) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`sync.auto.remote \"${autoRemote}\" does not exist in sync.remotes. Available: ${remoteNames.join(', ')}`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
remotes,
|
|
143
|
+
defaultRemote,
|
|
144
|
+
auto: {
|
|
145
|
+
enabled: auto.enabled ?? false,
|
|
146
|
+
intervalSeconds,
|
|
147
|
+
direction: auto.direction ?? 'push',
|
|
148
|
+
remote: autoRemote,
|
|
149
|
+
onWrite: auto.onWrite ?? false,
|
|
150
|
+
minWriteIntervalSeconds,
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
72
155
|
// ── loadConfig — resolution chain ────────────────────────────────────────────
|
|
73
156
|
|
|
74
157
|
const CONFIG_CANDIDATES = [
|
|
@@ -76,11 +159,21 @@ const CONFIG_CANDIDATES = [
|
|
|
76
159
|
'suemo.config.js',
|
|
77
160
|
]
|
|
78
161
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
162
|
+
const log = getLogger(['suemo', 'config'])
|
|
163
|
+
|
|
164
|
+
function getHomeConfigPath(): string | null {
|
|
165
|
+
const home = process.env.HOME ?? process.env.USERPROFILE
|
|
166
|
+
if (!home) return null
|
|
167
|
+
return join(home, '.suemo', 'suemo.ts')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveConfigPath(p: string): string {
|
|
171
|
+
if (p.startsWith('~/')) {
|
|
172
|
+
const home = process.env.HOME ?? process.env.USERPROFILE
|
|
173
|
+
if (home) return resolve(home, p.slice(2))
|
|
174
|
+
}
|
|
175
|
+
return resolve(p)
|
|
176
|
+
}
|
|
84
177
|
|
|
85
178
|
export async function loadConfig(
|
|
86
179
|
cwd = process.cwd(),
|
|
@@ -88,23 +181,40 @@ export async function loadConfig(
|
|
|
88
181
|
): Promise<SuemoConfig> {
|
|
89
182
|
// 1. Explicit --config flag
|
|
90
183
|
if (overridePath) {
|
|
91
|
-
|
|
184
|
+
const resolved = resolveConfigPath(overridePath)
|
|
185
|
+
log.debug('Loading config from --config', { path: resolved })
|
|
186
|
+
return importConfig(resolved)
|
|
187
|
+
}
|
|
188
|
+
// 2. Environment override
|
|
189
|
+
const envConfigPath = process.env.SUEMO_CONFIG_PATH?.trim()
|
|
190
|
+
if (envConfigPath) {
|
|
191
|
+
const resolved = resolveConfigPath(envConfigPath)
|
|
192
|
+
log.debug('Loading config from SUEMO_CONFIG_PATH', { path: resolved })
|
|
193
|
+
return importConfig(resolved)
|
|
92
194
|
}
|
|
93
|
-
//
|
|
195
|
+
// 3. Project-local
|
|
94
196
|
for (const name of CONFIG_CANDIDATES) {
|
|
95
197
|
const p = resolve(cwd, name)
|
|
96
|
-
if (existsSync(p))
|
|
198
|
+
if (existsSync(p)) {
|
|
199
|
+
log.debug('Loading project-local config', { path: p })
|
|
200
|
+
return importConfig(p)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 4. User-level
|
|
204
|
+
const homeConfig = getHomeConfigPath()
|
|
205
|
+
if (homeConfig && existsSync(homeConfig)) {
|
|
206
|
+
log.debug('Loading user-level config', { path: homeConfig })
|
|
207
|
+
return importConfig(homeConfig)
|
|
97
208
|
}
|
|
98
|
-
// 3. User-level
|
|
99
|
-
if (existsSync(HOME_CONFIG)) return importConfig(HOME_CONFIG)
|
|
100
209
|
|
|
101
210
|
throw new Error(
|
|
102
211
|
'No suemo config found.\n'
|
|
103
|
-
+ 'Run `suemo init` to create ~/.suemo/suemo.ts, or create suemo.config.ts in the project root.',
|
|
212
|
+
+ 'Run `suemo init config` to create ~/.suemo/suemo.ts, set SUEMO_CONFIG_PATH, or create suemo.config.ts in the project root.',
|
|
104
213
|
)
|
|
105
214
|
}
|
|
106
215
|
|
|
107
216
|
async function importConfig(path: string): Promise<SuemoConfig> {
|
|
217
|
+
log.debug('Importing config module', { path })
|
|
108
218
|
const mod = await import(pathToFileURL(path).href)
|
|
109
219
|
const cfg: unknown = mod.default ?? mod
|
|
110
220
|
if (!cfg || typeof cfg !== 'object') {
|
package/src/db/preflight.ts
CHANGED
|
@@ -12,18 +12,33 @@ export interface CompatibilityResult {
|
|
|
12
12
|
errors: string[]
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface CompatibilityOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Whether missing fn::embed() should fail compatibility.
|
|
18
|
+
* Keep true for runtime paths that execute vector/embed queries.
|
|
19
|
+
*/
|
|
20
|
+
requireEmbedding?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Human-readable label for logging this preflight execution.
|
|
23
|
+
*/
|
|
24
|
+
context?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
const MIN_MAJOR = 3
|
|
16
28
|
|
|
17
29
|
export async function checkCompatibility(
|
|
18
30
|
db: Surreal,
|
|
31
|
+
options: CompatibilityOptions = {},
|
|
19
32
|
): Promise<CompatibilityResult> {
|
|
20
33
|
const errors: string[] = []
|
|
21
34
|
let surrealVersion = 'unknown'
|
|
22
35
|
let surrealkv = false
|
|
23
36
|
let retention_ok = false
|
|
24
37
|
let embedding = false
|
|
38
|
+
const requireEmbedding = options.requireEmbedding ?? true
|
|
39
|
+
const context = options.context ?? 'default'
|
|
25
40
|
|
|
26
|
-
log.info('Running preflight compatibility checks')
|
|
41
|
+
log.info('Running preflight compatibility checks', { requireEmbedding, context })
|
|
27
42
|
|
|
28
43
|
// ── Check 1: version string ───────────────────────────────────────────────
|
|
29
44
|
try {
|
|
@@ -41,6 +56,7 @@ export async function checkCompatibility(
|
|
|
41
56
|
)
|
|
42
57
|
}
|
|
43
58
|
}
|
|
59
|
+
log.debug('SurrealDB version check complete', { surrealVersion })
|
|
44
60
|
} catch (e) {
|
|
45
61
|
errors.push(`Cannot retrieve SurrealDB version: ${String(e)}`)
|
|
46
62
|
}
|
|
@@ -54,6 +70,7 @@ export async function checkCompatibility(
|
|
|
54
70
|
// if it was around then), not throw. An error means VERSION is unsupported.
|
|
55
71
|
await db.query("SELECT * FROM suemo_preflight:probe VERSION d'2020-01-01T00:00:00Z'")
|
|
56
72
|
surrealkv = true
|
|
73
|
+
log.debug('SurrealKV VERSION probe passed')
|
|
57
74
|
} catch (e: unknown) {
|
|
58
75
|
const msg = String(e).toLowerCase()
|
|
59
76
|
if (msg.includes('version') || msg.includes('surrealkv') || msg.includes('not supported')) {
|
|
@@ -83,6 +100,7 @@ export async function checkCompatibility(
|
|
|
83
100
|
await db.query('CREATE suemo_retention_probe:v SET t = time::now()')
|
|
84
101
|
await db.query(`SELECT * FROM suemo_retention_probe:v VERSION d'${ninetyDaysAgo}'`)
|
|
85
102
|
retention_ok = true
|
|
103
|
+
log.debug('Retention probe passed', { ninetyDaysAgo })
|
|
86
104
|
} catch (e: unknown) {
|
|
87
105
|
const msg = String(e).toLowerCase()
|
|
88
106
|
if (msg.includes('retention') || msg.includes('version')) {
|
|
@@ -104,20 +122,28 @@ export async function checkCompatibility(
|
|
|
104
122
|
|
|
105
123
|
// ── Check 4: fn::embed() resolves ────────────────────────────────────────
|
|
106
124
|
// We don't actually embed anything — we just check that the function exists.
|
|
107
|
-
// An "Unknown function" error means
|
|
125
|
+
// An "Unknown function" error means embedding runtime is unavailable.
|
|
108
126
|
try {
|
|
109
127
|
// fn::embed requires the ML module and a configured model.
|
|
110
128
|
// If it throws "No embedding model configured" that's acceptable — the
|
|
111
129
|
// function exists. If it throws "Unknown function 'fn::embed'" → not available.
|
|
112
130
|
await db.query(`RETURN fn::embed("suemo preflight test")`)
|
|
113
131
|
embedding = true
|
|
132
|
+
log.debug('Embedding function probe passed')
|
|
114
133
|
} catch (e: unknown) {
|
|
115
134
|
const msg = String(e).toLowerCase()
|
|
116
135
|
if (msg.includes('unknown function') || msg.includes('fn::embed')) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
embedding = false
|
|
137
|
+
if (requireEmbedding) {
|
|
138
|
+
errors.push(
|
|
139
|
+
'fn::embed() is not available in this SurrealDB database. Import/configure a SurrealML embedding model for this namespace/database, then retry.',
|
|
140
|
+
)
|
|
141
|
+
} else {
|
|
142
|
+
log.warn('Embedding function unavailable; continuing due to non-strict preflight mode', {
|
|
143
|
+
context,
|
|
144
|
+
error: String(e),
|
|
145
|
+
})
|
|
146
|
+
}
|
|
121
147
|
} else {
|
|
122
148
|
// Other error (e.g. no model configured but function exists) — treat as available
|
|
123
149
|
embedding = true
|
package/src/db/schema.surql
CHANGED
|
@@ -13,6 +13,7 @@ DEFINE FIELD OVERWRITE content ON memory TYPE string;
|
|
|
13
13
|
DEFINE FIELD OVERWRITE summary ON memory TYPE option<string>;
|
|
14
14
|
DEFINE FIELD OVERWRITE tags ON memory TYPE set<string> DEFAULT {};
|
|
15
15
|
DEFINE FIELD OVERWRITE scope ON memory TYPE option<string>;
|
|
16
|
+
DEFINE FIELD OVERWRITE topic_key ON memory TYPE option<string> DEFAULT NONE;
|
|
16
17
|
DEFINE FIELD OVERWRITE embedding ON memory TYPE array<float> DEFAULT [];
|
|
17
18
|
DEFINE FIELD OVERWRITE confidence ON memory TYPE float DEFAULT 1.0;
|
|
18
19
|
DEFINE FIELD OVERWRITE salience ON memory TYPE float DEFAULT 0.5;
|
|
@@ -24,33 +25,37 @@ DEFINE FIELD OVERWRITE valid_from ON memory TYPE datetime DEFAULT time::
|
|
|
24
25
|
DEFINE FIELD OVERWRITE valid_until ON memory TYPE option<datetime> DEFAULT NONE;
|
|
25
26
|
|
|
26
27
|
DEFINE FIELD OVERWRITE source ON memory TYPE option<string>;
|
|
28
|
+
DEFINE FIELD OVERWRITE prompt_source ON memory TYPE option<record<memory>> DEFAULT NONE;
|
|
27
29
|
DEFINE FIELD OVERWRITE created_at ON memory TYPE datetime DEFAULT time::now();
|
|
28
30
|
DEFINE FIELD OVERWRITE updated_at ON memory TYPE datetime DEFAULT time::now();
|
|
29
31
|
DEFINE FIELD OVERWRITE consolidated ON memory TYPE bool DEFAULT false;
|
|
30
32
|
|
|
31
33
|
-- REFERENCES: bidirectional record link (SurrealDB v3.0)
|
|
32
34
|
DEFINE FIELD OVERWRITE consolidated_into ON memory
|
|
33
|
-
TYPE option<record<memory>>
|
|
35
|
+
TYPE option<record<memory>> REFERENCE;
|
|
34
36
|
|
|
35
37
|
-- FSRS spaced-repetition fields (all optional; populated on first recall())
|
|
36
38
|
DEFINE FIELD OVERWRITE fsrs_stability ON memory TYPE option<float>;
|
|
37
39
|
DEFINE FIELD OVERWRITE fsrs_difficulty ON memory TYPE option<float>;
|
|
38
40
|
DEFINE FIELD OVERWRITE fsrs_next_review ON memory TYPE option<datetime>;
|
|
39
41
|
|
|
40
|
-
-- Vector index: HNSW
|
|
42
|
+
-- Vector index: HNSW (keep syntax compatible with current SurrealDB target)
|
|
41
43
|
DEFINE INDEX OVERWRITE idx_memory_embedding
|
|
42
44
|
ON memory FIELDS embedding
|
|
43
|
-
HNSW DIMENSION
|
|
44
|
-
DEFER;
|
|
45
|
+
HNSW DIMENSION 384 DIST COSINE;
|
|
45
46
|
|
|
46
47
|
-- Full-text index: BM25 over content + summary
|
|
47
48
|
DEFINE ANALYZER OVERWRITE suemo_analyzer
|
|
48
49
|
TOKENIZERS blank,class
|
|
49
50
|
FILTERS lowercase,snowball(english);
|
|
50
51
|
|
|
51
|
-
DEFINE INDEX OVERWRITE
|
|
52
|
-
ON memory FIELDS content
|
|
53
|
-
|
|
52
|
+
DEFINE INDEX OVERWRITE idx_memory_content_fts
|
|
53
|
+
ON memory FIELDS content
|
|
54
|
+
FULLTEXT ANALYZER suemo_analyzer BM25;
|
|
55
|
+
|
|
56
|
+
DEFINE INDEX OVERWRITE idx_memory_summary_fts
|
|
57
|
+
ON memory FIELDS summary
|
|
58
|
+
FULLTEXT ANALYZER suemo_analyzer BM25;
|
|
54
59
|
|
|
55
60
|
-- Compound indexes for timeline and filter queries
|
|
56
61
|
DEFINE INDEX OVERWRITE idx_memory_scope_time
|
|
@@ -62,6 +67,9 @@ DEFINE INDEX OVERWRITE idx_memory_kind_valid
|
|
|
62
67
|
DEFINE INDEX OVERWRITE idx_memory_salience
|
|
63
68
|
ON memory FIELDS salience;
|
|
64
69
|
|
|
70
|
+
DEFINE INDEX OVERWRITE idx_memory_topic_key
|
|
71
|
+
ON memory FIELDS topic_key;
|
|
72
|
+
|
|
65
73
|
-- ── relates_to ───────────────────────────────────────────────────────────────
|
|
66
74
|
DEFINE TABLE OVERWRITE relates_to SCHEMAFULL
|
|
67
75
|
TYPE RELATION IN memory OUT memory;
|
|
@@ -73,6 +81,7 @@ DEFINE FIELD OVERWRITE strength ON relates_to TYPE float DEFAULT 0.5;
|
|
|
73
81
|
DEFINE FIELD OVERWRITE valid_from ON relates_to TYPE datetime DEFAULT time::now();
|
|
74
82
|
DEFINE FIELD OVERWRITE valid_until ON relates_to TYPE option<datetime> DEFAULT NONE;
|
|
75
83
|
DEFINE FIELD OVERWRITE created_at ON relates_to TYPE datetime DEFAULT time::now();
|
|
84
|
+
DEFINE FIELD OVERWRITE updated_at ON relates_to TYPE datetime DEFAULT time::now();
|
|
76
85
|
|
|
77
86
|
-- ── episode ───────────────────────────────────────────────────────────────────
|
|
78
87
|
DEFINE TABLE OVERWRITE episode SCHEMAFULL;
|
|
@@ -81,10 +90,11 @@ DEFINE FIELD OVERWRITE session_id ON episode TYPE string;
|
|
|
81
90
|
DEFINE FIELD OVERWRITE started_at ON episode TYPE datetime DEFAULT time::now();
|
|
82
91
|
DEFINE FIELD OVERWRITE ended_at ON episode TYPE option<datetime> DEFAULT NONE;
|
|
83
92
|
DEFINE FIELD OVERWRITE summary ON episode TYPE option<string>;
|
|
93
|
+
DEFINE FIELD OVERWRITE context ON episode TYPE option<object> DEFAULT NONE;
|
|
84
94
|
|
|
85
95
|
-- REFERENCES: bidirectional array of memory links
|
|
86
96
|
DEFINE FIELD OVERWRITE memory_ids ON episode
|
|
87
|
-
TYPE array<record<memory>>
|
|
97
|
+
TYPE array<record<memory>> REFERENCE
|
|
88
98
|
DEFAULT [];
|
|
89
99
|
|
|
90
100
|
-- ── consolidation_run ────────────────────────────────────────────────────────
|
|
@@ -102,8 +112,19 @@ DEFINE FIELD OVERWRITE error ON consolidation_run TYPE option<string> DEF
|
|
|
102
112
|
|
|
103
113
|
-- ── sync_cursor ───────────────────────────────────────────────────────────────
|
|
104
114
|
-- One record per (remote.url, remote.ns, remote.db) triple.
|
|
105
|
-
-- Stores
|
|
115
|
+
-- Stores per-remote cursors for push/pull sync based on updated_at.
|
|
106
116
|
DEFINE TABLE OVERWRITE sync_cursor SCHEMAFULL;
|
|
107
117
|
DEFINE FIELD OVERWRITE remote_key ON sync_cursor TYPE string; -- sha1(url+ns+db)
|
|
108
118
|
DEFINE FIELD OVERWRITE cursor ON sync_cursor TYPE datetime;
|
|
119
|
+
DEFINE FIELD OVERWRITE push_cursor ON sync_cursor TYPE datetime DEFAULT d'1970-01-01T00:00:00Z';
|
|
120
|
+
DEFINE FIELD OVERWRITE pull_cursor ON sync_cursor TYPE datetime DEFAULT d'1970-01-01T00:00:00Z';
|
|
109
121
|
DEFINE FIELD OVERWRITE last_synced ON sync_cursor TYPE datetime DEFAULT time::now();
|
|
122
|
+
|
|
123
|
+
-- ── suemo_stats ──────────────────────────────────────────────────────────────
|
|
124
|
+
DEFINE TABLE OVERWRITE suemo_stats SCHEMAFULL;
|
|
125
|
+
DEFINE FIELD OVERWRITE ns_db ON suemo_stats TYPE string;
|
|
126
|
+
DEFINE FIELD OVERWRITE total_writes ON suemo_stats TYPE int DEFAULT 0;
|
|
127
|
+
DEFINE FIELD OVERWRITE total_queries ON suemo_stats TYPE int DEFAULT 0;
|
|
128
|
+
DEFINE FIELD OVERWRITE last_write ON suemo_stats TYPE option<datetime> DEFAULT NONE;
|
|
129
|
+
DEFINE FIELD OVERWRITE last_query ON suemo_stats TYPE option<datetime> DEFAULT NONE;
|
|
130
|
+
DEFINE INDEX OVERWRITE idx_stats_ns ON suemo_stats FIELDS ns_db;
|
package/src/db/schema.ts
CHANGED
|
@@ -11,12 +11,15 @@ const log = getLogger(['suemo', 'db', 'schema'])
|
|
|
11
11
|
export async function runSchema(db: Surreal): Promise<void> {
|
|
12
12
|
log.info('Running schema migrations')
|
|
13
13
|
const statements = SCHEMA.split(/;\s*\n/).map((s) => s.trim()).filter(Boolean)
|
|
14
|
-
|
|
14
|
+
log.debug('Prepared schema statements', { count: statements.length, schemaBytes: SCHEMA.length })
|
|
15
|
+
for (const [index, stmt] of statements.entries()) {
|
|
15
16
|
try {
|
|
17
|
+
const snippet = stmt.length > 160 ? `${stmt.slice(0, 160)}…` : stmt
|
|
18
|
+
log.debug('Executing schema statement', { index, snippet })
|
|
16
19
|
await db.query(stmt)
|
|
17
|
-
log.debug('Schema statement OK', { stmt: stmt.slice(0, 60) })
|
|
20
|
+
log.debug('Schema statement OK', { index, stmt: stmt.slice(0, 60) })
|
|
18
21
|
} catch (e) {
|
|
19
|
-
log.error('Schema statement failed', { stmt, error: String(e) })
|
|
22
|
+
log.error('Schema statement failed', { index, stmt, error: String(e) })
|
|
20
23
|
throw e
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EmbeddingProvider } from '../config.ts'
|
|
2
|
+
import { getLogger } from '../logger.ts'
|
|
3
|
+
import { embedText } from './openai-compatible.ts'
|
|
4
|
+
|
|
5
|
+
const log = getLogger(['suemo', 'embedding'])
|
|
6
|
+
|
|
7
|
+
export interface EmbeddingResult {
|
|
8
|
+
clause: string
|
|
9
|
+
param?: number[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function getEmbedding(
|
|
13
|
+
text: string,
|
|
14
|
+
config: EmbeddingProvider,
|
|
15
|
+
): Promise<EmbeddingResult> {
|
|
16
|
+
log.debug('getEmbedding()', { provider: config.provider, dimension: config.dimension })
|
|
17
|
+
|
|
18
|
+
switch (config.provider) {
|
|
19
|
+
case 'surreal':
|
|
20
|
+
return { clause: 'fn::embed($content)' }
|
|
21
|
+
|
|
22
|
+
case 'openai-compatible': {
|
|
23
|
+
const vec = await embedText(text, config)
|
|
24
|
+
if (vec.length !== config.dimension) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Embedding dimension mismatch: expected ${config.dimension}, got ${vec.length}`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
return { clause: '$embedding', param: vec }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case 'stub': {
|
|
33
|
+
const vec = new Array(config.dimension).fill(0)
|
|
34
|
+
log.debug('stub embedding: returning zero vector', { dimension: config.dimension })
|
|
35
|
+
return { clause: '$embedding', param: vec }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildEmbeddingClause(config: EmbeddingProvider): string {
|
|
41
|
+
switch (config.provider) {
|
|
42
|
+
case 'surreal':
|
|
43
|
+
return 'fn::embed($content)'
|
|
44
|
+
case 'openai-compatible':
|
|
45
|
+
case 'stub':
|
|
46
|
+
return '$embedding'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getZeroVector(dimension: number): number[] {
|
|
51
|
+
return new Array(dimension).fill(0)
|
|
52
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/embedding/openai_compatible.ts
|
|
2
|
+
import { getLogger } from "../logger.ts";
|
|
3
|
+
|
|
4
|
+
const log = getLogger(["suemo", "embedding", "openai-compatible"]);
|
|
5
|
+
|
|
6
|
+
export interface OpenAICompatibleEmbeddingConfig {
|
|
7
|
+
url: string; // e.g. http://127.0.0.1:8080/v1/embeddings
|
|
8
|
+
model: string;
|
|
9
|
+
dimension: number;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function embedText(
|
|
14
|
+
text: string,
|
|
15
|
+
config: OpenAICompatibleEmbeddingConfig,
|
|
16
|
+
): Promise<number[]> {
|
|
17
|
+
log.debug("embedText()", { textPreview: text.slice(0, 60) });
|
|
18
|
+
|
|
19
|
+
const res = await fetch(config.url, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"Authorization": `Bearer ${config.apiKey ?? "local"}`,
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({ input: text, model: config.model }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw new Error(`Embedding server error ${res.status}: ${await res.text()}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const json = await res.json() as {
|
|
33
|
+
data: { embedding: number[]; index: number }[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const vec = json.data[0]?.embedding;
|
|
37
|
+
if (!vec) throw new Error("Embedding server returned empty data");
|
|
38
|
+
if (vec.length !== config.dimension) {
|
|
39
|
+
throw new Error(`Dimension mismatch: expected ${config.dimension}, got ${vec.length}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return vec;
|
|
43
|
+
}
|
package/src/goal.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
|
+
import type { SuemoConfig } from './config.ts'
|
|
2
3
|
import { getLogger } from './logger.ts'
|
|
3
4
|
import { invalidate, observe } from './memory/write.ts'
|
|
4
5
|
import type { MemoryNode } from './types.ts'
|
|
@@ -8,10 +9,11 @@ const log = getLogger(['suemo', 'goal'])
|
|
|
8
9
|
export async function goalSet(
|
|
9
10
|
db: Surreal,
|
|
10
11
|
content: string,
|
|
12
|
+
config: SuemoConfig,
|
|
11
13
|
opts: { scope?: string; tags?: string[] } = {},
|
|
12
14
|
): Promise<MemoryNode> {
|
|
13
15
|
log.info('goalSet()', { content: content.slice(0, 60) })
|
|
14
|
-
return observe(db, { content, kind: 'goal', ...opts })
|
|
16
|
+
return observe(db, { content, kind: 'goal', ...opts }, config)
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export async function goalResolve(db: Surreal, goalId: string): Promise<void> {
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// src/index.ts — public API surface
|
|
2
|
-
export { defineConfig, loadConfig } from './config.ts'
|
|
2
|
+
export { defineConfig, loadConfig, resolveSyncConfig } from './config.ts'
|
|
3
3
|
export type {
|
|
4
4
|
AuthConfig,
|
|
5
5
|
ConsolidationConfig,
|
|
6
6
|
EmbeddingProvider,
|
|
7
7
|
LLMConfig,
|
|
8
8
|
McpConfig,
|
|
9
|
+
ResolvedSyncAutoConfig,
|
|
10
|
+
ResolvedSyncConfig,
|
|
9
11
|
RetrievalConfig,
|
|
10
12
|
SuemoConfig,
|
|
11
13
|
SurrealTarget,
|
|
14
|
+
SyncAutoConfig,
|
|
12
15
|
SyncConfig,
|
|
16
|
+
SyncDirectionConfig,
|
|
13
17
|
} from './config.ts'
|