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
@@ -0,0 +1,75 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const ID_PATTERN = /^[A-Za-z0-9:_-]{3,160}$/;
6
+
7
+ export interface LocalMemoryOwner {
8
+ id: string;
9
+ source: 'environment' | 'local-file';
10
+ }
11
+
12
+ interface OwnerFile {
13
+ version: 1;
14
+ id: string;
15
+ createdAt: string;
16
+ }
17
+
18
+ function ownerPath(dataDir: string): string {
19
+ const resolved = path.resolve(dataDir);
20
+ // The normal runtime data directory is ~/.memorix/data, so keep identity
21
+ // beside it. Custom data directories and isolated tests own their identity
22
+ // inside the supplied directory instead of accidentally sharing a temp root.
23
+ const home = path.basename(resolved).toLocaleLowerCase('en-US') === 'data'
24
+ ? path.dirname(resolved)
25
+ : resolved;
26
+ return path.join(home, 'memorix-user.json');
27
+ }
28
+
29
+ function normalizedOwnerId(value: string | undefined): string | undefined {
30
+ const trimmed = value?.trim();
31
+ return trimmed && ID_PATTERN.test(trimmed) ? trimmed : undefined;
32
+ }
33
+
34
+ async function readOwnerFile(filePath: string): Promise<LocalMemoryOwner | undefined> {
35
+ try {
36
+ const value = JSON.parse(await fs.readFile(filePath, 'utf8')) as Partial<OwnerFile>;
37
+ const id = typeof value.id === 'string' ? normalizedOwnerId(value.id) : undefined;
38
+ return id ? { id, source: 'local-file' } : undefined;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Resolve a local stable user identity without deriving anything from prompts,
46
+ * host names, or account details. Reads do not create an identity; first write
47
+ * creates one atomically enough for normal local CLI/MCP use.
48
+ */
49
+ export async function resolveLocalMemoryOwner(
50
+ dataDir: string,
51
+ options: { create?: boolean } = {},
52
+ ): Promise<LocalMemoryOwner | undefined> {
53
+ const configured = normalizedOwnerId(process.env.MEMORIX_USER_ID);
54
+ if (configured) return { id: configured, source: 'environment' };
55
+
56
+ const filePath = ownerPath(dataDir);
57
+ const existing = await readOwnerFile(filePath);
58
+ if (existing || !options.create) return existing;
59
+
60
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
61
+ const created: OwnerFile = {
62
+ version: 1,
63
+ id: 'local-' + randomUUID(),
64
+ createdAt: new Date().toISOString(),
65
+ };
66
+ try {
67
+ await fs.writeFile(filePath, JSON.stringify(created, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
68
+ return { id: created.id, source: 'local-file' };
69
+ } catch (error: any) {
70
+ if (error?.code !== 'EEXIST') throw error;
71
+ const concurrent = await readOwnerFile(filePath);
72
+ if (concurrent) return concurrent;
73
+ throw new Error('Memorix could not establish a valid local memory owner identity.');
74
+ }
75
+ }
package/src/server.ts CHANGED
@@ -671,9 +671,10 @@ export async function createMemorixServer(
671
671
  'Use type to classify: gotcha ([GOTCHA] critical pitfall), decision ([DECISION] architecture choice), ' +
672
672
  'problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), ' +
673
673
  'discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), ' +
674
- 'session-request ([SESSION] original goal). ' +
675
- 'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity. ' +
676
- 'For a read-only task, do not store unless the user explicitly asks to save a record.',
674
+ 'session-request ([SESSION] original goal). ' +
675
+ 'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity. ' +
676
+ 'Set longTerm only when the caller explicitly wants an additional source-backed long-term candidate; it is never injected until an operator records a review through the CLI. ' +
677
+ 'For a read-only task, do not store unless the user explicitly asks to save a record.',
677
678
  inputSchema: {
678
679
  entityName: z.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
679
680
  type: z.enum(OBSERVATION_TYPES).describe('Observation type for classification'),
@@ -697,30 +698,53 @@ export async function createMemorixServer(
697
698
  visibility: z.enum(['personal', 'project', 'team']).optional().describe(
698
699
  'Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true.',
699
700
  ),
701
+ longTerm: z.object({
702
+ kind: z.enum(['episodic', 'semantic', 'procedural']).describe('Cognitive kind for an additional long-term candidate.'),
703
+ scope: z.enum(['project', 'user', 'team']).optional().default('project').describe('Long-term scope. User capture is private source evidence and needs a bound agent identity.'),
704
+ tags: z.array(z.string()).optional().describe('Small set of retrieval tags for the candidate.'),
705
+ applicability: z.string().optional().describe('When this durable fact or procedure applies.'),
706
+ }).optional().describe('Optional explicit request to create a source-backed long-term candidate alongside this observation. Candidates are not automatically injected.'),
700
707
  overrideReadOnly: z.boolean().optional().describe(
701
708
  'Use only when the user explicitly asks to save memory during a read-only or no-modification task.',
702
709
  ),
703
710
  },
704
711
  },
705
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, overrideReadOnly }) => {
712
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, longTerm, overrideReadOnly }) => {
706
713
  const unresolved = requireResolvedProject('store memory in the current project');
707
714
  if (unresolved) return unresolved;
708
715
  const readOnlyBoundary = blockReadOnlyAutopilotWrite(overrideReadOnly);
709
716
  if (readOnlyBoundary) return readOnlyBoundary;
710
- const requestedVisibility = (visibility ?? 'project') as 'personal' | 'project' | 'team';
717
+ // Keep visibility undefined for an ordinary upsert. storeObservation then
718
+ // preserves an existing targeted/personal record instead of silently
719
+ // widening it to project scope. A long-term scope is explicit and must
720
+ // still set the source observation's corresponding visibility.
721
+ const requestedVisibility = (
722
+ longTerm?.scope === 'user'
723
+ ? 'personal'
724
+ : longTerm?.scope === 'team'
725
+ ? 'team'
726
+ : visibility
727
+ ) as 'personal' | 'project' | 'team' | undefined;
728
+ const effectiveVisibility = requestedVisibility ?? 'project';
711
729
  const reader = getObservationReader();
712
- if (requestedVisibility !== 'project' && !currentAgentId) {
730
+ if (effectiveVisibility !== 'project' && !currentAgentId) {
713
731
  return {
714
732
  content: [{ type: 'text' as const, text: 'Personal or team memory requires memorix_session_start with joinTeam=true so Memorix can bind an owner.' }],
715
733
  isError: true as const,
716
734
  };
717
735
  }
718
- if (requestedVisibility === 'team' && !reader.isTeamMember) {
736
+ if (effectiveVisibility === 'team' && !reader.isTeamMember) {
719
737
  return {
720
738
  content: [{ type: 'text' as const, text: 'Team memory requires an active coordination membership in the current project.' }],
721
739
  isError: true as const,
722
740
  };
723
741
  }
742
+ if (longTerm?.scope === 'team' && !reader.isTeamMember) {
743
+ return {
744
+ content: [{ type: 'text' as const, text: 'Team long-term memory requires an active coordination membership in the current project.' }],
745
+ isError: true as const,
746
+ };
747
+ }
724
748
  try {
725
749
  return await withFreshIndex(async () => {
726
750
 
@@ -752,7 +776,7 @@ export async function createMemorixServer(
752
776
  // ── Formation Pipeline (active mode: decides storage) ─────────────
753
777
  let formationResult: FormedMemory | null = null;
754
778
  let formationNote = '';
755
- if (useFormation && !topicKey && !progress) {
779
+ if (useFormation && !topicKey && !progress && !longTerm) {
756
780
  let currentFormationStage: FormationStage | 'setup' = 'setup';
757
781
  const completedFormationStages: Partial<Record<FormationStage, number>> = {};
758
782
  const formationStartTime = Date.now();
@@ -915,7 +939,7 @@ export async function createMemorixServer(
915
939
  // This keeps memory count low and prevents duplication (Mem0-style).
916
940
  let compactAction = '';
917
941
  let compactMerged = false;
918
- if (!useFormation && !topicKey && !progress) {
942
+ if (!useFormation && !topicKey && !progress && !longTerm) {
919
943
  try {
920
944
  const searchResult = await compactSearch({
921
945
  query: `${title} ${narrative.substring(0, 200)}`,
@@ -1094,10 +1118,33 @@ export async function createMemorixServer(
1094
1118
  sourceDetail: 'explicit',
1095
1119
  valueCategory: formationResult?.evaluation.category,
1096
1120
  createdByAgentId: currentAgentId,
1097
- visibility,
1121
+ visibility: requestedVisibility,
1098
1122
  visibilityReader: reader,
1099
1123
  });
1100
1124
 
1125
+ let longTermNote = '';
1126
+ if (longTerm) {
1127
+ try {
1128
+ const { promoteObservationToLongTermMemory } = await import('./memory/long-term.js');
1129
+ const promoted = await promoteObservationToLongTermMemory({
1130
+ dataDir: projectDir,
1131
+ observation: obs,
1132
+ scope: longTerm.scope,
1133
+ kind: longTerm.kind,
1134
+ tags: longTerm.tags,
1135
+ applicability: longTerm.applicability,
1136
+ reader: {
1137
+ projectId: project.id,
1138
+ ...(reader.agentId ? { agentId: reader.agentId } : {}),
1139
+ ...(reader.isTeamMember ? { isTeamMember: true } : {}),
1140
+ },
1141
+ });
1142
+ longTermNote = `\nLong-term candidate: ${promoted.memory.id} (${promoted.memory.kind}/${promoted.memory.scope}). Review it with the CLI before it is delivered automatically.`;
1143
+ } catch (longTermError) {
1144
+ longTermNote = `\n[WARN] Observation stored, but long-term candidate was not created: ${longTermError instanceof Error ? longTermError.message : 'unknown error'}`;
1145
+ }
1146
+ }
1147
+
1101
1148
  // Add a reference to the entity's observations
1102
1149
  await graphManager.addObservations([
1103
1150
  { entityName, contents: [`[#${obs.id}] ${title}`] },
@@ -1123,7 +1170,7 @@ export async function createMemorixServer(
1123
1170
  // ── Formation Pipeline (shadow/fallback mode: observe only) ─────
1124
1171
  // Fire-and-forget: runs after storage to collect metrics.
1125
1172
  // Never blocks the MCP response — purely for A/B comparison data.
1126
- if (!useFormation && !topicKey && !progress) {
1173
+ if (!useFormation && !topicKey && !progress && !longTerm) {
1127
1174
  const shadowFormation = async () => {
1128
1175
  let oldCompactDecision: { action: string, targetId?: number, reason?: string, durationMs?: number } | null = null;
1129
1176
  try {
@@ -1215,7 +1262,7 @@ export async function createMemorixServer(
1215
1262
  content: [
1216
1263
  {
1217
1264
  type: 'text' as const,
1218
- text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)\nEntity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ''}${compactAction}${compressionNote}${enrichment}${formationNote}${attributionWarning}`,
1265
+ text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)\nEntity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ''}${compactAction}${compressionNote}${enrichment}${formationNote}${attributionWarning}${longTermNote}`,
1219
1266
  },
1220
1267
  ],
1221
1268
  };
@@ -1667,6 +1714,7 @@ export async function createMemorixServer(
1667
1714
  ...(external.caution
1668
1715
  ? { runtimeCautions: [{ kind: 'external-codegraph-fallback' as const, message: external.caution }] }
1669
1716
  : {}),
1717
+ reader: getObservationReader(),
1670
1718
  });
1671
1719
  const text = buildContextPackPrompt(pack);
1672
1720
 
@@ -2475,11 +2523,11 @@ export async function createMemorixServer(
2475
2523
  {
2476
2524
  title: 'Memory Details',
2477
2525
  description:
2478
- 'Fetch full observation or mini-skill details — includes source kind (explicit memory / hook trace / git evidence), ' +
2526
+ 'Fetch full observation, mini-skill, or curated durable-memory details — includes source kind (explicit memory / hook trace / git evidence), ' +
2479
2527
  'value category, and cross-references (~500-1000 tokens each). ' +
2480
2528
  'Do not re-fetch content already covered by a complete memorix_project_context brief unless a specific fact is still missing or the user asks for deeper history; provide purpose when intentionally expanding. ' +
2481
2529
  'Always use memorix_search first to find relevant IDs, then fetch only what you need. ' +
2482
- 'Accepts typed refs from search results (e.g. "obs:42", "skill:3") via the typedRefs field, ' +
2530
+ 'Accepts typed refs from search results (e.g. "obs:42", "skill:3") and durable refs from a project brief (e.g. "durable:<uuid>") via the typedRefs field, ' +
2483
2531
  'or legacy numeric ids / object refs for backward compatibility.',
2484
2532
  inputSchema: {
2485
2533
  ids: z.array(z.number()).optional().describe('Observation IDs to fetch (legacy, from memorix_search results)'),
@@ -2489,7 +2537,7 @@ export async function createMemorixServer(
2489
2537
  projectId: z.string().optional().describe('Project ID for global-search disambiguation'),
2490
2538
  }),
2491
2539
  ).optional().describe('Explicit observation refs. Prefer this for global search results.'),
2492
- typedRefs: z.array(z.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"'),
2540
+ typedRefs: z.array(z.string()).optional().describe('Typed memory refs from search results or a project brief, e.g. "obs:42", "skill:3", "durable:<uuid>", "obs:42@org/proj"'),
2493
2541
  purpose: z.string().optional().describe(
2494
2542
  'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
2495
2543
  ),
@@ -2503,15 +2551,20 @@ export async function createMemorixServer(
2503
2551
  const safeIds = coerceNumberArray(ids);
2504
2552
  const safeRefs = coerceObservationRefs(refs);
2505
2553
  const safeTypedRefs = coerceStringArray(typedRefs);
2554
+ const durableIds = safeTypedRefs.flatMap((ref) => {
2555
+ const match = /^durable:([0-9a-f-]{36})$/i.exec(ref.trim());
2556
+ return match ? [match[1]] : [];
2557
+ });
2558
+ const observationTypedRefs = safeTypedRefs.filter(ref => !/^durable:/i.test(ref.trim()));
2506
2559
  const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
2507
- || safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
2560
+ || observationTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
2508
2561
  if (!hasCrossProjectRef) {
2509
2562
  const boundary = requireExplicitAutopilotExpansion('memorix_detail', purpose);
2510
2563
  if (boundary) return boundary;
2511
2564
  const requestedObservationIds = [
2512
2565
  ...safeIds,
2513
2566
  ...safeRefs.map(ref => ref.id),
2514
- ...safeTypedRefs.flatMap((ref) => {
2567
+ ...observationTypedRefs.flatMap((ref) => {
2515
2568
  const match = /^obs:(\d+)(?:@.+)?$/i.exec(ref.trim());
2516
2569
  return match ? [Number(match[1])] : [];
2517
2570
  }),
@@ -2520,18 +2573,47 @@ export async function createMemorixServer(
2520
2573
  if (duplicate) return duplicate;
2521
2574
  }
2522
2575
 
2523
- // Priority: typedRefs > refs > ids (each is a complete, homogeneous input path)
2524
- let result;
2576
+ const formatted: string[] = [];
2525
2577
  try {
2526
- const reader = getObservationReader(hasCrossProjectRef ? 'global' : 'project');
2527
- if (safeTypedRefs.length > 0) {
2528
- // Pass typed ref strings directly — compactDetail handles parsing
2529
- result = await compactDetail(safeTypedRefs, { reader });
2530
- } else if (safeRefs.length > 0) {
2531
- result = await compactDetail(safeRefs, { reader });
2532
- } else {
2533
- // Bare numeric IDs are scoped to the current project
2534
- result = await compactDetail(safeIds.map(id => ({ id, projectId: project.id })), { reader });
2578
+ if (durableIds.length > 0) {
2579
+ const [{ getLongTermMemoryDetail }, { resolveLocalMemoryOwner }] = await Promise.all([
2580
+ import('./memory/long-term.js'),
2581
+ import('./memory/owner.js'),
2582
+ ]);
2583
+ const owner = await resolveLocalMemoryOwner(projectDir, { create: false });
2584
+ const observationReader = getObservationReader();
2585
+ const durableReader = {
2586
+ projectId: project.id,
2587
+ ...(owner ? { ownerId: owner.id } : {}),
2588
+ ...(observationReader.agentId ? { agentId: observationReader.agentId } : {}),
2589
+ ...(observationReader.isTeamMember ? { isTeamMember: true } : {}),
2590
+ };
2591
+ const details = await Promise.all(durableIds.slice(0, 5).map(async (id) => {
2592
+ try {
2593
+ return await getLongTermMemoryDetail({ dataDir: projectDir, id, reader: durableReader });
2594
+ } catch (error) {
2595
+ return { error: error instanceof Error ? error.message : String(error), id };
2596
+ }
2597
+ }));
2598
+ formatted.push(JSON.stringify({ durableMemory: details }, null, 2));
2599
+ }
2600
+
2601
+ if (observationTypedRefs.length > 0 || safeRefs.length > 0 || safeIds.length > 0) {
2602
+ const reader = getObservationReader(hasCrossProjectRef ? 'global' : 'project');
2603
+ const result = observationTypedRefs.length > 0
2604
+ ? await compactDetail(observationTypedRefs, { reader })
2605
+ : safeRefs.length > 0
2606
+ ? await compactDetail(safeRefs, { reader })
2607
+ : await compactDetail(safeIds.map(id => ({ id, projectId: project.id })), { reader });
2608
+ formatted.push(
2609
+ result.documents.length > 0
2610
+ ? result.formatted
2611
+ : observationTypedRefs.length > 0
2612
+ ? `No memories found for refs: ${observationTypedRefs.join(', ')}`
2613
+ : safeRefs.length > 0
2614
+ ? `No memories found for refs: ${safeRefs.map((ref) => `${ref.projectId ?? 'current'}#${ref.id}`).join(', ')}`
2615
+ : `No memories found for IDs: ${safeIds.join(', ')}`,
2616
+ );
2535
2617
  }
2536
2618
  } catch (err) {
2537
2619
  return { content: [{ type: 'text' as const, text: err instanceof Error ? err.message : String(err) }], isError: true };
@@ -2541,8 +2623,8 @@ export async function createMemorixServer(
2541
2623
  content: [
2542
2624
  {
2543
2625
  type: 'text' as const,
2544
- text: result.documents.length > 0
2545
- ? result.formatted
2626
+ text: formatted.length > 0
2627
+ ? formatted.join('\n\n')
2546
2628
  : safeTypedRefs.length > 0
2547
2629
  ? `No memories found for refs: ${safeTypedRefs.join(', ')}`
2548
2630
  : safeRefs.length > 0
@@ -497,6 +497,62 @@ const CREATE_KNOWLEDGE_WORKFLOW_RUNS_TABLE = [
497
497
  ');',
498
498
  ].join('\n');
499
499
 
500
+ // ── 1.3 Durable cognitive memory ───────────────────────────────────
501
+
502
+ const CREATE_LONG_TERM_MEMORIES_TABLE = `
503
+ CREATE TABLE IF NOT EXISTS long_term_memories (
504
+ id TEXT PRIMARY KEY,
505
+ originProjectId TEXT NOT NULL,
506
+ ownerId TEXT NOT NULL,
507
+ scope TEXT NOT NULL,
508
+ kind TEXT NOT NULL,
509
+ state TEXT NOT NULL,
510
+ portability TEXT NOT NULL DEFAULT 'project-bound',
511
+ title TEXT NOT NULL,
512
+ content TEXT NOT NULL,
513
+ factsJson TEXT NOT NULL DEFAULT '[]',
514
+ tagsJson TEXT NOT NULL DEFAULT '[]',
515
+ applicability TEXT,
516
+ origin TEXT NOT NULL,
517
+ createdAt TEXT NOT NULL,
518
+ updatedAt TEXT NOT NULL,
519
+ qualifiedAt TEXT,
520
+ approvedAt TEXT,
521
+ archivedAt TEXT,
522
+ supersededBy TEXT,
523
+ lastValidatedAt TEXT,
524
+ accessCount INTEGER NOT NULL DEFAULT 0,
525
+ lastAccessedAt TEXT
526
+ );
527
+ `;
528
+
529
+ const CREATE_LONG_TERM_MEMORY_EVIDENCE_TABLE = `
530
+ CREATE TABLE IF NOT EXISTS long_term_memory_evidence (
531
+ id TEXT PRIMARY KEY,
532
+ memoryId TEXT NOT NULL,
533
+ kind TEXT NOT NULL,
534
+ referenceId TEXT NOT NULL,
535
+ relation TEXT NOT NULL,
536
+ locator TEXT,
537
+ capturedHash TEXT,
538
+ createdAt TEXT NOT NULL,
539
+ FOREIGN KEY (memoryId) REFERENCES long_term_memories(id) ON DELETE CASCADE
540
+ );
541
+ `;
542
+
543
+ const CREATE_LONG_TERM_MEMORY_EVENTS_TABLE = `
544
+ CREATE TABLE IF NOT EXISTS long_term_memory_events (
545
+ id TEXT PRIMARY KEY,
546
+ memoryId TEXT NOT NULL,
547
+ kind TEXT NOT NULL,
548
+ fromState TEXT,
549
+ toState TEXT,
550
+ detail TEXT,
551
+ createdAt TEXT NOT NULL,
552
+ FOREIGN KEY (memoryId) REFERENCES long_term_memories(id) ON DELETE CASCADE
553
+ );
554
+ `;
555
+
500
556
  // ── Runtime maintenance jobs ───────────────────────────────────────
501
557
 
502
558
  const CREATE_MAINTENANCE_JOBS_TABLE = `
@@ -599,6 +655,11 @@ CREATE INDEX IF NOT EXISTS idx_knowledge_page_claims_claim ON knowledge_page_cla
599
655
  CREATE INDEX IF NOT EXISTS idx_knowledge_proposals_workspace_status ON knowledge_proposals(workspaceId, status, createdAt DESC);
600
656
  CREATE INDEX IF NOT EXISTS idx_knowledge_workflows_workspace_status ON knowledge_workflows(workspaceId, status, updatedAt DESC);
601
657
  CREATE INDEX IF NOT EXISTS idx_knowledge_workflow_runs_project_workflow ON knowledge_workflow_runs(projectId, workflowId, startedAt DESC);
658
+ CREATE INDEX IF NOT EXISTS idx_long_term_memories_owner_state ON long_term_memories(ownerId, state, updatedAt DESC);
659
+ CREATE INDEX IF NOT EXISTS idx_long_term_memories_project_state ON long_term_memories(originProjectId, state, updatedAt DESC);
660
+ CREATE INDEX IF NOT EXISTS idx_long_term_memories_scope_portability ON long_term_memories(scope, portability, state, updatedAt DESC);
661
+ CREATE INDEX IF NOT EXISTS idx_long_term_memory_evidence_memory ON long_term_memory_evidence(memoryId, createdAt);
662
+ CREATE INDEX IF NOT EXISTS idx_long_term_memory_events_memory ON long_term_memory_events(memoryId, createdAt);
602
663
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_ready ON maintenance_jobs(status, run_after);
603
664
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_project ON maintenance_jobs(project_id, status, run_after);
604
665
  CREATE INDEX IF NOT EXISTS idx_maintenance_targets_updated ON maintenance_targets(updated_at);
@@ -705,6 +766,19 @@ const SCHEMA_MIGRATIONS: SchemaMigration[] = [
705
766
  db.exec('CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_source ON compaction_checkpoints(project_id, source_key)');
706
767
  },
707
768
  },
769
+ {
770
+ id: '1.3-long-term-memory',
771
+ apply: (db) => {
772
+ db.exec(CREATE_LONG_TERM_MEMORIES_TABLE);
773
+ db.exec(CREATE_LONG_TERM_MEMORY_EVIDENCE_TABLE);
774
+ db.exec(CREATE_LONG_TERM_MEMORY_EVENTS_TABLE);
775
+ db.exec('CREATE INDEX IF NOT EXISTS idx_long_term_memories_owner_state ON long_term_memories(ownerId, state, updatedAt DESC)');
776
+ db.exec('CREATE INDEX IF NOT EXISTS idx_long_term_memories_project_state ON long_term_memories(originProjectId, state, updatedAt DESC)');
777
+ db.exec('CREATE INDEX IF NOT EXISTS idx_long_term_memories_scope_portability ON long_term_memories(scope, portability, state, updatedAt DESC)');
778
+ db.exec('CREATE INDEX IF NOT EXISTS idx_long_term_memory_evidence_memory ON long_term_memory_evidence(memoryId, createdAt)');
779
+ db.exec('CREATE INDEX IF NOT EXISTS idx_long_term_memory_events_memory ON long_term_memory_events(memoryId, createdAt)');
780
+ },
781
+ },
708
782
  ];
709
783
 
710
784
  function applySchemaMigrations(db: any): void {
package/src/types.ts CHANGED
@@ -648,3 +648,23 @@ export interface MCPConfigAdapter {
648
648
  /** Get the default config file path for this agent */
649
649
  getConfigPath(projectRoot?: string): string;
650
650
  }
651
+
652
+ // Durable cognitive memory is intentionally kept in its own module so the
653
+ // legacy Observation contract remains source/provenance focused.
654
+ export type {
655
+ CreateLongTermMemoryInput,
656
+ LongTermMemory,
657
+ LongTermMemoryEvidence,
658
+ LongTermMemoryEvidenceInput,
659
+ LongTermMemoryEvidenceKind,
660
+ LongTermMemoryEvidenceRelation,
661
+ LongTermMemoryEvent,
662
+ LongTermMemoryEventKind,
663
+ LongTermMemoryKind,
664
+ LongTermMemoryOrigin,
665
+ LongTermMemoryPortability,
666
+ LongTermMemoryReader,
667
+ LongTermMemoryScope,
668
+ LongTermMemorySelection,
669
+ LongTermMemoryState,
670
+ } from './memory/long-term-types.js';