suemo 0.0.2 → 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 +90 -17
- package/package.json +1 -1
- package/src/cli/commands/export-import.ts +26 -1
- package/src/cli/commands/health.ts +18 -1
- package/src/cli/commands/observe.ts +1 -0
- package/src/cli/commands/serve.ts +11 -1
- package/src/cli/commands/sync.ts +31 -6
- package/src/cognitive/health.ts +61 -1
- package/src/config.template.ts +22 -0
- package/src/config.ts +83 -2
- package/src/db/schema.surql +19 -1
- package/src/index.ts +5 -1
- package/src/mcp/dispatch.ts +105 -7
- package/src/mcp/server.ts +126 -3
- package/src/mcp/stdio.ts +75 -4
- package/src/mcp/tools.ts +6 -2
- package/src/memory/episode.ts +92 -0
- package/src/memory/read.ts +2 -0
- package/src/memory/write.ts +199 -2
- package/src/sync.ts +310 -66
- package/src/types.ts +30 -5
package/README.md
CHANGED
|
@@ -125,7 +125,8 @@ Commands:
|
|
|
125
125
|
doctor embed Diagnose fn::embed setup and print fix steps
|
|
126
126
|
health Memory health report
|
|
127
127
|
health vitals Last 10 consolidation runs + node counts
|
|
128
|
-
|
|
128
|
+
health stats Lightweight usage counters and timestamps
|
|
129
|
+
sync Sync memories with a remote SurrealDB instance
|
|
129
130
|
export Stream memories to JSONL on stdout
|
|
130
131
|
import <file> Import memories from a JSONL file
|
|
131
132
|
```
|
|
@@ -176,9 +177,56 @@ export default defineConfig({
|
|
|
176
177
|
weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
|
|
177
178
|
},
|
|
178
179
|
mcp: { port: 4242, host: '127.0.0.1' },
|
|
180
|
+
sync: {
|
|
181
|
+
remotes: {
|
|
182
|
+
vps: {
|
|
183
|
+
url: process.env.SUEMO_SYNC_URL!,
|
|
184
|
+
namespace: process.env.SUEMO_SYNC_NS!,
|
|
185
|
+
database: process.env.SUEMO_SYNC_DB!,
|
|
186
|
+
auth: {
|
|
187
|
+
user: process.env.SUEMO_SYNC_USER!,
|
|
188
|
+
pass: process.env.SUEMO_SYNC_PASS!,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
defaultRemote: 'vps',
|
|
193
|
+
auto: {
|
|
194
|
+
enabled: false,
|
|
195
|
+
intervalSeconds: 300,
|
|
196
|
+
direction: 'push',
|
|
197
|
+
remote: 'vps',
|
|
198
|
+
onWrite: false,
|
|
199
|
+
minWriteIntervalSeconds: 30,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
179
202
|
})
|
|
180
203
|
```
|
|
181
204
|
|
|
205
|
+
### Sync config notes
|
|
206
|
+
|
|
207
|
+
- `sync.remotes` supports multiple named remote instances.
|
|
208
|
+
- `sync.defaultRemote` is used by `suemo sync` unless `--remote` is provided.
|
|
209
|
+
- Legacy `sync.remote` (single target) is still supported for migration compatibility.
|
|
210
|
+
- `sync.auto` powers background sync in `suemo serve` (HTTP and stdio):
|
|
211
|
+
- `enabled`: master switch
|
|
212
|
+
- `intervalSeconds`: timer cadence
|
|
213
|
+
- `direction`: `push | pull | both`
|
|
214
|
+
- `remote`: named target from `sync.remotes`
|
|
215
|
+
- `onWrite`: trigger sync after mutating MCP tools
|
|
216
|
+
- `minWriteIntervalSeconds`: throttle for write-triggered sync
|
|
217
|
+
|
|
218
|
+
Manual sync examples:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
suemo sync --direction both
|
|
222
|
+
suemo sync --remote vps --direction pull
|
|
223
|
+
suemo sync --remote vps --direction push --dry-run
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Minimal local↔VPS smoke test guide:
|
|
227
|
+
|
|
228
|
+
- `data/scenarios/sync-local-vps.md`
|
|
229
|
+
|
|
182
230
|
Multiple agents on the same machine use separate config files that extend a shared base:
|
|
183
231
|
|
|
184
232
|
```ts
|
|
@@ -212,22 +260,28 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
|
|
|
212
260
|
|
|
213
261
|
## MCP Tools
|
|
214
262
|
|
|
215
|
-
| Tool
|
|
216
|
-
|
|
|
217
|
-
| `observe`
|
|
218
|
-
| `believe`
|
|
219
|
-
| `invalidate`
|
|
220
|
-
| `query`
|
|
221
|
-
| `recall`
|
|
222
|
-
| `wander`
|
|
223
|
-
| `timeline`
|
|
224
|
-
| `episode_start`
|
|
225
|
-
| `episode_end`
|
|
226
|
-
| `goal_set`
|
|
227
|
-
| `goal_resolve`
|
|
228
|
-
| `goal_list`
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
263
|
+
| Tool | Description |
|
|
264
|
+
| --------------------- | ------------------------------------------------------------------- |
|
|
265
|
+
| `observe` | Store an observation, belief, question, or hypothesis |
|
|
266
|
+
| `believe` | Store a belief — auto-detects and invalidates contradicting beliefs |
|
|
267
|
+
| `invalidate` | Soft-delete a node by ID (sets `valid_until`) |
|
|
268
|
+
| `query` | Hybrid semantic search (vector + BM25 + graph) |
|
|
269
|
+
| `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
|
|
270
|
+
| `wander` | Spreading-activation walk through the graph |
|
|
271
|
+
| `timeline` | Chronological memory slice with optional date range |
|
|
272
|
+
| `episode_start` | Begin a bounded session window |
|
|
273
|
+
| `episode_end` | Close a session, optionally with a summary |
|
|
274
|
+
| `goal_set` | Create a goal node |
|
|
275
|
+
| `goal_resolve` | Mark a goal achieved |
|
|
276
|
+
| `goal_list` | List active (or all) goals |
|
|
277
|
+
| `upsert_by_key` | Upsert a memory node by stable topic key |
|
|
278
|
+
| `capture_prompt` | Capture raw prompt and link derived observations |
|
|
279
|
+
| `session_context_get` | Fetch open episode summary/context by session ID |
|
|
280
|
+
| `session_context_set` | Update open episode summary/context by session ID |
|
|
281
|
+
| `consolidate` | Manually trigger NREM + REM consolidation |
|
|
282
|
+
| `health` | Graph health report |
|
|
283
|
+
| `vitals` | Last 10 consolidation runs + grouped node counts |
|
|
284
|
+
| `stats` | Lightweight usage stats and write/query counters |
|
|
231
285
|
|
|
232
286
|
Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
|
|
233
287
|
|
|
@@ -258,6 +312,25 @@ The CLI and MCP server are both thin shells. All logic lives in the domain layer
|
|
|
258
312
|
|
|
259
313
|
---
|
|
260
314
|
|
|
315
|
+
## Giant end-to-end scenario (all tools / fields)
|
|
316
|
+
|
|
317
|
+
For full-system stress testing (CLI + MCP + consolidation + relation kinds), use:
|
|
318
|
+
|
|
319
|
+
- `data/scenarios/giant-scenario.md` — walkthrough + validation queries
|
|
320
|
+
- `data/scenarios/run-giant-cli.sh` — comprehensive CLI flow
|
|
321
|
+
- `data/scenarios/run-giant-mcp.mjs` — comprehensive stdio MCP flow
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
|
|
325
|
+
```sh
|
|
326
|
+
./data/scenarios/run-giant-cli.sh ~/.suemo/suemo.ts giant-cli-main
|
|
327
|
+
bun data/scenarios/run-giant-mcp.mjs --config ~/.suemo/suemo.ts --scope giant-mcp-main
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
These scenarios intentionally exercise all memory kinds and all relation kinds, and produce enough incident-like data to observe NREM/REM behavior.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
261
334
|
## Stack
|
|
262
335
|
|
|
263
336
|
- **Runtime** — [Bun](https://bun.sh)
|
package/package.json
CHANGED
|
@@ -84,7 +84,32 @@ export const importCmd = app.sub('import')
|
|
|
84
84
|
await db.query('INSERT IGNORE INTO memory $row', { row })
|
|
85
85
|
imported++
|
|
86
86
|
} else if (type === 'relation') {
|
|
87
|
-
|
|
87
|
+
const inId = typeof row['in'] === 'string' ? row['in'] : undefined
|
|
88
|
+
const outId = typeof row['out'] === 'string' ? row['out'] : undefined
|
|
89
|
+
const kind = typeof row['kind'] === 'string' ? row['kind'] : undefined
|
|
90
|
+
const strength = typeof row['strength'] === 'number' ? row['strength'] : undefined
|
|
91
|
+
|
|
92
|
+
if (!inId || !outId || !kind || strength === undefined) {
|
|
93
|
+
skipped++
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await db.query(
|
|
98
|
+
`
|
|
99
|
+
LET $inRec = type::record($in);
|
|
100
|
+
LET $outRec = type::record($out);
|
|
101
|
+
RELATE $inRec->relates_to->$outRec CONTENT {
|
|
102
|
+
kind: $kind,
|
|
103
|
+
strength: $strength
|
|
104
|
+
};
|
|
105
|
+
`,
|
|
106
|
+
{
|
|
107
|
+
in: inId,
|
|
108
|
+
out: outId,
|
|
109
|
+
kind,
|
|
110
|
+
strength,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
88
113
|
imported++
|
|
89
114
|
} else {
|
|
90
115
|
skipped++
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
|
-
import { healthReport, vitals } from '../../cognitive/health.ts'
|
|
2
|
+
import { healthReport, suemoStats, vitals } from '../../cognitive/health.ts'
|
|
3
3
|
import { loadConfig } from '../../config.ts'
|
|
4
4
|
import { connect, disconnect } from '../../db/client.ts'
|
|
5
5
|
import { getLogger } from '../../logger.ts'
|
|
@@ -42,9 +42,26 @@ const vitalsCmd = health.sub('vitals')
|
|
|
42
42
|
}
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
+
const statsCmd = health.sub('stats')
|
|
46
|
+
.meta({ description: 'Lightweight usage stats' })
|
|
47
|
+
.run(async ({ flags }) => {
|
|
48
|
+
await initCliCommand('health stats', { debug: flags.debug, config: flags.config })
|
|
49
|
+
log.debug('Running health stats command')
|
|
50
|
+
const config = await loadConfig(process.cwd(), flags.config)
|
|
51
|
+
let db: Surreal | undefined
|
|
52
|
+
try {
|
|
53
|
+
db = await connect(config.surreal)
|
|
54
|
+
const s = await suemoStats(db)
|
|
55
|
+
console.log(JSON.stringify(s, null, 2))
|
|
56
|
+
} finally {
|
|
57
|
+
if (db) await disconnect()
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
45
61
|
export const healthCmd = health
|
|
46
62
|
.command(reportCmd)
|
|
47
63
|
.command(vitalsCmd)
|
|
64
|
+
.command(statsCmd)
|
|
48
65
|
// Default: run the report when just `suemo health` is called
|
|
49
66
|
.run(async ({ flags }) => {
|
|
50
67
|
await initCliCommand('health', { debug: flags.debug, config: flags.config })
|
|
@@ -44,6 +44,7 @@ export const observeCmd = app.sub('observe')
|
|
|
44
44
|
scope: flags.scope,
|
|
45
45
|
confidence: flags.confidence,
|
|
46
46
|
source: flags.source,
|
|
47
|
+
sessionId: flags.session,
|
|
47
48
|
}, config)
|
|
48
49
|
console.log(JSON.stringify({ id: node.id, kind: node.kind, valid_from: node.valid_from }, null, 2))
|
|
49
50
|
} finally {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfig } from '../../config.ts'
|
|
1
|
+
import { loadConfig, resolveSyncConfig } from '../../config.ts'
|
|
2
2
|
import { getLogger } from '../../logger.ts'
|
|
3
3
|
import { startMcpServer, startMcpStdioServer } from '../../mcp/server.ts'
|
|
4
4
|
import { app, initCliCommand } from '../shared.ts'
|
|
@@ -15,6 +15,16 @@ export const serveCmd = app.sub('serve')
|
|
|
15
15
|
.run(async ({ flags }) => {
|
|
16
16
|
await initCliCommand('serve', { debug: flags.debug, config: flags.config })
|
|
17
17
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
18
|
+
const sync = resolveSyncConfig(config)
|
|
19
|
+
if (sync?.auto.enabled) {
|
|
20
|
+
log.info('Auto-sync enabled', {
|
|
21
|
+
remote: sync.auto.remote,
|
|
22
|
+
direction: sync.auto.direction,
|
|
23
|
+
intervalSeconds: sync.auto.intervalSeconds,
|
|
24
|
+
onWrite: sync.auto.onWrite,
|
|
25
|
+
minWriteIntervalSeconds: sync.auto.minWriteIntervalSeconds,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
18
28
|
if (flags.stdio) {
|
|
19
29
|
log.debug('Starting MCP stdio transport')
|
|
20
30
|
await startMcpStdioServer(config)
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfig } from '../../config.ts'
|
|
1
|
+
import { loadConfig, resolveSyncConfig, type SurrealTarget } from '../../config.ts'
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { syncTo } from '../../sync.ts'
|
|
@@ -7,24 +7,49 @@ import { app, initCliCommand } from '../shared.ts'
|
|
|
7
7
|
const log = getLogger(['suemo', 'cli', 'sync'])
|
|
8
8
|
|
|
9
9
|
export const syncCmd = app.sub('sync')
|
|
10
|
-
.meta({ description: '
|
|
10
|
+
.meta({ description: 'Sync memories with remote SurrealDB (push/pull/both)' })
|
|
11
11
|
.flags({
|
|
12
12
|
'dry-run': { type: 'boolean', description: 'Show what would be pushed without writing', default: false },
|
|
13
|
+
direction: { type: 'string', description: 'Sync direction: push | pull | both', default: 'push' },
|
|
14
|
+
remote: { type: 'string', description: 'Named remote from sync.remotes (defaults to sync.defaultRemote)' },
|
|
13
15
|
})
|
|
14
16
|
.run(async ({ flags }) => {
|
|
15
17
|
await initCliCommand('sync', { debug: flags.debug, config: flags.config })
|
|
16
18
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
const sync = resolveSyncConfig(config)
|
|
20
|
+
if (!sync) {
|
|
21
|
+
console.error('No sync remotes configured. Set sync.remotes (or legacy sync.remote) in suemo config.')
|
|
19
22
|
process.exit(1)
|
|
20
23
|
}
|
|
24
|
+
|
|
25
|
+
const selectedRemoteName = flags.remote ?? sync.defaultRemote
|
|
26
|
+
const remote: SurrealTarget | undefined = sync.remotes[selectedRemoteName]
|
|
27
|
+
if (!remote) {
|
|
28
|
+
console.error(
|
|
29
|
+
`Unknown sync remote "${selectedRemoteName}". Available: ${Object.keys(sync.remotes).join(', ')}`,
|
|
30
|
+
)
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
log.debug('Running sync command', {
|
|
22
35
|
dryRun: flags['dry-run'],
|
|
23
|
-
|
|
36
|
+
direction: flags.direction,
|
|
37
|
+
remote: selectedRemoteName,
|
|
38
|
+
target: `${remote.url}/${remote.namespace}/${remote.database}`,
|
|
24
39
|
})
|
|
40
|
+
|
|
41
|
+
const direction = flags.direction as 'push' | 'pull' | 'both'
|
|
42
|
+
if (!['push', 'pull', 'both'].includes(direction)) {
|
|
43
|
+
console.error(`Invalid direction "${flags.direction}". Use: push, pull, or both`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
const db = await connect(config.surreal)
|
|
26
48
|
try {
|
|
27
|
-
const result = await syncTo(db,
|
|
49
|
+
const result = await syncTo(db, remote, {
|
|
50
|
+
dryRun: flags['dry-run'],
|
|
51
|
+
direction,
|
|
52
|
+
})
|
|
28
53
|
console.log(JSON.stringify(result, null, 2))
|
|
29
54
|
} finally {
|
|
30
55
|
await disconnect()
|
package/src/cognitive/health.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
2
|
import { checkCompatibility } from '../db/preflight.ts'
|
|
3
3
|
import { getLogger } from '../logger.ts'
|
|
4
|
-
import type { ConsolidationRun, HealthReport } from '../types.ts'
|
|
4
|
+
import type { ConsolidationRun, HealthReport, SuemoStats } from '../types.ts'
|
|
5
5
|
|
|
6
6
|
const log = getLogger(['suemo', 'cognitive', 'health'])
|
|
7
7
|
|
|
@@ -121,3 +121,63 @@ export async function vitals(db: Surreal): Promise<{
|
|
|
121
121
|
nodesByScope,
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
export async function suemoStats(db: Surreal): Promise<SuemoStats> {
|
|
126
|
+
log.info('suemoStats()')
|
|
127
|
+
|
|
128
|
+
const [totalR, activeR, kindR, relR, runR, statsR] = await Promise.all([
|
|
129
|
+
db.query<[{ count: number }[]]>('SELECT count() AS count FROM memory GROUP ALL'),
|
|
130
|
+
db.query<[{ count: number }[]]>(
|
|
131
|
+
'SELECT count() AS count FROM memory WHERE valid_until = NONE OR valid_until > time::now() GROUP ALL',
|
|
132
|
+
),
|
|
133
|
+
db.query<[{ kind: string; count: number }[]]>('SELECT kind, count() AS count FROM memory GROUP BY kind'),
|
|
134
|
+
db.query<[{ count: number }[]]>('SELECT count() AS count FROM relates_to GROUP ALL'),
|
|
135
|
+
db.query<[{ count: number }[]]>('SELECT count() AS count FROM consolidation_run GROUP ALL'),
|
|
136
|
+
db.query<[
|
|
137
|
+
{ total_writes: number; total_queries: number; last_write: string | null; last_query: string | null }[],
|
|
138
|
+
]>(
|
|
139
|
+
'SELECT total_writes, total_queries, last_write, last_query FROM suemo_stats:default LIMIT 1',
|
|
140
|
+
),
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
const byKind: Record<string, number> = {}
|
|
144
|
+
for (const r of kindR[0] ?? []) byKind[r.kind] = r.count
|
|
145
|
+
|
|
146
|
+
const s = statsR[0]?.[0]
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
totalNodes: totalR[0]?.[0]?.count ?? 0,
|
|
150
|
+
activeNodes: activeR[0]?.[0]?.count ?? 0,
|
|
151
|
+
nodesByKind: byKind,
|
|
152
|
+
relations: relR[0]?.[0]?.count ?? 0,
|
|
153
|
+
consolidationRuns: runR[0]?.[0]?.count ?? 0,
|
|
154
|
+
lastWrite: s?.last_write ?? null,
|
|
155
|
+
lastQuery: s?.last_query ?? null,
|
|
156
|
+
totalWrites: s?.total_writes ?? 0,
|
|
157
|
+
totalQueries: s?.total_queries ?? 0,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function incrementWriteStats(db: Surreal): Promise<void> {
|
|
162
|
+
await db.query(
|
|
163
|
+
`
|
|
164
|
+
UPSERT suemo_stats:default SET
|
|
165
|
+
ns_db = $nsDb,
|
|
166
|
+
total_writes = total_writes + 1,
|
|
167
|
+
last_write = time::now()
|
|
168
|
+
`,
|
|
169
|
+
{ nsDb: 'default' },
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function incrementQueryStats(db: Surreal): Promise<void> {
|
|
174
|
+
await db.query(
|
|
175
|
+
`
|
|
176
|
+
UPSERT suemo_stats:default SET
|
|
177
|
+
ns_db = $nsDb,
|
|
178
|
+
total_queries = total_queries + 1,
|
|
179
|
+
last_query = time::now()
|
|
180
|
+
`,
|
|
181
|
+
{ nsDb: 'default' },
|
|
182
|
+
)
|
|
183
|
+
}
|
package/src/config.template.ts
CHANGED
|
@@ -33,4 +33,26 @@ export default defineConfig({
|
|
|
33
33
|
port: Number(process.env.SUEMO_PORT) || 4242,
|
|
34
34
|
host: '127.0.0.1',
|
|
35
35
|
},
|
|
36
|
+
sync: {
|
|
37
|
+
remotes: {
|
|
38
|
+
vps: {
|
|
39
|
+
url: process.env.SUEMO_SYNC_URL ?? process.env.SUEMO_URL ?? 'ws://localhost:8000',
|
|
40
|
+
namespace: process.env.SUEMO_SYNC_NS ?? process.env.SUEMO_NS ?? 'suemo',
|
|
41
|
+
database: process.env.SUEMO_SYNC_DB ?? process.env.SUEMO_DB ?? 'suemo',
|
|
42
|
+
auth: {
|
|
43
|
+
user: process.env.SUEMO_SYNC_USER ?? process.env.SUEMO_USER ?? 'root',
|
|
44
|
+
pass: process.env.SUEMO_SYNC_PASS ?? process.env.SUEMO_PASS ?? 'pass',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultRemote: 'vps',
|
|
49
|
+
auto: {
|
|
50
|
+
enabled: false,
|
|
51
|
+
intervalSeconds: 300,
|
|
52
|
+
direction: 'push',
|
|
53
|
+
remote: 'vps',
|
|
54
|
+
onWrite: false,
|
|
55
|
+
minWriteIntervalSeconds: 30,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
36
58
|
})
|
package/src/config.ts
CHANGED
|
@@ -51,9 +51,41 @@ export interface McpConfig {
|
|
|
51
51
|
host: string
|
|
52
52
|
}
|
|
53
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
|
+
|
|
54
65
|
export interface SyncConfig {
|
|
55
|
-
remote
|
|
56
|
-
|
|
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
|
|
57
89
|
}
|
|
58
90
|
|
|
59
91
|
export interface SuemoConfig {
|
|
@@ -71,6 +103,55 @@ export function defineConfig(config: SuemoConfig): SuemoConfig {
|
|
|
71
103
|
return config
|
|
72
104
|
}
|
|
73
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
|
+
|
|
74
155
|
// ── loadConfig — resolution chain ────────────────────────────────────────────
|
|
75
156
|
|
|
76
157
|
const CONFIG_CANDIDATES = [
|
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,6 +25,7 @@ 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;
|
|
@@ -65,6 +67,9 @@ DEFINE INDEX OVERWRITE idx_memory_kind_valid
|
|
|
65
67
|
DEFINE INDEX OVERWRITE idx_memory_salience
|
|
66
68
|
ON memory FIELDS salience;
|
|
67
69
|
|
|
70
|
+
DEFINE INDEX OVERWRITE idx_memory_topic_key
|
|
71
|
+
ON memory FIELDS topic_key;
|
|
72
|
+
|
|
68
73
|
-- ── relates_to ───────────────────────────────────────────────────────────────
|
|
69
74
|
DEFINE TABLE OVERWRITE relates_to SCHEMAFULL
|
|
70
75
|
TYPE RELATION IN memory OUT memory;
|
|
@@ -76,6 +81,7 @@ DEFINE FIELD OVERWRITE strength ON relates_to TYPE float DEFAULT 0.5;
|
|
|
76
81
|
DEFINE FIELD OVERWRITE valid_from ON relates_to TYPE datetime DEFAULT time::now();
|
|
77
82
|
DEFINE FIELD OVERWRITE valid_until ON relates_to TYPE option<datetime> DEFAULT NONE;
|
|
78
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();
|
|
79
85
|
|
|
80
86
|
-- ── episode ───────────────────────────────────────────────────────────────────
|
|
81
87
|
DEFINE TABLE OVERWRITE episode SCHEMAFULL;
|
|
@@ -84,6 +90,7 @@ DEFINE FIELD OVERWRITE session_id ON episode TYPE string;
|
|
|
84
90
|
DEFINE FIELD OVERWRITE started_at ON episode TYPE datetime DEFAULT time::now();
|
|
85
91
|
DEFINE FIELD OVERWRITE ended_at ON episode TYPE option<datetime> DEFAULT NONE;
|
|
86
92
|
DEFINE FIELD OVERWRITE summary ON episode TYPE option<string>;
|
|
93
|
+
DEFINE FIELD OVERWRITE context ON episode TYPE option<object> DEFAULT NONE;
|
|
87
94
|
|
|
88
95
|
-- REFERENCES: bidirectional array of memory links
|
|
89
96
|
DEFINE FIELD OVERWRITE memory_ids ON episode
|
|
@@ -105,8 +112,19 @@ DEFINE FIELD OVERWRITE error ON consolidation_run TYPE option<string> DEF
|
|
|
105
112
|
|
|
106
113
|
-- ── sync_cursor ───────────────────────────────────────────────────────────────
|
|
107
114
|
-- One record per (remote.url, remote.ns, remote.db) triple.
|
|
108
|
-
-- Stores
|
|
115
|
+
-- Stores per-remote cursors for push/pull sync based on updated_at.
|
|
109
116
|
DEFINE TABLE OVERWRITE sync_cursor SCHEMAFULL;
|
|
110
117
|
DEFINE FIELD OVERWRITE remote_key ON sync_cursor TYPE string; -- sha1(url+ns+db)
|
|
111
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';
|
|
112
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/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'
|