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
|
@@ -74,9 +74,9 @@ export function declareAnchor(db, input) {
|
|
|
74
74
|
throw new Error(`${input.kind} anchors need at least one violation_signal term (the forbidden act / rejected alternative) — otherwise the detector can never deduce a contradiction`);
|
|
75
75
|
}
|
|
76
76
|
const info = db
|
|
77
|
-
.prepare(`INSERT INTO drift_anchors
|
|
78
|
-
(kind, statement, rationale, affects, detect_terms, violation_signal, tier, source, source_memory_id)
|
|
79
|
-
VALUES
|
|
77
|
+
.prepare(`INSERT INTO drift_anchors
|
|
78
|
+
(kind, statement, rationale, affects, detect_terms, violation_signal, tier, source, source_memory_id)
|
|
79
|
+
VALUES
|
|
80
80
|
(@kind, @statement, @rationale, @affects, @detect_terms, @violation_signal, @tier, @source, @source_memory_id)`)
|
|
81
81
|
.run({
|
|
82
82
|
kind: input.kind,
|
|
@@ -110,7 +110,7 @@ export function detectDrift(db, opts = {}) {
|
|
|
110
110
|
samples: [],
|
|
111
111
|
};
|
|
112
112
|
const anchors = db
|
|
113
|
-
.prepare(`SELECT id, kind, statement, affects, detect_terms, violation_signal, tier, created_at
|
|
113
|
+
.prepare(`SELECT id, kind, statement, affects, detect_terms, violation_signal, tier, created_at
|
|
114
114
|
FROM drift_anchors WHERE status = 'active'`)
|
|
115
115
|
.all();
|
|
116
116
|
res.anchorsScanned = anchors.length;
|
|
@@ -120,8 +120,8 @@ export function detectDrift(db, opts = {}) {
|
|
|
120
120
|
// the "why this edit" text, not just path + snippet. (session_file_edits.memory_id →
|
|
121
121
|
// memories.id; memories_fts.rowid = memories.id.)
|
|
122
122
|
const edits = db
|
|
123
|
-
.prepare(`SELECT e.id, e.file_path, e.context_snippet, e.memory_id, m.content AS memory_content, e.occurred_at
|
|
124
|
-
FROM session_file_edits e
|
|
123
|
+
.prepare(`SELECT e.id, e.file_path, e.context_snippet, e.memory_id, m.content AS memory_content, e.occurred_at
|
|
124
|
+
FROM session_file_edits e
|
|
125
125
|
LEFT JOIN memories m ON m.id = e.memory_id`)
|
|
126
126
|
.all();
|
|
127
127
|
res.editsScanned = edits.length;
|
|
@@ -137,19 +137,19 @@ export function detectDrift(db, opts = {}) {
|
|
|
137
137
|
// Write paths prepared only when persisting — keeps dryRun safe on a readonly connection.
|
|
138
138
|
const upsertContradicts = opts.dryRun
|
|
139
139
|
? null
|
|
140
|
-
: db.prepare(`
|
|
141
|
-
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
142
|
-
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
143
|
-
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
144
|
-
confidence = excluded.confidence,
|
|
145
|
-
evidence = excluded.evidence
|
|
146
|
-
WHERE drift_edges.status = 'open'
|
|
140
|
+
: db.prepare(`
|
|
141
|
+
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
142
|
+
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
143
|
+
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
144
|
+
confidence = excluded.confidence,
|
|
145
|
+
evidence = excluded.evidence
|
|
146
|
+
WHERE drift_edges.status = 'open'
|
|
147
147
|
`);
|
|
148
148
|
const insertAbsent = opts.dryRun
|
|
149
149
|
? null
|
|
150
|
-
: db.prepare(`
|
|
151
|
-
INSERT OR IGNORE INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
152
|
-
VALUES (@anchor_id, NULL, 'absent', @confidence, @evidence, 'open')
|
|
150
|
+
: db.prepare(`
|
|
151
|
+
INSERT OR IGNORE INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
152
|
+
VALUES (@anchor_id, NULL, 'absent', @confidence, @evidence, 'open')
|
|
153
153
|
`);
|
|
154
154
|
const apply = () => {
|
|
155
155
|
for (const a of anchors) {
|
|
@@ -351,13 +351,13 @@ export function detectFileViolations(db, opts = {}) {
|
|
|
351
351
|
.all();
|
|
352
352
|
const upsert = opts.dryRun
|
|
353
353
|
? null
|
|
354
|
-
: db.prepare(`
|
|
355
|
-
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
356
|
-
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
357
|
-
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
358
|
-
confidence = excluded.confidence,
|
|
359
|
-
evidence = excluded.evidence
|
|
360
|
-
WHERE drift_edges.status = 'open'
|
|
354
|
+
: db.prepare(`
|
|
355
|
+
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
356
|
+
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
357
|
+
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
358
|
+
confidence = excluded.confidence,
|
|
359
|
+
evidence = excluded.evidence
|
|
360
|
+
WHERE drift_edges.status = 'open'
|
|
361
361
|
`);
|
|
362
362
|
const apply = () => {
|
|
363
363
|
for (const a of anchors) {
|
package/dist/lib/drift-view.js
CHANGED
|
@@ -27,14 +27,14 @@ function parseEvidence(s) {
|
|
|
27
27
|
export function getDriftHeadline(db, opts = {}) {
|
|
28
28
|
const limit = opts.limit ?? 3;
|
|
29
29
|
const rows = db
|
|
30
|
-
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
31
|
-
e.file_path, e.context_snippet, e.occurred_at,
|
|
32
|
-
d.confidence, d.evidence
|
|
33
|
-
FROM drift_edges d
|
|
34
|
-
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
35
|
-
JOIN session_file_edits e ON e.id = d.edit_id
|
|
36
|
-
WHERE d.verdict = 'contradicts' AND d.status = 'open' AND a.status = 'active'
|
|
37
|
-
ORDER BY d.confidence DESC, (a.tier = 'human') DESC, e.occurred_at DESC
|
|
30
|
+
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
31
|
+
e.file_path, e.context_snippet, e.occurred_at,
|
|
32
|
+
d.confidence, d.evidence
|
|
33
|
+
FROM drift_edges d
|
|
34
|
+
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
35
|
+
JOIN session_file_edits e ON e.id = d.edit_id
|
|
36
|
+
WHERE d.verdict = 'contradicts' AND d.status = 'open' AND a.status = 'active'
|
|
37
|
+
ORDER BY d.confidence DESC, (a.tier = 'human') DESC, e.occurred_at DESC
|
|
38
38
|
LIMIT ?`)
|
|
39
39
|
.all(limit);
|
|
40
40
|
return rows.map((r) => {
|
|
@@ -61,12 +61,12 @@ export function getDriftHeadline(db, opts = {}) {
|
|
|
61
61
|
export function getDriftAbsences(db, opts = {}) {
|
|
62
62
|
const limit = opts.limit ?? 10;
|
|
63
63
|
const rows = db
|
|
64
|
-
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
65
|
-
d.confidence, d.evidence, d.detected_at
|
|
66
|
-
FROM drift_edges d
|
|
67
|
-
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
68
|
-
WHERE d.verdict = 'absent' AND d.status = 'open' AND a.status = 'active'
|
|
69
|
-
ORDER BY a.created_at ASC
|
|
64
|
+
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
65
|
+
d.confidence, d.evidence, d.detected_at
|
|
66
|
+
FROM drift_edges d
|
|
67
|
+
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
68
|
+
WHERE d.verdict = 'absent' AND d.status = 'open' AND a.status = 'active'
|
|
69
|
+
ORDER BY a.created_at ASC
|
|
70
70
|
LIMIT ?`)
|
|
71
71
|
.all(limit);
|
|
72
72
|
return rows.map((r) => {
|
|
@@ -85,8 +85,8 @@ export function getDriftAbsences(db, opts = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
function countOpen(db, verdict) {
|
|
87
87
|
return db
|
|
88
|
-
.prepare(`SELECT COUNT(*) AS n
|
|
89
|
-
FROM drift_edges d JOIN drift_anchors a ON a.id = d.anchor_id
|
|
88
|
+
.prepare(`SELECT COUNT(*) AS n
|
|
89
|
+
FROM drift_edges d JOIN drift_anchors a ON a.id = d.anchor_id
|
|
90
90
|
WHERE d.verdict = ? AND d.status = 'open' AND a.status = 'active'`)
|
|
91
91
|
.get(verdict).n;
|
|
92
92
|
}
|
package/dist/lib/guard.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// FAIL-OPEN by construction: every DB op is best-effort; only an explicit 'hard' contradiction yields
|
|
13
13
|
// a block. Lexical only (no embeddings): reuses lexical-match.ts (the same logic as the detector).
|
|
14
14
|
import { normPath, compileGlob, parseArray, matchViolation, SCRAPE_ANCHOR } from './lexical-match.js';
|
|
15
|
+
import { supersededAnchorIds } from './truth-engine.js';
|
|
15
16
|
// "accepted = the only thing the gate compares against": declared (status active), still live
|
|
16
17
|
// (lifecycle active|experiment — NOT at_risk/superseded/deprecated), and not explicitly card-disabled.
|
|
17
18
|
// at_risk(stale) anchors deliberately DON'T gate — we don't enforce a rule we're no longer sure of.
|
|
@@ -37,10 +38,16 @@ function bestEffort(fn) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
export function acceptedAnchors(db) {
|
|
40
|
-
|
|
41
|
+
const rows = db
|
|
41
42
|
.prepare(`SELECT id, kind, statement, rationale, affects, detect_terms, violation_signal, card_policy
|
|
42
43
|
FROM drift_anchors WHERE ${ACCEPTED_SQL}`)
|
|
43
44
|
.all();
|
|
45
|
+
// A superseded anchor must stop gating. The block/warn text tells the user to run
|
|
46
|
+
// resolve_drift(action:'supersede') — if the gate ignored that resolution, following the
|
|
47
|
+
// instruction would not lift the block (dead end). status/lifecycle are not touched by
|
|
48
|
+
// supersede, so the resolution record is the only place that knows.
|
|
49
|
+
const retired = supersededAnchorIds(db);
|
|
50
|
+
return retired.size === 0 ? rows : rows.filter((a) => !retired.has(a.id));
|
|
44
51
|
}
|
|
45
52
|
function buildActionCtx(input) {
|
|
46
53
|
const files = [];
|
|
@@ -175,11 +182,14 @@ export function formatReinject(matches, gate) {
|
|
|
175
182
|
export function buildBootDigest(db, opts = {}) {
|
|
176
183
|
const maxAnchors = opts.maxAnchors ?? 8;
|
|
177
184
|
const maxForks = opts.maxForks ?? 5;
|
|
178
|
-
|
|
185
|
+
// Same rule as the gate: never re-surface a decision the user has already superseded.
|
|
186
|
+
const retiredIds = supersededAnchorIds(db);
|
|
187
|
+
const anchorRows = db
|
|
179
188
|
.prepare(`SELECT id, statement, rationale FROM drift_anchors
|
|
180
189
|
WHERE ${ACCEPTED_SQL} AND kind IN ('prohibition', 'decision', 'constraint')
|
|
181
190
|
ORDER BY confidence DESC, updated_at DESC LIMIT ?`)
|
|
182
|
-
.all(maxAnchors);
|
|
191
|
+
.all(maxAnchors + retiredIds.size);
|
|
192
|
+
const anchors = anchorRows.filter((a) => !retiredIds.has(a.id)).slice(0, maxAnchors);
|
|
183
193
|
const forks = db
|
|
184
194
|
.prepare(`SELECT c.id, c.rationale, a.statement
|
|
185
195
|
FROM memory_write_candidates c
|
package/dist/lib/map-import.js
CHANGED
|
@@ -71,13 +71,13 @@ export function importMap(db, map) {
|
|
|
71
71
|
const anchorExists = db.prepare('SELECT 1 FROM drift_anchors WHERE id = ?');
|
|
72
72
|
let linked = 0;
|
|
73
73
|
const tx = db.transaction(() => {
|
|
74
|
-
db.prepare(`
|
|
75
|
-
INSERT INTO map_projects (project, job, audience, product_status, template, stages, related_projects, updated_at)
|
|
76
|
-
VALUES (@project, @job, @audience, @product_status, @template, @stages, @related_projects, unixepoch())
|
|
77
|
-
ON CONFLICT(project) DO UPDATE SET
|
|
78
|
-
job=excluded.job, audience=excluded.audience, product_status=excluded.product_status,
|
|
79
|
-
template=excluded.template, stages=excluded.stages, related_projects=excluded.related_projects,
|
|
80
|
-
updated_at=unixepoch()
|
|
74
|
+
db.prepare(`
|
|
75
|
+
INSERT INTO map_projects (project, job, audience, product_status, template, stages, related_projects, updated_at)
|
|
76
|
+
VALUES (@project, @job, @audience, @product_status, @template, @stages, @related_projects, unixepoch())
|
|
77
|
+
ON CONFLICT(project) DO UPDATE SET
|
|
78
|
+
job=excluded.job, audience=excluded.audience, product_status=excluded.product_status,
|
|
79
|
+
template=excluded.template, stages=excluded.stages, related_projects=excluded.related_projects,
|
|
80
|
+
updated_at=unixepoch()
|
|
81
81
|
`).run({
|
|
82
82
|
project: map.project,
|
|
83
83
|
job: map.job ?? null,
|
|
@@ -89,13 +89,13 @@ export function importMap(db, map) {
|
|
|
89
89
|
});
|
|
90
90
|
db.prepare('DELETE FROM map_edges WHERE project = ?').run(map.project);
|
|
91
91
|
db.prepare('DELETE FROM map_nodes WHERE project = ?').run(map.project);
|
|
92
|
-
const insNode = db.prepare(`
|
|
93
|
-
INSERT INTO map_nodes
|
|
94
|
-
(id, project, layer, stage, statement, status, facets, role, note, due,
|
|
95
|
-
paused_reason, related_project, spinout_candidate, anchor_id, review_by, revival_condition, reality, extra, updated_at)
|
|
96
|
-
VALUES
|
|
97
|
-
(@id, @project, @layer, @stage, @statement, @status, @facets, @role, @note, @due,
|
|
98
|
-
@paused_reason, @related_project, @spinout_candidate, @anchor_id, @review_by, @revival_condition, @reality, @extra, unixepoch())
|
|
92
|
+
const insNode = db.prepare(`
|
|
93
|
+
INSERT INTO map_nodes
|
|
94
|
+
(id, project, layer, stage, statement, status, facets, role, note, due,
|
|
95
|
+
paused_reason, related_project, spinout_candidate, anchor_id, review_by, revival_condition, reality, extra, updated_at)
|
|
96
|
+
VALUES
|
|
97
|
+
(@id, @project, @layer, @stage, @statement, @status, @facets, @role, @note, @due,
|
|
98
|
+
@paused_reason, @related_project, @spinout_candidate, @anchor_id, @review_by, @revival_condition, @reality, @extra, unixepoch())
|
|
99
99
|
`);
|
|
100
100
|
for (const n of map.nodes) {
|
|
101
101
|
if (!n.id)
|
|
@@ -134,9 +134,9 @@ export function importMap(db, map) {
|
|
|
134
134
|
extra: JSON.stringify(extra),
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
-
const insEdge = db.prepare(`
|
|
138
|
-
INSERT OR IGNORE INTO map_edges (project, from_id, to_id, type, strength, note)
|
|
139
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
137
|
+
const insEdge = db.prepare(`
|
|
138
|
+
INSERT OR IGNORE INTO map_edges (project, from_id, to_id, type, strength, note)
|
|
139
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
140
140
|
`);
|
|
141
141
|
for (const e of map.edges)
|
|
142
142
|
insEdge.run(map.project, e.from, e.to, e.type, e.strength ?? null, e.note ?? null);
|
|
@@ -65,6 +65,22 @@ export interface DecisionDetail extends TruthNode {
|
|
|
65
65
|
detected_at: number;
|
|
66
66
|
}>;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Anchors that have been explicitly superseded ("intent evolved, this is the new direction").
|
|
70
|
+
*
|
|
71
|
+
* Resolutions live in meta.t3_resolutions keyed `A<anchor_id>`, one record per anchor
|
|
72
|
+
* (a later resolve_drift overwrites the earlier one), so the current resolution for
|
|
73
|
+
* anchor N is simply t3_resolutions["A"+N].
|
|
74
|
+
*
|
|
75
|
+
* NOTE the asymmetry: a supersede record names BOTH sides (superseded_node = the old
|
|
76
|
+
* anchor, superseded_by = the new one). Only the OLD one is retired — the new anchor must
|
|
77
|
+
* keep enforcing, so we match on `superseded_node` and never on `superseded_by`.
|
|
78
|
+
*
|
|
79
|
+
* Used by the gate (guard.ts) so that the enforcement layer honours the same
|
|
80
|
+
* make-or-break rule as the reporting layer: a divergence accounted for by a recorded
|
|
81
|
+
* resolution is NOT drift.
|
|
82
|
+
*/
|
|
83
|
+
export declare function supersededAnchorIds(db: Database.Database): Set<number>;
|
|
68
84
|
export declare function getTruthView(db: Database.Database, opts?: {
|
|
69
85
|
domain?: string;
|
|
70
86
|
decision_mode?: string;
|
package/dist/lib/truth-engine.js
CHANGED
|
@@ -59,6 +59,22 @@ function classifySpecies(decision_mode) {
|
|
|
59
59
|
default: return 'hypothesis';
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
// ── Edge summary (one line the agent can act on) ─────────────────────────────
|
|
63
|
+
function summarizeEdges(verdict, edges) {
|
|
64
|
+
const latest = edges[0];
|
|
65
|
+
let ev = {};
|
|
66
|
+
try {
|
|
67
|
+
ev = JSON.parse(latest.evidence || '{}');
|
|
68
|
+
}
|
|
69
|
+
catch { /* ignore */ }
|
|
70
|
+
const file = ev.file_path ? String(ev.file_path).replace(/\\/g, '/').split('/').slice(-2).join('/') : null;
|
|
71
|
+
const hit = ev.hit_term ? ` hit "${ev.hit_term}"` : '';
|
|
72
|
+
const when = new Date(latest.detected_at * 1000).toISOString().slice(0, 10);
|
|
73
|
+
const head = verdict === 'contradicts'
|
|
74
|
+
? `${edges.length} open contradiction${edges.length > 1 ? 's' : ''}`
|
|
75
|
+
: `declared but not found in reality (${edges.length} absent signal${edges.length > 1 ? 's' : ''})`;
|
|
76
|
+
return `${head}${file ? ` — ${file}` : ''}${hit} (${when}). resolve_drift: fix if reality is wrong, dismiss if false positive.`;
|
|
77
|
+
}
|
|
62
78
|
function buildResolutionLookup(db) {
|
|
63
79
|
const t3res = safeJsonParse(db.prepare("SELECT value FROM meta WHERE key='t3_resolutions'").get()?.value, {});
|
|
64
80
|
const t2res = safeJsonParse(db.prepare("SELECT value FROM meta WHERE key='t2_resolutions'").get()?.value, {});
|
|
@@ -67,6 +83,11 @@ function buildResolutionLookup(db) {
|
|
|
67
83
|
return function resolutionFor(id) {
|
|
68
84
|
const matches = [];
|
|
69
85
|
for (const r of Object.values(t3res)) {
|
|
86
|
+
// A supersede record names both sides. Only the OLD anchor is accounted for by it;
|
|
87
|
+
// the replacement must stay checkable (otherwise a new North Star could never drift,
|
|
88
|
+
// because the supersede branch wins over the contradicts branch).
|
|
89
|
+
if (r && r.action === 'supersede' && r.superseded_by === id && r.superseded_node !== id)
|
|
90
|
+
continue;
|
|
70
91
|
if (r && (r.superseded_node === id || r.superseded_by === id ||
|
|
71
92
|
r.node === id || r.direction_node === id || r.constraint_node === id)) {
|
|
72
93
|
matches.push(r);
|
|
@@ -91,6 +112,33 @@ function buildResolutionLookup(db) {
|
|
|
91
112
|
};
|
|
92
113
|
}
|
|
93
114
|
// ── Core: getTruthView ───────────────────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Anchors that have been explicitly superseded ("intent evolved, this is the new direction").
|
|
117
|
+
*
|
|
118
|
+
* Resolutions live in meta.t3_resolutions keyed `A<anchor_id>`, one record per anchor
|
|
119
|
+
* (a later resolve_drift overwrites the earlier one), so the current resolution for
|
|
120
|
+
* anchor N is simply t3_resolutions["A"+N].
|
|
121
|
+
*
|
|
122
|
+
* NOTE the asymmetry: a supersede record names BOTH sides (superseded_node = the old
|
|
123
|
+
* anchor, superseded_by = the new one). Only the OLD one is retired — the new anchor must
|
|
124
|
+
* keep enforcing, so we match on `superseded_node` and never on `superseded_by`.
|
|
125
|
+
*
|
|
126
|
+
* Used by the gate (guard.ts) so that the enforcement layer honours the same
|
|
127
|
+
* make-or-break rule as the reporting layer: a divergence accounted for by a recorded
|
|
128
|
+
* resolution is NOT drift.
|
|
129
|
+
*/
|
|
130
|
+
export function supersededAnchorIds(db) {
|
|
131
|
+
const out = new Set();
|
|
132
|
+
for (const key of ['t3_resolutions', 't2_resolutions']) {
|
|
133
|
+
const res = safeJsonParse(db.prepare('SELECT value FROM meta WHERE key = ?').get(key)?.value, {});
|
|
134
|
+
for (const r of Object.values(res)) {
|
|
135
|
+
if (r && r.action === 'supersede' && typeof r.superseded_node === 'number') {
|
|
136
|
+
out.add(r.superseded_node);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
94
142
|
export function getTruthView(db, opts = {}) {
|
|
95
143
|
const now = Date.now();
|
|
96
144
|
const resolutionFor = buildResolutionLookup(db);
|
|
@@ -112,6 +160,21 @@ export function getTruthView(db, opts = {}) {
|
|
|
112
160
|
cardByNode.set(c.target_node_id, c);
|
|
113
161
|
}
|
|
114
162
|
}
|
|
163
|
+
// ── Open drift edges (what the detector actually observed) ──
|
|
164
|
+
// The detector writes drift_edges; a truth view that never reads them will report 🔵 for
|
|
165
|
+
// anchors it has hard evidence against. (2026-09-05: 11 open `contradicts` edges across 4
|
|
166
|
+
// anchors — one of them the PII constraint — all rendered "reality matches intent".)
|
|
167
|
+
const edgeRows = db
|
|
168
|
+
.prepare(`SELECT anchor_id, verdict, confidence, evidence, detected_at
|
|
169
|
+
FROM drift_edges WHERE status = 'open'
|
|
170
|
+
ORDER BY detected_at DESC`)
|
|
171
|
+
.all();
|
|
172
|
+
const openEdges = new Map();
|
|
173
|
+
for (const e of edgeRows) {
|
|
174
|
+
if (!openEdges.has(e.anchor_id))
|
|
175
|
+
openEdges.set(e.anchor_id, []);
|
|
176
|
+
openEdges.get(e.anchor_id).push(e);
|
|
177
|
+
}
|
|
115
178
|
// ── Active nodes + state derivation ──
|
|
116
179
|
let sql = `SELECT id, node_type, domain, decision_mode, statement, rationale, confidence, lifecycle,
|
|
117
180
|
card_policy, review_after
|
|
@@ -137,6 +200,9 @@ export function getTruthView(db, opts = {}) {
|
|
|
137
200
|
const pending = pendingByNode.get(r.id);
|
|
138
201
|
const card = cardByNode.get(r.id);
|
|
139
202
|
const overdue = r.review_after != null && r.review_after * 1000 < now;
|
|
203
|
+
const edges = openEdges.get(r.id) ?? [];
|
|
204
|
+
const contradicts = edges.filter((e) => e.verdict === 'contradicts');
|
|
205
|
+
const absent = edges.filter((e) => e.verdict === 'absent');
|
|
140
206
|
// ── State derivation (the make-or-break logic) ──
|
|
141
207
|
let state;
|
|
142
208
|
let accounted;
|
|
@@ -159,8 +225,22 @@ export function getTruthView(db, opts = {}) {
|
|
|
159
225
|
accounted = true;
|
|
160
226
|
accountedBy = 'supersede (intentional evolution)';
|
|
161
227
|
}
|
|
162
|
-
else if (
|
|
163
|
-
//
|
|
228
|
+
else if (res?.action === 'dismiss') {
|
|
229
|
+
// 🔵 a human looked at the signal and called it a false positive — that IS an answer.
|
|
230
|
+
state = 'aligned';
|
|
231
|
+
accounted = true;
|
|
232
|
+
accountedBy = 'dismiss (false positive)';
|
|
233
|
+
}
|
|
234
|
+
else if (contradicts.length > 0) {
|
|
235
|
+
// 🔴 the detector has hard evidence against this anchor and nobody has answered it.
|
|
236
|
+
// resolve_drift(fix|dismiss) closes the edges; supersede/acknowledge are handled above.
|
|
237
|
+
state = 'drift';
|
|
238
|
+
accounted = false;
|
|
239
|
+
accountedBy = null;
|
|
240
|
+
}
|
|
241
|
+
else if (pending || absent.length > 0) {
|
|
242
|
+
// 🟡 Soft signal awaiting human decision (a pending candidate, or "declared but not
|
|
243
|
+
// found in reality" — absent is weaker than contradicts, so it asks rather than alarms)
|
|
164
244
|
state = 'review';
|
|
165
245
|
accounted = false;
|
|
166
246
|
accountedBy = null;
|
|
@@ -177,9 +257,15 @@ export function getTruthView(db, opts = {}) {
|
|
|
177
257
|
accounted = true;
|
|
178
258
|
accountedBy = null;
|
|
179
259
|
}
|
|
260
|
+
// Say what was observed. Never claim convergence when nothing was checked — an agent
|
|
261
|
+
// reading "reality matches intent" will trust it; "no signal observed" it will verify.
|
|
180
262
|
const reality = card?.rationale
|
|
181
263
|
?? pending?.rationale
|
|
182
|
-
?? (
|
|
264
|
+
?? (contradicts.length > 0 ? summarizeEdges('contradicts', contradicts) : null)
|
|
265
|
+
?? (absent.length > 0 ? summarizeEdges('absent', absent) : null)
|
|
266
|
+
?? (state === 'aligned'
|
|
267
|
+
? (accountedBy ? 'Accounted for by recorded resolution' : 'No signal observed (not verified against reality)')
|
|
268
|
+
: null);
|
|
183
269
|
return {
|
|
184
270
|
id: r.id,
|
|
185
271
|
node_type: r.node_type,
|
package/dist/mcp/prompts.js
CHANGED
|
@@ -63,23 +63,23 @@ export function getPrompt(name, args) {
|
|
|
63
63
|
role: 'user',
|
|
64
64
|
content: {
|
|
65
65
|
type: 'text',
|
|
66
|
-
text: `You are an agent-memory writer. Read the session transcript below and propose memories to save. Output a JSON array; each item has {entity_name, entity_kind, layer, content, importance}.
|
|
67
|
-
|
|
68
|
-
Layers (use exactly one per memory):
|
|
69
|
-
- goal: WHY this work exists, target outcome
|
|
70
|
-
- context: WHY THIS NOW, situation, timing
|
|
71
|
-
- emotion: USER tone, feelings expressed
|
|
72
|
-
- implementation: HOW it was done, what worked, what failed
|
|
73
|
-
- caveat: PAIN lesson, "never X" / "always Y" — these are protected from forgetting
|
|
74
|
-
- learning: GROWTH, decisions made, insights
|
|
75
|
-
|
|
76
|
-
Rules:
|
|
77
|
-
- Max 6 entries (one per layer). Skip layers with nothing worth saving.
|
|
78
|
-
- Importance 0-1. Set 0.9+ to pin (use sparingly).
|
|
79
|
-
- Caveats must be 1 sentence and start with a verb.
|
|
80
|
-
- Quote nothing verbatim; summarize.
|
|
81
|
-
|
|
82
|
-
Transcript:
|
|
66
|
+
text: `You are an agent-memory writer. Read the session transcript below and propose memories to save. Output a JSON array; each item has {entity_name, entity_kind, layer, content, importance}.
|
|
67
|
+
|
|
68
|
+
Layers (use exactly one per memory):
|
|
69
|
+
- goal: WHY this work exists, target outcome
|
|
70
|
+
- context: WHY THIS NOW, situation, timing
|
|
71
|
+
- emotion: USER tone, feelings expressed
|
|
72
|
+
- implementation: HOW it was done, what worked, what failed
|
|
73
|
+
- caveat: PAIN lesson, "never X" / "always Y" — these are protected from forgetting
|
|
74
|
+
- learning: GROWTH, decisions made, insights
|
|
75
|
+
|
|
76
|
+
Rules:
|
|
77
|
+
- Max 6 entries (one per layer). Skip layers with nothing worth saving.
|
|
78
|
+
- Importance 0-1. Set 0.9+ to pin (use sparingly).
|
|
79
|
+
- Caveats must be 1 sentence and start with a verb.
|
|
80
|
+
- Quote nothing verbatim; summarize.
|
|
81
|
+
|
|
82
|
+
Transcript:
|
|
83
83
|
${transcript}${entityHint}`,
|
|
84
84
|
},
|
|
85
85
|
},
|
|
@@ -96,15 +96,15 @@ ${transcript}${entityHint}`,
|
|
|
96
96
|
role: 'user',
|
|
97
97
|
content: {
|
|
98
98
|
type: 'text',
|
|
99
|
-
text: `You are a caveat extractor. Read the source text below and output a JSON list of caveats. Each caveat:
|
|
100
|
-
- Starts with a verb ("Never", "Always", "Watch out", "Reject", "Confirm")
|
|
101
|
-
- Is one sentence
|
|
102
|
-
- Is concrete (a specific failure mode, not abstract advice)
|
|
103
|
-
- Captures something the reader does NOT want to relearn the hard way
|
|
104
|
-
|
|
105
|
-
Output format: [{"content": "Never X when Y, because Z", "importance": 0.7-1.0}]
|
|
106
|
-
|
|
107
|
-
Source text:
|
|
99
|
+
text: `You are a caveat extractor. Read the source text below and output a JSON list of caveats. Each caveat:
|
|
100
|
+
- Starts with a verb ("Never", "Always", "Watch out", "Reject", "Confirm")
|
|
101
|
+
- Is one sentence
|
|
102
|
+
- Is concrete (a specific failure mode, not abstract advice)
|
|
103
|
+
- Captures something the reader does NOT want to relearn the hard way
|
|
104
|
+
|
|
105
|
+
Output format: [{"content": "Never X when Y, because Z", "importance": 0.7-1.0}]
|
|
106
|
+
|
|
107
|
+
Source text:
|
|
108
108
|
${text}${entityHint}`,
|
|
109
109
|
},
|
|
110
110
|
},
|
|
@@ -121,17 +121,17 @@ ${text}${entityHint}`,
|
|
|
121
121
|
role: 'user',
|
|
122
122
|
content: {
|
|
123
123
|
type: 'text',
|
|
124
|
-
text: `Consolidate the past week's memories for entity "${entityName}" (week_offset=${weekOffset}).
|
|
125
|
-
|
|
126
|
-
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=4000) to retrieve recent memories.
|
|
127
|
-
Step 2: Read the returned memories and produce ONE learning-layer summary that captures:
|
|
128
|
-
- What we set out to do (goal trajectory)
|
|
129
|
-
- What actually happened (implementation summary)
|
|
130
|
-
- What we learned (1-3 insights)
|
|
131
|
-
- Any caveats we should never forget (preserve these — do NOT consolidate them away)
|
|
132
|
-
|
|
133
|
-
Step 3: Output a JSON object {entity_name, layer:"learning", content, importance:0.7}.
|
|
134
|
-
|
|
124
|
+
text: `Consolidate the past week's memories for entity "${entityName}" (week_offset=${weekOffset}).
|
|
125
|
+
|
|
126
|
+
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=4000) to retrieve recent memories.
|
|
127
|
+
Step 2: Read the returned memories and produce ONE learning-layer summary that captures:
|
|
128
|
+
- What we set out to do (goal trajectory)
|
|
129
|
+
- What actually happened (implementation summary)
|
|
130
|
+
- What we learned (1-3 insights)
|
|
131
|
+
- Any caveats we should never forget (preserve these — do NOT consolidate them away)
|
|
132
|
+
|
|
133
|
+
Step 3: Output a JSON object {entity_name, layer:"learning", content, importance:0.7}.
|
|
134
|
+
|
|
135
135
|
Do not write to memory directly — just return the JSON. The user will choose whether to save.`,
|
|
136
136
|
},
|
|
137
137
|
},
|
|
@@ -151,15 +151,15 @@ Do not write to memory directly — just return the JSON. The user will choose w
|
|
|
151
151
|
role: 'user',
|
|
152
152
|
content: {
|
|
153
153
|
type: 'text',
|
|
154
|
-
text: `Before doing this task, recall first.
|
|
155
|
-
|
|
156
|
-
Task: ${task}
|
|
157
|
-
|
|
158
|
-
Step 1: Call ${recallCmd}.
|
|
159
|
-
Step 2: Skim the returned memories. Identify any caveats that apply.
|
|
160
|
-
Step 3: Produce your output with INLINE citations to relevant memory_ids: e.g. "Use better-sqlite3 v12+ [memory:1234] because v11 breaks on Node 24 [memory:5678]."
|
|
161
|
-
Step 4: If you found NO relevant memories, say so explicitly: "No prior memories on this — proceeding from first principles."
|
|
162
|
-
|
|
154
|
+
text: `Before doing this task, recall first.
|
|
155
|
+
|
|
156
|
+
Task: ${task}
|
|
157
|
+
|
|
158
|
+
Step 1: Call ${recallCmd}.
|
|
159
|
+
Step 2: Skim the returned memories. Identify any caveats that apply.
|
|
160
|
+
Step 3: Produce your output with INLINE citations to relevant memory_ids: e.g. "Use better-sqlite3 v12+ [memory:1234] because v11 breaks on Node 24 [memory:5678]."
|
|
161
|
+
Step 4: If you found NO relevant memories, say so explicitly: "No prior memories on this — proceeding from first principles."
|
|
162
|
+
|
|
163
163
|
Goal: never solve a problem twice without checking.`,
|
|
164
164
|
},
|
|
165
165
|
},
|
|
@@ -176,18 +176,18 @@ Goal: never solve a problem twice without checking.`,
|
|
|
176
176
|
role: 'user',
|
|
177
177
|
content: {
|
|
178
178
|
type: 'text',
|
|
179
|
-
text: `Produce a handoff document for entity "${entityName}", aimed at: ${audience}.
|
|
180
|
-
|
|
181
|
-
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=6000).
|
|
182
|
-
Step 2: Skim memories grouped by layer.
|
|
183
|
-
Step 3: Output a markdown doc with these sections:
|
|
184
|
-
- **Identity**: name, kind, canonical key (if any)
|
|
185
|
-
- **Goal** (from goal-layer memories): what this entity is for
|
|
186
|
-
- **State** (from latest implementation memories): where things stand right now
|
|
187
|
-
- **Caveats** (from caveat-layer memories): what NEVER to do, with memory_id citations
|
|
188
|
-
- **Open questions**: things the prior session left unresolved
|
|
189
|
-
- **Suggested next steps**: 3 concrete actions for the audience
|
|
190
|
-
|
|
179
|
+
text: `Produce a handoff document for entity "${entityName}", aimed at: ${audience}.
|
|
180
|
+
|
|
181
|
+
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=6000).
|
|
182
|
+
Step 2: Skim memories grouped by layer.
|
|
183
|
+
Step 3: Output a markdown doc with these sections:
|
|
184
|
+
- **Identity**: name, kind, canonical key (if any)
|
|
185
|
+
- **Goal** (from goal-layer memories): what this entity is for
|
|
186
|
+
- **State** (from latest implementation memories): where things stand right now
|
|
187
|
+
- **Caveats** (from caveat-layer memories): what NEVER to do, with memory_id citations
|
|
188
|
+
- **Open questions**: things the prior session left unresolved
|
|
189
|
+
- **Suggested next steps**: 3 concrete actions for the audience
|
|
190
|
+
|
|
191
191
|
Keep it under 1 page. Cite memory_ids inline like [memory:1234].`,
|
|
192
192
|
},
|
|
193
193
|
},
|
package/dist/mcp/resources.js
CHANGED
|
@@ -71,10 +71,10 @@ function fmtMemory(row) {
|
|
|
71
71
|
access_count: row.access_count,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
const SELECT_MEMORY_BASE = `
|
|
75
|
-
SELECT m.id, m.layer, m.content, m.importance, m.protected, m.created_at, m.last_accessed_at, m.access_count,
|
|
76
|
-
e.name as entity_name, e.kind as entity_kind
|
|
77
|
-
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
74
|
+
const SELECT_MEMORY_BASE = `
|
|
75
|
+
SELECT m.id, m.layer, m.content, m.importance, m.protected, m.created_at, m.last_accessed_at, m.access_count,
|
|
76
|
+
e.name as entity_name, e.kind as entity_kind
|
|
77
|
+
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
78
78
|
`;
|
|
79
79
|
export function readResource(db, uri) {
|
|
80
80
|
// Static endpoints
|
|
@@ -101,9 +101,9 @@ export function readResource(db, uri) {
|
|
|
101
101
|
// heat_band is dynamic; approximate "hot" via access_count + recent access desc.
|
|
102
102
|
// For exact heat scoring, use the recall tool with band='hot'.
|
|
103
103
|
const rows = db
|
|
104
|
-
.prepare(`${SELECT_MEMORY_BASE}
|
|
105
|
-
WHERE m.last_accessed_at IS NOT NULL
|
|
106
|
-
ORDER BY m.access_count DESC, m.last_accessed_at DESC
|
|
104
|
+
.prepare(`${SELECT_MEMORY_BASE}
|
|
105
|
+
WHERE m.last_accessed_at IS NOT NULL
|
|
106
|
+
ORDER BY m.access_count DESC, m.last_accessed_at DESC
|
|
107
107
|
LIMIT 50`)
|
|
108
108
|
.all();
|
|
109
109
|
return {
|
package/dist/mcp/sampling.js
CHANGED
|
@@ -38,15 +38,15 @@ export async function sampleConsolidation(server, memoryTexts, entityName) {
|
|
|
38
38
|
if (memoryTexts.length === 0)
|
|
39
39
|
return { ok: false, reason: 'no input memories' };
|
|
40
40
|
const numbered = memoryTexts.map((t, i) => `[${i + 1}] ${t}`).join('\n\n');
|
|
41
|
-
const userPrompt = `Consolidate these ${memoryTexts.length} memories about "${entityName}" into a single learning-layer summary. The summary must:
|
|
42
|
-
- Preserve the trajectory (what was attempted, what worked, what changed)
|
|
43
|
-
- Be 2-4 sentences max
|
|
44
|
-
- Not invent facts not in the originals
|
|
45
|
-
- Not include verbatim quotes
|
|
46
|
-
|
|
47
|
-
Memories:
|
|
48
|
-
${numbered}
|
|
49
|
-
|
|
41
|
+
const userPrompt = `Consolidate these ${memoryTexts.length} memories about "${entityName}" into a single learning-layer summary. The summary must:
|
|
42
|
+
- Preserve the trajectory (what was attempted, what worked, what changed)
|
|
43
|
+
- Be 2-4 sentences max
|
|
44
|
+
- Not invent facts not in the originals
|
|
45
|
+
- Not include verbatim quotes
|
|
46
|
+
|
|
47
|
+
Memories:
|
|
48
|
+
${numbered}
|
|
49
|
+
|
|
50
50
|
Output the summary text only — no preamble, no JSON wrapper.`;
|
|
51
51
|
return sample(server, {
|
|
52
52
|
systemPrompt: 'You are a memory consolidator. Compress without distorting.',
|
|
@@ -56,17 +56,17 @@ Output the summary text only — no preamble, no JSON wrapper.`;
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
export async function sampleLayerClassification(server, content) {
|
|
59
|
-
const userPrompt = `Classify this memory content into exactly one layer:
|
|
60
|
-
- goal: WHY this work exists, target outcome
|
|
61
|
-
- context: WHY THIS NOW, situation, timing
|
|
62
|
-
- emotion: USER tone, feelings expressed
|
|
63
|
-
- implementation: HOW it was done, what worked, what failed
|
|
64
|
-
- caveat: PAIN lesson, "never X" / "always Y"
|
|
65
|
-
- learning: GROWTH, decisions made, insights
|
|
66
|
-
|
|
67
|
-
Content:
|
|
68
|
-
${content}
|
|
69
|
-
|
|
59
|
+
const userPrompt = `Classify this memory content into exactly one layer:
|
|
60
|
+
- goal: WHY this work exists, target outcome
|
|
61
|
+
- context: WHY THIS NOW, situation, timing
|
|
62
|
+
- emotion: USER tone, feelings expressed
|
|
63
|
+
- implementation: HOW it was done, what worked, what failed
|
|
64
|
+
- caveat: PAIN lesson, "never X" / "always Y"
|
|
65
|
+
- learning: GROWTH, decisions made, insights
|
|
66
|
+
|
|
67
|
+
Content:
|
|
68
|
+
${content}
|
|
69
|
+
|
|
70
70
|
Output just the layer name — no explanation, no JSON.`;
|
|
71
71
|
return sample(server, {
|
|
72
72
|
systemPrompt: 'You are a layer classifier. Output one word.',
|