linksee-memory 0.17.0 → 0.17.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.
@@ -6,11 +6,11 @@
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
- import { openDb, runMigrations } from '../db/migrate.js';
9
+ import { openDb, runMigrations, DbRefusedError } from '../db/migrate.js';
10
10
  import { computeHeat } from '../lib/heat-index.js';
11
11
  import { decideForgetting } from '../lib/forgetting.js';
12
12
  import { refreshMomentumForEntity } from '../lib/momentum.js';
13
- import { consolidate as runConsolidate } from '../lib/consolidate.js';
13
+ import { consolidate as runConsolidate, autoConsolidateEnabled } from '../lib/consolidate.js';
14
14
  import { isPastedExternalContent } from '../lib/session-parser.js';
15
15
  import { inferAltitude, inferType, inferState } from '../lib/session-extractor.js';
16
16
  import { normalizeEntityName } from '../lib/normalize.js';
@@ -72,8 +72,20 @@ if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand))
72
72
  const r = spawnSync(process.execPath, [binPath, ...process.argv.slice(3)], { stdio: 'inherit' });
73
73
  process.exit(r.status ?? (r.signal ? 1 : 0));
74
74
  }
75
- const db = openDb();
76
- runMigrations(db);
75
+ let db;
76
+ try {
77
+ db = openDb();
78
+ runMigrations(db);
79
+ }
80
+ catch (e) {
81
+ // A pre-June-2026 DB (schema < 6) is refused before any write; print what to do
82
+ // instead of a stack trace. MCP clients show the server's stderr in their logs.
83
+ if (e instanceof DbRefusedError) {
84
+ process.stderr.write(`${e.message}\n`);
85
+ process.exit(1);
86
+ }
87
+ throw e;
88
+ }
77
89
  // Distill triage runs on EVERY start, before the first request. It is one indexed scan and a
78
90
  // handful of JSON rewrites — cheap — and 0.16.0 had tucked it inside the auto-consolidate block
79
91
  // below, which runs at most once a week and only after a 3-second timer. So it never ran.
@@ -84,29 +96,37 @@ try {
84
96
  }
85
97
  }
86
98
  catch { /* never block startup */ }
87
- // Auto-maintenance: consolidate stale memories on startup (non-blocking)
88
- setTimeout(() => {
89
- try {
90
- let shouldRun = true;
99
+ // Auto-maintenance: consolidate stale memories on startup (non-blocking).
100
+ //
101
+ // OFF by default since 0.17.1 (L-1-106 / audit F1): consolidate deletes the original rows after
102
+ // writing a summary that keeps only the top 3 × 200 characters, ignores thread_id, and cascades
103
+ // away their edges — reasons and history lost with no action from the user. It now runs only
104
+ // when asked: LINKSEE_AUTO_CONSOLIDATE=1 (or `--auto-consolidate` on the server command line).
105
+ // There is no `consolidate` tool (merged away in 0.7.0). Rows already removed by earlier versions
106
+ // are not restored.
107
+ if (autoConsolidateEnabled(process.env, process.argv.slice(2)))
108
+ setTimeout(() => {
91
109
  try {
92
- // Check if consolidations table exists before querying
93
- const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='consolidations'").get();
94
- if (tableExists) {
95
- const lastRow = db.prepare('SELECT MAX(created_at) as ts FROM consolidations').get();
96
- if (lastRow?.ts && (Date.now() / 1000 - lastRow.ts) / 86400 < 7)
97
- shouldRun = false;
110
+ let shouldRun = true;
111
+ try {
112
+ // Check if consolidations table exists before querying
113
+ const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='consolidations'").get();
114
+ if (tableExists) {
115
+ const lastRow = db.prepare('SELECT MAX(created_at) as ts FROM consolidations').get();
116
+ if (lastRow?.ts && (Date.now() / 1000 - lastRow.ts) / 86400 < 7)
117
+ shouldRun = false;
118
+ }
119
+ }
120
+ catch {
121
+ shouldRun = false; /* genuinely unexpected — skip to be safe */
122
+ }
123
+ if (shouldRun) {
124
+ runConsolidate(db, { scope: 'all', min_age_days: 7 });
125
+ process.stderr.write('[linksee-memory] auto-consolidate complete\n');
98
126
  }
99
127
  }
100
- catch {
101
- shouldRun = false; /* genuinely unexpected — skip to be safe */
102
- }
103
- if (shouldRun) {
104
- runConsolidate(db, { scope: 'all', min_age_days: 7 });
105
- process.stderr.write('[linksee-memory] auto-consolidate complete\n');
106
- }
107
- }
108
- catch { /* non-fatal */ }
109
- }, 3000);
128
+ catch { /* non-fatal */ }
129
+ }, 3000);
110
130
  const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, {
111
131
  capabilities: {
112
132
  tools: {},
@@ -610,15 +630,15 @@ function appendAxisFilters(sql, params, filters) {
610
630
  return sql;
611
631
  }
612
632
  function runFtsQuery(query, layer, limit, axis) {
613
- let sql = `
614
- SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
615
- m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
616
- m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
617
- bm25(memories_fts) as bm25_score
618
- FROM memories_fts
619
- JOIN memories m ON m.id = memories_fts.rowid
620
- JOIN entities e ON e.id = m.entity_id
621
- WHERE memories_fts MATCH ?
633
+ let sql = `
634
+ SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
635
+ m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
636
+ m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
637
+ bm25(memories_fts) as bm25_score
638
+ FROM memories_fts
639
+ JOIN memories m ON m.id = memories_fts.rowid
640
+ JOIN entities e ON e.id = m.entity_id
641
+ WHERE memories_fts MATCH ?
622
642
  `;
623
643
  const params = [query];
624
644
  if (layer) {
@@ -632,14 +652,14 @@ function runFtsQuery(query, layer, limit, axis) {
632
652
  return db.prepare(sql).all(...params);
633
653
  }
634
654
  function runLikeQuery(query, entityName, layer, limit, axis) {
635
- let sql = `
636
- SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
637
- m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
638
- m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
639
- 0 as bm25_score
640
- FROM memories m
641
- JOIN entities e ON e.id = m.entity_id
642
- WHERE 1=1
655
+ let sql = `
656
+ SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
657
+ m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
658
+ m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
659
+ 0 as bm25_score
660
+ FROM memories m
661
+ JOIN entities e ON e.id = m.entity_id
662
+ WHERE 1=1
643
663
  `;
644
664
  const params = [];
645
665
  if (entityName) {
@@ -970,8 +990,8 @@ function handleForget(args) {
970
990
  }
971
991
  // Auto-sweep — also respect pin (importance >= 0.9) as protection
972
992
  const rows = db
973
- .prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
974
- FROM memories
993
+ .prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
994
+ FROM memories
975
995
  WHERE protected = 0 AND importance < 0.9`)
976
996
  .all();
977
997
  const now = Math.floor(Date.now() / 1000);
@@ -1020,17 +1040,17 @@ function handleConsolidate(args) {
1020
1040
  // read-only audit: count candidates using the same rules.
1021
1041
  const now = Math.floor(Date.now() / 1000);
1022
1042
  const ageCutoff = now - (args.min_age_days ?? 7) * 86400;
1023
- const candidates = db.prepare(`
1024
- SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
1025
- FROM memories m
1026
- JOIN entities e ON e.id = m.entity_id
1027
- WHERE m.protected = 0
1028
- AND m.importance < 0.9
1029
- AND m.layer IN ('context', 'emotion', 'implementation')
1030
- AND m.created_at <= ?
1031
- GROUP BY m.entity_id, m.layer
1032
- HAVING c >= 2
1033
- ORDER BY c DESC
1043
+ const candidates = db.prepare(`
1044
+ SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
1045
+ FROM memories m
1046
+ JOIN entities e ON e.id = m.entity_id
1047
+ WHERE m.protected = 0
1048
+ AND m.importance < 0.9
1049
+ AND m.layer IN ('context', 'emotion', 'implementation')
1050
+ AND m.created_at <= ?
1051
+ GROUP BY m.entity_id, m.layer
1052
+ HAVING c >= 2
1053
+ ORDER BY c DESC
1034
1054
  `).all(ageCutoff);
1035
1055
  const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
1036
1056
  return JSON.stringify({
@@ -1104,19 +1124,19 @@ function handleListEntities(args) {
1104
1124
  const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
1105
1125
  const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
1106
1126
  const offset = Math.max(0, Number(args?.offset ?? 0));
1107
- let sql = `
1108
- SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
1109
- e.updated_at, e.created_at,
1110
- COUNT(m.id) as memory_count,
1111
- MAX(m.last_accessed_at) as last_memory_access,
1112
- SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
1113
- SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
1114
- SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
1115
- SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
1116
- SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
1117
- FROM entities e
1118
- LEFT JOIN memories m ON m.entity_id = e.id
1119
- WHERE 1=1
1127
+ let sql = `
1128
+ SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
1129
+ e.updated_at, e.created_at,
1130
+ COUNT(m.id) as memory_count,
1131
+ MAX(m.last_accessed_at) as last_memory_access,
1132
+ SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
1133
+ SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
1134
+ SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
1135
+ SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
1136
+ SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
1137
+ FROM entities e
1138
+ LEFT JOIN memories m ON m.entity_id = e.id
1139
+ WHERE 1=1
1120
1140
  `;
1121
1141
  const params = [];
1122
1142
  if (kind) {
@@ -1168,29 +1188,29 @@ function handleRecallFile(args) {
1168
1188
  return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
1169
1189
  }
1170
1190
  // Daily breakdown
1171
- const daily = db.prepare(`
1172
- SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
1173
- FROM session_file_edits WHERE file_path LIKE ?
1174
- GROUP BY day, operation ORDER BY day
1191
+ const daily = db.prepare(`
1192
+ SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
1193
+ FROM session_file_edits WHERE file_path LIKE ?
1194
+ GROUP BY day, operation ORDER BY day
1175
1195
  `).all(`%${sub}%`);
1176
1196
  // Distinct context_snippets (intents) — deduped, ordered by recency
1177
- const intents = db.prepare(`
1178
- SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
1179
- FROM session_file_edits
1180
- WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
1181
- GROUP BY context_snippet
1182
- ORDER BY last_at DESC
1183
- LIMIT ?
1197
+ const intents = db.prepare(`
1198
+ SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
1199
+ FROM session_file_edits
1200
+ WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
1201
+ GROUP BY context_snippet
1202
+ ORDER BY last_at DESC
1203
+ LIMIT ?
1184
1204
  `).all(`%${sub}%`, maxIntents);
1185
1205
  // Linked memories
1186
- const memories = db.prepare(`
1187
- SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
1188
- FROM session_file_edits sfe
1189
- JOIN memories m ON m.id = sfe.memory_id
1190
- JOIN entities e ON e.id = m.entity_id
1191
- WHERE sfe.file_path LIKE ?
1192
- ORDER BY m.importance DESC
1193
- LIMIT 20
1206
+ const memories = db.prepare(`
1207
+ SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
1208
+ FROM session_file_edits sfe
1209
+ JOIN memories m ON m.id = sfe.memory_id
1210
+ JOIN entities e ON e.id = m.entity_id
1211
+ WHERE sfe.file_path LIKE ?
1212
+ ORDER BY m.importance DESC
1213
+ LIMIT 20
1194
1214
  `).all(`%${sub}%`);
1195
1215
  // Distinct file paths matched (the substring may match multiple files)
1196
1216
  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}%`);
@@ -1257,10 +1277,10 @@ async function handleConsolidateWithSampling(args) {
1257
1277
  const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
1258
1278
  const snapshot = new Map();
1259
1279
  const candidateRows = db
1260
- .prepare(`SELECT m.id, m.content, e.name as entity_name
1261
- FROM memories m JOIN entities e ON e.id = m.entity_id
1262
- WHERE m.protected = 0
1263
- AND m.layer IN ('context','emotion','implementation')
1280
+ .prepare(`SELECT m.id, m.content, e.name as entity_name
1281
+ FROM memories m JOIN entities e ON e.id = m.entity_id
1282
+ WHERE m.protected = 0
1283
+ AND m.layer IN ('context','emotion','implementation')
1264
1284
  AND m.created_at <= ?`)
1265
1285
  .all(ageCutoff);
1266
1286
  for (const r of candidateRows)
@@ -1626,8 +1646,8 @@ function resolveProjectFromRecentEdits() {
1626
1646
  if (projects.length <= 1)
1627
1647
  return { project: projects[0] };
1628
1648
  const rows = db
1629
- .prepare(`SELECT file_path FROM session_file_edits
1630
- WHERE occurred_at > unixepoch() - 86400
1649
+ .prepare(`SELECT file_path FROM session_file_edits
1650
+ WHERE occurred_at > unixepoch() - 86400
1631
1651
  ORDER BY occurred_at DESC LIMIT 50`)
1632
1652
  .all();
1633
1653
  if (rows.length === 0)
@@ -1815,25 +1835,25 @@ function handleFlagProposals(args) {
1815
1835
  const sessionContext = args.session_context ?? 'conversation session';
1816
1836
  const PROPOSAL_TAG = 'orphaned_proposal';
1817
1837
  const now = Math.floor(Date.now() / 1000);
1818
- const insertAnchor = db.prepare(`
1819
- INSERT INTO drift_anchors (
1820
- kind, statement, rationale, domain, decision_mode,
1821
- confidence, lifecycle, status, owner,
1822
- evidence_refs, created_at, updated_at
1823
- ) VALUES (
1824
- 'decision', ?, ?, ?, 'hypothesis',
1825
- ?, 'active', 'active', 'agent',
1826
- ?, ?, ?
1827
- )
1838
+ const insertAnchor = db.prepare(`
1839
+ INSERT INTO drift_anchors (
1840
+ kind, statement, rationale, domain, decision_mode,
1841
+ confidence, lifecycle, status, owner,
1842
+ evidence_refs, created_at, updated_at
1843
+ ) VALUES (
1844
+ 'decision', ?, ?, ?, 'hypothesis',
1845
+ ?, 'active', 'active', 'agent',
1846
+ ?, ?, ?
1847
+ )
1828
1848
  `);
1829
- const insertCandidate = db.prepare(`
1830
- INSERT INTO memory_write_candidates (
1831
- scope, candidate_type, target_node_id, rationale,
1832
- confidence, evidence_refs, status, created_at
1833
- ) VALUES (
1834
- 'orphaned_proposal', 'update_node', ?, ?,
1835
- ?, ?, 'pending_review', ?
1836
- )
1849
+ const insertCandidate = db.prepare(`
1850
+ INSERT INTO memory_write_candidates (
1851
+ scope, candidate_type, target_node_id, rationale,
1852
+ confidence, evidence_refs, status, created_at
1853
+ ) VALUES (
1854
+ 'orphaned_proposal', 'update_node', ?, ?,
1855
+ ?, ?, 'pending_review', ?
1856
+ )
1837
1857
  `);
1838
1858
  const results = [];
1839
1859
  const txn = db.transaction(() => {
@@ -1877,13 +1897,13 @@ function handleFlagProposals(args) {
1877
1897
  function handleDream(args) {
1878
1898
  const domainFilter = args?.domain;
1879
1899
  // 1. Fetch active North Star anchor(s)
1880
- const northStars = db.prepare(`
1881
- SELECT id, statement, rationale, domain, confidence, lifecycle,
1882
- datetime(review_after, 'unixepoch') as review_date,
1883
- datetime(created_at, 'unixepoch') as declared_at
1884
- FROM drift_anchors
1885
- WHERE node_type = 'north_star' AND status = 'active'
1886
- ORDER BY created_at DESC
1900
+ const northStars = db.prepare(`
1901
+ SELECT id, statement, rationale, domain, confidence, lifecycle,
1902
+ datetime(review_after, 'unixepoch') as review_date,
1903
+ datetime(created_at, 'unixepoch') as declared_at
1904
+ FROM drift_anchors
1905
+ WHERE node_type = 'north_star' AND status = 'active'
1906
+ ORDER BY created_at DESC
1887
1907
  `).all();
1888
1908
  // Re-injection friction — the active-observability loop closing back into reflection.
1889
1909
  // "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
@@ -1901,23 +1921,23 @@ function handleDream(args) {
1901
1921
  const distillLimit = Math.max(1, Math.min(25, Number(args?.distill ?? 8) || 8));
1902
1922
  let distillQueue = [];
1903
1923
  try {
1904
- const rows = db.prepare(`
1905
- SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
1906
- FROM memories m JOIN entities e ON e.id = m.entity_id
1907
- WHERE m.layer IN ('learning', 'caveat')
1908
- AND json_valid(m.content)
1909
- -- settle window: the Stop hook extracts every turn, so the newest rows belong to a
1910
- -- session still in motion. Don't ask the agent to distill the conversation it is in.
1911
- AND m.created_at < unixepoch() - 1800
1912
- AND json_extract(m.content, '$.distill_verdict') IS NULL
1913
- AND (json_extract(m.content, '$.needs_distill') = 1
1914
- OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
1915
- OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
1916
- ORDER BY (m.layer = 'caveat') DESC,
1917
- (e.name <> 'Local') DESC,
1918
- length(json_extract(m.content, '$.what')) DESC,
1919
- m.created_at DESC
1920
- LIMIT ${distillLimit}
1924
+ const rows = db.prepare(`
1925
+ SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
1926
+ FROM memories m JOIN entities e ON e.id = m.entity_id
1927
+ WHERE m.layer IN ('learning', 'caveat')
1928
+ AND json_valid(m.content)
1929
+ -- settle window: the Stop hook extracts every turn, so the newest rows belong to a
1930
+ -- session still in motion. Don't ask the agent to distill the conversation it is in.
1931
+ AND m.created_at < unixepoch() - 1800
1932
+ AND json_extract(m.content, '$.distill_verdict') IS NULL
1933
+ AND (json_extract(m.content, '$.needs_distill') = 1
1934
+ OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
1935
+ OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
1936
+ ORDER BY (m.layer = 'caveat') DESC,
1937
+ (e.name <> 'Local') DESC,
1938
+ length(json_extract(m.content, '$.what')) DESC,
1939
+ m.created_at DESC
1940
+ LIMIT ${distillLimit}
1921
1941
  `).all();
1922
1942
  distillQueue = rows.map((r) => {
1923
1943
  let c = {};
@@ -1957,26 +1977,26 @@ function handleDream(args) {
1957
1977
  }
1958
1978
  // 2. Fetch pending orphaned proposals
1959
1979
  const candidateQuery = domainFilter
1960
- ? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
1961
- mc.rationale as candidate_rationale, mc.confidence,
1962
- mc.evidence_refs, mc.status,
1963
- datetime(mc.created_at, 'unixepoch') as flagged_at,
1964
- da.statement, da.rationale as anchor_rationale, da.domain,
1965
- da.evidence_refs as anchor_evidence
1966
- FROM memory_write_candidates mc
1967
- JOIN drift_anchors da ON mc.target_node_id = da.id
1968
- WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
1969
- AND da.domain = ?
1980
+ ? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
1981
+ mc.rationale as candidate_rationale, mc.confidence,
1982
+ mc.evidence_refs, mc.status,
1983
+ datetime(mc.created_at, 'unixepoch') as flagged_at,
1984
+ da.statement, da.rationale as anchor_rationale, da.domain,
1985
+ da.evidence_refs as anchor_evidence
1986
+ FROM memory_write_candidates mc
1987
+ JOIN drift_anchors da ON mc.target_node_id = da.id
1988
+ WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
1989
+ AND da.domain = ?
1970
1990
  ORDER BY mc.created_at DESC`
1971
- : `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
1972
- mc.rationale as candidate_rationale, mc.confidence,
1973
- mc.evidence_refs, mc.status,
1974
- datetime(mc.created_at, 'unixepoch') as flagged_at,
1975
- da.statement, da.rationale as anchor_rationale, da.domain,
1976
- da.evidence_refs as anchor_evidence
1977
- FROM memory_write_candidates mc
1978
- JOIN drift_anchors da ON mc.target_node_id = da.id
1979
- WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
1991
+ : `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
1992
+ mc.rationale as candidate_rationale, mc.confidence,
1993
+ mc.evidence_refs, mc.status,
1994
+ datetime(mc.created_at, 'unixepoch') as flagged_at,
1995
+ da.statement, da.rationale as anchor_rationale, da.domain,
1996
+ da.evidence_refs as anchor_evidence
1997
+ FROM memory_write_candidates mc
1998
+ JOIN drift_anchors da ON mc.target_node_id = da.id
1999
+ WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
1980
2000
  ORDER BY mc.created_at DESC`;
1981
2001
  const candidates = domainFilter
1982
2002
  ? db.prepare(candidateQuery).all(domainFilter)
@@ -2058,12 +2078,12 @@ function handleResolveProposal(args) {
2058
2078
  throw new Error('verdict must be "surface" or "dismiss"');
2059
2079
  }
2060
2080
  // Verify candidate exists and is pending
2061
- const candidate = db.prepare(`
2062
- SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
2063
- da.statement
2064
- FROM memory_write_candidates mc
2065
- JOIN drift_anchors da ON mc.target_node_id = da.id
2066
- WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
2081
+ const candidate = db.prepare(`
2082
+ SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
2083
+ da.statement
2084
+ FROM memory_write_candidates mc
2085
+ JOIN drift_anchors da ON mc.target_node_id = da.id
2086
+ WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
2067
2087
  `).get(candidateId);
2068
2088
  if (!candidate) {
2069
2089
  throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
@@ -2199,7 +2219,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
2199
2219
  forget: 'remember({ forget: true, memory_id: <id> })',
2200
2220
  list_entities: 'recall() with no params',
2201
2221
  recall_file: 'recall({ path: "<file_path>" })',
2202
- consolidate: 'Auto-runs on server startup. No manual call needed.',
2222
+ consolidate: 'No longer a tool, and since 0.17.1 it does not run on server start either (it deletes the original rows). Opt in on the server with LINKSEE_AUTO_CONSOLIDATE=1 if you want the old weekly start-up run.',
2203
2223
  };
2204
2224
  if (name in migrations) {
2205
2225
  throw new Error(`Tool "${name}" was merged in v0.7.0. Migration: ${migrations[name]}`);
@@ -372,26 +372,19 @@ Recording `prior_belief` leaves a **belief-update history**. Later, this becomes
372
372
  2. ❌ Solve an error on the spot without recording — future you (or another agent) will hit the same failure
373
373
  3. ❌ Use `Read` everywhere instead of `read_smart` (wastes tokens)
374
374
  4. ❌ Write caveats in a flippant tone — preserve them seriously
375
- 5. ❌ Skip `consolidate` during long-running work — run it weekly
375
+ 5. ❌ Try to call a `consolidate` tool — there is none; consolidation is a server opt-in that deletes originals (see below)
376
376
  6. ❌ Recall the same entity twice in one session without new context (wastes tokens)
377
377
  7. ❌ Write `recall({ query: "what happened last time" })` — use specific keywords
378
378
 
379
379
  ---
380
380
 
381
- ## 🔁 Consolidate — periodic memory tidy-up
381
+ ## 🔁 Consolidation — off unless the user turns it on
382
382
 
383
- When memory has grown (rough guideline: DB > 20MB, memories > 15,000):
383
+ There is no `consolidate` tool. Consolidation clusters cold, low-importance memories older than 7 days into one `learning`-layer entry and **deletes the originals**, so since 0.17.1 it never runs on its own. The user can opt in by starting the server with `LINKSEE_AUTO_CONSOLIDATE=1` (then it runs at start-up, at most weekly).
384
384
 
385
- ```
386
- mcp__linksee__consolidate({
387
- scope: "session",
388
- min_age_days: 7
389
- })
390
- ```
391
-
392
- This clusters cold, low-importance memories older than 7 days → compresses them into a single `learning`-layer entry → deletes originals.
385
+ Do not suggest turning it on as routine housekeeping. If the user asks about a large DB, explain the trade-off (originals are deleted) and suggest backing up `~/.linksee-memory/` first.
393
386
 
394
- **`caveat` memories and active `goal` memories are never consolidated away.** Equivalent to sleep-time memory reorganization.
387
+ **`caveat` memories and active `goal` memories are never consolidated away.**
395
388
 
396
389
  ---
397
390
 
@@ -525,7 +518,6 @@ User: "That's it for today"
525
518
  })
526
519
 
527
520
  3. Report: "Recorded N decisions, M caveats. Retrievable via recall."
528
- 4. Optionally suggest: consolidate({scope:"session", min_age_days: 14})
529
521
  ```
530
522
 
531
523
  ### Case F2 — Flag orphaned proposals at session end
@@ -725,7 +717,7 @@ Now you have both bodies of knowledge before starting
725
717
  | Day 1 | low (little data) | Stop hook is still collecting |
726
718
  | Week 1 | medium | thousands of memories, FTS5 kicks in |
727
719
  | Month 1 | high | heat_score stabilizes, important memories surface |
728
- | Month 3+ | strongest | consolidate has run, learnings crystallized |
720
+ | Month 3+ | strongest | learnings crystallized (distilled) |
729
721
 
730
722
  **"Gets smarter with use"** — time is on your side. Today's record is read by tomorrow's you.
731
723