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.
@@ -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
  },
@@ -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 {
@@ -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.',
@@ -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`);