linksee-memory 0.12.0 → 0.13.0
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 +1014 -1004
- package/dist/bin/setup.js +60 -47
- 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-wiring.d.ts +54 -0
- package/dist/lib/guard-wiring.js +85 -0
- package/dist/lib/guard.js +38 -31
- package/dist/lib/map-import.js +17 -17
- package/dist/lib/truth-engine.js +13 -13
- 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 +130 -130
- package/package.json +96 -95
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.',
|
package/dist/mcp/server.js
CHANGED
|
@@ -479,15 +479,15 @@ function appendAxisFilters(sql, params, filters) {
|
|
|
479
479
|
return sql;
|
|
480
480
|
}
|
|
481
481
|
function runFtsQuery(query, layer, limit, axis) {
|
|
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 ?
|
|
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 ?
|
|
491
491
|
`;
|
|
492
492
|
const params = [query];
|
|
493
493
|
if (layer) {
|
|
@@ -501,14 +501,14 @@ function runFtsQuery(query, layer, limit, axis) {
|
|
|
501
501
|
return db.prepare(sql).all(...params);
|
|
502
502
|
}
|
|
503
503
|
function runLikeQuery(query, entityName, layer, limit, axis) {
|
|
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
|
|
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
|
|
512
512
|
`;
|
|
513
513
|
const params = [];
|
|
514
514
|
if (entityName) {
|
|
@@ -839,8 +839,8 @@ function handleForget(args) {
|
|
|
839
839
|
}
|
|
840
840
|
// Auto-sweep — also respect pin (importance >= 0.9) as protection
|
|
841
841
|
const rows = db
|
|
842
|
-
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
843
|
-
FROM memories
|
|
842
|
+
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
843
|
+
FROM memories
|
|
844
844
|
WHERE protected = 0 AND importance < 0.9`)
|
|
845
845
|
.all();
|
|
846
846
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -889,17 +889,17 @@ function handleConsolidate(args) {
|
|
|
889
889
|
// read-only audit: count candidates using the same rules.
|
|
890
890
|
const now = Math.floor(Date.now() / 1000);
|
|
891
891
|
const ageCutoff = now - (args.min_age_days ?? 7) * 86400;
|
|
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
|
|
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
|
|
903
903
|
`).all(ageCutoff);
|
|
904
904
|
const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
|
|
905
905
|
return JSON.stringify({
|
|
@@ -973,19 +973,19 @@ function handleListEntities(args) {
|
|
|
973
973
|
const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
|
|
974
974
|
const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
|
|
975
975
|
const offset = Math.max(0, Number(args?.offset ?? 0));
|
|
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
|
|
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
|
|
989
989
|
`;
|
|
990
990
|
const params = [];
|
|
991
991
|
if (kind) {
|
|
@@ -1037,29 +1037,29 @@ function handleRecallFile(args) {
|
|
|
1037
1037
|
return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
|
|
1038
1038
|
}
|
|
1039
1039
|
// Daily breakdown
|
|
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
|
|
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
|
|
1044
1044
|
`).all(`%${sub}%`);
|
|
1045
1045
|
// Distinct context_snippets (intents) — deduped, ordered by recency
|
|
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 ?
|
|
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 ?
|
|
1053
1053
|
`).all(`%${sub}%`, maxIntents);
|
|
1054
1054
|
// Linked memories
|
|
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
|
+
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
|
|
1063
1063
|
`).all(`%${sub}%`);
|
|
1064
1064
|
// Distinct file paths matched (the substring may match multiple files)
|
|
1065
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}%`);
|
|
@@ -1126,10 +1126,10 @@ async function handleConsolidateWithSampling(args) {
|
|
|
1126
1126
|
const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
|
|
1127
1127
|
const snapshot = new Map();
|
|
1128
1128
|
const candidateRows = db
|
|
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')
|
|
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')
|
|
1133
1133
|
AND m.created_at <= ?`)
|
|
1134
1134
|
.all(ageCutoff);
|
|
1135
1135
|
for (const r of candidateRows)
|
|
@@ -1352,8 +1352,8 @@ function resolveProjectFromRecentEdits() {
|
|
|
1352
1352
|
if (projects.length <= 1)
|
|
1353
1353
|
return { project: projects[0] };
|
|
1354
1354
|
const rows = db
|
|
1355
|
-
.prepare(`SELECT file_path FROM session_file_edits
|
|
1356
|
-
WHERE occurred_at > unixepoch() - 86400
|
|
1355
|
+
.prepare(`SELECT file_path FROM session_file_edits
|
|
1356
|
+
WHERE occurred_at > unixepoch() - 86400
|
|
1357
1357
|
ORDER BY occurred_at DESC LIMIT 50`)
|
|
1358
1358
|
.all();
|
|
1359
1359
|
if (rows.length === 0)
|
|
@@ -1504,25 +1504,25 @@ function handleFlagProposals(args) {
|
|
|
1504
1504
|
const sessionContext = args.session_context ?? 'conversation session';
|
|
1505
1505
|
const PROPOSAL_TAG = 'orphaned_proposal';
|
|
1506
1506
|
const now = Math.floor(Date.now() / 1000);
|
|
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
|
-
)
|
|
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
|
+
)
|
|
1517
1517
|
`);
|
|
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
|
-
)
|
|
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
|
+
)
|
|
1526
1526
|
`);
|
|
1527
1527
|
const results = [];
|
|
1528
1528
|
const txn = db.transaction(() => {
|
|
@@ -1566,13 +1566,13 @@ function handleFlagProposals(args) {
|
|
|
1566
1566
|
function handleDream(args) {
|
|
1567
1567
|
const domainFilter = args?.domain;
|
|
1568
1568
|
// 1. Fetch active North Star anchor(s)
|
|
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
|
|
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
|
|
1576
1576
|
`).all();
|
|
1577
1577
|
// Re-injection friction — the active-observability loop closing back into reflection.
|
|
1578
1578
|
// "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
|
|
@@ -1589,18 +1589,18 @@ function handleDream(args) {
|
|
|
1589
1589
|
// why-strings so the existing backlog is drainable without a backfill write.
|
|
1590
1590
|
let distillQueue = [];
|
|
1591
1591
|
try {
|
|
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
|
|
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
|
|
1604
1604
|
`).all();
|
|
1605
1605
|
distillQueue = rows.map((r) => {
|
|
1606
1606
|
let c = {};
|
|
@@ -1638,26 +1638,26 @@ function handleDream(args) {
|
|
|
1638
1638
|
}
|
|
1639
1639
|
// 2. Fetch pending orphaned proposals
|
|
1640
1640
|
const candidateQuery = domainFilter
|
|
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 = ?
|
|
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 = ?
|
|
1651
1651
|
ORDER BY mc.created_at DESC`
|
|
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'
|
|
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'
|
|
1661
1661
|
ORDER BY mc.created_at DESC`;
|
|
1662
1662
|
const candidates = domainFilter
|
|
1663
1663
|
? db.prepare(candidateQuery).all(domainFilter)
|
|
@@ -1737,12 +1737,12 @@ function handleResolveProposal(args) {
|
|
|
1737
1737
|
throw new Error('verdict must be "surface" or "dismiss"');
|
|
1738
1738
|
}
|
|
1739
1739
|
// Verify candidate exists and is pending
|
|
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'
|
|
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'
|
|
1746
1746
|
`).get(candidateId);
|
|
1747
1747
|
if (!candidate) {
|
|
1748
1748
|
throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
|