linksee-memory 0.1.0 → 0.1.2

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 CHANGED
@@ -296,6 +296,22 @@ Caveat and active-goal layers are always preserved. Consider scheduling a weekly
296
296
 
297
297
  ## Changelog
298
298
 
299
+ ### v0.1.1 — Pin threshold tweak (2026-04-19)
300
+
301
+ Based on real-world feedback that `importance=0.95` memories were not
302
+ being treated as pinned despite intent.
303
+
304
+ - **Pin threshold lowered from `>= 1.0` to `>= 0.9`.** Memories with
305
+ `importance >= 0.9` are now exempt from the auto-forget sweep and
306
+ surface `pinned: true` in `recall` and `remember` responses. This
307
+ matches the natural mental model ("0.9 = high importance = should
308
+ survive cleanup") without requiring exact `1.0`.
309
+ - All existing memories with `importance >= 0.9` (including older ones
310
+ set to `0.9` or `0.95`) become pinned automatically — no migration
311
+ needed.
312
+ - Updated tool descriptions and error messages to reflect the new
313
+ threshold.
314
+
299
315
  ### v0.1.0 — Major UX update (2026-04-18)
300
316
 
301
317
  Based on one week of dogfooding, here's what changed:
@@ -43,6 +43,20 @@ export function runMigrations(db) {
43
43
  if (currentVersion > 0 && currentVersion < 4) {
44
44
  db.exec(`INSERT INTO memories_fts(rowid, content) SELECT id, content FROM memories;`);
45
45
  }
46
+ // v0.1.1 data migration: pin threshold lowered from 1.0 to 0.9.
47
+ //
48
+ // Before v0.1.1, `remember()` only set `protected = 1` for memories
49
+ // inserted with importance >= 1.0. After v0.1.1, the threshold is 0.9.
50
+ // Existing rows written under the old rule need to be reconciled so that
51
+ // `recall().pinned`, `list_entities.pinned_count`, and the auto-forget
52
+ // guard (`WHERE protected = 0 AND importance < 0.9`) all agree.
53
+ //
54
+ // This runs on every startup but is a no-op after the first successful
55
+ // run (UPDATE filter excludes already-protected rows).
56
+ const hasMemories = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'memories'").get();
57
+ if (hasMemories?.name) {
58
+ db.prepare(`UPDATE memories SET protected = 1 WHERE importance >= 0.9 AND protected = 0`).run();
59
+ }
46
60
  }
47
61
  // CLI entrypoint
48
62
  if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('migrate.ts') || process.argv[1]?.endsWith('migrate.js')) {
@@ -12,7 +12,7 @@ import { refreshMomentumForEntity } from '../lib/momentum.js';
12
12
  import { consolidate as runConsolidate } from '../lib/consolidate.js';
13
13
  import { isPastedExternalContent } from '../lib/session-parser.js';
14
14
  import { handleReadSmart as handleReadSmartImpl } from './read-smart.js';
15
- const SERVER_VERSION = '0.1.0';
15
+ const SERVER_VERSION = '0.1.1';
16
16
  const db = openDb();
17
17
  runMigrations(db);
18
18
  const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, { capabilities: { tools: {} } });
@@ -57,7 +57,7 @@ const TOOLS = [
57
57
  entity_key: { type: 'string', description: 'Optional canonical key (email, domain, file path)' },
58
58
  layer: { type: 'string', description: 'One of: goal / context / emotion / implementation / caveat / learning. Common aliases (why, decisions, warnings, how, ...) are accepted.' },
59
59
  content: { type: 'string', description: 'The memory content (plain text or JSON)' },
60
- importance: { type: 'number', minimum: 0, maximum: 1, description: '0.0-1.0. Use 1.0 to "pin" a memory (protects from forgetting even outside caveat layer).' },
60
+ importance: { type: 'number', minimum: 0, maximum: 1, description: '0.0-1.0. Set to 0.9 or higher to "pin" a memory (protects from forgetting even outside caveat layer).' },
61
61
  force: { type: 'boolean', default: false, description: 'Bypass the paste-back/CI-log quality check. Only set when you are sure the content is original user or agent thought.' },
62
62
  },
63
63
  required: ['entity_name', 'entity_kind', 'layer', 'content'],
@@ -93,7 +93,7 @@ const TOOLS = [
93
93
  memory_id: { type: 'number', description: 'The memory.id to update' },
94
94
  content: { type: 'string', description: 'New content (plain text or JSON). If omitted, content is kept.' },
95
95
  layer: { type: 'string', description: 'Move to a different layer (aliases accepted). If omitted, layer is kept.' },
96
- importance: { type: 'number', minimum: 0, maximum: 1, description: 'New importance 0-1. Set to 1.0 to pin.' },
96
+ importance: { type: 'number', minimum: 0, maximum: 1, description: 'New importance 0-1. Set to 0.9 or higher to pin.' },
97
97
  },
98
98
  required: ['memory_id'],
99
99
  },
@@ -113,7 +113,7 @@ const TOOLS = [
113
113
  },
114
114
  {
115
115
  name: 'forget',
116
- description: 'Explicitly delete a memory by id, OR run auto-forgetting across all memories based on forgettingRisk (importance + heat + age). Caveat-layer, goal-layer, and importance=1.0 (pinned) memories are always preserved. Prefer update_memory for corrections — forget is destructive.',
116
+ description: 'Explicitly delete a memory by id, OR run auto-forgetting across all memories based on forgettingRisk (importance + heat + age). Caveat-layer, goal-layer, and pinned (importance>=0.9) memories are always preserved. Prefer update_memory for corrections — forget is destructive.',
117
117
  inputSchema: {
118
118
  type: 'object',
119
119
  properties: {
@@ -205,7 +205,7 @@ function handleRemember(args) {
205
205
  const importance = Math.min(1, Math.max(0, Number(args.importance ?? 0.5)));
206
206
  const result = db
207
207
  .prepare('INSERT INTO memories (entity_id, layer, content, importance, protected) VALUES (?, ?, ?, ?, ?)')
208
- .run(entityId, layer, rawContent, importance, importance >= 1.0 ? 1 : 0);
208
+ .run(entityId, layer, rawContent, importance, importance >= 0.9 ? 1 : 0);
209
209
  db.prepare('INSERT INTO events (entity_id, kind, payload) VALUES (?, ?, ?)').run(entityId, 'memory_stored', JSON.stringify({ layer, memory_id: result.lastInsertRowid }));
210
210
  const mom = refreshMomentumForEntity(db, entityId);
211
211
  return JSON.stringify({
@@ -213,7 +213,7 @@ function handleRemember(args) {
213
213
  memory_id: Number(result.lastInsertRowid),
214
214
  entity_id: entityId,
215
215
  layer,
216
- pinned: importance >= 1.0,
216
+ pinned: importance >= 0.9,
217
217
  momentum: { score: mom.score, band: mom.band },
218
218
  });
219
219
  }
@@ -372,7 +372,7 @@ function handleRecall(args) {
372
372
  const heatNorm = heat.score / 100;
373
373
  const momNorm = Math.min(1, (r.momentum_score ?? 0) / 10);
374
374
  const importanceBoost = r.importance; // 0-1
375
- // Composite: give a bit to importance so pinned (1.0) memories always rank high
375
+ // Composite: give a bit to importance so pinned (>=0.9) memories always rank high
376
376
  const w_rel = 0.45, w_heat = 0.25, w_mom = 0.15, w_imp = 0.15;
377
377
  const composite = w_rel * relevance + w_heat * heatNorm + w_mom * momNorm + w_imp * importanceBoost;
378
378
  // match_reasons: human-readable WHY this row is here
@@ -393,7 +393,7 @@ function handleRecall(args) {
393
393
  reasons.push('heat:warm');
394
394
  if (r.momentum_score >= 5)
395
395
  reasons.push('entity_active');
396
- if (r.importance >= 1.0)
396
+ if (r.importance >= 0.9)
397
397
  reasons.push('pinned');
398
398
  else if (r.importance >= 0.8)
399
399
  reasons.push('high_importance');
@@ -461,7 +461,7 @@ function handleRecall(args) {
461
461
  content: parsedContent,
462
462
  content_raw: r.content,
463
463
  importance: r.importance,
464
- pinned: r.importance >= 1.0,
464
+ pinned: r.importance >= 0.9,
465
465
  heat: Number(r.heat_score.toFixed(1)),
466
466
  band: r.heat_band,
467
467
  composite: Number(r.composite_score.toFixed(3)),
@@ -473,27 +473,27 @@ function handleRecall(args) {
473
473
  }
474
474
  function handleForget(args) {
475
475
  if (args.memory_id) {
476
- // Explicit delete by id — respect protected AND pinned (importance >= 1.0)
476
+ // Explicit delete by id — respect protected AND pinned (importance >= 0.9)
477
477
  const target = db.prepare('SELECT id, layer, importance, protected FROM memories WHERE id = ?').get(args.memory_id);
478
478
  if (!target) {
479
479
  return JSON.stringify({ ok: false, error: `memory_id ${args.memory_id} not found` });
480
480
  }
481
- if (target.protected === 1 || target.importance >= 1.0) {
481
+ if (target.protected === 1 || target.importance >= 0.9) {
482
482
  return JSON.stringify({
483
483
  ok: false,
484
484
  preserved: true,
485
- reason: target.protected === 1 ? `${target.layer}-layer is auto-protected` : 'pinned (importance=1.0)',
486
- hint: 'Use update_memory to lower importance below 1.0 first, then forget.',
485
+ reason: target.protected === 1 ? `${target.layer}-layer is auto-protected` : 'pinned (importance>=0.9)',
486
+ hint: 'Use update_memory to lower importance below 0.9 first, then forget.',
487
487
  });
488
488
  }
489
489
  const res = db.prepare('DELETE FROM memories WHERE id = ?').run(args.memory_id);
490
490
  return JSON.stringify({ ok: true, deleted: res.changes, memory_id: args.memory_id });
491
491
  }
492
- // Auto-sweep — also respect importance=1.0 as protection
492
+ // Auto-sweep — also respect pin (importance >= 0.9) as protection
493
493
  const rows = db
494
494
  .prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected
495
495
  FROM memories
496
- WHERE protected = 0 AND importance < 1.0`)
496
+ WHERE protected = 0 AND importance < 0.9`)
497
497
  .all();
498
498
  const now = Math.floor(Date.now() / 1000);
499
499
  const actions = [];
@@ -545,7 +545,7 @@ function handleConsolidate(args) {
545
545
  FROM memories m
546
546
  JOIN entities e ON e.id = m.entity_id
547
547
  WHERE m.protected = 0
548
- AND m.importance < 1.0
548
+ AND m.importance < 0.9
549
549
  AND m.layer IN ('context', 'emotion', 'implementation')
550
550
  AND m.created_at <= ?
551
551
  GROUP BY m.entity_id, m.layer
@@ -601,7 +601,7 @@ function handleUpdateMemory(args) {
601
601
  if (args.importance !== undefined) {
602
602
  const imp = Math.min(1, Math.max(0, Number(args.importance)));
603
603
  patch.importance = imp;
604
- patch.protected = imp >= 1.0 || existing.protected === 1 ? 1 : 0;
604
+ patch.protected = imp >= 0.9 || existing.protected === 1 ? 1 : 0;
605
605
  }
606
606
  const keys = Object.keys(patch);
607
607
  if (keys.length === 0) {
@@ -616,7 +616,7 @@ function handleUpdateMemory(args) {
616
616
  ok: true,
617
617
  memory_id: memoryId,
618
618
  updated_fields: keys,
619
- pinned: (patch.importance ?? existing.importance) >= 1.0,
619
+ pinned: (patch.importance ?? existing.importance) >= 0.9,
620
620
  });
621
621
  }
622
622
  function handleListEntities(args) {
@@ -633,7 +633,7 @@ function handleListEntities(args) {
633
633
  SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
634
634
  SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
635
635
  SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
636
- SUM(CASE WHEN m.importance >= 1.0 THEN 1 ELSE 0 END) as pinned_count
636
+ SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
637
637
  FROM entities e
638
638
  LEFT JOIN memories m ON m.entity_id = e.id
639
639
  WHERE 1=1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linksee-memory",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "mcpName": "io.github.michielinksee/linksee-memory",
5
5
  "description": "Local-first agent memory MCP — cross-agent brain with 6-layer structured memory + token-saving file diff cache",
6
6
  "type": "module",