memorix 1.2.10 → 1.3.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +28 -2
  3. package/README.zh-CN.md +28 -2
  4. package/assets/star-history-dark.svg +30 -0
  5. package/assets/star-history-light.svg +30 -0
  6. package/assets/star-history.json +599 -0
  7. package/dist/cli/index.js +1783 -557
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/index.js +1292 -267
  10. package/dist/index.js.map +1 -1
  11. package/dist/maintenance-runner.js +97 -21
  12. package/dist/maintenance-runner.js.map +1 -1
  13. package/dist/memcode-runtime/CHANGELOG.md +15 -0
  14. package/dist/sdk.d.ts +1 -1
  15. package/dist/sdk.js +1294 -269
  16. package/dist/sdk.js.map +1 -1
  17. package/dist/types.d.ts +99 -1
  18. package/dist/types.js.map +1 -1
  19. package/dist/vector-backfill-runner.js +97 -21
  20. package/dist/vector-backfill-runner.js.map +1 -1
  21. package/docs/1.3-MEMORY-ARCHITECTURE.md +236 -0
  22. package/docs/AGENT_OPERATOR_PLAYBOOK.md +3 -1
  23. package/docs/API_REFERENCE.md +20 -1
  24. package/docs/ARCHITECTURE.md +34 -1
  25. package/docs/DEVELOPMENT.md +3 -2
  26. package/docs/MODULES.md +27 -1
  27. package/docs/README.md +3 -3
  28. package/docs/dev-log/progress.txt +22 -0
  29. package/llms-full.txt +5 -1
  30. package/llms.txt +2 -0
  31. package/package.json +2 -1
  32. package/plugins/antigravity/memorix/rules/memorix.md +2 -0
  33. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  34. package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +3 -0
  35. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  36. package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +3 -0
  37. package/plugins/copilot/memorix/plugin.json +1 -1
  38. package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +3 -0
  39. package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +3 -0
  40. package/plugins/gemini/memorix/GEMINI.md +2 -0
  41. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  42. package/plugins/omp/memorix/package.json +1 -1
  43. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  44. package/plugins/pi/memorix/package.json +1 -1
  45. package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +3 -0
  46. package/server.json +2 -2
  47. package/src/cli/command-guide.ts +9 -1
  48. package/src/cli/commands/codegraph.ts +1 -0
  49. package/src/cli/commands/memory.ts +217 -1
  50. package/src/codegraph/auto-context.ts +11 -0
  51. package/src/codegraph/context-pack.ts +3 -0
  52. package/src/embedding/api-provider.ts +23 -9
  53. package/src/embedding/provider.ts +21 -10
  54. package/src/hooks/installers/index.ts +2 -0
  55. package/src/knowledge/context-assembly.ts +2 -1
  56. package/src/knowledge/workset.ts +77 -4
  57. package/src/memory/long-term-store.ts +242 -0
  58. package/src/memory/long-term-types.ts +136 -0
  59. package/src/memory/long-term.ts +626 -0
  60. package/src/memory/owner.ts +75 -0
  61. package/src/server.ts +112 -30
  62. package/src/store/sqlite-db.ts +74 -0
  63. package/src/types.ts +20 -0
@@ -18,6 +18,7 @@ Use Memorix as the shared memory layer for the active workspace when Memorix too
18
18
  | Need the full source for a search hit | `memorix_detail` | `memorix memory detail --id <id>` |
19
19
  | Need the sequence around one memory | `memorix_timeline` | `memorix memory timeline --id <id>` |
20
20
  | Learned reusable project knowledge | `memorix_store` | `memorix memory store --type <type> --entity <name> --title "<title>" "<text>"` |
21
+ | Stable source-backed fact or procedure needing long-term review | `memorix_store` with `longTerm` | `memorix memory long-term qualify|approve --id <id> --reason "..."` |
21
22
  | Task or bug is complete/outdated | `memorix_resolve` | `memorix memory resolve --ids <ids>` |
22
23
 
23
24
  ## Search Rules
@@ -42,4 +43,6 @@ Use Memorix as the shared memory layer for the active workspace when Memorix too
42
43
  | Accepted compromise | `trade-off` |
43
44
 
44
45
  - Use concise titles, stable entity names, relevant `filesModified`, and `topicKey` for evolving topics.
46
+ - Use `longTerm` only for a stable fact, reusable procedure, or completed episode that merits explicit review. It creates a candidate, not live context; keep the default project scope and do not use it for routine updates.
47
+ - A `user` + `portable` durable memory in a task brief is intentionally reusable across projects. When it matches the task, use it as background even if it originated elsewhere; it is not a current-project fact. Expand it only when needed with `memorix_detail` and `typedRefs: ["durable:<id>"]`, stating the missing fact as `purpose`.
45
48
  - Do not store secrets, credentials, raw private transcripts, trivial commands, or routine file reads.
@@ -18,6 +18,7 @@ Use Memorix as the shared memory layer for the active workspace when Memorix too
18
18
  | Need the full source for a search hit | `memorix_detail` | `memorix memory detail --id <id>` |
19
19
  | Need the sequence around one memory | `memorix_timeline` | `memorix memory timeline --id <id>` |
20
20
  | Learned reusable project knowledge | `memorix_store` | `memorix memory store --type <type> --entity <name> --title "<title>" "<text>"` |
21
+ | Stable source-backed fact or procedure needing long-term review | `memorix_store` with `longTerm` | `memorix memory long-term qualify|approve --id <id> --reason "..."` |
21
22
  | Task or bug is complete/outdated | `memorix_resolve` | `memorix memory resolve --ids <ids>` |
22
23
 
23
24
  ## Search Rules
@@ -42,4 +43,6 @@ Use Memorix as the shared memory layer for the active workspace when Memorix too
42
43
  | Accepted compromise | `trade-off` |
43
44
 
44
45
  - Use concise titles, stable entity names, relevant `filesModified`, and `topicKey` for evolving topics.
46
+ - Use `longTerm` only for a stable fact, reusable procedure, or completed episode that merits explicit review. It creates a candidate, not live context; keep the default project scope and do not use it for routine updates.
47
+ - A `user` + `portable` durable memory in a task brief is intentionally reusable across projects. When it matches the task, use it as background even if it originated elsewhere; it is not a current-project fact. Expand it only when needed with `memorix_detail` and `typedRefs: ["durable:<id>"]`, stating the missing fact as `purpose`.
45
48
  - Do not store secrets, credentials, raw private transcripts, trivial commands, or routine file reads.
@@ -11,6 +11,8 @@ Use Memorix when prior workspace context, decisions, fixes, or handoff state wou
11
11
  - Use `memorix_search` for focused lookup.
12
12
  - Use `memorix_detail` before relying on a specific memory.
13
13
  - Use `memorix_store` for durable workspace knowledge.
14
+ - Use `longTerm` on `memorix_store` only for a stable fact, reusable procedure, or completed episode that merits explicit review. It creates a candidate, not live context; keep the default project scope and do not use it for routine updates.
15
+ - A `user` + `portable` durable memory in a task brief is intentionally reusable across projects. When it matches the task, use it as background even if it originated elsewhere; it is not a current-project fact. Expand it only when needed with `memorix_detail` and `typedRefs: ["durable:<id>"]`, stating the missing fact as `purpose`.
14
16
  - Use `memorix_store_reasoning` for the reason behind a technical decision.
15
17
  - Use `memorix_resolve` when completed or outdated memories should stop surfacing.
16
18
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.10",
3
+ "version": "1.3.0",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.2.10",
3
+ "version": "1.3.0",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.10",
3
+ "version": "1.3.0",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.2.10",
3
+ "version": "1.3.0",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -18,6 +18,7 @@ Use Memorix as the shared memory layer for the active workspace when Memorix too
18
18
  | Need the full source for a search hit | `memorix_detail` | `memorix memory detail --id <id>` |
19
19
  | Need the sequence around one memory | `memorix_timeline` | `memorix memory timeline --id <id>` |
20
20
  | Learned reusable project knowledge | `memorix_store` | `memorix memory store --type <type> --entity <name> --title "<title>" "<text>"` |
21
+ | Stable source-backed fact or procedure needing long-term review | `memorix_store` with `longTerm` | `memorix memory long-term qualify|approve --id <id> --reason "..."` |
21
22
  | Task or bug is complete/outdated | `memorix_resolve` | `memorix memory resolve --ids <ids>` |
22
23
 
23
24
  ## Search Rules
@@ -42,4 +43,6 @@ Use Memorix as the shared memory layer for the active workspace when Memorix too
42
43
  | Accepted compromise | `trade-off` |
43
44
 
44
45
  - Use concise titles, stable entity names, relevant `filesModified`, and `topicKey` for evolving topics.
46
+ - Use `longTerm` only for a stable fact, reusable procedure, or completed episode that merits explicit review. It creates a candidate, not live context; keep the default project scope and do not use it for routine updates.
47
+ - A `user` + `portable` durable memory in a task brief is intentionally reusable across projects. When it matches the task, use it as background even if it originated elsewhere; it is not a current-project fact. Expand it only when needed with `memorix_detail` and `typedRefs: ["durable:<id>"]`, stating the missing fact as `purpose`.
45
48
  - Do not store secrets, credentials, raw private transcripts, trivial commands, or routine file reads.
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/AVIDS2/memorix",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.2.10",
10
+ "version": "1.3.0",
11
11
  "websiteUrl": "https://github.com/AVIDS2/memorix#readme",
12
12
  "icons": [
13
13
  {
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "memorix",
25
- "version": "1.2.10",
25
+ "version": "1.3.0",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -15,8 +15,16 @@ const GUIDES: Record<string, CliCommandGuide> = {
15
15
  'memorix memory resolve --ids 42,43 [--status resolved|archived]',
16
16
  'memorix memory consolidate --action preview|execute',
17
17
  'memorix memory promote --ids 42,43 [--trigger "..."] [--instruction "..."]',
18
+ 'memorix memory long-term list [--all]',
19
+ 'memorix memory long-term add --kind semantic --scope user --portability portable --title "..." --text "..." [--tags "..."] [--applicability "..."]',
20
+ 'memorix memory long-term promote --from-observation 42 --kind procedural --scope project',
21
+ 'memorix memory long-term qualify|approve|archive --id <id> --reason "..."',
22
+ 'memorix memory long-term supersede --id <old-id> --superseded-by <qualified-id> --reason "..."',
23
+ ],
24
+ notes: [
25
+ 'Personal and team visibility require `memorix identity join` or `memorix identity use` first.',
26
+ 'Candidates never enter a task Workset. Only manual or user-confirmed `user + portable` memory may cross local projects.',
18
27
  ],
19
- notes: ['Personal and team visibility require `memorix identity join` or `memorix identity use` first.'],
20
28
  },
21
29
  identity: {
22
30
  summary: 'Select the active local actor for private memory and coordination commands.',
@@ -217,6 +217,7 @@ export default defineCommand({
217
217
  ...(external.caution
218
218
  ? { runtimeCautions: [{ kind: 'external-codegraph-fallback' as const, message: external.caution }] }
219
219
  : {}),
220
+ reader,
220
221
  });
221
222
  emitResult({ project, pack, providerQuality: external.quality }, buildContextPackPrompt(pack), asJson);
222
223
  return;
@@ -51,17 +51,30 @@ export default defineCommand({
51
51
  instruction: { type: 'string', description: 'Custom instruction for promoted mini-skills' },
52
52
  tags: { type: 'string', description: 'Comma-separated extra tags for promoted mini-skills' },
53
53
  skillId: { type: 'string', description: 'Mini-skill ID for list/delete actions' },
54
+ kind: { type: 'string', description: 'Long-term memory kind: episodic, semantic, or procedural' },
55
+ scope: { type: 'string', description: 'Long-term memory scope: project, user, or team' },
56
+ portability: { type: 'string', description: 'Long-term memory portability: project-bound or portable' },
57
+ applicability: { type: 'string', description: 'When a long-term memory applies' },
58
+ fromObservation: { type: 'string', description: 'Observation ID to promote into long-term memory' },
59
+ supersededBy: { type: 'string', description: 'Qualified or approved long-term memory that replaces the current record' },
60
+ 'superseded-by': { type: 'string', description: 'Kebab-case alias for --supersededBy' },
61
+ reason: { type: 'string', description: 'Evidence review reason for qualify, approve, or archive' },
62
+ all: { type: 'boolean', description: 'Include archived and superseded long-term memories in list output' },
54
63
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
55
64
  },
56
65
  run: async ({ args }) => {
57
66
  const action = (args._ as string[])?.[0] || '';
58
67
  const positional = ((args._ as string[]) ?? []).slice(1);
68
+ const longTermAction = action === 'long-term'
69
+ ? (positional[0] || (args.action as string | undefined) || 'list').trim().toLowerCase()
70
+ : undefined;
59
71
  const asJson = !!args.json;
60
72
 
61
73
  try {
62
74
  const readOnlyActions = new Set(['', 'search', 'graph-context', 'recent', 'suggest-topic-key', 'detail', 'timeline']);
63
75
  const needsSearchIndex = action === 'search' || action === 'detail' || action === 'timeline';
64
- const context = readOnlyActions.has(action)
76
+ const longTermReadOnly = longTermAction === 'list' || longTermAction === 'show';
77
+ const context = readOnlyActions.has(action) || longTermReadOnly
65
78
  ? await getCliReadContext({ searchIndex: needsSearchIndex })
66
79
  : await getCliProjectContext({ searchIndex: true });
67
80
  const { project, dataDir, reader, identity } = context;
@@ -425,6 +438,178 @@ export default defineCommand({
425
438
  return;
426
439
  }
427
440
 
441
+ case 'long-term': {
442
+ const resolvedLongTermAction = longTermAction ?? 'list';
443
+ const longTermPositional = positional.slice(1);
444
+ const {
445
+ archiveLongTermMemory,
446
+ approveLongTermMemory,
447
+ createManualLongTermMemory,
448
+ getLongTermMemoryDetail,
449
+ listLongTermMemories,
450
+ promoteObservationToLongTermMemory,
451
+ qualifyLongTermMemory,
452
+ supersedeLongTermMemory,
453
+ } = await import('../../memory/long-term.js');
454
+ const { resolveLocalMemoryOwner } = await import('../../memory/owner.js');
455
+ const owner = await resolveLocalMemoryOwner(dataDir, { create: false });
456
+ const longTermReader = {
457
+ projectId: project.id,
458
+ ...(owner ? { ownerId: owner.id } : {}),
459
+ ...(reader.agentId ? { agentId: reader.agentId } : {}),
460
+ ...(reader.isTeamMember ? { isTeamMember: true } : {}),
461
+ };
462
+
463
+ const memoryId = (): string => {
464
+ const id = (args.id as string | undefined)?.trim()
465
+ || (args.fromObservation as string | undefined)?.trim()
466
+ || longTermPositional[0]?.trim();
467
+ if (!id) throw new Error('long-term memory id is required.');
468
+ return id;
469
+ };
470
+ const scope = () => longTermScope(args.scope as string | undefined);
471
+ const kind = () => longTermKind(args.kind as string | undefined);
472
+ const portability = () => longTermPortability(args.portability as string | undefined);
473
+ const reason = () => requiredLongTermReason(args.reason as string | undefined);
474
+
475
+ switch (resolvedLongTermAction) {
476
+ case 'list': {
477
+ const memories = await listLongTermMemories({
478
+ dataDir,
479
+ reader: longTermReader,
480
+ includeInactive: !!args.all,
481
+ limit: parsePositiveInt(args.limit as string | undefined, 50),
482
+ });
483
+ emitResult(
484
+ { project, memories },
485
+ memories.length === 0
486
+ ? 'No long-term memories are visible in the active scope.'
487
+ : memories.map(item => `- ${item.memory.id} [${item.memory.state}/${item.memory.kind}/${item.memory.scope}] ${item.memory.title}`).join('\n'),
488
+ asJson,
489
+ );
490
+ return;
491
+ }
492
+
493
+ case 'show': {
494
+ const detail = await getLongTermMemoryDetail({ dataDir, id: memoryId(), reader: longTermReader });
495
+ emitResult(
496
+ { project, ...detail },
497
+ [
498
+ `${detail.memory.id} [${detail.memory.state}/${detail.memory.kind}/${detail.memory.scope}]`,
499
+ detail.memory.title,
500
+ detail.memory.content,
501
+ `Evidence: ${detail.evidence.length}`,
502
+ `Audit events: ${detail.events.length}`,
503
+ ].join('\n'),
504
+ asJson,
505
+ );
506
+ return;
507
+ }
508
+
509
+ case 'add': {
510
+ const content = getStringArg(args.text as string | undefined, longTermPositional);
511
+ if (!content) {
512
+ emitError('text is required for "memorix memory long-term add"', asJson);
513
+ return;
514
+ }
515
+ const result = await createManualLongTermMemory({
516
+ dataDir,
517
+ projectId: project.id,
518
+ scope: scope(),
519
+ kind: kind(),
520
+ title: (args.title as string | undefined)?.trim() || content.slice(0, 100),
521
+ content,
522
+ facts: parseCsvList(args.facts as string | undefined),
523
+ tags: parseCsvList(args.tags as string | undefined),
524
+ applicability: (args.applicability as string | undefined)?.trim(),
525
+ portability: portability(),
526
+ reader: longTermReader,
527
+ });
528
+ emitResult(
529
+ { project, ...result },
530
+ `Created long-term memory candidate ${result.memory.id}. Qualify it after checking its evidence.`,
531
+ asJson,
532
+ );
533
+ return;
534
+ }
535
+
536
+ case 'promote': {
537
+ const rawId = (args.fromObservation as string | undefined)?.trim()
538
+ || (args.id as string | undefined)?.trim()
539
+ || longTermPositional[0]?.trim();
540
+ const observationId = Number.parseInt(rawId || '', 10);
541
+ if (!Number.isFinite(observationId)) {
542
+ emitError('fromObservation or id must be an observation ID for "memorix memory long-term promote"', asJson);
543
+ return;
544
+ }
545
+ const observation = getObservation(observationId, project.id);
546
+ if (!observation || !canManageObservation(observation, reader)) {
547
+ emitError('Observation was not found or is outside the active write scope.', asJson);
548
+ return;
549
+ }
550
+ const result = await promoteObservationToLongTermMemory({
551
+ dataDir,
552
+ observation,
553
+ scope: scope(),
554
+ kind: kind(),
555
+ tags: parseCsvList(args.tags as string | undefined),
556
+ applicability: (args.applicability as string | undefined)?.trim(),
557
+ reader: longTermReader,
558
+ });
559
+ emitResult(
560
+ { project, sourceObservationId: observation.id, ...result },
561
+ `Promoted observation #${observation.id} to long-term memory candidate ${result.memory.id}.`,
562
+ asJson,
563
+ );
564
+ return;
565
+ }
566
+
567
+ case 'qualify': {
568
+ await getLongTermMemoryDetail({ dataDir, id: memoryId(), reader: longTermReader });
569
+ const memory = await qualifyLongTermMemory({ dataDir, id: memoryId(), reason: reason() });
570
+ emitResult({ project, memory }, `Qualified long-term memory ${memory.id}.`, asJson);
571
+ return;
572
+ }
573
+
574
+ case 'approve': {
575
+ await getLongTermMemoryDetail({ dataDir, id: memoryId(), reader: longTermReader });
576
+ const memory = await approveLongTermMemory({ dataDir, id: memoryId(), reason: reason() });
577
+ emitResult({ project, memory }, `Approved long-term memory ${memory.id}.`, asJson);
578
+ return;
579
+ }
580
+
581
+ case 'archive': {
582
+ await getLongTermMemoryDetail({ dataDir, id: memoryId(), reader: longTermReader });
583
+ const memory = await archiveLongTermMemory({ dataDir, id: memoryId(), reason: reason() });
584
+ emitResult({ project, memory }, `Archived long-term memory ${memory.id}.`, asJson);
585
+ return;
586
+ }
587
+
588
+ case 'supersede': {
589
+ const supersededBy = (args.supersededBy as string | undefined)?.trim()
590
+ || (args['superseded-by'] as string | undefined)?.trim();
591
+ if (!supersededBy) {
592
+ emitError('supersededBy is required for "memorix memory long-term supersede"', asJson);
593
+ return;
594
+ }
595
+ await getLongTermMemoryDetail({ dataDir, id: memoryId(), reader: longTermReader });
596
+ await getLongTermMemoryDetail({ dataDir, id: supersededBy, reader: longTermReader });
597
+ const memory = await supersedeLongTermMemory({
598
+ dataDir,
599
+ id: memoryId(),
600
+ supersededBy,
601
+ reason: reason(),
602
+ });
603
+ emitResult({ project, memory }, `Superseded long-term memory ${memory.id} with ${supersededBy}.`, asJson);
604
+ return;
605
+ }
606
+
607
+ default:
608
+ emitError('long-term action must be list, show, add, promote, qualify, approve, archive, or supersede.', asJson);
609
+ }
610
+ return;
611
+ }
612
+
428
613
  default:
429
614
  console.log('Memorix Memory Commands');
430
615
  console.log('');
@@ -440,6 +625,13 @@ export default defineCommand({
440
625
  console.log(' memorix memory deduplicate [--query "..."] [--dryRun]');
441
626
  console.log(' memorix memory consolidate [--action preview|execute] [--threshold 0.45]');
442
627
  console.log(' memorix memory promote --ids 42,43 [--trigger "..."] [--instruction "..."]');
628
+ console.log(' memorix memory long-term list [--all]');
629
+ console.log(' memorix memory long-term add --kind semantic --scope user --portability portable --title "..." --text "..." [--tags "..."] [--applicability "..."]');
630
+ console.log(' memorix memory long-term promote --fromObservation 42 --kind procedural --scope project');
631
+ console.log(' memorix memory long-term qualify --id <id> --reason "verified against evidence"');
632
+ console.log(' memorix memory long-term approve --id <id> --reason "reviewed by operator"');
633
+ console.log(' memorix memory long-term archive --id <id> --reason "no longer current"');
634
+ console.log(' memorix memory long-term supersede --id <old-id> --superseded-by <qualified-id> --reason "replaced"');
443
635
  }
444
636
  } catch (error) {
445
637
  emitError(error instanceof Error ? error.message : String(error), asJson);
@@ -459,3 +651,27 @@ function getIdArg(args: Record<string, unknown>, positional: string[]): string {
459
651
  positional.join(',')
460
652
  );
461
653
  }
654
+
655
+ function longTermKind(value: string | undefined): 'episodic' | 'semantic' | 'procedural' {
656
+ const normalized = (value ?? 'semantic').trim().toLowerCase();
657
+ if (normalized === 'episodic' || normalized === 'semantic' || normalized === 'procedural') return normalized;
658
+ throw new Error('long-term kind must be episodic, semantic, or procedural.');
659
+ }
660
+
661
+ function longTermScope(value: string | undefined): 'project' | 'user' | 'team' {
662
+ const normalized = (value ?? 'project').trim().toLowerCase();
663
+ if (normalized === 'project' || normalized === 'user' || normalized === 'team') return normalized;
664
+ throw new Error('long-term scope must be project, user, or team.');
665
+ }
666
+
667
+ function longTermPortability(value: string | undefined): 'project-bound' | 'portable' {
668
+ const normalized = (value ?? 'project-bound').trim().toLowerCase();
669
+ if (normalized === 'project-bound' || normalized === 'portable') return normalized;
670
+ throw new Error('long-term portability must be project-bound or portable.');
671
+ }
672
+
673
+ function requiredLongTermReason(value: string | undefined): string {
674
+ const reason = value?.trim();
675
+ if (!reason) throw new Error('reason is required for this long-term memory transition.');
676
+ return reason;
677
+ }
@@ -388,6 +388,7 @@ export async function buildAutoProjectContext(input: {
388
388
  stale: overview.freshness.stale,
389
389
  },
390
390
  runtimeCautions,
391
+ ...(input.reader ? { reader: input.reader } : {}),
391
392
  ...(input.deliveryTarget ? { deliveryTarget: input.deliveryTarget } : {}),
392
393
  });
393
394
 
@@ -601,6 +602,16 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
601
602
  : '- none yet',
602
603
  );
603
604
 
605
+ if (context.workset.durableMemory.length > 0) {
606
+ lines.push('', 'Durable memory');
607
+ for (const memory of context.workset.durableMemory.slice(0, 3)) {
608
+ lines.push(
609
+ '- ' + memory.kind + ' (' + memory.scope + ', ' + memory.state + '): '
610
+ + compactContinuationText(memory.summary, 28),
611
+ );
612
+ }
613
+ }
614
+
604
615
  const continuation = context.workset.continuation;
605
616
  if (continuation?.previousSession || continuation?.memories.length || continuation?.compactCheckpoint) {
606
617
  lines.push('', 'Resume from prior work');
@@ -10,6 +10,7 @@ import type {
10
10
  } from './types.js';
11
11
  import type { CodeGraphStore } from './store.js';
12
12
  import { isCodeGraphExcludedPath } from './exclude.js';
13
+ import type { ObservationReader } from '../types.js';
13
14
 
14
15
  export interface ContextPackMemory {
15
16
  id: number;
@@ -276,6 +277,7 @@ export async function attachTaskWorkset(input: {
276
277
  semanticCode?: ExternalCodeGraphOutline;
277
278
  providerQuality?: CodeGraphProviderQuality;
278
279
  runtimeCautions?: WorksetCaution[];
280
+ reader?: ObservationReader;
279
281
  }): Promise<ContextPack> {
280
282
  const semanticStartHere = input.semanticCode
281
283
  ? [...input.semanticCode.relatedFiles, ...input.semanticCode.entryPoints.map(entry => entry.path)]
@@ -316,6 +318,7 @@ export async function attachTaskWorkset(input: {
316
318
  stale: input.pack.warnings.filter(warning => warning.status === 'stale').length,
317
319
  },
318
320
  runtimeCautions: input.runtimeCautions,
321
+ ...(input.reader ? { reader: input.reader } : {}),
319
322
  deliveryTarget: 'context-pack',
320
323
  });
321
324
  return { ...input.pack, workset };
@@ -9,7 +9,7 @@ import { createHash } from 'node:crypto';
9
9
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
10
10
  import { join } from 'node:path';
11
11
  import { homedir } from 'node:os';
12
- import type { EmbeddingProvider } from './provider.js';
12
+ import type { EmbeddingProvider, EmbeddingRequestOptions } from './provider.js';
13
13
 
14
14
  const CACHE_DIR = process.env.MEMORIX_DATA_DIR || join(homedir(), '.memorix', 'data');
15
15
  const CACHE_FILE = join(CACHE_DIR, '.embedding-api-cache.json');
@@ -306,6 +306,10 @@ export interface APIEmbeddingProviderCreateOptions {
306
306
  * the provider. This keeps startup/cache hydration off the remote API path.
307
307
  */
308
308
  allowNetworkProbe?: boolean;
309
+ /** Bound the one-off remote dimension probe used on a cold API start. */
310
+ requestTimeoutMs?: number;
311
+ /** Disable retries for a bounded, best-effort API initialization. */
312
+ retry?: boolean;
309
313
  }
310
314
 
311
315
  function resolveEnvEmbeddingApiKey(): string | undefined {
@@ -370,7 +374,10 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
370
374
  }
371
375
  } else {
372
376
  if (!allowNetworkProbe) return null;
373
- probeDimensions = await APIEmbeddingProvider.probeAPI(config);
377
+ probeDimensions = await APIEmbeddingProvider.probeAPI(config, {
378
+ timeoutMs: options.requestTimeoutMs,
379
+ retry: options.retry,
380
+ });
374
381
  console.error(`[memorix] API embedding: ${config.model} @ ${config.baseUrl} (${probeDimensions}d)`);
375
382
  // Persist for next cold start
376
383
  saveCachedDims(config, probeDimensions).catch(() => {});
@@ -430,7 +437,7 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
430
437
  return { apiKey, baseUrl, model, requestedDimensions };
431
438
  }
432
439
 
433
- private static async probeAPI(config: APIEmbeddingConfig): Promise<number> {
440
+ private static async probeAPI(config: APIEmbeddingConfig, options?: EmbeddingRequestOptions): Promise<number> {
434
441
  const body: Record<string, unknown> = {
435
442
  model: config.model,
436
443
  input: 'dimension probe',
@@ -443,6 +450,7 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
443
450
  `${config.baseUrl}/embeddings`,
444
451
  config.apiKey,
445
452
  body,
453
+ options,
446
454
  );
447
455
 
448
456
  if (response.data.length === 0 || !response.data[0].embedding) {
@@ -452,7 +460,7 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
452
460
  return response.data[0].embedding.length;
453
461
  }
454
462
 
455
- async embed(text: string): Promise<number[]> {
463
+ async embed(text: string, options?: EmbeddingRequestOptions): Promise<number[]> {
456
464
  const normalized = normalizeText(text);
457
465
  const hash = textHash(normalized, this.cacheKeyNamespace);
458
466
 
@@ -477,6 +485,7 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
477
485
  `${this.config.baseUrl}/embeddings`,
478
486
  this.config.apiKey,
479
487
  body,
488
+ options,
480
489
  );
481
490
  const embedding = response.data[0].embedding;
482
491
  if (embedding.length !== this.dimensions) {
@@ -521,7 +530,7 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
521
530
  return embedding;
522
531
  }
523
532
 
524
- async embedBatch(texts: string[]): Promise<number[][]> {
533
+ async embedBatch(texts: string[], options?: EmbeddingRequestOptions): Promise<number[][]> {
525
534
  await ensureDiskCacheLoaded();
526
535
  const normalizedTexts = texts.map(normalizeText);
527
536
  const results: number[][] = new Array(texts.length);
@@ -562,6 +571,7 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
562
571
  `${this.config.baseUrl}/embeddings`,
563
572
  this.config.apiKey,
564
573
  body,
574
+ options,
565
575
  );
566
576
 
567
577
  this.trackUsage(response);
@@ -638,10 +648,14 @@ async function fetchWithRetry(
638
648
  url: string,
639
649
  apiKey: string,
640
650
  body: Record<string, unknown>,
651
+ options: EmbeddingRequestOptions = {},
641
652
  attempt = 0,
642
653
  ): Promise<EmbeddingAPIResponse> {
643
654
  const controller = new AbortController();
644
- const timeout = setTimeout(() => controller.abort(), 10_000);
655
+ const timeoutMs = typeof options.timeoutMs === 'number' && Number.isFinite(options.timeoutMs)
656
+ ? Math.max(100, Math.min(Math.floor(options.timeoutMs), 60_000))
657
+ : 10_000;
658
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
645
659
  let response: Response;
646
660
  try {
647
661
  response = await fetch(url, {
@@ -656,7 +670,7 @@ async function fetchWithRetry(
656
670
  } catch (err: unknown) {
657
671
  clearTimeout(timeout);
658
672
  if (err instanceof Error && err.name === 'AbortError') {
659
- throw new Error(`Embedding API timeout after 10s: ${url}`);
673
+ throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
660
674
  }
661
675
  throw err;
662
676
  }
@@ -666,13 +680,13 @@ async function fetchWithRetry(
666
680
  return response.json() as Promise<EmbeddingAPIResponse>;
667
681
  }
668
682
 
669
- if ((response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
683
+ if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
670
684
  const delay = BASE_DELAY_MS * Math.pow(2, attempt);
671
685
  const retryAfter = response.headers.get('retry-after');
672
686
  const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : delay;
673
687
  console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${waitMs}ms`);
674
688
  await new Promise(resolve => setTimeout(resolve, waitMs));
675
- return fetchWithRetry(url, apiKey, body, attempt + 1);
689
+ return fetchWithRetry(url, apiKey, body, options, attempt + 1);
676
690
  }
677
691
 
678
692
  const errorText = await response.text().catch(() => 'unknown error');