linksee-memory 0.11.5 → 0.12.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/README.md +1007 -1004
- package/dist/db/migrate.js +28 -28
- package/dist/db/schema.sql +461 -461
- package/dist/lib/drift-anchors.js +3 -3
- package/dist/lib/drift-detection.js +20 -20
- package/dist/lib/drift-view.js +16 -16
- package/dist/lib/guard.js +13 -3
- package/dist/lib/map-import.js +17 -17
- package/dist/lib/truth-engine.d.ts +16 -0
- package/dist/lib/truth-engine.js +89 -3
- package/dist/mcp/prompts.js +58 -58
- package/dist/mcp/resources.js +7 -7
- package/dist/mcp/sampling.js +20 -20
- package/dist/mcp/server.js +197 -135
- package/package.json +96 -95
package/dist/mcp/server.js
CHANGED
|
@@ -174,6 +174,7 @@ const TOOLS = [
|
|
|
174
174
|
path: { type: 'string', description: 'File path or substring. When set, returns file edit history with per-edit user-intent context instead of memory search.' },
|
|
175
175
|
max_intents: { type: 'number', description: 'For file mode: max user-intent snippets. Default 10.', default: 10 },
|
|
176
176
|
scope_to_roots: { type: 'boolean', default: false, description: 'For file mode: filter to client-provided roots.' },
|
|
177
|
+
explain: { type: 'boolean', default: false, description: 'Include ranking internals (composite, heat, band, momentum, match_reasons, score_breakdown). Off by default so more memories fit the token budget.' },
|
|
177
178
|
kind: { type: 'string', enum: ['person', 'company', 'project', 'concept', 'file', 'other'], description: 'For overview mode: filter by entity kind.' },
|
|
178
179
|
min_memories: { type: 'number', description: 'For overview mode: minimum memory count. Default 1.', default: 1 },
|
|
179
180
|
},
|
|
@@ -200,6 +201,7 @@ const TOOLS = [
|
|
|
200
201
|
properties: {
|
|
201
202
|
domain: { type: 'string', description: 'Filter by domain (strategy, product, engineering, growth, etc.)' },
|
|
202
203
|
decision_mode: { type: 'string', description: 'Filter by decision_mode (hypothesis, constraint, commitment, source_of_truth)' },
|
|
204
|
+
verbose: { type: 'boolean', default: false, description: 'Return full aligned nodes and all candidates. Default is compact: attention items in full, aligned as id+statement per domain, candidates as counts.' },
|
|
203
205
|
},
|
|
204
206
|
},
|
|
205
207
|
},
|
|
@@ -477,15 +479,15 @@ function appendAxisFilters(sql, params, filters) {
|
|
|
477
479
|
return sql;
|
|
478
480
|
}
|
|
479
481
|
function runFtsQuery(query, layer, limit, axis) {
|
|
480
|
-
let sql = `
|
|
481
|
-
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
482
|
-
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
483
|
-
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
484
|
-
bm25(memories_fts) as bm25_score
|
|
485
|
-
FROM memories_fts
|
|
486
|
-
JOIN memories m ON m.id = memories_fts.rowid
|
|
487
|
-
JOIN entities e ON e.id = m.entity_id
|
|
488
|
-
WHERE memories_fts MATCH ?
|
|
482
|
+
let sql = `
|
|
483
|
+
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
484
|
+
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
485
|
+
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
486
|
+
bm25(memories_fts) as bm25_score
|
|
487
|
+
FROM memories_fts
|
|
488
|
+
JOIN memories m ON m.id = memories_fts.rowid
|
|
489
|
+
JOIN entities e ON e.id = m.entity_id
|
|
490
|
+
WHERE memories_fts MATCH ?
|
|
489
491
|
`;
|
|
490
492
|
const params = [query];
|
|
491
493
|
if (layer) {
|
|
@@ -499,14 +501,14 @@ function runFtsQuery(query, layer, limit, axis) {
|
|
|
499
501
|
return db.prepare(sql).all(...params);
|
|
500
502
|
}
|
|
501
503
|
function runLikeQuery(query, entityName, layer, limit, axis) {
|
|
502
|
-
let sql = `
|
|
503
|
-
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
504
|
-
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
505
|
-
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
506
|
-
0 as bm25_score
|
|
507
|
-
FROM memories m
|
|
508
|
-
JOIN entities e ON e.id = m.entity_id
|
|
509
|
-
WHERE 1=1
|
|
504
|
+
let sql = `
|
|
505
|
+
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
506
|
+
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
507
|
+
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
508
|
+
0 as bm25_score
|
|
509
|
+
FROM memories m
|
|
510
|
+
JOIN entities e ON e.id = m.entity_id
|
|
511
|
+
WHERE 1=1
|
|
510
512
|
`;
|
|
511
513
|
const params = [];
|
|
512
514
|
if (entityName) {
|
|
@@ -767,12 +769,18 @@ function handleRecall(args) {
|
|
|
767
769
|
content: parsedContent,
|
|
768
770
|
importance: r.importance,
|
|
769
771
|
pinned: r.importance >= 0.9,
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
772
|
+
// Ranking internals only on request — they roughly double the per-memory cost and an
|
|
773
|
+
// agent acting on the memory needs what/why/state, not the scorer's arithmetic.
|
|
774
|
+
...(args?.explain ? {
|
|
775
|
+
heat: Number(r.heat_score.toFixed(1)),
|
|
776
|
+
band: r.heat_band,
|
|
777
|
+
composite: Number(r.composite_score.toFixed(3)),
|
|
778
|
+
match_reasons: r._reasons,
|
|
779
|
+
score_breakdown: r._breakdown,
|
|
780
|
+
} : {}),
|
|
775
781
|
};
|
|
782
|
+
if (!args?.explain)
|
|
783
|
+
delete memObj.entity.momentum;
|
|
776
784
|
const memTokens = estimateTokens(JSON.stringify(memObj));
|
|
777
785
|
if (memoriesOut.length > 0 && accTokens + memTokens > maxTokens) {
|
|
778
786
|
stoppedBy = 'tokens';
|
|
@@ -831,8 +839,8 @@ function handleForget(args) {
|
|
|
831
839
|
}
|
|
832
840
|
// Auto-sweep — also respect pin (importance >= 0.9) as protection
|
|
833
841
|
const rows = db
|
|
834
|
-
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
835
|
-
FROM memories
|
|
842
|
+
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
843
|
+
FROM memories
|
|
836
844
|
WHERE protected = 0 AND importance < 0.9`)
|
|
837
845
|
.all();
|
|
838
846
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -881,17 +889,17 @@ function handleConsolidate(args) {
|
|
|
881
889
|
// read-only audit: count candidates using the same rules.
|
|
882
890
|
const now = Math.floor(Date.now() / 1000);
|
|
883
891
|
const ageCutoff = now - (args.min_age_days ?? 7) * 86400;
|
|
884
|
-
const candidates = db.prepare(`
|
|
885
|
-
SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
|
|
886
|
-
FROM memories m
|
|
887
|
-
JOIN entities e ON e.id = m.entity_id
|
|
888
|
-
WHERE m.protected = 0
|
|
889
|
-
AND m.importance < 0.9
|
|
890
|
-
AND m.layer IN ('context', 'emotion', 'implementation')
|
|
891
|
-
AND m.created_at <= ?
|
|
892
|
-
GROUP BY m.entity_id, m.layer
|
|
893
|
-
HAVING c >= 2
|
|
894
|
-
ORDER BY c DESC
|
|
892
|
+
const candidates = db.prepare(`
|
|
893
|
+
SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
|
|
894
|
+
FROM memories m
|
|
895
|
+
JOIN entities e ON e.id = m.entity_id
|
|
896
|
+
WHERE m.protected = 0
|
|
897
|
+
AND m.importance < 0.9
|
|
898
|
+
AND m.layer IN ('context', 'emotion', 'implementation')
|
|
899
|
+
AND m.created_at <= ?
|
|
900
|
+
GROUP BY m.entity_id, m.layer
|
|
901
|
+
HAVING c >= 2
|
|
902
|
+
ORDER BY c DESC
|
|
895
903
|
`).all(ageCutoff);
|
|
896
904
|
const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
|
|
897
905
|
return JSON.stringify({
|
|
@@ -965,19 +973,19 @@ function handleListEntities(args) {
|
|
|
965
973
|
const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
|
|
966
974
|
const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
|
|
967
975
|
const offset = Math.max(0, Number(args?.offset ?? 0));
|
|
968
|
-
let sql = `
|
|
969
|
-
SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
|
|
970
|
-
e.updated_at, e.created_at,
|
|
971
|
-
COUNT(m.id) as memory_count,
|
|
972
|
-
MAX(m.last_accessed_at) as last_memory_access,
|
|
973
|
-
SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
|
|
974
|
-
SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
|
|
975
|
-
SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
|
|
976
|
-
SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
|
|
977
|
-
SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
|
|
978
|
-
FROM entities e
|
|
979
|
-
LEFT JOIN memories m ON m.entity_id = e.id
|
|
980
|
-
WHERE 1=1
|
|
976
|
+
let sql = `
|
|
977
|
+
SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
|
|
978
|
+
e.updated_at, e.created_at,
|
|
979
|
+
COUNT(m.id) as memory_count,
|
|
980
|
+
MAX(m.last_accessed_at) as last_memory_access,
|
|
981
|
+
SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
|
|
982
|
+
SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
|
|
983
|
+
SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
|
|
984
|
+
SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
|
|
985
|
+
SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
|
|
986
|
+
FROM entities e
|
|
987
|
+
LEFT JOIN memories m ON m.entity_id = e.id
|
|
988
|
+
WHERE 1=1
|
|
981
989
|
`;
|
|
982
990
|
const params = [];
|
|
983
991
|
if (kind) {
|
|
@@ -1029,29 +1037,29 @@ function handleRecallFile(args) {
|
|
|
1029
1037
|
return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
|
|
1030
1038
|
}
|
|
1031
1039
|
// Daily breakdown
|
|
1032
|
-
const daily = db.prepare(`
|
|
1033
|
-
SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
|
|
1034
|
-
FROM session_file_edits WHERE file_path LIKE ?
|
|
1035
|
-
GROUP BY day, operation ORDER BY day
|
|
1040
|
+
const daily = db.prepare(`
|
|
1041
|
+
SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
|
|
1042
|
+
FROM session_file_edits WHERE file_path LIKE ?
|
|
1043
|
+
GROUP BY day, operation ORDER BY day
|
|
1036
1044
|
`).all(`%${sub}%`);
|
|
1037
1045
|
// Distinct context_snippets (intents) — deduped, ordered by recency
|
|
1038
|
-
const intents = db.prepare(`
|
|
1039
|
-
SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
|
|
1040
|
-
FROM session_file_edits
|
|
1041
|
-
WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
|
|
1042
|
-
GROUP BY context_snippet
|
|
1043
|
-
ORDER BY last_at DESC
|
|
1044
|
-
LIMIT ?
|
|
1046
|
+
const intents = db.prepare(`
|
|
1047
|
+
SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
|
|
1048
|
+
FROM session_file_edits
|
|
1049
|
+
WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
|
|
1050
|
+
GROUP BY context_snippet
|
|
1051
|
+
ORDER BY last_at DESC
|
|
1052
|
+
LIMIT ?
|
|
1045
1053
|
`).all(`%${sub}%`, maxIntents);
|
|
1046
1054
|
// Linked memories
|
|
1047
|
-
const memories = db.prepare(`
|
|
1048
|
-
SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
|
|
1049
|
-
FROM session_file_edits sfe
|
|
1050
|
-
JOIN memories m ON m.id = sfe.memory_id
|
|
1051
|
-
JOIN entities e ON e.id = m.entity_id
|
|
1052
|
-
WHERE sfe.file_path LIKE ?
|
|
1053
|
-
ORDER BY m.importance DESC
|
|
1054
|
-
LIMIT 20
|
|
1055
|
+
const memories = db.prepare(`
|
|
1056
|
+
SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
|
|
1057
|
+
FROM session_file_edits sfe
|
|
1058
|
+
JOIN memories m ON m.id = sfe.memory_id
|
|
1059
|
+
JOIN entities e ON e.id = m.entity_id
|
|
1060
|
+
WHERE sfe.file_path LIKE ?
|
|
1061
|
+
ORDER BY m.importance DESC
|
|
1062
|
+
LIMIT 20
|
|
1055
1063
|
`).all(`%${sub}%`);
|
|
1056
1064
|
// Distinct file paths matched (the substring may match multiple files)
|
|
1057
1065
|
const paths = db.prepare(`SELECT file_path, COUNT(*) as edits FROM session_file_edits WHERE file_path LIKE ? GROUP BY file_path ORDER BY edits DESC`).all(`%${sub}%`);
|
|
@@ -1118,10 +1126,10 @@ async function handleConsolidateWithSampling(args) {
|
|
|
1118
1126
|
const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
|
|
1119
1127
|
const snapshot = new Map();
|
|
1120
1128
|
const candidateRows = db
|
|
1121
|
-
.prepare(`SELECT m.id, m.content, e.name as entity_name
|
|
1122
|
-
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1123
|
-
WHERE m.protected = 0
|
|
1124
|
-
AND m.layer IN ('context','emotion','implementation')
|
|
1129
|
+
.prepare(`SELECT m.id, m.content, e.name as entity_name
|
|
1130
|
+
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1131
|
+
WHERE m.protected = 0
|
|
1132
|
+
AND m.layer IN ('context','emotion','implementation')
|
|
1125
1133
|
AND m.created_at <= ?`)
|
|
1126
1134
|
.all(ageCutoff);
|
|
1127
1135
|
for (const r of candidateRows)
|
|
@@ -1287,14 +1295,35 @@ function handleDriftStatus(args) {
|
|
|
1287
1295
|
by_state.held > 0 ? `⚪ ${by_state.held} held` : null,
|
|
1288
1296
|
`🔵 ${by_state.aligned} aligned`,
|
|
1289
1297
|
].filter(Boolean).join(' · ');
|
|
1298
|
+
if (args?.verbose) {
|
|
1299
|
+
return JSON.stringify({
|
|
1300
|
+
ok: true,
|
|
1301
|
+
triage: `${nodes} anchors: ${triage}`,
|
|
1302
|
+
nextReopen: view.nextReopen,
|
|
1303
|
+
attention: view.attention,
|
|
1304
|
+
alignedByDomain: view.alignedByDomain,
|
|
1305
|
+
candidates: view.candidates,
|
|
1306
|
+
counts: view.counts,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
// Compact by default. The agent asked "what needs attention" — answer that in full and keep
|
|
1310
|
+
// the rest to one line per node. (Full form was ~15k tokens for 43 anchors; the 2 drifting
|
|
1311
|
+
// items were buried under 41 aligned ones with rationale.)
|
|
1312
|
+
const aligned = view.alignedByDomain.map((g) => ({
|
|
1313
|
+
domain: g.domain,
|
|
1314
|
+
count: g.nodes.length,
|
|
1315
|
+
nodes: g.nodes.map((n) => ({ id: n.id, statement: n.statement, reality: n.reality })),
|
|
1316
|
+
}));
|
|
1317
|
+
const cand = view.candidates;
|
|
1290
1318
|
return JSON.stringify({
|
|
1291
1319
|
ok: true,
|
|
1292
1320
|
triage: `${nodes} anchors: ${triage}`,
|
|
1293
1321
|
nextReopen: view.nextReopen,
|
|
1294
1322
|
attention: view.attention,
|
|
1295
|
-
|
|
1296
|
-
candidates:
|
|
1323
|
+
aligned,
|
|
1324
|
+
candidates: { auto: cand?.auto?.length ?? 0, suppressed: cand?.suppressed?.length ?? 0 },
|
|
1297
1325
|
counts: view.counts,
|
|
1326
|
+
hint: 'verbose:true for full aligned nodes and candidate details; check_decision(anchor_id) for one node.',
|
|
1298
1327
|
});
|
|
1299
1328
|
}
|
|
1300
1329
|
// Fix ① (2026-06-17): infer the Map project from the client's workspace roots when the
|
|
@@ -1315,9 +1344,39 @@ async function resolveProjectFromRoots() {
|
|
|
1315
1344
|
});
|
|
1316
1345
|
return matches.length === 1 ? matches[0] : undefined; // unique repo match only; else ② handles it
|
|
1317
1346
|
}
|
|
1347
|
+
// ①b (2026-09-05): when the client sends no roots (common — not every MCP host implements
|
|
1348
|
+
// roots), infer the map from the files edited most recently. The DB already knows what you
|
|
1349
|
+
// touched; the map slug is a path segment of those files.
|
|
1350
|
+
function resolveProjectFromRecentEdits() {
|
|
1351
|
+
const projects = listMapProjects(db);
|
|
1352
|
+
if (projects.length <= 1)
|
|
1353
|
+
return { project: projects[0] };
|
|
1354
|
+
const rows = db
|
|
1355
|
+
.prepare(`SELECT file_path FROM session_file_edits
|
|
1356
|
+
WHERE occurred_at > unixepoch() - 86400
|
|
1357
|
+
ORDER BY occurred_at DESC LIMIT 50`)
|
|
1358
|
+
.all();
|
|
1359
|
+
if (rows.length === 0)
|
|
1360
|
+
return {};
|
|
1361
|
+
const hits = new Map();
|
|
1362
|
+
for (const r of rows) {
|
|
1363
|
+
const segs = r.file_path.toLowerCase().replace(/\\/g, '/').split('/');
|
|
1364
|
+
for (const p of projects)
|
|
1365
|
+
if (segs.includes(p.toLowerCase()))
|
|
1366
|
+
hits.set(p, (hits.get(p) ?? 0) + 1);
|
|
1367
|
+
}
|
|
1368
|
+
if (hits.size === 0)
|
|
1369
|
+
return {};
|
|
1370
|
+
const [best, second] = [...hits.entries()].sort((a, b) => b[1] - a[1]);
|
|
1371
|
+
if (second && second[1] === best[1])
|
|
1372
|
+
return {}; // tie → still ambiguous
|
|
1373
|
+
return { project: best[0], from: 'recent_edits' };
|
|
1374
|
+
}
|
|
1318
1375
|
async function handleWhereAmI(args) {
|
|
1319
|
-
// ①
|
|
1320
|
-
|
|
1376
|
+
// ① roots; ①b recent edits; ② (inside whereAmI) is the last fallback.
|
|
1377
|
+
let project = args?.project ?? (await resolveProjectFromRoots());
|
|
1378
|
+
if (!project)
|
|
1379
|
+
project = resolveProjectFromRecentEdits().project;
|
|
1321
1380
|
const res = whereAmI(db, {
|
|
1322
1381
|
query: args?.query, node_id: args?.node_id, project, limit: args?.limit,
|
|
1323
1382
|
});
|
|
@@ -1325,7 +1384,7 @@ async function handleWhereAmI(args) {
|
|
|
1325
1384
|
return JSON.stringify({
|
|
1326
1385
|
ok: true, located: false, reason: 'ambiguous_project',
|
|
1327
1386
|
available_projects: res.ambiguous.available,
|
|
1328
|
-
hint: `Multiple maps imported — pass project: one of [${res.ambiguous.available.join(', ')}]
|
|
1387
|
+
hint: `Multiple maps imported and none matched your workspace roots or the files you edited in the last 24h — pass project: one of [${res.ambiguous.available.join(', ')}].`,
|
|
1329
1388
|
});
|
|
1330
1389
|
}
|
|
1331
1390
|
if (res.matched.length === 0) {
|
|
@@ -1445,25 +1504,25 @@ function handleFlagProposals(args) {
|
|
|
1445
1504
|
const sessionContext = args.session_context ?? 'conversation session';
|
|
1446
1505
|
const PROPOSAL_TAG = 'orphaned_proposal';
|
|
1447
1506
|
const now = Math.floor(Date.now() / 1000);
|
|
1448
|
-
const insertAnchor = db.prepare(`
|
|
1449
|
-
INSERT INTO drift_anchors (
|
|
1450
|
-
kind, statement, rationale, domain, decision_mode,
|
|
1451
|
-
confidence, lifecycle, status, owner,
|
|
1452
|
-
evidence_refs, created_at, updated_at
|
|
1453
|
-
) VALUES (
|
|
1454
|
-
'decision', ?, ?, ?, 'hypothesis',
|
|
1455
|
-
?, 'active', 'active', 'agent',
|
|
1456
|
-
?, ?, ?
|
|
1457
|
-
)
|
|
1507
|
+
const insertAnchor = db.prepare(`
|
|
1508
|
+
INSERT INTO drift_anchors (
|
|
1509
|
+
kind, statement, rationale, domain, decision_mode,
|
|
1510
|
+
confidence, lifecycle, status, owner,
|
|
1511
|
+
evidence_refs, created_at, updated_at
|
|
1512
|
+
) VALUES (
|
|
1513
|
+
'decision', ?, ?, ?, 'hypothesis',
|
|
1514
|
+
?, 'active', 'active', 'agent',
|
|
1515
|
+
?, ?, ?
|
|
1516
|
+
)
|
|
1458
1517
|
`);
|
|
1459
|
-
const insertCandidate = db.prepare(`
|
|
1460
|
-
INSERT INTO memory_write_candidates (
|
|
1461
|
-
scope, candidate_type, target_node_id, rationale,
|
|
1462
|
-
confidence, evidence_refs, status, created_at
|
|
1463
|
-
) VALUES (
|
|
1464
|
-
'orphaned_proposal', 'update_node', ?, ?,
|
|
1465
|
-
?, ?, 'pending_review', ?
|
|
1466
|
-
)
|
|
1518
|
+
const insertCandidate = db.prepare(`
|
|
1519
|
+
INSERT INTO memory_write_candidates (
|
|
1520
|
+
scope, candidate_type, target_node_id, rationale,
|
|
1521
|
+
confidence, evidence_refs, status, created_at
|
|
1522
|
+
) VALUES (
|
|
1523
|
+
'orphaned_proposal', 'update_node', ?, ?,
|
|
1524
|
+
?, ?, 'pending_review', ?
|
|
1525
|
+
)
|
|
1467
1526
|
`);
|
|
1468
1527
|
const results = [];
|
|
1469
1528
|
const txn = db.transaction(() => {
|
|
@@ -1507,13 +1566,13 @@ function handleFlagProposals(args) {
|
|
|
1507
1566
|
function handleDream(args) {
|
|
1508
1567
|
const domainFilter = args?.domain;
|
|
1509
1568
|
// 1. Fetch active North Star anchor(s)
|
|
1510
|
-
const northStars = db.prepare(`
|
|
1511
|
-
SELECT id, statement, rationale, domain, confidence, lifecycle,
|
|
1512
|
-
datetime(review_after, 'unixepoch') as review_date,
|
|
1513
|
-
datetime(created_at, 'unixepoch') as declared_at
|
|
1514
|
-
FROM drift_anchors
|
|
1515
|
-
WHERE node_type = 'north_star' AND status = 'active'
|
|
1516
|
-
ORDER BY created_at DESC
|
|
1569
|
+
const northStars = db.prepare(`
|
|
1570
|
+
SELECT id, statement, rationale, domain, confidence, lifecycle,
|
|
1571
|
+
datetime(review_after, 'unixepoch') as review_date,
|
|
1572
|
+
datetime(created_at, 'unixepoch') as declared_at
|
|
1573
|
+
FROM drift_anchors
|
|
1574
|
+
WHERE node_type = 'north_star' AND status = 'active'
|
|
1575
|
+
ORDER BY created_at DESC
|
|
1517
1576
|
`).all();
|
|
1518
1577
|
// Re-injection friction — the active-observability loop closing back into reflection.
|
|
1519
1578
|
// "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
|
|
@@ -1530,15 +1589,18 @@ function handleDream(args) {
|
|
|
1530
1589
|
// why-strings so the existing backlog is drainable without a backfill write.
|
|
1531
1590
|
let distillQueue = [];
|
|
1532
1591
|
try {
|
|
1533
|
-
const rows = db.prepare(`
|
|
1534
|
-
SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
|
|
1535
|
-
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1536
|
-
WHERE m.layer IN ('learning', 'caveat')
|
|
1537
|
-
AND json_valid(m.content)
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1592
|
+
const rows = db.prepare(`
|
|
1593
|
+
SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
|
|
1594
|
+
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1595
|
+
WHERE m.layer IN ('learning', 'caveat')
|
|
1596
|
+
AND json_valid(m.content)
|
|
1597
|
+
-- settle window: the Stop hook extracts every turn, so the newest rows belong to a
|
|
1598
|
+
-- session still in motion. Don't ask the agent to distill the conversation it is in.
|
|
1599
|
+
AND m.created_at < unixepoch() - 1800
|
|
1600
|
+
AND (json_extract(m.content, '$.needs_distill') = 1
|
|
1601
|
+
OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
|
|
1602
|
+
OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
|
|
1603
|
+
ORDER BY m.created_at DESC LIMIT 8
|
|
1542
1604
|
`).all();
|
|
1543
1605
|
distillQueue = rows.map((r) => {
|
|
1544
1606
|
let c = {};
|
|
@@ -1576,26 +1638,26 @@ function handleDream(args) {
|
|
|
1576
1638
|
}
|
|
1577
1639
|
// 2. Fetch pending orphaned proposals
|
|
1578
1640
|
const candidateQuery = domainFilter
|
|
1579
|
-
? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1580
|
-
mc.rationale as candidate_rationale, mc.confidence,
|
|
1581
|
-
mc.evidence_refs, mc.status,
|
|
1582
|
-
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1583
|
-
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1584
|
-
da.evidence_refs as anchor_evidence
|
|
1585
|
-
FROM memory_write_candidates mc
|
|
1586
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1587
|
-
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1588
|
-
AND da.domain = ?
|
|
1641
|
+
? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1642
|
+
mc.rationale as candidate_rationale, mc.confidence,
|
|
1643
|
+
mc.evidence_refs, mc.status,
|
|
1644
|
+
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1645
|
+
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1646
|
+
da.evidence_refs as anchor_evidence
|
|
1647
|
+
FROM memory_write_candidates mc
|
|
1648
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1649
|
+
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1650
|
+
AND da.domain = ?
|
|
1589
1651
|
ORDER BY mc.created_at DESC`
|
|
1590
|
-
: `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1591
|
-
mc.rationale as candidate_rationale, mc.confidence,
|
|
1592
|
-
mc.evidence_refs, mc.status,
|
|
1593
|
-
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1594
|
-
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1595
|
-
da.evidence_refs as anchor_evidence
|
|
1596
|
-
FROM memory_write_candidates mc
|
|
1597
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1598
|
-
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1652
|
+
: `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1653
|
+
mc.rationale as candidate_rationale, mc.confidence,
|
|
1654
|
+
mc.evidence_refs, mc.status,
|
|
1655
|
+
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1656
|
+
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1657
|
+
da.evidence_refs as anchor_evidence
|
|
1658
|
+
FROM memory_write_candidates mc
|
|
1659
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1660
|
+
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1599
1661
|
ORDER BY mc.created_at DESC`;
|
|
1600
1662
|
const candidates = domainFilter
|
|
1601
1663
|
? db.prepare(candidateQuery).all(domainFilter)
|
|
@@ -1675,12 +1737,12 @@ function handleResolveProposal(args) {
|
|
|
1675
1737
|
throw new Error('verdict must be "surface" or "dismiss"');
|
|
1676
1738
|
}
|
|
1677
1739
|
// Verify candidate exists and is pending
|
|
1678
|
-
const candidate = db.prepare(`
|
|
1679
|
-
SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
|
|
1680
|
-
da.statement
|
|
1681
|
-
FROM memory_write_candidates mc
|
|
1682
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1683
|
-
WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
|
|
1740
|
+
const candidate = db.prepare(`
|
|
1741
|
+
SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
|
|
1742
|
+
da.statement
|
|
1743
|
+
FROM memory_write_candidates mc
|
|
1744
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1745
|
+
WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
|
|
1684
1746
|
`).get(candidateId);
|
|
1685
1747
|
if (!candidate) {
|
|
1686
1748
|
throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
|