linksee-memory 0.7.2 → 0.10.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/LICENSE +21 -21
- package/README.md +839 -678
- package/dist/bin/declare-anchor.d.ts +2 -0
- package/dist/bin/declare-anchor.js +146 -0
- package/dist/bin/detect-drift.d.ts +2 -0
- package/dist/bin/detect-drift.js +91 -0
- package/dist/bin/guard-hook.d.ts +2 -0
- package/dist/bin/guard-hook.js +96 -0
- package/dist/bin/import-sessions.js +44 -11
- package/dist/bin/install-skill.js +14 -14
- package/dist/bin/setup.js +107 -18
- package/dist/bin/stats.js +20 -20
- package/dist/db/migrate.js +52 -28
- package/dist/db/schema.sql +371 -232
- package/dist/lib/consolidate.js +19 -19
- package/dist/lib/drift-anchors.d.ts +78 -0
- package/dist/lib/drift-anchors.js +224 -0
- package/dist/lib/drift-detection.d.ts +62 -0
- package/dist/lib/drift-detection.js +416 -0
- package/dist/lib/drift-view.d.ts +62 -0
- package/dist/lib/drift-view.js +120 -0
- package/dist/lib/edge-detection.js +8 -8
- package/dist/lib/guard.d.ts +81 -0
- package/dist/lib/guard.js +321 -0
- package/dist/lib/lexical-match.d.ts +6 -0
- package/dist/lib/lexical-match.js +87 -0
- package/dist/lib/momentum.js +7 -7
- package/dist/lib/session-extractor.d.ts +1 -0
- package/dist/lib/session-extractor.js +62 -11
- package/dist/lib/truth-engine.d.ts +84 -0
- package/dist/lib/truth-engine.js +417 -0
- package/dist/mcp/read-smart.js +8 -8
- package/dist/mcp/server.js +573 -3
- package/dist/skill/SKILL.md +734 -631
- package/package.json +10 -5
package/dist/mcp/server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// linksee-memory MCP server (stdio transport).
|
|
3
|
-
// Tools: remember / recall / read_smart
|
|
4
|
-
// v0.
|
|
3
|
+
// Tools: remember / recall / read_smart / drift_status / check_decision / declare_anchor / resolve_drift / flag_proposals / dream / resolve_proposal
|
|
4
|
+
// v0.10.0 — Re-injection layer: pre-action guard (PreToolUse/SessionStart hooks) + extraction
|
|
5
|
+
// quality gate + distillation routine (dream distill_queue) + harden/soften escalation
|
|
5
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -19,7 +20,10 @@ import { PROMPTS, getPrompt } from './prompts.js';
|
|
|
19
20
|
import { fetchRoots, isInsideRoots } from './roots.js';
|
|
20
21
|
import { sampleConsolidation } from './sampling.js';
|
|
21
22
|
import { confirmForget } from './elicitation.js';
|
|
22
|
-
|
|
23
|
+
import { getTruthView, getDecisionDetail, resolveDrift } from '../lib/truth-engine.js';
|
|
24
|
+
import { declareAnchor, setNodeFields } from '../lib/drift-anchors.js';
|
|
25
|
+
import { getReinjectionFriction, setGateMode } from '../lib/guard.js';
|
|
26
|
+
const SERVER_VERSION = '0.10.0';
|
|
23
27
|
const db = openDb();
|
|
24
28
|
runMigrations(db);
|
|
25
29
|
// Auto-maintenance: consolidate stale memories on startup (non-blocking)
|
|
@@ -142,6 +146,148 @@ const TOOLS = [
|
|
|
142
146
|
required: ['path'],
|
|
143
147
|
},
|
|
144
148
|
},
|
|
149
|
+
// ── Drift tools (v0.8.0) ─────────────────────────────────────────────────
|
|
150
|
+
{
|
|
151
|
+
name: 'drift_status',
|
|
152
|
+
description: 'Check what\'s drifting right now — the "Intent Datadog" for your product decisions.\n\nReturns a structured truth map showing which decisions/constraints/hypotheses are:\n🔴 drift (unaccounted divergence from intent)\n🟡 review (soft signal, awaiting human decision)\n⚪ held (acknowledged, time-boxed, not forgotten)\n🔵 aligned (reality matches intent)\n\nNodes are classified into 4 species:\n• hypothesis → Decision Cards (decision journal format)\n• constraint → Rules (pass/fail checklist)\n• commitment → Heartbeats (cadence monitoring)\n• source_of_truth → Reference (stable anchors)\n\nWHEN TO CALL:\n• At session start — "what needs my attention?"\n• Before making a decision — check for existing anchors on the topic\n• After completing work — verify drift state changed\n• When the user asks about product health / what\'s broken / what\'s stale',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
domain: { type: 'string', description: 'Filter by domain (strategy, product, engineering, growth, etc.)' },
|
|
157
|
+
decision_mode: { type: 'string', description: 'Filter by decision_mode (hypothesis, constraint, commitment, source_of_truth)' },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'check_decision',
|
|
163
|
+
description: 'Deep-dive into a specific decision/anchor — its state, premises, drift edges, and pending candidates.\n\nReturns the full context for one truth-map node: what was decided, why, what reality says, whether it\'s drifting, and what actions are pending.\n\nWHEN TO CALL:\n• When the user asks about a specific decision ("what happened with X?")\n• Before resolving a drift signal — understand the full picture first\n• When reviewing premises of a decision ("is this still true?")',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
anchor_id: { type: 'number', description: 'The drift_anchor ID to inspect' },
|
|
168
|
+
},
|
|
169
|
+
required: ['anchor_id'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'declare_anchor',
|
|
174
|
+
description: 'Declare a new decision, constraint, or prohibition as a truth-map anchor.\n\nAnchors are NORMATIVE claims: "we decided X", "Y is forbidden", "Z must always hold."\nThe drift detector later checks these against committed reality.\n\ndeclare-don\'t-mine: anchors come ONLY from explicit human declaration, never from pattern extraction.\n\nWHEN TO CALL:\n• When the user makes a product decision ("let\'s go with approach A")\n• When a constraint is established ("never do X")\n• When a commitment is made ("we ship weekly")\n• When the user says "anchor this" / "record this decision"',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {
|
|
178
|
+
kind: { type: 'string', enum: ['prohibition', 'decision', 'constraint'], description: 'Anchor type' },
|
|
179
|
+
statement: { type: 'string', description: 'The normative claim (>= 8 chars)' },
|
|
180
|
+
rationale: { type: 'string', description: 'Why this was decided' },
|
|
181
|
+
affects: { type: 'array', items: { type: 'string' }, description: 'Path globs that scope this anchor' },
|
|
182
|
+
detect_terms: { type: 'array', items: { type: 'string' }, description: 'Keywords for scoping' },
|
|
183
|
+
violation_signal: { type: 'array', items: { type: 'string' }, description: 'Terms whose presence = violation (required for prohibition/decision)' },
|
|
184
|
+
tier: { type: 'string', enum: ['human', 'explicit'], description: 'Declaration tier (default: human)' },
|
|
185
|
+
// v9 ProjectCoreNode fields
|
|
186
|
+
node_type: { type: 'string', description: 'Node type label' },
|
|
187
|
+
domain: { type: 'string', description: 'Domain (strategy, product, engineering, etc.)' },
|
|
188
|
+
decision_mode: { type: 'string', enum: ['hypothesis', 'constraint', 'commitment', 'source_of_truth'], description: 'Classification for 4-species display' },
|
|
189
|
+
confidence: { type: 'number', minimum: 0, maximum: 1, description: 'Confidence level (0.0-1.0)' },
|
|
190
|
+
lifecycle: { type: 'string', description: 'Lifecycle state (active, at_risk, retired)' },
|
|
191
|
+
review_after: { type: 'string', description: 'ISO date for next review (e.g. "2026-07-04")' },
|
|
192
|
+
},
|
|
193
|
+
required: ['kind', 'statement'],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'resolve_drift',
|
|
198
|
+
description: 'Record a resolution for a drifting anchor — the human feedback loop.\n\n6 actions:\n• fix — "we fixed the code/reality to match intent" → state becomes aligned\n• supersede — "intent evolved, this is the new direction" → state becomes aligned\n• acknowledge — "we know, parking it for now" → state becomes held (with optional review date)\n• dismiss — "false positive, not actually drifting" → edges dismissed\n• harden — "re-injected but still violated, enforce it" → card_policy.gate_mode=hard (PreToolUse will BLOCK)\n• soften — "back off to a warning" → gate_mode=soft\n\nWHEN TO CALL:\n• After drift_status shows 🔴 drift or 🟡 review items\n• When the user says "that\'s fixed" / "ignore that" / "we changed direction"\n• When acknowledging a known gap with a review date',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
anchor_id: { type: 'number', description: 'The drift_anchor ID to resolve' },
|
|
203
|
+
action: { type: 'string', enum: ['fix', 'supersede', 'acknowledge', 'dismiss', 'harden', 'soften'], description: 'Resolution action' },
|
|
204
|
+
rationale: { type: 'string', description: 'Why this resolution (recorded for audit trail)' },
|
|
205
|
+
review_after: { type: 'string', description: 'For acknowledge: ISO date to re-check (e.g. "2026-07-04")' },
|
|
206
|
+
superseded_by: { type: 'number', description: 'For supersede: the new anchor ID that replaces this one' },
|
|
207
|
+
},
|
|
208
|
+
required: ['anchor_id', 'action'],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'flag_proposals',
|
|
213
|
+
description: 'Record orphaned proposals — options you presented that the user never addressed.\n\n' +
|
|
214
|
+
'Conversations are tree-shaped but experienced linearly. When you present 3 options and the user ' +
|
|
215
|
+
'engages with only 1, the other 2 become "orphaned proposals" — unresolved decision branches that ' +
|
|
216
|
+
'both you and the user lose track of.\n\n' +
|
|
217
|
+
'WHEN TO CALL:\n' +
|
|
218
|
+
'• When you notice the user engaged with only some of the options you presented\n' +
|
|
219
|
+
'• When the conversation shifted topic and earlier proposals were never resolved\n' +
|
|
220
|
+
'• At session end, review what you proposed vs what was addressed\n' +
|
|
221
|
+
'• When the user says "what else did we discuss?" or "何か忘れてない?"\n\n' +
|
|
222
|
+
'Each proposal becomes a review-state anchor on the dashboard — visible until the user decides.\n' +
|
|
223
|
+
'This is declaration, not mining: YOU are the curator recognizing what went unaddressed.',
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {
|
|
227
|
+
proposals: {
|
|
228
|
+
type: 'array',
|
|
229
|
+
description: 'Array of unresolved proposals (1-10 items)',
|
|
230
|
+
items: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
statement: { type: 'string', description: 'The proposal itself — what was suggested but not addressed (prefix with [未解決] for dashboard clarity)' },
|
|
234
|
+
rationale: { type: 'string', description: 'Context: what discussion it came from, what the user chose instead, why this matters' },
|
|
235
|
+
domain: { type: 'string', description: 'Topic domain (e.g. monetization, product, engineering, strategy, growth, roadmap)' },
|
|
236
|
+
confidence: { type: 'number', minimum: 0, maximum: 1, description: 'How confident you are this is worth revisiting (0.3-0.7 typical)' },
|
|
237
|
+
siblings: { type: 'array', items: { type: 'string' }, description: 'Other options from the same proposal set (for context)' },
|
|
238
|
+
decided: { type: 'string', description: 'What the user chose or engaged with instead' },
|
|
239
|
+
},
|
|
240
|
+
required: ['statement', 'rationale', 'domain'],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
session_context: { type: 'string', description: 'Brief description of the conversation/session where these proposals arose' },
|
|
244
|
+
},
|
|
245
|
+
required: ['proposals'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
// ── Dreaming Memory (v0.9.0) ──────────────────────────────
|
|
249
|
+
{
|
|
250
|
+
name: 'dream',
|
|
251
|
+
description: 'Dreaming Memory — consolidate orphaned proposals against the North Star.\n\n' +
|
|
252
|
+
'Returns the project\'s North Star (direction/goals/ICP/phase) alongside unresolved proposals ' +
|
|
253
|
+
'that agents flagged during conversations. YOUR job as the evaluating agent is to decide:\n\n' +
|
|
254
|
+
'• surface — genuinely important unresolved fork point given the current direction\n' +
|
|
255
|
+
'• dismiss — outdated, already implicitly resolved, or irrelevant to current goals\n\n' +
|
|
256
|
+
'Think like a General Doctor doing triage: the North Star is the patient\'s chart, ' +
|
|
257
|
+
'each proposal is a symptom. Not every symptom needs treatment.\n\n' +
|
|
258
|
+
'WHEN TO CALL:\n' +
|
|
259
|
+
'• At session start to triage accumulated proposals\n' +
|
|
260
|
+
'• When the user asks "何か見落としてない?" or "what should we revisit?"\n' +
|
|
261
|
+
'• Periodically to prevent proposal backlog from growing stale\n\n' +
|
|
262
|
+
'After evaluation, call resolve_proposal for each candidate with your verdict.\n\n' +
|
|
263
|
+
'ALSO RETURNS: `distill_queue` — auto-captured memories whose content is still a RAW user utterance. ' +
|
|
264
|
+
'Rewrite each via remember(memory_id, content) per the guide in the response (one-line what, true why, ' +
|
|
265
|
+
'"distilled": true). Drain up to 8 per call — the SessionStart digest reminds you while the queue is non-empty. ' +
|
|
266
|
+
'And `friction` — anchors re-surfaced at the gate yet still contradicted (resolve_drift action:"harden" to enforce).',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
domain: { type: 'string', description: 'Filter proposals by domain (e.g. strategy, product, engineering)' },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'resolve_proposal',
|
|
276
|
+
description: 'Record your evaluation verdict for an orphaned proposal after dreaming.\n\n' +
|
|
277
|
+
'Call this after `dream` for each candidate you evaluated against the North Star.\n' +
|
|
278
|
+
'• surface — Keep visible on dashboard for human decision\n' +
|
|
279
|
+
'• dismiss — Remove from dashboard (outdated/irrelevant/implicitly resolved)\n\n' +
|
|
280
|
+
'Always reference the North Star criteria in your rationale.',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
candidate_id: { type: 'number', description: 'The candidate ID from dream results' },
|
|
285
|
+
verdict: { type: 'string', enum: ['surface', 'dismiss'], description: 'Your evaluation verdict' },
|
|
286
|
+
rationale: { type: 'string', description: 'Why — must reference North Star criteria' },
|
|
287
|
+
},
|
|
288
|
+
required: ['candidate_id', 'verdict', 'rationale'],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
145
291
|
];
|
|
146
292
|
// ============================================================
|
|
147
293
|
// Handlers
|
|
@@ -1067,6 +1213,407 @@ async function handleRecallUnified(args) {
|
|
|
1067
1213
|
return handleRecall(args);
|
|
1068
1214
|
}
|
|
1069
1215
|
// ============================================================
|
|
1216
|
+
// Drift tool handlers (v0.8.0)
|
|
1217
|
+
// ============================================================
|
|
1218
|
+
function handleDriftStatus(args) {
|
|
1219
|
+
const view = getTruthView(db, {
|
|
1220
|
+
domain: args?.domain,
|
|
1221
|
+
decision_mode: args?.decision_mode,
|
|
1222
|
+
});
|
|
1223
|
+
// Build a concise triage line
|
|
1224
|
+
const { by_state, nodes } = view.counts;
|
|
1225
|
+
const triage = [
|
|
1226
|
+
by_state.drift > 0 ? `🔴 ${by_state.drift} drifting` : null,
|
|
1227
|
+
by_state.review > 0 ? `🟡 ${by_state.review} needs review` : null,
|
|
1228
|
+
by_state.held > 0 ? `⚪ ${by_state.held} held` : null,
|
|
1229
|
+
`🔵 ${by_state.aligned} aligned`,
|
|
1230
|
+
].filter(Boolean).join(' · ');
|
|
1231
|
+
return JSON.stringify({
|
|
1232
|
+
ok: true,
|
|
1233
|
+
triage: `${nodes} anchors: ${triage}`,
|
|
1234
|
+
nextReopen: view.nextReopen,
|
|
1235
|
+
attention: view.attention,
|
|
1236
|
+
alignedByDomain: view.alignedByDomain,
|
|
1237
|
+
candidates: view.candidates,
|
|
1238
|
+
counts: view.counts,
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function handleCheckDecision(args) {
|
|
1242
|
+
if (!args?.anchor_id)
|
|
1243
|
+
throw new Error('anchor_id is required');
|
|
1244
|
+
const detail = getDecisionDetail(db, args.anchor_id);
|
|
1245
|
+
if (!detail) {
|
|
1246
|
+
return JSON.stringify({ ok: false, error: `Anchor ${args.anchor_id} not found or not active` });
|
|
1247
|
+
}
|
|
1248
|
+
return JSON.stringify({ ok: true, decision: detail });
|
|
1249
|
+
}
|
|
1250
|
+
function handleDeclareAnchor(args) {
|
|
1251
|
+
if (!args?.kind || !args?.statement) {
|
|
1252
|
+
throw new Error('kind and statement are required');
|
|
1253
|
+
}
|
|
1254
|
+
// Create the base anchor
|
|
1255
|
+
const anchor = declareAnchor(db, {
|
|
1256
|
+
kind: args.kind,
|
|
1257
|
+
statement: args.statement,
|
|
1258
|
+
rationale: args.rationale,
|
|
1259
|
+
affects: args.affects,
|
|
1260
|
+
detect_terms: args.detect_terms,
|
|
1261
|
+
violation_signal: args.violation_signal,
|
|
1262
|
+
tier: args.tier ?? 'human',
|
|
1263
|
+
});
|
|
1264
|
+
// Apply v9 ProjectCoreNode fields if provided
|
|
1265
|
+
const nodeFields = {};
|
|
1266
|
+
if (args.node_type !== undefined)
|
|
1267
|
+
nodeFields.node_type = args.node_type;
|
|
1268
|
+
if (args.domain !== undefined)
|
|
1269
|
+
nodeFields.domain = args.domain;
|
|
1270
|
+
if (args.decision_mode !== undefined)
|
|
1271
|
+
nodeFields.decision_mode = args.decision_mode;
|
|
1272
|
+
if (args.confidence !== undefined)
|
|
1273
|
+
nodeFields.confidence = args.confidence;
|
|
1274
|
+
if (args.lifecycle !== undefined)
|
|
1275
|
+
nodeFields.lifecycle = args.lifecycle;
|
|
1276
|
+
if (args.review_after !== undefined) {
|
|
1277
|
+
nodeFields.review_after = Math.floor(new Date(args.review_after).getTime() / 1000);
|
|
1278
|
+
}
|
|
1279
|
+
if (Object.keys(nodeFields).length > 0) {
|
|
1280
|
+
setNodeFields(db, anchor.id, nodeFields);
|
|
1281
|
+
}
|
|
1282
|
+
return JSON.stringify({
|
|
1283
|
+
ok: true,
|
|
1284
|
+
anchor_id: anchor.id,
|
|
1285
|
+
statement: anchor.statement,
|
|
1286
|
+
kind: anchor.kind,
|
|
1287
|
+
message: `Anchor #${anchor.id} declared: "${anchor.statement.substring(0, 80)}"`,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
function handleResolveDrift(args) {
|
|
1291
|
+
if (!args?.anchor_id || !args?.action) {
|
|
1292
|
+
throw new Error('anchor_id and action are required');
|
|
1293
|
+
}
|
|
1294
|
+
// Enforcement-policy actions — apply the dream "escalate_to_hard" recommendation in ONE call.
|
|
1295
|
+
// Folded into resolve_drift (not a new tool) to honor anchor #1. Intercepted here so the WIP
|
|
1296
|
+
// truth-engine.ts resolveDrift() stays untouched.
|
|
1297
|
+
if (args.action === 'harden' || args.action === 'soften') {
|
|
1298
|
+
const mode = args.action === 'harden' ? 'hard' : 'soft';
|
|
1299
|
+
const res = setGateMode(db, args.anchor_id, mode);
|
|
1300
|
+
return JSON.stringify({
|
|
1301
|
+
...res,
|
|
1302
|
+
action: args.action,
|
|
1303
|
+
message: `anchor #${args.anchor_id} gate_mode → '${mode}'${args.rationale ? ` (${args.rationale})` : ''}. Future PreToolUse on this anchor will ${mode === 'hard' ? 'BLOCK' : 'soft-warn'}.`,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
const result = resolveDrift(db, {
|
|
1307
|
+
anchor_id: args.anchor_id,
|
|
1308
|
+
action: args.action,
|
|
1309
|
+
rationale: args.rationale,
|
|
1310
|
+
review_after: args.review_after,
|
|
1311
|
+
superseded_by: args.superseded_by,
|
|
1312
|
+
});
|
|
1313
|
+
return JSON.stringify(result);
|
|
1314
|
+
}
|
|
1315
|
+
// ============================================================
|
|
1316
|
+
// flag_proposals — orphaned proposal detection (v0.9.0 prototype)
|
|
1317
|
+
// Agent explicitly declares unresolved proposals from the conversation.
|
|
1318
|
+
// Each proposal → anchor(hypothesis) + pending_review candidate → "review" state on dashboard.
|
|
1319
|
+
// ============================================================
|
|
1320
|
+
function handleFlagProposals(args) {
|
|
1321
|
+
const proposals = args?.proposals;
|
|
1322
|
+
if (!Array.isArray(proposals) || proposals.length === 0) {
|
|
1323
|
+
return JSON.stringify({ ok: false, error: 'proposals array is required (1-10 items)' });
|
|
1324
|
+
}
|
|
1325
|
+
if (proposals.length > 10) {
|
|
1326
|
+
return JSON.stringify({ ok: false, error: 'Max 10 proposals per call (batch in multiple calls if needed)' });
|
|
1327
|
+
}
|
|
1328
|
+
const sessionContext = args.session_context ?? 'conversation session';
|
|
1329
|
+
const PROPOSAL_TAG = 'orphaned_proposal';
|
|
1330
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1331
|
+
const insertAnchor = db.prepare(`
|
|
1332
|
+
INSERT INTO drift_anchors (
|
|
1333
|
+
kind, statement, rationale, domain, decision_mode,
|
|
1334
|
+
confidence, lifecycle, status, owner,
|
|
1335
|
+
evidence_refs, created_at, updated_at
|
|
1336
|
+
) VALUES (
|
|
1337
|
+
'decision', ?, ?, ?, 'hypothesis',
|
|
1338
|
+
?, 'active', 'active', 'agent',
|
|
1339
|
+
?, ?, ?
|
|
1340
|
+
)
|
|
1341
|
+
`);
|
|
1342
|
+
const insertCandidate = db.prepare(`
|
|
1343
|
+
INSERT INTO memory_write_candidates (
|
|
1344
|
+
scope, candidate_type, target_node_id, rationale,
|
|
1345
|
+
confidence, evidence_refs, status, created_at
|
|
1346
|
+
) VALUES (
|
|
1347
|
+
'orphaned_proposal', 'update_node', ?, ?,
|
|
1348
|
+
?, ?, 'pending_review', ?
|
|
1349
|
+
)
|
|
1350
|
+
`);
|
|
1351
|
+
const results = [];
|
|
1352
|
+
const txn = db.transaction(() => {
|
|
1353
|
+
for (const p of proposals) {
|
|
1354
|
+
const statement = String(p.statement ?? '').trim();
|
|
1355
|
+
if (statement.length < 10)
|
|
1356
|
+
continue; // skip junk
|
|
1357
|
+
const rationale = String(p.rationale ?? '').trim();
|
|
1358
|
+
const domain = String(p.domain ?? 'general').trim();
|
|
1359
|
+
const confidence = Math.max(0, Math.min(1, Number(p.confidence) || 0.5));
|
|
1360
|
+
const evidenceRefs = JSON.stringify([{
|
|
1361
|
+
type: 'proposal_set',
|
|
1362
|
+
tag: PROPOSAL_TAG,
|
|
1363
|
+
session_context: sessionContext,
|
|
1364
|
+
decided: p.decided ?? null,
|
|
1365
|
+
siblings: p.siblings ?? [],
|
|
1366
|
+
flagged_at: new Date().toISOString(),
|
|
1367
|
+
}]);
|
|
1368
|
+
// 1. Create anchor
|
|
1369
|
+
const anchorResult = insertAnchor.run(statement, rationale, domain, confidence, evidenceRefs, now, now);
|
|
1370
|
+
const anchorId = Number(anchorResult.lastInsertRowid);
|
|
1371
|
+
// 2. Create pending_review candidate → triggers "review" state
|
|
1372
|
+
insertCandidate.run(anchorId, `Orphaned proposal: ${sessionContext}`, confidence, evidenceRefs, now);
|
|
1373
|
+
results.push({ anchor_id: anchorId, statement, domain });
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
txn();
|
|
1377
|
+
return JSON.stringify({
|
|
1378
|
+
ok: true,
|
|
1379
|
+
flagged: results.length,
|
|
1380
|
+
proposals: results,
|
|
1381
|
+
message: `Flagged ${results.length} orphaned proposal(s) for review. They will appear as "review" items on the dashboard.`,
|
|
1382
|
+
tag: PROPOSAL_TAG,
|
|
1383
|
+
hint: 'User can resolve each via resolve_drift(anchor_id, action) or on the dashboard.',
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
// ============================================================
|
|
1387
|
+
// dream — Dreaming Memory: North Star + orphaned proposals for agent evaluation
|
|
1388
|
+
// The agent IS the Doctor. MCP just provides data + update path.
|
|
1389
|
+
// ============================================================
|
|
1390
|
+
function handleDream(args) {
|
|
1391
|
+
const domainFilter = args?.domain;
|
|
1392
|
+
// 1. Fetch active North Star anchor(s)
|
|
1393
|
+
const northStars = db.prepare(`
|
|
1394
|
+
SELECT id, statement, rationale, domain, confidence, lifecycle,
|
|
1395
|
+
datetime(review_after, 'unixepoch') as review_date,
|
|
1396
|
+
datetime(created_at, 'unixepoch') as declared_at
|
|
1397
|
+
FROM drift_anchors
|
|
1398
|
+
WHERE node_type = 'north_star' AND status = 'active'
|
|
1399
|
+
ORDER BY created_at DESC
|
|
1400
|
+
`).all();
|
|
1401
|
+
// Re-injection friction — the active-observability loop closing back into reflection.
|
|
1402
|
+
// "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
|
|
1403
|
+
let friction = [];
|
|
1404
|
+
try {
|
|
1405
|
+
friction = getReinjectionFriction(db, { minContradicts: 3 });
|
|
1406
|
+
}
|
|
1407
|
+
catch {
|
|
1408
|
+
/* additive — never break dream on a friction-query error */
|
|
1409
|
+
}
|
|
1410
|
+
// Distillation queue — the quality gate's LLM half. The hook-path extractor stores RAW
|
|
1411
|
+
// utterances (no LLM there); the agent rewrites them here into clean what/why via
|
|
1412
|
+
// remember(memory_id, content). Matches needs_distill (new) AND the legacy hardcoded
|
|
1413
|
+
// why-strings so the existing backlog is drainable without a backfill write.
|
|
1414
|
+
let distillQueue = [];
|
|
1415
|
+
try {
|
|
1416
|
+
const rows = db.prepare(`
|
|
1417
|
+
SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
|
|
1418
|
+
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1419
|
+
WHERE m.layer IN ('learning', 'caveat')
|
|
1420
|
+
AND json_valid(m.content)
|
|
1421
|
+
AND (json_extract(m.content, '$.needs_distill') = 1
|
|
1422
|
+
OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
|
|
1423
|
+
OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
|
|
1424
|
+
ORDER BY m.created_at DESC LIMIT 8
|
|
1425
|
+
`).all();
|
|
1426
|
+
distillQueue = rows.map((r) => {
|
|
1427
|
+
let c = {};
|
|
1428
|
+
try {
|
|
1429
|
+
c = JSON.parse(r.content);
|
|
1430
|
+
}
|
|
1431
|
+
catch { /* keep empty */ }
|
|
1432
|
+
return {
|
|
1433
|
+
memory_id: r.id,
|
|
1434
|
+
layer: r.layer,
|
|
1435
|
+
entity: r.entity,
|
|
1436
|
+
raw_what: String(c.what ?? '').slice(0, 220),
|
|
1437
|
+
context_hint: c.context_hint ? String(c.context_hint).slice(0, 220) : undefined,
|
|
1438
|
+
affects: c.affects,
|
|
1439
|
+
created: r.created,
|
|
1440
|
+
};
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
/* additive — never break dream on a distill-query error */
|
|
1445
|
+
}
|
|
1446
|
+
if (northStars.length === 0) {
|
|
1447
|
+
return JSON.stringify({
|
|
1448
|
+
ok: true,
|
|
1449
|
+
north_star: null,
|
|
1450
|
+
candidates: [],
|
|
1451
|
+
friction,
|
|
1452
|
+
friction_total: friction.length,
|
|
1453
|
+
distill_queue: distillQueue,
|
|
1454
|
+
distill_total: distillQueue.length,
|
|
1455
|
+
message: friction.length > 0
|
|
1456
|
+
? 'No North Star declared yet — but the re-injection layer surfaced friction below: anchors being violated despite being re-surfaced. Declare a North Star (declare_anchor node_type:"north_star"), and act on the friction items.'
|
|
1457
|
+
: 'No North Star declared yet. Declare one with declare_anchor(node_type: "north_star") before dreaming.',
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
// 2. Fetch pending orphaned proposals
|
|
1461
|
+
const candidateQuery = domainFilter
|
|
1462
|
+
? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1463
|
+
mc.rationale as candidate_rationale, mc.confidence,
|
|
1464
|
+
mc.evidence_refs, mc.status,
|
|
1465
|
+
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1466
|
+
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1467
|
+
da.evidence_refs as anchor_evidence
|
|
1468
|
+
FROM memory_write_candidates mc
|
|
1469
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1470
|
+
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1471
|
+
AND da.domain = ?
|
|
1472
|
+
ORDER BY mc.created_at DESC`
|
|
1473
|
+
: `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1474
|
+
mc.rationale as candidate_rationale, mc.confidence,
|
|
1475
|
+
mc.evidence_refs, mc.status,
|
|
1476
|
+
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1477
|
+
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1478
|
+
da.evidence_refs as anchor_evidence
|
|
1479
|
+
FROM memory_write_candidates mc
|
|
1480
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1481
|
+
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1482
|
+
ORDER BY mc.created_at DESC`;
|
|
1483
|
+
const candidates = domainFilter
|
|
1484
|
+
? db.prepare(candidateQuery).all(domainFilter)
|
|
1485
|
+
: db.prepare(candidateQuery).all();
|
|
1486
|
+
// 3. Enrich with proposal context from evidence_refs
|
|
1487
|
+
const enrichedCandidates = candidates.map(c => {
|
|
1488
|
+
let context = {};
|
|
1489
|
+
try {
|
|
1490
|
+
const refs = JSON.parse(c.anchor_evidence || '[]');
|
|
1491
|
+
const proposalRef = refs.find((r) => r.tag === 'orphaned_proposal');
|
|
1492
|
+
if (proposalRef) {
|
|
1493
|
+
context = {
|
|
1494
|
+
decided: proposalRef.decided,
|
|
1495
|
+
siblings: proposalRef.siblings,
|
|
1496
|
+
session_context: proposalRef.session_context,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
catch { /* malformed JSON — skip enrichment */ }
|
|
1501
|
+
return {
|
|
1502
|
+
candidate_id: c.candidate_id,
|
|
1503
|
+
anchor_id: c.anchor_id,
|
|
1504
|
+
statement: c.statement,
|
|
1505
|
+
rationale: c.anchor_rationale,
|
|
1506
|
+
domain: c.domain,
|
|
1507
|
+
confidence: c.confidence,
|
|
1508
|
+
flagged_at: c.flagged_at,
|
|
1509
|
+
...context,
|
|
1510
|
+
};
|
|
1511
|
+
});
|
|
1512
|
+
const guideParts = [];
|
|
1513
|
+
if (enrichedCandidates.length > 0) {
|
|
1514
|
+
guideParts.push('Evaluate each candidate against the North Star: "Given our current direction/ICP/phase, is this ' +
|
|
1515
|
+
'unresolved branch still important?" Then call resolve_proposal(candidate_id, verdict, rationale) for each.');
|
|
1516
|
+
}
|
|
1517
|
+
if (friction.length > 0) {
|
|
1518
|
+
guideParts.push(`⚠ FRICTION: ${friction.length} accepted anchor(s) keep being re-surfaced at the gate (see "friction"). ` +
|
|
1519
|
+
'For each suggested_action="escalate_to_hard", call resolve_drift(anchor_id, action:"harden") to make the ' +
|
|
1520
|
+
'gate BLOCK it; for "review_or_supersede", call resolve_drift(anchor_id, action:"supersede", …) if the rule ' +
|
|
1521
|
+
'has outrun reality.');
|
|
1522
|
+
}
|
|
1523
|
+
if (distillQueue.length > 0) {
|
|
1524
|
+
guideParts.push(`🧪 DISTILL: ${distillQueue.length} auto-extracted memories hold RAW user utterances (see "distill_queue"). ` +
|
|
1525
|
+
'For each: rewrite into ONE clean decision/warning using raw_what + context_hint (resolve references like ' +
|
|
1526
|
+
'"option a"), then save via remember(memory_id, content) with the full structured JSON — a one-line `what` ' +
|
|
1527
|
+
'(the actual decision, not the chat), a true `why`, the original affects, `"distilled": true` (REQUIRED — ' +
|
|
1528
|
+
'this marker is what protects your rewrite from the next session re-import; omit it and the raw utterance ' +
|
|
1529
|
+
'resurrects), and NO needs_distill field. ' +
|
|
1530
|
+
'If raw_what carries no real decision/warning, set its type to "note" and state to "superseded" instead.');
|
|
1531
|
+
}
|
|
1532
|
+
if (guideParts.length === 0)
|
|
1533
|
+
guideParts.push('No pending proposals, no gate friction, nothing to distill. The dream is clear.');
|
|
1534
|
+
return JSON.stringify({
|
|
1535
|
+
ok: true,
|
|
1536
|
+
north_star: northStars[0],
|
|
1537
|
+
all_north_stars: northStars.length > 1 ? northStars : undefined,
|
|
1538
|
+
candidates: enrichedCandidates,
|
|
1539
|
+
total: enrichedCandidates.length,
|
|
1540
|
+
friction,
|
|
1541
|
+
friction_total: friction.length,
|
|
1542
|
+
distill_queue: distillQueue,
|
|
1543
|
+
distill_total: distillQueue.length,
|
|
1544
|
+
guide: guideParts.join(' '),
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
// ============================================================
|
|
1548
|
+
// resolve_proposal — Agent writes back surface/dismiss verdict
|
|
1549
|
+
// ============================================================
|
|
1550
|
+
function handleResolveProposal(args) {
|
|
1551
|
+
const candidateId = args?.candidate_id;
|
|
1552
|
+
const verdict = args?.verdict;
|
|
1553
|
+
const rationale = args?.rationale;
|
|
1554
|
+
if (!candidateId || !verdict || !rationale) {
|
|
1555
|
+
throw new Error('candidate_id, verdict, and rationale are required');
|
|
1556
|
+
}
|
|
1557
|
+
if (!['surface', 'dismiss'].includes(verdict)) {
|
|
1558
|
+
throw new Error('verdict must be "surface" or "dismiss"');
|
|
1559
|
+
}
|
|
1560
|
+
// Verify candidate exists and is pending
|
|
1561
|
+
const candidate = db.prepare(`
|
|
1562
|
+
SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
|
|
1563
|
+
da.statement
|
|
1564
|
+
FROM memory_write_candidates mc
|
|
1565
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1566
|
+
WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
|
|
1567
|
+
`).get(candidateId);
|
|
1568
|
+
if (!candidate) {
|
|
1569
|
+
throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
|
|
1570
|
+
}
|
|
1571
|
+
if (candidate.status !== 'pending_review') {
|
|
1572
|
+
return JSON.stringify({
|
|
1573
|
+
ok: false,
|
|
1574
|
+
error: `Candidate #${candidateId} is already "${candidate.status}" — cannot re-evaluate`,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1578
|
+
const doctorNote = {
|
|
1579
|
+
type: 'doctor_evaluation',
|
|
1580
|
+
verdict,
|
|
1581
|
+
rationale,
|
|
1582
|
+
evaluated_at: new Date().toISOString(),
|
|
1583
|
+
};
|
|
1584
|
+
// Append doctor evaluation to evidence trail
|
|
1585
|
+
let refs = [];
|
|
1586
|
+
try {
|
|
1587
|
+
refs = JSON.parse(candidate.evidence_refs || '[]');
|
|
1588
|
+
}
|
|
1589
|
+
catch { /* keep empty */ }
|
|
1590
|
+
refs.push(doctorNote);
|
|
1591
|
+
const updatedRefs = JSON.stringify(refs);
|
|
1592
|
+
const txn = db.transaction(() => {
|
|
1593
|
+
if (verdict === 'dismiss') {
|
|
1594
|
+
// Reject candidate + retire anchor (removes from dashboard)
|
|
1595
|
+
db.prepare('UPDATE memory_write_candidates SET status = ?, evidence_refs = ? WHERE id = ?').run('rejected', updatedRefs, candidateId);
|
|
1596
|
+
db.prepare('UPDATE drift_anchors SET status = ?, lifecycle = ?, updated_at = ? WHERE id = ?').run('retired', 'deprecated', now, candidate.target_node_id);
|
|
1597
|
+
}
|
|
1598
|
+
else {
|
|
1599
|
+
// Surface: keep pending_review, record doctor's endorsement
|
|
1600
|
+
db.prepare('UPDATE memory_write_candidates SET evidence_refs = ? WHERE id = ?').run(updatedRefs, candidateId);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
txn();
|
|
1604
|
+
const shortStmt = candidate.statement.substring(0, 60);
|
|
1605
|
+
return JSON.stringify({
|
|
1606
|
+
ok: true,
|
|
1607
|
+
candidate_id: candidateId,
|
|
1608
|
+
anchor_id: candidate.target_node_id,
|
|
1609
|
+
verdict,
|
|
1610
|
+
statement: candidate.statement,
|
|
1611
|
+
message: verdict === 'dismiss'
|
|
1612
|
+
? `Dismissed: "${shortStmt}…" — removed from dashboard`
|
|
1613
|
+
: `Surfaced: "${shortStmt}…" — kept for human review on dashboard`,
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
// ============================================================
|
|
1070
1617
|
// MCP wiring
|
|
1071
1618
|
// ============================================================
|
|
1072
1619
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
@@ -1084,6 +1631,29 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1084
1631
|
case 'read_smart':
|
|
1085
1632
|
text = handleReadSmart(args);
|
|
1086
1633
|
break;
|
|
1634
|
+
// Drift tools (v0.8.0)
|
|
1635
|
+
case 'drift_status':
|
|
1636
|
+
text = handleDriftStatus(args);
|
|
1637
|
+
break;
|
|
1638
|
+
case 'check_decision':
|
|
1639
|
+
text = handleCheckDecision(args);
|
|
1640
|
+
break;
|
|
1641
|
+
case 'declare_anchor':
|
|
1642
|
+
text = handleDeclareAnchor(args);
|
|
1643
|
+
break;
|
|
1644
|
+
case 'resolve_drift':
|
|
1645
|
+
text = handleResolveDrift(args);
|
|
1646
|
+
break;
|
|
1647
|
+
case 'flag_proposals':
|
|
1648
|
+
text = handleFlagProposals(args);
|
|
1649
|
+
break;
|
|
1650
|
+
// Dreaming Memory (v0.9.0)
|
|
1651
|
+
case 'dream':
|
|
1652
|
+
text = handleDream(args);
|
|
1653
|
+
break;
|
|
1654
|
+
case 'resolve_proposal':
|
|
1655
|
+
text = handleResolveProposal(args);
|
|
1656
|
+
break;
|
|
1087
1657
|
default: {
|
|
1088
1658
|
const migrations = {
|
|
1089
1659
|
update_memory: 'remember({ memory_id: <id>, content: "...", importance: 0.8 })',
|