linksee-memory 0.16.1 → 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 +183 -136
- 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)) {
|
|
@@ -388,6 +390,35 @@ const TOOLS = [
|
|
|
388
390
|
required: ['candidate_id', 'verdict', 'rationale'],
|
|
389
391
|
},
|
|
390
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
|
+
},
|
|
391
422
|
];
|
|
392
423
|
// ============================================================
|
|
393
424
|
// Handlers
|
|
@@ -579,15 +610,15 @@ function appendAxisFilters(sql, params, filters) {
|
|
|
579
610
|
return sql;
|
|
580
611
|
}
|
|
581
612
|
function runFtsQuery(query, layer, limit, axis) {
|
|
582
|
-
let sql = `
|
|
583
|
-
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
584
|
-
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
585
|
-
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
586
|
-
bm25(memories_fts) as bm25_score
|
|
587
|
-
FROM memories_fts
|
|
588
|
-
JOIN memories m ON m.id = memories_fts.rowid
|
|
589
|
-
JOIN entities e ON e.id = m.entity_id
|
|
590
|
-
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 ?
|
|
591
622
|
`;
|
|
592
623
|
const params = [query];
|
|
593
624
|
if (layer) {
|
|
@@ -601,14 +632,14 @@ function runFtsQuery(query, layer, limit, axis) {
|
|
|
601
632
|
return db.prepare(sql).all(...params);
|
|
602
633
|
}
|
|
603
634
|
function runLikeQuery(query, entityName, layer, limit, axis) {
|
|
604
|
-
let sql = `
|
|
605
|
-
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
606
|
-
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
607
|
-
m.altitude as _altitude, m.mem_type as _mem_type, m.mem_state as _mem_state, m.thread_id as _thread_id,
|
|
608
|
-
0 as bm25_score
|
|
609
|
-
FROM memories m
|
|
610
|
-
JOIN entities e ON e.id = m.entity_id
|
|
611
|
-
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
|
|
612
643
|
`;
|
|
613
644
|
const params = [];
|
|
614
645
|
if (entityName) {
|
|
@@ -939,8 +970,8 @@ function handleForget(args) {
|
|
|
939
970
|
}
|
|
940
971
|
// Auto-sweep — also respect pin (importance >= 0.9) as protection
|
|
941
972
|
const rows = db
|
|
942
|
-
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
943
|
-
FROM memories
|
|
973
|
+
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected, altitude
|
|
974
|
+
FROM memories
|
|
944
975
|
WHERE protected = 0 AND importance < 0.9`)
|
|
945
976
|
.all();
|
|
946
977
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -989,17 +1020,17 @@ function handleConsolidate(args) {
|
|
|
989
1020
|
// read-only audit: count candidates using the same rules.
|
|
990
1021
|
const now = Math.floor(Date.now() / 1000);
|
|
991
1022
|
const ageCutoff = now - (args.min_age_days ?? 7) * 86400;
|
|
992
|
-
const candidates = db.prepare(`
|
|
993
|
-
SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
|
|
994
|
-
FROM memories m
|
|
995
|
-
JOIN entities e ON e.id = m.entity_id
|
|
996
|
-
WHERE m.protected = 0
|
|
997
|
-
AND m.importance < 0.9
|
|
998
|
-
AND m.layer IN ('context', 'emotion', 'implementation')
|
|
999
|
-
AND m.created_at <= ?
|
|
1000
|
-
GROUP BY m.entity_id, m.layer
|
|
1001
|
-
HAVING c >= 2
|
|
1002
|
-
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
|
|
1003
1034
|
`).all(ageCutoff);
|
|
1004
1035
|
const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
|
|
1005
1036
|
return JSON.stringify({
|
|
@@ -1073,19 +1104,19 @@ function handleListEntities(args) {
|
|
|
1073
1104
|
const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
|
|
1074
1105
|
const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
|
|
1075
1106
|
const offset = Math.max(0, Number(args?.offset ?? 0));
|
|
1076
|
-
let sql = `
|
|
1077
|
-
SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
|
|
1078
|
-
e.updated_at, e.created_at,
|
|
1079
|
-
COUNT(m.id) as memory_count,
|
|
1080
|
-
MAX(m.last_accessed_at) as last_memory_access,
|
|
1081
|
-
SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
|
|
1082
|
-
SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
|
|
1083
|
-
SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
|
|
1084
|
-
SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
|
|
1085
|
-
SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
|
|
1086
|
-
FROM entities e
|
|
1087
|
-
LEFT JOIN memories m ON m.entity_id = e.id
|
|
1088
|
-
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
|
|
1089
1120
|
`;
|
|
1090
1121
|
const params = [];
|
|
1091
1122
|
if (kind) {
|
|
@@ -1137,29 +1168,29 @@ function handleRecallFile(args) {
|
|
|
1137
1168
|
return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
|
|
1138
1169
|
}
|
|
1139
1170
|
// Daily breakdown
|
|
1140
|
-
const daily = db.prepare(`
|
|
1141
|
-
SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
|
|
1142
|
-
FROM session_file_edits WHERE file_path LIKE ?
|
|
1143
|
-
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
|
|
1144
1175
|
`).all(`%${sub}%`);
|
|
1145
1176
|
// Distinct context_snippets (intents) — deduped, ordered by recency
|
|
1146
|
-
const intents = db.prepare(`
|
|
1147
|
-
SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
|
|
1148
|
-
FROM session_file_edits
|
|
1149
|
-
WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
|
|
1150
|
-
GROUP BY context_snippet
|
|
1151
|
-
ORDER BY last_at DESC
|
|
1152
|
-
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 ?
|
|
1153
1184
|
`).all(`%${sub}%`, maxIntents);
|
|
1154
1185
|
// Linked memories
|
|
1155
|
-
const memories = db.prepare(`
|
|
1156
|
-
SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
|
|
1157
|
-
FROM session_file_edits sfe
|
|
1158
|
-
JOIN memories m ON m.id = sfe.memory_id
|
|
1159
|
-
JOIN entities e ON e.id = m.entity_id
|
|
1160
|
-
WHERE sfe.file_path LIKE ?
|
|
1161
|
-
ORDER BY m.importance DESC
|
|
1162
|
-
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
|
|
1163
1194
|
`).all(`%${sub}%`);
|
|
1164
1195
|
// Distinct file paths matched (the substring may match multiple files)
|
|
1165
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}%`);
|
|
@@ -1226,10 +1257,10 @@ async function handleConsolidateWithSampling(args) {
|
|
|
1226
1257
|
const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
|
|
1227
1258
|
const snapshot = new Map();
|
|
1228
1259
|
const candidateRows = db
|
|
1229
|
-
.prepare(`SELECT m.id, m.content, e.name as entity_name
|
|
1230
|
-
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1231
|
-
WHERE m.protected = 0
|
|
1232
|
-
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')
|
|
1233
1264
|
AND m.created_at <= ?`)
|
|
1234
1265
|
.all(ageCutoff);
|
|
1235
1266
|
for (const r of candidateRows)
|
|
@@ -1595,8 +1626,8 @@ function resolveProjectFromRecentEdits() {
|
|
|
1595
1626
|
if (projects.length <= 1)
|
|
1596
1627
|
return { project: projects[0] };
|
|
1597
1628
|
const rows = db
|
|
1598
|
-
.prepare(`SELECT file_path FROM session_file_edits
|
|
1599
|
-
WHERE occurred_at > unixepoch() - 86400
|
|
1629
|
+
.prepare(`SELECT file_path FROM session_file_edits
|
|
1630
|
+
WHERE occurred_at > unixepoch() - 86400
|
|
1600
1631
|
ORDER BY occurred_at DESC LIMIT 50`)
|
|
1601
1632
|
.all();
|
|
1602
1633
|
if (rows.length === 0)
|
|
@@ -1784,25 +1815,25 @@ function handleFlagProposals(args) {
|
|
|
1784
1815
|
const sessionContext = args.session_context ?? 'conversation session';
|
|
1785
1816
|
const PROPOSAL_TAG = 'orphaned_proposal';
|
|
1786
1817
|
const now = Math.floor(Date.now() / 1000);
|
|
1787
|
-
const insertAnchor = db.prepare(`
|
|
1788
|
-
INSERT INTO drift_anchors (
|
|
1789
|
-
kind, statement, rationale, domain, decision_mode,
|
|
1790
|
-
confidence, lifecycle, status, owner,
|
|
1791
|
-
evidence_refs, created_at, updated_at
|
|
1792
|
-
) VALUES (
|
|
1793
|
-
'decision', ?, ?, ?, 'hypothesis',
|
|
1794
|
-
?, 'active', 'active', 'agent',
|
|
1795
|
-
?, ?, ?
|
|
1796
|
-
)
|
|
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
|
+
)
|
|
1797
1828
|
`);
|
|
1798
|
-
const insertCandidate = db.prepare(`
|
|
1799
|
-
INSERT INTO memory_write_candidates (
|
|
1800
|
-
scope, candidate_type, target_node_id, rationale,
|
|
1801
|
-
confidence, evidence_refs, status, created_at
|
|
1802
|
-
) VALUES (
|
|
1803
|
-
'orphaned_proposal', 'update_node', ?, ?,
|
|
1804
|
-
?, ?, 'pending_review', ?
|
|
1805
|
-
)
|
|
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
|
+
)
|
|
1806
1837
|
`);
|
|
1807
1838
|
const results = [];
|
|
1808
1839
|
const txn = db.transaction(() => {
|
|
@@ -1846,13 +1877,13 @@ function handleFlagProposals(args) {
|
|
|
1846
1877
|
function handleDream(args) {
|
|
1847
1878
|
const domainFilter = args?.domain;
|
|
1848
1879
|
// 1. Fetch active North Star anchor(s)
|
|
1849
|
-
const northStars = db.prepare(`
|
|
1850
|
-
SELECT id, statement, rationale, domain, confidence, lifecycle,
|
|
1851
|
-
datetime(review_after, 'unixepoch') as review_date,
|
|
1852
|
-
datetime(created_at, 'unixepoch') as declared_at
|
|
1853
|
-
FROM drift_anchors
|
|
1854
|
-
WHERE node_type = 'north_star' AND status = 'active'
|
|
1855
|
-
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
|
|
1856
1887
|
`).all();
|
|
1857
1888
|
// Re-injection friction — the active-observability loop closing back into reflection.
|
|
1858
1889
|
// "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
|
|
@@ -1870,23 +1901,23 @@ function handleDream(args) {
|
|
|
1870
1901
|
const distillLimit = Math.max(1, Math.min(25, Number(args?.distill ?? 8) || 8));
|
|
1871
1902
|
let distillQueue = [];
|
|
1872
1903
|
try {
|
|
1873
|
-
const rows = db.prepare(`
|
|
1874
|
-
SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
|
|
1875
|
-
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1876
|
-
WHERE m.layer IN ('learning', 'caveat')
|
|
1877
|
-
AND json_valid(m.content)
|
|
1878
|
-
-- settle window: the Stop hook extracts every turn, so the newest rows belong to a
|
|
1879
|
-
-- session still in motion. Don't ask the agent to distill the conversation it is in.
|
|
1880
|
-
AND m.created_at < unixepoch() - 1800
|
|
1881
|
-
AND json_extract(m.content, '$.distill_verdict') IS NULL
|
|
1882
|
-
AND (json_extract(m.content, '$.needs_distill') = 1
|
|
1883
|
-
OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
|
|
1884
|
-
OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
|
|
1885
|
-
ORDER BY (m.layer = 'caveat') DESC,
|
|
1886
|
-
(e.name <> 'Local') DESC,
|
|
1887
|
-
length(json_extract(m.content, '$.what')) DESC,
|
|
1888
|
-
m.created_at DESC
|
|
1889
|
-
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}
|
|
1890
1921
|
`).all();
|
|
1891
1922
|
distillQueue = rows.map((r) => {
|
|
1892
1923
|
let c = {};
|
|
@@ -1926,26 +1957,26 @@ function handleDream(args) {
|
|
|
1926
1957
|
}
|
|
1927
1958
|
// 2. Fetch pending orphaned proposals
|
|
1928
1959
|
const candidateQuery = domainFilter
|
|
1929
|
-
? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1930
|
-
mc.rationale as candidate_rationale, mc.confidence,
|
|
1931
|
-
mc.evidence_refs, mc.status,
|
|
1932
|
-
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1933
|
-
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1934
|
-
da.evidence_refs as anchor_evidence
|
|
1935
|
-
FROM memory_write_candidates mc
|
|
1936
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1937
|
-
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1938
|
-
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 = ?
|
|
1939
1970
|
ORDER BY mc.created_at DESC`
|
|
1940
|
-
: `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1941
|
-
mc.rationale as candidate_rationale, mc.confidence,
|
|
1942
|
-
mc.evidence_refs, mc.status,
|
|
1943
|
-
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1944
|
-
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1945
|
-
da.evidence_refs as anchor_evidence
|
|
1946
|
-
FROM memory_write_candidates mc
|
|
1947
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1948
|
-
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'
|
|
1949
1980
|
ORDER BY mc.created_at DESC`;
|
|
1950
1981
|
const candidates = domainFilter
|
|
1951
1982
|
? db.prepare(candidateQuery).all(domainFilter)
|
|
@@ -2027,12 +2058,12 @@ function handleResolveProposal(args) {
|
|
|
2027
2058
|
throw new Error('verdict must be "surface" or "dismiss"');
|
|
2028
2059
|
}
|
|
2029
2060
|
// Verify candidate exists and is pending
|
|
2030
|
-
const candidate = db.prepare(`
|
|
2031
|
-
SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
|
|
2032
|
-
da.statement
|
|
2033
|
-
FROM memory_write_candidates mc
|
|
2034
|
-
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
2035
|
-
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'
|
|
2036
2067
|
`).get(candidateId);
|
|
2037
2068
|
if (!candidate) {
|
|
2038
2069
|
throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
|
|
@@ -2097,6 +2128,18 @@ const LEGACY_TOOLS = {
|
|
|
2097
2128
|
dream: 'recall({ dream: true }) — recall() with no arguments already includes the counts',
|
|
2098
2129
|
resolve_proposal: "resolve_drift({ candidate_id, action: 'surface' | 'dismiss', rationale })",
|
|
2099
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
|
+
}
|
|
2100
2143
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2101
2144
|
const showLegacy = process.env.LINKSEE_LEGACY_TOOLS === '1';
|
|
2102
2145
|
const tools = TOOLS
|
|
@@ -2146,6 +2189,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2146
2189
|
case 'resolve_proposal':
|
|
2147
2190
|
text = handleResolveProposal(args);
|
|
2148
2191
|
break;
|
|
2192
|
+
// Share a fix (link only)
|
|
2193
|
+
case 'share_draft':
|
|
2194
|
+
text = handleShareDraft(args);
|
|
2195
|
+
break;
|
|
2149
2196
|
default: {
|
|
2150
2197
|
const migrations = {
|
|
2151
2198
|
update_memory: 'remember({ memory_id: <id>, content: "...", importance: 0.8 })',
|