linksee-memory 0.16.0 → 0.17.0
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 +1081 -1044
- package/dist/bin/setup.js +14 -14
- package/dist/bin/share-draft.d.ts +2 -0
- package/dist/bin/share-draft.js +90 -0
- package/dist/db/migrate.js +28 -28
- 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/map-import.js +17 -17
- package/dist/lib/share-draft.d.ts +56 -0
- package/dist/lib/share-draft.js +234 -0
- package/dist/mcp/prompts.js +58 -58
- package/dist/mcp/resources.js +7 -7
- package/dist/mcp/sampling.js +20 -20
- package/dist/mcp/server.js +197 -145
- package/package.json +107 -106
package/dist/mcp/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// linksee-memory MCP server (stdio transport).
|
|
3
|
-
// Tools: remember / recall / read_smart / drift_status / check_decision / declare_anchor / resolve_drift / flag_proposals / dream / resolve_proposal
|
|
3
|
+
// Tools: remember / recall / read_smart / drift_status / check_decision / declare_anchor / resolve_drift / flag_proposals / dream / resolve_proposal / share_draft
|
|
4
4
|
// v0.10.0 — Re-injection layer: pre-action guard (PreToolUse/SessionStart hooks) + extraction
|
|
5
5
|
// quality gate + distillation routine (dream distill_queue) + harden/soften escalation
|
|
6
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -26,6 +26,7 @@ import { getReinjectionFriction, setGateMode } from '../lib/guard.js';
|
|
|
26
26
|
import { logAnchorTouch } from '../lib/anchor-touch.js';
|
|
27
27
|
import { triageDistillQueue, distillPending } from '../lib/distill-triage.js';
|
|
28
28
|
import { whereAmI, listMapProjects } from '../lib/map-view.js';
|
|
29
|
+
import { buildShareDraft, KINDS, RUNTIMES } from '../lib/share-draft.js';
|
|
29
30
|
import { readFileSync, existsSync } from 'node:fs';
|
|
30
31
|
import { fileURLToPath } from 'node:url';
|
|
31
32
|
import { dirname, join } from 'node:path';
|
|
@@ -62,6 +63,7 @@ const SUBCOMMANDS = {
|
|
|
62
63
|
declare: 'declare-anchor.js',
|
|
63
64
|
detect: 'detect-drift.js',
|
|
64
65
|
export: 'export-report.js',
|
|
66
|
+
'share-draft': 'share-draft.js',
|
|
65
67
|
};
|
|
66
68
|
const subcommand = process.argv[2];
|
|
67
69
|
if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand)) {
|
|
@@ -72,6 +74,16 @@ if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand))
|
|
|
72
74
|
}
|
|
73
75
|
const db = openDb();
|
|
74
76
|
runMigrations(db);
|
|
77
|
+
// Distill triage runs on EVERY start, before the first request. It is one indexed scan and a
|
|
78
|
+
// handful of JSON rewrites — cheap — and 0.16.0 had tucked it inside the auto-consolidate block
|
|
79
|
+
// below, which runs at most once a week and only after a 3-second timer. So it never ran.
|
|
80
|
+
try {
|
|
81
|
+
const t = triageDistillQueue(db);
|
|
82
|
+
if (t.noise + t.stale > 0) {
|
|
83
|
+
process.stderr.write(`[linksee-memory] distill triage: archived ${t.noise} noise + ${t.stale} stale, ${t.remaining} remain\n`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* never block startup */ }
|
|
75
87
|
// Auto-maintenance: consolidate stale memories on startup (non-blocking)
|
|
76
88
|
setTimeout(() => {
|
|
77
89
|
try {
|
|
@@ -91,13 +103,6 @@ setTimeout(() => {
|
|
|
91
103
|
if (shouldRun) {
|
|
92
104
|
runConsolidate(db, { scope: 'all', min_age_days: 7 });
|
|
93
105
|
process.stderr.write('[linksee-memory] auto-consolidate complete\n');
|
|
94
|
-
// Triage the distill queue by rule before any agent is asked to think about it.
|
|
95
|
-
try {
|
|
96
|
-
const t = triageDistillQueue(db);
|
|
97
|
-
if (t.noise + t.stale > 0)
|
|
98
|
-
process.stderr.write(`[linksee-memory] distill triage: archived ${t.noise} noise + ${t.stale} stale, ${t.remaining} remain\n`);
|
|
99
|
-
}
|
|
100
|
-
catch { /* never block startup */ }
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
catch { /* non-fatal */ }
|
|
@@ -385,6 +390,35 @@ const TOOLS = [
|
|
|
385
390
|
required: ['candidate_id', 'verdict', 'rationale'],
|
|
386
391
|
},
|
|
387
392
|
},
|
|
393
|
+
// ── Share a fix (opt-in, link only) ──────────────────────
|
|
394
|
+
{
|
|
395
|
+
name: 'share_draft',
|
|
396
|
+
description: 'Make a LINK to a reviewed draft of a field note for other agents (KanseiLINK note page). ' +
|
|
397
|
+
'ONLY when the user says they want to share — never call it on your own.\n\n' +
|
|
398
|
+
'Use after a SaaS / MCP / API connection got stuck and was fixed (or was not). You write the fields; ' +
|
|
399
|
+
'this tool only checks them and builds the URL. Linksee sends nothing: the user opens the link, ' +
|
|
400
|
+
'reviews the filled form, and only their click on that page sends it.\n\n' +
|
|
401
|
+
'No values, only kinds: write "401 invalid_token", not the token. A field that looks like a key, ' +
|
|
402
|
+
'token, JWT, e-mail, tenant URL (acme.slack.com), IP, local path (C:\, /Users/, /home/) or a long ' +
|
|
403
|
+
'hex/base64 run stops the build and returns which field. The whole URL must fit in 2,000 characters ' +
|
|
404
|
+
'(a Japanese character takes 9) — if not, it asks you to shorten; it does not cut.\n\n' +
|
|
405
|
+
'Returns { url, notice } — show the user both. Linksee records only that a draft was made (no field text).',
|
|
406
|
+
inputSchema: {
|
|
407
|
+
type: 'object',
|
|
408
|
+
properties: {
|
|
409
|
+
service: { type: 'string', description: 'KanseiLINK service_id, lowercase (e.g. freee, slack, google-drive). ^[a-z0-9_.-]{1,160}$' },
|
|
410
|
+
task: { type: 'string', maxLength: 300, description: 'What you were trying to do (≤300 chars)' },
|
|
411
|
+
symptom: { type: 'string', maxLength: 600, description: 'The KIND of error that came back — no values (≤600 chars)' },
|
|
412
|
+
tried: { type: 'string', maxLength: 2000, description: 'The steps you tried (≤2,000 chars)' },
|
|
413
|
+
result: { type: 'string', maxLength: 1000, description: 'How it was solved, or that it was not (≤1,000 chars)' },
|
|
414
|
+
conditions: { type: 'string', maxLength: 600, description: 'Optional. Tool + version, kind of environment (≤600 chars)' },
|
|
415
|
+
kind: { type: 'string', enum: [...KINDS], description: 'execution = you ran it · documentation = you read it · hypothesis = untested guess' },
|
|
416
|
+
runtime: { type: 'string', enum: [...RUNTIMES], description: 'Optional. Which AI wrote this' },
|
|
417
|
+
},
|
|
418
|
+
required: ['service', 'task', 'symptom', 'tried', 'result', 'kind'],
|
|
419
|
+
additionalProperties: false,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
388
422
|
];
|
|
389
423
|
// ============================================================
|
|
390
424
|
// Handlers
|
|
@@ -576,15 +610,15 @@ function appendAxisFilters(sql, params, filters) {
|
|
|
576
610
|
return sql;
|
|
577
611
|
}
|
|
578
612
|
function runFtsQuery(query, layer, limit, axis) {
|
|
579
|
-
let sql = `
|
|
580
|
-
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
581
|
-
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
582
|
-
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
583
|
-
bm25(memories_fts) as bm25_score
|
|
584
|
-
FROM memories_fts
|
|
585
|
-
JOIN memories m ON m.id = memories_fts.rowid
|
|
586
|
-
JOIN entities e ON e.id = m.entity_id
|
|
587
|
-
WHERE memories_fts MATCH ?
|
|
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 ?
|
|
588
622
|
`;
|
|
589
623
|
const params = [query];
|
|
590
624
|
if (layer) {
|
|
@@ -598,14 +632,14 @@ function runFtsQuery(query, layer, limit, axis) {
|
|
|
598
632
|
return db.prepare(sql).all(...params);
|
|
599
633
|
}
|
|
600
634
|
function runLikeQuery(query, entityName, layer, limit, axis) {
|
|
601
|
-
let sql = `
|
|
602
|
-
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
603
|
-
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
604
|
-
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
605
|
-
0 as bm25_score
|
|
606
|
-
FROM memories m
|
|
607
|
-
JOIN entities e ON e.id = m.entity_id
|
|
608
|
-
WHERE 1=1
|
|
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
|
|
609
643
|
`;
|
|
610
644
|
const params = [];
|
|
611
645
|
if (entityName) {
|
|
@@ -936,8 +970,8 @@ function handleForget(args) {
|
|
|
936
970
|
}
|
|
937
971
|
// Auto-sweep — also respect pin (importance >= 0.9) as protection
|
|
938
972
|
const rows = db
|
|
939
|
-
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
940
|
-
FROM memories
|
|
973
|
+
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
974
|
+
FROM memories
|
|
941
975
|
WHERE protected = 0 AND importance < 0.9`)
|
|
942
976
|
.all();
|
|
943
977
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -986,17 +1020,17 @@ function handleConsolidate(args) {
|
|
|
986
1020
|
// read-only audit: count candidates using the same rules.
|
|
987
1021
|
const now = Math.floor(Date.now() / 1000);
|
|
988
1022
|
const ageCutoff = now - (args.min_age_days ?? 7) * 86400;
|
|
989
|
-
const candidates = db.prepare(`
|
|
990
|
-
SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
|
|
991
|
-
FROM memories m
|
|
992
|
-
JOIN entities e ON e.id = m.entity_id
|
|
993
|
-
WHERE m.protected = 0
|
|
994
|
-
AND m.importance < 0.9
|
|
995
|
-
AND m.layer IN ('context', 'emotion', 'implementation')
|
|
996
|
-
AND m.created_at <= ?
|
|
997
|
-
GROUP BY m.entity_id, m.layer
|
|
998
|
-
HAVING c >= 2
|
|
999
|
-
ORDER BY c DESC
|
|
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
|
|
1000
1034
|
`).all(ageCutoff);
|
|
1001
1035
|
const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
|
|
1002
1036
|
return JSON.stringify({
|
|
@@ -1070,19 +1104,19 @@ function handleListEntities(args) {
|
|
|
1070
1104
|
const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
|
|
1071
1105
|
const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
|
|
1072
1106
|
const offset = Math.max(0, Number(args?.offset ?? 0));
|
|
1073
|
-
let sql = `
|
|
1074
|
-
SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
|
|
1075
|
-
e.updated_at, e.created_at,
|
|
1076
|
-
COUNT(m.id) as memory_count,
|
|
1077
|
-
MAX(m.last_accessed_at) as last_memory_access,
|
|
1078
|
-
SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
|
|
1079
|
-
SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
|
|
1080
|
-
SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
|
|
1081
|
-
SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
|
|
1082
|
-
SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
|
|
1083
|
-
FROM entities e
|
|
1084
|
-
LEFT JOIN memories m ON m.entity_id = e.id
|
|
1085
|
-
WHERE 1=1
|
|
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
|
|
1086
1120
|
`;
|
|
1087
1121
|
const params = [];
|
|
1088
1122
|
if (kind) {
|
|
@@ -1134,29 +1168,29 @@ function handleRecallFile(args) {
|
|
|
1134
1168
|
return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
|
|
1135
1169
|
}
|
|
1136
1170
|
// Daily breakdown
|
|
1137
|
-
const daily = db.prepare(`
|
|
1138
|
-
SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
|
|
1139
|
-
FROM session_file_edits WHERE file_path LIKE ?
|
|
1140
|
-
GROUP BY day, operation ORDER BY day
|
|
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
|
|
1141
1175
|
`).all(`%${sub}%`);
|
|
1142
1176
|
// Distinct context_snippets (intents) — deduped, ordered by recency
|
|
1143
|
-
const intents = db.prepare(`
|
|
1144
|
-
SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
|
|
1145
|
-
FROM session_file_edits
|
|
1146
|
-
WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
|
|
1147
|
-
GROUP BY context_snippet
|
|
1148
|
-
ORDER BY last_at DESC
|
|
1149
|
-
LIMIT ?
|
|
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 ?
|
|
1150
1184
|
`).all(`%${sub}%`, maxIntents);
|
|
1151
1185
|
// Linked memories
|
|
1152
|
-
const memories = db.prepare(`
|
|
1153
|
-
SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
|
|
1154
|
-
FROM session_file_edits sfe
|
|
1155
|
-
JOIN memories m ON m.id = sfe.memory_id
|
|
1156
|
-
JOIN entities e ON e.id = m.entity_id
|
|
1157
|
-
WHERE sfe.file_path LIKE ?
|
|
1158
|
-
ORDER BY m.importance DESC
|
|
1159
|
-
LIMIT 20
|
|
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
|
|
1160
1194
|
`).all(`%${sub}%`);
|
|
1161
1195
|
// Distinct file paths matched (the substring may match multiple files)
|
|
1162
1196
|
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}%`);
|
|
@@ -1223,10 +1257,10 @@ async function handleConsolidateWithSampling(args) {
|
|
|
1223
1257
|
const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
|
|
1224
1258
|
const snapshot = new Map();
|
|
1225
1259
|
const candidateRows = db
|
|
1226
|
-
.prepare(`SELECT m.id, m.content, e.name as entity_name
|
|
1227
|
-
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1228
|
-
WHERE m.protected = 0
|
|
1229
|
-
AND m.layer IN ('context','emotion','implementation')
|
|
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')
|
|
1230
1264
|
AND m.created_at <= ?`)
|
|
1231
1265
|
.all(ageCutoff);
|
|
1232
1266
|
for (const r of candidateRows)
|
|
@@ -1470,7 +1504,7 @@ async function handleRecallUnified(args) {
|
|
|
1470
1504
|
// Folded surfaces (roadmap 5): locating yourself and the triage session are ways of
|
|
1471
1505
|
// recalling, not separate tools.
|
|
1472
1506
|
if (args?.dream)
|
|
1473
|
-
return handleDream({ domain: args.domain });
|
|
1507
|
+
return handleDream({ domain: args.domain, distill: args.distill });
|
|
1474
1508
|
if (args?.where !== undefined && args?.where !== false) {
|
|
1475
1509
|
return handleWhereAmI({ query: args.where === true ? undefined : String(args.where), project: args.project, limit: args.limit });
|
|
1476
1510
|
}
|
|
@@ -1592,8 +1626,8 @@ function resolveProjectFromRecentEdits() {
|
|
|
1592
1626
|
if (projects.length <= 1)
|
|
1593
1627
|
return { project: projects[0] };
|
|
1594
1628
|
const rows = db
|
|
1595
|
-
.prepare(`SELECT file_path FROM session_file_edits
|
|
1596
|
-
WHERE occurred_at > unixepoch() - 86400
|
|
1629
|
+
.prepare(`SELECT file_path FROM session_file_edits
|
|
1630
|
+
WHERE occurred_at > unixepoch() - 86400
|
|
1597
1631
|
ORDER BY occurred_at DESC LIMIT 50`)
|
|
1598
1632
|
.all();
|
|
1599
1633
|
if (rows.length === 0)
|
|
@@ -1781,25 +1815,25 @@ function handleFlagProposals(args) {
|
|
|
1781
1815
|
const sessionContext = args.session_context ?? 'conversation session';
|
|
1782
1816
|
const PROPOSAL_TAG = 'orphaned_proposal';
|
|
1783
1817
|
const now = Math.floor(Date.now() / 1000);
|
|
1784
|
-
const insertAnchor = db.prepare(`
|
|
1785
|
-
INSERT INTO drift_anchors (
|
|
1786
|
-
kind, statement, rationale, domain, decision_mode,
|
|
1787
|
-
confidence, lifecycle, status, owner,
|
|
1788
|
-
evidence_refs, created_at, updated_at
|
|
1789
|
-
) VALUES (
|
|
1790
|
-
'decision', ?, ?, ?, 'hypothesis',
|
|
1791
|
-
?, 'active', 'active', 'agent',
|
|
1792
|
-
?, ?, ?
|
|
1793
|
-
)
|
|
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
|
+
)
|
|
1794
1828
|
`);
|
|
1795
|
-
const insertCandidate = db.prepare(`
|
|
1796
|
-
INSERT INTO memory_write_candidates (
|
|
1797
|
-
scope, candidate_type, target_node_id, rationale,
|
|
1798
|
-
confidence, evidence_refs, status, created_at
|
|
1799
|
-
) VALUES (
|
|
1800
|
-
'orphaned_proposal', 'update_node', ?, ?,
|
|
1801
|
-
?, ?, 'pending_review', ?
|
|
1802
|
-
)
|
|
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
|
+
)
|
|
1803
1837
|
`);
|
|
1804
1838
|
const results = [];
|
|
1805
1839
|
const txn = db.transaction(() => {
|
|
@@ -1843,13 +1877,13 @@ function handleFlagProposals(args) {
|
|
|
1843
1877
|
function handleDream(args) {
|
|
1844
1878
|
const domainFilter = args?.domain;
|
|
1845
1879
|
// 1. Fetch active North Star anchor(s)
|
|
1846
|
-
const northStars = db.prepare(`
|
|
1847
|
-
SELECT id, statement, rationale, domain, confidence, lifecycle,
|
|
1848
|
-
datetime(review_after, 'unixepoch') as review_date,
|
|
1849
|
-
datetime(created_at, 'unixepoch') as declared_at
|
|
1850
|
-
FROM drift_anchors
|
|
1851
|
-
WHERE node_type = 'north_star' AND status = 'active'
|
|
1852
|
-
ORDER BY created_at DESC
|
|
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
|
|
1853
1887
|
`).all();
|
|
1854
1888
|
// Re-injection friction — the active-observability loop closing back into reflection.
|
|
1855
1889
|
// "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
|
|
@@ -1867,23 +1901,23 @@ function handleDream(args) {
|
|
|
1867
1901
|
const distillLimit = Math.max(1, Math.min(25, Number(args?.distill ?? 8) || 8));
|
|
1868
1902
|
let distillQueue = [];
|
|
1869
1903
|
try {
|
|
1870
|
-
const rows = db.prepare(`
|
|
1871
|
-
SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
|
|
1872
|
-
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1873
|
-
WHERE m.layer IN ('learning', 'caveat')
|
|
1874
|
-
AND json_valid(m.content)
|
|
1875
|
-
-- settle window: the Stop hook extracts every turn, so the newest rows belong to a
|
|
1876
|
-
-- session still in motion. Don't ask the agent to distill the conversation it is in.
|
|
1877
|
-
AND m.created_at < unixepoch() - 1800
|
|
1878
|
-
AND json_extract(m.content, '$.distill_verdict') IS NULL
|
|
1879
|
-
AND (json_extract(m.content, '$.needs_distill') = 1
|
|
1880
|
-
OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
|
|
1881
|
-
OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
|
|
1882
|
-
ORDER BY (m.layer = 'caveat') DESC,
|
|
1883
|
-
(e.name <> 'Local') DESC,
|
|
1884
|
-
length(json_extract(m.content, '$.what')) DESC,
|
|
1885
|
-
m.created_at DESC
|
|
1886
|
-
LIMIT ${distillLimit}
|
|
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}
|
|
1887
1921
|
`).all();
|
|
1888
1922
|
distillQueue = rows.map((r) => {
|
|
1889
1923
|
let c = {};
|
|
@@ -1923,26 +1957,26 @@ function handleDream(args) {
|
|
|
1923
1957
|
}
|
|
1924
1958
|
// 2. Fetch pending orphaned proposals
|
|
1925
1959
|
const candidateQuery = domainFilter
|
|
1926
|
-
? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1927
|
-
mc.rationale as candidate_rationale, mc.confidence,
|
|
1928
|
-
mc.evidence_refs, mc.status,
|
|
1929
|
-
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1930
|
-
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1931
|
-
da.evidence_refs as anchor_evidence
|
|
1932
|
-
FROM memory_write_candidates mc
|
|
1933
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1934
|
-
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1935
|
-
AND da.domain = ?
|
|
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 = ?
|
|
1936
1970
|
ORDER BY mc.created_at DESC`
|
|
1937
|
-
: `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1938
|
-
mc.rationale as candidate_rationale, mc.confidence,
|
|
1939
|
-
mc.evidence_refs, mc.status,
|
|
1940
|
-
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1941
|
-
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1942
|
-
da.evidence_refs as anchor_evidence
|
|
1943
|
-
FROM memory_write_candidates mc
|
|
1944
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1945
|
-
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
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'
|
|
1946
1980
|
ORDER BY mc.created_at DESC`;
|
|
1947
1981
|
const candidates = domainFilter
|
|
1948
1982
|
? db.prepare(candidateQuery).all(domainFilter)
|
|
@@ -2004,7 +2038,9 @@ function handleDream(args) {
|
|
|
2004
2038
|
friction,
|
|
2005
2039
|
friction_total: friction.length,
|
|
2006
2040
|
distill_queue: distillQueue,
|
|
2007
|
-
distill_total:
|
|
2041
|
+
distill_total: distillPending(db),
|
|
2042
|
+
distill_shown: distillQueue.length,
|
|
2043
|
+
distill_hint: 'Ordered by value (caveats first, named entities, longer text). Pass distill: N (max 25) for a longer drain. Rules already set a distill_verdict on acknowledgements, bare paths and 90-day-old never-recalled items — you only see what needs judgement.',
|
|
2008
2044
|
guide: guideParts.join(' '),
|
|
2009
2045
|
});
|
|
2010
2046
|
}
|
|
@@ -2022,12 +2058,12 @@ function handleResolveProposal(args) {
|
|
|
2022
2058
|
throw new Error('verdict must be "surface" or "dismiss"');
|
|
2023
2059
|
}
|
|
2024
2060
|
// Verify candidate exists and is pending
|
|
2025
|
-
const candidate = db.prepare(`
|
|
2026
|
-
SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
|
|
2027
|
-
da.statement
|
|
2028
|
-
FROM memory_write_candidates mc
|
|
2029
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
2030
|
-
WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
|
|
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'
|
|
2031
2067
|
`).get(candidateId);
|
|
2032
2068
|
if (!candidate) {
|
|
2033
2069
|
throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
|
|
@@ -2092,6 +2128,18 @@ const LEGACY_TOOLS = {
|
|
|
2092
2128
|
dream: 'recall({ dream: true }) — recall() with no arguments already includes the counts',
|
|
2093
2129
|
resolve_proposal: "resolve_drift({ candidate_id, action: 'surface' | 'dismiss', rationale })",
|
|
2094
2130
|
};
|
|
2131
|
+
// share_draft: check fields + build the link. No network. Records only that a draft was made.
|
|
2132
|
+
function handleShareDraft(args) {
|
|
2133
|
+
const r = buildShareDraft(args);
|
|
2134
|
+
if (!r.ok)
|
|
2135
|
+
return JSON.stringify(r, null, 2);
|
|
2136
|
+
try {
|
|
2137
|
+
// events row with no entity and no payload: the fact and the time, nothing from the fields.
|
|
2138
|
+
db.prepare('INSERT INTO events (entity_id, kind, payload) VALUES (NULL, ?, NULL)').run('share_draft_created');
|
|
2139
|
+
}
|
|
2140
|
+
catch { /* the record must never break the tool */ }
|
|
2141
|
+
return JSON.stringify(r, null, 2);
|
|
2142
|
+
}
|
|
2095
2143
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2096
2144
|
const showLegacy = process.env.LINKSEE_LEGACY_TOOLS === '1';
|
|
2097
2145
|
const tools = TOOLS
|
|
@@ -2141,6 +2189,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2141
2189
|
case 'resolve_proposal':
|
|
2142
2190
|
text = handleResolveProposal(args);
|
|
2143
2191
|
break;
|
|
2192
|
+
// Share a fix (link only)
|
|
2193
|
+
case 'share_draft':
|
|
2194
|
+
text = handleShareDraft(args);
|
|
2195
|
+
break;
|
|
2144
2196
|
default: {
|
|
2145
2197
|
const migrations = {
|
|
2146
2198
|
update_memory: 'remember({ memory_id: <id>, content: "...", importance: 0.8 })',
|