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.
- package/README.md +958 -1081
- package/dist/bin/guard-hook.js +19 -4
- 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 +30 -0
- package/dist/db/migrate.js +178 -37
- 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 +197 -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/mcp/prompts.js +59 -59
- package/dist/mcp/resources.js +7 -7
- package/dist/mcp/sampling.js +20 -20
- package/dist/mcp/server.js +180 -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';
|
|
@@ -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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
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
|
-
|
|
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: '
|
|
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]}`);
|
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
|
|