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
|
@@ -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
|
|
|
@@ -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.
|
|
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.
|
|
25
|
+
"version": "1.3.0",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"packageArguments": [
|
|
28
28
|
{
|
package/src/cli/command-guide.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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');
|