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 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
- sync Push memories to a remote SurrealDB instance
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 | Description |
216
- | --------------- | ------------------------------------------------------------------- |
217
- | `observe` | Store an observation, belief, question, or hypothesis |
218
- | `believe` | Store a belief — auto-detects and invalidates contradicting beliefs |
219
- | `invalidate` | Soft-delete a node by ID (sets `valid_until`) |
220
- | `query` | Hybrid semantic search (vector + BM25 + graph) |
221
- | `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
222
- | `wander` | Spreading-activation walk through the graph |
223
- | `timeline` | Chronological memory slice with optional date range |
224
- | `episode_start` | Begin a bounded session window |
225
- | `episode_end` | Close a session, optionally with a summary |
226
- | `goal_set` | Create a goal node |
227
- | `goal_resolve` | Mark a goal achieved |
228
- | `goal_list` | List active (or all) goals |
229
- | `consolidate` | Manually trigger NREM + REM consolidation |
230
- | `health` | Graph health report |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
5
  "author": {
6
6
  "name": "Umar Alfarouk",
@@ -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
- await db.query('INSERT IGNORE INTO relates_to $row', { row })
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)
@@ -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: 'Push memories to remote SurrealDB (append-only)' })
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
- if (!config.sync) {
18
- console.error('No sync.remote configured in suemo config.')
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
- target: `${config.sync.remote.url}/${config.sync.remote.namespace}/${config.sync.remote.database}`,
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, config.sync.remote, { dryRun: flags['dry-run'] })
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()
@@ -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
+ }
@@ -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: SurrealTarget
56
- cursor?: string // ISO datetime; absence means full sync on first run
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 = [
@@ -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 the last successfully synced created_at timestamp.
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'