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.
- package/README.md +260 -1081
- package/dist/bin/guard-hook.js +19 -4
- package/dist/bin/migrate-legacy.d.ts +33 -0
- package/dist/bin/migrate-legacy.js +501 -0
- package/dist/bin/runtime.d.ts +52 -0
- package/dist/bin/runtime.js +381 -0
- package/dist/bin/setup.js +191 -76
- package/dist/bin/share-draft.js +28 -28
- package/dist/bin/sync-session.js +10 -1
- package/dist/db/migrate.d.ts +82 -1
- package/dist/db/migrate.js +250 -40
- package/dist/lib/consolidate.d.ts +9 -0
- package/dist/lib/consolidate.js +14 -0
- package/dist/lib/drift-anchors.js +3 -3
- package/dist/lib/drift-detection.js +20 -20
- package/dist/lib/drift-view.js +16 -16
- package/dist/lib/guard-wiring.d.ts +47 -1
- package/dist/lib/guard-wiring.js +114 -11
- package/dist/lib/hook-runtime.d.ts +41 -0
- package/dist/lib/hook-runtime.js +202 -0
- package/dist/lib/map-import.js +17 -17
- package/dist/lib/native-hint.d.ts +5 -0
- package/dist/lib/native-hint.js +42 -0
- package/dist/lib/settings-file.d.ts +23 -0
- package/dist/lib/settings-file.js +137 -0
- package/dist/lib/test-faults.d.ts +3 -0
- package/dist/lib/test-faults.js +30 -0
- package/dist/mcp/prompts.js +59 -59
- package/dist/mcp/resources.js +7 -7
- package/dist/mcp/sampling.js +20 -20
- package/dist/mcp/server.js +182 -160
- package/dist/skill/SKILL.md +6 -14
- package/package.json +107 -107
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
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
|
-
|
|
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: '
|
|
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]}`);
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -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. ❌
|
|
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
|
-
## 🔁
|
|
381
|
+
## 🔁 Consolidation — off unless the user turns it on
|
|
382
382
|
|
|
383
|
-
|
|
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.**
|
|
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 |
|
|
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
|
|