linksee-memory 0.12.0 → 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.
@@ -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) {
@@ -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
@@ -16,8 +16,8 @@ import { supersededAnchorIds } from './truth-engine.js';
16
16
  // "accepted = the only thing the gate compares against": declared (status active), still live
17
17
  // (lifecycle active|experiment — NOT at_risk/superseded/deprecated), and not explicitly card-disabled.
18
18
  // at_risk(stale) anchors deliberately DON'T gate — we don't enforce a rule we're no longer sure of.
19
- const ACCEPTED_SQL = `status = 'active'
20
- AND lifecycle IN ('active', 'experiment')
19
+ const ACCEPTED_SQL = `status = 'active'
20
+ AND lifecycle IN ('active', 'experiment')
21
21
  AND COALESCE(json_extract(card_policy, '$.enabled'), 1) != 0`;
22
22
  const nowSec = () => Math.floor(Date.now() / 1000);
23
23
  function jsonGet(json, key, def) {
@@ -39,7 +39,7 @@ function bestEffort(fn) {
39
39
  }
40
40
  export function acceptedAnchors(db) {
41
41
  const rows = db
42
- .prepare(`SELECT id, kind, statement, rationale, affects, detect_terms, violation_signal, card_policy
42
+ .prepare(`SELECT id, kind, statement, rationale, affects, detect_terms, violation_signal, card_policy
43
43
  FROM drift_anchors WHERE ${ACCEPTED_SQL}`)
44
44
  .all();
45
45
  // A superseded anchor must stop gating. The block/warn text tells the user to run
@@ -130,7 +130,7 @@ function withinCooldown(db, anchorId, sessionId) {
130
130
  return !!row;
131
131
  }
132
132
  function logInjection(db, matches, act, surface, sessionId) {
133
- const ins = db.prepare(`INSERT INTO injection_log (anchor_id, session_id, trigger, surface, tool_name, action_snip, verdict)
133
+ const ins = db.prepare(`INSERT INTO injection_log (anchor_id, session_id, trigger, surface, tool_name, action_snip, verdict)
134
134
  VALUES (?, ?, 'gate', ?, ?, ?, ?)`);
135
135
  const snip = (act.lines[0] ?? '').slice(0, 120);
136
136
  const tx = db.transaction(() => {
@@ -185,16 +185,16 @@ export function buildBootDigest(db, opts = {}) {
185
185
  // Same rule as the gate: never re-surface a decision the user has already superseded.
186
186
  const retiredIds = supersededAnchorIds(db);
187
187
  const anchorRows = db
188
- .prepare(`SELECT id, statement, rationale FROM drift_anchors
189
- WHERE ${ACCEPTED_SQL} AND kind IN ('prohibition', 'decision', 'constraint')
188
+ .prepare(`SELECT id, statement, rationale FROM drift_anchors
189
+ WHERE ${ACCEPTED_SQL} AND kind IN ('prohibition', 'decision', 'constraint')
190
190
  ORDER BY confidence DESC, updated_at DESC LIMIT ?`)
191
191
  .all(maxAnchors + retiredIds.size);
192
192
  const anchors = anchorRows.filter((a) => !retiredIds.has(a.id)).slice(0, maxAnchors);
193
193
  const forks = db
194
- .prepare(`SELECT c.id, c.rationale, a.statement
195
- FROM memory_write_candidates c
196
- LEFT JOIN drift_anchors a ON a.id = c.target_node_id
197
- WHERE c.scope = 'orphaned_proposal' AND c.status = 'pending_review'
194
+ .prepare(`SELECT c.id, c.rationale, a.statement
195
+ FROM memory_write_candidates c
196
+ LEFT JOIN drift_anchors a ON a.id = c.target_node_id
197
+ WHERE c.scope = 'orphaned_proposal' AND c.status = 'pending_review'
198
198
  ORDER BY c.created_at DESC LIMIT ?`)
199
199
  .all(maxForks);
200
200
  // Distillation pressure — the routine's structural trigger. The drain must not depend on
@@ -202,10 +202,10 @@ export function buildBootDigest(db, opts = {}) {
202
202
  // EVERY session boot says so. Inflow (new sessions) vs drain (8/dream) stays visible.
203
203
  let distill = 0;
204
204
  try {
205
- distill = db.prepare(`SELECT COUNT(*) AS n FROM memories
206
- WHERE layer IN ('learning', 'caveat') AND json_valid(content)
207
- AND (json_extract(content, '$.needs_distill') = 1
208
- OR json_extract(content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
205
+ distill = db.prepare(`SELECT COUNT(*) AS n FROM memories
206
+ WHERE layer IN ('learning', 'caveat') AND json_valid(content)
207
+ AND (json_extract(content, '$.needs_distill') = 1
208
+ OR json_extract(content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
209
209
  OR json_extract(content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')`).get().n;
210
210
  }
211
211
  catch {
@@ -238,11 +238,11 @@ export function buildBootDigest(db, opts = {}) {
238
238
  export function backfillHeeded(db) {
239
239
  bestEffort(() => {
240
240
  const live = `SELECT anchor_id FROM drift_edges WHERE verdict = 'contradicts' AND status = 'open'`;
241
- db.prepare(`UPDATE injection_log SET heeded = 0
242
- WHERE heeded IS NULL AND surface IN ('warn', 'inform') AND verdict = 'contradicts'
241
+ db.prepare(`UPDATE injection_log SET heeded = 0
242
+ WHERE heeded IS NULL AND surface IN ('warn', 'inform') AND verdict = 'contradicts'
243
243
  AND anchor_id IN (${live})`).run();
244
- db.prepare(`UPDATE injection_log SET heeded = 1
245
- WHERE heeded IS NULL AND surface IN ('warn', 'inform')
244
+ db.prepare(`UPDATE injection_log SET heeded = 1
245
+ WHERE heeded IS NULL AND surface IN ('warn', 'inform')
246
246
  AND anchor_id NOT IN (${live})`).run();
247
247
  db.prepare(`UPDATE injection_log SET heeded = 1 WHERE heeded IS NULL AND surface = 'block'`).run();
248
248
  });
@@ -254,16 +254,16 @@ export function getReinjectionFriction(db, opts = {}) {
254
254
  const minC = opts.minContradicts ?? 3;
255
255
  backfillHeeded(db);
256
256
  const rows = db
257
- .prepare(`SELECT i.anchor_id,
258
- SUM(CASE WHEN i.verdict = 'contradicts' THEN 1 ELSE 0 END) AS gate_contradicts,
259
- SUM(CASE WHEN i.surface = 'block' THEN 1 ELSE 0 END) AS gate_blocks,
260
- SUM(CASE WHEN i.heeded = 0 THEN 1 ELSE 0 END) AS ignored,
261
- datetime(MAX(i.occurred_at), 'unixepoch') AS last_at,
262
- a.statement, a.lifecycle, a.card_policy
263
- FROM injection_log i
264
- JOIN drift_anchors a ON a.id = i.anchor_id
265
- WHERE a.status = 'active'
266
- GROUP BY i.anchor_id
257
+ .prepare(`SELECT i.anchor_id,
258
+ SUM(CASE WHEN i.verdict = 'contradicts' THEN 1 ELSE 0 END) AS gate_contradicts,
259
+ SUM(CASE WHEN i.surface = 'block' THEN 1 ELSE 0 END) AS gate_blocks,
260
+ SUM(CASE WHEN i.heeded = 0 THEN 1 ELSE 0 END) AS ignored,
261
+ datetime(MAX(i.occurred_at), 'unixepoch') AS last_at,
262
+ a.statement, a.lifecycle, a.card_policy
263
+ FROM injection_log i
264
+ JOIN drift_anchors a ON a.id = i.anchor_id
265
+ WHERE a.status = 'active'
266
+ GROUP BY i.anchor_id
267
267
  HAVING gate_contradicts >= ?`)
268
268
  .all(minC);
269
269
  const realityStmt = db.prepare(`SELECT COUNT(*) AS n FROM drift_edges WHERE anchor_id = ? AND verdict = 'contradicts' AND status = 'open'`);
@@ -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);
@@ -144,7 +144,7 @@ export function getTruthView(db, opts = {}) {
144
144
  const resolutionFor = buildResolutionLookup(db);
145
145
  // ── Candidates (indexed by target node) ──
146
146
  const candRows = db
147
- .prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status, proposed_node
147
+ .prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status, proposed_node
148
148
  FROM memory_write_candidates ORDER BY id DESC`)
149
149
  .all();
150
150
  const pendingByNode = new Map();
@@ -165,8 +165,8 @@ export function getTruthView(db, opts = {}) {
165
165
  // anchors it has hard evidence against. (2026-09-05: 11 open `contradicts` edges across 4
166
166
  // anchors — one of them the PII constraint — all rendered "reality matches intent".)
167
167
  const edgeRows = db
168
- .prepare(`SELECT anchor_id, verdict, confidence, evidence, detected_at
169
- FROM drift_edges WHERE status = 'open'
168
+ .prepare(`SELECT anchor_id, verdict, confidence, evidence, detected_at
169
+ FROM drift_edges WHERE status = 'open'
170
170
  ORDER BY detected_at DESC`)
171
171
  .all();
172
172
  const openEdges = new Map();
@@ -176,8 +176,8 @@ export function getTruthView(db, opts = {}) {
176
176
  openEdges.get(e.anchor_id).push(e);
177
177
  }
178
178
  // ── Active nodes + state derivation ──
179
- let sql = `SELECT id, node_type, domain, decision_mode, statement, rationale, confidence, lifecycle,
180
- card_policy, review_after
179
+ let sql = `SELECT id, node_type, domain, decision_mode, statement, rationale, confidence, lifecycle,
180
+ card_policy, review_after
181
181
  FROM drift_anchors WHERE status = 'active'`;
182
182
  const params = [];
183
183
  if (opts.domain) {
@@ -350,8 +350,8 @@ export function getTruthView(db, opts = {}) {
350
350
  // ── check_decision: single-node deep view ────────────────────────────────────
351
351
  export function getDecisionDetail(db, anchorId) {
352
352
  const row = db
353
- .prepare(`SELECT id, kind, node_type, domain, decision_mode, statement, rationale, confidence,
354
- lifecycle, card_policy, review_after, affects, detect_terms, violation_signal, tier
353
+ .prepare(`SELECT id, kind, node_type, domain, decision_mode, statement, rationale, confidence,
354
+ lifecycle, card_policy, review_after, affects, detect_terms, violation_signal, tier
355
355
  FROM drift_anchors WHERE id = ? AND status = 'active'`)
356
356
  .get(anchorId);
357
357
  if (!row)
@@ -369,14 +369,14 @@ export function getDecisionDetail(db, anchorId) {
369
369
  // State derivation (same logic)
370
370
  let state, accounted, accountedBy;
371
371
  const pendingCand = db
372
- .prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status
373
- FROM memory_write_candidates
374
- WHERE target_node_id = ? AND status = 'pending_review'
372
+ .prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status
373
+ FROM memory_write_candidates
374
+ WHERE target_node_id = ? AND status = 'pending_review'
375
375
  ORDER BY id DESC`)
376
376
  .all(anchorId);
377
377
  const cardCand = db
378
- .prepare(`SELECT rationale FROM memory_write_candidates
379
- WHERE target_node_id = ? AND proposed_node LIKE '%"src":"t%'
378
+ .prepare(`SELECT rationale FROM memory_write_candidates
379
+ WHERE target_node_id = ? AND proposed_node LIKE '%"src":"t%'
380
380
  ORDER BY id DESC LIMIT 1`)
381
381
  .get(anchorId);
382
382
  const hasPending = pendingCand.length > 0;
@@ -414,7 +414,7 @@ export function getDecisionDetail(db, anchorId) {
414
414
  ?? (state === 'aligned' ? 'Committed reality matches intent (convergent)' : null);
415
415
  // Drift edges for this anchor
416
416
  const edges = db
417
- .prepare(`SELECT id AS edge_id, verdict, confidence, status, detected_at
417
+ .prepare(`SELECT id AS edge_id, verdict, confidence, status, detected_at
418
418
  FROM drift_edges WHERE anchor_id = ? ORDER BY detected_at DESC`)
419
419
  .all(anchorId);
420
420
  return {
@@ -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.',