@smyslenny/agent-memory 4.0.0-alpha.1 → 4.1.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.
- package/dist/bin/agent-memory.js +1164 -882
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +48 -6
- package/dist/index.js +124 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +140 -23
- package/dist/mcp/server.js.map +1 -1
- package/docs/design/.next-id +1 -0
- package/docs/design/0016-v41-warm-boot-surface-emotion.md +228 -0
- package/package.json +2 -2
package/dist/bin/agent-memory.js
CHANGED
|
@@ -1,126 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// AgentMemory v2 — Sleep-cycle memory for AI agents
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
// src/core/db.ts
|
|
9
14
|
import Database from "better-sqlite3";
|
|
10
15
|
import { randomUUID } from "crypto";
|
|
11
|
-
var SCHEMA_VERSION = 5;
|
|
12
|
-
var SCHEMA_SQL = `
|
|
13
|
-
-- Memory entries
|
|
14
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
15
|
-
id TEXT PRIMARY KEY,
|
|
16
|
-
content TEXT NOT NULL,
|
|
17
|
-
type TEXT NOT NULL CHECK(type IN ('identity','emotion','knowledge','event')),
|
|
18
|
-
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority BETWEEN 0 AND 3),
|
|
19
|
-
emotion_val REAL NOT NULL DEFAULT 0.0,
|
|
20
|
-
vitality REAL NOT NULL DEFAULT 1.0,
|
|
21
|
-
stability REAL NOT NULL DEFAULT 1.0,
|
|
22
|
-
access_count INTEGER NOT NULL DEFAULT 0,
|
|
23
|
-
last_accessed TEXT,
|
|
24
|
-
created_at TEXT NOT NULL,
|
|
25
|
-
updated_at TEXT NOT NULL,
|
|
26
|
-
source TEXT,
|
|
27
|
-
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
28
|
-
hash TEXT,
|
|
29
|
-
UNIQUE(hash, agent_id)
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
-- URI paths (Content-Path separation, from nocturne)
|
|
33
|
-
CREATE TABLE IF NOT EXISTS paths (
|
|
34
|
-
id TEXT PRIMARY KEY,
|
|
35
|
-
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
36
|
-
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
37
|
-
uri TEXT NOT NULL,
|
|
38
|
-
alias TEXT,
|
|
39
|
-
domain TEXT NOT NULL,
|
|
40
|
-
created_at TEXT NOT NULL,
|
|
41
|
-
UNIQUE(agent_id, uri)
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
-- Association network (knowledge graph)
|
|
45
|
-
CREATE TABLE IF NOT EXISTS links (
|
|
46
|
-
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
47
|
-
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
48
|
-
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
49
|
-
relation TEXT NOT NULL,
|
|
50
|
-
weight REAL NOT NULL DEFAULT 1.0,
|
|
51
|
-
created_at TEXT NOT NULL,
|
|
52
|
-
PRIMARY KEY (agent_id, source_id, target_id)
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
-- Snapshots (version control, from nocturne + Memory Palace)
|
|
56
|
-
CREATE TABLE IF NOT EXISTS snapshots (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
59
|
-
content TEXT NOT NULL,
|
|
60
|
-
changed_by TEXT,
|
|
61
|
-
action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
|
|
62
|
-
created_at TEXT NOT NULL
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
-- Full-text search index (BM25)
|
|
66
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
67
|
-
id UNINDEXED,
|
|
68
|
-
content,
|
|
69
|
-
tokenize='unicode61'
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
-- Embeddings (optional semantic layer)
|
|
73
|
-
CREATE TABLE IF NOT EXISTS embeddings (
|
|
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
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
-- Schema version tracking
|
|
108
|
-
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
109
|
-
key TEXT PRIMARY KEY,
|
|
110
|
-
value TEXT NOT NULL
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
-- Indexes for common queries
|
|
114
|
-
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
115
|
-
CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
|
|
116
|
-
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
117
|
-
CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
118
|
-
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
119
|
-
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
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);
|
|
123
|
-
`;
|
|
124
16
|
function openDatabase(opts) {
|
|
125
17
|
const db = new Database(opts.path);
|
|
126
18
|
if (opts.walMode !== false) {
|
|
@@ -198,6 +90,11 @@ function migrateDatabase(db, from, to) {
|
|
|
198
90
|
v = 5;
|
|
199
91
|
continue;
|
|
200
92
|
}
|
|
93
|
+
if (v === 5) {
|
|
94
|
+
migrateV5ToV6(db);
|
|
95
|
+
v = 6;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
201
98
|
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
202
99
|
}
|
|
203
100
|
}
|
|
@@ -283,6 +180,8 @@ function inferSchemaVersion(db) {
|
|
|
283
180
|
const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
|
|
284
181
|
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
285
182
|
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
183
|
+
const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
|
|
184
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
|
|
286
185
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
|
|
287
186
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
|
|
288
187
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
|
|
@@ -309,6 +208,9 @@ function ensureIndexes(db) {
|
|
|
309
208
|
if (tableExists(db, "maintenance_jobs")) {
|
|
310
209
|
db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
|
|
311
210
|
}
|
|
211
|
+
if (tableHasColumn(db, "memories", "emotion_tag")) {
|
|
212
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
|
|
213
|
+
}
|
|
312
214
|
if (tableExists(db, "feedback_events")) {
|
|
313
215
|
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
|
|
314
216
|
if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
|
|
@@ -442,14 +344,149 @@ function migrateV4ToV5(db) {
|
|
|
442
344
|
throw e;
|
|
443
345
|
}
|
|
444
346
|
}
|
|
347
|
+
function migrateV5ToV6(db) {
|
|
348
|
+
if (tableHasColumn(db, "memories", "emotion_tag")) {
|
|
349
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
db.exec("BEGIN");
|
|
354
|
+
db.exec("ALTER TABLE memories ADD COLUMN emotion_tag TEXT;");
|
|
355
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
|
|
356
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
|
|
357
|
+
db.exec("COMMIT");
|
|
358
|
+
} catch (e) {
|
|
359
|
+
try {
|
|
360
|
+
db.exec("ROLLBACK");
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
throw e;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
var SCHEMA_VERSION, SCHEMA_SQL;
|
|
367
|
+
var init_db = __esm({
|
|
368
|
+
"src/core/db.ts"() {
|
|
369
|
+
"use strict";
|
|
370
|
+
SCHEMA_VERSION = 6;
|
|
371
|
+
SCHEMA_SQL = `
|
|
372
|
+
-- Memory entries
|
|
373
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
374
|
+
id TEXT PRIMARY KEY,
|
|
375
|
+
content TEXT NOT NULL,
|
|
376
|
+
type TEXT NOT NULL CHECK(type IN ('identity','emotion','knowledge','event')),
|
|
377
|
+
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority BETWEEN 0 AND 3),
|
|
378
|
+
emotion_val REAL NOT NULL DEFAULT 0.0,
|
|
379
|
+
vitality REAL NOT NULL DEFAULT 1.0,
|
|
380
|
+
stability REAL NOT NULL DEFAULT 1.0,
|
|
381
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
382
|
+
last_accessed TEXT,
|
|
383
|
+
created_at TEXT NOT NULL,
|
|
384
|
+
updated_at TEXT NOT NULL,
|
|
385
|
+
source TEXT,
|
|
386
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
387
|
+
hash TEXT,
|
|
388
|
+
emotion_tag TEXT,
|
|
389
|
+
UNIQUE(hash, agent_id)
|
|
390
|
+
);
|
|
445
391
|
|
|
446
|
-
|
|
447
|
-
|
|
392
|
+
-- URI paths (Content-Path separation, from nocturne)
|
|
393
|
+
CREATE TABLE IF NOT EXISTS paths (
|
|
394
|
+
id TEXT PRIMARY KEY,
|
|
395
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
396
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
397
|
+
uri TEXT NOT NULL,
|
|
398
|
+
alias TEXT,
|
|
399
|
+
domain TEXT NOT NULL,
|
|
400
|
+
created_at TEXT NOT NULL,
|
|
401
|
+
UNIQUE(agent_id, uri)
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
-- Association network (knowledge graph)
|
|
405
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
406
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
407
|
+
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
408
|
+
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
409
|
+
relation TEXT NOT NULL,
|
|
410
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
411
|
+
created_at TEXT NOT NULL,
|
|
412
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
-- Snapshots (version control, from nocturne + Memory Palace)
|
|
416
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
417
|
+
id TEXT PRIMARY KEY,
|
|
418
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
419
|
+
content TEXT NOT NULL,
|
|
420
|
+
changed_by TEXT,
|
|
421
|
+
action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
|
|
422
|
+
created_at TEXT NOT NULL
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
-- Full-text search index (BM25)
|
|
426
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
427
|
+
id UNINDEXED,
|
|
428
|
+
content,
|
|
429
|
+
tokenize='unicode61'
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
-- Embeddings (optional semantic layer)
|
|
433
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
434
|
+
id TEXT PRIMARY KEY,
|
|
435
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
436
|
+
provider_id TEXT NOT NULL,
|
|
437
|
+
vector BLOB,
|
|
438
|
+
content_hash TEXT NOT NULL,
|
|
439
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
|
|
440
|
+
created_at TEXT NOT NULL,
|
|
441
|
+
UNIQUE(memory_id, provider_id)
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
-- Maintenance jobs (reflect / reindex checkpoints)
|
|
445
|
+
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
446
|
+
job_id TEXT PRIMARY KEY,
|
|
447
|
+
phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
|
|
448
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
449
|
+
checkpoint TEXT,
|
|
450
|
+
error TEXT,
|
|
451
|
+
started_at TEXT NOT NULL,
|
|
452
|
+
finished_at TEXT
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
-- Feedback signals (recall/surface usefulness + governance priors)
|
|
456
|
+
CREATE TABLE IF NOT EXISTS feedback_events (
|
|
457
|
+
id TEXT PRIMARY KEY,
|
|
458
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
459
|
+
source TEXT NOT NULL DEFAULT 'surface',
|
|
460
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
461
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
462
|
+
event_type TEXT NOT NULL DEFAULT 'surface:useful',
|
|
463
|
+
value REAL NOT NULL DEFAULT 1.0,
|
|
464
|
+
created_at TEXT NOT NULL
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
-- Schema version tracking
|
|
468
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
469
|
+
key TEXT PRIMARY KEY,
|
|
470
|
+
value TEXT NOT NULL
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
-- Indexes for common queries
|
|
474
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
475
|
+
CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
|
|
476
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
477
|
+
CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
478
|
+
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
480
|
+
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
481
|
+
CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
448
486
|
|
|
449
487
|
// src/search/tokenizer.ts
|
|
450
488
|
import { readFileSync } from "fs";
|
|
451
489
|
import { createRequire } from "module";
|
|
452
|
-
var _jieba;
|
|
453
490
|
function getJieba() {
|
|
454
491
|
if (_jieba !== void 0) return _jieba;
|
|
455
492
|
try {
|
|
@@ -463,38 +500,6 @@ function getJieba() {
|
|
|
463
500
|
}
|
|
464
501
|
return _jieba;
|
|
465
502
|
}
|
|
466
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
467
|
-
"\u7684",
|
|
468
|
-
"\u4E86",
|
|
469
|
-
"\u5728",
|
|
470
|
-
"\u662F",
|
|
471
|
-
"\u6211",
|
|
472
|
-
"\u6709",
|
|
473
|
-
"\u548C",
|
|
474
|
-
"\u5C31",
|
|
475
|
-
"\u4E0D",
|
|
476
|
-
"\u4EBA",
|
|
477
|
-
"\u90FD",
|
|
478
|
-
"\u4E00",
|
|
479
|
-
"\u4E2A",
|
|
480
|
-
"\u4E0A",
|
|
481
|
-
"\u4E5F",
|
|
482
|
-
"\u5230",
|
|
483
|
-
"\u4ED6",
|
|
484
|
-
"\u6CA1",
|
|
485
|
-
"\u8FD9",
|
|
486
|
-
"\u8981",
|
|
487
|
-
"\u4F1A",
|
|
488
|
-
"\u5BF9",
|
|
489
|
-
"\u8BF4",
|
|
490
|
-
"\u800C",
|
|
491
|
-
"\u53BB",
|
|
492
|
-
"\u4E4B",
|
|
493
|
-
"\u88AB",
|
|
494
|
-
"\u5979",
|
|
495
|
-
"\u628A",
|
|
496
|
-
"\u90A3"
|
|
497
|
-
]);
|
|
498
503
|
function tokenize(text) {
|
|
499
504
|
const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
|
|
500
505
|
const tokens = [];
|
|
@@ -524,6 +529,44 @@ function tokenizeForIndex(text) {
|
|
|
524
529
|
const tokens = tokenize(text);
|
|
525
530
|
return tokens.join(" ");
|
|
526
531
|
}
|
|
532
|
+
var _jieba, STOPWORDS;
|
|
533
|
+
var init_tokenizer = __esm({
|
|
534
|
+
"src/search/tokenizer.ts"() {
|
|
535
|
+
"use strict";
|
|
536
|
+
STOPWORDS = /* @__PURE__ */ new Set([
|
|
537
|
+
"\u7684",
|
|
538
|
+
"\u4E86",
|
|
539
|
+
"\u5728",
|
|
540
|
+
"\u662F",
|
|
541
|
+
"\u6211",
|
|
542
|
+
"\u6709",
|
|
543
|
+
"\u548C",
|
|
544
|
+
"\u5C31",
|
|
545
|
+
"\u4E0D",
|
|
546
|
+
"\u4EBA",
|
|
547
|
+
"\u90FD",
|
|
548
|
+
"\u4E00",
|
|
549
|
+
"\u4E2A",
|
|
550
|
+
"\u4E0A",
|
|
551
|
+
"\u4E5F",
|
|
552
|
+
"\u5230",
|
|
553
|
+
"\u4ED6",
|
|
554
|
+
"\u6CA1",
|
|
555
|
+
"\u8FD9",
|
|
556
|
+
"\u8981",
|
|
557
|
+
"\u4F1A",
|
|
558
|
+
"\u5BF9",
|
|
559
|
+
"\u8BF4",
|
|
560
|
+
"\u800C",
|
|
561
|
+
"\u53BB",
|
|
562
|
+
"\u4E4B",
|
|
563
|
+
"\u88AB",
|
|
564
|
+
"\u5979",
|
|
565
|
+
"\u628A",
|
|
566
|
+
"\u90A3"
|
|
567
|
+
]);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
527
570
|
|
|
528
571
|
// src/search/embedding.ts
|
|
529
572
|
import { createHash } from "crypto";
|
|
@@ -651,6 +694,11 @@ function createLocalHttpEmbeddingProvider(opts) {
|
|
|
651
694
|
function normalizeEmbeddingBaseUrl(baseUrl) {
|
|
652
695
|
return trimTrailingSlashes(baseUrl);
|
|
653
696
|
}
|
|
697
|
+
var init_embedding = __esm({
|
|
698
|
+
"src/search/embedding.ts"() {
|
|
699
|
+
"use strict";
|
|
700
|
+
}
|
|
701
|
+
});
|
|
654
702
|
|
|
655
703
|
// src/search/providers.ts
|
|
656
704
|
function parseDimension(raw) {
|
|
@@ -750,6 +798,12 @@ function getConfiguredEmbeddingProviderId(opts) {
|
|
|
750
798
|
return null;
|
|
751
799
|
}
|
|
752
800
|
}
|
|
801
|
+
var init_providers = __esm({
|
|
802
|
+
"src/search/providers.ts"() {
|
|
803
|
+
"use strict";
|
|
804
|
+
init_embedding();
|
|
805
|
+
}
|
|
806
|
+
});
|
|
753
807
|
|
|
754
808
|
// src/search/vector.ts
|
|
755
809
|
function encodeVector(vector) {
|
|
@@ -862,27 +916,18 @@ function searchByVector(db, queryVector, opts) {
|
|
|
862
916
|
provider_id: row.provider_id
|
|
863
917
|
}));
|
|
864
918
|
}
|
|
919
|
+
var init_vector = __esm({
|
|
920
|
+
"src/search/vector.ts"() {
|
|
921
|
+
"use strict";
|
|
922
|
+
init_db();
|
|
923
|
+
}
|
|
924
|
+
});
|
|
865
925
|
|
|
866
926
|
// src/core/memory.ts
|
|
927
|
+
import { createHash as createHash2 } from "crypto";
|
|
867
928
|
function contentHash(content) {
|
|
868
929
|
return createHash2("sha256").update(content.trim()).digest("hex").slice(0, 16);
|
|
869
930
|
}
|
|
870
|
-
var TYPE_PRIORITY = {
|
|
871
|
-
identity: 0,
|
|
872
|
-
emotion: 1,
|
|
873
|
-
knowledge: 2,
|
|
874
|
-
event: 3
|
|
875
|
-
};
|
|
876
|
-
var PRIORITY_STABILITY = {
|
|
877
|
-
0: Infinity,
|
|
878
|
-
// P0: never decays
|
|
879
|
-
1: 365,
|
|
880
|
-
// P1: 365-day half-life
|
|
881
|
-
2: 90,
|
|
882
|
-
// P2: 90-day half-life
|
|
883
|
-
3: 14
|
|
884
|
-
// P3: 14-day half-life
|
|
885
|
-
};
|
|
886
931
|
function resolveEmbeddingProviderId(explicitProviderId) {
|
|
887
932
|
if (explicitProviderId !== void 0) {
|
|
888
933
|
return explicitProviderId;
|
|
@@ -909,8 +954,8 @@ function createMemory(db, input) {
|
|
|
909
954
|
const timestamp = now();
|
|
910
955
|
db.prepare(
|
|
911
956
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
912
|
-
access_count, created_at, updated_at, source, agent_id, hash)
|
|
913
|
-
VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?)`
|
|
957
|
+
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
|
|
958
|
+
VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?)`
|
|
914
959
|
).run(
|
|
915
960
|
id,
|
|
916
961
|
input.content,
|
|
@@ -922,7 +967,8 @@ function createMemory(db, input) {
|
|
|
922
967
|
timestamp,
|
|
923
968
|
input.source ?? null,
|
|
924
969
|
agentId,
|
|
925
|
-
hash
|
|
970
|
+
hash,
|
|
971
|
+
input.emotion_tag ?? null
|
|
926
972
|
);
|
|
927
973
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
928
974
|
markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
@@ -966,6 +1012,10 @@ function updateMemory(db, id, input) {
|
|
|
966
1012
|
fields.push("source = ?");
|
|
967
1013
|
values.push(input.source);
|
|
968
1014
|
}
|
|
1015
|
+
if (input.emotion_tag !== void 0) {
|
|
1016
|
+
fields.push("emotion_tag = ?");
|
|
1017
|
+
values.push(input.emotion_tag);
|
|
1018
|
+
}
|
|
969
1019
|
fields.push("updated_at = ?");
|
|
970
1020
|
values.push(now());
|
|
971
1021
|
values.push(id);
|
|
@@ -1007,6 +1057,10 @@ function listMemories(db, opts) {
|
|
|
1007
1057
|
conditions.push("vitality >= ?");
|
|
1008
1058
|
params.push(opts.min_vitality);
|
|
1009
1059
|
}
|
|
1060
|
+
if (opts?.emotion_tag) {
|
|
1061
|
+
conditions.push("emotion_tag = ?");
|
|
1062
|
+
params.push(opts.emotion_tag);
|
|
1063
|
+
}
|
|
1010
1064
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1011
1065
|
const limit = opts?.limit ?? 100;
|
|
1012
1066
|
const offset = opts?.offset ?? 0;
|
|
@@ -1031,72 +1085,34 @@ function countMemories(db, agent_id = "default") {
|
|
|
1031
1085
|
by_priority: Object.fromEntries(byPriority.map((r) => [`P${r.priority}`, r.c]))
|
|
1032
1086
|
};
|
|
1033
1087
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
for (const m of emotions) {
|
|
1059
|
-
sections.push(`- ${m.content}
|
|
1060
|
-
`);
|
|
1061
|
-
exported++;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (knowledge.length) {
|
|
1065
|
-
sections.push("\n## Knowledge\n");
|
|
1066
|
-
for (const m of knowledge) {
|
|
1067
|
-
sections.push(`- ${m.content}
|
|
1068
|
-
`);
|
|
1069
|
-
exported++;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
const memoryPath = join(dirPath, "MEMORY.md");
|
|
1073
|
-
writeFileSync(memoryPath, sections.join("\n"));
|
|
1074
|
-
files.push(memoryPath);
|
|
1075
|
-
}
|
|
1076
|
-
const events = listMemories(db, { agent_id: agentId, type: "event", limit: 1e4 });
|
|
1077
|
-
const byDate = /* @__PURE__ */ new Map();
|
|
1078
|
-
for (const ev of events) {
|
|
1079
|
-
const date = ev.created_at.slice(0, 10);
|
|
1080
|
-
if (!byDate.has(date)) byDate.set(date, []);
|
|
1081
|
-
byDate.get(date).push(ev);
|
|
1082
|
-
}
|
|
1083
|
-
for (const [date, mems] of byDate) {
|
|
1084
|
-
const lines = [`# ${date}
|
|
1085
|
-
`];
|
|
1086
|
-
for (const m of mems) {
|
|
1087
|
-
lines.push(`- ${m.content}
|
|
1088
|
-
`);
|
|
1089
|
-
exported++;
|
|
1090
|
-
}
|
|
1091
|
-
const filePath = join(dirPath, `${date}.md`);
|
|
1092
|
-
writeFileSync(filePath, lines.join("\n"));
|
|
1093
|
-
files.push(filePath);
|
|
1088
|
+
var TYPE_PRIORITY, PRIORITY_STABILITY;
|
|
1089
|
+
var init_memory = __esm({
|
|
1090
|
+
"src/core/memory.ts"() {
|
|
1091
|
+
"use strict";
|
|
1092
|
+
init_db();
|
|
1093
|
+
init_tokenizer();
|
|
1094
|
+
init_providers();
|
|
1095
|
+
init_vector();
|
|
1096
|
+
TYPE_PRIORITY = {
|
|
1097
|
+
identity: 0,
|
|
1098
|
+
emotion: 1,
|
|
1099
|
+
knowledge: 2,
|
|
1100
|
+
event: 3
|
|
1101
|
+
};
|
|
1102
|
+
PRIORITY_STABILITY = {
|
|
1103
|
+
0: Infinity,
|
|
1104
|
+
// P0: never decays
|
|
1105
|
+
1: 365,
|
|
1106
|
+
// P1: 365-day half-life
|
|
1107
|
+
2: 90,
|
|
1108
|
+
// P2: 90-day half-life
|
|
1109
|
+
3: 14
|
|
1110
|
+
// P3: 14-day half-life
|
|
1111
|
+
};
|
|
1094
1112
|
}
|
|
1095
|
-
|
|
1096
|
-
}
|
|
1113
|
+
});
|
|
1097
1114
|
|
|
1098
1115
|
// src/core/path.ts
|
|
1099
|
-
var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
|
|
1100
1116
|
function parseUri(uri) {
|
|
1101
1117
|
const match = uri.match(/^([a-z]+):\/\/(.+)$/);
|
|
1102
1118
|
if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
|
|
@@ -1130,10 +1146,85 @@ function getPath(db, id) {
|
|
|
1130
1146
|
function getPathByUri(db, uri, agent_id = "default") {
|
|
1131
1147
|
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
|
|
1132
1148
|
}
|
|
1149
|
+
var DEFAULT_DOMAINS;
|
|
1150
|
+
var init_path = __esm({
|
|
1151
|
+
"src/core/path.ts"() {
|
|
1152
|
+
"use strict";
|
|
1153
|
+
init_db();
|
|
1154
|
+
DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1133
1157
|
|
|
1134
1158
|
// src/sleep/boot.ts
|
|
1159
|
+
var boot_exports = {};
|
|
1160
|
+
__export(boot_exports, {
|
|
1161
|
+
boot: () => boot,
|
|
1162
|
+
formatNarrativeBoot: () => formatNarrativeBoot,
|
|
1163
|
+
formatRelativeDate: () => formatRelativeDate,
|
|
1164
|
+
loadWarmBootLayers: () => loadWarmBootLayers
|
|
1165
|
+
});
|
|
1166
|
+
function formatRelativeDate(isoDate) {
|
|
1167
|
+
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
1168
|
+
const diffDays = Math.floor(diffMs / 864e5);
|
|
1169
|
+
if (diffDays <= 0) return "\u4ECA\u5929";
|
|
1170
|
+
if (diffDays === 1) return "\u6628\u5929";
|
|
1171
|
+
if (diffDays <= 7) return `${diffDays}\u5929\u524D`;
|
|
1172
|
+
return isoDate.slice(0, 10);
|
|
1173
|
+
}
|
|
1174
|
+
function loadWarmBootLayers(db, agentId) {
|
|
1175
|
+
const identity = listMemories(db, { agent_id: agentId, type: "identity", limit: 50 });
|
|
1176
|
+
const emotion = listMemories(db, { agent_id: agentId, type: "emotion", limit: 5 });
|
|
1177
|
+
const event = listMemories(db, { agent_id: agentId, type: "event", limit: 7 });
|
|
1178
|
+
const knowledge = listMemories(db, {
|
|
1179
|
+
agent_id: agentId,
|
|
1180
|
+
type: "knowledge",
|
|
1181
|
+
min_vitality: 0.5,
|
|
1182
|
+
limit: 10
|
|
1183
|
+
});
|
|
1184
|
+
return { identity, emotion, event, knowledge };
|
|
1185
|
+
}
|
|
1186
|
+
function formatNarrativeBoot(layers, agentName) {
|
|
1187
|
+
const lines = [];
|
|
1188
|
+
lines.push(`# ${agentName}\u7684\u56DE\u5FC6`);
|
|
1189
|
+
lines.push("");
|
|
1190
|
+
if (layers.identity.length > 0) {
|
|
1191
|
+
lines.push("## \u6211\u662F\u8C01");
|
|
1192
|
+
for (const mem of layers.identity) {
|
|
1193
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
|
|
1194
|
+
}
|
|
1195
|
+
lines.push("");
|
|
1196
|
+
}
|
|
1197
|
+
if (layers.emotion.length > 0) {
|
|
1198
|
+
lines.push("## \u6700\u8FD1\u7684\u5FC3\u60C5");
|
|
1199
|
+
for (const mem of layers.emotion) {
|
|
1200
|
+
const tag = mem.emotion_tag;
|
|
1201
|
+
const time = formatRelativeDate(mem.updated_at);
|
|
1202
|
+
const tagStr = tag ? `${tag}, ${time}` : time;
|
|
1203
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${tagStr})`);
|
|
1204
|
+
}
|
|
1205
|
+
lines.push("");
|
|
1206
|
+
}
|
|
1207
|
+
if (layers.event.length > 0) {
|
|
1208
|
+
lines.push("## \u6700\u8FD1\u53D1\u751F\u7684\u4E8B");
|
|
1209
|
+
for (const mem of layers.event) {
|
|
1210
|
+
const time = formatRelativeDate(mem.updated_at);
|
|
1211
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${time})`);
|
|
1212
|
+
}
|
|
1213
|
+
lines.push("");
|
|
1214
|
+
}
|
|
1215
|
+
if (layers.knowledge.length > 0) {
|
|
1216
|
+
lines.push("## \u8FD8\u8BB0\u5F97\u7684\u77E5\u8BC6");
|
|
1217
|
+
for (const mem of layers.knowledge) {
|
|
1218
|
+
lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
|
|
1219
|
+
}
|
|
1220
|
+
lines.push("");
|
|
1221
|
+
}
|
|
1222
|
+
return lines.join("\n");
|
|
1223
|
+
}
|
|
1135
1224
|
function boot(db, opts) {
|
|
1136
1225
|
const agentId = opts?.agent_id ?? "default";
|
|
1226
|
+
const format = opts?.format ?? "json";
|
|
1227
|
+
const agentName = opts?.agent_name ?? "Agent";
|
|
1137
1228
|
const corePaths = opts?.corePaths ?? [
|
|
1138
1229
|
"core://agent",
|
|
1139
1230
|
"core://user",
|
|
@@ -1177,17 +1268,105 @@ function boot(db, opts) {
|
|
|
1177
1268
|
}
|
|
1178
1269
|
}
|
|
1179
1270
|
}
|
|
1180
|
-
|
|
1271
|
+
const result = {
|
|
1181
1272
|
identityMemories: [...memories.values()],
|
|
1182
1273
|
bootPaths
|
|
1183
1274
|
};
|
|
1275
|
+
if (format === "narrative") {
|
|
1276
|
+
const layers = loadWarmBootLayers(db, agentId);
|
|
1277
|
+
result.layers = layers;
|
|
1278
|
+
result.narrative = formatNarrativeBoot(layers, agentName);
|
|
1279
|
+
}
|
|
1280
|
+
return result;
|
|
1184
1281
|
}
|
|
1282
|
+
var init_boot = __esm({
|
|
1283
|
+
"src/sleep/boot.ts"() {
|
|
1284
|
+
"use strict";
|
|
1285
|
+
init_path();
|
|
1286
|
+
init_memory();
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1185
1289
|
|
|
1186
|
-
// src/
|
|
1187
|
-
|
|
1188
|
-
import
|
|
1290
|
+
// src/bin/agent-memory.ts
|
|
1291
|
+
init_db();
|
|
1292
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1293
|
+
import { basename, resolve } from "path";
|
|
1294
|
+
|
|
1295
|
+
// src/core/export.ts
|
|
1296
|
+
init_memory();
|
|
1297
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1298
|
+
import { join } from "path";
|
|
1299
|
+
function exportMemories(db, dirPath, opts) {
|
|
1300
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1301
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
|
|
1302
|
+
let exported = 0;
|
|
1303
|
+
const files = [];
|
|
1304
|
+
const identities = listMemories(db, { agent_id: agentId, type: "identity" });
|
|
1305
|
+
const knowledge = listMemories(db, { agent_id: agentId, type: "knowledge" });
|
|
1306
|
+
const emotions = listMemories(db, { agent_id: agentId, type: "emotion" });
|
|
1307
|
+
if (identities.length || knowledge.length || emotions.length) {
|
|
1308
|
+
const sections = ["# Agent Memory Export\n"];
|
|
1309
|
+
if (identities.length) {
|
|
1310
|
+
sections.push("## Identity\n");
|
|
1311
|
+
for (const m of identities) {
|
|
1312
|
+
sections.push(`- ${m.content}
|
|
1313
|
+
`);
|
|
1314
|
+
exported++;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (emotions.length) {
|
|
1318
|
+
sections.push("\n## Emotions\n");
|
|
1319
|
+
for (const m of emotions) {
|
|
1320
|
+
sections.push(`- ${m.content}
|
|
1321
|
+
`);
|
|
1322
|
+
exported++;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (knowledge.length) {
|
|
1326
|
+
sections.push("\n## Knowledge\n");
|
|
1327
|
+
for (const m of knowledge) {
|
|
1328
|
+
sections.push(`- ${m.content}
|
|
1329
|
+
`);
|
|
1330
|
+
exported++;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const memoryPath = join(dirPath, "MEMORY.md");
|
|
1334
|
+
writeFileSync(memoryPath, sections.join("\n"));
|
|
1335
|
+
files.push(memoryPath);
|
|
1336
|
+
}
|
|
1337
|
+
const events = listMemories(db, { agent_id: agentId, type: "event", limit: 1e4 });
|
|
1338
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
1339
|
+
for (const ev of events) {
|
|
1340
|
+
const date = ev.created_at.slice(0, 10);
|
|
1341
|
+
if (!byDate.has(date)) byDate.set(date, []);
|
|
1342
|
+
byDate.get(date).push(ev);
|
|
1343
|
+
}
|
|
1344
|
+
for (const [date, mems] of byDate) {
|
|
1345
|
+
const lines = [`# ${date}
|
|
1346
|
+
`];
|
|
1347
|
+
for (const m of mems) {
|
|
1348
|
+
lines.push(`- ${m.content}
|
|
1349
|
+
`);
|
|
1350
|
+
exported++;
|
|
1351
|
+
}
|
|
1352
|
+
const filePath = join(dirPath, `${date}.md`);
|
|
1353
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
1354
|
+
files.push(filePath);
|
|
1355
|
+
}
|
|
1356
|
+
return { exported, files };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/bin/agent-memory.ts
|
|
1360
|
+
init_boot();
|
|
1361
|
+
|
|
1362
|
+
// src/app/surface.ts
|
|
1363
|
+
init_memory();
|
|
1364
|
+
|
|
1365
|
+
// src/search/hybrid.ts
|
|
1366
|
+
init_memory();
|
|
1189
1367
|
|
|
1190
1368
|
// src/search/bm25.ts
|
|
1369
|
+
init_tokenizer();
|
|
1191
1370
|
function searchBM25(db, query, opts) {
|
|
1192
1371
|
const limit = opts?.limit ?? 20;
|
|
1193
1372
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -1258,6 +1437,8 @@ function rebuildBm25Index(db, opts) {
|
|
|
1258
1437
|
}
|
|
1259
1438
|
|
|
1260
1439
|
// src/search/hybrid.ts
|
|
1440
|
+
init_providers();
|
|
1441
|
+
init_vector();
|
|
1261
1442
|
var PRIORITY_WEIGHT = {
|
|
1262
1443
|
0: 4,
|
|
1263
1444
|
1: 3,
|
|
@@ -1451,714 +1632,742 @@ async function reindexEmbeddings(db, opts) {
|
|
|
1451
1632
|
};
|
|
1452
1633
|
}
|
|
1453
1634
|
|
|
1454
|
-
// src/
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
-
};
|
|
1476
|
-
}
|
|
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);
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1635
|
+
// src/app/surface.ts
|
|
1636
|
+
init_providers();
|
|
1637
|
+
init_tokenizer();
|
|
1638
|
+
init_vector();
|
|
1529
1639
|
|
|
1530
|
-
// src/
|
|
1531
|
-
|
|
1532
|
-
var MERGE_THRESHOLD = 0.82;
|
|
1640
|
+
// src/app/feedback.ts
|
|
1641
|
+
init_db();
|
|
1533
1642
|
function clamp01(value) {
|
|
1534
1643
|
if (!Number.isFinite(value)) return 0;
|
|
1535
1644
|
return Math.max(0, Math.min(1, value));
|
|
1536
1645
|
}
|
|
1537
|
-
function
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1646
|
+
function recordFeedbackEvent(db, input) {
|
|
1647
|
+
const id = newId();
|
|
1648
|
+
const created_at = now();
|
|
1649
|
+
const agentId = input.agent_id ?? "default";
|
|
1650
|
+
const useful = input.useful ? 1 : 0;
|
|
1651
|
+
const value = input.useful ? 1 : 0;
|
|
1652
|
+
const eventType = `${input.source}:${input.useful ? "useful" : "not_useful"}`;
|
|
1653
|
+
const exists = db.prepare("SELECT id FROM memories WHERE id = ?").get(input.memory_id);
|
|
1654
|
+
if (!exists) {
|
|
1655
|
+
throw new Error(`Memory not found: ${input.memory_id}`);
|
|
1547
1656
|
}
|
|
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
1657
|
try {
|
|
1557
|
-
|
|
1658
|
+
db.prepare(
|
|
1659
|
+
`INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
|
|
1660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1661
|
+
).run(id, input.memory_id, input.source, useful, agentId, eventType, value, created_at);
|
|
1558
1662
|
} catch {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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;
|
|
1663
|
+
db.prepare(
|
|
1664
|
+
`INSERT INTO feedback_events (id, memory_id, event_type, value, created_at)
|
|
1665
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1666
|
+
).run(id, input.memory_id, eventType, value, created_at);
|
|
1581
1667
|
}
|
|
1582
|
-
return
|
|
1668
|
+
return {
|
|
1669
|
+
id,
|
|
1670
|
+
memory_id: input.memory_id,
|
|
1671
|
+
source: input.source,
|
|
1672
|
+
useful: input.useful,
|
|
1673
|
+
agent_id: agentId,
|
|
1674
|
+
created_at,
|
|
1675
|
+
value
|
|
1676
|
+
};
|
|
1583
1677
|
}
|
|
1584
|
-
function
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1678
|
+
function getFeedbackSummary(db, memoryId, agentId) {
|
|
1679
|
+
try {
|
|
1680
|
+
const row = db.prepare(
|
|
1681
|
+
`SELECT COUNT(*) as total,
|
|
1682
|
+
COALESCE(SUM(CASE WHEN useful = 1 THEN 1 ELSE 0 END), 0) as useful,
|
|
1683
|
+
COALESCE(SUM(CASE WHEN useful = 0 THEN 1 ELSE 0 END), 0) as not_useful
|
|
1684
|
+
FROM feedback_events
|
|
1685
|
+
WHERE memory_id = ?
|
|
1686
|
+
AND (? IS NULL OR agent_id = ?)`
|
|
1687
|
+
).get(memoryId, agentId ?? null, agentId ?? null);
|
|
1688
|
+
if (!row || row.total === 0) {
|
|
1689
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
1593
1690
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1691
|
+
return {
|
|
1692
|
+
total: row.total,
|
|
1693
|
+
useful: row.useful,
|
|
1694
|
+
not_useful: row.not_useful,
|
|
1695
|
+
score: clamp01(row.useful / row.total)
|
|
1696
|
+
};
|
|
1697
|
+
} catch {
|
|
1698
|
+
const row = db.prepare(
|
|
1699
|
+
`SELECT COUNT(*) as total,
|
|
1700
|
+
COALESCE(SUM(CASE WHEN value >= 0.5 THEN 1 ELSE 0 END), 0) as useful,
|
|
1701
|
+
COALESCE(SUM(CASE WHEN value < 0.5 THEN 1 ELSE 0 END), 0) as not_useful,
|
|
1702
|
+
COALESCE(AVG(value), 0.5) as avg_value
|
|
1703
|
+
FROM feedback_events
|
|
1704
|
+
WHERE memory_id = ?`
|
|
1705
|
+
).get(memoryId);
|
|
1706
|
+
if (!row || row.total === 0) {
|
|
1707
|
+
return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
|
|
1599
1708
|
}
|
|
1709
|
+
return {
|
|
1710
|
+
total: row.total,
|
|
1711
|
+
useful: row.useful,
|
|
1712
|
+
not_useful: row.not_useful,
|
|
1713
|
+
score: clamp01(row.avg_value)
|
|
1714
|
+
};
|
|
1600
1715
|
}
|
|
1601
|
-
return null;
|
|
1602
1716
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1717
|
+
|
|
1718
|
+
// src/app/surface.ts
|
|
1719
|
+
var INTENT_PRIORS = {
|
|
1720
|
+
factual: {
|
|
1721
|
+
identity: 0.25,
|
|
1722
|
+
emotion: 0.15,
|
|
1723
|
+
knowledge: 1,
|
|
1724
|
+
event: 0.8
|
|
1725
|
+
},
|
|
1726
|
+
preference: {
|
|
1727
|
+
identity: 1,
|
|
1728
|
+
emotion: 0.85,
|
|
1729
|
+
knowledge: 0.55,
|
|
1730
|
+
event: 0.25
|
|
1731
|
+
},
|
|
1732
|
+
temporal: {
|
|
1733
|
+
identity: 0.15,
|
|
1734
|
+
emotion: 0.35,
|
|
1735
|
+
knowledge: 0.5,
|
|
1736
|
+
event: 1
|
|
1737
|
+
},
|
|
1738
|
+
planning: {
|
|
1739
|
+
identity: 0.65,
|
|
1740
|
+
emotion: 0.2,
|
|
1741
|
+
knowledge: 1,
|
|
1742
|
+
event: 0.6
|
|
1743
|
+
},
|
|
1744
|
+
design: {
|
|
1745
|
+
identity: 0.8,
|
|
1746
|
+
emotion: 0.35,
|
|
1747
|
+
knowledge: 1,
|
|
1748
|
+
event: 0.25
|
|
1611
1749
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1750
|
+
};
|
|
1751
|
+
var DESIGN_HINT_RE = /\b(ui|ux|design|style|component|layout|brand|palette|theme)\b|风格|界面|设计|配色|低饱和|玻璃拟态|渐变/i;
|
|
1752
|
+
var PLANNING_HINT_RE = /\b(plan|planning|todo|next|ship|build|implement|roadmap|task|milestone)\b|计划|下一步|待办|实现|重构/i;
|
|
1753
|
+
var FACTUAL_HINT_RE = /\b(what|fact|constraint|rule|docs|document|api|status)\b|规则|约束|文档|接口|事实/i;
|
|
1754
|
+
var TEMPORAL_HINT_RE = /\b(today|yesterday|tomorrow|recent|before|after|when|timeline)\b|今天|昨天|明天|最近|时间线|何时/i;
|
|
1755
|
+
var PREFERENCE_HINT_RE = /\b(prefer|preference|like|dislike|avoid|favorite)\b|喜欢|偏好|不喜欢|避免|讨厌/i;
|
|
1756
|
+
function clamp012(value) {
|
|
1757
|
+
if (!Number.isFinite(value)) return 0;
|
|
1758
|
+
return Math.max(0, Math.min(1, value));
|
|
1614
1759
|
}
|
|
1615
|
-
function
|
|
1616
|
-
|
|
1617
|
-
|
|
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
|
|
1760
|
+
function uniqueTokenSet(values) {
|
|
1761
|
+
return new Set(
|
|
1762
|
+
values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
|
|
1626
1763
|
);
|
|
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
|
-
};
|
|
1635
1764
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
provider,
|
|
1644
|
-
recordAccess: false
|
|
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);
|
|
1765
|
+
function overlapScore(left, right) {
|
|
1766
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
1767
|
+
let shared = 0;
|
|
1768
|
+
for (const token of left) {
|
|
1769
|
+
if (right.has(token)) shared += 1;
|
|
1770
|
+
}
|
|
1771
|
+
return clamp012(shared / Math.max(left.size, right.size));
|
|
1655
1772
|
}
|
|
1656
|
-
function
|
|
1657
|
-
|
|
1658
|
-
|
|
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)");
|
|
1684
|
-
return {
|
|
1685
|
-
pass: failed.length === 0,
|
|
1686
|
-
scores: { specificity, novelty, relevance, coherence },
|
|
1687
|
-
failedCriteria: failed
|
|
1688
|
-
};
|
|
1773
|
+
function rankScore(rank, window) {
|
|
1774
|
+
if (!rank) return 0;
|
|
1775
|
+
return clamp012(1 - (rank - 1) / Math.max(window, 1));
|
|
1689
1776
|
}
|
|
1690
|
-
|
|
1691
|
-
const
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1777
|
+
function topicLabel(...parts) {
|
|
1778
|
+
const token = parts.flatMap((part) => tokenize(part ?? "")).find((value) => value.trim().length > 1);
|
|
1779
|
+
const label = (token ?? "context").replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
1780
|
+
return label || "context";
|
|
1781
|
+
}
|
|
1782
|
+
function intentKeywordBoost(memory, intent) {
|
|
1783
|
+
const content = memory.content;
|
|
1784
|
+
switch (intent) {
|
|
1785
|
+
case "design":
|
|
1786
|
+
return DESIGN_HINT_RE.test(content) ? 1 : 0.65;
|
|
1787
|
+
case "planning":
|
|
1788
|
+
return PLANNING_HINT_RE.test(content) ? 1 : 0.7;
|
|
1789
|
+
case "factual":
|
|
1790
|
+
return FACTUAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
1791
|
+
case "temporal":
|
|
1792
|
+
return TEMPORAL_HINT_RE.test(content) ? 1 : 0.75;
|
|
1793
|
+
case "preference":
|
|
1794
|
+
return PREFERENCE_HINT_RE.test(content) ? 1 : 0.8;
|
|
1696
1795
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1796
|
+
}
|
|
1797
|
+
function intentMatch(memory, intent) {
|
|
1798
|
+
if (!intent) return 0;
|
|
1799
|
+
const prior = INTENT_PRIORS[intent][memory.type] ?? 0;
|
|
1800
|
+
return clamp012(prior * intentKeywordBoost(memory, intent));
|
|
1801
|
+
}
|
|
1802
|
+
function buildReasonCodes(input) {
|
|
1803
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
1804
|
+
reasons.add(`type:${input.memory.type}`);
|
|
1805
|
+
if (input.semanticScore > 0.2) {
|
|
1806
|
+
reasons.add(`semantic:${topicLabel(input.query, input.task)}`);
|
|
1707
1807
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
1808
|
+
if (input.lexicalScore > 0.2 && input.query) {
|
|
1809
|
+
reasons.add(`lexical:${topicLabel(input.query)}`);
|
|
1711
1810
|
}
|
|
1712
|
-
if (input.
|
|
1713
|
-
|
|
1811
|
+
if (input.taskMatch > 0.2) {
|
|
1812
|
+
reasons.add(`task:${topicLabel(input.task, input.intent)}`);
|
|
1714
1813
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
if (!best) {
|
|
1718
|
-
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
1814
|
+
if (input.intent) {
|
|
1815
|
+
reasons.add(`intent:${input.intent}`);
|
|
1719
1816
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
-
};
|
|
1817
|
+
if (input.feedbackScore >= 0.67) {
|
|
1818
|
+
reasons.add("feedback:reinforced");
|
|
1819
|
+
} else if (input.feedbackScore <= 0.33) {
|
|
1820
|
+
reasons.add("feedback:negative");
|
|
1729
1821
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1822
|
+
return [...reasons];
|
|
1823
|
+
}
|
|
1824
|
+
function collectBranch(signals, rows, key, similarity) {
|
|
1825
|
+
for (const row of rows) {
|
|
1826
|
+
const existing = signals.get(row.memory.id) ?? { memory: row.memory };
|
|
1827
|
+
const currentRank = existing[key];
|
|
1828
|
+
if (currentRank === void 0 || row.rank < currentRank) {
|
|
1829
|
+
existing[key] = row.rank;
|
|
1830
|
+
}
|
|
1831
|
+
if (similarity) {
|
|
1832
|
+
const currentSimilarity = similarity.get(row.memory.id);
|
|
1833
|
+
if (currentSimilarity !== void 0) {
|
|
1834
|
+
existing.semanticSimilarity = Math.max(existing.semanticSimilarity ?? 0, currentSimilarity);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
signals.set(row.memory.id, existing);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async function surfaceMemories(db, input) {
|
|
1841
|
+
const agentId = input.agent_id ?? "default";
|
|
1842
|
+
const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
|
|
1843
|
+
const lexicalWindow = Math.max(24, limit * 6);
|
|
1844
|
+
const minVitality = input.min_vitality ?? 0.05;
|
|
1845
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
1846
|
+
const signals = /* @__PURE__ */ new Map();
|
|
1847
|
+
const trimmedQuery = input.query?.trim();
|
|
1848
|
+
const trimmedTask = input.task?.trim();
|
|
1849
|
+
const recentTurns = (input.recent_turns ?? []).map((turn) => turn.trim()).filter(Boolean).slice(-4);
|
|
1850
|
+
const queryTokens = uniqueTokenSet([trimmedQuery, ...recentTurns]);
|
|
1851
|
+
const taskTokens = uniqueTokenSet([trimmedTask]);
|
|
1852
|
+
if (trimmedQuery) {
|
|
1853
|
+
collectBranch(
|
|
1854
|
+
signals,
|
|
1855
|
+
searchBM25(db, trimmedQuery, {
|
|
1856
|
+
agent_id: agentId,
|
|
1857
|
+
limit: lexicalWindow,
|
|
1858
|
+
min_vitality: minVitality
|
|
1859
|
+
}),
|
|
1860
|
+
"queryRank"
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
if (trimmedTask) {
|
|
1864
|
+
collectBranch(
|
|
1865
|
+
signals,
|
|
1866
|
+
searchBM25(db, trimmedTask, {
|
|
1867
|
+
agent_id: agentId,
|
|
1868
|
+
limit: lexicalWindow,
|
|
1869
|
+
min_vitality: minVitality
|
|
1870
|
+
}),
|
|
1871
|
+
"taskRank"
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
if (recentTurns.length > 0) {
|
|
1875
|
+
collectBranch(
|
|
1876
|
+
signals,
|
|
1877
|
+
searchBM25(db, recentTurns.join(" "), {
|
|
1878
|
+
agent_id: agentId,
|
|
1879
|
+
limit: lexicalWindow,
|
|
1880
|
+
min_vitality: minVitality
|
|
1881
|
+
}),
|
|
1882
|
+
"recentRank"
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
const semanticQuery = [trimmedQuery, trimmedTask, ...recentTurns].filter(Boolean).join("\n").trim();
|
|
1886
|
+
if (provider && semanticQuery) {
|
|
1887
|
+
try {
|
|
1888
|
+
const [queryVector] = await provider.embed([semanticQuery]);
|
|
1889
|
+
if (queryVector) {
|
|
1890
|
+
const vectorRows = searchByVector(db, queryVector, {
|
|
1891
|
+
providerId: provider.id,
|
|
1892
|
+
agent_id: agentId,
|
|
1893
|
+
limit: lexicalWindow,
|
|
1894
|
+
min_vitality: minVitality
|
|
1895
|
+
});
|
|
1896
|
+
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
1897
|
+
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
1737
1898
|
}
|
|
1738
|
-
}
|
|
1899
|
+
} catch {
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const fallbackMemories = listMemories(db, {
|
|
1903
|
+
agent_id: agentId,
|
|
1904
|
+
min_vitality: minVitality,
|
|
1905
|
+
limit: Math.max(48, lexicalWindow)
|
|
1906
|
+
});
|
|
1907
|
+
for (const memory of fallbackMemories) {
|
|
1908
|
+
if (!signals.has(memory.id)) {
|
|
1909
|
+
signals.set(memory.id, { memory });
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).filter((memory) => input.emotion_tag ? memory.emotion_tag === input.emotion_tag : true).map((memory) => {
|
|
1913
|
+
const signal = signals.get(memory.id) ?? { memory };
|
|
1914
|
+
const memoryTokens = new Set(tokenize(memory.content));
|
|
1915
|
+
const lexicalOverlap = overlapScore(memoryTokens, queryTokens);
|
|
1916
|
+
const taskOverlap = overlapScore(memoryTokens, taskTokens);
|
|
1917
|
+
const lexicalScore = clamp012(
|
|
1918
|
+
0.45 * rankScore(signal.queryRank, lexicalWindow) + 0.15 * rankScore(signal.recentRank, lexicalWindow) + 0.15 * rankScore(signal.taskRank, lexicalWindow) + 0.25 * lexicalOverlap
|
|
1919
|
+
);
|
|
1920
|
+
const semanticScore = signal.semanticSimilarity !== void 0 ? clamp012(Math.max(signal.semanticSimilarity, lexicalOverlap * 0.7)) : trimmedQuery || recentTurns.length > 0 ? clamp012(lexicalOverlap * 0.7) : 0;
|
|
1921
|
+
const intentScore = intentMatch(memory, input.intent);
|
|
1922
|
+
const taskMatch = trimmedTask ? clamp012(0.7 * taskOverlap + 0.3 * intentScore) : intentScore;
|
|
1923
|
+
const priorityScore = priorityPrior(memory.priority);
|
|
1924
|
+
const feedbackSummary = getFeedbackSummary(db, memory.id, agentId);
|
|
1925
|
+
const feedbackScore = feedbackSummary.score;
|
|
1926
|
+
const score = clamp012(
|
|
1927
|
+
0.35 * semanticScore + 0.2 * lexicalScore + 0.15 * taskMatch + 0.1 * memory.vitality + 0.1 * priorityScore + 0.1 * feedbackScore
|
|
1928
|
+
);
|
|
1739
1929
|
return {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1930
|
+
memory,
|
|
1931
|
+
score,
|
|
1932
|
+
semantic_score: semanticScore,
|
|
1933
|
+
lexical_score: lexicalScore,
|
|
1934
|
+
task_match: taskMatch,
|
|
1935
|
+
vitality: memory.vitality,
|
|
1936
|
+
priority_prior: priorityScore,
|
|
1937
|
+
feedback_score: feedbackScore,
|
|
1938
|
+
feedback_summary: feedbackSummary,
|
|
1939
|
+
reason_codes: buildReasonCodes({
|
|
1940
|
+
memory,
|
|
1941
|
+
query: semanticQuery || trimmedQuery,
|
|
1942
|
+
task: trimmedTask,
|
|
1943
|
+
intent: input.intent,
|
|
1944
|
+
semanticScore,
|
|
1945
|
+
lexicalScore,
|
|
1946
|
+
taskMatch,
|
|
1947
|
+
feedbackScore
|
|
1948
|
+
}),
|
|
1949
|
+
lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
|
|
1950
|
+
semantic_rank: signal.semanticRank,
|
|
1951
|
+
semantic_similarity: signal.semanticSimilarity
|
|
1746
1952
|
};
|
|
1747
|
-
}
|
|
1953
|
+
}).sort((left, right) => {
|
|
1954
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
1955
|
+
if (right.semantic_score !== left.semantic_score) return right.semantic_score - left.semantic_score;
|
|
1956
|
+
if (right.lexical_score !== left.lexical_score) return right.lexical_score - left.lexical_score;
|
|
1957
|
+
if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
|
|
1958
|
+
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
1959
|
+
}).slice(0, limit);
|
|
1748
1960
|
return {
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1961
|
+
count: results.length,
|
|
1962
|
+
query: trimmedQuery,
|
|
1963
|
+
task: trimmedTask,
|
|
1964
|
+
intent: input.intent,
|
|
1965
|
+
results
|
|
1752
1966
|
};
|
|
1753
1967
|
}
|
|
1754
1968
|
|
|
1969
|
+
// src/transports/http.ts
|
|
1970
|
+
init_db();
|
|
1971
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1972
|
+
import http from "http";
|
|
1973
|
+
|
|
1755
1974
|
// src/sleep/sync.ts
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1975
|
+
init_memory();
|
|
1976
|
+
init_path();
|
|
1977
|
+
|
|
1978
|
+
// src/core/guard.ts
|
|
1979
|
+
init_providers();
|
|
1980
|
+
init_tokenizer();
|
|
1981
|
+
init_path();
|
|
1982
|
+
|
|
1983
|
+
// src/core/merge.ts
|
|
1984
|
+
function uniqueNonEmpty(values) {
|
|
1985
|
+
return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))];
|
|
1763
1986
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
-
}
|
|
1802
|
-
}
|
|
1987
|
+
function splitClauses(content) {
|
|
1988
|
+
return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
|
|
1803
1989
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
-
});
|
|
1990
|
+
function mergeAliases(existing, incoming, content) {
|
|
1991
|
+
const aliases = uniqueNonEmpty([
|
|
1992
|
+
existing !== content ? existing : void 0,
|
|
1993
|
+
incoming !== content ? incoming : void 0
|
|
1994
|
+
]);
|
|
1995
|
+
return aliases.length > 0 ? aliases : void 0;
|
|
1818
1996
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
vectorLimit: input.vectorLimit,
|
|
1828
|
-
provider: input.provider,
|
|
1829
|
-
recordAccess: input.recordAccess
|
|
1830
|
-
});
|
|
1997
|
+
function replaceIdentity(context) {
|
|
1998
|
+
const content = context.incoming.content.trim();
|
|
1999
|
+
return {
|
|
2000
|
+
strategy: "replace",
|
|
2001
|
+
content,
|
|
2002
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2003
|
+
notes: ["identity canonicalized to the newest authoritative phrasing"]
|
|
2004
|
+
};
|
|
1831
2005
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2006
|
+
function appendEmotionEvidence(context) {
|
|
2007
|
+
const lines = uniqueNonEmpty([
|
|
2008
|
+
...context.existing.content.split(/\n+/),
|
|
2009
|
+
context.incoming.content
|
|
2010
|
+
]);
|
|
2011
|
+
const content = lines.length <= 1 ? lines[0] ?? context.incoming.content.trim() : [lines[0], "", ...lines.slice(1).map((line) => `- ${line.replace(/^-\s*/, "")}`)].join("\n");
|
|
2012
|
+
return {
|
|
2013
|
+
strategy: "append_evidence",
|
|
2014
|
+
content,
|
|
2015
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2016
|
+
notes: ["emotion evidence appended to preserve timeline without duplicating identical lines"]
|
|
2017
|
+
};
|
|
1837
2018
|
}
|
|
1838
|
-
function
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
const
|
|
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}`);
|
|
1848
|
-
}
|
|
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);
|
|
1859
|
-
}
|
|
2019
|
+
function synthesizeKnowledge(context) {
|
|
2020
|
+
const clauses = uniqueNonEmpty([
|
|
2021
|
+
...splitClauses(context.existing.content),
|
|
2022
|
+
...splitClauses(context.incoming.content)
|
|
2023
|
+
]);
|
|
2024
|
+
const content = clauses.length <= 1 ? clauses[0] ?? context.incoming.content.trim() : clauses.join("\uFF1B");
|
|
1860
2025
|
return {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
agent_id: agentId,
|
|
1866
|
-
created_at,
|
|
1867
|
-
value
|
|
2026
|
+
strategy: "synthesize",
|
|
2027
|
+
content,
|
|
2028
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2029
|
+
notes: ["knowledge statements synthesized into a canonical summary"]
|
|
1868
2030
|
};
|
|
1869
2031
|
}
|
|
1870
|
-
function
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
-
};
|
|
2032
|
+
function compactEventTimeline(context) {
|
|
2033
|
+
const points = uniqueNonEmpty([
|
|
2034
|
+
...context.existing.content.split(/\n+/),
|
|
2035
|
+
context.incoming.content
|
|
2036
|
+
]).map((line) => line.replace(/^-\s*/, ""));
|
|
2037
|
+
const content = points.length <= 1 ? points[0] ?? context.incoming.content.trim() : ["Timeline:", ...points.map((line) => `- ${line}`)].join("\n");
|
|
2038
|
+
return {
|
|
2039
|
+
strategy: "compact_timeline",
|
|
2040
|
+
content,
|
|
2041
|
+
aliases: mergeAliases(context.existing.content, context.incoming.content, content),
|
|
2042
|
+
notes: ["event observations compacted into a single timeline window"]
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function buildMergePlan(context) {
|
|
2046
|
+
const type = context.incoming.type ?? context.existing.type;
|
|
2047
|
+
switch (type) {
|
|
2048
|
+
case "identity":
|
|
2049
|
+
return replaceIdentity(context);
|
|
2050
|
+
case "emotion":
|
|
2051
|
+
return appendEmotionEvidence(context);
|
|
2052
|
+
case "knowledge":
|
|
2053
|
+
return synthesizeKnowledge(context);
|
|
2054
|
+
case "event":
|
|
2055
|
+
return compactEventTimeline(context);
|
|
1907
2056
|
}
|
|
1908
2057
|
}
|
|
1909
2058
|
|
|
1910
|
-
// src/
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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;
|
|
2059
|
+
// src/core/guard.ts
|
|
2060
|
+
init_memory();
|
|
2061
|
+
var NEAR_EXACT_THRESHOLD = 0.93;
|
|
2062
|
+
var MERGE_THRESHOLD = 0.82;
|
|
1948
2063
|
function clamp013(value) {
|
|
1949
2064
|
if (!Number.isFinite(value)) return 0;
|
|
1950
2065
|
return Math.max(0, Math.min(1, value));
|
|
1951
2066
|
}
|
|
1952
|
-
function uniqueTokenSet2(
|
|
1953
|
-
return new Set(
|
|
1954
|
-
values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
|
|
1955
|
-
);
|
|
2067
|
+
function uniqueTokenSet2(text) {
|
|
2068
|
+
return new Set(tokenize(text));
|
|
1956
2069
|
}
|
|
1957
2070
|
function overlapScore2(left, right) {
|
|
1958
|
-
|
|
2071
|
+
const a = new Set(left);
|
|
2072
|
+
const b = new Set(right);
|
|
2073
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
1959
2074
|
let shared = 0;
|
|
1960
|
-
for (const token of
|
|
1961
|
-
if (
|
|
2075
|
+
for (const token of a) {
|
|
2076
|
+
if (b.has(token)) shared += 1;
|
|
1962
2077
|
}
|
|
1963
|
-
return
|
|
2078
|
+
return shared / Math.max(a.size, b.size);
|
|
1964
2079
|
}
|
|
1965
|
-
function
|
|
1966
|
-
|
|
1967
|
-
return
|
|
2080
|
+
function extractEntities(text) {
|
|
2081
|
+
const matches = text.match(/[A-Z][A-Za-z0-9_-]+|\d+(?:[-/:]\d+)*|[#@][\w-]+|[\u4e00-\u9fff]{2,}|\w+:\/\/[^\s]+/g) ?? [];
|
|
2082
|
+
return new Set(matches.map((value) => value.trim()).filter(Boolean));
|
|
1968
2083
|
}
|
|
1969
|
-
function
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2084
|
+
function safeDomain(uri) {
|
|
2085
|
+
if (!uri) return null;
|
|
2086
|
+
try {
|
|
2087
|
+
return parseUri(uri).domain;
|
|
2088
|
+
} catch {
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
1973
2091
|
}
|
|
1974
|
-
function
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2092
|
+
function getPrimaryUri(db, memoryId, agentId) {
|
|
2093
|
+
const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
|
|
2094
|
+
return row?.uri ?? null;
|
|
2095
|
+
}
|
|
2096
|
+
function uriScopeMatch(inputUri, candidateUri) {
|
|
2097
|
+
if (inputUri && candidateUri) {
|
|
2098
|
+
if (inputUri === candidateUri) return 1;
|
|
2099
|
+
const inputDomain2 = safeDomain(inputUri);
|
|
2100
|
+
const candidateDomain2 = safeDomain(candidateUri);
|
|
2101
|
+
if (inputDomain2 && candidateDomain2 && inputDomain2 === candidateDomain2) return 0.85;
|
|
2102
|
+
return 0;
|
|
2103
|
+
}
|
|
2104
|
+
if (!inputUri && !candidateUri) {
|
|
2105
|
+
return 0.65;
|
|
2106
|
+
}
|
|
2107
|
+
const inputDomain = safeDomain(inputUri ?? null);
|
|
2108
|
+
const candidateDomain = safeDomain(candidateUri ?? null);
|
|
2109
|
+
if (inputDomain && candidateDomain && inputDomain === candidateDomain) {
|
|
2110
|
+
return 0.75;
|
|
1987
2111
|
}
|
|
2112
|
+
return 0.2;
|
|
1988
2113
|
}
|
|
1989
|
-
function
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2114
|
+
function extractObservedAt(parts, fallback) {
|
|
2115
|
+
for (const part of parts) {
|
|
2116
|
+
if (!part) continue;
|
|
2117
|
+
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
2118
|
+
if (!match) continue;
|
|
2119
|
+
const iso = match[2] ? `${match[1]}T${match[2]}Z` : `${match[1]}T00:00:00Z`;
|
|
2120
|
+
const parsed = new Date(iso);
|
|
2121
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
2122
|
+
return parsed;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (fallback) {
|
|
2126
|
+
const parsed = new Date(fallback);
|
|
2127
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
2128
|
+
return parsed;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return null;
|
|
1993
2132
|
}
|
|
1994
|
-
function
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2133
|
+
function timeProximity(input, memory, candidateUri) {
|
|
2134
|
+
if (input.type !== "event") {
|
|
2135
|
+
return 1;
|
|
2136
|
+
}
|
|
2137
|
+
const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
|
|
2138
|
+
const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
|
|
2139
|
+
if (!inputTime || !existingTime) {
|
|
2140
|
+
return 0.5;
|
|
2141
|
+
}
|
|
2142
|
+
const diffDays = Math.abs(inputTime.getTime() - existingTime.getTime()) / (1e3 * 60 * 60 * 24);
|
|
2143
|
+
return clamp013(1 - diffDays / 7);
|
|
2144
|
+
}
|
|
2145
|
+
function scoreCandidate(input, candidate, candidateUri) {
|
|
2146
|
+
const lexicalOverlap = overlapScore2(uniqueTokenSet2(input.content), uniqueTokenSet2(candidate.memory.content));
|
|
2147
|
+
const entityOverlap = Math.max(
|
|
2148
|
+
overlapScore2(extractEntities(input.content), extractEntities(candidate.memory.content)),
|
|
2149
|
+
lexicalOverlap * 0.75
|
|
2150
|
+
);
|
|
2151
|
+
const uriMatch = uriScopeMatch(input.uri, candidateUri);
|
|
2152
|
+
const temporal = timeProximity(input, candidate.memory, candidateUri);
|
|
2153
|
+
const semantic = clamp013(candidate.vector_score ?? lexicalOverlap);
|
|
2154
|
+
const dedupScore = clamp013(
|
|
2155
|
+
0.5 * semantic + 0.2 * lexicalOverlap + 0.15 * uriMatch + 0.1 * entityOverlap + 0.05 * temporal
|
|
2156
|
+
);
|
|
2157
|
+
return {
|
|
2158
|
+
semantic_similarity: semantic,
|
|
2159
|
+
lexical_overlap: lexicalOverlap,
|
|
2160
|
+
uri_scope_match: uriMatch,
|
|
2161
|
+
entity_overlap: entityOverlap,
|
|
2162
|
+
time_proximity: temporal,
|
|
2163
|
+
dedup_score: dedupScore
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
async function recallCandidates(db, input, agentId) {
|
|
2167
|
+
const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
|
|
2168
|
+
const response = await recallMemories(db, input.content, {
|
|
2169
|
+
agent_id: agentId,
|
|
2170
|
+
limit: Math.max(6, input.candidateLimit ?? 8),
|
|
2171
|
+
lexicalLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
2172
|
+
vectorLimit: Math.max(8, input.candidateLimit ?? 8),
|
|
2173
|
+
provider,
|
|
2174
|
+
recordAccess: false
|
|
2175
|
+
});
|
|
2176
|
+
return response.results.filter((row) => row.memory.type === input.type).map((row) => {
|
|
2177
|
+
const uri = getPrimaryUri(db, row.memory.id, agentId);
|
|
2178
|
+
return {
|
|
2179
|
+
result: row,
|
|
2180
|
+
uri,
|
|
2181
|
+
domain: safeDomain(uri),
|
|
2182
|
+
score: scoreCandidate(input, row, uri)
|
|
2183
|
+
};
|
|
2184
|
+
}).sort((left, right) => right.score.dedup_score - left.score.dedup_score);
|
|
2185
|
+
}
|
|
2186
|
+
function fourCriterionGate(input) {
|
|
2187
|
+
const content = input.content.trim();
|
|
2188
|
+
const failed = [];
|
|
2189
|
+
const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
|
|
2190
|
+
const minLength = priority <= 1 ? 4 : 8;
|
|
2191
|
+
const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
|
|
2192
|
+
if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
|
|
2193
|
+
const tokens = tokenize(content);
|
|
2194
|
+
const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
|
|
2195
|
+
if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
|
|
2196
|
+
const hasCJK = /[\u4e00-\u9fff]/.test(content);
|
|
2197
|
+
const hasCapitalized = /[A-Z][a-z]+/.test(content);
|
|
2198
|
+
const hasNumbers = /\d+/.test(content);
|
|
2199
|
+
const hasURI = /\w+:\/\//.test(content);
|
|
2200
|
+
const hasEntityMarkers = /[@#]/.test(content);
|
|
2201
|
+
const hasMeaningfulLength = content.length >= 15;
|
|
2202
|
+
const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
|
|
2203
|
+
const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
|
|
2204
|
+
if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
|
|
2205
|
+
const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
|
|
2206
|
+
const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
|
|
2207
|
+
const excessiveRepetition = /(.)\1{9,}/.test(content);
|
|
2208
|
+
let coherence = 1;
|
|
2209
|
+
if (allCaps) coherence -= 0.5;
|
|
2210
|
+
if (!hasWhitespaceOrPunctuation) coherence -= 0.3;
|
|
2211
|
+
if (excessiveRepetition) coherence -= 0.5;
|
|
2212
|
+
coherence = Math.max(0, coherence);
|
|
2213
|
+
if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
|
|
2214
|
+
return {
|
|
2215
|
+
pass: failed.length === 0,
|
|
2216
|
+
scores: { specificity, novelty, relevance, coherence },
|
|
2217
|
+
failedCriteria: failed
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
async function guard(db, input) {
|
|
2221
|
+
const hash = contentHash(input.content);
|
|
2222
|
+
const agentId = input.agent_id ?? "default";
|
|
2223
|
+
const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
|
|
2224
|
+
if (exactMatch) {
|
|
2225
|
+
return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
|
|
2226
|
+
}
|
|
2227
|
+
if (input.uri) {
|
|
2228
|
+
const existingPath = getPathByUri(db, input.uri, agentId);
|
|
2229
|
+
if (existingPath) {
|
|
2230
|
+
return {
|
|
2231
|
+
action: "update",
|
|
2232
|
+
reason: `URI ${input.uri} already exists, updating canonical content`,
|
|
2233
|
+
existingId: existingPath.memory_id,
|
|
2234
|
+
updatedContent: input.content
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
const gateResult = fourCriterionGate(input);
|
|
2239
|
+
if (!gateResult.pass) {
|
|
2240
|
+
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
2241
|
+
}
|
|
2242
|
+
if (input.conservative) {
|
|
2243
|
+
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
1999
2244
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2245
|
+
const candidates = await recallCandidates(db, input, agentId);
|
|
2246
|
+
const best = candidates[0];
|
|
2247
|
+
if (!best) {
|
|
2248
|
+
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
2002
2249
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2250
|
+
const score = best.score;
|
|
2251
|
+
if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
|
|
2252
|
+
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
2253
|
+
return {
|
|
2254
|
+
action: shouldUpdateMetadata ? "update" : "skip",
|
|
2255
|
+
reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
|
|
2256
|
+
existingId: best.result.memory.id,
|
|
2257
|
+
score
|
|
2258
|
+
};
|
|
2005
2259
|
}
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2260
|
+
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
2261
|
+
const mergePlan = buildMergePlan({
|
|
2262
|
+
existing: best.result.memory,
|
|
2263
|
+
incoming: {
|
|
2264
|
+
content: input.content,
|
|
2265
|
+
type: input.type,
|
|
2266
|
+
source: input.source
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
return {
|
|
2270
|
+
action: "merge",
|
|
2271
|
+
reason: `Semantic near-duplicate detected (score=${score.dedup_score.toFixed(3)}), applying ${mergePlan.strategy}`,
|
|
2272
|
+
existingId: best.result.memory.id,
|
|
2273
|
+
mergedContent: mergePlan.content,
|
|
2274
|
+
mergePlan,
|
|
2275
|
+
score
|
|
2276
|
+
};
|
|
2008
2277
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2278
|
+
return {
|
|
2279
|
+
action: "add",
|
|
2280
|
+
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
2281
|
+
score
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// src/sleep/sync.ts
|
|
2286
|
+
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
2287
|
+
if (!uri) return;
|
|
2288
|
+
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
2289
|
+
try {
|
|
2290
|
+
createPath(db, memoryId, uri, void 0, void 0, agentId);
|
|
2291
|
+
} catch {
|
|
2013
2292
|
}
|
|
2014
|
-
return [...reasons];
|
|
2015
2293
|
}
|
|
2016
|
-
function
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2294
|
+
async function syncOne(db, input) {
|
|
2295
|
+
const memInput = {
|
|
2296
|
+
content: input.content,
|
|
2297
|
+
type: input.type ?? "event",
|
|
2298
|
+
priority: input.priority,
|
|
2299
|
+
emotion_val: input.emotion_val,
|
|
2300
|
+
source: input.source,
|
|
2301
|
+
agent_id: input.agent_id,
|
|
2302
|
+
uri: input.uri,
|
|
2303
|
+
provider: input.provider,
|
|
2304
|
+
conservative: input.conservative,
|
|
2305
|
+
emotion_tag: input.emotion_tag
|
|
2306
|
+
};
|
|
2307
|
+
const guardResult = await guard(db, memInput);
|
|
2308
|
+
switch (guardResult.action) {
|
|
2309
|
+
case "skip":
|
|
2310
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
2311
|
+
case "add": {
|
|
2312
|
+
const mem = createMemory(db, memInput);
|
|
2313
|
+
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
2314
|
+
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
2315
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason };
|
|
2022
2316
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
if (
|
|
2026
|
-
|
|
2317
|
+
case "update": {
|
|
2318
|
+
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
2319
|
+
if (guardResult.updatedContent !== void 0) {
|
|
2320
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
2027
2321
|
}
|
|
2322
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2323
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2028
2324
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
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);
|
|
2325
|
+
case "merge": {
|
|
2326
|
+
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
2327
|
+
return { action: "skipped", reason: "Missing merge data" };
|
|
2090
2328
|
}
|
|
2091
|
-
|
|
2329
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
2330
|
+
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2331
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2092
2332
|
}
|
|
2093
2333
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// src/app/remember.ts
|
|
2337
|
+
async function rememberMemory(db, input) {
|
|
2338
|
+
return syncOne(db, {
|
|
2339
|
+
content: input.content,
|
|
2340
|
+
type: input.type,
|
|
2341
|
+
priority: input.priority,
|
|
2342
|
+
emotion_val: input.emotion_val,
|
|
2343
|
+
uri: input.uri,
|
|
2344
|
+
source: input.source,
|
|
2345
|
+
agent_id: input.agent_id,
|
|
2346
|
+
provider: input.provider,
|
|
2347
|
+
conservative: input.conservative,
|
|
2348
|
+
emotion_tag: input.emotion_tag
|
|
2098
2349
|
});
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/app/recall.ts
|
|
2353
|
+
async function recallMemory(db, input) {
|
|
2354
|
+
const result = await recallMemories(db, input.query, {
|
|
2355
|
+
agent_id: input.agent_id,
|
|
2356
|
+
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
2357
|
+
min_vitality: input.min_vitality,
|
|
2358
|
+
lexicalLimit: input.lexicalLimit,
|
|
2359
|
+
vectorLimit: input.vectorLimit,
|
|
2360
|
+
provider: input.provider,
|
|
2361
|
+
recordAccess: input.recordAccess
|
|
2362
|
+
});
|
|
2363
|
+
if (input.emotion_tag) {
|
|
2364
|
+
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
2103
2365
|
}
|
|
2104
|
-
|
|
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);
|
|
2152
|
-
return {
|
|
2153
|
-
count: results.length,
|
|
2154
|
-
query: trimmedQuery,
|
|
2155
|
-
task: trimmedTask,
|
|
2156
|
-
intent: input.intent,
|
|
2157
|
-
results
|
|
2158
|
-
};
|
|
2366
|
+
return result;
|
|
2159
2367
|
}
|
|
2160
2368
|
|
|
2161
2369
|
// src/sleep/decay.ts
|
|
2370
|
+
init_db();
|
|
2162
2371
|
var MIN_VITALITY = {
|
|
2163
2372
|
0: 1,
|
|
2164
2373
|
// P0: identity — never decays
|
|
@@ -2219,6 +2428,7 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
2219
2428
|
}
|
|
2220
2429
|
|
|
2221
2430
|
// src/sleep/tidy.ts
|
|
2431
|
+
init_memory();
|
|
2222
2432
|
function runTidy(db, opts) {
|
|
2223
2433
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
2224
2434
|
const agentId = opts?.agent_id;
|
|
@@ -2235,6 +2445,8 @@ function runTidy(db, opts) {
|
|
|
2235
2445
|
}
|
|
2236
2446
|
|
|
2237
2447
|
// src/sleep/govern.ts
|
|
2448
|
+
init_memory();
|
|
2449
|
+
init_tokenizer();
|
|
2238
2450
|
function clamp014(value) {
|
|
2239
2451
|
if (!Number.isFinite(value)) return 0;
|
|
2240
2452
|
return Math.max(0, Math.min(1, value));
|
|
@@ -2335,6 +2547,7 @@ function runGovern(db, opts) {
|
|
|
2335
2547
|
}
|
|
2336
2548
|
|
|
2337
2549
|
// src/sleep/jobs.ts
|
|
2550
|
+
init_db();
|
|
2338
2551
|
function parseCheckpoint(raw) {
|
|
2339
2552
|
if (!raw) return null;
|
|
2340
2553
|
try {
|
|
@@ -2560,6 +2773,7 @@ async function reflectMemories(db, input) {
|
|
|
2560
2773
|
}
|
|
2561
2774
|
|
|
2562
2775
|
// src/app/status.ts
|
|
2776
|
+
init_memory();
|
|
2563
2777
|
function getMemoryStatus(db, input) {
|
|
2564
2778
|
const agentId = input?.agent_id ?? "default";
|
|
2565
2779
|
const stats = countMemories(db, agentId);
|
|
@@ -3062,6 +3276,7 @@ async function startHttpServer(options) {
|
|
|
3062
3276
|
}
|
|
3063
3277
|
|
|
3064
3278
|
// src/bin/agent-memory.ts
|
|
3279
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
3065
3280
|
var args = process.argv.slice(2);
|
|
3066
3281
|
var command = args[0];
|
|
3067
3282
|
function getDbPath() {
|
|
@@ -3079,9 +3294,10 @@ Usage: agent-memory <command> [options]
|
|
|
3079
3294
|
Commands:
|
|
3080
3295
|
init Create database
|
|
3081
3296
|
db:migrate Run schema migrations (no-op if up-to-date)
|
|
3082
|
-
remember <content> [--uri X] [--type T] Store a memory
|
|
3083
|
-
recall <query> [--limit N]
|
|
3084
|
-
boot
|
|
3297
|
+
remember <content> [--uri X] [--type T] [--emotion-tag TAG] Store a memory
|
|
3298
|
+
recall <query> [--limit N] [--emotion-tag TAG] Search memories (hybrid retrieval)
|
|
3299
|
+
boot [--format json|narrative] [--agent-name NAME] Load identity memories
|
|
3300
|
+
surface [--out FILE] [--days N] [--limit N] Export recent memories as Markdown
|
|
3085
3301
|
status Show statistics
|
|
3086
3302
|
reflect [decay|tidy|govern|all] Run sleep cycle
|
|
3087
3303
|
reindex [--full] [--batch-size N] Rebuild FTS index and embeddings (if configured)
|
|
@@ -3144,12 +3360,14 @@ async function main() {
|
|
|
3144
3360
|
const db = openDatabase({ path: getDbPath() });
|
|
3145
3361
|
const uri = getFlag("--uri");
|
|
3146
3362
|
const type = getFlag("--type") ?? "knowledge";
|
|
3363
|
+
const emotionTag = getFlag("--emotion-tag");
|
|
3147
3364
|
const result = await rememberMemory(db, {
|
|
3148
3365
|
content,
|
|
3149
3366
|
type,
|
|
3150
3367
|
uri,
|
|
3151
3368
|
source: "manual",
|
|
3152
|
-
agent_id: getAgentId()
|
|
3369
|
+
agent_id: getAgentId(),
|
|
3370
|
+
emotion_tag: emotionTag
|
|
3153
3371
|
});
|
|
3154
3372
|
console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
|
|
3155
3373
|
db.close();
|
|
@@ -3162,10 +3380,12 @@ async function main() {
|
|
|
3162
3380
|
process.exit(1);
|
|
3163
3381
|
}
|
|
3164
3382
|
const db = openDatabase({ path: getDbPath() });
|
|
3383
|
+
const emotionTag = getFlag("--emotion-tag");
|
|
3165
3384
|
const result = await recallMemory(db, {
|
|
3166
3385
|
query,
|
|
3167
3386
|
agent_id: getAgentId(),
|
|
3168
|
-
limit: Number.parseInt(getFlag("--limit") ?? "10", 10)
|
|
3387
|
+
limit: Number.parseInt(getFlag("--limit") ?? "10", 10),
|
|
3388
|
+
emotion_tag: emotionTag
|
|
3169
3389
|
});
|
|
3170
3390
|
console.log(`\u{1F50D} Results: ${result.results.length} (${result.mode})
|
|
3171
3391
|
`);
|
|
@@ -3183,15 +3403,77 @@ async function main() {
|
|
|
3183
3403
|
}
|
|
3184
3404
|
case "boot": {
|
|
3185
3405
|
const db = openDatabase({ path: getDbPath() });
|
|
3186
|
-
const
|
|
3187
|
-
|
|
3406
|
+
const format = getFlag("--format") ?? "narrative";
|
|
3407
|
+
const agentName = getFlag("--agent-name") ?? "Agent";
|
|
3408
|
+
const result = boot(db, { agent_id: getAgentId(), format, agent_name: agentName });
|
|
3409
|
+
if (format === "narrative" && result.narrative) {
|
|
3410
|
+
console.log(result.narrative);
|
|
3411
|
+
} else {
|
|
3412
|
+
console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
|
|
3188
3413
|
`);
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3414
|
+
for (const memory of result.identityMemories) {
|
|
3415
|
+
console.log(` \u{1F534} ${memory.content.slice(0, 100)}`);
|
|
3416
|
+
}
|
|
3417
|
+
if (result.bootPaths.length) {
|
|
3418
|
+
console.log(`
|
|
3194
3419
|
\u{1F4CD} Boot paths: ${result.bootPaths.join(", ")}`);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
db.close();
|
|
3423
|
+
break;
|
|
3424
|
+
}
|
|
3425
|
+
case "surface": {
|
|
3426
|
+
const db = openDatabase({ path: getDbPath() });
|
|
3427
|
+
const days = Number.parseInt(getFlag("--days") ?? "7", 10);
|
|
3428
|
+
const limit = Number.parseInt(getFlag("--limit") ?? "50", 10);
|
|
3429
|
+
const minVitality = Number.parseFloat(getFlag("--min-vitality") ?? "0.1");
|
|
3430
|
+
const outFile = getFlag("--out");
|
|
3431
|
+
const typesRaw = getFlag("--types");
|
|
3432
|
+
const types = typesRaw ? typesRaw.split(",").map((t) => t.trim()) : void 0;
|
|
3433
|
+
const cutoff = new Date(Date.now() - days * 864e5).toISOString();
|
|
3434
|
+
const surfaceResult = await surfaceMemories(db, {
|
|
3435
|
+
task: "context loading",
|
|
3436
|
+
intent: "temporal",
|
|
3437
|
+
agent_id: getAgentId(),
|
|
3438
|
+
types,
|
|
3439
|
+
limit: Math.max(limit, 100),
|
|
3440
|
+
// fetch more, filter by date after
|
|
3441
|
+
min_vitality: minVitality
|
|
3442
|
+
});
|
|
3443
|
+
const filtered = surfaceResult.results.filter((r) => r.memory.updated_at >= cutoff).slice(0, limit);
|
|
3444
|
+
const grouped = {};
|
|
3445
|
+
for (const r of filtered) {
|
|
3446
|
+
const t = r.memory.type;
|
|
3447
|
+
if (!grouped[t]) grouped[t] = [];
|
|
3448
|
+
grouped[t].push(r);
|
|
3449
|
+
}
|
|
3450
|
+
const { formatRelativeDate: formatRelativeDate2 } = await Promise.resolve().then(() => (init_boot(), boot_exports));
|
|
3451
|
+
const lines = [];
|
|
3452
|
+
lines.push("# Recent Memories");
|
|
3453
|
+
lines.push("");
|
|
3454
|
+
lines.push(`> Auto-generated by AgentMemory surface. Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
3455
|
+
lines.push("");
|
|
3456
|
+
const typeOrder = ["identity", "emotion", "knowledge", "event"];
|
|
3457
|
+
const typeLabels = { identity: "Identity", emotion: "Emotion", knowledge: "Knowledge", event: "Events" };
|
|
3458
|
+
for (const t of typeOrder) {
|
|
3459
|
+
const items = grouped[t];
|
|
3460
|
+
if (!items?.length) continue;
|
|
3461
|
+
lines.push(`## ${typeLabels[t]}`);
|
|
3462
|
+
for (const item of items) {
|
|
3463
|
+
const content = item.memory.content.split("\n")[0].slice(0, 200);
|
|
3464
|
+
const time = formatRelativeDate2(item.memory.updated_at);
|
|
3465
|
+
const tag = item.memory.emotion_tag;
|
|
3466
|
+
const meta = t === "emotion" && tag ? `(${tag}, ${time})` : `(${time})`;
|
|
3467
|
+
lines.push(`- ${content} ${meta}`);
|
|
3468
|
+
}
|
|
3469
|
+
lines.push("");
|
|
3470
|
+
}
|
|
3471
|
+
const markdown = lines.join("\n");
|
|
3472
|
+
if (outFile) {
|
|
3473
|
+
writeFileSync2(outFile, markdown, "utf-8");
|
|
3474
|
+
console.log(`\u2705 Surface: ${filtered.length} memories written to ${outFile}`);
|
|
3475
|
+
} else {
|
|
3476
|
+
console.log(markdown);
|
|
3195
3477
|
}
|
|
3196
3478
|
db.close();
|
|
3197
3479
|
break;
|