@smyslenny/agent-memory 2.2.0 → 4.0.0-alpha.1

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.
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // AgentMemory v2 — Sleep-cycle memory for AI agents
3
3
 
4
+ // src/bin/agent-memory.ts
5
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
6
+ import { basename, resolve } from "path";
7
+
4
8
  // src/core/db.ts
5
9
  import Database from "better-sqlite3";
6
10
  import { randomUUID } from "crypto";
7
- var SCHEMA_VERSION = 3;
11
+ var SCHEMA_VERSION = 5;
8
12
  var SCHEMA_SQL = `
9
13
  -- Memory entries
10
14
  CREATE TABLE IF NOT EXISTS memories (
@@ -67,14 +71,37 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
67
71
 
68
72
  -- Embeddings (optional semantic layer)
69
73
  CREATE TABLE IF NOT EXISTS embeddings (
70
- agent_id TEXT NOT NULL DEFAULT 'default',
71
- memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
72
- model TEXT NOT NULL,
73
- dim INTEGER NOT NULL,
74
- vector BLOB NOT NULL,
75
- created_at TEXT NOT NULL,
76
- updated_at TEXT NOT NULL,
77
- PRIMARY KEY (agent_id, memory_id, model)
74
+ id TEXT PRIMARY KEY,
75
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
76
+ provider_id TEXT NOT NULL,
77
+ vector BLOB,
78
+ content_hash TEXT NOT NULL,
79
+ status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
80
+ created_at TEXT NOT NULL,
81
+ UNIQUE(memory_id, provider_id)
82
+ );
83
+
84
+ -- Maintenance jobs (reflect / reindex checkpoints)
85
+ CREATE TABLE IF NOT EXISTS maintenance_jobs (
86
+ job_id TEXT PRIMARY KEY,
87
+ phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
88
+ status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
89
+ checkpoint TEXT,
90
+ error TEXT,
91
+ started_at TEXT NOT NULL,
92
+ finished_at TEXT
93
+ );
94
+
95
+ -- Feedback signals (recall/surface usefulness + governance priors)
96
+ CREATE TABLE IF NOT EXISTS feedback_events (
97
+ id TEXT PRIMARY KEY,
98
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
99
+ source TEXT NOT NULL DEFAULT 'surface',
100
+ useful INTEGER NOT NULL DEFAULT 1,
101
+ agent_id TEXT NOT NULL DEFAULT 'default',
102
+ event_type TEXT NOT NULL DEFAULT 'surface:useful',
103
+ value REAL NOT NULL DEFAULT 1.0,
104
+ created_at TEXT NOT NULL
78
105
  );
79
106
 
80
107
  -- Schema version tracking
@@ -91,6 +118,8 @@ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
91
118
  CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
92
119
  CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
93
120
  CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
121
+ CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
122
+ CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
94
123
  `;
95
124
  function openDatabase(opts) {
96
125
  const db = new Database(opts.path);
@@ -111,6 +140,7 @@ function openDatabase(opts) {
111
140
  migrateDatabase(db, currentVersion, SCHEMA_VERSION);
112
141
  }
113
142
  ensureIndexes(db);
143
+ ensureFeedbackEventSchema(db);
114
144
  return db;
115
145
  }
116
146
  function now() {
@@ -129,6 +159,14 @@ function getSchemaVersion(db) {
129
159
  return null;
130
160
  }
131
161
  }
162
+ function tableExists(db, table) {
163
+ try {
164
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(table);
165
+ return Boolean(row?.name);
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
132
170
  function tableHasColumn(db, table, column) {
133
171
  try {
134
172
  const cols = db.prepare(`PRAGMA table_info(${table})`).all();
@@ -150,6 +188,16 @@ function migrateDatabase(db, from, to) {
150
188
  v = 3;
151
189
  continue;
152
190
  }
191
+ if (v === 3) {
192
+ migrateV3ToV4(db);
193
+ v = 4;
194
+ continue;
195
+ }
196
+ if (v === 4) {
197
+ migrateV4ToV5(db);
198
+ v = 5;
199
+ continue;
200
+ }
153
201
  throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
154
202
  }
155
203
  }
@@ -231,14 +279,12 @@ function migrateV1ToV2(db) {
231
279
  function inferSchemaVersion(db) {
232
280
  const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
233
281
  const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
234
- const hasEmbeddings = (() => {
235
- try {
236
- const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
237
- return Boolean(row);
238
- } catch {
239
- return false;
240
- }
241
- })();
282
+ const hasEmbeddings = tableExists(db, "embeddings");
283
+ const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
284
+ const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
285
+ const hasFeedbackEvents = tableExists(db, "feedback_events");
286
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
287
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
242
288
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
243
289
  if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
244
290
  return 1;
@@ -251,14 +297,37 @@ function ensureIndexes(db) {
251
297
  db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
252
298
  db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
253
299
  }
254
- try {
255
- const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
256
- if (row) {
300
+ if (tableExists(db, "embeddings")) {
301
+ if (tableHasColumn(db, "embeddings", "provider_id")) {
302
+ db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_provider_status ON embeddings(provider_id, status);");
303
+ db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory_provider ON embeddings(memory_id, provider_id);");
304
+ } else {
257
305
  db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
258
306
  db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
259
307
  }
260
- } catch {
261
308
  }
309
+ if (tableExists(db, "maintenance_jobs")) {
310
+ db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
311
+ }
312
+ if (tableExists(db, "feedback_events")) {
313
+ db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
314
+ if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
315
+ db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
316
+ }
317
+ }
318
+ }
319
+ function ensureFeedbackEventSchema(db) {
320
+ if (!tableExists(db, "feedback_events")) return;
321
+ if (!tableHasColumn(db, "feedback_events", "source")) {
322
+ db.exec("ALTER TABLE feedback_events ADD COLUMN source TEXT NOT NULL DEFAULT 'surface';");
323
+ }
324
+ if (!tableHasColumn(db, "feedback_events", "useful")) {
325
+ db.exec("ALTER TABLE feedback_events ADD COLUMN useful INTEGER NOT NULL DEFAULT 1;");
326
+ }
327
+ if (!tableHasColumn(db, "feedback_events", "agent_id")) {
328
+ db.exec("ALTER TABLE feedback_events ADD COLUMN agent_id TEXT NOT NULL DEFAULT 'default';");
329
+ }
330
+ db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
262
331
  }
263
332
  function migrateV2ToV3(db) {
264
333
  try {
@@ -285,9 +354,97 @@ function migrateV2ToV3(db) {
285
354
  throw e;
286
355
  }
287
356
  }
357
+ function migrateV3ToV4(db) {
358
+ const alreadyMigrated = tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
359
+ if (alreadyMigrated) {
360
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
361
+ return;
362
+ }
363
+ try {
364
+ db.exec("BEGIN");
365
+ const legacyRows = tableExists(db, "embeddings") ? db.prepare(
366
+ `SELECT e.agent_id, e.memory_id, e.model, e.vector, e.created_at, m.hash
367
+ FROM embeddings e
368
+ LEFT JOIN memories m ON m.id = e.memory_id`
369
+ ).all() : [];
370
+ db.exec(`
371
+ CREATE TABLE IF NOT EXISTS embeddings_v4 (
372
+ id TEXT PRIMARY KEY,
373
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
374
+ provider_id TEXT NOT NULL,
375
+ vector BLOB,
376
+ content_hash TEXT NOT NULL,
377
+ status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
378
+ created_at TEXT NOT NULL,
379
+ UNIQUE(memory_id, provider_id)
380
+ );
381
+ `);
382
+ const insert = db.prepare(
383
+ `INSERT INTO embeddings_v4 (id, memory_id, provider_id, vector, content_hash, status, created_at)
384
+ VALUES (?, ?, ?, ?, ?, 'ready', ?)`
385
+ );
386
+ for (const row of legacyRows) {
387
+ insert.run(newId(), row.memory_id, `legacy:${row.agent_id}:${row.model}`, row.vector, row.hash ?? "", row.created_at);
388
+ }
389
+ if (tableExists(db, "embeddings")) {
390
+ db.exec("DROP TABLE embeddings;");
391
+ }
392
+ db.exec("ALTER TABLE embeddings_v4 RENAME TO embeddings;");
393
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
394
+ db.exec("COMMIT");
395
+ } catch (e) {
396
+ try {
397
+ db.exec("ROLLBACK");
398
+ } catch {
399
+ }
400
+ throw e;
401
+ }
402
+ }
403
+ function migrateV4ToV5(db) {
404
+ const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
405
+ const hasFeedbackEvents = tableExists(db, "feedback_events");
406
+ if (hasMaintenanceJobs && hasFeedbackEvents) {
407
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
408
+ return;
409
+ }
410
+ try {
411
+ db.exec("BEGIN");
412
+ db.exec(`
413
+ CREATE TABLE IF NOT EXISTS maintenance_jobs (
414
+ job_id TEXT PRIMARY KEY,
415
+ phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
416
+ status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
417
+ checkpoint TEXT,
418
+ error TEXT,
419
+ started_at TEXT NOT NULL,
420
+ finished_at TEXT
421
+ );
422
+ `);
423
+ db.exec(`
424
+ CREATE TABLE IF NOT EXISTS feedback_events (
425
+ id TEXT PRIMARY KEY,
426
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
427
+ source TEXT NOT NULL DEFAULT 'surface',
428
+ useful INTEGER NOT NULL DEFAULT 1,
429
+ agent_id TEXT NOT NULL DEFAULT 'default',
430
+ event_type TEXT NOT NULL DEFAULT 'surface:useful',
431
+ value REAL NOT NULL DEFAULT 1.0,
432
+ created_at TEXT NOT NULL
433
+ );
434
+ `);
435
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
436
+ db.exec("COMMIT");
437
+ } catch (e) {
438
+ try {
439
+ db.exec("ROLLBACK");
440
+ } catch {
441
+ }
442
+ throw e;
443
+ }
444
+ }
288
445
 
289
446
  // src/core/memory.ts
290
- import { createHash } from "crypto";
447
+ import { createHash as createHash2 } from "crypto";
291
448
 
292
449
  // src/search/tokenizer.ts
293
450
  import { readFileSync } from "fs";
@@ -368,9 +525,347 @@ function tokenizeForIndex(text) {
368
525
  return tokens.join(" ");
369
526
  }
370
527
 
528
+ // src/search/embedding.ts
529
+ import { createHash } from "crypto";
530
+ function trimTrailingSlashes(value) {
531
+ return value.replace(/\/+$/, "");
532
+ }
533
+ function resolveEndpoint(baseUrl, endpoint = "/embeddings") {
534
+ const trimmed = trimTrailingSlashes(baseUrl);
535
+ if (trimmed.endsWith("/embeddings")) {
536
+ return trimmed;
537
+ }
538
+ const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
539
+ return `${trimmed}${normalizedEndpoint}`;
540
+ }
541
+ function stableProviderId(prefix, input) {
542
+ const digest = createHash("sha256").update(input).digest("hex").slice(0, 12);
543
+ return `${prefix}:${digest}`;
544
+ }
545
+ function getFetch(fetchImpl) {
546
+ const candidate = fetchImpl ?? globalThis.fetch;
547
+ if (!candidate) {
548
+ throw new Error("Global fetch is not available in this runtime");
549
+ }
550
+ return candidate;
551
+ }
552
+ function assertEmbeddingVector(vector, dimension, context) {
553
+ if (!Array.isArray(vector) || !vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
554
+ throw new Error(`${context} returned an invalid embedding vector`);
555
+ }
556
+ if (vector.length !== dimension) {
557
+ throw new Error(`${context} returned dimension ${vector.length}, expected ${dimension}`);
558
+ }
559
+ return vector;
560
+ }
561
+ function parseOpenAIResponse(json2, dimension, context) {
562
+ const rows = json2?.data;
563
+ if (!Array.isArray(rows)) {
564
+ throw new Error(`${context} returned an invalid embeddings payload`);
565
+ }
566
+ return rows.map((row, index) => assertEmbeddingVector(row?.embedding, dimension, `${context} item ${index}`));
567
+ }
568
+ function parseLocalHttpResponse(json2, dimension, context) {
569
+ if (Array.isArray(json2.embeddings)) {
570
+ const embeddings = json2.embeddings;
571
+ return embeddings.map((row, index) => assertEmbeddingVector(row, dimension, `${context} item ${index}`));
572
+ }
573
+ return parseOpenAIResponse(json2, dimension, context);
574
+ }
575
+ async function runEmbeddingRequest(input) {
576
+ const fetchFn = getFetch(input.fetchImpl);
577
+ const response = await fetchFn(input.url, {
578
+ method: "POST",
579
+ headers: {
580
+ "content-type": "application/json",
581
+ ...input.headers
582
+ },
583
+ body: JSON.stringify(input.body)
584
+ });
585
+ if (!response.ok) {
586
+ const detail = await response.text().catch(() => "");
587
+ throw new Error(`${input.context} request failed: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
588
+ }
589
+ const json2 = await response.json();
590
+ return input.parser(json2, input.dimension, input.context);
591
+ }
592
+ function createOpenAICompatibleEmbeddingProvider(opts) {
593
+ const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
594
+ const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
595
+ const id = stableProviderId(`openai-compatible:${opts.model}`, providerDescriptor);
596
+ return {
597
+ id,
598
+ model: opts.model,
599
+ dimension: opts.dimension,
600
+ async embed(texts) {
601
+ if (texts.length === 0) return [];
602
+ return runEmbeddingRequest({
603
+ context: "openai-compatible embedding provider",
604
+ url,
605
+ dimension: opts.dimension,
606
+ fetchImpl: opts.fetchImpl,
607
+ headers: {
608
+ ...opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {},
609
+ ...opts.headers
610
+ },
611
+ body: {
612
+ model: opts.model,
613
+ input: texts
614
+ },
615
+ parser: parseOpenAIResponse
616
+ });
617
+ },
618
+ async healthcheck() {
619
+ await this.embed(["healthcheck"]);
620
+ }
621
+ };
622
+ }
623
+ function createLocalHttpEmbeddingProvider(opts) {
624
+ const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
625
+ const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
626
+ const id = stableProviderId(`local-http:${opts.model}`, providerDescriptor);
627
+ return {
628
+ id,
629
+ model: opts.model,
630
+ dimension: opts.dimension,
631
+ async embed(texts) {
632
+ if (texts.length === 0) return [];
633
+ return runEmbeddingRequest({
634
+ context: "local-http embedding provider",
635
+ url,
636
+ dimension: opts.dimension,
637
+ fetchImpl: opts.fetchImpl,
638
+ headers: opts.headers,
639
+ body: {
640
+ model: opts.model,
641
+ input: texts
642
+ },
643
+ parser: parseLocalHttpResponse
644
+ });
645
+ },
646
+ async healthcheck() {
647
+ await this.embed(["healthcheck"]);
648
+ }
649
+ };
650
+ }
651
+ function normalizeEmbeddingBaseUrl(baseUrl) {
652
+ return trimTrailingSlashes(baseUrl);
653
+ }
654
+
655
+ // src/search/providers.ts
656
+ function parseDimension(raw) {
657
+ if (!raw) return void 0;
658
+ const value = Number.parseInt(raw, 10);
659
+ return Number.isFinite(value) && value > 0 ? value : void 0;
660
+ }
661
+ function parseProvider(raw) {
662
+ if (!raw) return null;
663
+ if (raw === "openai-compatible" || raw === "local-http") {
664
+ return raw;
665
+ }
666
+ throw new Error(`Unsupported embedding provider: ${raw}`);
667
+ }
668
+ function getEmbeddingProviderConfigFromEnv(env = process.env) {
669
+ const provider = parseProvider(env.AGENT_MEMORY_EMBEDDING_PROVIDER);
670
+ if (!provider) return null;
671
+ const baseUrl = env.AGENT_MEMORY_EMBEDDING_BASE_URL;
672
+ const model = env.AGENT_MEMORY_EMBEDDING_MODEL;
673
+ const dimension = parseDimension(env.AGENT_MEMORY_EMBEDDING_DIMENSION);
674
+ if (!baseUrl) {
675
+ throw new Error("AGENT_MEMORY_EMBEDDING_BASE_URL is required when embeddings are enabled");
676
+ }
677
+ if (!model) {
678
+ throw new Error("AGENT_MEMORY_EMBEDDING_MODEL is required when embeddings are enabled");
679
+ }
680
+ if (!dimension) {
681
+ throw new Error("AGENT_MEMORY_EMBEDDING_DIMENSION is required when embeddings are enabled");
682
+ }
683
+ if (provider === "openai-compatible" && !env.AGENT_MEMORY_EMBEDDING_API_KEY) {
684
+ throw new Error("AGENT_MEMORY_EMBEDDING_API_KEY is required for openai-compatible providers");
685
+ }
686
+ return {
687
+ provider,
688
+ baseUrl,
689
+ model,
690
+ dimension,
691
+ apiKey: env.AGENT_MEMORY_EMBEDDING_API_KEY
692
+ };
693
+ }
694
+ function createEmbeddingProvider(input, opts) {
695
+ const normalized = {
696
+ ...input,
697
+ baseUrl: normalizeEmbeddingBaseUrl(input.baseUrl)
698
+ };
699
+ if (normalized.provider === "openai-compatible") {
700
+ return createOpenAICompatibleEmbeddingProvider({
701
+ baseUrl: normalized.baseUrl,
702
+ model: normalized.model,
703
+ dimension: normalized.dimension,
704
+ apiKey: normalized.apiKey,
705
+ fetchImpl: opts?.fetchImpl
706
+ });
707
+ }
708
+ return createLocalHttpEmbeddingProvider({
709
+ baseUrl: normalized.baseUrl,
710
+ model: normalized.model,
711
+ dimension: normalized.dimension,
712
+ fetchImpl: opts?.fetchImpl
713
+ });
714
+ }
715
+ function resolveEmbeddingProviderConfig(opts) {
716
+ const envConfig = getEmbeddingProviderConfigFromEnv(opts?.env);
717
+ if (!envConfig && !opts?.config?.provider) {
718
+ return null;
719
+ }
720
+ const provider = opts?.config?.provider ?? envConfig?.provider;
721
+ const baseUrl = opts?.config?.baseUrl ?? envConfig?.baseUrl;
722
+ const model = opts?.config?.model ?? envConfig?.model;
723
+ const dimension = opts?.config?.dimension ?? envConfig?.dimension;
724
+ const apiKey = opts?.config?.apiKey ?? envConfig?.apiKey;
725
+ if (!provider || !baseUrl || !model || !dimension) {
726
+ throw new Error("Incomplete embedding provider configuration");
727
+ }
728
+ if (provider === "openai-compatible" && !apiKey) {
729
+ throw new Error("OpenAI-compatible embedding providers require an API key");
730
+ }
731
+ return { provider, baseUrl, model, dimension, apiKey };
732
+ }
733
+ function getEmbeddingProvider(opts) {
734
+ const config = resolveEmbeddingProviderConfig({ config: opts?.config, env: opts?.env });
735
+ if (!config) return null;
736
+ return createEmbeddingProvider(config, { fetchImpl: opts?.fetchImpl });
737
+ }
738
+ function getEmbeddingProviderFromEnv(env = process.env) {
739
+ try {
740
+ return getEmbeddingProvider({ env });
741
+ } catch {
742
+ return null;
743
+ }
744
+ }
745
+ function getConfiguredEmbeddingProviderId(opts) {
746
+ try {
747
+ const provider = getEmbeddingProvider({ config: opts?.config, env: opts?.env });
748
+ return provider?.id ?? null;
749
+ } catch {
750
+ return null;
751
+ }
752
+ }
753
+
754
+ // src/search/vector.ts
755
+ function encodeVector(vector) {
756
+ const float32 = vector instanceof Float32Array ? vector : Float32Array.from(vector);
757
+ return Buffer.from(float32.buffer.slice(float32.byteOffset, float32.byteOffset + float32.byteLength));
758
+ }
759
+ function decodeVector(blob) {
760
+ const buffer = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
761
+ const aligned = buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength ? buffer.buffer : buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
762
+ return Array.from(new Float32Array(aligned));
763
+ }
764
+ function cosineSimilarity(a, b) {
765
+ const length = Math.min(a.length, b.length);
766
+ if (length === 0 || a.length !== b.length) return 0;
767
+ let dot = 0;
768
+ let normA = 0;
769
+ let normB = 0;
770
+ for (let index = 0; index < length; index++) {
771
+ const left = Number(a[index] ?? 0);
772
+ const right = Number(b[index] ?? 0);
773
+ dot += left * right;
774
+ normA += left * left;
775
+ normB += right * right;
776
+ }
777
+ if (normA === 0 || normB === 0) return 0;
778
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
779
+ }
780
+ function markMemoryEmbeddingPending(db, memoryId, providerId, contentHash2) {
781
+ db.prepare(
782
+ `INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
783
+ VALUES (?, ?, ?, NULL, ?, 'pending', ?)
784
+ ON CONFLICT(memory_id, provider_id) DO UPDATE SET
785
+ vector = NULL,
786
+ content_hash = excluded.content_hash,
787
+ status = 'pending'`
788
+ ).run(newId(), memoryId, providerId, contentHash2, now());
789
+ }
790
+ function markAllEmbeddingsPending(db, memoryId, contentHash2) {
791
+ const result = db.prepare(
792
+ `UPDATE embeddings
793
+ SET vector = NULL,
794
+ content_hash = ?,
795
+ status = 'pending'
796
+ WHERE memory_id = ?`
797
+ ).run(contentHash2, memoryId);
798
+ return result.changes;
799
+ }
800
+ function upsertReadyEmbedding(input) {
801
+ input.db.prepare(
802
+ `INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
803
+ VALUES (?, ?, ?, ?, ?, 'ready', ?)
804
+ ON CONFLICT(memory_id, provider_id) DO UPDATE SET
805
+ vector = excluded.vector,
806
+ content_hash = excluded.content_hash,
807
+ status = 'ready'`
808
+ ).run(newId(), input.memoryId, input.providerId, encodeVector(input.vector), input.contentHash, now());
809
+ }
810
+ function markEmbeddingFailed(db, memoryId, providerId, contentHash2) {
811
+ db.prepare(
812
+ `INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
813
+ VALUES (?, ?, ?, NULL, ?, 'failed', ?)
814
+ ON CONFLICT(memory_id, provider_id) DO UPDATE SET
815
+ vector = NULL,
816
+ content_hash = excluded.content_hash,
817
+ status = 'failed'`
818
+ ).run(newId(), memoryId, providerId, contentHash2, now());
819
+ }
820
+ function searchByVector(db, queryVector, opts) {
821
+ const limit = opts.limit ?? 20;
822
+ const agentId = opts.agent_id ?? "default";
823
+ const minVitality = opts.min_vitality ?? 0;
824
+ const rows = db.prepare(
825
+ `SELECT e.provider_id, e.vector, e.content_hash,
826
+ m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
827
+ m.stability, m.access_count, m.last_accessed, m.created_at,
828
+ m.updated_at, m.source, m.agent_id, m.hash
829
+ FROM embeddings e
830
+ JOIN memories m ON m.id = e.memory_id
831
+ WHERE e.provider_id = ?
832
+ AND e.status = 'ready'
833
+ AND e.vector IS NOT NULL
834
+ AND e.content_hash = m.hash
835
+ AND m.agent_id = ?
836
+ AND m.vitality >= ?`
837
+ ).all(opts.providerId, agentId, minVitality);
838
+ const scored = rows.map((row) => ({
839
+ provider_id: row.provider_id,
840
+ memory: {
841
+ id: row.id,
842
+ content: row.content,
843
+ type: row.type,
844
+ priority: row.priority,
845
+ emotion_val: row.emotion_val,
846
+ vitality: row.vitality,
847
+ stability: row.stability,
848
+ access_count: row.access_count,
849
+ last_accessed: row.last_accessed,
850
+ created_at: row.created_at,
851
+ updated_at: row.updated_at,
852
+ source: row.source,
853
+ agent_id: row.agent_id,
854
+ hash: row.hash
855
+ },
856
+ similarity: cosineSimilarity(queryVector, decodeVector(row.vector))
857
+ })).filter((row) => Number.isFinite(row.similarity) && row.similarity > 0).sort((left, right) => right.similarity - left.similarity).slice(0, limit);
858
+ return scored.map((row, index) => ({
859
+ memory: row.memory,
860
+ similarity: row.similarity,
861
+ rank: index + 1,
862
+ provider_id: row.provider_id
863
+ }));
864
+ }
865
+
371
866
  // src/core/memory.ts
372
867
  function contentHash(content) {
373
- return createHash("sha256").update(content.trim()).digest("hex").slice(0, 16);
868
+ return createHash2("sha256").update(content.trim()).digest("hex").slice(0, 16);
374
869
  }
375
870
  var TYPE_PRIORITY = {
376
871
  identity: 0,
@@ -388,6 +883,19 @@ var PRIORITY_STABILITY = {
388
883
  3: 14
389
884
  // P3: 14-day half-life
390
885
  };
886
+ function resolveEmbeddingProviderId(explicitProviderId) {
887
+ if (explicitProviderId !== void 0) {
888
+ return explicitProviderId;
889
+ }
890
+ return getConfiguredEmbeddingProviderId();
891
+ }
892
+ function markEmbeddingDirtyIfNeeded(db, memoryId, hash, providerId) {
893
+ if (!providerId) return;
894
+ try {
895
+ markMemoryEmbeddingPending(db, memoryId, providerId, hash);
896
+ } catch {
897
+ }
898
+ }
391
899
  function createMemory(db, input) {
392
900
  const hash = contentHash(input.content);
393
901
  const agentId = input.agent_id ?? "default";
@@ -417,6 +925,7 @@ function createMemory(db, input) {
417
925
  hash
418
926
  );
419
927
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
928
+ markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
420
929
  return getMemory(db, id);
421
930
  }
422
931
  function getMemory(db, id) {
@@ -427,9 +936,11 @@ function updateMemory(db, id, input) {
427
936
  if (!existing) return null;
428
937
  const fields = [];
429
938
  const values = [];
939
+ let nextHash = null;
430
940
  if (input.content !== void 0) {
941
+ nextHash = contentHash(input.content);
431
942
  fields.push("content = ?", "hash = ?");
432
- values.push(input.content, contentHash(input.content));
943
+ values.push(input.content, nextHash);
433
944
  }
434
945
  if (input.type !== void 0) {
435
946
  fields.push("type = ?");
@@ -462,6 +973,13 @@ function updateMemory(db, id, input) {
462
973
  if (input.content !== void 0) {
463
974
  db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
464
975
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
976
+ if (nextHash) {
977
+ try {
978
+ markAllEmbeddingsPending(db, id, nextHash);
979
+ } catch {
980
+ }
981
+ markEmbeddingDirtyIfNeeded(db, id, nextHash, resolveEmbeddingProviderId(input.embedding_provider_id));
982
+ }
465
983
  }
466
984
  return getMemory(db, id);
467
985
  }
@@ -577,144 +1095,98 @@ function exportMemories(db, dirPath, opts) {
577
1095
  return { exported, files };
578
1096
  }
579
1097
 
580
- // src/search/intent.ts
581
- var INTENT_PATTERNS = {
582
- factual: [
583
- // English
584
- /^(what|who|where|which|how much|how many)\b/i,
585
- /\b(name|address|number|password|config|setting)\b/i,
586
- // Chinese - questions about facts
587
- /是(什么|谁|哪|啥)/,
588
- /叫(什么|啥)/,
589
- /(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
590
- /(多少|几个|哪个|哪些|哪里)/,
591
- // Chinese - lookup patterns
592
- /(查一下|找一下|看看|搜一下)/,
593
- /(.+)是什么$/
594
- ],
595
- temporal: [
596
- // English
597
- /^(when|what time|how long)\b/i,
598
- /\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
599
- /\b(first|latest|newest|oldest|previous|next)\b/i,
600
- // Chinese - time expressions
601
- /什么时候/,
602
- /(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
603
- /(几月|几号|几点|多久|多长时间)/,
604
- /(上次|下次|第一次|最后一次|那天|那时)/,
605
- // Date patterns
606
- /\d{4}[-/.]\d{1,2}/,
607
- /\d{1,2}月\d{1,2}[日号]/,
608
- // Chinese - temporal context
609
- /(历史|记录|日志|以来|至今|期间)/
610
- ],
611
- causal: [
612
- // English
613
- /^(why|how come|what caused)\b/i,
614
- /\b(because|due to|reason|cause|result)\b/i,
615
- // Chinese - causal questions
616
- /为(什么|啥|何)/,
617
- /(原因|导致|造成|引起|因为|所以|结果)/,
618
- /(怎么回事|怎么了|咋回事|咋了)/,
619
- /(为啥|凭啥|凭什么)/,
620
- // Chinese - problem/diagnosis
621
- /(出(了|了什么)?问题|报错|失败|出错|bug)/
622
- ],
623
- exploratory: [
624
- // English
625
- /^(how|tell me about|explain|describe|show me)\b/i,
626
- /^(what do you think|what about|any)\b/i,
627
- /\b(overview|summary|list|compare)\b/i,
628
- // Chinese - exploratory
629
- /(怎么样|怎样|如何)/,
630
- /(介绍|说说|讲讲|聊聊|谈谈)/,
631
- /(有哪些|有什么|有没有)/,
632
- /(关于|对于|至于|关联)/,
633
- /(总结|概括|梳理|回顾|盘点)/,
634
- // Chinese - opinion/analysis
635
- /(看法|想法|意见|建议|评价|感觉|觉得)/,
636
- /(对比|比较|区别|差异|优缺点)/
637
- ]
638
- };
639
- var CN_STRUCTURE_BOOSTS = {
640
- factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
641
- temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
642
- causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
643
- exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
644
- };
645
- function classifyIntent(query) {
646
- const scores = {
647
- factual: 0,
648
- exploratory: 0,
649
- temporal: 0,
650
- causal: 0
651
- };
652
- for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
653
- for (const pattern of patterns) {
654
- if (pattern.test(query)) {
655
- scores[intent] += 1;
656
- }
657
- }
1098
+ // src/core/path.ts
1099
+ var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
1100
+ function parseUri(uri) {
1101
+ const match = uri.match(/^([a-z]+):\/\/(.+)$/);
1102
+ if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
1103
+ return { domain: match[1], path: match[2] };
1104
+ }
1105
+ function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
1106
+ const { domain } = parseUri(uri);
1107
+ const domains = validDomains ?? DEFAULT_DOMAINS;
1108
+ if (!domains.has(domain)) {
1109
+ throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
658
1110
  }
659
- for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
660
- for (const pattern of patterns) {
661
- if (pattern.test(query)) {
662
- scores[intent] += 0.5;
663
- }
664
- }
1111
+ const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
1112
+ if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
1113
+ if (agent_id && agent_id !== memoryAgent) {
1114
+ throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
665
1115
  }
666
- const tokens = tokenize(query);
667
- const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
668
- if (totalPatternScore === 0 && tokens.length <= 3) {
669
- scores.factual += 1;
670
- }
671
- let maxIntent = "factual";
672
- let maxScore = 0;
673
- for (const [intent, score] of Object.entries(scores)) {
674
- if (score > maxScore) {
675
- maxScore = score;
676
- maxIntent = intent;
677
- }
1116
+ const agentId = agent_id ?? memoryAgent;
1117
+ const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
1118
+ if (existing) {
1119
+ throw new Error(`URI already exists: ${uri}`);
678
1120
  }
679
- const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
680
- const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
681
- return { intent: maxIntent, confidence };
1121
+ const id = newId();
1122
+ db.prepare(
1123
+ "INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
1124
+ ).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
1125
+ return getPath(db, id);
682
1126
  }
683
- function getStrategy(intent) {
684
- switch (intent) {
685
- case "factual":
686
- return { boostRecent: false, boostPriority: true, limit: 5 };
687
- case "temporal":
688
- return { boostRecent: true, boostPriority: false, limit: 10 };
689
- case "causal":
690
- return { boostRecent: false, boostPriority: false, limit: 10 };
691
- case "exploratory":
692
- return { boostRecent: false, boostPriority: false, limit: 15 };
693
- }
1127
+ function getPath(db, id) {
1128
+ return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
1129
+ }
1130
+ function getPathByUri(db, uri, agent_id = "default") {
1131
+ return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
694
1132
  }
695
1133
 
696
- // src/search/rerank.ts
697
- function rerank(results, opts) {
698
- const now2 = Date.now();
699
- const scored = results.map((r) => {
700
- let finalScore = r.score;
701
- if (opts.boostPriority) {
702
- const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
703
- finalScore *= priorityMultiplier;
1134
+ // src/sleep/boot.ts
1135
+ function boot(db, opts) {
1136
+ const agentId = opts?.agent_id ?? "default";
1137
+ const corePaths = opts?.corePaths ?? [
1138
+ "core://agent",
1139
+ "core://user",
1140
+ "core://agent/identity",
1141
+ "core://user/identity"
1142
+ ];
1143
+ const memories = /* @__PURE__ */ new Map();
1144
+ const identities = listMemories(db, { agent_id: agentId, priority: 0 });
1145
+ for (const mem of identities) {
1146
+ memories.set(mem.id, mem);
1147
+ recordAccess(db, mem.id, 1.1);
1148
+ }
1149
+ const bootPaths = [];
1150
+ for (const uri of corePaths) {
1151
+ const path = getPathByUri(db, uri, agentId);
1152
+ if (path) {
1153
+ bootPaths.push(uri);
1154
+ if (!memories.has(path.memory_id)) {
1155
+ const mem = getMemory(db, path.memory_id);
1156
+ if (mem) {
1157
+ memories.set(mem.id, mem);
1158
+ recordAccess(db, mem.id, 1.1);
1159
+ }
1160
+ }
704
1161
  }
705
- if (opts.boostRecent && r.memory.updated_at) {
706
- const age = now2 - new Date(r.memory.updated_at).getTime();
707
- const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
708
- const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
709
- finalScore *= recencyBoost;
1162
+ }
1163
+ const bootEntry = getPathByUri(db, "system://boot", agentId);
1164
+ if (bootEntry) {
1165
+ const bootMem = getMemory(db, bootEntry.memory_id);
1166
+ if (bootMem) {
1167
+ const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
1168
+ for (const uri of additionalUris) {
1169
+ const path = getPathByUri(db, uri, agentId);
1170
+ if (path && !memories.has(path.memory_id)) {
1171
+ const mem = getMemory(db, path.memory_id);
1172
+ if (mem) {
1173
+ memories.set(mem.id, mem);
1174
+ bootPaths.push(uri);
1175
+ }
1176
+ }
1177
+ }
710
1178
  }
711
- finalScore *= Math.max(0.1, r.memory.vitality);
712
- return { ...r, score: finalScore };
713
- });
714
- scored.sort((a, b) => b.score - a.score);
715
- return scored.slice(0, opts.limit);
1179
+ }
1180
+ return {
1181
+ identityMemories: [...memories.values()],
1182
+ bootPaths
1183
+ };
716
1184
  }
717
1185
 
1186
+ // src/transports/http.ts
1187
+ import { randomUUID as randomUUID2 } from "crypto";
1188
+ import http from "http";
1189
+
718
1190
  // src/search/bm25.ts
719
1191
  function searchBM25(db, query, opts) {
720
1192
  const limit = opts?.limit ?? 20;
@@ -733,12 +1205,13 @@ function searchBM25(db, query, opts) {
733
1205
  ORDER BY rank
734
1206
  LIMIT ?`
735
1207
  ).all(ftsQuery, agentId, minVitality, limit);
736
- return rows.map((row) => {
1208
+ return rows.map((row, index) => {
737
1209
  const { score: _score, ...memoryFields } = row;
738
1210
  return {
739
1211
  memory: memoryFields,
740
1212
  score: Math.abs(row.score),
741
1213
  // FTS5 rank is negative (lower = better)
1214
+ rank: index + 1,
742
1215
  matchReason: "bm25"
743
1216
  };
744
1217
  });
@@ -753,10 +1226,10 @@ function searchSimple(db, query, agentId, minVitality, limit) {
753
1226
  ORDER BY priority ASC, updated_at DESC
754
1227
  LIMIT ?`
755
1228
  ).all(agentId, minVitality, `%${query}%`, limit);
756
- return rows.map((m, i) => ({
757
- memory: m,
758
- score: 1 / (i + 1),
759
- // Simple rank by position
1229
+ return rows.map((memory, index) => ({
1230
+ memory,
1231
+ score: 1 / (index + 1),
1232
+ rank: index + 1,
760
1233
  matchReason: "like"
761
1234
  }));
762
1235
  }
@@ -765,381 +1238,923 @@ function buildFtsQuery(text) {
765
1238
  if (tokens.length === 0) return null;
766
1239
  return tokens.map((w) => `"${w}"`).join(" OR ");
767
1240
  }
1241
+ function rebuildBm25Index(db, opts) {
1242
+ const memories = opts?.agent_id ? db.prepare("SELECT id, content FROM memories WHERE agent_id = ?").all(opts.agent_id) : db.prepare("SELECT id, content FROM memories").all();
1243
+ const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
1244
+ const deleteOne = db.prepare("DELETE FROM memories_fts WHERE id = ?");
1245
+ const transaction = db.transaction(() => {
1246
+ if (!opts?.agent_id) {
1247
+ db.exec("DELETE FROM memories_fts");
1248
+ }
1249
+ for (const memory of memories) {
1250
+ if (opts?.agent_id) {
1251
+ deleteOne.run(memory.id);
1252
+ }
1253
+ insert.run(memory.id, tokenizeForIndex(memory.content));
1254
+ }
1255
+ });
1256
+ transaction();
1257
+ return { reindexed: memories.length };
1258
+ }
768
1259
 
769
- // src/search/embeddings.ts
770
- function encodeEmbedding(vector) {
771
- const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
772
- return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
773
- }
774
- function decodeEmbedding(buf) {
775
- const copy = Buffer.from(buf);
776
- return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
777
- }
778
- function upsertEmbedding(db, input) {
779
- const ts = now();
780
- const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
781
- const blob = encodeEmbedding(vec);
782
- db.prepare(
783
- `INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
784
- VALUES (?, ?, ?, ?, ?, ?, ?)
785
- ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
786
- dim = excluded.dim,
787
- vector = excluded.vector,
788
- updated_at = excluded.updated_at`
789
- ).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
1260
+ // src/search/hybrid.ts
1261
+ var PRIORITY_WEIGHT = {
1262
+ 0: 4,
1263
+ 1: 3,
1264
+ 2: 2,
1265
+ 3: 1
1266
+ };
1267
+ var PRIORITY_PRIOR = {
1268
+ 0: 1,
1269
+ 1: 0.75,
1270
+ 2: 0.5,
1271
+ 3: 0.25
1272
+ };
1273
+ function scoreBm25Only(results, limit) {
1274
+ return results.map((row) => {
1275
+ const weight = PRIORITY_WEIGHT[row.memory.priority] ?? 1;
1276
+ const vitality = Math.max(0.1, row.memory.vitality);
1277
+ return {
1278
+ memory: row.memory,
1279
+ score: row.score * weight * vitality,
1280
+ bm25_rank: row.rank,
1281
+ bm25_score: row.score
1282
+ };
1283
+ }).sort((left, right) => right.score - left.score).slice(0, limit);
1284
+ }
1285
+ function priorityPrior(priority) {
1286
+ return PRIORITY_PRIOR[priority] ?? 0.25;
1287
+ }
1288
+ function fusionScore(input) {
1289
+ const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
1290
+ const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
1291
+ return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
1292
+ }
1293
+ function fuseHybridResults(lexical, vector, limit) {
1294
+ const candidates = /* @__PURE__ */ new Map();
1295
+ for (const row of lexical) {
1296
+ candidates.set(row.memory.id, {
1297
+ memory: row.memory,
1298
+ score: 0,
1299
+ bm25_rank: row.rank,
1300
+ bm25_score: row.score
1301
+ });
1302
+ }
1303
+ for (const row of vector) {
1304
+ const existing = candidates.get(row.memory.id);
1305
+ if (existing) {
1306
+ existing.vector_rank = row.rank;
1307
+ existing.vector_score = row.similarity;
1308
+ } else {
1309
+ candidates.set(row.memory.id, {
1310
+ memory: row.memory,
1311
+ score: 0,
1312
+ vector_rank: row.rank,
1313
+ vector_score: row.similarity
1314
+ });
1315
+ }
1316
+ }
1317
+ return [...candidates.values()].map((row) => ({
1318
+ ...row,
1319
+ score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
1320
+ })).sort((left, right) => {
1321
+ if (right.score !== left.score) return right.score - left.score;
1322
+ return right.memory.updated_at.localeCompare(left.memory.updated_at);
1323
+ }).slice(0, limit);
1324
+ }
1325
+ async function searchVectorBranch(db, query, opts) {
1326
+ const [queryVector] = await opts.provider.embed([query]);
1327
+ if (!queryVector) return [];
1328
+ return searchByVector(db, queryVector, {
1329
+ providerId: opts.provider.id,
1330
+ agent_id: opts.agent_id,
1331
+ limit: opts.limit,
1332
+ min_vitality: opts.min_vitality
1333
+ });
1334
+ }
1335
+ async function recallMemories(db, query, opts) {
1336
+ const limit = opts?.limit ?? 10;
1337
+ const agentId = opts?.agent_id ?? "default";
1338
+ const minVitality = opts?.min_vitality ?? 0;
1339
+ const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
1340
+ const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
1341
+ const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
1342
+ const lexical = searchBM25(db, query, {
1343
+ agent_id: agentId,
1344
+ limit: lexicalLimit,
1345
+ min_vitality: minVitality
1346
+ });
1347
+ let vector = [];
1348
+ if (provider) {
1349
+ try {
1350
+ vector = await searchVectorBranch(db, query, {
1351
+ provider,
1352
+ agent_id: agentId,
1353
+ limit: vectorLimit,
1354
+ min_vitality: minVitality
1355
+ });
1356
+ } catch {
1357
+ vector = [];
1358
+ }
1359
+ }
1360
+ const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
1361
+ const results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit);
1362
+ if (opts?.recordAccess !== false) {
1363
+ for (const row of results) {
1364
+ recordAccess(db, row.memory.id);
1365
+ }
1366
+ }
1367
+ return {
1368
+ mode,
1369
+ providerId: provider?.id ?? null,
1370
+ usedVectorSearch: vector.length > 0,
1371
+ results
1372
+ };
790
1373
  }
791
- function listEmbeddings(db, agent_id, model) {
1374
+ function listReindexCandidates(db, providerId, agentId, force) {
792
1375
  const rows = db.prepare(
793
- "SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
794
- ).all(agent_id, model);
795
- return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
1376
+ `SELECT m.id as memoryId,
1377
+ m.content as content,
1378
+ m.hash as contentHash,
1379
+ e.status as embeddingStatus,
1380
+ e.content_hash as embeddingHash
1381
+ FROM memories m
1382
+ LEFT JOIN embeddings e
1383
+ ON e.memory_id = m.id
1384
+ AND e.provider_id = ?
1385
+ WHERE m.agent_id = ?
1386
+ AND m.hash IS NOT NULL`
1387
+ ).all(providerId, agentId);
1388
+ return rows.filter((row) => {
1389
+ if (force) return true;
1390
+ if (!row.embeddingStatus) return true;
1391
+ if (row.embeddingStatus !== "ready") return true;
1392
+ return row.embeddingHash !== row.contentHash;
1393
+ }).map((row) => ({
1394
+ memoryId: row.memoryId,
1395
+ content: row.content,
1396
+ contentHash: row.contentHash
1397
+ }));
796
1398
  }
797
-
798
- // src/search/hybrid.ts
799
- function cosine(a, b) {
800
- const n = Math.min(a.length, b.length);
801
- let dot = 0;
802
- let na = 0;
803
- let nb = 0;
804
- for (let i = 0; i < n; i++) {
805
- const x = a[i];
806
- const y = b[i];
807
- dot += x * y;
808
- na += x * x;
809
- nb += y * y;
810
- }
811
- if (na === 0 || nb === 0) return 0;
812
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
813
- }
814
- function rrfScore(rank, k) {
815
- return 1 / (k + rank);
816
- }
817
- function fuseRrf(lists, k) {
818
- const out = /* @__PURE__ */ new Map();
819
- for (const list of lists) {
820
- for (let i = 0; i < list.items.length; i++) {
821
- const it = list.items[i];
822
- const rank = i + 1;
823
- const add = rrfScore(rank, k);
824
- const prev = out.get(it.id);
825
- if (!prev) out.set(it.id, { score: add, sources: [list.name] });
826
- else {
827
- prev.score += add;
828
- if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
1399
+ async function reindexEmbeddings(db, opts) {
1400
+ const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
1401
+ if (!provider) {
1402
+ return {
1403
+ enabled: false,
1404
+ providerId: null,
1405
+ scanned: 0,
1406
+ pending: 0,
1407
+ embedded: 0,
1408
+ failed: 0
1409
+ };
1410
+ }
1411
+ const agentId = opts?.agent_id ?? "default";
1412
+ const force = opts?.force ?? false;
1413
+ const batchSize = Math.max(1, opts?.batchSize ?? 16);
1414
+ const candidates = listReindexCandidates(db, provider.id, agentId, force);
1415
+ for (const candidate of candidates) {
1416
+ markMemoryEmbeddingPending(db, candidate.memoryId, provider.id, candidate.contentHash);
1417
+ }
1418
+ let embedded = 0;
1419
+ let failed = 0;
1420
+ for (let index = 0; index < candidates.length; index += batchSize) {
1421
+ const batch = candidates.slice(index, index + batchSize);
1422
+ try {
1423
+ const vectors = await provider.embed(batch.map((row) => row.content));
1424
+ if (vectors.length !== batch.length) {
1425
+ throw new Error(`Expected ${batch.length} embeddings, received ${vectors.length}`);
1426
+ }
1427
+ for (let offset = 0; offset < batch.length; offset++) {
1428
+ upsertReadyEmbedding({
1429
+ db,
1430
+ memoryId: batch[offset].memoryId,
1431
+ providerId: provider.id,
1432
+ vector: vectors[offset],
1433
+ contentHash: batch[offset].contentHash
1434
+ });
1435
+ embedded += 1;
1436
+ }
1437
+ } catch {
1438
+ for (const candidate of batch) {
1439
+ markEmbeddingFailed(db, candidate.memoryId, provider.id, candidate.contentHash);
1440
+ failed += 1;
829
1441
  }
830
1442
  }
831
1443
  }
832
- return out;
1444
+ return {
1445
+ enabled: true,
1446
+ providerId: provider.id,
1447
+ scanned: candidates.length,
1448
+ pending: candidates.length,
1449
+ embedded,
1450
+ failed
1451
+ };
833
1452
  }
834
- function fetchMemories(db, ids, agentId) {
835
- if (ids.length === 0) return [];
836
- const placeholders = ids.map(() => "?").join(", ");
837
- const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
838
- const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
839
- return rows;
1453
+
1454
+ // src/core/merge.ts
1455
+ function uniqueNonEmpty(values) {
1456
+ return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))];
1457
+ }
1458
+ function splitClauses(content) {
1459
+ return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
1460
+ }
1461
+ function mergeAliases(existing, incoming, content) {
1462
+ const aliases = uniqueNonEmpty([
1463
+ existing !== content ? existing : void 0,
1464
+ incoming !== content ? incoming : void 0
1465
+ ]);
1466
+ return aliases.length > 0 ? aliases : void 0;
1467
+ }
1468
+ function replaceIdentity(context) {
1469
+ const content = context.incoming.content.trim();
1470
+ return {
1471
+ strategy: "replace",
1472
+ content,
1473
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1474
+ notes: ["identity canonicalized to the newest authoritative phrasing"]
1475
+ };
840
1476
  }
841
- async function searchHybrid(db, query, opts) {
842
- const agentId = opts?.agent_id ?? "default";
843
- const limit = opts?.limit ?? 10;
844
- const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
845
- const semanticCandidates = opts?.semanticCandidates ?? 50;
846
- const rrfK = opts?.rrfK ?? 60;
847
- const bm25 = searchBM25(db, query, {
848
- agent_id: agentId,
849
- limit: limit * bm25Mult
850
- });
851
- const provider = opts?.embeddingProvider ?? null;
852
- const model = opts?.embeddingModel ?? provider?.model;
853
- if (!provider || !model) {
854
- return bm25.slice(0, limit);
855
- }
856
- const embedFn = provider.embedQuery ?? provider.embed;
857
- const qVec = Float32Array.from(await embedFn.call(provider, query));
858
- const embeddings = listEmbeddings(db, agentId, model);
859
- const scored = [];
860
- for (const e of embeddings) {
861
- scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
862
- }
863
- scored.sort((a, b) => b.score - a.score);
864
- const semanticTop = scored.slice(0, semanticCandidates);
865
- const fused = fuseRrf(
866
- [
867
- { name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
868
- { name: "semantic", items: semanticTop }
869
- ],
870
- rrfK
871
- );
872
- const ids = [...fused.keys()];
873
- const memories = fetchMemories(db, ids, agentId);
874
- const byId = new Map(memories.map((m) => [m.id, m]));
875
- const out = [];
876
- for (const [id, meta] of fused) {
877
- const mem = byId.get(id);
878
- if (!mem) continue;
879
- out.push({
880
- memory: mem,
881
- score: meta.score,
882
- matchReason: meta.sources.sort().join("+")
883
- });
1477
+ function appendEmotionEvidence(context) {
1478
+ const lines = uniqueNonEmpty([
1479
+ ...context.existing.content.split(/\n+/),
1480
+ context.incoming.content
1481
+ ]);
1482
+ const content = lines.length <= 1 ? lines[0] ?? context.incoming.content.trim() : [lines[0], "", ...lines.slice(1).map((line) => `- ${line.replace(/^-\s*/, "")}`)].join("\n");
1483
+ return {
1484
+ strategy: "append_evidence",
1485
+ content,
1486
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1487
+ notes: ["emotion evidence appended to preserve timeline without duplicating identical lines"]
1488
+ };
1489
+ }
1490
+ function synthesizeKnowledge(context) {
1491
+ const clauses = uniqueNonEmpty([
1492
+ ...splitClauses(context.existing.content),
1493
+ ...splitClauses(context.incoming.content)
1494
+ ]);
1495
+ const content = clauses.length <= 1 ? clauses[0] ?? context.incoming.content.trim() : clauses.join("\uFF1B");
1496
+ return {
1497
+ strategy: "synthesize",
1498
+ content,
1499
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1500
+ notes: ["knowledge statements synthesized into a canonical summary"]
1501
+ };
1502
+ }
1503
+ function compactEventTimeline(context) {
1504
+ const points = uniqueNonEmpty([
1505
+ ...context.existing.content.split(/\n+/),
1506
+ context.incoming.content
1507
+ ]).map((line) => line.replace(/^-\s*/, ""));
1508
+ const content = points.length <= 1 ? points[0] ?? context.incoming.content.trim() : ["Timeline:", ...points.map((line) => `- ${line}`)].join("\n");
1509
+ return {
1510
+ strategy: "compact_timeline",
1511
+ content,
1512
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1513
+ notes: ["event observations compacted into a single timeline window"]
1514
+ };
1515
+ }
1516
+ function buildMergePlan(context) {
1517
+ const type = context.incoming.type ?? context.existing.type;
1518
+ switch (type) {
1519
+ case "identity":
1520
+ return replaceIdentity(context);
1521
+ case "emotion":
1522
+ return appendEmotionEvidence(context);
1523
+ case "knowledge":
1524
+ return synthesizeKnowledge(context);
1525
+ case "event":
1526
+ return compactEventTimeline(context);
884
1527
  }
885
- out.sort((a, b) => b.score - a.score);
886
- return out.slice(0, limit);
887
1528
  }
888
1529
 
889
- // src/search/providers.ts
890
- var QWEN_DEFAULT_INSTRUCTION = "Given a query, retrieve the most semantically relevant document";
891
- function getDefaultInstruction(model) {
892
- const m = model.toLowerCase();
893
- if (m.includes("qwen")) return QWEN_DEFAULT_INSTRUCTION;
894
- if (m.includes("gemini")) return null;
895
- return null;
1530
+ // src/core/guard.ts
1531
+ var NEAR_EXACT_THRESHOLD = 0.93;
1532
+ var MERGE_THRESHOLD = 0.82;
1533
+ function clamp01(value) {
1534
+ if (!Number.isFinite(value)) return 0;
1535
+ return Math.max(0, Math.min(1, value));
1536
+ }
1537
+ function uniqueTokenSet(text) {
1538
+ return new Set(tokenize(text));
1539
+ }
1540
+ function overlapScore(left, right) {
1541
+ const a = new Set(left);
1542
+ const b = new Set(right);
1543
+ if (a.size === 0 || b.size === 0) return 0;
1544
+ let shared = 0;
1545
+ for (const token of a) {
1546
+ if (b.has(token)) shared += 1;
1547
+ }
1548
+ return shared / Math.max(a.size, b.size);
1549
+ }
1550
+ function extractEntities(text) {
1551
+ const matches = text.match(/[A-Z][A-Za-z0-9_-]+|\d+(?:[-/:]\d+)*|[#@][\w-]+|[\u4e00-\u9fff]{2,}|\w+:\/\/[^\s]+/g) ?? [];
1552
+ return new Set(matches.map((value) => value.trim()).filter(Boolean));
1553
+ }
1554
+ function safeDomain(uri) {
1555
+ if (!uri) return null;
1556
+ try {
1557
+ return parseUri(uri).domain;
1558
+ } catch {
1559
+ return null;
1560
+ }
896
1561
  }
897
- function resolveInstruction(model) {
898
- const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
899
- if (override !== void 0) {
900
- const normalized = override.trim();
901
- if (!normalized) return null;
902
- const lowered = normalized.toLowerCase();
903
- if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
904
- return normalized;
905
- }
906
- return getDefaultInstruction(model);
907
- }
908
- function buildQueryInput(query, instructionPrefix) {
909
- if (!instructionPrefix) return query;
910
- return `Instruct: ${instructionPrefix}
911
- Query: ${query}`;
912
- }
913
- function getEmbeddingProviderFromEnv() {
914
- const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
915
- if (provider === "none" || provider === "off" || provider === "false") return null;
916
- if (provider === "openai") {
917
- const apiKey = process.env.OPENAI_API_KEY;
918
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
919
- const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
920
- if (!apiKey) return null;
921
- const instruction = resolveInstruction(model);
922
- return createOpenAIProvider({ apiKey, model, baseUrl, instruction });
923
- }
924
- if (provider === "gemini" || provider === "google") {
925
- const apiKey = process.env.GEMINI_API_KEY ?? process.env.OPENAI_API_KEY;
926
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "gemini-embedding-001";
927
- const baseUrl = process.env.GEMINI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta";
928
- if (!apiKey) return null;
929
- const instruction = resolveInstruction(model);
930
- return createOpenAIProvider({ id: "gemini", apiKey, model, baseUrl, instruction });
931
- }
932
- if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
933
- const apiKey = process.env.DASHSCOPE_API_KEY;
934
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
935
- const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
936
- if (!apiKey) return null;
937
- const instruction = resolveInstruction(model);
938
- return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
1562
+ function getPrimaryUri(db, memoryId, agentId) {
1563
+ const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
1564
+ return row?.uri ?? null;
1565
+ }
1566
+ function uriScopeMatch(inputUri, candidateUri) {
1567
+ if (inputUri && candidateUri) {
1568
+ if (inputUri === candidateUri) return 1;
1569
+ const inputDomain2 = safeDomain(inputUri);
1570
+ const candidateDomain2 = safeDomain(candidateUri);
1571
+ if (inputDomain2 && candidateDomain2 && inputDomain2 === candidateDomain2) return 0.85;
1572
+ return 0;
1573
+ }
1574
+ if (!inputUri && !candidateUri) {
1575
+ return 0.65;
1576
+ }
1577
+ const inputDomain = safeDomain(inputUri ?? null);
1578
+ const candidateDomain = safeDomain(candidateUri ?? null);
1579
+ if (inputDomain && candidateDomain && inputDomain === candidateDomain) {
1580
+ return 0.75;
1581
+ }
1582
+ return 0.2;
1583
+ }
1584
+ function extractObservedAt(parts, fallback) {
1585
+ for (const part of parts) {
1586
+ if (!part) continue;
1587
+ const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
1588
+ if (!match) continue;
1589
+ const iso = match[2] ? `${match[1]}T${match[2]}Z` : `${match[1]}T00:00:00Z`;
1590
+ const parsed = new Date(iso);
1591
+ if (!Number.isNaN(parsed.getTime())) {
1592
+ return parsed;
1593
+ }
1594
+ }
1595
+ if (fallback) {
1596
+ const parsed = new Date(fallback);
1597
+ if (!Number.isNaN(parsed.getTime())) {
1598
+ return parsed;
1599
+ }
939
1600
  }
940
1601
  return null;
941
1602
  }
942
- function authHeader(apiKey) {
943
- return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
1603
+ function timeProximity(input, memory, candidateUri) {
1604
+ if (input.type !== "event") {
1605
+ return 1;
1606
+ }
1607
+ const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
1608
+ const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
1609
+ if (!inputTime || !existingTime) {
1610
+ return 0.5;
1611
+ }
1612
+ const diffDays = Math.abs(inputTime.getTime() - existingTime.getTime()) / (1e3 * 60 * 60 * 24);
1613
+ return clamp01(1 - diffDays / 7);
1614
+ }
1615
+ function scoreCandidate(input, candidate, candidateUri) {
1616
+ const lexicalOverlap = overlapScore(uniqueTokenSet(input.content), uniqueTokenSet(candidate.memory.content));
1617
+ const entityOverlap = Math.max(
1618
+ overlapScore(extractEntities(input.content), extractEntities(candidate.memory.content)),
1619
+ lexicalOverlap * 0.75
1620
+ );
1621
+ const uriMatch = uriScopeMatch(input.uri, candidateUri);
1622
+ const temporal = timeProximity(input, candidate.memory, candidateUri);
1623
+ const semantic = clamp01(candidate.vector_score ?? lexicalOverlap);
1624
+ const dedupScore = clamp01(
1625
+ 0.5 * semantic + 0.2 * lexicalOverlap + 0.15 * uriMatch + 0.1 * entityOverlap + 0.05 * temporal
1626
+ );
1627
+ return {
1628
+ semantic_similarity: semantic,
1629
+ lexical_overlap: lexicalOverlap,
1630
+ uri_scope_match: uriMatch,
1631
+ entity_overlap: entityOverlap,
1632
+ time_proximity: temporal,
1633
+ dedup_score: dedupScore
1634
+ };
944
1635
  }
945
- function normalizeEmbedding(e) {
946
- if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
947
- if (e.length === 0) throw new Error("Invalid embedding: empty");
948
- return e.map((x) => {
949
- if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
950
- return x;
1636
+ async function recallCandidates(db, input, agentId) {
1637
+ const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
1638
+ const response = await recallMemories(db, input.content, {
1639
+ agent_id: agentId,
1640
+ limit: Math.max(6, input.candidateLimit ?? 8),
1641
+ lexicalLimit: Math.max(8, input.candidateLimit ?? 8),
1642
+ vectorLimit: Math.max(8, input.candidateLimit ?? 8),
1643
+ provider,
1644
+ recordAccess: false
951
1645
  });
1646
+ return response.results.filter((row) => row.memory.type === input.type).map((row) => {
1647
+ const uri = getPrimaryUri(db, row.memory.id, agentId);
1648
+ return {
1649
+ result: row,
1650
+ uri,
1651
+ domain: safeDomain(uri),
1652
+ score: scoreCandidate(input, row, uri)
1653
+ };
1654
+ }).sort((left, right) => right.score.dedup_score - left.score.dedup_score);
952
1655
  }
953
- function createOpenAIProvider(opts) {
954
- const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
955
- const instructionPrefix = opts.instruction ?? null;
956
- async function requestEmbedding(input) {
957
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
958
- method: "POST",
959
- headers: {
960
- "content-type": "application/json",
961
- authorization: authHeader(opts.apiKey)
962
- },
963
- body: JSON.stringify({ model: opts.model, input })
964
- });
965
- if (!resp.ok) {
966
- const body = await resp.text().catch(() => "");
967
- throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
968
- }
969
- const data = await resp.json();
970
- return normalizeEmbedding(data.data?.[0]?.embedding);
971
- }
1656
+ function fourCriterionGate(input) {
1657
+ const content = input.content.trim();
1658
+ const failed = [];
1659
+ const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
1660
+ const minLength = priority <= 1 ? 4 : 8;
1661
+ const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
1662
+ if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
1663
+ const tokens = tokenize(content);
1664
+ const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
1665
+ if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
1666
+ const hasCJK = /[\u4e00-\u9fff]/.test(content);
1667
+ const hasCapitalized = /[A-Z][a-z]+/.test(content);
1668
+ const hasNumbers = /\d+/.test(content);
1669
+ const hasURI = /\w+:\/\//.test(content);
1670
+ const hasEntityMarkers = /[@#]/.test(content);
1671
+ const hasMeaningfulLength = content.length >= 15;
1672
+ const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
1673
+ const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
1674
+ if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
1675
+ const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
1676
+ const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
1677
+ const excessiveRepetition = /(.)\1{9,}/.test(content);
1678
+ let coherence = 1;
1679
+ if (allCaps) coherence -= 0.5;
1680
+ if (!hasWhitespaceOrPunctuation) coherence -= 0.3;
1681
+ if (excessiveRepetition) coherence -= 0.5;
1682
+ coherence = Math.max(0, coherence);
1683
+ if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
972
1684
  return {
973
- id: opts.id ?? "openai",
974
- model: opts.model,
975
- instructionPrefix,
976
- async embed(text) {
977
- return requestEmbedding(text);
978
- },
979
- async embedQuery(query) {
980
- return requestEmbedding(buildQueryInput(query, instructionPrefix));
981
- }
1685
+ pass: failed.length === 0,
1686
+ scores: { specificity, novelty, relevance, coherence },
1687
+ failedCriteria: failed
982
1688
  };
983
1689
  }
984
- function createDashScopeProvider(opts) {
985
- const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
986
- const instructionPrefix = opts.instruction ?? null;
987
- async function requestEmbedding(text) {
988
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
989
- method: "POST",
990
- headers: {
991
- "content-type": "application/json",
992
- authorization: authHeader(opts.apiKey)
993
- },
994
- body: JSON.stringify({
995
- model: opts.model,
996
- input: { texts: [text] }
997
- })
998
- });
999
- if (!resp.ok) {
1000
- const body = await resp.text().catch(() => "");
1001
- throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
1690
+ async function guard(db, input) {
1691
+ const hash = contentHash(input.content);
1692
+ const agentId = input.agent_id ?? "default";
1693
+ const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
1694
+ if (exactMatch) {
1695
+ return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
1696
+ }
1697
+ if (input.uri) {
1698
+ const existingPath = getPathByUri(db, input.uri, agentId);
1699
+ if (existingPath) {
1700
+ return {
1701
+ action: "update",
1702
+ reason: `URI ${input.uri} already exists, updating canonical content`,
1703
+ existingId: existingPath.memory_id,
1704
+ updatedContent: input.content
1705
+ };
1002
1706
  }
1003
- const data = await resp.json();
1004
- const emb = data.output?.embeddings?.[0]?.embedding ?? data.output?.embeddings?.[0]?.vector ?? data.output?.embedding ?? data.data?.[0]?.embedding;
1005
- return normalizeEmbedding(emb);
1707
+ }
1708
+ const gateResult = fourCriterionGate(input);
1709
+ if (!gateResult.pass) {
1710
+ return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
1711
+ }
1712
+ if (input.conservative) {
1713
+ return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
1714
+ }
1715
+ const candidates = await recallCandidates(db, input, agentId);
1716
+ const best = candidates[0];
1717
+ if (!best) {
1718
+ return { action: "add", reason: "No relevant semantic candidates found" };
1719
+ }
1720
+ const score = best.score;
1721
+ if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
1722
+ const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
1723
+ return {
1724
+ action: shouldUpdateMetadata ? "update" : "skip",
1725
+ reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
1726
+ existingId: best.result.memory.id,
1727
+ score
1728
+ };
1729
+ }
1730
+ if (score.dedup_score >= MERGE_THRESHOLD) {
1731
+ const mergePlan = buildMergePlan({
1732
+ existing: best.result.memory,
1733
+ incoming: {
1734
+ content: input.content,
1735
+ type: input.type,
1736
+ source: input.source
1737
+ }
1738
+ });
1739
+ return {
1740
+ action: "merge",
1741
+ reason: `Semantic near-duplicate detected (score=${score.dedup_score.toFixed(3)}), applying ${mergePlan.strategy}`,
1742
+ existingId: best.result.memory.id,
1743
+ mergedContent: mergePlan.content,
1744
+ mergePlan,
1745
+ score
1746
+ };
1006
1747
  }
1007
1748
  return {
1008
- id: "dashscope",
1009
- model: opts.model,
1010
- instructionPrefix,
1011
- async embed(text) {
1012
- return requestEmbedding(text);
1013
- },
1014
- async embedQuery(query) {
1015
- return requestEmbedding(buildQueryInput(query, instructionPrefix));
1016
- }
1749
+ action: "add",
1750
+ reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
1751
+ score
1017
1752
  };
1018
1753
  }
1019
1754
 
1020
- // src/search/embed.ts
1021
- async function embedMemory(db, memoryId, provider, opts) {
1022
- const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
1023
- if (!row) return false;
1024
- if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
1025
- const model = opts?.model ?? provider.model;
1026
- const maxChars = opts?.maxChars ?? 2e3;
1027
- const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
1028
- const vector = await provider.embed(text);
1029
- upsertEmbedding(db, {
1030
- agent_id: row.agent_id,
1031
- memory_id: row.id,
1032
- model,
1033
- vector
1034
- });
1035
- return true;
1755
+ // src/sleep/sync.ts
1756
+ function ensureUriPath(db, memoryId, uri, agentId) {
1757
+ if (!uri) return;
1758
+ if (getPathByUri(db, uri, agentId ?? "default")) return;
1759
+ try {
1760
+ createPath(db, memoryId, uri, void 0, void 0, agentId);
1761
+ } catch {
1762
+ }
1036
1763
  }
1037
- async function embedMissingForAgent(db, provider, opts) {
1038
- const agentId = opts?.agent_id ?? "default";
1039
- const model = opts?.model ?? provider.model;
1040
- const limit = opts?.limit ?? 1e3;
1041
- const rows = db.prepare(
1042
- `SELECT m.id
1043
- FROM memories m
1044
- LEFT JOIN embeddings e
1045
- ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
1046
- WHERE m.agent_id = ? AND e.memory_id IS NULL
1047
- ORDER BY m.updated_at DESC
1048
- LIMIT ?`
1049
- ).all(model, agentId, limit);
1050
- let embedded = 0;
1051
- for (const r of rows) {
1052
- const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
1053
- if (ok) embedded++;
1764
+ async function syncOne(db, input) {
1765
+ const memInput = {
1766
+ content: input.content,
1767
+ type: input.type ?? "event",
1768
+ priority: input.priority,
1769
+ emotion_val: input.emotion_val,
1770
+ source: input.source,
1771
+ agent_id: input.agent_id,
1772
+ uri: input.uri,
1773
+ provider: input.provider,
1774
+ conservative: input.conservative
1775
+ };
1776
+ const guardResult = await guard(db, memInput);
1777
+ switch (guardResult.action) {
1778
+ case "skip":
1779
+ return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
1780
+ case "add": {
1781
+ const mem = createMemory(db, memInput);
1782
+ if (!mem) return { action: "skipped", reason: "createMemory returned null" };
1783
+ ensureUriPath(db, mem.id, input.uri, input.agent_id);
1784
+ return { action: "added", memoryId: mem.id, reason: guardResult.reason };
1785
+ }
1786
+ case "update": {
1787
+ if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
1788
+ if (guardResult.updatedContent !== void 0) {
1789
+ updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
1790
+ }
1791
+ ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
1792
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
1793
+ }
1794
+ case "merge": {
1795
+ if (!guardResult.existingId || !guardResult.mergedContent) {
1796
+ return { action: "skipped", reason: "Missing merge data" };
1797
+ }
1798
+ updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
1799
+ ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
1800
+ return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
1801
+ }
1054
1802
  }
1055
- return { embedded, scanned: rows.length };
1056
1803
  }
1057
1804
 
1058
- // src/core/path.ts
1059
- var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
1060
- function parseUri(uri) {
1061
- const match = uri.match(/^([a-z]+):\/\/(.+)$/);
1062
- if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
1063
- return { domain: match[1], path: match[2] };
1805
+ // src/app/remember.ts
1806
+ async function rememberMemory(db, input) {
1807
+ return syncOne(db, {
1808
+ content: input.content,
1809
+ type: input.type,
1810
+ priority: input.priority,
1811
+ emotion_val: input.emotion_val,
1812
+ uri: input.uri,
1813
+ source: input.source,
1814
+ agent_id: input.agent_id,
1815
+ provider: input.provider,
1816
+ conservative: input.conservative
1817
+ });
1064
1818
  }
1065
- function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
1066
- const { domain } = parseUri(uri);
1067
- const domains = validDomains ?? DEFAULT_DOMAINS;
1068
- if (!domains.has(domain)) {
1069
- throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
1819
+
1820
+ // src/app/recall.ts
1821
+ async function recallMemory(db, input) {
1822
+ return recallMemories(db, input.query, {
1823
+ agent_id: input.agent_id,
1824
+ limit: input.limit,
1825
+ min_vitality: input.min_vitality,
1826
+ lexicalLimit: input.lexicalLimit,
1827
+ vectorLimit: input.vectorLimit,
1828
+ provider: input.provider,
1829
+ recordAccess: input.recordAccess
1830
+ });
1831
+ }
1832
+
1833
+ // src/app/feedback.ts
1834
+ function clamp012(value) {
1835
+ if (!Number.isFinite(value)) return 0;
1836
+ return Math.max(0, Math.min(1, value));
1837
+ }
1838
+ function recordFeedbackEvent(db, input) {
1839
+ const id = newId();
1840
+ const created_at = now();
1841
+ const agentId = input.agent_id ?? "default";
1842
+ const useful = input.useful ? 1 : 0;
1843
+ const value = input.useful ? 1 : 0;
1844
+ const eventType = `${input.source}:${input.useful ? "useful" : "not_useful"}`;
1845
+ const exists = db.prepare("SELECT id FROM memories WHERE id = ?").get(input.memory_id);
1846
+ if (!exists) {
1847
+ throw new Error(`Memory not found: ${input.memory_id}`);
1070
1848
  }
1071
- const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
1072
- if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
1073
- if (agent_id && agent_id !== memoryAgent) {
1074
- throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
1849
+ try {
1850
+ db.prepare(
1851
+ `INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
1852
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1853
+ ).run(id, input.memory_id, input.source, useful, agentId, eventType, value, created_at);
1854
+ } catch {
1855
+ db.prepare(
1856
+ `INSERT INTO feedback_events (id, memory_id, event_type, value, created_at)
1857
+ VALUES (?, ?, ?, ?, ?)`
1858
+ ).run(id, input.memory_id, eventType, value, created_at);
1075
1859
  }
1076
- const agentId = agent_id ?? memoryAgent;
1077
- const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
1078
- if (existing) {
1079
- throw new Error(`URI already exists: ${uri}`);
1860
+ return {
1861
+ id,
1862
+ memory_id: input.memory_id,
1863
+ source: input.source,
1864
+ useful: input.useful,
1865
+ agent_id: agentId,
1866
+ created_at,
1867
+ value
1868
+ };
1869
+ }
1870
+ function getFeedbackSummary(db, memoryId, agentId) {
1871
+ try {
1872
+ const row = db.prepare(
1873
+ `SELECT COUNT(*) as total,
1874
+ COALESCE(SUM(CASE WHEN useful = 1 THEN 1 ELSE 0 END), 0) as useful,
1875
+ COALESCE(SUM(CASE WHEN useful = 0 THEN 1 ELSE 0 END), 0) as not_useful
1876
+ FROM feedback_events
1877
+ WHERE memory_id = ?
1878
+ AND (? IS NULL OR agent_id = ?)`
1879
+ ).get(memoryId, agentId ?? null, agentId ?? null);
1880
+ if (!row || row.total === 0) {
1881
+ return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
1882
+ }
1883
+ return {
1884
+ total: row.total,
1885
+ useful: row.useful,
1886
+ not_useful: row.not_useful,
1887
+ score: clamp012(row.useful / row.total)
1888
+ };
1889
+ } catch {
1890
+ const row = db.prepare(
1891
+ `SELECT COUNT(*) as total,
1892
+ COALESCE(SUM(CASE WHEN value >= 0.5 THEN 1 ELSE 0 END), 0) as useful,
1893
+ COALESCE(SUM(CASE WHEN value < 0.5 THEN 1 ELSE 0 END), 0) as not_useful,
1894
+ COALESCE(AVG(value), 0.5) as avg_value
1895
+ FROM feedback_events
1896
+ WHERE memory_id = ?`
1897
+ ).get(memoryId);
1898
+ if (!row || row.total === 0) {
1899
+ return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
1900
+ }
1901
+ return {
1902
+ total: row.total,
1903
+ useful: row.useful,
1904
+ not_useful: row.not_useful,
1905
+ score: clamp012(row.avg_value)
1906
+ };
1080
1907
  }
1081
- const id = newId();
1082
- db.prepare(
1083
- "INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
1084
- ).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
1085
- return getPath(db, id);
1086
1908
  }
1087
- function getPath(db, id) {
1088
- return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
1909
+
1910
+ // src/app/surface.ts
1911
+ var INTENT_PRIORS = {
1912
+ factual: {
1913
+ identity: 0.25,
1914
+ emotion: 0.15,
1915
+ knowledge: 1,
1916
+ event: 0.8
1917
+ },
1918
+ preference: {
1919
+ identity: 1,
1920
+ emotion: 0.85,
1921
+ knowledge: 0.55,
1922
+ event: 0.25
1923
+ },
1924
+ temporal: {
1925
+ identity: 0.15,
1926
+ emotion: 0.35,
1927
+ knowledge: 0.5,
1928
+ event: 1
1929
+ },
1930
+ planning: {
1931
+ identity: 0.65,
1932
+ emotion: 0.2,
1933
+ knowledge: 1,
1934
+ event: 0.6
1935
+ },
1936
+ design: {
1937
+ identity: 0.8,
1938
+ emotion: 0.35,
1939
+ knowledge: 1,
1940
+ event: 0.25
1941
+ }
1942
+ };
1943
+ var DESIGN_HINT_RE = /\b(ui|ux|design|style|component|layout|brand|palette|theme)\b|风格|界面|设计|配色|低饱和|玻璃拟态|渐变/i;
1944
+ var PLANNING_HINT_RE = /\b(plan|planning|todo|next|ship|build|implement|roadmap|task|milestone)\b|计划|下一步|待办|实现|重构/i;
1945
+ var FACTUAL_HINT_RE = /\b(what|fact|constraint|rule|docs|document|api|status)\b|规则|约束|文档|接口|事实/i;
1946
+ var TEMPORAL_HINT_RE = /\b(today|yesterday|tomorrow|recent|before|after|when|timeline)\b|今天|昨天|明天|最近|时间线|何时/i;
1947
+ var PREFERENCE_HINT_RE = /\b(prefer|preference|like|dislike|avoid|favorite)\b|喜欢|偏好|不喜欢|避免|讨厌/i;
1948
+ function clamp013(value) {
1949
+ if (!Number.isFinite(value)) return 0;
1950
+ return Math.max(0, Math.min(1, value));
1951
+ }
1952
+ function uniqueTokenSet2(values) {
1953
+ return new Set(
1954
+ values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
1955
+ );
1089
1956
  }
1090
- function getPathByUri(db, uri, agent_id = "default") {
1091
- return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
1957
+ function overlapScore2(left, right) {
1958
+ if (left.size === 0 || right.size === 0) return 0;
1959
+ let shared = 0;
1960
+ for (const token of left) {
1961
+ if (right.has(token)) shared += 1;
1962
+ }
1963
+ return clamp013(shared / Math.max(left.size, right.size));
1092
1964
  }
1093
-
1094
- // src/sleep/boot.ts
1095
- function boot(db, opts) {
1096
- const agentId = opts?.agent_id ?? "default";
1097
- const corePaths = opts?.corePaths ?? [
1098
- "core://agent",
1099
- "core://user",
1100
- "core://agent/identity",
1101
- "core://user/identity"
1102
- ];
1103
- const memories = /* @__PURE__ */ new Map();
1104
- const identities = listMemories(db, { agent_id: agentId, priority: 0 });
1105
- for (const mem of identities) {
1106
- memories.set(mem.id, mem);
1107
- recordAccess(db, mem.id, 1.1);
1965
+ function rankScore(rank, window) {
1966
+ if (!rank) return 0;
1967
+ return clamp013(1 - (rank - 1) / Math.max(window, 1));
1968
+ }
1969
+ function topicLabel(...parts) {
1970
+ const token = parts.flatMap((part) => tokenize(part ?? "")).find((value) => value.trim().length > 1);
1971
+ const label = (token ?? "context").replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 32);
1972
+ return label || "context";
1973
+ }
1974
+ function intentKeywordBoost(memory, intent) {
1975
+ const content = memory.content;
1976
+ switch (intent) {
1977
+ case "design":
1978
+ return DESIGN_HINT_RE.test(content) ? 1 : 0.65;
1979
+ case "planning":
1980
+ return PLANNING_HINT_RE.test(content) ? 1 : 0.7;
1981
+ case "factual":
1982
+ return FACTUAL_HINT_RE.test(content) ? 1 : 0.75;
1983
+ case "temporal":
1984
+ return TEMPORAL_HINT_RE.test(content) ? 1 : 0.75;
1985
+ case "preference":
1986
+ return PREFERENCE_HINT_RE.test(content) ? 1 : 0.8;
1108
1987
  }
1109
- const bootPaths = [];
1110
- for (const uri of corePaths) {
1111
- const path = getPathByUri(db, uri, agentId);
1112
- if (path) {
1113
- bootPaths.push(uri);
1114
- if (!memories.has(path.memory_id)) {
1115
- const mem = getMemory(db, path.memory_id);
1116
- if (mem) {
1117
- memories.set(mem.id, mem);
1118
- recordAccess(db, mem.id, 1.1);
1119
- }
1988
+ }
1989
+ function intentMatch(memory, intent) {
1990
+ if (!intent) return 0;
1991
+ const prior = INTENT_PRIORS[intent][memory.type] ?? 0;
1992
+ return clamp013(prior * intentKeywordBoost(memory, intent));
1993
+ }
1994
+ function buildReasonCodes(input) {
1995
+ const reasons = /* @__PURE__ */ new Set();
1996
+ reasons.add(`type:${input.memory.type}`);
1997
+ if (input.semanticScore > 0.2) {
1998
+ reasons.add(`semantic:${topicLabel(input.query, input.task)}`);
1999
+ }
2000
+ if (input.lexicalScore > 0.2 && input.query) {
2001
+ reasons.add(`lexical:${topicLabel(input.query)}`);
2002
+ }
2003
+ if (input.taskMatch > 0.2) {
2004
+ reasons.add(`task:${topicLabel(input.task, input.intent)}`);
2005
+ }
2006
+ if (input.intent) {
2007
+ reasons.add(`intent:${input.intent}`);
2008
+ }
2009
+ if (input.feedbackScore >= 0.67) {
2010
+ reasons.add("feedback:reinforced");
2011
+ } else if (input.feedbackScore <= 0.33) {
2012
+ reasons.add("feedback:negative");
2013
+ }
2014
+ return [...reasons];
2015
+ }
2016
+ function collectBranch(signals, rows, key, similarity) {
2017
+ for (const row of rows) {
2018
+ const existing = signals.get(row.memory.id) ?? { memory: row.memory };
2019
+ const currentRank = existing[key];
2020
+ if (currentRank === void 0 || row.rank < currentRank) {
2021
+ existing[key] = row.rank;
2022
+ }
2023
+ if (similarity) {
2024
+ const currentSimilarity = similarity.get(row.memory.id);
2025
+ if (currentSimilarity !== void 0) {
2026
+ existing.semanticSimilarity = Math.max(existing.semanticSimilarity ?? 0, currentSimilarity);
1120
2027
  }
1121
2028
  }
2029
+ signals.set(row.memory.id, existing);
1122
2030
  }
1123
- const bootEntry = getPathByUri(db, "system://boot", agentId);
1124
- if (bootEntry) {
1125
- const bootMem = getMemory(db, bootEntry.memory_id);
1126
- if (bootMem) {
1127
- const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
1128
- for (const uri of additionalUris) {
1129
- const path = getPathByUri(db, uri, agentId);
1130
- if (path && !memories.has(path.memory_id)) {
1131
- const mem = getMemory(db, path.memory_id);
1132
- if (mem) {
1133
- memories.set(mem.id, mem);
1134
- bootPaths.push(uri);
1135
- }
1136
- }
2031
+ }
2032
+ async function surfaceMemories(db, input) {
2033
+ const agentId = input.agent_id ?? "default";
2034
+ const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
2035
+ const lexicalWindow = Math.max(24, limit * 6);
2036
+ const minVitality = input.min_vitality ?? 0.05;
2037
+ const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
2038
+ const signals = /* @__PURE__ */ new Map();
2039
+ const trimmedQuery = input.query?.trim();
2040
+ const trimmedTask = input.task?.trim();
2041
+ const recentTurns = (input.recent_turns ?? []).map((turn) => turn.trim()).filter(Boolean).slice(-4);
2042
+ const queryTokens = uniqueTokenSet2([trimmedQuery, ...recentTurns]);
2043
+ const taskTokens = uniqueTokenSet2([trimmedTask]);
2044
+ if (trimmedQuery) {
2045
+ collectBranch(
2046
+ signals,
2047
+ searchBM25(db, trimmedQuery, {
2048
+ agent_id: agentId,
2049
+ limit: lexicalWindow,
2050
+ min_vitality: minVitality
2051
+ }),
2052
+ "queryRank"
2053
+ );
2054
+ }
2055
+ if (trimmedTask) {
2056
+ collectBranch(
2057
+ signals,
2058
+ searchBM25(db, trimmedTask, {
2059
+ agent_id: agentId,
2060
+ limit: lexicalWindow,
2061
+ min_vitality: minVitality
2062
+ }),
2063
+ "taskRank"
2064
+ );
2065
+ }
2066
+ if (recentTurns.length > 0) {
2067
+ collectBranch(
2068
+ signals,
2069
+ searchBM25(db, recentTurns.join(" "), {
2070
+ agent_id: agentId,
2071
+ limit: lexicalWindow,
2072
+ min_vitality: minVitality
2073
+ }),
2074
+ "recentRank"
2075
+ );
2076
+ }
2077
+ const semanticQuery = [trimmedQuery, trimmedTask, ...recentTurns].filter(Boolean).join("\n").trim();
2078
+ if (provider && semanticQuery) {
2079
+ try {
2080
+ const [queryVector] = await provider.embed([semanticQuery]);
2081
+ if (queryVector) {
2082
+ const vectorRows = searchByVector(db, queryVector, {
2083
+ providerId: provider.id,
2084
+ agent_id: agentId,
2085
+ limit: lexicalWindow,
2086
+ min_vitality: minVitality
2087
+ });
2088
+ const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
2089
+ collectBranch(signals, vectorRows, "semanticRank", similarity);
1137
2090
  }
2091
+ } catch {
1138
2092
  }
1139
2093
  }
2094
+ const fallbackMemories = listMemories(db, {
2095
+ agent_id: agentId,
2096
+ min_vitality: minVitality,
2097
+ limit: Math.max(48, lexicalWindow)
2098
+ });
2099
+ for (const memory of fallbackMemories) {
2100
+ if (!signals.has(memory.id)) {
2101
+ signals.set(memory.id, { memory });
2102
+ }
2103
+ }
2104
+ const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).map((memory) => {
2105
+ const signal = signals.get(memory.id) ?? { memory };
2106
+ const memoryTokens = new Set(tokenize(memory.content));
2107
+ const lexicalOverlap = overlapScore2(memoryTokens, queryTokens);
2108
+ const taskOverlap = overlapScore2(memoryTokens, taskTokens);
2109
+ const lexicalScore = clamp013(
2110
+ 0.45 * rankScore(signal.queryRank, lexicalWindow) + 0.15 * rankScore(signal.recentRank, lexicalWindow) + 0.15 * rankScore(signal.taskRank, lexicalWindow) + 0.25 * lexicalOverlap
2111
+ );
2112
+ const semanticScore = signal.semanticSimilarity !== void 0 ? clamp013(Math.max(signal.semanticSimilarity, lexicalOverlap * 0.7)) : trimmedQuery || recentTurns.length > 0 ? clamp013(lexicalOverlap * 0.7) : 0;
2113
+ const intentScore = intentMatch(memory, input.intent);
2114
+ const taskMatch = trimmedTask ? clamp013(0.7 * taskOverlap + 0.3 * intentScore) : intentScore;
2115
+ const priorityScore = priorityPrior(memory.priority);
2116
+ const feedbackSummary = getFeedbackSummary(db, memory.id, agentId);
2117
+ const feedbackScore = feedbackSummary.score;
2118
+ const score = clamp013(
2119
+ 0.35 * semanticScore + 0.2 * lexicalScore + 0.15 * taskMatch + 0.1 * memory.vitality + 0.1 * priorityScore + 0.1 * feedbackScore
2120
+ );
2121
+ return {
2122
+ memory,
2123
+ score,
2124
+ semantic_score: semanticScore,
2125
+ lexical_score: lexicalScore,
2126
+ task_match: taskMatch,
2127
+ vitality: memory.vitality,
2128
+ priority_prior: priorityScore,
2129
+ feedback_score: feedbackScore,
2130
+ feedback_summary: feedbackSummary,
2131
+ reason_codes: buildReasonCodes({
2132
+ memory,
2133
+ query: semanticQuery || trimmedQuery,
2134
+ task: trimmedTask,
2135
+ intent: input.intent,
2136
+ semanticScore,
2137
+ lexicalScore,
2138
+ taskMatch,
2139
+ feedbackScore
2140
+ }),
2141
+ lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
2142
+ semantic_rank: signal.semanticRank,
2143
+ semantic_similarity: signal.semanticSimilarity
2144
+ };
2145
+ }).sort((left, right) => {
2146
+ if (right.score !== left.score) return right.score - left.score;
2147
+ if (right.semantic_score !== left.semantic_score) return right.semantic_score - left.semantic_score;
2148
+ if (right.lexical_score !== left.lexical_score) return right.lexical_score - left.lexical_score;
2149
+ if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
2150
+ return right.memory.updated_at.localeCompare(left.memory.updated_at);
2151
+ }).slice(0, limit);
1140
2152
  return {
1141
- identityMemories: [...memories.values()],
1142
- bootPaths
2153
+ count: results.length,
2154
+ query: trimmedQuery,
2155
+ task: trimmedTask,
2156
+ intent: input.intent,
2157
+ results
1143
2158
  };
1144
2159
  }
1145
2160
 
@@ -1203,74 +2218,100 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
1203
2218
  ).all(...agentId ? [threshold, agentId] : [threshold]);
1204
2219
  }
1205
2220
 
1206
- // src/core/snapshot.ts
1207
- function createSnapshot(db, memoryId, action, changedBy) {
1208
- const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
1209
- if (!memory) throw new Error(`Memory not found: ${memoryId}`);
1210
- const id = newId();
1211
- db.prepare(
1212
- `INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
1213
- VALUES (?, ?, ?, ?, ?, ?)`
1214
- ).run(id, memoryId, memory.content, changedBy ?? null, action, now());
1215
- return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
1216
- }
1217
-
1218
2221
  // src/sleep/tidy.ts
1219
2222
  function runTidy(db, opts) {
1220
2223
  const threshold = opts?.vitalityThreshold ?? 0.05;
1221
- const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
1222
2224
  const agentId = opts?.agent_id;
1223
2225
  let archived = 0;
1224
- let orphansCleaned = 0;
1225
- let snapshotsPruned = 0;
1226
2226
  const transaction = db.transaction(() => {
1227
2227
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
1228
2228
  for (const mem of decayed) {
1229
- try {
1230
- createSnapshot(db, mem.id, "delete", "tidy");
1231
- } catch {
1232
- }
1233
2229
  deleteMemory(db, mem.id);
1234
- archived++;
2230
+ archived += 1;
1235
2231
  }
1236
- const orphans = agentId ? db.prepare(
1237
- `DELETE FROM paths
1238
- WHERE agent_id = ?
1239
- AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
1240
- ).run(agentId, agentId) : db.prepare(
1241
- "DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
1242
- ).run();
1243
- orphansCleaned = orphans.changes;
1244
- const memoriesWithSnapshots = agentId ? db.prepare(
1245
- `SELECT s.memory_id, COUNT(*) as cnt
1246
- FROM snapshots s
1247
- JOIN memories m ON m.id = s.memory_id
1248
- WHERE m.agent_id = ?
1249
- GROUP BY s.memory_id HAVING cnt > ?`
1250
- ).all(agentId, maxSnapshots) : db.prepare(
1251
- `SELECT memory_id, COUNT(*) as cnt FROM snapshots
1252
- GROUP BY memory_id HAVING cnt > ?`
1253
- ).all(maxSnapshots);
1254
- for (const { memory_id } of memoriesWithSnapshots) {
1255
- const pruned = db.prepare(
1256
- `DELETE FROM snapshots WHERE id NOT IN (
1257
- SELECT id FROM snapshots WHERE memory_id = ?
1258
- ORDER BY created_at DESC LIMIT ?
1259
- ) AND memory_id = ?`
1260
- ).run(memory_id, maxSnapshots, memory_id);
1261
- snapshotsPruned += pruned.changes;
2232
+ });
2233
+ transaction();
2234
+ return { archived, orphansCleaned: 0 };
2235
+ }
2236
+
2237
+ // src/sleep/govern.ts
2238
+ function clamp014(value) {
2239
+ if (!Number.isFinite(value)) return 0;
2240
+ return Math.max(0, Math.min(1, value));
2241
+ }
2242
+ function overlapScore3(left, right) {
2243
+ if (left.size === 0 || right.size === 0) return 0;
2244
+ let shared = 0;
2245
+ for (const token of left) {
2246
+ if (right.has(token)) shared += 1;
2247
+ }
2248
+ return shared / Math.max(left.size, right.size);
2249
+ }
2250
+ function feedbackPenalty(db, memoryId) {
2251
+ try {
2252
+ const row = db.prepare(
2253
+ `SELECT COUNT(*) as count, COALESCE(AVG(value), 0) as avgValue
2254
+ FROM feedback_events
2255
+ WHERE memory_id = ?`
2256
+ ).get(memoryId);
2257
+ if (!row || row.count === 0) return 1;
2258
+ return clamp014(1 - row.avgValue);
2259
+ } catch {
2260
+ return 1;
2261
+ }
2262
+ }
2263
+ function ageScore(memory, referenceMs = Date.now()) {
2264
+ const createdAt = new Date(memory.created_at).getTime();
2265
+ if (Number.isNaN(createdAt)) return 0;
2266
+ const ageDays = Math.max(0, (referenceMs - createdAt) / (1e3 * 60 * 60 * 24));
2267
+ return clamp014(ageDays / 180);
2268
+ }
2269
+ function computeEvictionScore(input) {
2270
+ return clamp014(
2271
+ 0.4 * (1 - clamp014(input.vitality)) + 0.2 * clamp014(input.redundancy_score) + 0.2 * clamp014(input.age_score) + 0.1 * clamp014(input.low_feedback_penalty) + 0.1 * clamp014(input.low_priority_penalty)
2272
+ );
2273
+ }
2274
+ function rankEvictionCandidates(db, opts) {
2275
+ const agentId = opts?.agent_id;
2276
+ const rows = db.prepare(
2277
+ agentId ? `SELECT * FROM memories WHERE agent_id = ? AND priority > 0 AND TRIM(content) != ''` : `SELECT * FROM memories WHERE priority > 0 AND TRIM(content) != ''`
2278
+ ).all(...agentId ? [agentId] : []);
2279
+ const tokenSets = new Map(rows.map((memory) => [memory.id, new Set(tokenize(memory.content))]));
2280
+ return rows.map((memory) => {
2281
+ const ownTokens = tokenSets.get(memory.id) ?? /* @__PURE__ */ new Set();
2282
+ const redundancy = rows.filter((candidate2) => candidate2.id !== memory.id && candidate2.type === memory.type).reduce((maxOverlap, candidate2) => {
2283
+ const candidateTokens = tokenSets.get(candidate2.id) ?? /* @__PURE__ */ new Set();
2284
+ return Math.max(maxOverlap, overlapScore3(ownTokens, candidateTokens));
2285
+ }, 0);
2286
+ const candidate = {
2287
+ memory,
2288
+ redundancy_score: redundancy,
2289
+ age_score: ageScore(memory),
2290
+ low_feedback_penalty: feedbackPenalty(db, memory.id),
2291
+ low_priority_penalty: clamp014(memory.priority / 3),
2292
+ eviction_score: 0
2293
+ };
2294
+ candidate.eviction_score = computeEvictionScore({
2295
+ vitality: memory.vitality,
2296
+ redundancy_score: candidate.redundancy_score,
2297
+ age_score: candidate.age_score,
2298
+ low_feedback_penalty: candidate.low_feedback_penalty,
2299
+ low_priority_penalty: candidate.low_priority_penalty
2300
+ });
2301
+ return candidate;
2302
+ }).sort((left, right) => {
2303
+ if (right.eviction_score !== left.eviction_score) {
2304
+ return right.eviction_score - left.eviction_score;
1262
2305
  }
2306
+ return left.memory.priority - right.memory.priority;
1263
2307
  });
1264
- transaction();
1265
- return { archived, orphansCleaned, snapshotsPruned };
1266
2308
  }
1267
-
1268
- // src/sleep/govern.ts
1269
2309
  function runGovern(db, opts) {
1270
2310
  const agentId = opts?.agent_id;
2311
+ const maxMemories = opts?.maxMemories ?? 200;
1271
2312
  let orphanPaths = 0;
1272
- let orphanLinks = 0;
1273
2313
  let emptyMemories = 0;
2314
+ let evicted = 0;
1274
2315
  const transaction = db.transaction(() => {
1275
2316
  const pathResult = agentId ? db.prepare(
1276
2317
  `DELETE FROM paths
@@ -1278,170 +2319,749 @@ function runGovern(db, opts) {
1278
2319
  AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
1279
2320
  ).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
1280
2321
  orphanPaths = pathResult.changes;
1281
- const linkResult = agentId ? db.prepare(
1282
- `DELETE FROM links WHERE
1283
- agent_id = ? AND (
1284
- source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
1285
- target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
1286
- )`
1287
- ).run(agentId, agentId, agentId) : db.prepare(
1288
- `DELETE FROM links WHERE
1289
- source_id NOT IN (SELECT id FROM memories) OR
1290
- target_id NOT IN (SELECT id FROM memories)`
1291
- ).run();
1292
- orphanLinks = linkResult.changes;
1293
2322
  const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
1294
2323
  emptyMemories = emptyResult.changes;
2324
+ const total = db.prepare(agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ?" : "SELECT COUNT(*) as c FROM memories").get(...agentId ? [agentId] : []).c;
2325
+ const excess = Math.max(0, total - maxMemories);
2326
+ if (excess <= 0) return;
2327
+ const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
2328
+ for (const candidate of candidates) {
2329
+ deleteMemory(db, candidate.memory.id);
2330
+ evicted += 1;
2331
+ }
1295
2332
  });
1296
2333
  transaction();
1297
- return { orphanPaths, orphanLinks, emptyMemories };
2334
+ return { orphanPaths, emptyMemories, evicted };
1298
2335
  }
1299
2336
 
1300
- // src/core/guard.ts
1301
- function guard(db, input) {
1302
- const hash = contentHash(input.content);
1303
- const agentId = input.agent_id ?? "default";
1304
- const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
1305
- if (exactMatch) {
1306
- return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
2337
+ // src/sleep/jobs.ts
2338
+ function parseCheckpoint(raw) {
2339
+ if (!raw) return null;
2340
+ try {
2341
+ return JSON.parse(raw);
2342
+ } catch {
2343
+ return null;
1307
2344
  }
1308
- if (input.uri) {
1309
- const existingPath = getPathByUri(db, input.uri, agentId);
1310
- if (existingPath) {
1311
- return {
1312
- action: "update",
1313
- reason: `URI ${input.uri} already exists, updating`,
1314
- existingId: existingPath.memory_id
1315
- };
2345
+ }
2346
+ function serializeCheckpoint(checkpoint) {
2347
+ if (!checkpoint) return null;
2348
+ return JSON.stringify(checkpoint);
2349
+ }
2350
+ function toJob(row) {
2351
+ if (!row) return null;
2352
+ return {
2353
+ ...row,
2354
+ checkpoint: parseCheckpoint(row.checkpoint)
2355
+ };
2356
+ }
2357
+ function createInitialCheckpoint(phase) {
2358
+ return {
2359
+ requestedPhase: phase,
2360
+ nextPhase: phase === "all" ? "decay" : phase,
2361
+ completedPhases: [],
2362
+ phaseResults: {}
2363
+ };
2364
+ }
2365
+ function createMaintenanceJob(db, phase, checkpoint = createInitialCheckpoint(phase)) {
2366
+ const jobId = newId();
2367
+ const startedAt = now();
2368
+ db.prepare(
2369
+ `INSERT INTO maintenance_jobs (job_id, phase, status, checkpoint, error, started_at, finished_at)
2370
+ VALUES (?, ?, 'running', ?, NULL, ?, NULL)`
2371
+ ).run(jobId, phase, serializeCheckpoint(checkpoint), startedAt);
2372
+ return getMaintenanceJob(db, jobId);
2373
+ }
2374
+ function getMaintenanceJob(db, jobId) {
2375
+ const row = db.prepare("SELECT * FROM maintenance_jobs WHERE job_id = ?").get(jobId);
2376
+ return toJob(row);
2377
+ }
2378
+ function findResumableMaintenanceJob(db, phase) {
2379
+ const row = db.prepare(
2380
+ `SELECT *
2381
+ FROM maintenance_jobs
2382
+ WHERE phase = ?
2383
+ AND status IN ('running', 'failed')
2384
+ ORDER BY started_at DESC
2385
+ LIMIT 1`
2386
+ ).get(phase);
2387
+ return toJob(row);
2388
+ }
2389
+ function updateMaintenanceCheckpoint(db, jobId, checkpoint) {
2390
+ db.prepare(
2391
+ `UPDATE maintenance_jobs
2392
+ SET checkpoint = ?,
2393
+ error = NULL,
2394
+ finished_at = NULL,
2395
+ status = 'running'
2396
+ WHERE job_id = ?`
2397
+ ).run(serializeCheckpoint(checkpoint), jobId);
2398
+ return getMaintenanceJob(db, jobId);
2399
+ }
2400
+ function failMaintenanceJob(db, jobId, error, checkpoint) {
2401
+ db.prepare(
2402
+ `UPDATE maintenance_jobs
2403
+ SET status = 'failed',
2404
+ checkpoint = COALESCE(?, checkpoint),
2405
+ error = ?,
2406
+ finished_at = ?
2407
+ WHERE job_id = ?`
2408
+ ).run(serializeCheckpoint(checkpoint), error, now(), jobId);
2409
+ return getMaintenanceJob(db, jobId);
2410
+ }
2411
+ function completeMaintenanceJob(db, jobId, checkpoint) {
2412
+ db.prepare(
2413
+ `UPDATE maintenance_jobs
2414
+ SET status = 'completed',
2415
+ checkpoint = COALESCE(?, checkpoint),
2416
+ error = NULL,
2417
+ finished_at = ?
2418
+ WHERE job_id = ?`
2419
+ ).run(serializeCheckpoint(checkpoint), now(), jobId);
2420
+ return getMaintenanceJob(db, jobId);
2421
+ }
2422
+
2423
+ // src/sleep/orchestrator.ts
2424
+ var DEFAULT_RUNNERS = {
2425
+ decay: (db, opts) => runDecay(db, opts),
2426
+ tidy: (db, opts) => runTidy(db, opts),
2427
+ govern: (db, opts) => runGovern(db, opts)
2428
+ };
2429
+ var PHASE_SEQUENCE = ["decay", "tidy", "govern"];
2430
+ function getSummaryStats(db, agentId) {
2431
+ const row = agentId ? db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories WHERE agent_id = ?").get(agentId) : db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories").get();
2432
+ return {
2433
+ total: row.total,
2434
+ avgVitality: row.avg
2435
+ };
2436
+ }
2437
+ function getPhaseSequence(phase) {
2438
+ return phase === "all" ? [...PHASE_SEQUENCE] : [phase];
2439
+ }
2440
+ function resolveJob(db, opts) {
2441
+ if (opts.jobId) {
2442
+ const job = getMaintenanceJob(db, opts.jobId);
2443
+ if (!job) {
2444
+ throw new Error(`Maintenance job not found: ${opts.jobId}`);
2445
+ }
2446
+ if (job.phase !== opts.phase) {
2447
+ throw new Error(`Maintenance job ${opts.jobId} phase mismatch: expected ${opts.phase}, got ${job.phase}`);
1316
2448
  }
2449
+ return { job, resumed: true };
1317
2450
  }
1318
- const ftsTokens = tokenize(input.content.slice(0, 200));
1319
- const ftsQuery = ftsTokens.length > 0 ? ftsTokens.slice(0, 8).map((w) => `"${w}"`).join(" OR ") : null;
1320
- if (ftsQuery) {
1321
- try {
1322
- const similar = db.prepare(
1323
- `SELECT m.id, m.content, m.type, rank
1324
- FROM memories_fts f
1325
- JOIN memories m ON m.id = f.id
1326
- WHERE memories_fts MATCH ? AND m.agent_id = ?
1327
- ORDER BY rank
1328
- LIMIT 3`
1329
- ).all(ftsQuery, agentId);
1330
- if (similar.length > 0) {
1331
- const topRank = Math.abs(similar[0].rank);
1332
- const tokenCount = ftsTokens.length;
1333
- const dynamicThreshold = tokenCount * 1.5;
1334
- if (topRank > dynamicThreshold) {
1335
- const existing = similar[0];
1336
- if (existing.type === input.type) {
1337
- const merged = `${existing.content}
1338
-
1339
- [Updated] ${input.content}`;
1340
- return {
1341
- action: "merge",
1342
- reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
1343
- existingId: existing.id,
1344
- mergedContent: merged
1345
- };
1346
- }
1347
- }
1348
- }
1349
- } catch {
2451
+ if (opts.resume !== false) {
2452
+ const resumable = findResumableMaintenanceJob(db, opts.phase);
2453
+ if (resumable) {
2454
+ return { job: resumable, resumed: true };
1350
2455
  }
1351
2456
  }
1352
- const gateResult = fourCriterionGate(input);
1353
- if (!gateResult.pass) {
1354
- return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
2457
+ return {
2458
+ job: createMaintenanceJob(db, opts.phase),
2459
+ resumed: false
2460
+ };
2461
+ }
2462
+ function nextPhase(current, requested) {
2463
+ if (requested !== "all") return null;
2464
+ const index = PHASE_SEQUENCE.indexOf(current);
2465
+ return PHASE_SEQUENCE[index + 1] ?? null;
2466
+ }
2467
+ async function runReflectOrchestrator(db, opts) {
2468
+ const runners = {
2469
+ ...DEFAULT_RUNNERS,
2470
+ ...opts.runners
2471
+ };
2472
+ const before = getSummaryStats(db, opts.agent_id);
2473
+ const { job: baseJob, resumed } = resolveJob(db, opts);
2474
+ let checkpoint = baseJob.checkpoint ?? createInitialCheckpoint(opts.phase);
2475
+ const jobId = baseJob.job_id;
2476
+ const orderedPhases = getPhaseSequence(opts.phase);
2477
+ const startPhase = checkpoint.nextPhase ?? orderedPhases[orderedPhases.length - 1] ?? "decay";
2478
+ const startIndex = Math.max(0, orderedPhases.indexOf(startPhase));
2479
+ const phasesToRun = checkpoint.nextPhase === null ? [] : orderedPhases.slice(startIndex);
2480
+ const totalPhases = Math.max(orderedPhases.length, 1);
2481
+ opts.onProgress?.({
2482
+ status: "started",
2483
+ phase: opts.phase,
2484
+ progress: checkpoint.completedPhases.length / totalPhases,
2485
+ jobId,
2486
+ detail: {
2487
+ resumed,
2488
+ nextPhase: checkpoint.nextPhase
2489
+ }
2490
+ });
2491
+ try {
2492
+ for (const phase of phasesToRun) {
2493
+ const result = await Promise.resolve(runners[phase](db, { agent_id: opts.agent_id }));
2494
+ checkpoint = {
2495
+ ...checkpoint,
2496
+ completedPhases: [.../* @__PURE__ */ new Set([...checkpoint.completedPhases, phase])],
2497
+ phaseResults: {
2498
+ ...checkpoint.phaseResults,
2499
+ [phase]: result
2500
+ },
2501
+ nextPhase: nextPhase(phase, opts.phase)
2502
+ };
2503
+ updateMaintenanceCheckpoint(db, jobId, checkpoint);
2504
+ opts.onProgress?.({
2505
+ status: "phase-completed",
2506
+ phase,
2507
+ progress: checkpoint.completedPhases.length / totalPhases,
2508
+ jobId,
2509
+ detail: result
2510
+ });
2511
+ }
2512
+ } catch (error) {
2513
+ const message = error instanceof Error ? error.message : String(error);
2514
+ const failed = failMaintenanceJob(db, jobId, message, checkpoint) ?? baseJob;
2515
+ opts.onProgress?.({
2516
+ status: "failed",
2517
+ phase: checkpoint.nextPhase ?? opts.phase,
2518
+ progress: checkpoint.completedPhases.length / totalPhases,
2519
+ jobId,
2520
+ detail: { error: message }
2521
+ });
2522
+ throw Object.assign(new Error(message), { job: failed, checkpoint });
1355
2523
  }
1356
- return { action: "add", reason: "Passed all guard checks" };
2524
+ const completedCheckpoint = {
2525
+ ...checkpoint,
2526
+ nextPhase: null
2527
+ };
2528
+ const job = completeMaintenanceJob(db, jobId, completedCheckpoint) ?? baseJob;
2529
+ const after = getSummaryStats(db, opts.agent_id);
2530
+ opts.onProgress?.({
2531
+ status: "completed",
2532
+ phase: opts.phase,
2533
+ progress: 1,
2534
+ jobId,
2535
+ detail: completedCheckpoint.phaseResults
2536
+ });
2537
+ return {
2538
+ job,
2539
+ jobId,
2540
+ phase: opts.phase,
2541
+ resumed,
2542
+ checkpoint: completedCheckpoint,
2543
+ results: completedCheckpoint.phaseResults,
2544
+ before,
2545
+ after
2546
+ };
1357
2547
  }
1358
- function fourCriterionGate(input) {
1359
- const content = input.content.trim();
1360
- const failed = [];
1361
- const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
1362
- const minLength = priority <= 1 ? 4 : 8;
1363
- const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
1364
- if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
1365
- const tokens = tokenize(content);
1366
- const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
1367
- if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
1368
- const hasCJK = /[\u4e00-\u9fff]/.test(content);
1369
- const hasCapitalized = /[A-Z][a-z]+/.test(content);
1370
- const hasNumbers = /\d+/.test(content);
1371
- const hasURI = /\w+:\/\//.test(content);
1372
- const hasEntityMarkers = /[@#]/.test(content);
1373
- const hasMeaningfulLength = content.length >= 15;
1374
- const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
1375
- const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
1376
- if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
1377
- const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
1378
- const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
1379
- const excessiveRepetition = /(.)\1{9,}/.test(content);
1380
- let coherence = 1;
1381
- if (allCaps) {
1382
- coherence -= 0.5;
2548
+
2549
+ // src/app/reflect.ts
2550
+ async function reflectMemories(db, input) {
2551
+ const options = {
2552
+ phase: input.phase,
2553
+ agent_id: input.agent_id,
2554
+ jobId: input.jobId,
2555
+ resume: input.resume,
2556
+ runners: input.runners,
2557
+ onProgress: input.onProgress
2558
+ };
2559
+ return runReflectOrchestrator(db, options);
2560
+ }
2561
+
2562
+ // src/app/status.ts
2563
+ function getMemoryStatus(db, input) {
2564
+ const agentId = input?.agent_id ?? "default";
2565
+ const stats = countMemories(db, agentId);
2566
+ const lowVitality = db.prepare(
2567
+ "SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?"
2568
+ ).get(agentId);
2569
+ const totalPaths = db.prepare(
2570
+ "SELECT COUNT(*) as c FROM paths WHERE agent_id = ?"
2571
+ ).get(agentId);
2572
+ const feedbackEvents = db.prepare(
2573
+ "SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
2574
+ ).get(agentId);
2575
+ return {
2576
+ ...stats,
2577
+ paths: totalPaths.c,
2578
+ low_vitality: lowVitality.c,
2579
+ feedback_events: feedbackEvents.c,
2580
+ agent_id: agentId
2581
+ };
2582
+ }
2583
+
2584
+ // src/app/reindex.ts
2585
+ async function reindexMemories(db, input) {
2586
+ input?.onProgress?.({ status: "started", stage: "fts", progress: 0 });
2587
+ try {
2588
+ const fts = rebuildBm25Index(db, { agent_id: input?.agent_id });
2589
+ input?.onProgress?.({
2590
+ status: "stage-completed",
2591
+ stage: "fts",
2592
+ progress: 0.5,
2593
+ detail: fts
2594
+ });
2595
+ const embeddings = await reindexEmbeddings(db, {
2596
+ agent_id: input?.agent_id,
2597
+ provider: input?.provider,
2598
+ force: input?.force,
2599
+ batchSize: input?.batchSize
2600
+ });
2601
+ input?.onProgress?.({
2602
+ status: "stage-completed",
2603
+ stage: "embeddings",
2604
+ progress: 0.9,
2605
+ detail: embeddings
2606
+ });
2607
+ const result = { fts, embeddings };
2608
+ input?.onProgress?.({
2609
+ status: "completed",
2610
+ stage: "done",
2611
+ progress: 1,
2612
+ detail: result
2613
+ });
2614
+ return result;
2615
+ } catch (error) {
2616
+ input?.onProgress?.({
2617
+ status: "failed",
2618
+ stage: "done",
2619
+ progress: 1,
2620
+ detail: error instanceof Error ? error.message : String(error)
2621
+ });
2622
+ throw error;
1383
2623
  }
1384
- if (!hasWhitespaceOrPunctuation) {
1385
- coherence -= 0.3;
2624
+ }
2625
+
2626
+ // src/transports/http.ts
2627
+ var VALID_MEMORY_TYPES = /* @__PURE__ */ new Set(["identity", "emotion", "knowledge", "event"]);
2628
+ var VALID_PHASES = /* @__PURE__ */ new Set(["decay", "tidy", "govern", "all"]);
2629
+ var VALID_INTENTS = /* @__PURE__ */ new Set(["factual", "preference", "temporal", "planning", "design"]);
2630
+ function json(value) {
2631
+ return JSON.stringify(value);
2632
+ }
2633
+ function now2() {
2634
+ return (/* @__PURE__ */ new Date()).toISOString();
2635
+ }
2636
+ function sendJson(res, statusCode, payload) {
2637
+ res.writeHead(statusCode, {
2638
+ "content-type": "application/json; charset=utf-8",
2639
+ "cache-control": "no-store"
2640
+ });
2641
+ res.end(json(payload));
2642
+ }
2643
+ function sendError(res, statusCode, error, details) {
2644
+ sendJson(res, statusCode, { error, details });
2645
+ }
2646
+ function openSse(res) {
2647
+ res.writeHead(200, {
2648
+ "content-type": "text/event-stream; charset=utf-8",
2649
+ "cache-control": "no-cache, no-transform",
2650
+ connection: "keep-alive"
2651
+ });
2652
+ res.write(": connected\n\n");
2653
+ }
2654
+ function sendSse(res, event, payload) {
2655
+ if (res.writableEnded) return;
2656
+ res.write(`event: ${event}
2657
+ `);
2658
+ res.write(`data: ${json(payload)}
2659
+
2660
+ `);
2661
+ }
2662
+ async function readJsonBody(req) {
2663
+ const chunks = [];
2664
+ for await (const chunk of req) {
2665
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1386
2666
  }
1387
- if (excessiveRepetition) {
1388
- coherence -= 0.5;
2667
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
2668
+ if (!raw) return {};
2669
+ try {
2670
+ const parsed = JSON.parse(raw);
2671
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2672
+ throw new Error("Request body must be a JSON object");
2673
+ }
2674
+ return parsed;
2675
+ } catch (error) {
2676
+ throw new Error(error instanceof Error ? error.message : "Invalid JSON body");
1389
2677
  }
1390
- coherence = Math.max(0, coherence);
1391
- if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
2678
+ }
2679
+ function asString(value) {
2680
+ return typeof value === "string" ? value : void 0;
2681
+ }
2682
+ function asBoolean(value) {
2683
+ return typeof value === "boolean" ? value : void 0;
2684
+ }
2685
+ function asNumber(value) {
2686
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2687
+ }
2688
+ function asStringArray(value) {
2689
+ if (!Array.isArray(value)) return void 0;
2690
+ const result = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
2691
+ return result.length > 0 ? result : [];
2692
+ }
2693
+ function wantsSse(req, body) {
2694
+ const accept = req.headers.accept ?? "";
2695
+ return accept.includes("text/event-stream") || body.stream === true;
2696
+ }
2697
+ function formatRecallResponse(result) {
1392
2698
  return {
1393
- pass: failed.length === 0,
1394
- scores: { specificity, novelty, relevance, coherence },
1395
- failedCriteria: failed
2699
+ mode: result.mode,
2700
+ provider_id: result.providerId,
2701
+ used_vector_search: result.usedVectorSearch,
2702
+ count: result.results.length,
2703
+ memories: result.results.map((row) => ({
2704
+ id: row.memory.id,
2705
+ content: row.memory.content,
2706
+ type: row.memory.type,
2707
+ priority: row.memory.priority,
2708
+ vitality: row.memory.vitality,
2709
+ score: row.score,
2710
+ bm25_rank: row.bm25_rank,
2711
+ vector_rank: row.vector_rank,
2712
+ bm25_score: row.bm25_score,
2713
+ vector_score: row.vector_score,
2714
+ updated_at: row.memory.updated_at
2715
+ }))
1396
2716
  };
1397
2717
  }
1398
-
1399
- // src/sleep/sync.ts
1400
- function syncOne(db, input) {
1401
- const memInput = {
1402
- content: input.content,
1403
- type: input.type ?? "event",
1404
- priority: input.priority,
1405
- emotion_val: input.emotion_val,
1406
- source: input.source,
1407
- agent_id: input.agent_id,
1408
- uri: input.uri
2718
+ function formatSurfaceResponse(result) {
2719
+ return {
2720
+ count: result.count,
2721
+ query: result.query,
2722
+ task: result.task,
2723
+ intent: result.intent,
2724
+ results: result.results.map((row) => ({
2725
+ id: row.memory.id,
2726
+ content: row.memory.content,
2727
+ type: row.memory.type,
2728
+ priority: row.memory.priority,
2729
+ vitality: row.memory.vitality,
2730
+ score: row.score,
2731
+ semantic_score: row.semantic_score,
2732
+ lexical_score: row.lexical_score,
2733
+ task_match: row.task_match,
2734
+ priority_prior: row.priority_prior,
2735
+ feedback_score: row.feedback_score,
2736
+ feedback_summary: row.feedback_summary,
2737
+ reason_codes: row.reason_codes,
2738
+ updated_at: row.memory.updated_at
2739
+ }))
1409
2740
  };
1410
- const guardResult = guard(db, memInput);
1411
- switch (guardResult.action) {
1412
- case "skip":
1413
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
1414
- case "add": {
1415
- const mem = createMemory(db, memInput);
1416
- if (!mem) return { action: "skipped", reason: "createMemory returned null" };
1417
- if (input.uri) {
1418
- try {
1419
- createPath(db, mem.id, input.uri);
1420
- } catch {
2741
+ }
2742
+ function createJob(jobs, kind, agentId) {
2743
+ const job = {
2744
+ id: randomUUID2(),
2745
+ kind,
2746
+ status: "running",
2747
+ stage: "queued",
2748
+ progress: 0,
2749
+ agent_id: agentId,
2750
+ started_at: now2(),
2751
+ finished_at: null
2752
+ };
2753
+ jobs.set(job.id, job);
2754
+ return job;
2755
+ }
2756
+ function updateJob(job, patch) {
2757
+ Object.assign(job, patch);
2758
+ return job;
2759
+ }
2760
+ function createHttpServer(options) {
2761
+ const ownsDb = !options?.db;
2762
+ const db = options?.db ?? openDatabase({ path: options?.dbPath ?? process.env.AGENT_MEMORY_DB ?? "./agent-memory.db" });
2763
+ const defaultAgentId = options?.agentId ?? process.env.AGENT_MEMORY_AGENT_ID ?? "default";
2764
+ const jobs = /* @__PURE__ */ new Map();
2765
+ const executeReflectJob = async (job, body, stream) => {
2766
+ const phase = asString(body.phase) ?? "all";
2767
+ if (!VALID_PHASES.has(phase)) {
2768
+ throw new Error(`Invalid phase: ${String(body.phase)}`);
2769
+ }
2770
+ updateJob(job, { stage: phase, progress: 0.01 });
2771
+ try {
2772
+ const result = await reflectMemories(db, {
2773
+ phase,
2774
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
2775
+ runners: options?.reflectRunners,
2776
+ onProgress: (event) => {
2777
+ updateJob(job, {
2778
+ stage: String(event.phase),
2779
+ progress: event.progress,
2780
+ backend_job_id: event.jobId ?? job.backend_job_id
2781
+ });
2782
+ if (stream) {
2783
+ sendSse(stream, "progress", {
2784
+ job,
2785
+ event
2786
+ });
2787
+ }
1421
2788
  }
1422
- }
1423
- return { action: "added", memoryId: mem.id, reason: guardResult.reason };
2789
+ });
2790
+ updateJob(job, {
2791
+ status: "completed",
2792
+ stage: "done",
2793
+ progress: 1,
2794
+ backend_job_id: result.jobId,
2795
+ finished_at: now2(),
2796
+ result
2797
+ });
2798
+ return { job, result };
2799
+ } catch (error) {
2800
+ updateJob(job, {
2801
+ status: "failed",
2802
+ stage: "failed",
2803
+ progress: 1,
2804
+ finished_at: now2(),
2805
+ error: error instanceof Error ? error.message : String(error)
2806
+ });
2807
+ throw error;
1424
2808
  }
1425
- case "update": {
1426
- if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
1427
- createSnapshot(db, guardResult.existingId, "update", "sync");
1428
- updateMemory(db, guardResult.existingId, { content: input.content });
1429
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
2809
+ };
2810
+ const executeReindexJob = async (job, body, stream) => {
2811
+ updateJob(job, { stage: "fts", progress: 0.01 });
2812
+ try {
2813
+ const result = await reindexMemories(db, {
2814
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
2815
+ provider: options?.provider,
2816
+ force: asBoolean(body.full) ?? false,
2817
+ batchSize: asNumber(body.batch_size) ?? 16,
2818
+ onProgress: (event) => {
2819
+ updateJob(job, {
2820
+ stage: event.stage,
2821
+ progress: event.progress
2822
+ });
2823
+ if (stream) {
2824
+ sendSse(stream, "progress", {
2825
+ job,
2826
+ event
2827
+ });
2828
+ }
2829
+ }
2830
+ });
2831
+ updateJob(job, {
2832
+ status: "completed",
2833
+ stage: "done",
2834
+ progress: 1,
2835
+ finished_at: now2(),
2836
+ result
2837
+ });
2838
+ return { job, result };
2839
+ } catch (error) {
2840
+ updateJob(job, {
2841
+ status: "failed",
2842
+ stage: "failed",
2843
+ progress: 1,
2844
+ finished_at: now2(),
2845
+ error: error instanceof Error ? error.message : String(error)
2846
+ });
2847
+ throw error;
1430
2848
  }
1431
- case "merge": {
1432
- if (!guardResult.existingId || !guardResult.mergedContent) {
1433
- return { action: "skipped", reason: "Missing merge data" };
2849
+ };
2850
+ const server = http.createServer(async (req, res) => {
2851
+ try {
2852
+ const method = req.method ?? "GET";
2853
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
2854
+ const pathname = url.pathname;
2855
+ if (method === "GET" && pathname === "/health") {
2856
+ sendJson(res, 200, {
2857
+ ok: true,
2858
+ service: "agent-memory",
2859
+ time: now2()
2860
+ });
2861
+ return;
1434
2862
  }
1435
- createSnapshot(db, guardResult.existingId, "merge", "sync");
1436
- updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
1437
- return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
2863
+ if (method === "GET" && pathname === "/v1/status") {
2864
+ const agentId = url.searchParams.get("agent_id") ?? defaultAgentId;
2865
+ sendJson(res, 200, getMemoryStatus(db, { agent_id: agentId }));
2866
+ return;
2867
+ }
2868
+ if (method === "GET" && pathname.startsWith("/v1/jobs/")) {
2869
+ const id = decodeURIComponent(pathname.slice("/v1/jobs/".length));
2870
+ const job = jobs.get(id);
2871
+ if (job) {
2872
+ sendJson(res, 200, job);
2873
+ return;
2874
+ }
2875
+ const maintenanceJob = getMaintenanceJob(db, id);
2876
+ if (maintenanceJob) {
2877
+ sendJson(res, 200, maintenanceJob);
2878
+ return;
2879
+ }
2880
+ sendError(res, 404, `Job not found: ${id}`);
2881
+ return;
2882
+ }
2883
+ if (method !== "POST") {
2884
+ sendError(res, 404, `Route not found: ${method} ${pathname}`);
2885
+ return;
2886
+ }
2887
+ const body = await readJsonBody(req);
2888
+ if (pathname === "/v1/memories") {
2889
+ const content = asString(body.content)?.trim();
2890
+ if (!content) {
2891
+ sendError(res, 400, "content is required");
2892
+ return;
2893
+ }
2894
+ const type = asString(body.type) ?? "knowledge";
2895
+ if (!VALID_MEMORY_TYPES.has(type)) {
2896
+ sendError(res, 400, `Invalid memory type: ${String(body.type)}`);
2897
+ return;
2898
+ }
2899
+ const result = await rememberMemory(db, {
2900
+ content,
2901
+ type,
2902
+ uri: asString(body.uri),
2903
+ source: asString(body.source),
2904
+ emotion_val: asNumber(body.emotion_val),
2905
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
2906
+ conservative: asBoolean(body.conservative),
2907
+ provider: options?.provider
2908
+ });
2909
+ sendJson(res, 200, result);
2910
+ return;
2911
+ }
2912
+ if (pathname === "/v1/recall") {
2913
+ const query = asString(body.query)?.trim();
2914
+ if (!query) {
2915
+ sendError(res, 400, "query is required");
2916
+ return;
2917
+ }
2918
+ const result = await recallMemory(db, {
2919
+ query,
2920
+ limit: asNumber(body.limit),
2921
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
2922
+ provider: options?.provider
2923
+ });
2924
+ sendJson(res, 200, formatRecallResponse(result));
2925
+ return;
2926
+ }
2927
+ if (pathname === "/v1/surface") {
2928
+ const types = asStringArray(body.types)?.filter((type) => VALID_MEMORY_TYPES.has(type));
2929
+ const intent = asString(body.intent);
2930
+ if (intent !== void 0 && !VALID_INTENTS.has(intent)) {
2931
+ sendError(res, 400, `Invalid intent: ${intent}`);
2932
+ return;
2933
+ }
2934
+ const result = await surfaceMemories(db, {
2935
+ query: asString(body.query),
2936
+ task: asString(body.task),
2937
+ recent_turns: asStringArray(body.recent_turns),
2938
+ intent,
2939
+ types,
2940
+ limit: asNumber(body.limit),
2941
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
2942
+ provider: options?.provider
2943
+ });
2944
+ sendJson(res, 200, formatSurfaceResponse(result));
2945
+ return;
2946
+ }
2947
+ if (pathname === "/v1/feedback") {
2948
+ const memoryId = asString(body.memory_id)?.trim();
2949
+ const source = asString(body.source);
2950
+ const useful = asBoolean(body.useful);
2951
+ if (!memoryId) {
2952
+ sendError(res, 400, "memory_id is required");
2953
+ return;
2954
+ }
2955
+ if (source !== "recall" && source !== "surface") {
2956
+ sendError(res, 400, "source must be 'recall' or 'surface'");
2957
+ return;
2958
+ }
2959
+ if (useful === void 0) {
2960
+ sendError(res, 400, "useful must be boolean");
2961
+ return;
2962
+ }
2963
+ const result = recordFeedbackEvent(db, {
2964
+ memory_id: memoryId,
2965
+ source,
2966
+ useful,
2967
+ agent_id: asString(body.agent_id) ?? defaultAgentId
2968
+ });
2969
+ sendJson(res, 200, result);
2970
+ return;
2971
+ }
2972
+ if (pathname === "/v1/reflect") {
2973
+ const agentId = asString(body.agent_id) ?? defaultAgentId;
2974
+ const job = createJob(jobs, "reflect", agentId);
2975
+ if (wantsSse(req, body)) {
2976
+ openSse(res);
2977
+ sendSse(res, "job", job);
2978
+ void executeReflectJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
2979
+ sendSse(res, "done", { job: currentJob, result: result2 });
2980
+ }).catch((error) => {
2981
+ sendSse(res, "error", {
2982
+ job,
2983
+ error: error instanceof Error ? error.message : String(error)
2984
+ });
2985
+ }).finally(() => {
2986
+ if (!res.writableEnded) res.end();
2987
+ });
2988
+ return;
2989
+ }
2990
+ const result = await executeReflectJob(job, body);
2991
+ sendJson(res, 200, result);
2992
+ return;
2993
+ }
2994
+ if (pathname === "/v1/reindex") {
2995
+ const agentId = asString(body.agent_id) ?? defaultAgentId;
2996
+ const job = createJob(jobs, "reindex", agentId);
2997
+ if (wantsSse(req, body)) {
2998
+ openSse(res);
2999
+ sendSse(res, "job", job);
3000
+ void executeReindexJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
3001
+ sendSse(res, "done", { job: currentJob, result: result2 });
3002
+ }).catch((error) => {
3003
+ sendSse(res, "error", {
3004
+ job,
3005
+ error: error instanceof Error ? error.message : String(error)
3006
+ });
3007
+ }).finally(() => {
3008
+ if (!res.writableEnded) res.end();
3009
+ });
3010
+ return;
3011
+ }
3012
+ const result = await executeReindexJob(job, body);
3013
+ sendJson(res, 200, result);
3014
+ return;
3015
+ }
3016
+ sendError(res, 404, `Route not found: ${method} ${pathname}`);
3017
+ } catch (error) {
3018
+ sendError(res, 500, error instanceof Error ? error.message : String(error));
1438
3019
  }
1439
- }
3020
+ });
3021
+ return {
3022
+ server,
3023
+ db,
3024
+ jobs,
3025
+ listen(port = 3e3, host = "127.0.0.1") {
3026
+ return new Promise((resolve2, reject) => {
3027
+ server.once("error", reject);
3028
+ server.listen(port, host, () => {
3029
+ server.off("error", reject);
3030
+ const address = server.address();
3031
+ if (!address || typeof address === "string") {
3032
+ resolve2({ port, host });
3033
+ return;
3034
+ }
3035
+ resolve2({ port: address.port, host: address.address });
3036
+ });
3037
+ });
3038
+ },
3039
+ close() {
3040
+ return new Promise((resolve2, reject) => {
3041
+ server.close((error) => {
3042
+ if (ownsDb) {
3043
+ try {
3044
+ db.close();
3045
+ } catch {
3046
+ }
3047
+ }
3048
+ if (error) {
3049
+ reject(error);
3050
+ return;
3051
+ }
3052
+ resolve2();
3053
+ });
3054
+ });
3055
+ }
3056
+ };
3057
+ }
3058
+ async function startHttpServer(options) {
3059
+ const service = createHttpServer(options);
3060
+ await service.listen(options?.port, options?.host);
3061
+ return service;
1440
3062
  }
1441
3063
 
1442
3064
  // src/bin/agent-memory.ts
1443
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
1444
- import { resolve, basename } from "path";
1445
3065
  var args = process.argv.slice(2);
1446
3066
  var command = args[0];
1447
3067
  function getDbPath() {
@@ -1452,34 +3072,52 @@ function getAgentId() {
1452
3072
  }
1453
3073
  function printHelp() {
1454
3074
  console.log(`
1455
- \u{1F9E0} AgentMemory v2 \u2014 Sleep-cycle memory for AI agents
3075
+ \u{1F9E0} AgentMemory v4 \u2014 Sleep-cycle memory for AI agents
1456
3076
 
1457
3077
  Usage: agent-memory <command> [options]
1458
3078
 
1459
3079
  Commands:
1460
- init Create database
1461
- db:migrate Run schema migrations (no-op if up-to-date)
1462
- embed [--limit N] Embed missing memories (requires provider)
3080
+ init Create database
3081
+ db:migrate Run schema migrations (no-op if up-to-date)
1463
3082
  remember <content> [--uri X] [--type T] Store a memory
1464
- recall <query> [--limit N] Search memories
1465
- boot Load identity memories
1466
- status Show statistics
1467
- reflect [decay|tidy|govern|all] Run sleep cycle
1468
- reindex Rebuild FTS index with jieba tokenizer
1469
- migrate <dir> Import from Markdown files
1470
- export <dir> Export memories to Markdown files
1471
- help Show this help
3083
+ recall <query> [--limit N] Search memories (hybrid retrieval, auto-fallback to BM25)
3084
+ boot Load identity memories
3085
+ status Show statistics
3086
+ reflect [decay|tidy|govern|all] Run sleep cycle
3087
+ reindex [--full] [--batch-size N] Rebuild FTS index and embeddings (if configured)
3088
+ serve [--host H] [--port N] Start the HTTP/SSE API server
3089
+ migrate <dir> Import from Markdown files
3090
+ export <dir> Export memories to Markdown files
3091
+ help Show this help
1472
3092
 
1473
3093
  Environment:
1474
- AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
1475
- AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
3094
+ AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
3095
+ AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
1476
3096
  `);
1477
3097
  }
1478
3098
  function getFlag(flag) {
1479
- const idx = args.indexOf(flag);
1480
- if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
3099
+ const index = args.indexOf(flag);
3100
+ if (index >= 0 && index + 1 < args.length) return args[index + 1];
1481
3101
  return void 0;
1482
3102
  }
3103
+ function hasFlag(flag) {
3104
+ return args.includes(flag);
3105
+ }
3106
+ function getPositionalArgs(startIndex = 1) {
3107
+ const values = [];
3108
+ for (let index = startIndex; index < args.length; index++) {
3109
+ const token = args[index];
3110
+ if (token.startsWith("--")) {
3111
+ const next = args[index + 1];
3112
+ if (next !== void 0 && !next.startsWith("--")) {
3113
+ index += 1;
3114
+ }
3115
+ continue;
3116
+ }
3117
+ values.push(token);
3118
+ }
3119
+ return values;
3120
+ }
1483
3121
  async function main() {
1484
3122
  try {
1485
3123
  switch (command) {
@@ -1497,22 +3135,8 @@ async function main() {
1497
3135
  db.close();
1498
3136
  break;
1499
3137
  }
1500
- case "embed": {
1501
- const provider = getEmbeddingProviderFromEnv();
1502
- if (!provider) {
1503
- console.error("Embedding provider not configured. Set AGENT_MEMORY_EMBEDDINGS_PROVIDER=openai|qwen and the corresponding API key.");
1504
- process.exit(1);
1505
- }
1506
- const db = openDatabase({ path: getDbPath() });
1507
- const agentId = getAgentId();
1508
- const limit = parseInt(getFlag("--limit") ?? "200");
1509
- const r = await embedMissingForAgent(db, provider, { agent_id: agentId, limit });
1510
- console.log(`\u2705 Embedded: ${r.embedded}/${r.scanned} (agent_id=${agentId}, model=${provider.model})`);
1511
- db.close();
1512
- break;
1513
- }
1514
3138
  case "remember": {
1515
- const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
3139
+ const content = getPositionalArgs(1).join(" ");
1516
3140
  if (!content) {
1517
3141
  console.error("Usage: agent-memory remember <content>");
1518
3142
  process.exit(1);
@@ -1520,39 +3144,39 @@ async function main() {
1520
3144
  const db = openDatabase({ path: getDbPath() });
1521
3145
  const uri = getFlag("--uri");
1522
3146
  const type = getFlag("--type") ?? "knowledge";
1523
- const agentId = getAgentId();
1524
- const provider = getEmbeddingProviderFromEnv();
1525
- const result = syncOne(db, { content, type, uri, agent_id: agentId });
1526
- if (provider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
1527
- try {
1528
- await embedMemory(db, result.memoryId, provider, { agent_id: agentId });
1529
- } catch {
1530
- }
1531
- }
3147
+ const result = await rememberMemory(db, {
3148
+ content,
3149
+ type,
3150
+ uri,
3151
+ source: "manual",
3152
+ agent_id: getAgentId()
3153
+ });
1532
3154
  console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
1533
3155
  db.close();
1534
3156
  break;
1535
3157
  }
1536
3158
  case "recall": {
1537
- const query = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
3159
+ const query = getPositionalArgs(1).join(" ");
1538
3160
  if (!query) {
1539
3161
  console.error("Usage: agent-memory recall <query>");
1540
3162
  process.exit(1);
1541
3163
  }
1542
3164
  const db = openDatabase({ path: getDbPath() });
1543
- const limit = parseInt(getFlag("--limit") ?? "10");
1544
- const { intent } = classifyIntent(query);
1545
- const strategy = getStrategy(intent);
1546
- const agentId = getAgentId();
1547
- const provider = getEmbeddingProviderFromEnv();
1548
- const raw = await searchHybrid(db, query, { agent_id: agentId, embeddingProvider: provider, limit: limit * 2 });
1549
- const results = rerank(raw, { ...strategy, limit });
1550
- console.log(`\u{1F50D} Intent: ${intent} | Results: ${results.length}
3165
+ const result = await recallMemory(db, {
3166
+ query,
3167
+ agent_id: getAgentId(),
3168
+ limit: Number.parseInt(getFlag("--limit") ?? "10", 10)
3169
+ });
3170
+ console.log(`\u{1F50D} Results: ${result.results.length} (${result.mode})
1551
3171
  `);
1552
- for (const r of results) {
1553
- const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
1554
- const v = (r.memory.vitality * 100).toFixed(0);
1555
- console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
3172
+ for (const row of result.results) {
3173
+ const priorityLabel = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][row.memory.priority];
3174
+ const vitality = (row.memory.vitality * 100).toFixed(0);
3175
+ const branches = [
3176
+ row.bm25_rank ? `bm25#${row.bm25_rank}` : null,
3177
+ row.vector_rank ? `vec#${row.vector_rank}` : null
3178
+ ].filter(Boolean).join(" + ");
3179
+ console.log(`${priorityLabel} P${row.memory.priority} [${vitality}%] ${row.memory.content.slice(0, 80)}${branches ? ` (${branches})` : ""}`);
1556
3180
  }
1557
3181
  db.close();
1558
3182
  break;
@@ -1562,8 +3186,8 @@ async function main() {
1562
3186
  const result = boot(db, { agent_id: getAgentId() });
1563
3187
  console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
1564
3188
  `);
1565
- for (const m of result.identityMemories) {
1566
- console.log(` \u{1F534} ${m.content.slice(0, 100)}`);
3189
+ for (const memory of result.identityMemories) {
3190
+ console.log(` \u{1F534} ${memory.content.slice(0, 100)}`);
1567
3191
  }
1568
3192
  if (result.bootPaths.length) {
1569
3193
  console.log(`
@@ -1574,74 +3198,83 @@ async function main() {
1574
3198
  }
1575
3199
  case "status": {
1576
3200
  const db = openDatabase({ path: getDbPath() });
1577
- const agentId = getAgentId();
1578
- const stats = countMemories(db, agentId);
1579
- const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
1580
- const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
1581
- const links = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(agentId).c;
1582
- const snaps = db.prepare(
1583
- `SELECT COUNT(*) as c FROM snapshots s
1584
- JOIN memories m ON m.id = s.memory_id
1585
- WHERE m.agent_id = ?`
1586
- ).get(agentId).c;
3201
+ const status = getMemoryStatus(db, { agent_id: getAgentId() });
1587
3202
  console.log("\u{1F9E0} AgentMemory Status\n");
1588
- console.log(` Total memories: ${stats.total}`);
1589
- console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1590
- console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1591
- console.log(` Paths: ${paths} | Links: ${links} | Snapshots: ${snaps}`);
1592
- console.log(` Low vitality (<10%): ${lowVit}`);
3203
+ console.log(` Total memories: ${status.total}`);
3204
+ console.log(` By type: ${Object.entries(status.by_type).map(([key, value]) => `${key}=${value}`).join(", ")}`);
3205
+ console.log(` By priority: ${Object.entries(status.by_priority).map(([key, value]) => `${key}=${value}`).join(", ")}`);
3206
+ console.log(` Paths: ${status.paths}`);
3207
+ console.log(` Low vitality (<10%): ${status.low_vitality}`);
3208
+ console.log(` Feedback events: ${status.feedback_events}`);
1593
3209
  db.close();
1594
3210
  break;
1595
3211
  }
1596
3212
  case "reflect": {
1597
3213
  const phase = args[1] ?? "all";
1598
3214
  const db = openDatabase({ path: getDbPath() });
1599
- const agentId = getAgentId();
1600
- console.log(`\u{1F319} Running ${phase} phase...
1601
- `);
1602
- if (phase === "decay" || phase === "all") {
1603
- const r = runDecay(db, { agent_id: agentId });
1604
- console.log(` Decay: ${r.updated} updated, ${r.decayed} decayed, ${r.belowThreshold} below threshold`);
1605
- }
1606
- if (phase === "tidy" || phase === "all") {
1607
- const r = runTidy(db, { agent_id: agentId });
1608
- console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans, ${r.snapshotsPruned} snapshots pruned`);
1609
- }
1610
- if (phase === "govern" || phase === "all") {
1611
- const r = runGovern(db, { agent_id: agentId });
1612
- console.log(` Govern: ${r.orphanPaths} paths, ${r.orphanLinks} links, ${r.emptyMemories} empty cleaned`);
3215
+ const result = await reflectMemories(db, { phase, agent_id: getAgentId() });
3216
+ console.log(`\u{1F319} Reflect job ${result.jobId}${result.resumed ? " (resume)" : ""}`);
3217
+ for (const [name, summary] of Object.entries(result.results)) {
3218
+ console.log(` ${name}: ${JSON.stringify(summary)}`);
1613
3219
  }
1614
3220
  db.close();
1615
3221
  break;
1616
3222
  }
1617
3223
  case "reindex": {
1618
3224
  const db = openDatabase({ path: getDbPath() });
1619
- const memories = db.prepare("SELECT id, content FROM memories").all();
1620
- db.exec("DELETE FROM memories_fts");
1621
- const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
1622
- let count = 0;
1623
- const txn = db.transaction(() => {
1624
- for (const mem of memories) {
1625
- insert.run(mem.id, tokenizeForIndex(mem.content));
1626
- count++;
1627
- }
3225
+ const result = await reindexMemories(db, {
3226
+ agent_id: getAgentId(),
3227
+ force: hasFlag("--full"),
3228
+ batchSize: Number.parseInt(getFlag("--batch-size") ?? "16", 10)
1628
3229
  });
1629
- txn();
1630
- console.log(`\u{1F504} Reindexed ${count} memories with jieba tokenizer`);
3230
+ console.log(`\u{1F504} Reindexed ${result.fts.reindexed} memories in BM25 index`);
3231
+ if (result.embeddings.enabled) {
3232
+ console.log(`\u{1F9EC} Embeddings: provider=${result.embeddings.providerId} scanned=${result.embeddings.scanned} embedded=${result.embeddings.embedded} failed=${result.embeddings.failed}`);
3233
+ } else {
3234
+ console.log("\u{1F9EC} Embeddings: disabled (no provider configured)");
3235
+ }
1631
3236
  db.close();
1632
3237
  break;
1633
3238
  }
3239
+ case "serve": {
3240
+ const port = Number.parseInt(getFlag("--port") ?? process.env.AGENT_MEMORY_HTTP_PORT ?? "3000", 10);
3241
+ const host = getFlag("--host") ?? process.env.AGENT_MEMORY_HTTP_HOST ?? "127.0.0.1";
3242
+ const service = await startHttpServer({
3243
+ dbPath: getDbPath(),
3244
+ agentId: getAgentId(),
3245
+ port,
3246
+ host
3247
+ });
3248
+ const address = service.server.address();
3249
+ if (address && typeof address !== "string") {
3250
+ console.log(`\u{1F310} AgentMemory HTTP server listening on http://${address.address}:${address.port}`);
3251
+ } else {
3252
+ console.log(`\u{1F310} AgentMemory HTTP server listening on http://${host}:${port}`);
3253
+ }
3254
+ const shutdown = async () => {
3255
+ try {
3256
+ await service.close();
3257
+ } finally {
3258
+ process.exit(0);
3259
+ }
3260
+ };
3261
+ process.once("SIGINT", () => {
3262
+ void shutdown();
3263
+ });
3264
+ process.once("SIGTERM", () => {
3265
+ void shutdown();
3266
+ });
3267
+ break;
3268
+ }
1634
3269
  case "export": {
1635
3270
  const dir = args[1];
1636
3271
  if (!dir) {
1637
3272
  console.error("Usage: agent-memory export <directory>");
1638
3273
  process.exit(1);
1639
3274
  }
1640
- const dirPath = resolve(dir);
1641
3275
  const db = openDatabase({ path: getDbPath() });
1642
- const agentId = getAgentId();
1643
- const result = exportMemories(db, dirPath, { agent_id: agentId });
1644
- console.log(`\u2705 Export complete: ${result.exported} items to ${dirPath} (${result.files.length} files)`);
3276
+ const result = exportMemories(db, resolve(dir), { agent_id: getAgentId() });
3277
+ console.log(`\u2705 Export complete: ${result.exported} items to ${resolve(dir)} (${result.files.length} files)`);
1645
3278
  db.close();
1646
3279
  break;
1647
3280
  }
@@ -1659,10 +3292,10 @@ async function main() {
1659
3292
  const db = openDatabase({ path: getDbPath() });
1660
3293
  const agentId = getAgentId();
1661
3294
  let imported = 0;
1662
- const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((f) => resolve(dirPath, f)).find((p) => existsSync2(p));
3295
+ const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((file) => resolve(dirPath, file)).find((file) => existsSync2(file));
1663
3296
  if (memoryFile) {
1664
3297
  const content = readFileSync2(memoryFile, "utf-8");
1665
- const sections = content.split(/^## /m).filter((s) => s.trim());
3298
+ const sections = content.split(/^## /m).filter((section) => section.trim());
1666
3299
  for (const section of sections) {
1667
3300
  const lines = section.split("\n");
1668
3301
  const title = lines[0]?.trim();
@@ -1670,40 +3303,46 @@ async function main() {
1670
3303
  if (!body) continue;
1671
3304
  const type = title?.toLowerCase().includes("\u5173\u4E8E") || title?.toLowerCase().includes("about") ? "identity" : "knowledge";
1672
3305
  const uri = `knowledge://memory-md/${title?.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "-").toLowerCase()}`;
1673
- syncOne(db, { content: `## ${title}
1674
- ${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentId });
1675
- imported++;
3306
+ await rememberMemory(db, {
3307
+ content: `## ${title}
3308
+ ${body}`,
3309
+ type,
3310
+ uri,
3311
+ source: `migrate:${basename(memoryFile)}`,
3312
+ agent_id: agentId
3313
+ });
3314
+ imported += 1;
1676
3315
  }
1677
3316
  console.log(`\u{1F4C4} ${basename(memoryFile)}: ${sections.length} sections imported`);
1678
3317
  }
1679
- const mdFiles = readdirSync(dirPath).filter((f) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(f)).sort();
3318
+ const mdFiles = readdirSync(dirPath).filter((file) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(file)).sort();
1680
3319
  for (const file of mdFiles) {
1681
3320
  const content = readFileSync2(resolve(dirPath, file), "utf-8");
1682
3321
  const date = file.replace(/\.(md|qmd)$/i, "");
1683
- syncOne(db, {
3322
+ await rememberMemory(db, {
1684
3323
  content,
1685
3324
  type: "event",
1686
3325
  uri: `event://journal/${date}`,
1687
3326
  source: `migrate:${file}`,
1688
3327
  agent_id: agentId
1689
3328
  });
1690
- imported++;
3329
+ imported += 1;
1691
3330
  }
1692
3331
  if (mdFiles.length) console.log(`\u{1F4DD} Journals: ${mdFiles.length} files imported`);
1693
3332
  const weeklyDir = resolve(dirPath, "weekly");
1694
3333
  if (existsSync2(weeklyDir)) {
1695
- const weeklyFiles = readdirSync(weeklyDir).filter((f) => f.endsWith(".md") || f.endsWith(".qmd"));
3334
+ const weeklyFiles = readdirSync(weeklyDir).filter((file) => file.endsWith(".md") || file.endsWith(".qmd"));
1696
3335
  for (const file of weeklyFiles) {
1697
3336
  const content = readFileSync2(resolve(weeklyDir, file), "utf-8");
1698
3337
  const week = file.replace(/\.(md|qmd)$/i, "");
1699
- syncOne(db, {
3338
+ await rememberMemory(db, {
1700
3339
  content,
1701
3340
  type: "knowledge",
1702
3341
  uri: `knowledge://weekly/${week}`,
1703
3342
  source: `migrate:weekly/${file}`,
1704
3343
  agent_id: agentId
1705
3344
  });
1706
- imported++;
3345
+ imported += 1;
1707
3346
  }
1708
3347
  if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
1709
3348
  }
@@ -1723,8 +3362,8 @@ ${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentI
1723
3362
  printHelp();
1724
3363
  process.exit(1);
1725
3364
  }
1726
- } catch (err) {
1727
- const message = err instanceof Error ? err.message : String(err);
3365
+ } catch (error) {
3366
+ const message = error instanceof Error ? error.message : String(error);
1728
3367
  console.error("Error:", message);
1729
3368
  process.exit(1);
1730
3369
  }