linksee-memory 0.14.0 → 0.15.1
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 +17 -27
- package/dist/lib/truth-engine.d.ts +8 -0
- package/dist/lib/truth-engine.js +10 -5
- package/dist/mcp/server.js +149 -12
- package/dist/skill/SKILL.md +26 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -513,44 +513,34 @@ Nothing ever leaves your machine, so step 3 fully erases everything Linksee stor
|
|
|
513
513
|
|
|
514
514
|
</details>
|
|
515
515
|
|
|
516
|
-
##
|
|
516
|
+
## 6 Tools
|
|
517
517
|
|
|
518
|
-
|
|
518
|
+
Two pillars, one surface. **Memory** and **drift** each get the minimum; nothing else is exposed.
|
|
519
|
+
Eleven tools bled model-dependent behaviour across Claude / GPT / Cursor / Codex / Gemini — six
|
|
520
|
+
is what an agent can hold without a manual.
|
|
519
521
|
|
|
520
522
|
| Tool | What it does |
|
|
521
523
|
|---|---|
|
|
522
|
-
| `
|
|
523
|
-
| `
|
|
524
|
-
| `read_smart` | **Token-saving file reader** with AST diff caching.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
| `resolve_drift` | **Close the loop.** Record a resolution: `fix` (reality now matches), `supersede` (intent evolved), `acknowledge` (parking with review date), or `dismiss` (false positive). |
|
|
534
|
-
| `where_am_i` | **"Where on the Map am I, and what else does this touch?"** Locates the current topic/file on the Current Truth Map and returns its journey stage + blast radius (the `must-stay-consistent-with` / `should-align-with` dependents) + the decision behind it. The per-turn re-anchor that stops you optimizing one node while silently breaking its neighbors. |
|
|
535
|
-
|
|
536
|
-
### Fork-point tools (v0.10)
|
|
537
|
-
|
|
538
|
-
| Tool | What it does |
|
|
539
|
-
|---|---|
|
|
540
|
-
| `flag_proposals` | **Record orphaned proposals** — options you presented that the user never addressed. Conversations are tree-shaped but experienced linearly; the branches nobody engaged with become unresolved fork points that both you and the user lose track of. |
|
|
541
|
-
| `dream` | **Consolidate orphaned proposals against the North Star.** Returns the project's direction/goals/ICP alongside unresolved proposals; the evaluating agent decides per candidate: surface (genuinely important fork) or dismiss (outdated / irrelevant / implicitly resolved). |
|
|
542
|
-
| `resolve_proposal` | **Record the verdict** for each dreamed proposal: `surface` (keep visible on the dashboard for human decision) or `dismiss` (remove from the dashboard). |
|
|
543
|
-
|
|
544
|
-
Previous versions exposed 3 tools — v0.8.0 added 4 drift tools that let agents query and act on product-level intent ↔ reality divergence; v0.10 added the fork-point trio for orphaned-proposal triage; `where_am_i` adds the Current Truth Map's per-turn positional re-anchor. The memory tools are unchanged.
|
|
524
|
+
| `recall` | **Start here.** No arguments → the **session brief**: what needs attention, where you are on the Map, open loops, top entities. `query` → search; `path` → a file's edit history with the user intent behind each edit; `where: "<topic>"` → your position on the Current Truth Map + blast radius; `dream: true` → the triage session (North Star, orphaned proposals, distill queue, friction). |
|
|
525
|
+
| `remember` | **Save / update / delete.** `content` is the only required field — entity and layer default to the project you're in. Add `anchor: {}` to record a decision **and** enforce it in one call (re-injected before Edit/Write/Bash and on session start). |
|
|
526
|
+
| `read_smart` | **Token-saving file reader** with AST diff caching. Re-read unchanged = ~50 tokens; modified = changed chunks only. |
|
|
527
|
+
| `drift_status` | **"What's drifting right now?"** The truth map: 🔴 drift / 🟡 review / ⚪ held / 🔵 verified / ⚫ unverified, with the evidence for each. `anchor_id` → deep-dive into one decision. |
|
|
528
|
+
| `declare_anchor` | **Record a normative claim** — `decision` / `prohibition` / `constraint` the detector checks against reality, or `proposal`: an option you presented that the user never addressed, parked as a review item. |
|
|
529
|
+
| `resolve_drift` | **Close the loop.** `fix` · `supersede` · `acknowledge` · `dismiss` (with `hit_term`, and the gate stops firing on it) · `harden` / `soften`. With `candidate_id`: `surface` or `dismiss` an orphaned proposal. |
|
|
530
|
+
|
|
531
|
+
The five earlier names — `where_am_i`, `check_decision`, `flag_proposals`, `dream`,
|
|
532
|
+
`resolve_proposal` — are folded into the six above. They are hidden from `tools/list` but **still
|
|
533
|
+
answer if called**, so a skill or agent written against an older version keeps working.
|
|
534
|
+
`LINKSEE_LEGACY_TOOLS=1` lists them.
|
|
545
535
|
|
|
546
536
|
### CLI utilities
|
|
547
537
|
|
|
548
538
|
| Command | Purpose |
|
|
549
539
|
|---|---|
|
|
550
|
-
| `npx -y linksee-memory setup` | One-command setup: MCP server + skill + Stop hook
|
|
540
|
+
| `npx -y linksee-memory setup` | One-command setup: MCP server + skill + Stop hook + the re-injection guard for every repo (`--project-guard` for this repo only, `--no-guard` to skip). Idempotent — skips what's already done. |
|
|
551
541
|
| `npx linksee-memory` | MCP server (stdio) |
|
|
552
542
|
| `npx -y linksee-memory sync` | Claude Code Stop-hook entry point |
|
|
553
|
-
| `npx -y linksee-memory guard` | Re-injection guard hook: `PreToolUse` gate (`Edit`/`Write`/`Bash`) + `SessionStart` boot digest. Wired
|
|
543
|
+
| `npx -y linksee-memory guard` | Re-injection guard hook: `PreToolUse` gate (`Edit`/`Write`/`Bash`) + `SessionStart` boot digest. Wired by `setup` for every repo (see [Re-injection Guard](#reinjection-guard)); fail-open. |
|
|
554
544
|
| `npx -y linksee-memory import` | Batch-import Claude Code session JSONL history |
|
|
555
545
|
| `npx -y linksee-memory install-skill` | Install the Claude Code Skill that teaches the agent when to call recall/remember/read_smart |
|
|
556
546
|
| `npx -y linksee-memory stats` | Summary of the local DB (entity count / layer breakdown / top entities / top edited files). Add `--json` for machine-readable output. |
|
|
@@ -95,6 +95,14 @@ export type ResolutionAction = 'fix' | 'supersede' | 'acknowledge' | 'dismiss';
|
|
|
95
95
|
export interface ResolveInput {
|
|
96
96
|
/** For dismiss: silence only this match term. Omitted → silence the anchor at the gate. */
|
|
97
97
|
hit_term?: string;
|
|
98
|
+
/**
|
|
99
|
+
* For dismiss: false = close these drift edges as false positives but leave the gate
|
|
100
|
+
* watching. The default (true) also stops the gate. Two different verdicts hide behind
|
|
101
|
+
* "dismiss": "this detection was wrong" and "stop detecting this". Anchor #2 ("no destructive
|
|
102
|
+
* migrations") had a false positive from migrate.ts — the right answer was to close the edge
|
|
103
|
+
* and keep the gate, since "ALTER TABLE memories DROP" is exactly what it exists to catch.
|
|
104
|
+
*/
|
|
105
|
+
gate?: boolean;
|
|
98
106
|
anchor_id: number;
|
|
99
107
|
action: ResolutionAction;
|
|
100
108
|
rationale?: string;
|
package/dist/lib/truth-engine.js
CHANGED
|
@@ -543,12 +543,17 @@ export function resolveDrift(db, input) {
|
|
|
543
543
|
// real detections survive.
|
|
544
544
|
if (input.action === 'dismiss') {
|
|
545
545
|
db.prepare("UPDATE drift_edges SET status = 'dismissed' WHERE anchor_id = ? AND status = 'open'").run(input.anchor_id);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
resolution.gate_dismissed = input.hit_term ? input.hit_term.toLowerCase() : 'all matches';
|
|
546
|
+
if (input.gate === false) {
|
|
547
|
+
resolution.gate_dismissed = 'none (edges closed; gate still watching)';
|
|
548
|
+
resolution.gate = false;
|
|
550
549
|
}
|
|
551
|
-
|
|
550
|
+
else
|
|
551
|
+
try {
|
|
552
|
+
db.prepare(`INSERT INTO gate_dismissals (anchor_id, hit_term, rationale) VALUES (?, ?, ?)
|
|
553
|
+
ON CONFLICT(anchor_id, COALESCE(hit_term, '')) DO UPDATE SET rationale = excluded.rationale`).run(input.anchor_id, input.hit_term ? input.hit_term.toLowerCase() : null, input.rationale ?? null);
|
|
554
|
+
resolution.gate_dismissed = input.hit_term ? input.hit_term.toLowerCase() : 'all matches';
|
|
555
|
+
}
|
|
556
|
+
catch { /* pre-v16 DB → edges-only dismiss, as before */ }
|
|
552
557
|
}
|
|
553
558
|
// If action is 'fix', mark open edges as resolved
|
|
554
559
|
if (input.action === 'fix') {
|
package/dist/mcp/server.js
CHANGED
|
@@ -172,7 +172,7 @@ const TOOLS = [
|
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
name: 'recall',
|
|
175
|
-
description: 'Your persistent memory across all AI tools. CALL THIS BEFORE STARTING ANY TASK to check for past caveats (pain records), decisions, and learnings — prevents repeating mistakes across sessions.\n\nTypical usage: recall({ query: "keywords" }) for search, recall({ path: "file.ts" }) for file history, recall() for overview.\n\nWHEN TO CALL:\n• Before starting any new task or touching a file\n• When the user mentions "before" / "前に" / "last time" / "remember when"\n• When an error occurs — check if you\'ve seen it before\n• When making a decision — check for prior decisions on the same topic\n\nTHREE MODES (auto-detected):\n• Search (default): provide query → returns memories ranked by relevance + heat\n• File history: provide path → returns complete edit history with user-intent context\n•
|
|
175
|
+
description: 'Your persistent memory across all AI tools. CALL THIS BEFORE STARTING ANY TASK to check for past caveats (pain records), decisions, and learnings — prevents repeating mistakes across sessions.\n\nTypical usage: recall({ query: "keywords" }) for search, recall({ path: "file.ts" }) for file history, recall() for overview.\n\nWHEN TO CALL:\n• Before starting any new task or touching a file\n• When the user mentions "before" / "前に" / "last time" / "remember when"\n• When an error occurs — check if you\'ve seen it before\n• When making a decision — check for prior decisions on the same topic\n\nTHREE MODES (auto-detected):\n• Search (default): provide query → returns memories ranked by relevance + heat\n• File history: provide path → returns complete edit history with user-intent context\n• Session brief: omit all params → what needs attention (🔴/🟡), where you are on the Map, open loops (proposals / distill queue / friction), top entities. CALL THIS FIRST in a new session.\n• Locate: where: "<topic>" → your position on the Current Truth Map + blast radius (what else moves if you touch this). where: true → auto-locate from recent edits\n• Dream: dream: true → North Star + orphaned proposals to triage + distill_queue + friction (the full triage session; the brief carries only the counts)\n• Entity list: overview: true (the old no-arg behaviour)\n\nWorks across Claude, GPT, Cursor, Codex, Gemini — one local SQLite file, nothing leaves your machine.',
|
|
176
176
|
inputSchema: {
|
|
177
177
|
type: 'object',
|
|
178
178
|
properties: {
|
|
@@ -192,6 +192,10 @@ const TOOLS = [
|
|
|
192
192
|
max_intents: { type: 'number', description: 'For file mode: max user-intent snippets. Default 10.', default: 10 },
|
|
193
193
|
scope_to_roots: { type: 'boolean', default: false, description: 'For file mode: filter to client-provided roots.' },
|
|
194
194
|
explain: { type: 'boolean', default: false, description: 'Include ranking internals (composite, heat, band, momentum, match_reasons, score_breakdown). Off by default so more memories fit the token budget.' },
|
|
195
|
+
where: { description: 'Locate on the Current Truth Map. A topic string, or true to auto-locate from the files you edited recently. Returns your node, journey stage, blast radius, and the decision behind it.', anyOf: [{ type: 'string' }, { type: 'boolean' }] },
|
|
196
|
+
project: { type: 'string', description: 'With where: the Map project slug, when several maps are imported.' },
|
|
197
|
+
dream: { type: 'boolean', default: false, description: 'Return the full triage set: North Star, orphaned proposals (each with candidate_id), distill_queue, friction. Resolve proposals with resolve_drift({ candidate_id, action }); rewrite distill items with remember({ memory_id, content }).' },
|
|
198
|
+
overview: { type: 'boolean', default: false, description: 'Return the entity list instead of the session brief when no query is given.' },
|
|
195
199
|
kind: { type: 'string', enum: ['person', 'company', 'project', 'concept', 'file', 'other'], description: 'For overview mode: filter by entity kind.' },
|
|
196
200
|
min_memories: { type: 'number', description: 'For overview mode: minimum memory count. Default 1.', default: 1 },
|
|
197
201
|
},
|
|
@@ -219,6 +223,7 @@ const TOOLS = [
|
|
|
219
223
|
domain: { type: 'string', description: 'Filter by domain (strategy, product, engineering, growth, etc.)' },
|
|
220
224
|
decision_mode: { type: 'string', description: 'Filter by decision_mode (hypothesis, constraint, commitment, source_of_truth)' },
|
|
221
225
|
verbose: { type: 'boolean', default: false, description: 'Return full aligned nodes and all candidates. Default is compact: attention items in full, aligned as id+statement per domain, candidates as counts.' },
|
|
226
|
+
anchor_id: { type: 'number', description: 'Deep-dive into ONE decision instead of the map: its state, premises, drift edges, pending candidates. (Absorbs check_decision.)' },
|
|
222
227
|
},
|
|
223
228
|
},
|
|
224
229
|
},
|
|
@@ -252,7 +257,10 @@ const TOOLS = [
|
|
|
252
257
|
inputSchema: {
|
|
253
258
|
type: 'object',
|
|
254
259
|
properties: {
|
|
255
|
-
kind: { type: 'string', enum: ['prohibition', 'decision', 'constraint'], description:
|
|
260
|
+
kind: { type: 'string', enum: ['prohibition', 'decision', 'constraint', 'proposal'], description: "Anchor type. 'proposal' = an option you presented that the user never addressed (an orphaned fork) — it lands as a review item and can be triaged later via recall({ dream: true }). (Absorbs flag_proposals.)" },
|
|
261
|
+
decided: { type: 'string', description: "For kind 'proposal': what the user chose or engaged with instead." },
|
|
262
|
+
siblings: { type: 'array', items: { type: 'string' }, description: "For kind 'proposal': the other options from the same set, for context." },
|
|
263
|
+
session_context: { type: 'string', description: "For kind 'proposal': one line on the conversation it came from." },
|
|
256
264
|
statement: { type: 'string', description: 'The normative claim (>= 8 chars)' },
|
|
257
265
|
rationale: { type: 'string', description: 'Why this was decided' },
|
|
258
266
|
affects: { type: 'array', items: { type: 'string' }, description: 'Path globs that scope this anchor' },
|
|
@@ -272,12 +280,14 @@ const TOOLS = [
|
|
|
272
280
|
},
|
|
273
281
|
{
|
|
274
282
|
name: 'resolve_drift',
|
|
275
|
-
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 AND the gate stops firing on it (
|
|
283
|
+
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 AND the gate stops firing on it (hit_term: just that word; gate:false: close the edges but keep the gate watching)\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',
|
|
276
284
|
inputSchema: {
|
|
277
285
|
type: 'object',
|
|
278
286
|
properties: {
|
|
279
287
|
anchor_id: { type: 'number', description: 'The drift_anchor ID to resolve' },
|
|
280
|
-
action: { type: 'string', enum: ['fix', 'supersede', 'acknowledge', 'dismiss', 'harden', 'soften'], description:
|
|
288
|
+
action: { type: 'string', enum: ['fix', 'supersede', 'acknowledge', 'dismiss', 'harden', 'soften', 'surface'], description: "Resolution action. With candidate_id (an orphaned proposal): 'surface' keeps it visible for the human, 'dismiss' retires it." },
|
|
289
|
+
candidate_id: { type: 'number', description: 'Resolve an orphaned proposal (from recall({ dream: true })) instead of an anchor: pass its candidate_id with action surface | dismiss and a rationale that references the North Star. (Absorbs resolve_proposal.)' },
|
|
290
|
+
gate: { type: 'boolean', default: true, description: "For dismiss: false = these detections were false positives, close them, but KEEP the gate watching this anchor (its signals still fire). Use when the anchor's rule is right and only this evidence was wrong — e.g. a lexical hit in a file that is not what the rule is about. Default true also silences the gate (whole anchor, or just hit_term)." },
|
|
281
291
|
rationale: { type: 'string', description: 'Why this resolution (recorded for audit trail)' },
|
|
282
292
|
review_after: { type: 'string', description: 'For acknowledge: ISO date to re-check (e.g. "2026-07-04")' },
|
|
283
293
|
superseded_by: { type: 'number', description: 'For supersede: the new anchor ID that replaces this one' },
|
|
@@ -1383,7 +1393,78 @@ async function inferDefaultEntity() {
|
|
|
1383
1393
|
catch { /* fall through */ }
|
|
1384
1394
|
return { name: 'workspace', kind: 'project', from: 'fallback' };
|
|
1385
1395
|
}
|
|
1396
|
+
/**
|
|
1397
|
+
* The session brief: what an agent needs in the first call of a session, in one call.
|
|
1398
|
+
*
|
|
1399
|
+
* On 2026-09-05 this took four calls (recall / drift_status / dream / where_am_i), ~20k tokens,
|
|
1400
|
+
* and one of them failed. The brief carries the attention items in full and everything else as
|
|
1401
|
+
* counts plus a hint for the drill-down — small enough to call freely, complete enough that the
|
|
1402
|
+
* agent does not have to know which of five tools holds which fact.
|
|
1403
|
+
*/
|
|
1404
|
+
async function handleSessionBrief() {
|
|
1405
|
+
const view = getTruthView(db, {});
|
|
1406
|
+
const bs = view.counts.by_state;
|
|
1407
|
+
const triage = `${view.counts.nodes} anchors: ` + [
|
|
1408
|
+
bs.drift > 0 ? `🔴 ${bs.drift} drifting` : null,
|
|
1409
|
+
bs.review > 0 ? `🟡 ${bs.review} needs review` : null,
|
|
1410
|
+
bs.held > 0 ? `⚪ ${bs.held} held` : null,
|
|
1411
|
+
`🔵 ${bs.aligned} verified`,
|
|
1412
|
+
bs.unverified > 0 ? `⚫ ${bs.unverified} unverified` : null,
|
|
1413
|
+
].filter(Boolean).join(' · ');
|
|
1414
|
+
const attention = view.attention.slice(0, 8).map((n) => ({
|
|
1415
|
+
id: n.id, state: n.state, statement: n.statement.slice(0, 160), reality: (n.reality ?? '').slice(0, 160),
|
|
1416
|
+
}));
|
|
1417
|
+
let where = null;
|
|
1418
|
+
try {
|
|
1419
|
+
const w = JSON.parse(await handleWhereAmI({}));
|
|
1420
|
+
where = w.located
|
|
1421
|
+
? { project: w.project, you_are_here: w.you_are_here }
|
|
1422
|
+
: w.reason
|
|
1423
|
+
? { reason: w.reason, available_projects: w.available_projects }
|
|
1424
|
+
: null;
|
|
1425
|
+
}
|
|
1426
|
+
catch { /* no map, or roots unavailable — the brief still stands */ }
|
|
1427
|
+
let open_loops = null;
|
|
1428
|
+
try {
|
|
1429
|
+
const d = JSON.parse(handleDream({}));
|
|
1430
|
+
open_loops = {
|
|
1431
|
+
north_star: d.north_star ? { id: d.north_star.id, statement: String(d.north_star.statement).slice(0, 160) } : null,
|
|
1432
|
+
proposals: d.total ?? 0,
|
|
1433
|
+
proposals_top: (d.candidates ?? []).slice(0, 3).map((c) => ({ candidate_id: c.candidate_id ?? c.id, statement: String(c.statement ?? c.target_statement ?? '').slice(0, 120) })),
|
|
1434
|
+
distill_queue: d.distill_total ?? 0,
|
|
1435
|
+
friction: d.friction_total ?? 0,
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
catch { /* fine */ }
|
|
1439
|
+
let entities = [];
|
|
1440
|
+
try {
|
|
1441
|
+
entities = (JSON.parse(handleListEntities({ limit: 8 })).entities ?? []).map((e) => ({ name: e.name, kind: e.kind, memories: e.memory_count }));
|
|
1442
|
+
}
|
|
1443
|
+
catch { /* fine */ }
|
|
1444
|
+
return JSON.stringify({
|
|
1445
|
+
ok: true,
|
|
1446
|
+
brief: true,
|
|
1447
|
+
triage,
|
|
1448
|
+
attention,
|
|
1449
|
+
where,
|
|
1450
|
+
open_loops,
|
|
1451
|
+
entities,
|
|
1452
|
+
next: [
|
|
1453
|
+
'recall({ query }) to search; recall({ path }) for a file\'s history',
|
|
1454
|
+
'drift_status() for the full truth map; drift_status({ anchor_id }) for one decision',
|
|
1455
|
+
'recall({ dream: true }) to triage proposals and drain the distill queue',
|
|
1456
|
+
'remember({ content, anchor: {} }) to record a decision and enforce it',
|
|
1457
|
+
],
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1386
1460
|
async function handleRecallUnified(args) {
|
|
1461
|
+
// Folded surfaces (roadmap 5): locating yourself and the triage session are ways of
|
|
1462
|
+
// recalling, not separate tools.
|
|
1463
|
+
if (args?.dream)
|
|
1464
|
+
return handleDream({ domain: args.domain });
|
|
1465
|
+
if (args?.where !== undefined && args?.where !== false) {
|
|
1466
|
+
return handleWhereAmI({ query: args.where === true ? undefined : String(args.where), project: args.project, limit: args.limit });
|
|
1467
|
+
}
|
|
1387
1468
|
// File history mode (path takes priority; if query also provided, include it as context)
|
|
1388
1469
|
if (args.path) {
|
|
1389
1470
|
const fileResult = await handleRecallFileWithRoots({
|
|
@@ -1404,17 +1485,15 @@ async function handleRecallUnified(args) {
|
|
|
1404
1485
|
}
|
|
1405
1486
|
return fileResult;
|
|
1406
1487
|
}
|
|
1407
|
-
//
|
|
1488
|
+
// No search criteria at all → the session brief (or the entity list on request).
|
|
1408
1489
|
const hasQuery = args.query && String(args.query).trim().length > 0;
|
|
1409
1490
|
const hasFilters = args.entity_name || args.layer || args.altitude ||
|
|
1410
1491
|
args.mem_type || args.mem_state || args.thread_id || args.band;
|
|
1411
1492
|
if (!hasQuery && !hasFilters) {
|
|
1412
|
-
|
|
1413
|
-
kind: args.kind,
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
offset: args.offset,
|
|
1417
|
-
});
|
|
1493
|
+
if (args.overview || args.kind || args.min_memories !== undefined || args.offset) {
|
|
1494
|
+
return handleListEntities({ kind: args.kind, min_memories: args.min_memories, limit: args.limit, offset: args.offset });
|
|
1495
|
+
}
|
|
1496
|
+
return handleSessionBrief();
|
|
1418
1497
|
}
|
|
1419
1498
|
// Search mode (default)
|
|
1420
1499
|
return handleRecall(args);
|
|
@@ -1423,6 +1502,8 @@ async function handleRecallUnified(args) {
|
|
|
1423
1502
|
// Drift tool handlers (v0.8.0)
|
|
1424
1503
|
// ============================================================
|
|
1425
1504
|
function handleDriftStatus(args) {
|
|
1505
|
+
if (args?.anchor_id)
|
|
1506
|
+
return handleCheckDecision({ anchor_id: args.anchor_id });
|
|
1426
1507
|
const view = getTruthView(db, {
|
|
1427
1508
|
domain: args?.domain,
|
|
1428
1509
|
decision_mode: args?.decision_mode,
|
|
@@ -1575,6 +1656,26 @@ function handleDeclareAnchor(args) {
|
|
|
1575
1656
|
if (!args?.kind || !args?.statement) {
|
|
1576
1657
|
throw new Error('kind and statement are required');
|
|
1577
1658
|
}
|
|
1659
|
+
// An orphaned proposal is a fork the user never took. Same tool as any other declaration —
|
|
1660
|
+
// the agent should not need a separate verb for "I noticed something went unaddressed".
|
|
1661
|
+
if (args.kind === 'proposal') {
|
|
1662
|
+
const r = JSON.parse(handleFlagProposals({
|
|
1663
|
+
session_context: args.session_context,
|
|
1664
|
+
proposals: [{
|
|
1665
|
+
statement: args.statement, rationale: args.rationale, domain: args.domain ?? 'general',
|
|
1666
|
+
confidence: args.confidence, decided: args.decided, siblings: args.siblings,
|
|
1667
|
+
}],
|
|
1668
|
+
}));
|
|
1669
|
+
const anchorId = r.proposals?.[0]?.anchor_id;
|
|
1670
|
+
if (!r.ok || !anchorId) {
|
|
1671
|
+
return JSON.stringify({ ok: false, error: r.error ?? 'proposal was not recorded (statement must be >= 10 chars)' });
|
|
1672
|
+
}
|
|
1673
|
+
const cand = db.prepare(`SELECT id FROM memory_write_candidates WHERE target_node_id = ? AND scope = 'orphaned_proposal' ORDER BY id DESC LIMIT 1`).get(anchorId);
|
|
1674
|
+
return JSON.stringify({
|
|
1675
|
+
ok: true, anchor_id: anchorId, candidate_id: cand?.id ?? null, kind: 'proposal', statement: args.statement,
|
|
1676
|
+
message: `Proposal recorded as review item #${anchorId}. Triage later: recall({ dream: true }) → resolve_drift({ candidate_id: ${cand?.id ?? '<id>'}, action: 'surface' | 'dismiss', rationale }).`,
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1578
1679
|
// Create the base anchor
|
|
1579
1680
|
const anchor = declareAnchor(db, {
|
|
1580
1681
|
kind: args.kind,
|
|
@@ -1613,6 +1714,21 @@ function handleDeclareAnchor(args) {
|
|
|
1613
1714
|
});
|
|
1614
1715
|
}
|
|
1615
1716
|
function handleResolveDrift(args) {
|
|
1717
|
+
// A proposal verdict is a resolution like any other; it just targets a candidate row.
|
|
1718
|
+
if (args?.candidate_id) {
|
|
1719
|
+
if (!['surface', 'dismiss'].includes(args.action)) {
|
|
1720
|
+
return JSON.stringify({ ok: false, error: `candidate_id takes action 'surface' or 'dismiss' (got '${args.action}')` });
|
|
1721
|
+
}
|
|
1722
|
+
if (!args.rationale) {
|
|
1723
|
+
return JSON.stringify({ ok: false, error: 'rationale is required for a proposal verdict — say what in the North Star decided it' });
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
return handleResolveProposal({ candidate_id: args.candidate_id, verdict: args.action, rationale: args.rationale });
|
|
1727
|
+
}
|
|
1728
|
+
catch (e) {
|
|
1729
|
+
return JSON.stringify({ ok: false, error: String(e?.message ?? e) });
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1616
1732
|
if (!args?.anchor_id || !args?.action) {
|
|
1617
1733
|
throw new Error('anchor_id and action are required');
|
|
1618
1734
|
}
|
|
@@ -1636,6 +1752,7 @@ function handleResolveDrift(args) {
|
|
|
1636
1752
|
review_after: args.review_after,
|
|
1637
1753
|
superseded_by: args.superseded_by,
|
|
1638
1754
|
hit_term: args.hit_term,
|
|
1755
|
+
gate: args.gate,
|
|
1639
1756
|
});
|
|
1640
1757
|
return JSON.stringify(result);
|
|
1641
1758
|
}
|
|
@@ -1946,7 +2063,27 @@ function handleResolveProposal(args) {
|
|
|
1946
2063
|
// ============================================================
|
|
1947
2064
|
// MCP wiring
|
|
1948
2065
|
// ============================================================
|
|
1949
|
-
|
|
2066
|
+
// Six tools on the surface (2026-09-07, roadmap 5). Anchor #1's reason for "3 tools, never a
|
|
2067
|
+
// 4th" was real — eight tools bled model-dependent behaviour across Claude/GPT/Cursor/Codex/
|
|
2068
|
+
// Gemini — and the surface had crept to eleven. The five below are folded into the six
|
|
2069
|
+
// (see LEGACY_TOOLS) and hidden from tools/list, but a call to any of them still works: an
|
|
2070
|
+
// agent or a SKILL.md written against 0.14 does not break. LINKSEE_LEGACY_TOOLS=1 lists them.
|
|
2071
|
+
const LEGACY_TOOLS = {
|
|
2072
|
+
where_am_i: 'recall({ where: "<topic>" }) — or recall() with no arguments for the session brief',
|
|
2073
|
+
check_decision: 'drift_status({ anchor_id })',
|
|
2074
|
+
flag_proposals: "declare_anchor({ kind: 'proposal', statement, rationale, domain, decided?, siblings? })",
|
|
2075
|
+
dream: 'recall({ dream: true }) — recall() with no arguments already includes the counts',
|
|
2076
|
+
resolve_proposal: "resolve_drift({ candidate_id, action: 'surface' | 'dismiss', rationale })",
|
|
2077
|
+
};
|
|
2078
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2079
|
+
const showLegacy = process.env.LINKSEE_LEGACY_TOOLS === '1';
|
|
2080
|
+
const tools = TOOLS
|
|
2081
|
+
.filter((t) => showLegacy || !(t.name in LEGACY_TOOLS))
|
|
2082
|
+
.map((t) => (t.name in LEGACY_TOOLS
|
|
2083
|
+
? { ...t, description: `(legacy — folded into ${LEGACY_TOOLS[t.name]}. Still works.)\n\n${t.description}` }
|
|
2084
|
+
: t));
|
|
2085
|
+
return { tools };
|
|
2086
|
+
});
|
|
1950
2087
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1951
2088
|
const { name, arguments: args } = req.params;
|
|
1952
2089
|
try {
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -175,6 +175,11 @@ open → decided → in_progress → done
|
|
|
175
175
|
|
|
176
176
|
### ① Task Start — Always recall before starting work
|
|
177
177
|
|
|
178
|
+
**First call of a session: `recall()` with no arguments.** It returns the session brief — what
|
|
179
|
+
needs attention (🔴/🟡 anchors), where you are on the Map, open loops (proposals to triage,
|
|
180
|
+
raw memories to distill, friction at the gate), and the top entities. One call, small, complete.
|
|
181
|
+
Then search with `recall({ query })` for the task at hand.
|
|
182
|
+
|
|
178
183
|
Before starting any new task, inject past context.
|
|
179
184
|
|
|
180
185
|
**At the very beginning of a conversation**, use `list_entities` first to understand what you know:
|
|
@@ -534,20 +539,18 @@ User: "That's it for today"
|
|
|
534
539
|
|
|
535
540
|
```
|
|
536
541
|
1. Review the session: which proposals did you make that the user never addressed?
|
|
537
|
-
2.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
},
|
|
548
|
-
...
|
|
549
|
-
]
|
|
542
|
+
2. For each one:
|
|
543
|
+
declare_anchor({
|
|
544
|
+
kind: "proposal",
|
|
545
|
+
statement: "[未解決] LinkedIn B2B: SaaS企業のCTO/VPE向けDMアウトリーチ",
|
|
546
|
+
rationale: "3つのGTMチャネルを提示したがX/Twitterのみ採用。LinkedIn経由の検討が未着手",
|
|
547
|
+
domain: "growth",
|
|
548
|
+
confidence: 0.5,
|
|
549
|
+
decided: "X/Twitter data-driven growth",
|
|
550
|
+
siblings: ["X/Twitter", "LinkedIn B2B", "Dev Community"],
|
|
551
|
+
session_context: "GTM channel strategy discussion"
|
|
550
552
|
})
|
|
553
|
+
→ { anchor_id, candidate_id }
|
|
551
554
|
3. Report: "Flagged N unresolved proposals for dashboard review."
|
|
552
555
|
```
|
|
553
556
|
|
|
@@ -555,27 +558,27 @@ Each proposal becomes a review-state anchor on the Linksee Dashboard — visible
|
|
|
555
558
|
|
|
556
559
|
### Case F3 — Dream: triage orphaned proposals against the North Star
|
|
557
560
|
|
|
558
|
-
**Not all orphaned proposals are worth surfacing.** Many are outdated, already implicitly resolved, or irrelevant to the current direction.
|
|
561
|
+
**Not all orphaned proposals are worth surfacing.** Many are outdated, already implicitly resolved, or irrelevant to the current direction. `recall({ dream: true })` returns the project's **North Star** (direction/goals/ICP/phase) alongside accumulated proposals so you can evaluate each one.
|
|
559
562
|
|
|
560
563
|
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
564
|
|
|
562
|
-
**When to dream:**
|
|
565
|
+
**When to run the triage (`recall({ dream: true })`):**
|
|
563
566
|
- At session start, if there are accumulated proposals
|
|
564
567
|
- When the user asks "何か見落としてない?" or "what should we revisit?"
|
|
565
568
|
- Periodically (weekly) to prevent proposal backlog from growing stale
|
|
566
569
|
|
|
567
570
|
```
|
|
568
|
-
1. dream
|
|
569
|
-
→ Returns: north_star + candidates[]
|
|
571
|
+
1. recall({ dream: true })
|
|
572
|
+
→ Returns: north_star + candidates[] (each with candidate_id)
|
|
570
573
|
|
|
571
574
|
2. For each candidate, evaluate against North Star:
|
|
572
575
|
- Does this affect the current phase/goals? → surface
|
|
573
576
|
- Is this for a different ICP or future phase? → dismiss
|
|
574
577
|
- Already implicitly resolved by later decisions? → dismiss
|
|
575
578
|
|
|
576
|
-
3.
|
|
579
|
+
3. resolve_drift({
|
|
577
580
|
candidate_id: <id>,
|
|
578
|
-
|
|
581
|
+
action: "surface" | "dismiss",
|
|
579
582
|
rationale: "North Star says ICP = solo devs; this is enterprise-only → dismiss"
|
|
580
583
|
})
|
|
581
584
|
```
|
|
@@ -596,14 +599,14 @@ Candidate C: "kintone enterprise integration"
|
|
|
596
599
|
|
|
597
600
|
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
601
|
|
|
599
|
-
### Case F4 — Distill: rewrite raw auto-captured memories (every dream call)
|
|
602
|
+
### Case F4 — Distill: rewrite raw auto-captured memories (every `recall({ dream: true })` call)
|
|
600
603
|
|
|
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.**
|
|
604
|
+
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). `recall({ dream: true })` returns them as `distill_queue`. **You are the distiller.**
|
|
602
605
|
|
|
603
|
-
**When:** every `dream
|
|
606
|
+
**When:** every `recall({ dream: true })` call — drain up to 8 items while triaging proposals. The SessionStart boot digest reminds you while the queue is non-empty.
|
|
604
607
|
|
|
605
608
|
```
|
|
606
|
-
1. dream
|
|
609
|
+
1. recall({ dream: true }) → distill_queue: [{memory_id, layer, raw_what, context_hint, affects, created}]
|
|
607
610
|
|
|
608
611
|
2. For each item, rewrite into ONE clean record:
|
|
609
612
|
- what = the actual decision/warning in one line — RESOLVE references
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linksee-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
5
5
|
"description": "Local-first agent memory MCP — cross-agent brain with drift detection, 6-layer structured memory + token-saving file diff cache",
|
|
6
6
|
"type": "module",
|