linksee-memory 0.11.5 → 0.12.1

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