linksee-memory 0.8.0 → 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 -782
- 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/schema.sql +25 -2
- package/dist/lib/consolidate.js +19 -19
- 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.js +11 -11
- package/dist/mcp/read-smart.js +8 -8
- package/dist/mcp/server.js +410 -5
- package/dist/skill/SKILL.md +734 -631
- package/package.json +7 -4
|
@@ -157,7 +157,9 @@ function findFirstIntent(session) {
|
|
|
157
157
|
// Decisions & learnings — user messages containing explicit commitment words.
|
|
158
158
|
// ============================================================
|
|
159
159
|
const DECISION_PATTERNS = [
|
|
160
|
-
|
|
160
|
+
// ひらがな「いこう」「すすめよう」 variants were missed (real loss observed: "OK. CからAいこう").
|
|
161
|
+
// Widening is safe now that content-dedup + chitchat guard + needs_distill triage exist downstream.
|
|
162
|
+
/決めた|採用|確定|これで(いい|進め)|OK進めて|やろう|行こう|いこう|進めよう|すすめよう/,
|
|
161
163
|
/learn(ed)?|decid(?:e|ed|ing)|chose|picked|going with|let'?s\s+go|pivot(?:ing|ed)?|switch(?:ing)?\s+to|settled\s+on|approved|we'?ll\s+use|commit(?:ting)?\s+to/i,
|
|
162
164
|
];
|
|
163
165
|
const FAILURE_PATTERNS = [
|
|
@@ -240,6 +242,10 @@ export function extractSession(session, projectName) {
|
|
|
240
242
|
// Priority: goal(first_intent) > caveat > decision > context. capturedTurns
|
|
241
243
|
// tracks caveat-claimed turns so the decision pass skips them.
|
|
242
244
|
const capturedTurns = new Set();
|
|
245
|
+
// Content-level dedup: the SAME utterance sent twice (interrupt + resend is common)
|
|
246
|
+
// arrives as two distinct turns and used to become two identical memories.
|
|
247
|
+
const seenWhats = new Set();
|
|
248
|
+
const whatKey = (layer, text) => `${layer}::${text.replace(/\s+/g, ' ').trim().toLowerCase().slice(0, 200)}`;
|
|
243
249
|
// Detect fully-automated sessions (e.g. scheduled cron tasks) — no user intent to extract
|
|
244
250
|
const firstRawUserText = session.turns.find((t) => t.role === 'user' && !t.tool_results)?.text ?? '';
|
|
245
251
|
const automated = isAutomatedSession(firstRawUserText);
|
|
@@ -262,6 +268,7 @@ export function extractSession(session, projectName) {
|
|
|
262
268
|
}),
|
|
263
269
|
importance: automated ? 0.3 : 0.8,
|
|
264
270
|
thread_id: session.session_id,
|
|
271
|
+
occurred_at: firstIntent.timestamp,
|
|
265
272
|
source: { session_id: session.session_id, turn_uuid: firstIntent.uuid, kind: 'first_intent' },
|
|
266
273
|
});
|
|
267
274
|
}
|
|
@@ -282,6 +289,7 @@ export function extractSession(session, projectName) {
|
|
|
282
289
|
}),
|
|
283
290
|
importance: 0.2,
|
|
284
291
|
thread_id: session.session_id,
|
|
292
|
+
occurred_at: session.started_at,
|
|
285
293
|
source: { session_id: session.session_id, kind: 'automated_task' },
|
|
286
294
|
});
|
|
287
295
|
}
|
|
@@ -306,8 +314,12 @@ export function extractSession(session, projectName) {
|
|
|
306
314
|
const wouldBeDecision = matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 15 && !isChitchatWithBuriedDecision(t.text, DECISION_PATTERNS);
|
|
307
315
|
if (wouldBeCaveat || wouldBeDecision)
|
|
308
316
|
continue;
|
|
309
|
-
clarifyCount++;
|
|
310
317
|
const msgText = t.text.slice(0, 600);
|
|
318
|
+
const ctxKey = whatKey('context', msgText);
|
|
319
|
+
if (seenWhats.has(ctxKey))
|
|
320
|
+
continue;
|
|
321
|
+
seenWhats.add(ctxKey);
|
|
322
|
+
clarifyCount++;
|
|
311
323
|
memories.push({
|
|
312
324
|
layer: 'context',
|
|
313
325
|
content: buildStructuredContent({
|
|
@@ -322,6 +334,7 @@ export function extractSession(session, projectName) {
|
|
|
322
334
|
}),
|
|
323
335
|
importance: 0.5,
|
|
324
336
|
thread_id: session.session_id,
|
|
337
|
+
occurred_at: t.timestamp,
|
|
325
338
|
source: { session_id: session.session_id, turn_uuid: t.uuid, kind: 'clarification' },
|
|
326
339
|
});
|
|
327
340
|
}
|
|
@@ -339,13 +352,16 @@ export function extractSession(session, projectName) {
|
|
|
339
352
|
const userIntent = (first.preceding_user_text || '').slice(0, 400);
|
|
340
353
|
const contentSnippet = ops.map((o) => o.tool_input_preview).slice(0, 2).join(' | ').slice(0, 500);
|
|
341
354
|
const fileName = path.replace(/\\/g, '/').split('/').pop() || path;
|
|
355
|
+
// `what` must be op-specific, NOT the shared user utterance: one turn editing 5 files
|
|
356
|
+
// produces 5 of these memories, and a shared raw-utterance `what` rendered as 5 visual
|
|
357
|
+
// duplicates in every digest/recall view. The intent lives ONCE, titled, in `why`.
|
|
342
358
|
const memoryContent = buildStructuredContent({
|
|
343
359
|
title: `${opsKinds} ${fileName} (${ops.length} ops)`,
|
|
344
360
|
altitude: 'implementation',
|
|
345
361
|
type: 'work',
|
|
346
362
|
state: 'done',
|
|
347
|
-
what:
|
|
348
|
-
why: userIntent ?
|
|
363
|
+
what: `${opsKinds} ${path} (${ops.length} ops)`,
|
|
364
|
+
why: userIntent ? makeTitle(userIntent, 160) : '(no explicit preceding intent)',
|
|
349
365
|
affects: [path],
|
|
350
366
|
next_action: null,
|
|
351
367
|
evidence_refs: [{ type: 'session', id: session.session_id, label: 'source session' }],
|
|
@@ -358,6 +374,7 @@ export function extractSession(session, projectName) {
|
|
|
358
374
|
content: memoryContent,
|
|
359
375
|
importance: 0.6,
|
|
360
376
|
thread_id: session.session_id,
|
|
377
|
+
occurred_at: first.timestamp,
|
|
361
378
|
source: { session_id: session.session_id, turn_uuid: first.turn_uuid, kind: 'file_edit' },
|
|
362
379
|
});
|
|
363
380
|
// Create a file_edit link record for EACH physical op (NOT deduped),
|
|
@@ -378,7 +395,16 @@ export function extractSession(session, projectName) {
|
|
|
378
395
|
}
|
|
379
396
|
// 4) Caveat layer — user messages matching caveat/failure patterns
|
|
380
397
|
// Stricter filter: must NOT be pasted external content.
|
|
398
|
+
// needs_distill: the stored `what` is a RAW utterance (best heuristic extraction can do
|
|
399
|
+
// without an LLM in the hook path) — `dream` queues these for agent rewriting.
|
|
400
|
+
// context_hint: tail of the assistant message the user was replying to, so the distiller
|
|
401
|
+
// can resolve references like「a)やろう」without re-reading the transcript.
|
|
402
|
+
let prevAssistantForCaveat = '';
|
|
381
403
|
for (const t of session.turns) {
|
|
404
|
+
if (t.role === 'assistant') {
|
|
405
|
+
prevAssistantForCaveat = t.text;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
382
408
|
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
383
409
|
continue;
|
|
384
410
|
if (t === firstIntent)
|
|
@@ -389,6 +415,13 @@ export function extractSession(session, projectName) {
|
|
|
389
415
|
continue;
|
|
390
416
|
if (matchesAny(t.text, CAVEAT_PATTERNS) && t.text.length > 20 && !isChitchatWithBuriedDecision(t.text, CAVEAT_PATTERNS)) {
|
|
391
417
|
const caveatText = t.text.slice(0, 500);
|
|
418
|
+
const key = whatKey('caveat', caveatText);
|
|
419
|
+
if (seenWhats.has(key)) {
|
|
420
|
+
if (t.uuid)
|
|
421
|
+
capturedTurns.add(t.uuid);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
seenWhats.add(key);
|
|
392
425
|
memories.push({
|
|
393
426
|
layer: 'caveat',
|
|
394
427
|
content: buildStructuredContent({
|
|
@@ -397,14 +430,17 @@ export function extractSession(session, projectName) {
|
|
|
397
430
|
type: inferType(caveatText, 'caveat'),
|
|
398
431
|
state: inferState(caveatText, 'caveat'),
|
|
399
432
|
what: caveatText,
|
|
400
|
-
why: '
|
|
433
|
+
why: 'user-stated warning (auto-extracted) — pending distillation',
|
|
401
434
|
affects: extractAffectedPaths(session.file_ops),
|
|
402
435
|
next_action: null,
|
|
403
436
|
evidence_refs: [{ type: 'session', id: session.session_id, label: 'caveat source' }],
|
|
404
437
|
session_id: session.session_id,
|
|
438
|
+
needs_distill: true,
|
|
439
|
+
context_hint: prevAssistantForCaveat.slice(-280),
|
|
405
440
|
}),
|
|
406
441
|
importance: 0.75,
|
|
407
442
|
thread_id: session.session_id,
|
|
443
|
+
occurred_at: t.timestamp,
|
|
408
444
|
source: { session_id: session.session_id, turn_uuid: t.uuid, kind: 'caveat' },
|
|
409
445
|
});
|
|
410
446
|
if (t.uuid)
|
|
@@ -412,11 +448,14 @@ export function extractSession(session, projectName) {
|
|
|
412
448
|
}
|
|
413
449
|
}
|
|
414
450
|
// 5) Learning layer — messages matching decision patterns
|
|
415
|
-
// Same strict filter applies.
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
// the full structured format with agent_proposal + user_approval_scope.
|
|
451
|
+
// Same strict filter applies. Raw `what` + needs_distill + context_hint, same contract
|
|
452
|
+
// as the caveat pass above — `dream` surfaces these for agent distillation.
|
|
453
|
+
let prevAssistantForDecision = '';
|
|
419
454
|
for (const t of session.turns) {
|
|
455
|
+
if (t.role === 'assistant') {
|
|
456
|
+
prevAssistantForDecision = t.text;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
420
459
|
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
421
460
|
continue;
|
|
422
461
|
if (t === firstIntent)
|
|
@@ -427,8 +466,15 @@ export function extractSession(session, projectName) {
|
|
|
427
466
|
continue;
|
|
428
467
|
if (isPastedExternalContent(t.text))
|
|
429
468
|
continue;
|
|
430
|
-
|
|
469
|
+
// Length floor 8 (was 15): short JP approvals ARE the decision record ("OK. CからAいこう",
|
|
470
|
+
// "Bだね。以下OK." — both real, both were lost at 15). They only carry meaning WITH the
|
|
471
|
+
// context_hint captured below, which is exactly what the distill pass resolves.
|
|
472
|
+
if (matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 8 && !isChitchatWithBuriedDecision(t.text, DECISION_PATTERNS)) {
|
|
431
473
|
const decisionText = t.text.slice(0, 500);
|
|
474
|
+
const key = whatKey('learning', decisionText);
|
|
475
|
+
if (seenWhats.has(key))
|
|
476
|
+
continue;
|
|
477
|
+
seenWhats.add(key);
|
|
432
478
|
memories.push({
|
|
433
479
|
layer: 'learning',
|
|
434
480
|
content: buildStructuredContent({
|
|
@@ -437,14 +483,17 @@ export function extractSession(session, projectName) {
|
|
|
437
483
|
type: inferType(decisionText, 'learning'),
|
|
438
484
|
state: inferState(decisionText, 'learning'),
|
|
439
485
|
what: decisionText,
|
|
440
|
-
why: '
|
|
486
|
+
why: 'decision keyword matched (auto-extracted) — pending distillation',
|
|
441
487
|
affects: extractAffectedPaths(session.file_ops),
|
|
442
488
|
next_action: null,
|
|
443
489
|
evidence_refs: [{ type: 'session', id: session.session_id, label: 'decision source' }],
|
|
444
490
|
session_id: session.session_id,
|
|
491
|
+
needs_distill: true,
|
|
492
|
+
context_hint: prevAssistantForDecision.slice(-280),
|
|
445
493
|
}),
|
|
446
494
|
importance: 0.7,
|
|
447
495
|
thread_id: session.session_id,
|
|
496
|
+
occurred_at: t.timestamp,
|
|
448
497
|
source: { session_id: session.session_id, turn_uuid: t.uuid, kind: 'decision' },
|
|
449
498
|
});
|
|
450
499
|
}
|
|
@@ -468,6 +517,7 @@ export function extractSession(session, projectName) {
|
|
|
468
517
|
}),
|
|
469
518
|
importance: 0.4,
|
|
470
519
|
thread_id: session.session_id,
|
|
520
|
+
occurred_at: session.ended_at,
|
|
471
521
|
source: { session_id: session.session_id, kind: 'error_recovery' },
|
|
472
522
|
});
|
|
473
523
|
}
|
|
@@ -500,6 +550,7 @@ export function extractSession(session, projectName) {
|
|
|
500
550
|
}),
|
|
501
551
|
importance: 0.4,
|
|
502
552
|
thread_id: session.session_id,
|
|
553
|
+
occurred_at: session.ended_at,
|
|
503
554
|
source: { session_id: session.session_id, kind: 'session_summary' },
|
|
504
555
|
});
|
|
505
556
|
}
|
package/dist/lib/truth-engine.js
CHANGED
|
@@ -96,7 +96,7 @@ export function getTruthView(db, opts = {}) {
|
|
|
96
96
|
const resolutionFor = buildResolutionLookup(db);
|
|
97
97
|
// ── Candidates (indexed by target node) ──
|
|
98
98
|
const candRows = db
|
|
99
|
-
.prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status, proposed_node
|
|
99
|
+
.prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status, proposed_node
|
|
100
100
|
FROM memory_write_candidates ORDER BY id DESC`)
|
|
101
101
|
.all();
|
|
102
102
|
const pendingByNode = new Map();
|
|
@@ -113,8 +113,8 @@ export function getTruthView(db, opts = {}) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
// ── Active nodes + state derivation ──
|
|
116
|
-
let sql = `SELECT id, node_type, domain, decision_mode, statement, rationale, confidence, lifecycle,
|
|
117
|
-
card_policy, review_after
|
|
116
|
+
let sql = `SELECT id, node_type, domain, decision_mode, statement, rationale, confidence, lifecycle,
|
|
117
|
+
card_policy, review_after
|
|
118
118
|
FROM drift_anchors WHERE status = 'active'`;
|
|
119
119
|
const params = [];
|
|
120
120
|
if (opts.domain) {
|
|
@@ -264,8 +264,8 @@ export function getTruthView(db, opts = {}) {
|
|
|
264
264
|
// ── check_decision: single-node deep view ────────────────────────────────────
|
|
265
265
|
export function getDecisionDetail(db, anchorId) {
|
|
266
266
|
const row = db
|
|
267
|
-
.prepare(`SELECT id, kind, node_type, domain, decision_mode, statement, rationale, confidence,
|
|
268
|
-
lifecycle, card_policy, review_after, affects, detect_terms, violation_signal, tier
|
|
267
|
+
.prepare(`SELECT id, kind, node_type, domain, decision_mode, statement, rationale, confidence,
|
|
268
|
+
lifecycle, card_policy, review_after, affects, detect_terms, violation_signal, tier
|
|
269
269
|
FROM drift_anchors WHERE id = ? AND status = 'active'`)
|
|
270
270
|
.get(anchorId);
|
|
271
271
|
if (!row)
|
|
@@ -283,14 +283,14 @@ export function getDecisionDetail(db, anchorId) {
|
|
|
283
283
|
// State derivation (same logic)
|
|
284
284
|
let state, accounted, accountedBy;
|
|
285
285
|
const pendingCand = db
|
|
286
|
-
.prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status
|
|
287
|
-
FROM memory_write_candidates
|
|
288
|
-
WHERE target_node_id = ? AND status = 'pending_review'
|
|
286
|
+
.prepare(`SELECT id, candidate_type, target_node_id, rationale, confidence, status
|
|
287
|
+
FROM memory_write_candidates
|
|
288
|
+
WHERE target_node_id = ? AND status = 'pending_review'
|
|
289
289
|
ORDER BY id DESC`)
|
|
290
290
|
.all(anchorId);
|
|
291
291
|
const cardCand = db
|
|
292
|
-
.prepare(`SELECT rationale FROM memory_write_candidates
|
|
293
|
-
WHERE target_node_id = ? AND proposed_node LIKE '%"src":"t%'
|
|
292
|
+
.prepare(`SELECT rationale FROM memory_write_candidates
|
|
293
|
+
WHERE target_node_id = ? AND proposed_node LIKE '%"src":"t%'
|
|
294
294
|
ORDER BY id DESC LIMIT 1`)
|
|
295
295
|
.get(anchorId);
|
|
296
296
|
const hasPending = pendingCand.length > 0;
|
|
@@ -328,7 +328,7 @@ export function getDecisionDetail(db, anchorId) {
|
|
|
328
328
|
?? (state === 'aligned' ? 'Committed reality matches intent (convergent)' : null);
|
|
329
329
|
// Drift edges for this anchor
|
|
330
330
|
const edges = db
|
|
331
|
-
.prepare(`SELECT id AS edge_id, verdict, confidence, status, detected_at
|
|
331
|
+
.prepare(`SELECT id AS edge_id, verdict, confidence, status, detected_at
|
|
332
332
|
FROM drift_edges WHERE anchor_id = ? ORDER BY detected_at DESC`)
|
|
333
333
|
.all(anchorId);
|
|
334
334
|
return {
|
package/dist/mcp/read-smart.js
CHANGED
|
@@ -30,14 +30,14 @@ export function handleReadSmart(db, args) {
|
|
|
30
30
|
const fileHash = hashFile(content);
|
|
31
31
|
const chunks = chunkFile(path, content);
|
|
32
32
|
const chunkMeta = chunks.map(toMeta);
|
|
33
|
-
db.prepare(`INSERT INTO file_snapshots (path, content_hash, mtime, size_bytes, chunks, last_read_at, read_count)
|
|
34
|
-
VALUES (?, ?, ?, ?, ?, unixepoch(), 1)
|
|
35
|
-
ON CONFLICT(path) DO UPDATE SET
|
|
36
|
-
content_hash = excluded.content_hash,
|
|
37
|
-
mtime = excluded.mtime,
|
|
38
|
-
size_bytes = excluded.size_bytes,
|
|
39
|
-
chunks = excluded.chunks,
|
|
40
|
-
last_read_at = unixepoch(),
|
|
33
|
+
db.prepare(`INSERT INTO file_snapshots (path, content_hash, mtime, size_bytes, chunks, last_read_at, read_count)
|
|
34
|
+
VALUES (?, ?, ?, ?, ?, unixepoch(), 1)
|
|
35
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
36
|
+
content_hash = excluded.content_hash,
|
|
37
|
+
mtime = excluded.mtime,
|
|
38
|
+
size_bytes = excluded.size_bytes,
|
|
39
|
+
chunks = excluded.chunks,
|
|
40
|
+
last_read_at = unixepoch(),
|
|
41
41
|
read_count = read_count + 1`).run(path, fileHash, mtime, size, JSON.stringify(chunkMeta));
|
|
42
42
|
return JSON.stringify({
|
|
43
43
|
ok: true,
|