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
|
@@ -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
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export type DriftState = 'drift' | 'review' | 'held' | 'aligned';
|
|
3
|
+
export type Species = 'hypothesis' | 'constraint' | 'commitment' | 'source_of_truth';
|
|
4
|
+
export interface TruthNode {
|
|
5
|
+
id: number;
|
|
6
|
+
node_type: string | null;
|
|
7
|
+
domain: string | null;
|
|
8
|
+
decision_mode: string | null;
|
|
9
|
+
species: Species;
|
|
10
|
+
statement: string;
|
|
11
|
+
rationale: string | null;
|
|
12
|
+
confidence: number;
|
|
13
|
+
lifecycle: string;
|
|
14
|
+
cadence_days: number | null;
|
|
15
|
+
review_after: number | null;
|
|
16
|
+
state: DriftState;
|
|
17
|
+
accounted: boolean;
|
|
18
|
+
accountedBy: string | null;
|
|
19
|
+
reality: string | null;
|
|
20
|
+
reviewDate: string | null;
|
|
21
|
+
overdue: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface TruthCandidate {
|
|
24
|
+
id: number;
|
|
25
|
+
candidate_type: string;
|
|
26
|
+
target_node_id: number | null;
|
|
27
|
+
target_statement: string | null;
|
|
28
|
+
rationale: string;
|
|
29
|
+
confidence: number;
|
|
30
|
+
status: string;
|
|
31
|
+
}
|
|
32
|
+
export interface TruthCounts {
|
|
33
|
+
nodes: number;
|
|
34
|
+
by_mode: Record<string, number>;
|
|
35
|
+
by_species: Record<Species, number>;
|
|
36
|
+
by_state: Record<DriftState, number>;
|
|
37
|
+
auto: number;
|
|
38
|
+
suppressed: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TruthView {
|
|
41
|
+
attention: TruthNode[];
|
|
42
|
+
alignedByDomain: Array<{
|
|
43
|
+
domain: string;
|
|
44
|
+
nodes: TruthNode[];
|
|
45
|
+
}>;
|
|
46
|
+
candidates: {
|
|
47
|
+
auto: TruthCandidate[];
|
|
48
|
+
suppressed: TruthCandidate[];
|
|
49
|
+
};
|
|
50
|
+
counts: TruthCounts;
|
|
51
|
+
nextReopen: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface DecisionDetail extends TruthNode {
|
|
54
|
+
kind: string | null;
|
|
55
|
+
affects: string[];
|
|
56
|
+
detect_terms: string[];
|
|
57
|
+
violation_signal: string[];
|
|
58
|
+
tier: string;
|
|
59
|
+
pendingCandidates: TruthCandidate[];
|
|
60
|
+
driftEdges: Array<{
|
|
61
|
+
edge_id: number;
|
|
62
|
+
verdict: string;
|
|
63
|
+
confidence: number;
|
|
64
|
+
status: string;
|
|
65
|
+
detected_at: number;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
export declare function getTruthView(db: Database.Database, opts?: {
|
|
69
|
+
domain?: string;
|
|
70
|
+
decision_mode?: string;
|
|
71
|
+
}): TruthView;
|
|
72
|
+
export declare function getDecisionDetail(db: Database.Database, anchorId: number): DecisionDetail | null;
|
|
73
|
+
export type ResolutionAction = 'fix' | 'supersede' | 'acknowledge' | 'dismiss';
|
|
74
|
+
export interface ResolveInput {
|
|
75
|
+
anchor_id: number;
|
|
76
|
+
action: ResolutionAction;
|
|
77
|
+
rationale?: string;
|
|
78
|
+
review_after?: string;
|
|
79
|
+
superseded_by?: number;
|
|
80
|
+
}
|
|
81
|
+
export declare function resolveDrift(db: Database.Database, input: ResolveInput): {
|
|
82
|
+
ok: boolean;
|
|
83
|
+
resolution: any;
|
|
84
|
+
};
|