linksee-memory 0.8.0 → 0.11.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 +923 -782
- package/dist/bin/guard-hook.d.ts +2 -0
- package/dist/bin/guard-hook.js +96 -0
- package/dist/bin/import-sessions.js +40 -7
- package/dist/bin/map-import.d.ts +2 -0
- package/dist/bin/map-import.js +493 -0
- package/dist/bin/setup.js +94 -5
- package/dist/db/migrate.js +36 -0
- package/dist/db/schema.sql +96 -2
- package/dist/lib/drift-detection.js +38 -2
- 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/map-import.d.ts +55 -0
- package/dist/lib/map-import.js +150 -0
- package/dist/lib/map-reconcile.d.ts +29 -0
- package/dist/lib/map-reconcile.js +245 -0
- package/dist/lib/map-view.d.ts +103 -0
- package/dist/lib/map-view.js +201 -0
- package/dist/lib/session-extractor.d.ts +1 -0
- package/dist/lib/session-extractor.js +62 -11
- package/dist/mcp/server.js +455 -5
- package/dist/skill/SKILL.md +103 -0
- package/package.json +12 -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 / drift_status / check_decision / declare_anchor / resolve_drift
|
|
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';
|
|
@@ -21,7 +22,9 @@ import { sampleConsolidation } from './sampling.js';
|
|
|
21
22
|
import { confirmForget } from './elicitation.js';
|
|
22
23
|
import { getTruthView, getDecisionDetail, resolveDrift } from '../lib/truth-engine.js';
|
|
23
24
|
import { declareAnchor, setNodeFields } from '../lib/drift-anchors.js';
|
|
24
|
-
|
|
25
|
+
import { getReinjectionFriction, setGateMode } from '../lib/guard.js';
|
|
26
|
+
import { whereAmI } from '../lib/map-view.js';
|
|
27
|
+
const SERVER_VERSION = '0.10.0';
|
|
25
28
|
const db = openDb();
|
|
26
29
|
runMigrations(db);
|
|
27
30
|
// Auto-maintenance: consolidate stale memories on startup (non-blocking)
|
|
@@ -156,6 +159,19 @@ const TOOLS = [
|
|
|
156
159
|
},
|
|
157
160
|
},
|
|
158
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: 'where_am_i',
|
|
164
|
+
description: 'Locate the current topic on the Current Truth Map and report "you are here" + blast radius — the per-turn re-anchor.\n\nReturns the matching Map node(s) + journey stage (発見→…→拡張), the BLAST RADIUS (what becomes suspect if you change this — the must-stay-consistent-with / should-align-with / realizes dependents; e.g. editing the README implicates the LP), and the decision behind the node (linked anchor), if any.\n\nThree ways to call:\n• NO ARGS → auto-locates from the files you JUST edited this session (the zero-effort re-anchor — call it freely as you work).\n• query: "<topic>" → lexical locate by topic.\n• node_id: "<id>" → exact node.\n\nThis is how you avoid optimizing one node while silently breaking its neighbors (change the spec → npm/Docs/LP must move too). Matching is lexical (no embeddings).\n\nWHEN TO CALL:\n• Right after editing files — call with no args to see what you just touched + its blast radius.\n• When the topic shifts — re-anchor to the new node.\n• When the user asks "what does changing X affect?" / "where does this fit?"',
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
query: { type: 'string', description: 'Topic to locate (e.g. "changing the telemetry contract"). Omit to auto-locate from your recent edits.' },
|
|
169
|
+
node_id: { type: 'string', description: 'Exact Map node id, if known (e.g. "readme") — bypasses lexical match' },
|
|
170
|
+
project: { type: 'string', description: 'Map project slug (defaults to the most recently imported Map)' },
|
|
171
|
+
limit: { type: 'number', description: 'Max matches (default 3)' },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
159
175
|
{
|
|
160
176
|
name: 'check_decision',
|
|
161
177
|
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?")',
|
|
@@ -193,12 +209,12 @@ const TOOLS = [
|
|
|
193
209
|
},
|
|
194
210
|
{
|
|
195
211
|
name: 'resolve_drift',
|
|
196
|
-
description: 'Record a resolution for a drifting anchor — the human feedback loop.\n\
|
|
212
|
+
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',
|
|
197
213
|
inputSchema: {
|
|
198
214
|
type: 'object',
|
|
199
215
|
properties: {
|
|
200
216
|
anchor_id: { type: 'number', description: 'The drift_anchor ID to resolve' },
|
|
201
|
-
action: { type: 'string', enum: ['fix', 'supersede', 'acknowledge', 'dismiss'], description: 'Resolution action' },
|
|
217
|
+
action: { type: 'string', enum: ['fix', 'supersede', 'acknowledge', 'dismiss', 'harden', 'soften'], description: 'Resolution action' },
|
|
202
218
|
rationale: { type: 'string', description: 'Why this resolution (recorded for audit trail)' },
|
|
203
219
|
review_after: { type: 'string', description: 'For acknowledge: ISO date to re-check (e.g. "2026-07-04")' },
|
|
204
220
|
superseded_by: { type: 'number', description: 'For supersede: the new anchor ID that replaces this one' },
|
|
@@ -206,6 +222,86 @@ const TOOLS = [
|
|
|
206
222
|
required: ['anchor_id', 'action'],
|
|
207
223
|
},
|
|
208
224
|
},
|
|
225
|
+
{
|
|
226
|
+
name: 'flag_proposals',
|
|
227
|
+
description: 'Record orphaned proposals — options you presented that the user never addressed.\n\n' +
|
|
228
|
+
'Conversations are tree-shaped but experienced linearly. When you present 3 options and the user ' +
|
|
229
|
+
'engages with only 1, the other 2 become "orphaned proposals" — unresolved decision branches that ' +
|
|
230
|
+
'both you and the user lose track of.\n\n' +
|
|
231
|
+
'WHEN TO CALL:\n' +
|
|
232
|
+
'• When you notice the user engaged with only some of the options you presented\n' +
|
|
233
|
+
'• When the conversation shifted topic and earlier proposals were never resolved\n' +
|
|
234
|
+
'• At session end, review what you proposed vs what was addressed\n' +
|
|
235
|
+
'• When the user says "what else did we discuss?" or "何か忘れてない?"\n\n' +
|
|
236
|
+
'Each proposal becomes a review-state anchor on the dashboard — visible until the user decides.\n' +
|
|
237
|
+
'This is declaration, not mining: YOU are the curator recognizing what went unaddressed.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
proposals: {
|
|
242
|
+
type: 'array',
|
|
243
|
+
description: 'Array of unresolved proposals (1-10 items)',
|
|
244
|
+
items: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
statement: { type: 'string', description: 'The proposal itself — what was suggested but not addressed (prefix with [未解決] for dashboard clarity)' },
|
|
248
|
+
rationale: { type: 'string', description: 'Context: what discussion it came from, what the user chose instead, why this matters' },
|
|
249
|
+
domain: { type: 'string', description: 'Topic domain (e.g. monetization, product, engineering, strategy, growth, roadmap)' },
|
|
250
|
+
confidence: { type: 'number', minimum: 0, maximum: 1, description: 'How confident you are this is worth revisiting (0.3-0.7 typical)' },
|
|
251
|
+
siblings: { type: 'array', items: { type: 'string' }, description: 'Other options from the same proposal set (for context)' },
|
|
252
|
+
decided: { type: 'string', description: 'What the user chose or engaged with instead' },
|
|
253
|
+
},
|
|
254
|
+
required: ['statement', 'rationale', 'domain'],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
session_context: { type: 'string', description: 'Brief description of the conversation/session where these proposals arose' },
|
|
258
|
+
},
|
|
259
|
+
required: ['proposals'],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
// ── Dreaming Memory (v0.9.0) ──────────────────────────────
|
|
263
|
+
{
|
|
264
|
+
name: 'dream',
|
|
265
|
+
description: 'Dreaming Memory — consolidate orphaned proposals against the North Star.\n\n' +
|
|
266
|
+
'Returns the project\'s North Star (direction/goals/ICP/phase) alongside unresolved proposals ' +
|
|
267
|
+
'that agents flagged during conversations. YOUR job as the evaluating agent is to decide:\n\n' +
|
|
268
|
+
'• surface — genuinely important unresolved fork point given the current direction\n' +
|
|
269
|
+
'• dismiss — outdated, already implicitly resolved, or irrelevant to current goals\n\n' +
|
|
270
|
+
'Think like a General Doctor doing triage: the North Star is the patient\'s chart, ' +
|
|
271
|
+
'each proposal is a symptom. Not every symptom needs treatment.\n\n' +
|
|
272
|
+
'WHEN TO CALL:\n' +
|
|
273
|
+
'• At session start to triage accumulated proposals\n' +
|
|
274
|
+
'• When the user asks "何か見落としてない?" or "what should we revisit?"\n' +
|
|
275
|
+
'• Periodically to prevent proposal backlog from growing stale\n\n' +
|
|
276
|
+
'After evaluation, call resolve_proposal for each candidate with your verdict.\n\n' +
|
|
277
|
+
'ALSO RETURNS: `distill_queue` — auto-captured memories whose content is still a RAW user utterance. ' +
|
|
278
|
+
'Rewrite each via remember(memory_id, content) per the guide in the response (one-line what, true why, ' +
|
|
279
|
+
'"distilled": true). Drain up to 8 per call — the SessionStart digest reminds you while the queue is non-empty. ' +
|
|
280
|
+
'And `friction` — anchors re-surfaced at the gate yet still contradicted (resolve_drift action:"harden" to enforce).',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
domain: { type: 'string', description: 'Filter proposals by domain (e.g. strategy, product, engineering)' },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'resolve_proposal',
|
|
290
|
+
description: 'Record your evaluation verdict for an orphaned proposal after dreaming.\n\n' +
|
|
291
|
+
'Call this after `dream` for each candidate you evaluated against the North Star.\n' +
|
|
292
|
+
'• surface — Keep visible on dashboard for human decision\n' +
|
|
293
|
+
'• dismiss — Remove from dashboard (outdated/irrelevant/implicitly resolved)\n\n' +
|
|
294
|
+
'Always reference the North Star criteria in your rationale.',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
candidate_id: { type: 'number', description: 'The candidate ID from dream results' },
|
|
299
|
+
verdict: { type: 'string', enum: ['surface', 'dismiss'], description: 'Your evaluation verdict' },
|
|
300
|
+
rationale: { type: 'string', description: 'Why — must reference North Star criteria' },
|
|
301
|
+
},
|
|
302
|
+
required: ['candidate_id', 'verdict', 'rationale'],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
209
305
|
];
|
|
210
306
|
// ============================================================
|
|
211
307
|
// Handlers
|
|
@@ -1156,6 +1252,34 @@ function handleDriftStatus(args) {
|
|
|
1156
1252
|
counts: view.counts,
|
|
1157
1253
|
});
|
|
1158
1254
|
}
|
|
1255
|
+
function handleWhereAmI(args) {
|
|
1256
|
+
const res = whereAmI(db, {
|
|
1257
|
+
query: args?.query, node_id: args?.node_id, project: args?.project, limit: args?.limit,
|
|
1258
|
+
});
|
|
1259
|
+
if (res.matched.length === 0) {
|
|
1260
|
+
return JSON.stringify({
|
|
1261
|
+
ok: true, project: res.project, located: false,
|
|
1262
|
+
hint: res.project ? 'No node matched. Try a node id or a topical keyword.' : 'No Map imported yet — run linksee-memory-map.',
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
const located = res.matched.map((m) => ({
|
|
1266
|
+
node: m.node.id,
|
|
1267
|
+
stage: m.stage_label,
|
|
1268
|
+
status: m.node.status,
|
|
1269
|
+
statement: m.node.statement,
|
|
1270
|
+
why: m.match_reason,
|
|
1271
|
+
// the whole point: what else moves if you touch this
|
|
1272
|
+
blast_radius: m.blast.map((b) => `${b.id} (${b.relation})`),
|
|
1273
|
+
decision: m.anchor ? `#${m.anchor.id}: ${m.anchor.statement}` : null,
|
|
1274
|
+
}));
|
|
1275
|
+
const top = res.matched[0];
|
|
1276
|
+
const youAreHere = `You are at "${top.node.id}"`
|
|
1277
|
+
+ (top.stage_label ? ` in stage 「${top.stage_label}」` : '')
|
|
1278
|
+
+ `. Touching it implicates ${top.blast.length} node(s)`
|
|
1279
|
+
+ (top.blast.length ? `: ${top.blast.map((b) => b.id).join(', ')}` : ' (isolated)')
|
|
1280
|
+
+ '.';
|
|
1281
|
+
return JSON.stringify({ ok: true, project: res.project, located: true, you_are_here: youAreHere, job: res.job, matched: located });
|
|
1282
|
+
}
|
|
1159
1283
|
function handleCheckDecision(args) {
|
|
1160
1284
|
if (!args?.anchor_id)
|
|
1161
1285
|
throw new Error('anchor_id is required');
|
|
@@ -1209,6 +1333,18 @@ function handleResolveDrift(args) {
|
|
|
1209
1333
|
if (!args?.anchor_id || !args?.action) {
|
|
1210
1334
|
throw new Error('anchor_id and action are required');
|
|
1211
1335
|
}
|
|
1336
|
+
// Enforcement-policy actions — apply the dream "escalate_to_hard" recommendation in ONE call.
|
|
1337
|
+
// Folded into resolve_drift (not a new tool) to honor anchor #1. Intercepted here so the WIP
|
|
1338
|
+
// truth-engine.ts resolveDrift() stays untouched.
|
|
1339
|
+
if (args.action === 'harden' || args.action === 'soften') {
|
|
1340
|
+
const mode = args.action === 'harden' ? 'hard' : 'soft';
|
|
1341
|
+
const res = setGateMode(db, args.anchor_id, mode);
|
|
1342
|
+
return JSON.stringify({
|
|
1343
|
+
...res,
|
|
1344
|
+
action: args.action,
|
|
1345
|
+
message: `anchor #${args.anchor_id} gate_mode → '${mode}'${args.rationale ? ` (${args.rationale})` : ''}. Future PreToolUse on this anchor will ${mode === 'hard' ? 'BLOCK' : 'soft-warn'}.`,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1212
1348
|
const result = resolveDrift(db, {
|
|
1213
1349
|
anchor_id: args.anchor_id,
|
|
1214
1350
|
action: args.action,
|
|
@@ -1219,6 +1355,307 @@ function handleResolveDrift(args) {
|
|
|
1219
1355
|
return JSON.stringify(result);
|
|
1220
1356
|
}
|
|
1221
1357
|
// ============================================================
|
|
1358
|
+
// flag_proposals — orphaned proposal detection (v0.9.0 prototype)
|
|
1359
|
+
// Agent explicitly declares unresolved proposals from the conversation.
|
|
1360
|
+
// Each proposal → anchor(hypothesis) + pending_review candidate → "review" state on dashboard.
|
|
1361
|
+
// ============================================================
|
|
1362
|
+
function handleFlagProposals(args) {
|
|
1363
|
+
const proposals = args?.proposals;
|
|
1364
|
+
if (!Array.isArray(proposals) || proposals.length === 0) {
|
|
1365
|
+
return JSON.stringify({ ok: false, error: 'proposals array is required (1-10 items)' });
|
|
1366
|
+
}
|
|
1367
|
+
if (proposals.length > 10) {
|
|
1368
|
+
return JSON.stringify({ ok: false, error: 'Max 10 proposals per call (batch in multiple calls if needed)' });
|
|
1369
|
+
}
|
|
1370
|
+
const sessionContext = args.session_context ?? 'conversation session';
|
|
1371
|
+
const PROPOSAL_TAG = 'orphaned_proposal';
|
|
1372
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1373
|
+
const insertAnchor = db.prepare(`
|
|
1374
|
+
INSERT INTO drift_anchors (
|
|
1375
|
+
kind, statement, rationale, domain, decision_mode,
|
|
1376
|
+
confidence, lifecycle, status, owner,
|
|
1377
|
+
evidence_refs, created_at, updated_at
|
|
1378
|
+
) VALUES (
|
|
1379
|
+
'decision', ?, ?, ?, 'hypothesis',
|
|
1380
|
+
?, 'active', 'active', 'agent',
|
|
1381
|
+
?, ?, ?
|
|
1382
|
+
)
|
|
1383
|
+
`);
|
|
1384
|
+
const insertCandidate = db.prepare(`
|
|
1385
|
+
INSERT INTO memory_write_candidates (
|
|
1386
|
+
scope, candidate_type, target_node_id, rationale,
|
|
1387
|
+
confidence, evidence_refs, status, created_at
|
|
1388
|
+
) VALUES (
|
|
1389
|
+
'orphaned_proposal', 'update_node', ?, ?,
|
|
1390
|
+
?, ?, 'pending_review', ?
|
|
1391
|
+
)
|
|
1392
|
+
`);
|
|
1393
|
+
const results = [];
|
|
1394
|
+
const txn = db.transaction(() => {
|
|
1395
|
+
for (const p of proposals) {
|
|
1396
|
+
const statement = String(p.statement ?? '').trim();
|
|
1397
|
+
if (statement.length < 10)
|
|
1398
|
+
continue; // skip junk
|
|
1399
|
+
const rationale = String(p.rationale ?? '').trim();
|
|
1400
|
+
const domain = String(p.domain ?? 'general').trim();
|
|
1401
|
+
const confidence = Math.max(0, Math.min(1, Number(p.confidence) || 0.5));
|
|
1402
|
+
const evidenceRefs = JSON.stringify([{
|
|
1403
|
+
type: 'proposal_set',
|
|
1404
|
+
tag: PROPOSAL_TAG,
|
|
1405
|
+
session_context: sessionContext,
|
|
1406
|
+
decided: p.decided ?? null,
|
|
1407
|
+
siblings: p.siblings ?? [],
|
|
1408
|
+
flagged_at: new Date().toISOString(),
|
|
1409
|
+
}]);
|
|
1410
|
+
// 1. Create anchor
|
|
1411
|
+
const anchorResult = insertAnchor.run(statement, rationale, domain, confidence, evidenceRefs, now, now);
|
|
1412
|
+
const anchorId = Number(anchorResult.lastInsertRowid);
|
|
1413
|
+
// 2. Create pending_review candidate → triggers "review" state
|
|
1414
|
+
insertCandidate.run(anchorId, `Orphaned proposal: ${sessionContext}`, confidence, evidenceRefs, now);
|
|
1415
|
+
results.push({ anchor_id: anchorId, statement, domain });
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
txn();
|
|
1419
|
+
return JSON.stringify({
|
|
1420
|
+
ok: true,
|
|
1421
|
+
flagged: results.length,
|
|
1422
|
+
proposals: results,
|
|
1423
|
+
message: `Flagged ${results.length} orphaned proposal(s) for review. They will appear as "review" items on the dashboard.`,
|
|
1424
|
+
tag: PROPOSAL_TAG,
|
|
1425
|
+
hint: 'User can resolve each via resolve_drift(anchor_id, action) or on the dashboard.',
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
// ============================================================
|
|
1429
|
+
// dream — Dreaming Memory: North Star + orphaned proposals for agent evaluation
|
|
1430
|
+
// The agent IS the Doctor. MCP just provides data + update path.
|
|
1431
|
+
// ============================================================
|
|
1432
|
+
function handleDream(args) {
|
|
1433
|
+
const domainFilter = args?.domain;
|
|
1434
|
+
// 1. Fetch active North Star anchor(s)
|
|
1435
|
+
const northStars = db.prepare(`
|
|
1436
|
+
SELECT id, statement, rationale, domain, confidence, lifecycle,
|
|
1437
|
+
datetime(review_after, 'unixepoch') as review_date,
|
|
1438
|
+
datetime(created_at, 'unixepoch') as declared_at
|
|
1439
|
+
FROM drift_anchors
|
|
1440
|
+
WHERE node_type = 'north_star' AND status = 'active'
|
|
1441
|
+
ORDER BY created_at DESC
|
|
1442
|
+
`).all();
|
|
1443
|
+
// Re-injection friction — the active-observability loop closing back into reflection.
|
|
1444
|
+
// "Re-surfaced N×, yet still contradicted in reality" is the machine evidence behind #15443.
|
|
1445
|
+
let friction = [];
|
|
1446
|
+
try {
|
|
1447
|
+
friction = getReinjectionFriction(db, { minContradicts: 3 });
|
|
1448
|
+
}
|
|
1449
|
+
catch {
|
|
1450
|
+
/* additive — never break dream on a friction-query error */
|
|
1451
|
+
}
|
|
1452
|
+
// Distillation queue — the quality gate's LLM half. The hook-path extractor stores RAW
|
|
1453
|
+
// utterances (no LLM there); the agent rewrites them here into clean what/why via
|
|
1454
|
+
// remember(memory_id, content). Matches needs_distill (new) AND the legacy hardcoded
|
|
1455
|
+
// why-strings so the existing backlog is drainable without a backfill write.
|
|
1456
|
+
let distillQueue = [];
|
|
1457
|
+
try {
|
|
1458
|
+
const rows = db.prepare(`
|
|
1459
|
+
SELECT m.id, m.layer, m.content, datetime(m.created_at, 'unixepoch') AS created, e.name AS entity
|
|
1460
|
+
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
1461
|
+
WHERE m.layer IN ('learning', 'caveat')
|
|
1462
|
+
AND json_valid(m.content)
|
|
1463
|
+
AND (json_extract(m.content, '$.needs_distill') = 1
|
|
1464
|
+
OR json_extract(m.content, '$.why') = 'Decision detected by pattern match — may need agent enrichment'
|
|
1465
|
+
OR json_extract(m.content, '$.why') = 'User-stated warning/prohibition — auto-extracted by caveat pattern match')
|
|
1466
|
+
ORDER BY m.created_at DESC LIMIT 8
|
|
1467
|
+
`).all();
|
|
1468
|
+
distillQueue = rows.map((r) => {
|
|
1469
|
+
let c = {};
|
|
1470
|
+
try {
|
|
1471
|
+
c = JSON.parse(r.content);
|
|
1472
|
+
}
|
|
1473
|
+
catch { /* keep empty */ }
|
|
1474
|
+
return {
|
|
1475
|
+
memory_id: r.id,
|
|
1476
|
+
layer: r.layer,
|
|
1477
|
+
entity: r.entity,
|
|
1478
|
+
raw_what: String(c.what ?? '').slice(0, 220),
|
|
1479
|
+
context_hint: c.context_hint ? String(c.context_hint).slice(0, 220) : undefined,
|
|
1480
|
+
affects: c.affects,
|
|
1481
|
+
created: r.created,
|
|
1482
|
+
};
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
/* additive — never break dream on a distill-query error */
|
|
1487
|
+
}
|
|
1488
|
+
if (northStars.length === 0) {
|
|
1489
|
+
return JSON.stringify({
|
|
1490
|
+
ok: true,
|
|
1491
|
+
north_star: null,
|
|
1492
|
+
candidates: [],
|
|
1493
|
+
friction,
|
|
1494
|
+
friction_total: friction.length,
|
|
1495
|
+
distill_queue: distillQueue,
|
|
1496
|
+
distill_total: distillQueue.length,
|
|
1497
|
+
message: friction.length > 0
|
|
1498
|
+
? '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.'
|
|
1499
|
+
: 'No North Star declared yet. Declare one with declare_anchor(node_type: "north_star") before dreaming.',
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
// 2. Fetch pending orphaned proposals
|
|
1503
|
+
const candidateQuery = domainFilter
|
|
1504
|
+
? `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1505
|
+
mc.rationale as candidate_rationale, mc.confidence,
|
|
1506
|
+
mc.evidence_refs, mc.status,
|
|
1507
|
+
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1508
|
+
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1509
|
+
da.evidence_refs as anchor_evidence
|
|
1510
|
+
FROM memory_write_candidates mc
|
|
1511
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1512
|
+
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1513
|
+
AND da.domain = ?
|
|
1514
|
+
ORDER BY mc.created_at DESC`
|
|
1515
|
+
: `SELECT mc.id as candidate_id, mc.target_node_id as anchor_id,
|
|
1516
|
+
mc.rationale as candidate_rationale, mc.confidence,
|
|
1517
|
+
mc.evidence_refs, mc.status,
|
|
1518
|
+
datetime(mc.created_at, 'unixepoch') as flagged_at,
|
|
1519
|
+
da.statement, da.rationale as anchor_rationale, da.domain,
|
|
1520
|
+
da.evidence_refs as anchor_evidence
|
|
1521
|
+
FROM memory_write_candidates mc
|
|
1522
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1523
|
+
WHERE mc.scope = 'orphaned_proposal' AND mc.status = 'pending_review'
|
|
1524
|
+
ORDER BY mc.created_at DESC`;
|
|
1525
|
+
const candidates = domainFilter
|
|
1526
|
+
? db.prepare(candidateQuery).all(domainFilter)
|
|
1527
|
+
: db.prepare(candidateQuery).all();
|
|
1528
|
+
// 3. Enrich with proposal context from evidence_refs
|
|
1529
|
+
const enrichedCandidates = candidates.map(c => {
|
|
1530
|
+
let context = {};
|
|
1531
|
+
try {
|
|
1532
|
+
const refs = JSON.parse(c.anchor_evidence || '[]');
|
|
1533
|
+
const proposalRef = refs.find((r) => r.tag === 'orphaned_proposal');
|
|
1534
|
+
if (proposalRef) {
|
|
1535
|
+
context = {
|
|
1536
|
+
decided: proposalRef.decided,
|
|
1537
|
+
siblings: proposalRef.siblings,
|
|
1538
|
+
session_context: proposalRef.session_context,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
catch { /* malformed JSON — skip enrichment */ }
|
|
1543
|
+
return {
|
|
1544
|
+
candidate_id: c.candidate_id,
|
|
1545
|
+
anchor_id: c.anchor_id,
|
|
1546
|
+
statement: c.statement,
|
|
1547
|
+
rationale: c.anchor_rationale,
|
|
1548
|
+
domain: c.domain,
|
|
1549
|
+
confidence: c.confidence,
|
|
1550
|
+
flagged_at: c.flagged_at,
|
|
1551
|
+
...context,
|
|
1552
|
+
};
|
|
1553
|
+
});
|
|
1554
|
+
const guideParts = [];
|
|
1555
|
+
if (enrichedCandidates.length > 0) {
|
|
1556
|
+
guideParts.push('Evaluate each candidate against the North Star: "Given our current direction/ICP/phase, is this ' +
|
|
1557
|
+
'unresolved branch still important?" Then call resolve_proposal(candidate_id, verdict, rationale) for each.');
|
|
1558
|
+
}
|
|
1559
|
+
if (friction.length > 0) {
|
|
1560
|
+
guideParts.push(`⚠ FRICTION: ${friction.length} accepted anchor(s) keep being re-surfaced at the gate (see "friction"). ` +
|
|
1561
|
+
'For each suggested_action="escalate_to_hard", call resolve_drift(anchor_id, action:"harden") to make the ' +
|
|
1562
|
+
'gate BLOCK it; for "review_or_supersede", call resolve_drift(anchor_id, action:"supersede", …) if the rule ' +
|
|
1563
|
+
'has outrun reality.');
|
|
1564
|
+
}
|
|
1565
|
+
if (distillQueue.length > 0) {
|
|
1566
|
+
guideParts.push(`🧪 DISTILL: ${distillQueue.length} auto-extracted memories hold RAW user utterances (see "distill_queue"). ` +
|
|
1567
|
+
'For each: rewrite into ONE clean decision/warning using raw_what + context_hint (resolve references like ' +
|
|
1568
|
+
'"option a"), then save via remember(memory_id, content) with the full structured JSON — a one-line `what` ' +
|
|
1569
|
+
'(the actual decision, not the chat), a true `why`, the original affects, `"distilled": true` (REQUIRED — ' +
|
|
1570
|
+
'this marker is what protects your rewrite from the next session re-import; omit it and the raw utterance ' +
|
|
1571
|
+
'resurrects), and NO needs_distill field. ' +
|
|
1572
|
+
'If raw_what carries no real decision/warning, set its type to "note" and state to "superseded" instead.');
|
|
1573
|
+
}
|
|
1574
|
+
if (guideParts.length === 0)
|
|
1575
|
+
guideParts.push('No pending proposals, no gate friction, nothing to distill. The dream is clear.');
|
|
1576
|
+
return JSON.stringify({
|
|
1577
|
+
ok: true,
|
|
1578
|
+
north_star: northStars[0],
|
|
1579
|
+
all_north_stars: northStars.length > 1 ? northStars : undefined,
|
|
1580
|
+
candidates: enrichedCandidates,
|
|
1581
|
+
total: enrichedCandidates.length,
|
|
1582
|
+
friction,
|
|
1583
|
+
friction_total: friction.length,
|
|
1584
|
+
distill_queue: distillQueue,
|
|
1585
|
+
distill_total: distillQueue.length,
|
|
1586
|
+
guide: guideParts.join(' '),
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
// ============================================================
|
|
1590
|
+
// resolve_proposal — Agent writes back surface/dismiss verdict
|
|
1591
|
+
// ============================================================
|
|
1592
|
+
function handleResolveProposal(args) {
|
|
1593
|
+
const candidateId = args?.candidate_id;
|
|
1594
|
+
const verdict = args?.verdict;
|
|
1595
|
+
const rationale = args?.rationale;
|
|
1596
|
+
if (!candidateId || !verdict || !rationale) {
|
|
1597
|
+
throw new Error('candidate_id, verdict, and rationale are required');
|
|
1598
|
+
}
|
|
1599
|
+
if (!['surface', 'dismiss'].includes(verdict)) {
|
|
1600
|
+
throw new Error('verdict must be "surface" or "dismiss"');
|
|
1601
|
+
}
|
|
1602
|
+
// Verify candidate exists and is pending
|
|
1603
|
+
const candidate = db.prepare(`
|
|
1604
|
+
SELECT mc.id, mc.target_node_id, mc.status, mc.evidence_refs,
|
|
1605
|
+
da.statement
|
|
1606
|
+
FROM memory_write_candidates mc
|
|
1607
|
+
JOIN drift_anchors da ON mc.target_node_id = da.id
|
|
1608
|
+
WHERE mc.id = ? AND mc.scope = 'orphaned_proposal'
|
|
1609
|
+
`).get(candidateId);
|
|
1610
|
+
if (!candidate) {
|
|
1611
|
+
throw new Error(`Candidate #${candidateId} not found or not an orphaned proposal`);
|
|
1612
|
+
}
|
|
1613
|
+
if (candidate.status !== 'pending_review') {
|
|
1614
|
+
return JSON.stringify({
|
|
1615
|
+
ok: false,
|
|
1616
|
+
error: `Candidate #${candidateId} is already "${candidate.status}" — cannot re-evaluate`,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1620
|
+
const doctorNote = {
|
|
1621
|
+
type: 'doctor_evaluation',
|
|
1622
|
+
verdict,
|
|
1623
|
+
rationale,
|
|
1624
|
+
evaluated_at: new Date().toISOString(),
|
|
1625
|
+
};
|
|
1626
|
+
// Append doctor evaluation to evidence trail
|
|
1627
|
+
let refs = [];
|
|
1628
|
+
try {
|
|
1629
|
+
refs = JSON.parse(candidate.evidence_refs || '[]');
|
|
1630
|
+
}
|
|
1631
|
+
catch { /* keep empty */ }
|
|
1632
|
+
refs.push(doctorNote);
|
|
1633
|
+
const updatedRefs = JSON.stringify(refs);
|
|
1634
|
+
const txn = db.transaction(() => {
|
|
1635
|
+
if (verdict === 'dismiss') {
|
|
1636
|
+
// Reject candidate + retire anchor (removes from dashboard)
|
|
1637
|
+
db.prepare('UPDATE memory_write_candidates SET status = ?, evidence_refs = ? WHERE id = ?').run('rejected', updatedRefs, candidateId);
|
|
1638
|
+
db.prepare('UPDATE drift_anchors SET status = ?, lifecycle = ?, updated_at = ? WHERE id = ?').run('retired', 'deprecated', now, candidate.target_node_id);
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
// Surface: keep pending_review, record doctor's endorsement
|
|
1642
|
+
db.prepare('UPDATE memory_write_candidates SET evidence_refs = ? WHERE id = ?').run(updatedRefs, candidateId);
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
txn();
|
|
1646
|
+
const shortStmt = candidate.statement.substring(0, 60);
|
|
1647
|
+
return JSON.stringify({
|
|
1648
|
+
ok: true,
|
|
1649
|
+
candidate_id: candidateId,
|
|
1650
|
+
anchor_id: candidate.target_node_id,
|
|
1651
|
+
verdict,
|
|
1652
|
+
statement: candidate.statement,
|
|
1653
|
+
message: verdict === 'dismiss'
|
|
1654
|
+
? `Dismissed: "${shortStmt}…" — removed from dashboard`
|
|
1655
|
+
: `Surfaced: "${shortStmt}…" — kept for human review on dashboard`,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
// ============================================================
|
|
1222
1659
|
// MCP wiring
|
|
1223
1660
|
// ============================================================
|
|
1224
1661
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
@@ -1240,6 +1677,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1240
1677
|
case 'drift_status':
|
|
1241
1678
|
text = handleDriftStatus(args);
|
|
1242
1679
|
break;
|
|
1680
|
+
case 'where_am_i':
|
|
1681
|
+
text = handleWhereAmI(args);
|
|
1682
|
+
break;
|
|
1243
1683
|
case 'check_decision':
|
|
1244
1684
|
text = handleCheckDecision(args);
|
|
1245
1685
|
break;
|
|
@@ -1249,6 +1689,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1249
1689
|
case 'resolve_drift':
|
|
1250
1690
|
text = handleResolveDrift(args);
|
|
1251
1691
|
break;
|
|
1692
|
+
case 'flag_proposals':
|
|
1693
|
+
text = handleFlagProposals(args);
|
|
1694
|
+
break;
|
|
1695
|
+
// Dreaming Memory (v0.9.0)
|
|
1696
|
+
case 'dream':
|
|
1697
|
+
text = handleDream(args);
|
|
1698
|
+
break;
|
|
1699
|
+
case 'resolve_proposal':
|
|
1700
|
+
text = handleResolveProposal(args);
|
|
1701
|
+
break;
|
|
1252
1702
|
default: {
|
|
1253
1703
|
const migrations = {
|
|
1254
1704
|
update_memory: 'remember({ memory_id: <id>, content: "...", importance: 0.8 })',
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -523,6 +523,109 @@ User: "That's it for today"
|
|
|
523
523
|
4. Optionally suggest: consolidate({scope:"session", min_age_days: 14})
|
|
524
524
|
```
|
|
525
525
|
|
|
526
|
+
### Case F2 — Flag orphaned proposals at session end
|
|
527
|
+
|
|
528
|
+
**Conversations are tree-shaped but experienced linearly.** When you present multiple options and the user engages with only some, the rest become "orphaned proposals" — unresolved decision branches that both you and the user lose track of.
|
|
529
|
+
|
|
530
|
+
**WHEN TO FLAG:**
|
|
531
|
+
- At session end, review what you proposed vs what was addressed
|
|
532
|
+
- When the conversation shifted topic and earlier proposals were never resolved
|
|
533
|
+
- When the user engaged with only 1 out of N options you presented
|
|
534
|
+
|
|
535
|
+
```
|
|
536
|
+
1. Review the session: which proposals did you make that the user never addressed?
|
|
537
|
+
2. flag_proposals({
|
|
538
|
+
session_context: "GTM channel strategy discussion",
|
|
539
|
+
proposals: [
|
|
540
|
+
{
|
|
541
|
+
statement: "[未解決] LinkedIn B2B: SaaS企業のCTO/VPE向けDMアウトリーチ",
|
|
542
|
+
rationale: "3つのGTMチャネルを提示したがX/Twitterのみ採用。LinkedIn経由の検討が未着手",
|
|
543
|
+
domain: "growth",
|
|
544
|
+
confidence: 0.5,
|
|
545
|
+
decided: "X/Twitter data-driven growth",
|
|
546
|
+
siblings: ["X/Twitter", "LinkedIn B2B", "Dev Community"]
|
|
547
|
+
},
|
|
548
|
+
...
|
|
549
|
+
]
|
|
550
|
+
})
|
|
551
|
+
3. Report: "Flagged N unresolved proposals for dashboard review."
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Each proposal becomes a review-state anchor on the Linksee Dashboard — visible until the user decides. This is **declaration, not mining**: you are the curator recognizing what went unaddressed.
|
|
555
|
+
|
|
556
|
+
### Case F3 — Dream: triage orphaned proposals against the North Star
|
|
557
|
+
|
|
558
|
+
**Not all orphaned proposals are worth surfacing.** Many are outdated, already implicitly resolved, or irrelevant to the current direction. The `dream` tool returns the project's **North Star** (direction/goals/ICP/phase) alongside accumulated proposals so you can evaluate each one.
|
|
559
|
+
|
|
560
|
+
Think like a General Doctor doing triage: the North Star is the patient's chart, each proposal is a symptom. Not every symptom needs treatment.
|
|
561
|
+
|
|
562
|
+
**When to dream:**
|
|
563
|
+
- At session start, if there are accumulated proposals
|
|
564
|
+
- When the user asks "何か見落としてない?" or "what should we revisit?"
|
|
565
|
+
- Periodically (weekly) to prevent proposal backlog from growing stale
|
|
566
|
+
|
|
567
|
+
```
|
|
568
|
+
1. dream()
|
|
569
|
+
→ Returns: north_star + candidates[]
|
|
570
|
+
|
|
571
|
+
2. For each candidate, evaluate against North Star:
|
|
572
|
+
- Does this affect the current phase/goals? → surface
|
|
573
|
+
- Is this for a different ICP or future phase? → dismiss
|
|
574
|
+
- Already implicitly resolved by later decisions? → dismiss
|
|
575
|
+
|
|
576
|
+
3. resolve_proposal({
|
|
577
|
+
candidate_id: <id>,
|
|
578
|
+
verdict: "surface" | "dismiss",
|
|
579
|
+
rationale: "North Star says ICP = solo devs; this is enterprise-only → dismiss"
|
|
580
|
+
})
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Example evaluation against North Star:**
|
|
584
|
+
```
|
|
585
|
+
North Star: "local-first agent memory for solo devs, HN Launch phase"
|
|
586
|
+
|
|
587
|
+
Candidate A: "CLI-first onboarding wizard"
|
|
588
|
+
→ SURFACE: directly improves DX for ICP, relevant to HN launch
|
|
589
|
+
|
|
590
|
+
Candidate B: "AR glasses integration (2027-28)"
|
|
591
|
+
→ DISMISS: outside current phase, future vision only
|
|
592
|
+
|
|
593
|
+
Candidate C: "kintone enterprise integration"
|
|
594
|
+
→ DISMISS: ICP mismatch (enterprise B2B vs solo devs)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
The North Star is declared via `declare_anchor(node_type: "north_star")` and should be updated when the project enters a new phase (e.g., post-HN → growth phase). This keeps the Doctor's judgment frame current.
|
|
598
|
+
|
|
599
|
+
### Case F4 — Distill: rewrite raw auto-captured memories (every dream call)
|
|
600
|
+
|
|
601
|
+
The session hook captures decisions/caveats as **RAW user utterances** (no LLM runs in the hook path — heuristic extraction is the best it can do). `dream` returns them as `distill_queue`. **You are the distiller.**
|
|
602
|
+
|
|
603
|
+
**When:** every `dream()` call — drain up to 8 items while triaging proposals. The SessionStart boot digest reminds you while the queue is non-empty.
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
1. dream() → distill_queue: [{memory_id, layer, raw_what, context_hint, affects, created}]
|
|
607
|
+
|
|
608
|
+
2. For each item, rewrite into ONE clean record:
|
|
609
|
+
- what = the actual decision/warning in one line — RESOLVE references
|
|
610
|
+
("a)やろう" → what option a actually was, using context_hint)
|
|
611
|
+
- why = the real reason, never "detected by pattern match"
|
|
612
|
+
- keep the original affects; keep layer as-is (protected caveats cannot move —
|
|
613
|
+
put the true type in the content `type` field instead)
|
|
614
|
+
- "distilled": true ← REQUIRED. This marker protects your rewrite from the
|
|
615
|
+
next session re-import (wipe+reinsert). Omit it and the raw
|
|
616
|
+
utterance silently resurrects.
|
|
617
|
+
- drop needs_distill / context_hint from the rewritten JSON
|
|
618
|
+
|
|
619
|
+
3. remember({ memory_id: <id>, content: <full structured JSON> })
|
|
620
|
+
|
|
621
|
+
4. No real decision in raw_what? → type: "note", state: "superseded"
|
|
622
|
+
(false positives get retired in place, never deleted).
|
|
623
|
+
Referent unresolvable (another session's numbered list)? → distill honestly:
|
|
624
|
+
state what IS known, point evidence_refs at the source session.
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Quality bar (measured 2026-06-10, n=32): 81% fully resolvable.** What resolves references is YOUR cross-session memory — if `raw_what` mentions unfamiliar codenames, `recall` the project first, then distill.
|
|
628
|
+
|
|
526
629
|
### Case G — User explicitly says "remember this"
|
|
527
630
|
|
|
528
631
|
User: "Remember this: DocuSign is more stable than CloudSign"
|