linksee-memory 0.17.0 → 0.17.2

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';
@@ -64,6 +64,8 @@ const SUBCOMMANDS = {
64
64
  detect: 'detect-drift.js',
65
65
  export: 'export-report.js',
66
66
  'share-draft': 'share-draft.js',
67
+ migrate: 'migrate-legacy.js', // 0.17.2: schema 1–5 → current, without starting the server
68
+ runtime: 'runtime.js', // 0.17.2: runtime list | prune
67
69
  };
68
70
  const subcommand = process.argv[2];
69
71
  if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand)) {
@@ -72,8 +74,20 @@ if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand))
72
74
  const r = spawnSync(process.execPath, [binPath, ...process.argv.slice(3)], { stdio: 'inherit' });
73
75
  process.exit(r.status ?? (r.signal ? 1 : 0));
74
76
  }
75
- const db = openDb();
76
- runMigrations(db);
77
+ let db;
78
+ try {
79
+ db = openDb();
80
+ runMigrations(db);
81
+ }
82
+ catch (e) {
83
+ // A pre-June-2026 DB (schema < 6) is refused before any write; print what to do
84
+ // instead of a stack trace. MCP clients show the server's stderr in their logs.
85
+ if (e instanceof DbRefusedError) {
86
+ process.stderr.write(`${e.message}\n`);
87
+ process.exit(1);
88
+ }
89
+ throw e;
90
+ }
77
91
  // Distill triage runs on EVERY start, before the first request. It is one indexed scan and a
78
92
  // handful of JSON rewrites — cheap — and 0.16.0 had tucked it inside the auto-consolidate block
79
93
  // below, which runs at most once a week and only after a 3-second timer. So it never ran.
@@ -84,29 +98,37 @@ try {
84
98
  }
85
99
  }
86
100
  catch { /* never block startup */ }
87
- // Auto-maintenance: consolidate stale memories on startup (non-blocking)
88
- setTimeout(() => {
89
- try {
90
- let shouldRun = true;
101
+ // Auto-maintenance: consolidate stale memories on startup (non-blocking).
102
+ //
103
+ // OFF by default since 0.17.1 (L-1-106 / audit F1): consolidate deletes the original rows after
104
+ // writing a summary that keeps only the top 3 × 200 characters, ignores thread_id, and cascades
105
+ // away their edges — reasons and history lost with no action from the user. It now runs only
106
+ // when asked: LINKSEE_AUTO_CONSOLIDATE=1 (or `--auto-consolidate` on the server command line).
107
+ // There is no `consolidate` tool (merged away in 0.7.0). Rows already removed by earlier versions
108
+ // are not restored.
109
+ if (autoConsolidateEnabled(process.env, process.argv.slice(2)))
110
+ setTimeout(() => {
91
111
  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;
112
+ let shouldRun = true;
113
+ try {
114
+ // Check if consolidations table exists before querying
115
+ const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='consolidations'").get();
116
+ if (tableExists) {
117
+ const lastRow = db.prepare('SELECT MAX(created_at) as ts FROM consolidations').get();
118
+ if (lastRow?.ts && (Date.now() / 1000 - lastRow.ts) / 86400 < 7)
119
+ shouldRun = false;
120
+ }
121
+ }
122
+ catch {
123
+ shouldRun = false; /* genuinely unexpected — skip to be safe */
124
+ }
125
+ if (shouldRun) {
126
+ runConsolidate(db, { scope: 'all', min_age_days: 7 });
127
+ process.stderr.write('[linksee-memory] auto-consolidate complete\n');
98
128
  }
99
129
  }
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);
130
+ catch { /* non-fatal */ }
131
+ }, 3000);
110
132
  const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, {
111
133
  capabilities: {
112
134
  tools: {},
@@ -610,15 +632,15 @@ function appendAxisFilters(sql, params, filters) {
610
632
  return sql;
611
633
  }
612
634
  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 ?
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
+ bm25(memories_fts) as bm25_score
640
+ FROM memories_fts
641
+ JOIN memories m ON m.id = memories_fts.rowid
642
+ JOIN entities e ON e.id = m.entity_id
643
+ WHERE memories_fts MATCH ?
622
644
  `;
623
645
  const params = [query];
624
646
  if (layer) {
@@ -632,14 +654,14 @@ function runFtsQuery(query, layer, limit, axis) {
632
654
  return db.prepare(sql).all(...params);
633
655
  }
634
656
  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
657
+ let sql = `
658
+ SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
659
+ m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
660
+ m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
661
+ 0 as bm25_score
662
+ FROM memories m
663
+ JOIN entities e ON e.id = m.entity_id
664
+ WHERE 1=1
643
665
  `;
644
666
  const params = [];
645
667
  if (entityName) {
@@ -970,8 +992,8 @@ function handleForget(args) {
970
992
  }
971
993
  // Auto-sweep — also respect pin (importance >= 0.9) as protection
972
994
  const rows = db
973
- .prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
974
- FROM memories
995
+ .prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
996
+ FROM memories
975
997
  WHERE protected = 0 AND importance < 0.9`)
976
998
  .all();
977
999
  const now = Math.floor(Date.now() / 1000);
@@ -1020,17 +1042,17 @@ function handleConsolidate(args) {
1020
1042
  // read-only audit: count candidates using the same rules.
1021
1043
  const now = Math.floor(Date.now() / 1000);
1022
1044
  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
1045
+ const candidates = db.prepare(`
1046
+ SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
1047
+ FROM memories m
1048
+ JOIN entities e ON e.id = m.entity_id
1049
+ WHERE m.protected = 0
1050
+ AND m.importance < 0.9
1051
+ AND m.layer IN ('context', 'emotion', 'implementation')
1052
+ AND m.created_at <= ?
1053
+ GROUP BY m.entity_id, m.layer
1054
+ HAVING c >= 2
1055
+ ORDER BY c DESC
1034
1056
  `).all(ageCutoff);
1035
1057
  const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
1036
1058
  return JSON.stringify({
@@ -1104,19 +1126,19 @@ function handleListEntities(args) {
1104
1126
  const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
1105
1127
  const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
1106
1128
  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
1129
+ let sql = `
1130
+ SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
1131
+ e.updated_at, e.created_at,
1132
+ COUNT(m.id) as memory_count,
1133
+ MAX(m.last_accessed_at) as last_memory_access,
1134
+ SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
1135
+ SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
1136
+ SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
1137
+ SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
1138
+ SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
1139
+ FROM entities e
1140
+ LEFT JOIN memories m ON m.entity_id = e.id
1141
+ WHERE 1=1
1120
1142
  `;
1121
1143
  const params = [];
1122
1144
  if (kind) {
@@ -1168,29 +1190,29 @@ function handleRecallFile(args) {
1168
1190
  return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
1169
1191
  }
1170
1192
  // 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
1193
+ const daily = db.prepare(`
1194
+ SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
1195
+ FROM session_file_edits WHERE file_path LIKE ?
1196
+ GROUP BY day, operation ORDER BY day
1175
1197
  `).all(`%${sub}%`);
1176
1198
  // 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 ?
1199
+ const intents = db.prepare(`
1200
+ SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
1201
+ FROM session_file_edits
1202
+ WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
1203
+ GROUP BY context_snippet
1204
+ ORDER BY last_at DESC
1205
+ LIMIT ?
1184
1206
  `).all(`%${sub}%`, maxIntents);
1185
1207
  // 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
1208
+ const memories = db.prepare(`
1209
+ SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
1210
+ FROM session_file_edits sfe
1211
+ JOIN memories m ON m.id = sfe.memory_id
1212
+ JOIN entities e ON e.id = m.entity_id
1213
+ WHERE sfe.file_path LIKE ?
1214
+ ORDER BY m.importance DESC
1215
+ LIMIT 20
1194
1216
  `).all(`%${sub}%`);
1195
1217
  // Distinct file paths matched (the substring may match multiple files)
1196
1218
  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 +1279,10 @@ async function handleConsolidateWithSampling(args) {
1257
1279
  const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
1258
1280
  const snapshot = new Map();
1259
1281
  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')
1282
+ .prepare(`SELECT m.id, m.content, e.name as entity_name
1283
+ FROM memories m JOIN entities e ON e.id = m.entity_id
1284
+ WHERE m.protected = 0
1285
+ AND m.layer IN ('context','emotion','implementation')
1264
1286
  AND m.created_at <= ?`)
1265
1287
  .all(ageCutoff);
1266
1288
  for (const r of candidateRows)
@@ -1626,8 +1648,8 @@ function resolveProjectFromRecentEdits() {
1626
1648
  if (projects.length <= 1)
1627
1649
  return { project: projects[0] };
1628
1650
  const rows = db
1629
- .prepare(`SELECT file_path FROM session_file_edits
1630
- WHERE occurred_at > unixepoch() - 86400
1651
+ .prepare(`SELECT file_path FROM session_file_edits
1652
+ WHERE occurred_at > unixepoch() - 86400
1631
1653
  ORDER BY occurred_at DESC LIMIT 50`)
1632
1654
  .all();
1633
1655
  if (rows.length === 0)
@@ -1815,25 +1837,25 @@ function handleFlagProposals(args) {
1815
1837
  const sessionContext = args.session_context ?? 'conversation session';
1816
1838
  const PROPOSAL_TAG = 'orphaned_proposal';
1817
1839
  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
- )
1840
+ const insertAnchor = db.prepare(`
1841
+ INSERT INTO drift_anchors (
1842
+ kind, statement, rationale, domain, decision_mode,
1843
+ confidence, lifecycle, status, owner,
1844
+ evidence_refs, created_at, updated_at
1845
+ ) VALUES (
1846
+ 'decision', ?, ?, ?, 'hypothesis',
1847
+ ?, 'active', 'active', 'agent',
1848
+ ?, ?, ?
1849
+ )
1828
1850
  `);
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
- )
1851
+ const insertCandidate = db.prepare(`
1852
+ INSERT INTO memory_write_candidates (
1853
+ scope, candidate_type, target_node_id, rationale,
1854
+ confidence, evidence_refs, status, created_at
1855
+ ) VALUES (
1856
+ 'orphaned_proposal', 'update_node', ?, ?,
1857
+ ?, ?, 'pending_review', ?
1858
+ )
1837
1859
  `);
1838
1860
  const results = [];
1839
1861
  const txn = db.transaction(() => {
@@ -1877,13 +1899,13 @@ function handleFlagProposals(args) {
1877
1899
  function handleDream(args) {
1878
1900
  const domainFilter = args?.domain;
1879
1901
  // 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
1902
+ const northStars = db.prepare(`
1903
+ SELECT id, statement, rationale, domain, confidence, lifecycle,
1904
+ datetime(review_after, 'unixepoch') as review_date,
1905
+ datetime(created_at, 'unixepoch') as declared_at
1906
+ FROM drift_anchors
1907
+ WHERE node_type = 'north_star' AND status = 'active'
1908
+ ORDER BY created_at DESC
1887
1909
  `).all();
1888
1910
  // Re-injection friction — the active-observability loop closing back into reflection.
1889
1911
  // "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
@@ -1901,23 +1923,23 @@ function handleDream(args) {
1901
1923
  const distillLimit = Math.max(1, Math.min(25, Number(args?.distill ?? 8) || 8));
1902
1924
  let distillQueue = [];
1903
1925
  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}
1926
+ const rows = db.prepare(`
1927
+ SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
1928
+ FROM memories m JOIN entities e ON e.id = m.entity_id
1929
+ WHERE m.layer IN ('learning', 'caveat')
1930
+ AND json_valid(m.content)
1931
+ -- settle window: the Stop hook extracts every turn, so the newest rows belong to a
1932
+ -- session still in motion. Don't ask the agent to distill the conversation it is in.
1933
+ AND m.created_at < unixepoch() - 1800
1934
+ AND json_extract(m.content, '$.distill_verdict') IS NULL
1935
+ AND (json_extract(m.content, '$.needs_distill') = 1
1936
+ OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
1937
+ OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
1938
+ ORDER BY (m.layer = 'caveat') DESC,
1939
+ (e.name <> 'Local') DESC,
1940
+ length(json_extract(m.content, '$.what')) DESC,
1941
+ m.created_at DESC
1942
+ LIMIT ${distillLimit}
1921
1943
  `).all();
1922
1944
  distillQueue = rows.map((r) => {
1923
1945
  let c = {};
@@ -1957,26 +1979,26 @@ function handleDream(args) {
1957
1979
  }
1958
1980
  // 2. Fetch pending orphaned proposals
1959
1981
  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 = ?
1982
+ ? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
1983
+ mc.rationale as candidate_rationale, mc.confidence,
1984
+ mc.evidence_refs, mc.status,
1985
+ datetime(mc.created_at, 'unixepoch') as flagged_at,
1986
+ da.statement, da.rationale as anchor_rationale, da.domain,
1987
+ da.evidence_refs as anchor_evidence
1988
+ FROM memory_write_candidates mc
1989
+ JOIN drift_anchors da ON mc.target_node_id = da.id
1990
+ WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
1991
+ AND da.domain = ?
1970
1992
  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'
1993
+ : `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
1994
+ mc.rationale as candidate_rationale, mc.confidence,
1995
+ mc.evidence_refs, mc.status,
1996
+ datetime(mc.created_at, 'unixepoch') as flagged_at,
1997
+ da.statement, da.rationale as anchor_rationale, da.domain,
1998
+ da.evidence_refs as anchor_evidence
1999
+ FROM memory_write_candidates mc
2000
+ JOIN drift_anchors da ON mc.target_node_id = da.id
2001
+ WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
1980
2002
  ORDER BY mc.created_at DESC`;
1981
2003
  const candidates = domainFilter
1982
2004
  ? db.prepare(candidateQuery).all(domainFilter)
@@ -2058,12 +2080,12 @@ function handleResolveProposal(args) {
2058
2080
  throw new Error('verdict must be "surface" or "dismiss"');
2059
2081
  }
2060
2082
  // 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'
2083
+ const candidate = db.prepare(`
2084
+ SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
2085
+ da.statement
2086
+ FROM memory_write_candidates mc
2087
+ JOIN drift_anchors da ON mc.target_node_id = da.id
2088
+ WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
2067
2089
  `).get(candidateId);
2068
2090
  if (!candidate) {
2069
2091
  throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
@@ -2199,7 +2221,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
2199
2221
  forget: 'remember({ forget: true, memory_id: <id> })',
2200
2222
  list_entities: 'recall() with no params',
2201
2223
  recall_file: 'recall({ path: "<file_path>" })',
2202
- consolidate: 'Auto-runs on server startup. No manual call needed.',
2224
+ 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
2225
  };
2204
2226
  if (name in migrations) {
2205
2227
  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