suemo 0.0.2 → 0.0.4
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 +100 -19
- 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 +73 -1
- package/src/cli/commands/sync.ts +31 -6
- package/src/cli/commands/wander.ts +4 -0
- package/src/cognitive/consolidate.ts +8 -17
- package/src/cognitive/contradiction.ts +5 -11
- package/src/cognitive/health.ts +61 -1
- package/src/config.template.ts +22 -0
- package/src/config.ts +87 -6
- package/src/db/preflight.ts +6 -2
- package/src/db/schema.surql +19 -1
- package/src/db/schema.ts +56 -8
- package/src/embedding/index.ts +2 -2
- package/src/index.ts +5 -1
- package/src/mcp/dispatch.ts +105 -7
- package/src/mcp/server.ts +127 -4
- package/src/mcp/stdio.ts +98 -9
- package/src/mcp/tools.ts +6 -2
- package/src/memory/episode.ts +98 -0
- package/src/memory/read.ts +64 -43
- package/src/memory/write.ts +211 -16
- package/src/sync.ts +310 -66
- package/src/types.ts +31 -6
package/README.md
CHANGED
|
@@ -96,6 +96,14 @@ suemo serve
|
|
|
96
96
|
# Listening on http://127.0.0.1:4242
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
For development with automatic restart on file changes:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
suemo serve --dev
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`--dev` re-runs `suemo serve` under Bun watch mode and restarts MCP when source files change.
|
|
106
|
+
|
|
99
107
|
---
|
|
100
108
|
|
|
101
109
|
## CLI Reference
|
|
@@ -125,7 +133,8 @@ Commands:
|
|
|
125
133
|
doctor embed Diagnose fn::embed setup and print fix steps
|
|
126
134
|
health Memory health report
|
|
127
135
|
health vitals Last 10 consolidation runs + node counts
|
|
128
|
-
|
|
136
|
+
health stats Lightweight usage counters and timestamps
|
|
137
|
+
sync Sync memories with a remote SurrealDB instance
|
|
129
138
|
export Stream memories to JSONL on stdout
|
|
130
139
|
import <file> Import memories from a JSONL file
|
|
131
140
|
```
|
|
@@ -160,8 +169,8 @@ export default defineConfig({
|
|
|
160
169
|
},
|
|
161
170
|
},
|
|
162
171
|
embedding: {
|
|
163
|
-
provider: '
|
|
164
|
-
dimension:
|
|
172
|
+
provider: 'surrealml', // fn::embed() — configured in SurrealDB
|
|
173
|
+
dimension: 384,
|
|
165
174
|
},
|
|
166
175
|
consolidation: {
|
|
167
176
|
trigger: 'timer',
|
|
@@ -176,9 +185,56 @@ export default defineConfig({
|
|
|
176
185
|
weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
|
|
177
186
|
},
|
|
178
187
|
mcp: { port: 4242, host: '127.0.0.1' },
|
|
188
|
+
sync: {
|
|
189
|
+
remotes: {
|
|
190
|
+
vps: {
|
|
191
|
+
url: process.env.SUEMO_SYNC_URL!,
|
|
192
|
+
namespace: process.env.SUEMO_SYNC_NS!,
|
|
193
|
+
database: process.env.SUEMO_SYNC_DB!,
|
|
194
|
+
auth: {
|
|
195
|
+
user: process.env.SUEMO_SYNC_USER!,
|
|
196
|
+
pass: process.env.SUEMO_SYNC_PASS!,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
defaultRemote: 'vps',
|
|
201
|
+
auto: {
|
|
202
|
+
enabled: false,
|
|
203
|
+
intervalSeconds: 300,
|
|
204
|
+
direction: 'push',
|
|
205
|
+
remote: 'vps',
|
|
206
|
+
onWrite: false,
|
|
207
|
+
minWriteIntervalSeconds: 30,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
179
210
|
})
|
|
180
211
|
```
|
|
181
212
|
|
|
213
|
+
### Sync config notes
|
|
214
|
+
|
|
215
|
+
- `sync.remotes` supports multiple named remote instances.
|
|
216
|
+
- `sync.defaultRemote` is used by `suemo sync` unless `--remote` is provided.
|
|
217
|
+
- Legacy `sync.remote` (single target) is still supported for migration compatibility.
|
|
218
|
+
- `sync.auto` powers background sync in `suemo serve` (HTTP and stdio):
|
|
219
|
+
- `enabled`: master switch
|
|
220
|
+
- `intervalSeconds`: timer cadence
|
|
221
|
+
- `direction`: `push | pull | both`
|
|
222
|
+
- `remote`: named target from `sync.remotes`
|
|
223
|
+
- `onWrite`: trigger sync after mutating MCP tools
|
|
224
|
+
- `minWriteIntervalSeconds`: throttle for write-triggered sync
|
|
225
|
+
|
|
226
|
+
Manual sync examples:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
suemo sync --direction both
|
|
230
|
+
suemo sync --remote vps --direction pull
|
|
231
|
+
suemo sync --remote vps --direction push --dry-run
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Minimal local↔VPS smoke test guide:
|
|
235
|
+
|
|
236
|
+
- `data/scenarios/sync-local-vps.md`
|
|
237
|
+
|
|
182
238
|
Multiple agents on the same machine use separate config files that extend a shared base:
|
|
183
239
|
|
|
184
240
|
```ts
|
|
@@ -212,22 +268,28 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
|
|
|
212
268
|
|
|
213
269
|
## MCP Tools
|
|
214
270
|
|
|
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
|
-
| `
|
|
271
|
+
| Tool | Description |
|
|
272
|
+
| --------------------- | ------------------------------------------------------------------- |
|
|
273
|
+
| `observe` | Store an observation, belief, question, or hypothesis |
|
|
274
|
+
| `believe` | Store a belief — auto-detects and invalidates contradicting beliefs |
|
|
275
|
+
| `invalidate` | Soft-delete a node by ID (sets `valid_until`) |
|
|
276
|
+
| `query` | Hybrid semantic search (vector + BM25 + graph) |
|
|
277
|
+
| `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
|
|
278
|
+
| `wander` | Spreading-activation walk through the graph |
|
|
279
|
+
| `timeline` | Chronological memory slice with optional date range |
|
|
280
|
+
| `episode_start` | Begin a bounded session window |
|
|
281
|
+
| `episode_end` | Close a session, optionally with a summary |
|
|
282
|
+
| `goal_set` | Create a goal node |
|
|
283
|
+
| `goal_resolve` | Mark a goal achieved |
|
|
284
|
+
| `goal_list` | List active (or all) goals |
|
|
285
|
+
| `upsert_by_key` | Upsert a memory node by stable topic key |
|
|
286
|
+
| `capture_prompt` | Capture raw prompt and link derived observations |
|
|
287
|
+
| `session_context_get` | Fetch open episode summary/context by session ID |
|
|
288
|
+
| `session_context_set` | Update open episode summary/context by session ID |
|
|
289
|
+
| `consolidate` | Manually trigger NREM + REM consolidation |
|
|
290
|
+
| `health` | Graph health report |
|
|
291
|
+
| `vitals` | Last 10 consolidation runs + grouped node counts |
|
|
292
|
+
| `stats` | Lightweight usage stats and write/query counters |
|
|
231
293
|
|
|
232
294
|
Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
|
|
233
295
|
|
|
@@ -258,6 +320,25 @@ The CLI and MCP server are both thin shells. All logic lives in the domain layer
|
|
|
258
320
|
|
|
259
321
|
---
|
|
260
322
|
|
|
323
|
+
## Giant end-to-end scenario (all tools / fields)
|
|
324
|
+
|
|
325
|
+
For full-system stress testing (CLI + MCP + consolidation + relation kinds), use:
|
|
326
|
+
|
|
327
|
+
- `data/scenarios/giant-scenario.md` — walkthrough + validation queries
|
|
328
|
+
- `data/scenarios/run-giant-cli.sh` — comprehensive CLI flow
|
|
329
|
+
- `data/scenarios/run-giant-mcp.mjs` — comprehensive stdio MCP flow
|
|
330
|
+
|
|
331
|
+
Example:
|
|
332
|
+
|
|
333
|
+
```sh
|
|
334
|
+
./data/scenarios/run-giant-cli.sh ~/.suemo/suemo.ts giant-cli-main
|
|
335
|
+
bun data/scenarios/run-giant-mcp.mjs --config ~/.suemo/suemo.ts --scope giant-mcp-main
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
These scenarios intentionally exercise all memory kinds and all relation kinds, and produce enough incident-like data to observe NREM/REM behavior.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
261
342
|
## Stack
|
|
262
343
|
|
|
263
344
|
- **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,20 +1,92 @@
|
|
|
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'
|
|
5
5
|
|
|
6
6
|
const log = getLogger(['suemo', 'cli', 'serve'])
|
|
7
7
|
|
|
8
|
+
function printDevRestartBanner(): void {
|
|
9
|
+
if (process.env.SUEMO_DEV_WATCH !== '1') return
|
|
10
|
+
const now = new Date().toISOString()
|
|
11
|
+
console.log(`\n[suemo:dev] MCP restarted (${now})\n`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function runServeDevMode(): Promise<never> {
|
|
15
|
+
const scriptPath = process.argv[1] ?? 'src/cli/index.ts'
|
|
16
|
+
const forwardedArgs = process.argv.slice(2).filter((arg) => arg !== '--dev')
|
|
17
|
+
const bunExecutable = process.execPath.includes('bun') ? process.execPath : 'bun'
|
|
18
|
+
const cmd = [bunExecutable, '--watch', scriptPath, ...forwardedArgs]
|
|
19
|
+
|
|
20
|
+
log.info('Starting serve dev mode (bun --watch)', {
|
|
21
|
+
scriptPath,
|
|
22
|
+
forwardedArgs,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const child = Bun.spawn({
|
|
26
|
+
cmd,
|
|
27
|
+
env: { ...process.env, SUEMO_DEV_WATCH: '1' },
|
|
28
|
+
stdin: 'inherit',
|
|
29
|
+
stdout: 'inherit',
|
|
30
|
+
stderr: 'inherit',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const shutdown = (signal: NodeJS.Signals): void => {
|
|
34
|
+
log.info('Stopping dev watcher', { signal })
|
|
35
|
+
try {
|
|
36
|
+
child.kill(signal)
|
|
37
|
+
} catch {
|
|
38
|
+
// noop
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.once('SIGINT', () => {
|
|
43
|
+
shutdown('SIGINT')
|
|
44
|
+
})
|
|
45
|
+
process.once('SIGTERM', () => {
|
|
46
|
+
shutdown('SIGTERM')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const exitCode = await child.exited
|
|
50
|
+
process.exit(exitCode)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function maybeDelayDevStartup(): Promise<void> {
|
|
54
|
+
if (process.env.SUEMO_DEV_WATCH !== '1') return
|
|
55
|
+
const rawDelay = process.env.SUEMO_DEV_RESTART_DELAY_MS ?? '250'
|
|
56
|
+
const parsed = Number(rawDelay)
|
|
57
|
+
const delayMs = Number.isFinite(parsed) && parsed > 0 ? parsed : 250
|
|
58
|
+
if (delayMs > 0) {
|
|
59
|
+
log.debug('Delaying startup for dev restart stability', { delayMs })
|
|
60
|
+
await Bun.sleep(delayMs)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
8
64
|
export const serveCmd = app.sub('serve')
|
|
9
65
|
.meta({ description: 'Start the MCP server (HTTP or stdio)' })
|
|
10
66
|
.flags({
|
|
11
67
|
port: { type: 'number', short: 'p', description: 'Port to listen on (overrides config)' },
|
|
12
68
|
host: { type: 'string', description: 'Host to bind to (overrides config)' },
|
|
13
69
|
stdio: { type: 'boolean', description: 'Use stdio transport instead of HTTP' },
|
|
70
|
+
dev: { type: 'boolean', description: 'Restart MCP server on code changes (bun --watch)' },
|
|
14
71
|
})
|
|
15
72
|
.run(async ({ flags }) => {
|
|
73
|
+
printDevRestartBanner()
|
|
74
|
+
await maybeDelayDevStartup()
|
|
16
75
|
await initCliCommand('serve', { debug: flags.debug, config: flags.config })
|
|
76
|
+
if (flags.dev) {
|
|
77
|
+
await runServeDevMode()
|
|
78
|
+
}
|
|
17
79
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
80
|
+
const sync = resolveSyncConfig(config)
|
|
81
|
+
if (sync?.auto.enabled) {
|
|
82
|
+
log.info('Auto-sync enabled', {
|
|
83
|
+
remote: sync.auto.remote,
|
|
84
|
+
direction: sync.auto.direction,
|
|
85
|
+
intervalSeconds: sync.auto.intervalSeconds,
|
|
86
|
+
onWrite: sync.auto.onWrite,
|
|
87
|
+
minWriteIntervalSeconds: sync.auto.minWriteIntervalSeconds,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
18
90
|
if (flags.stdio) {
|
|
19
91
|
log.debug('Starting MCP stdio transport')
|
|
20
92
|
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()
|
|
@@ -33,6 +33,10 @@ export const wanderCmd = app.sub('wander')
|
|
|
33
33
|
if (flags.json) {
|
|
34
34
|
console.log(JSON.stringify(nodes, null, 2))
|
|
35
35
|
} else {
|
|
36
|
+
if (nodes.length === 0) {
|
|
37
|
+
console.log('No memories found for this wander query.')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
36
40
|
for (const n of nodes) {
|
|
37
41
|
console.log(`[${n.kind}] ${n.id} salience=${n.salience.toFixed(2)}`)
|
|
38
42
|
console.log(` ${n.content.slice(0, 120)}`)
|
|
@@ -77,22 +77,16 @@ async function runNREM(
|
|
|
77
77
|
// Find similar unassigned nodes using DB-side cosine
|
|
78
78
|
const similarResult = await db.query<[{ id: string; score: number }[]]>(
|
|
79
79
|
`
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
SELECT id, score
|
|
81
|
+
FROM (
|
|
82
|
+
SELECT id, vector::similarity::cosine(embedding, $emb) AS score
|
|
82
83
|
FROM memory
|
|
83
84
|
WHERE consolidated = false
|
|
84
85
|
AND id != $self
|
|
85
86
|
AND (valid_until = NONE OR valid_until > time::now())
|
|
86
|
-
AND embedding <|10, 20|> $emb
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
SELECT id, score
|
|
90
|
-
FROM (
|
|
91
|
-
SELECT id, vector::similarity::cosine(embedding, $emb) AS score
|
|
92
|
-
FROM $cand
|
|
93
87
|
)
|
|
94
88
|
ORDER BY score DESC
|
|
95
|
-
LIMIT
|
|
89
|
+
LIMIT 500
|
|
96
90
|
`,
|
|
97
91
|
{ emb: node.embedding, self: node.id },
|
|
98
92
|
)
|
|
@@ -238,13 +232,10 @@ async function runREM(
|
|
|
238
232
|
const candidates = await db.query<[MemoryNode[]]>(
|
|
239
233
|
`
|
|
240
234
|
SELECT * FROM memory
|
|
241
|
-
WHERE id
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
AND (valid_until = NONE OR valid_until > time::now())
|
|
246
|
-
AND embedding <|10, 40|> $emb
|
|
247
|
-
)
|
|
235
|
+
WHERE id != $self
|
|
236
|
+
AND consolidated = true
|
|
237
|
+
AND (valid_until = NONE OR valid_until > time::now())
|
|
238
|
+
ORDER BY vector::similarity::cosine(embedding, $emb) DESC
|
|
248
239
|
LIMIT 10
|
|
249
240
|
`,
|
|
250
241
|
{ self: node.id, emb: node.embedding },
|
|
@@ -21,21 +21,15 @@ export async function detectContradiction(
|
|
|
21
21
|
|
|
22
22
|
log.debug('detectContradiction()', { nodeId: newNode.id })
|
|
23
23
|
|
|
24
|
-
const candidates = await db.query<[
|
|
24
|
+
const candidates = await db.query<unknown[]>(
|
|
25
25
|
`
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
SELECT id, score
|
|
27
|
+
FROM (
|
|
28
|
+
SELECT id, vector::similarity::cosine(embedding, $emb) AS score
|
|
28
29
|
FROM memory
|
|
29
30
|
WHERE kind = 'belief'
|
|
30
31
|
AND id != $self
|
|
31
32
|
AND (valid_until = NONE OR valid_until > time::now())
|
|
32
|
-
AND embedding <|3, 20|> $emb
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
SELECT id, score
|
|
36
|
-
FROM (
|
|
37
|
-
SELECT id, vector::similarity::cosine(embedding, $emb) AS score
|
|
38
|
-
FROM $cand
|
|
39
33
|
)
|
|
40
34
|
ORDER BY score DESC
|
|
41
35
|
LIMIT 3
|
|
@@ -43,7 +37,7 @@ export async function detectContradiction(
|
|
|
43
37
|
{ emb: newNode.embedding, self: newNode.id },
|
|
44
38
|
)
|
|
45
39
|
|
|
46
|
-
const top = candidates[
|
|
40
|
+
const top = candidates.at(-1) as { id: string; score: number }[] | undefined
|
|
47
41
|
if (!top || top.length === 0) return null
|
|
48
42
|
|
|
49
43
|
const best = top[0]!
|
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 = IF total_writes = NONE THEN 1 ELSE total_writes + 1 END,
|
|
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 = IF total_queries = NONE THEN 1 ELSE total_queries + 1 END,
|
|
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
|
})
|