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.
- package/CHANGELOG.md +15 -0
- package/README.md +28 -2
- package/README.zh-CN.md +28 -2
- package/assets/star-history-dark.svg +30 -0
- package/assets/star-history-light.svg +30 -0
- package/assets/star-history.json +599 -0
- package/dist/cli/index.js +1783 -557
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1292 -267
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +97 -21
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +15 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +1294 -269
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +99 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-backfill-runner.js +97 -21
- package/dist/vector-backfill-runner.js.map +1 -1
- package/docs/1.3-MEMORY-ARCHITECTURE.md +236 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +3 -1
- package/docs/API_REFERENCE.md +20 -1
- package/docs/ARCHITECTURE.md +34 -1
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/MODULES.md +27 -1
- package/docs/README.md +3 -3
- package/docs/dev-log/progress.txt +22 -0
- package/llms-full.txt +5 -1
- package/llms.txt +2 -0
- package/package.json +2 -1
- package/plugins/antigravity/memorix/rules/memorix.md +2 -0
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/gemini/memorix/GEMINI.md +2 -0
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/server.json +2 -2
- package/src/cli/command-guide.ts +9 -1
- package/src/cli/commands/codegraph.ts +1 -0
- package/src/cli/commands/memory.ts +217 -1
- package/src/codegraph/auto-context.ts +11 -0
- package/src/codegraph/context-pack.ts +3 -0
- package/src/embedding/api-provider.ts +23 -9
- package/src/embedding/provider.ts +21 -10
- package/src/hooks/installers/index.ts +2 -0
- package/src/knowledge/context-assembly.ts +2 -1
- package/src/knowledge/workset.ts +77 -4
- package/src/memory/long-term-store.ts +242 -0
- package/src/memory/long-term-types.ts +136 -0
- package/src/memory/long-term.ts +626 -0
- package/src/memory/owner.ts +75 -0
- package/src/server.ts +112 -30
- package/src/store/sqlite-db.ts +74 -0
- 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.
|
|
47
|
+
version: '1.3';
|
|
47
48
|
target: ContextDeliveryTarget;
|
|
48
49
|
elapsedMs: number;
|
|
49
50
|
budget: {
|
package/src/knowledge/workset.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|