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
@@ -27,15 +27,22 @@
27
27
  * Architecture inspired by Mem0's multi-provider embedding design.
28
28
  */
29
29
 
30
+ export interface EmbeddingRequestOptions {
31
+ /** Bound one remote embedding request without changing the provider default. */
32
+ timeoutMs?: number;
33
+ /** Disable provider retries for latency-sensitive best-effort work. */
34
+ retry?: boolean;
35
+ }
36
+
30
37
  export interface EmbeddingProvider {
31
38
  /** Provider name for logging/cache keys */
32
39
  readonly name: string;
33
40
  /** Vector dimensions (e.g., 384 for bge-small) */
34
41
  readonly dimensions: number;
35
42
  /** Generate embedding for a single text */
36
- embed(text: string): Promise<number[]>;
43
+ embed(text: string, options?: EmbeddingRequestOptions): Promise<number[]>;
37
44
  /** Generate embeddings for multiple texts (batch) */
38
- embedBatch(texts: string[]): Promise<number[][]>;
45
+ embedBatch(texts: string[], options?: EmbeddingRequestOptions): Promise<number[][]>;
39
46
  /** Return already-persisted embeddings without generating new vectors. */
40
47
  getCachedEmbeddings?(texts: string[]): Promise<(number[] | null)[]>;
41
48
  }
@@ -43,6 +50,10 @@ export interface EmbeddingProvider {
43
50
  export interface GetEmbeddingProviderOptions {
44
51
  /** Avoid remote API dimension probes while preparing a startup index. */
45
52
  allowNetworkProbe?: boolean;
53
+ /** Bound a remote API dimension probe for a latency-sensitive caller. */
54
+ requestTimeoutMs?: number;
55
+ /** Disable retries while resolving a best-effort provider. */
56
+ retry?: boolean;
46
57
  }
47
58
 
48
59
  /** Singleton provider instance (null = not available) */
@@ -205,9 +216,9 @@ function wrapProvider(candidate: EmbeddingProvider): EmbeddingProvider {
205
216
  return {
206
217
  name: candidate.name,
207
218
  dimensions: candidate.dimensions,
208
- async embed(text: string): Promise<number[]> {
219
+ async embed(text: string, options?: EmbeddingRequestOptions): Promise<number[]> {
209
220
  try {
210
- return await candidate.embed(text);
221
+ return await candidate.embed(text, options);
211
222
  } catch (error) {
212
223
  if (kind === 'api' && getEmbeddingMode() === 'auto' && isTemporaryEmbeddingFailure(error)) {
213
224
  warnOnce('[memorix] API embedding temporarily unavailable — switching to local fallback provider');
@@ -215,7 +226,7 @@ function wrapProvider(candidate: EmbeddingProvider): EmbeddingProvider {
215
226
  if (fallback) {
216
227
  provider = wrapProvider(fallback);
217
228
  warnOnce(`[memorix] Embedding fallback activated: ${provider.name} (${provider.dimensions}d)`);
218
- return provider.embed(text);
229
+ return provider.embed(text, options);
219
230
  }
220
231
  }
221
232
 
@@ -225,9 +236,9 @@ function wrapProvider(candidate: EmbeddingProvider): EmbeddingProvider {
225
236
  throw error;
226
237
  }
227
238
  },
228
- async embedBatch(texts: string[]): Promise<number[][]> {
239
+ async embedBatch(texts: string[], options?: EmbeddingRequestOptions): Promise<number[][]> {
229
240
  try {
230
- return await candidate.embedBatch(texts);
241
+ return await candidate.embedBatch(texts, options);
231
242
  } catch (error) {
232
243
  if (kind === 'api' && getEmbeddingMode() === 'auto' && isTemporaryEmbeddingFailure(error)) {
233
244
  warnOnce('[memorix] API embedding temporarily unavailable — switching to local fallback provider');
@@ -235,7 +246,7 @@ function wrapProvider(candidate: EmbeddingProvider): EmbeddingProvider {
235
246
  if (fallback) {
236
247
  provider = wrapProvider(fallback);
237
248
  warnOnce(`[memorix] Embedding fallback activated: ${provider.name} (${provider.dimensions}d)`);
238
- return provider.embedBatch(texts);
249
+ return provider.embedBatch(texts, options);
239
250
  }
240
251
  }
241
252
 
@@ -331,7 +342,7 @@ export async function getEmbeddingProvider(
331
342
 
332
343
  // API mode: remote embedding via OpenAI-compatible endpoint
333
344
  if (mode === 'api') {
334
- const initialized = await createAPIProvider();
345
+ const initialized = await createAPIProvider(options);
335
346
  if (!initialized) return null;
336
347
  provider = wrapProvider(initialized);
337
348
  warnOnce(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
@@ -340,7 +351,7 @@ export async function getEmbeddingProvider(
340
351
 
341
352
  // Auto mode: try configured API first, then local fallbacks
342
353
  if (hasAPIEmbeddingConfig()) {
343
- const initialized = await createAPIProvider();
354
+ const initialized = await createAPIProvider(options);
344
355
  if (initialized) {
345
356
  provider = wrapProvider(initialized);
346
357
  warnOnce(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
@@ -1247,6 +1247,8 @@ function getAgentRulesContent(agent?: AgentName, scope: 'project' | 'global' = '
1247
1247
  '- Include \x60filesModified\x60 when relevant',
1248
1248
  '- Use \x60topicKey\x60 for topics that evolve over time (prevents duplicates)',
1249
1249
  '- For "why" decisions, use \x60memorix_store_reasoning\x60',
1250
+ '- For a stable fact, reusable procedure, or completed episode that merits deliberate long-term review, include \x60longTerm\x60 in \x60memorix_store\x60 with the appropriate kind and normally \x60scope: "project"\x60. It creates a candidate only: do not use it for routine updates, do not make project-derived evidence portable user memory, and do not assume it enters context until an operator qualifies and approves it through \x60memorix memory long-term\x60.',
1251
+ '- A \x60user\x60 + \x60portable\x60 durable memory delivered in a task brief is intentionally available across projects. When it matches the task, use it as reusable background even if its origin differs; do not treat it as a current-project fact. Expand it only when needed with \x60memorix_detail\x60 using its \x60durable:<id>\x60 reference and a specific purpose.',
1250
1252
  '',
1251
1253
  "**Don't store:** greetings, simple file reads, trivial commands (ls, pwd, git status).",
1252
1254
  '',
@@ -18,6 +18,7 @@ export type ContextCandidateKind =
18
18
  | 'semantic-code'
19
19
  | 'start-here'
20
20
  | 'memory'
21
+ | 'durable-memory'
21
22
  | 'claim'
22
23
  | 'knowledge-page'
23
24
  | 'workflow'
@@ -43,7 +44,7 @@ export interface ContextReceiptOmission {
43
44
  }
44
45
 
45
46
  export interface ContextReceipt {
46
- version: '1.2.4';
47
+ version: '1.3';
47
48
  target: ContextDeliveryTarget;
48
49
  elapsedMs: number;
49
50
  budget: {
@@ -6,6 +6,7 @@ import { KnowledgeWorkspaceStore } from './workspace-store.js';
6
6
  import { loadKnowledgeWorkspace } from './workspace.js';
7
7
  import { WorkflowStore } from './workflow-store.js';
8
8
  import { selectWorkflows } from './workflows.js';
9
+ import { renderLongTermMemorySummary, selectLongTermMemoriesForTask } from '../memory/long-term.js';
9
10
  import type {
10
11
  ContextCandidateFreshness,
11
12
  ContextCandidateKind,
@@ -18,6 +19,8 @@ import type { CodeGraphProviderQuality, ExternalCodeGraphOutline } from '../code
18
19
  import type { KnowledgeClaim, ClaimEvidenceRef } from './types.js';
19
20
  import type { KnowledgePageRecord, KnowledgeWorkspace } from './workspace-types.js';
20
21
  import type { WorkflowSelection } from './workflow-types.js';
22
+ import type { ObservationReader } from '../types.js';
23
+ import type { LongTermMemory, LongTermMemoryEvidence } from '../memory/long-term-types.js';
21
24
 
22
25
  export type WorksetCautionKind =
23
26
  | 'dirty-worktree'
@@ -77,6 +80,18 @@ export interface WorksetMemorySource {
77
80
  reason?: string;
78
81
  }
79
82
 
83
+ /** A task-matching curated long-term item. The full record stays behind CLI detail. */
84
+ export interface WorksetLongTermMemory {
85
+ id: string;
86
+ kind: LongTermMemory['kind'];
87
+ scope: LongTermMemory['scope'];
88
+ state: LongTermMemory['state'];
89
+ title: string;
90
+ summary: string;
91
+ evidenceRefs: string[];
92
+ reason: string;
93
+ }
94
+
80
95
  /** Prior-work evidence selected only for an explicit or inferred continuation. */
81
96
  export interface WorksetContinuation {
82
97
  previousSession?: {
@@ -103,7 +118,7 @@ export interface WorksetContinuation {
103
118
  }
104
119
 
105
120
  export interface TaskWorkset {
106
- version: '1.2';
121
+ version: '1.3';
107
122
  task: string;
108
123
  lens: string;
109
124
  currentFacts: string[];
@@ -117,6 +132,7 @@ export interface TaskWorkset {
117
132
  hiddenCautionMemoryCount: number;
118
133
  claims: WorksetClaim[];
119
134
  pages: WorksetPage[];
135
+ durableMemory: WorksetLongTermMemory[];
120
136
  workflows: WorksetWorkflow[];
121
137
  cautions: WorksetCaution[];
122
138
  verification: string[];
@@ -164,6 +180,8 @@ export interface BuildTaskWorksetInput {
164
180
  stale: number;
165
181
  };
166
182
  runtimeCautions?: WorksetCaution[];
183
+ /** Reader identity used only for team-scoped durable memory filtering. */
184
+ reader?: ObservationReader;
167
185
  maxTokens?: number;
168
186
  /** Delivery surface controls receipt semantics without changing prompt shape. */
169
187
  deliveryTarget?: ContextDeliveryTarget;
@@ -297,6 +315,7 @@ function receiptOmissionKind(raw: string): ContextCandidateKind | undefined {
297
315
  if (raw.includes('state')) return 'code-state';
298
316
  if (raw.includes('semantic')) return 'semantic-code';
299
317
  if (raw.includes('start')) return 'start-here';
318
+ if (raw.includes('durable-memory')) return 'durable-memory';
300
319
  if (raw.includes('memory')) return 'memory';
301
320
  if (raw.includes('claim')) return 'claim';
302
321
  if (raw.includes('knowledge-page')) return 'knowledge-page';
@@ -593,6 +612,27 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
593
612
  }
594
613
  }
595
614
 
615
+ if (input.durableMemory.length > 0) {
616
+ appendLine(lines, '', maxTokens, omitted, 'durable-memory-heading');
617
+ appendLine(lines, 'Durable memory', maxTokens, omitted, 'durable-memory-heading');
618
+ for (const memory of input.durableMemory.slice(0, 3)) {
619
+ appendLine(
620
+ lines,
621
+ '- ' + memory.kind + ' (' + memory.scope + ', ' + memory.state + '; ref durable:' + memory.id + '): ' + memory.summary,
622
+ maxTokens,
623
+ omitted,
624
+ 'durable-memory',
625
+ selected,
626
+ {
627
+ kind: 'durable-memory',
628
+ id: 'durable:' + memory.id,
629
+ reason: memory.reason,
630
+ trust: 'source-backed',
631
+ },
632
+ );
633
+ }
634
+ }
635
+
596
636
  if (input.workflows.length > 0) {
597
637
  appendLine(lines, '', maxTokens, omitted, 'workflow-heading');
598
638
  appendLine(lines, 'Project workflow', maxTokens, omitted, 'workflow-heading');
@@ -674,6 +714,35 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
674
714
  reason: selection.reasons[claim.id] ?? 'source-qualified task match',
675
715
  }));
676
716
 
717
+ let durableMemory: WorksetLongTermMemory[] = [];
718
+ let durableEvidence: LongTermMemoryEvidence[] = [];
719
+ if (task) {
720
+ try {
721
+ const selected = await selectLongTermMemoriesForTask({
722
+ dataDir: input.dataDir,
723
+ projectId: input.projectId,
724
+ task,
725
+ ...(input.reader?.agentId ? { agentId: input.reader.agentId } : {}),
726
+ ...(input.reader?.isTeamMember ? { isTeamMember: true } : {}),
727
+ limit: 3,
728
+ });
729
+ durableMemory = selected.map(item => ({
730
+ id: item.memory.id,
731
+ kind: item.memory.kind,
732
+ scope: item.memory.scope,
733
+ state: item.memory.state,
734
+ title: item.memory.title,
735
+ summary: renderLongTermMemorySummary(item.memory, 20),
736
+ evidenceRefs: item.evidence.map(evidence => evidence.id),
737
+ reason: item.reason,
738
+ }));
739
+ durableEvidence = selected.flatMap(item => item.evidence);
740
+ } catch {
741
+ // Long-term memory is optional enrichment; a local identity or schema
742
+ // problem must never block current source/code context delivery.
743
+ }
744
+ }
745
+
677
746
  let workspace: KnowledgeWorkspace | undefined;
678
747
  let pages: WorksetPage[] = [];
679
748
  let workflows: WorksetWorkflow[] = [];
@@ -719,7 +788,10 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
719
788
  const evidenceIds = unique(selection.claims.flatMap(claim => evidenceIdsForClaim(
720
789
  claim,
721
790
  evidenceByClaim.get(claim.id) ?? [],
722
- )));
791
+ )).concat(
792
+ durableMemory.map(memory => 'durable:' + memory.id),
793
+ durableEvidence.map(evidence => 'durable-evidence:' + evidence.id),
794
+ ));
723
795
  const verification = unique([
724
796
  ...workflows.flatMap(workflow => workflow.verificationGates),
725
797
  ...input.verificationHints,
@@ -758,7 +830,7 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
758
830
  }
759
831
  : undefined;
760
832
  const base = {
761
- version: '1.2' as const,
833
+ version: '1.3' as const,
762
834
  task,
763
835
  lens: input.lens,
764
836
  currentFacts: input.currentFacts?.map(fact => fact.startsWith('Historical note:')
@@ -773,6 +845,7 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
773
845
  hiddenCautionMemoryCount: input.hiddenCautionMemoryCount ?? 0,
774
846
  claims,
775
847
  pages,
848
+ durableMemory,
776
849
  workflows,
777
850
  cautions: normalizedCautions,
778
851
  verification,
@@ -789,7 +862,7 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
789
862
  budget: { maxTokens },
790
863
  });
791
864
  const receipt: ContextReceipt = {
792
- version: '1.2.4',
865
+ version: '1.3',
793
866
  target: input.deliveryTarget ?? 'project-context',
794
867
  elapsedMs: Math.max(0, Date.now() - startedAt),
795
868
  budget: {
@@ -0,0 +1,242 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getDatabase } from '../store/sqlite-db.js';
3
+ import type {
4
+ LongTermMemory,
5
+ LongTermMemoryEvidence,
6
+ LongTermMemoryEvent,
7
+ } from './long-term-types.js';
8
+
9
+ function optionalText(value: unknown): string | undefined {
10
+ return typeof value === 'string' && value.trim() ? value : undefined;
11
+ }
12
+
13
+ function arrayValue(value: unknown): string[] {
14
+ if (typeof value !== 'string' || !value) return [];
15
+ try {
16
+ const parsed = JSON.parse(value);
17
+ return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : [];
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ function rowToMemory(row: any): LongTermMemory {
24
+ return {
25
+ id: row.id,
26
+ originProjectId: row.originProjectId,
27
+ ownerId: row.ownerId,
28
+ scope: row.scope,
29
+ kind: row.kind,
30
+ state: row.state,
31
+ portability: row.portability,
32
+ title: row.title,
33
+ content: row.content,
34
+ facts: arrayValue(row.factsJson),
35
+ tags: arrayValue(row.tagsJson),
36
+ ...(optionalText(row.applicability) ? { applicability: row.applicability } : {}),
37
+ origin: row.origin,
38
+ createdAt: row.createdAt,
39
+ updatedAt: row.updatedAt,
40
+ ...(optionalText(row.qualifiedAt) ? { qualifiedAt: row.qualifiedAt } : {}),
41
+ ...(optionalText(row.approvedAt) ? { approvedAt: row.approvedAt } : {}),
42
+ ...(optionalText(row.archivedAt) ? { archivedAt: row.archivedAt } : {}),
43
+ ...(optionalText(row.supersededBy) ? { supersededBy: row.supersededBy } : {}),
44
+ ...(optionalText(row.lastValidatedAt) ? { lastValidatedAt: row.lastValidatedAt } : {}),
45
+ accessCount: Number(row.accessCount ?? 0),
46
+ ...(optionalText(row.lastAccessedAt) ? { lastAccessedAt: row.lastAccessedAt } : {}),
47
+ } as LongTermMemory;
48
+ }
49
+
50
+ function rowToEvidence(row: any): LongTermMemoryEvidence {
51
+ return {
52
+ id: row.id,
53
+ memoryId: row.memoryId,
54
+ kind: row.kind,
55
+ referenceId: row.referenceId,
56
+ relation: row.relation,
57
+ ...(optionalText(row.locator) ? { locator: row.locator } : {}),
58
+ ...(optionalText(row.capturedHash) ? { capturedHash: row.capturedHash } : {}),
59
+ createdAt: row.createdAt,
60
+ } as LongTermMemoryEvidence;
61
+ }
62
+
63
+ function rowToEvent(row: any): LongTermMemoryEvent {
64
+ return {
65
+ id: row.id,
66
+ memoryId: row.memoryId,
67
+ kind: row.kind,
68
+ ...(optionalText(row.fromState) ? { fromState: row.fromState } : {}),
69
+ ...(optionalText(row.toState) ? { toState: row.toState } : {}),
70
+ ...(optionalText(row.detail) ? { detail: row.detail } : {}),
71
+ createdAt: row.createdAt,
72
+ } as LongTermMemoryEvent;
73
+ }
74
+
75
+ /** Persistence only. Lifecycle, privacy, and portability policy live in long-term.ts. */
76
+ export class LongTermMemoryStore {
77
+ private db: any = null;
78
+
79
+ async init(dataDir: string): Promise<void> {
80
+ this.db = getDatabase(dataDir);
81
+ }
82
+
83
+ transaction<T>(fn: () => T): T {
84
+ return this.db.transaction(fn)();
85
+ }
86
+
87
+ get(id: string): LongTermMemory | undefined {
88
+ const row = this.db.prepare('SELECT * FROM long_term_memories WHERE id = ?').get(id);
89
+ return row ? rowToMemory(row) : undefined;
90
+ }
91
+
92
+ list(options: { ownerId?: string; originProjectId?: string; limit?: number } = {}): LongTermMemory[] {
93
+ const limit = Math.max(1, Math.min(1_000, Math.floor(options.limit ?? 500)));
94
+ const clauses: string[] = [];
95
+ const params: unknown[] = [];
96
+ if (options.ownerId) {
97
+ clauses.push('ownerId = ?');
98
+ params.push(options.ownerId);
99
+ }
100
+ if (options.originProjectId) {
101
+ clauses.push('originProjectId = ?');
102
+ params.push(options.originProjectId);
103
+ }
104
+ const where = clauses.length ? ' WHERE ' + clauses.join(' AND ') : '';
105
+ const rows = this.db.prepare(
106
+ 'SELECT * FROM long_term_memories' + where + ' ORDER BY updatedAt DESC, id LIMIT ?',
107
+ ).all(...params, limit);
108
+ return rows.map(rowToMemory);
109
+ }
110
+
111
+ insert(memory: LongTermMemory): void {
112
+ this.db.prepare(`
113
+ INSERT INTO long_term_memories (
114
+ id, originProjectId, ownerId, scope, kind, state, portability,
115
+ title, content, factsJson, tagsJson, applicability, origin,
116
+ createdAt, updatedAt, qualifiedAt, approvedAt, archivedAt,
117
+ supersededBy, lastValidatedAt, accessCount, lastAccessedAt
118
+ ) VALUES (
119
+ @id, @originProjectId, @ownerId, @scope, @kind, @state, @portability,
120
+ @title, @content, @factsJson, @tagsJson, @applicability, @origin,
121
+ @createdAt, @updatedAt, @qualifiedAt, @approvedAt, @archivedAt,
122
+ @supersededBy, @lastValidatedAt, @accessCount, @lastAccessedAt
123
+ )
124
+ `).run({
125
+ ...memory,
126
+ factsJson: JSON.stringify(memory.facts),
127
+ tagsJson: JSON.stringify(memory.tags),
128
+ applicability: memory.applicability ?? null,
129
+ qualifiedAt: memory.qualifiedAt ?? null,
130
+ approvedAt: memory.approvedAt ?? null,
131
+ archivedAt: memory.archivedAt ?? null,
132
+ supersededBy: memory.supersededBy ?? null,
133
+ lastValidatedAt: memory.lastValidatedAt ?? null,
134
+ lastAccessedAt: memory.lastAccessedAt ?? null,
135
+ });
136
+ }
137
+
138
+ update(memory: LongTermMemory): void {
139
+ this.db.prepare(`
140
+ UPDATE long_term_memories SET
141
+ scope = @scope,
142
+ kind = @kind,
143
+ state = @state,
144
+ portability = @portability,
145
+ title = @title,
146
+ content = @content,
147
+ factsJson = @factsJson,
148
+ tagsJson = @tagsJson,
149
+ applicability = @applicability,
150
+ updatedAt = @updatedAt,
151
+ qualifiedAt = @qualifiedAt,
152
+ approvedAt = @approvedAt,
153
+ archivedAt = @archivedAt,
154
+ supersededBy = @supersededBy,
155
+ lastValidatedAt = @lastValidatedAt,
156
+ accessCount = @accessCount,
157
+ lastAccessedAt = @lastAccessedAt
158
+ WHERE id = @id
159
+ `).run({
160
+ ...memory,
161
+ factsJson: JSON.stringify(memory.facts),
162
+ tagsJson: JSON.stringify(memory.tags),
163
+ applicability: memory.applicability ?? null,
164
+ qualifiedAt: memory.qualifiedAt ?? null,
165
+ approvedAt: memory.approvedAt ?? null,
166
+ archivedAt: memory.archivedAt ?? null,
167
+ supersededBy: memory.supersededBy ?? null,
168
+ lastValidatedAt: memory.lastValidatedAt ?? null,
169
+ lastAccessedAt: memory.lastAccessedAt ?? null,
170
+ });
171
+ }
172
+
173
+ insertEvidence(input: Omit<LongTermMemoryEvidence, 'id' | 'createdAt'> & { createdAt?: string }): LongTermMemoryEvidence {
174
+ const evidence: LongTermMemoryEvidence = {
175
+ id: randomUUID(),
176
+ ...input,
177
+ createdAt: input.createdAt ?? new Date().toISOString(),
178
+ };
179
+ this.db.prepare(`
180
+ INSERT INTO long_term_memory_evidence (
181
+ id, memoryId, kind, referenceId, relation, locator, capturedHash, createdAt
182
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
183
+ `).run(
184
+ evidence.id,
185
+ evidence.memoryId,
186
+ evidence.kind,
187
+ evidence.referenceId,
188
+ evidence.relation,
189
+ evidence.locator ?? null,
190
+ evidence.capturedHash ?? null,
191
+ evidence.createdAt,
192
+ );
193
+ return evidence;
194
+ }
195
+
196
+ listEvidence(memoryId: string): LongTermMemoryEvidence[] {
197
+ return this.db.prepare(`
198
+ SELECT * FROM long_term_memory_evidence WHERE memoryId = ? ORDER BY createdAt ASC, id
199
+ `).all(memoryId).map(rowToEvidence);
200
+ }
201
+
202
+ recordEvent(input: Omit<LongTermMemoryEvent, 'id' | 'createdAt'> & { createdAt?: string }): LongTermMemoryEvent {
203
+ const event: LongTermMemoryEvent = {
204
+ id: randomUUID(),
205
+ ...input,
206
+ createdAt: input.createdAt ?? new Date().toISOString(),
207
+ };
208
+ this.db.prepare(`
209
+ INSERT INTO long_term_memory_events (id, memoryId, kind, fromState, toState, detail, createdAt)
210
+ VALUES (?, ?, ?, ?, ?, ?, ?)
211
+ `).run(
212
+ event.id,
213
+ event.memoryId,
214
+ event.kind,
215
+ event.fromState ?? null,
216
+ event.toState ?? null,
217
+ event.detail ?? null,
218
+ event.createdAt,
219
+ );
220
+ return event;
221
+ }
222
+
223
+ listEvents(memoryId: string): LongTermMemoryEvent[] {
224
+ return this.db.prepare(`
225
+ SELECT * FROM long_term_memory_events WHERE memoryId = ? ORDER BY createdAt ASC, rowid ASC
226
+ `).all(memoryId).map(rowToEvent);
227
+ }
228
+
229
+ markAccess(ids: string[], at = new Date().toISOString()): void {
230
+ const unique = [...new Set(ids.filter(Boolean))];
231
+ if (unique.length === 0) return;
232
+ const statement = this.db.prepare(`
233
+ UPDATE long_term_memories
234
+ SET accessCount = accessCount + 1, lastAccessedAt = ?
235
+ WHERE id = ?
236
+ `);
237
+ this.db.transaction(() => {
238
+ for (const id of unique) statement.run(at, id);
239
+ })();
240
+ }
241
+
242
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Durable cognitive-memory contracts. These types are intentionally separate
3
+ * from Observation provenance and Knowledge Claim truth contracts.
4
+ */
5
+
6
+ export const LONG_TERM_MEMORY_KINDS = ['episodic', 'semantic', 'procedural'] as const;
7
+ export type LongTermMemoryKind = typeof LONG_TERM_MEMORY_KINDS[number];
8
+
9
+ export const LONG_TERM_MEMORY_SCOPES = ['project', 'user', 'team'] as const;
10
+ export type LongTermMemoryScope = typeof LONG_TERM_MEMORY_SCOPES[number];
11
+
12
+ export const LONG_TERM_MEMORY_STATES = [
13
+ 'candidate',
14
+ 'qualified',
15
+ 'approved',
16
+ 'archived',
17
+ 'superseded',
18
+ ] as const;
19
+ export type LongTermMemoryState = typeof LONG_TERM_MEMORY_STATES[number];
20
+
21
+ export const LONG_TERM_MEMORY_PORTABILITIES = ['project-bound', 'portable'] as const;
22
+ export type LongTermMemoryPortability = typeof LONG_TERM_MEMORY_PORTABILITIES[number];
23
+
24
+ export const LONG_TERM_MEMORY_EVIDENCE_KINDS = [
25
+ 'observation',
26
+ 'claim',
27
+ 'workflow',
28
+ 'session',
29
+ 'checkpoint',
30
+ 'git',
31
+ 'code',
32
+ 'test',
33
+ 'run',
34
+ 'document',
35
+ 'manual',
36
+ 'user',
37
+ ] as const;
38
+ export type LongTermMemoryEvidenceKind = typeof LONG_TERM_MEMORY_EVIDENCE_KINDS[number];
39
+
40
+ export const LONG_TERM_MEMORY_EVIDENCE_RELATIONS = [
41
+ 'supports',
42
+ 'verifies',
43
+ 'derives',
44
+ 'user-confirmed',
45
+ ] as const;
46
+ export type LongTermMemoryEvidenceRelation = typeof LONG_TERM_MEMORY_EVIDENCE_RELATIONS[number];
47
+
48
+ export type LongTermMemoryOrigin = 'manual' | 'agent' | 'hook' | 'git' | 'migration';
49
+
50
+ export interface LongTermMemoryEvidence {
51
+ id: string;
52
+ memoryId: string;
53
+ kind: LongTermMemoryEvidenceKind;
54
+ referenceId: string;
55
+ relation: LongTermMemoryEvidenceRelation;
56
+ locator?: string;
57
+ capturedHash?: string;
58
+ createdAt: string;
59
+ }
60
+
61
+ export interface LongTermMemory {
62
+ id: string;
63
+ /** Project from which this item and its evidence originated. */
64
+ originProjectId: string;
65
+ /** Local stable owner identity. Team and project records retain attribution. */
66
+ ownerId: string;
67
+ scope: LongTermMemoryScope;
68
+ kind: LongTermMemoryKind;
69
+ state: LongTermMemoryState;
70
+ portability: LongTermMemoryPortability;
71
+ title: string;
72
+ content: string;
73
+ facts: string[];
74
+ tags: string[];
75
+ /** Short condition explaining when the procedure/fact applies. */
76
+ applicability?: string;
77
+ origin: LongTermMemoryOrigin;
78
+ createdAt: string;
79
+ updatedAt: string;
80
+ qualifiedAt?: string;
81
+ approvedAt?: string;
82
+ archivedAt?: string;
83
+ supersededBy?: string;
84
+ lastValidatedAt?: string;
85
+ accessCount: number;
86
+ lastAccessedAt?: string;
87
+ }
88
+
89
+ export type LongTermMemoryEventKind = 'created' | 'qualified' | 'approved' | 'archived' | 'superseded';
90
+
91
+ export interface LongTermMemoryEvent {
92
+ id: string;
93
+ memoryId: string;
94
+ kind: LongTermMemoryEventKind;
95
+ fromState?: LongTermMemoryState;
96
+ toState?: LongTermMemoryState;
97
+ detail?: string;
98
+ createdAt: string;
99
+ }
100
+
101
+ export interface LongTermMemoryReader {
102
+ projectId: string;
103
+ ownerId?: string;
104
+ agentId?: string;
105
+ isTeamMember?: boolean;
106
+ }
107
+
108
+ export interface LongTermMemoryEvidenceInput {
109
+ kind: LongTermMemoryEvidenceKind;
110
+ referenceId: string;
111
+ relation: LongTermMemoryEvidenceRelation;
112
+ locator?: string;
113
+ capturedHash?: string;
114
+ }
115
+
116
+ export interface CreateLongTermMemoryInput {
117
+ originProjectId: string;
118
+ ownerId: string;
119
+ scope: LongTermMemoryScope;
120
+ kind: LongTermMemoryKind;
121
+ portability?: LongTermMemoryPortability;
122
+ title: string;
123
+ content: string;
124
+ facts?: string[];
125
+ tags?: string[];
126
+ applicability?: string;
127
+ origin?: LongTermMemoryOrigin;
128
+ evidence: LongTermMemoryEvidenceInput[];
129
+ }
130
+
131
+ export interface LongTermMemorySelection {
132
+ memory: LongTermMemory;
133
+ evidence: LongTermMemoryEvidence[];
134
+ score: number;
135
+ reason: string;
136
+ }