memorix 1.2.9 → 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 +24 -0
- package/README.md +30 -4
- 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 +1945 -579
- 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 +24 -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 +5 -3
- package/docs/API_REFERENCE.md +20 -1
- package/docs/ARCHITECTURE.md +34 -1
- package/docs/DEVELOPMENT.md +10 -3
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/MODULES.md +27 -1
- package/docs/README.md +3 -3
- package/docs/SETUP.md +6 -4
- package/docs/dev-log/progress.txt +22 -0
- package/docs/hooks-architecture.md +1 -1
- package/llms-full.txt +5 -1
- package/llms.txt +2 -0
- package/package.json +7 -2
- 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 +38 -0
- package/src/cli/command-guide.ts +9 -1
- package/src/cli/commands/agent-integrations.ts +12 -6
- package/src/cli/commands/codegraph.ts +1 -0
- package/src/cli/commands/memory.ts +217 -1
- package/src/cli/commands/setup.ts +200 -23
- 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
|
@@ -162,7 +162,9 @@ const OPENCLAW_BUNDLE_AGENTS = new Set<AgentName>(['openclaw']);
|
|
|
162
162
|
const ANTIGRAVITY_PLUGIN_AGENTS = new Set<AgentName>(['antigravity']);
|
|
163
163
|
const HERMES_PLUGIN_AGENTS = new Set<AgentName>(['hermes']);
|
|
164
164
|
const OMP_PACKAGE_AGENTS = new Set<AgentName>(['omp']);
|
|
165
|
-
|
|
165
|
+
// These hosts have a native package/plugin surface. Their default setup must not
|
|
166
|
+
// create project-local fallback files beside the user's own agent configuration.
|
|
167
|
+
const PACKAGE_OWNED_INTEGRATION_AGENTS = new Set<AgentName>(['codex', 'antigravity', 'openclaw', 'hermes', 'omp']);
|
|
166
168
|
const SUPPORTED_SETUP_AGENTS: AgentName[] = [
|
|
167
169
|
'claude',
|
|
168
170
|
'codex',
|
|
@@ -484,6 +486,32 @@ function mergeTomlMcpConfig(existingContent: string | null, generatedContent: st
|
|
|
484
486
|
return `${parts.join('\n\n')}\n`;
|
|
485
487
|
}
|
|
486
488
|
|
|
489
|
+
type LegacyArtifactStatus = 'removed' | 'preserved' | 'missing';
|
|
490
|
+
|
|
491
|
+
export interface CodexLegacyMigrationResult {
|
|
492
|
+
mcp: LegacyArtifactStatus;
|
|
493
|
+
projectHooks: LegacyArtifactStatus;
|
|
494
|
+
pluginFolder: LegacyArtifactStatus;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function getTomlSection(content: string, section: string): string | null {
|
|
498
|
+
const lines = content.split(/\r?\n/);
|
|
499
|
+
const header = `[${section}]`;
|
|
500
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
501
|
+
if (start < 0) return null;
|
|
502
|
+
|
|
503
|
+
let end = lines.length;
|
|
504
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
505
|
+
const trimmed = lines[index].trim();
|
|
506
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
507
|
+
end = index;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return lines.slice(start + 1, end).join('\n');
|
|
513
|
+
}
|
|
514
|
+
|
|
487
515
|
function isLegacyCodexMemorixNodeServer(server: MCPServerEntry): boolean {
|
|
488
516
|
if (server.name !== 'memorix' || server.command.trim().toLowerCase() !== 'node') return false;
|
|
489
517
|
const startsMemorixStdio = server.args.some((arg) => arg.trim().toLowerCase() === 'serve');
|
|
@@ -494,6 +522,22 @@ function isLegacyCodexMemorixNodeServer(server: MCPServerEntry): boolean {
|
|
|
494
522
|
return startsMemorixStdio && sourceEntry;
|
|
495
523
|
}
|
|
496
524
|
|
|
525
|
+
function isOwnedLegacyCodexMcp(content: string): boolean {
|
|
526
|
+
const section = getTomlSection(content, 'mcp_servers.memorix');
|
|
527
|
+
if (section === null || getTomlSection(content, 'mcp_servers.memorix.env') !== null) return false;
|
|
528
|
+
|
|
529
|
+
const allowedKeys = new Set(['command', 'args', 'type', 'startup_timeout_sec', 'tool_timeout_sec', 'enabled']);
|
|
530
|
+
for (const rawLine of section.split(/\r?\n/)) {
|
|
531
|
+
const line = rawLine.trim();
|
|
532
|
+
if (!line || line.startsWith('#')) continue;
|
|
533
|
+
const key = line.match(/^([A-Za-z0-9_]+)\s*=/)?.[1];
|
|
534
|
+
if (!key || !allowedKeys.has(key)) return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const server = new CodexMCPAdapter().parse(content).find((entry) => entry.name === 'memorix');
|
|
538
|
+
return Boolean(server && isLegacyCodexMemorixNodeServer(server));
|
|
539
|
+
}
|
|
540
|
+
|
|
497
541
|
/**
|
|
498
542
|
* Codex plugins now own Memorix stdio MCP registration. Remove only the old
|
|
499
543
|
* source-path form emitted by pre-plugin setup, never a user's custom server.
|
|
@@ -508,9 +552,7 @@ export async function removeLegacyCodexMemorixMcpConfig(
|
|
|
508
552
|
return { configPath, removed: false };
|
|
509
553
|
}
|
|
510
554
|
|
|
511
|
-
|
|
512
|
-
const legacyServer = adapter.parse(existingContent).find(isLegacyCodexMemorixNodeServer);
|
|
513
|
-
if (!legacyServer) return { configPath, removed: false };
|
|
555
|
+
if (!isOwnedLegacyCodexMcp(existingContent)) return { configPath, removed: false };
|
|
514
556
|
|
|
515
557
|
let nextContent = removeTomlSection(existingContent, 'mcp_servers.memorix.env');
|
|
516
558
|
nextContent = removeTomlSection(nextContent, 'mcp_servers.memorix');
|
|
@@ -518,6 +560,88 @@ export async function removeLegacyCodexMemorixMcpConfig(
|
|
|
518
560
|
return { configPath, removed: true };
|
|
519
561
|
}
|
|
520
562
|
|
|
563
|
+
function collectHookCommands(value: unknown, commands: string[]): void {
|
|
564
|
+
if (Array.isArray(value)) {
|
|
565
|
+
for (const item of value) collectHookCommands(item, commands);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (!value || typeof value !== 'object') return;
|
|
569
|
+
|
|
570
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
571
|
+
if ((key === 'command' || key === 'commandWindows') && typeof child === 'string') {
|
|
572
|
+
commands.push(child);
|
|
573
|
+
} else {
|
|
574
|
+
collectHookCommands(child, commands);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function isOwnedLegacyCodexHooks(content: string): boolean {
|
|
580
|
+
try {
|
|
581
|
+
const parsed = JSON.parse(content) as { hooks?: unknown };
|
|
582
|
+
if (!parsed.hooks || typeof parsed.hooks !== 'object') return false;
|
|
583
|
+
const commands: string[] = [];
|
|
584
|
+
collectHookCommands(parsed.hooks, commands);
|
|
585
|
+
return commands.length > 0
|
|
586
|
+
&& commands.every((command) => /^memorix(?:\.cmd)?\s+hook(?:\s|$)/i.test(command.trim()));
|
|
587
|
+
} catch {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function removeOwnedLegacyCodexPluginFolder(homeDir: string): Promise<LegacyArtifactStatus> {
|
|
593
|
+
const canonicalPath = path.join(homeDir, 'plugins', 'memorix');
|
|
594
|
+
const legacyPath = path.join(homeDir, '.codex', 'plugins', 'memorix');
|
|
595
|
+
if (!existsSync(legacyPath)) return 'missing';
|
|
596
|
+
if (!existsSync(canonicalPath)) return 'preserved';
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const manifest = JSON.parse(await readFile(path.join(legacyPath, '.codex-plugin', 'plugin.json'), 'utf-8')) as Record<string, unknown>;
|
|
600
|
+
const repository = String(manifest.repository ?? '');
|
|
601
|
+
if (manifest.name !== 'memorix' || !repository.includes('github.com/AVIDS2/memorix')) return 'preserved';
|
|
602
|
+
await rm(legacyPath, { recursive: true, force: true });
|
|
603
|
+
return 'removed';
|
|
604
|
+
} catch {
|
|
605
|
+
return 'preserved';
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export async function migrateLegacyCodexIntegration(options: {
|
|
610
|
+
homeDir?: string;
|
|
611
|
+
projectRoot?: string;
|
|
612
|
+
} = {}): Promise<CodexLegacyMigrationResult> {
|
|
613
|
+
const homeDir = options.homeDir ?? homedir();
|
|
614
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
615
|
+
const configPath = path.join(homeDir, '.codex', 'config.toml');
|
|
616
|
+
let mcp: LegacyArtifactStatus = 'missing';
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
const content = await readFile(configPath, 'utf-8');
|
|
620
|
+
if (getTomlSection(content, 'mcp_servers.memorix') !== null) {
|
|
621
|
+
mcp = (await removeLegacyCodexMemorixMcpConfig(configPath)).removed ? 'removed' : 'preserved';
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
mcp = 'missing';
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const hooksPath = path.join(projectRoot, '.codex', 'hooks.json');
|
|
628
|
+
let projectHooks: LegacyArtifactStatus = 'missing';
|
|
629
|
+
try {
|
|
630
|
+
const content = await readFile(hooksPath, 'utf-8');
|
|
631
|
+
if (isOwnedLegacyCodexHooks(content)) {
|
|
632
|
+
await rm(hooksPath, { force: true });
|
|
633
|
+
projectHooks = 'removed';
|
|
634
|
+
} else {
|
|
635
|
+
projectHooks = 'preserved';
|
|
636
|
+
}
|
|
637
|
+
} catch {
|
|
638
|
+
projectHooks = 'missing';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const pluginFolder = await removeOwnedLegacyCodexPluginFolder(homeDir);
|
|
642
|
+
return { mcp, projectHooks, pluginFolder };
|
|
643
|
+
}
|
|
644
|
+
|
|
521
645
|
function mergeYamlMcpConfig(existingContent: string | null, generatedContent: string): string {
|
|
522
646
|
let existing: Record<string, unknown> = {};
|
|
523
647
|
try {
|
|
@@ -678,7 +802,9 @@ export async function installPluginPackage(options: PluginInstallOptions): Promi
|
|
|
678
802
|
}
|
|
679
803
|
|
|
680
804
|
if (options.agent === 'codex') {
|
|
681
|
-
|
|
805
|
+
// Codex discovers personal plugins from the user marketplace. Keep the
|
|
806
|
+
// package outside ~/.codex so setup does not look like project/config state.
|
|
807
|
+
const pluginPath = path.join(home, 'plugins', 'memorix');
|
|
682
808
|
const marketplacePath = path.join(home, '.agents', 'plugins', 'marketplace.json');
|
|
683
809
|
await copyDir(source, pluginPath);
|
|
684
810
|
await stampCodexPluginVersion(pluginPath);
|
|
@@ -908,6 +1034,39 @@ function tryInstallClaudePlugin(marketplaceRoot: string): { ok: boolean; message
|
|
|
908
1034
|
};
|
|
909
1035
|
}
|
|
910
1036
|
|
|
1037
|
+
export type CodexPluginState = 'ready' | 'disabled' | 'missing' | 'unknown';
|
|
1038
|
+
|
|
1039
|
+
export function parseCodexPluginState(content: string): CodexPluginState {
|
|
1040
|
+
try {
|
|
1041
|
+
// Some Windows builds emit a one-line diagnostic before the requested JSON.
|
|
1042
|
+
const start = content.indexOf('{');
|
|
1043
|
+
const end = content.lastIndexOf('}');
|
|
1044
|
+
if (start < 0 || end < start) return 'unknown';
|
|
1045
|
+
const parsed = JSON.parse(content.slice(start, end + 1)) as { installed?: unknown };
|
|
1046
|
+
if (!Array.isArray(parsed.installed)) return 'unknown';
|
|
1047
|
+
|
|
1048
|
+
const plugin = parsed.installed.find((entry): entry is Record<string, unknown> => (
|
|
1049
|
+
Boolean(entry)
|
|
1050
|
+
&& typeof entry === 'object'
|
|
1051
|
+
&& (entry as Record<string, unknown>).pluginId === 'memorix@personal'
|
|
1052
|
+
));
|
|
1053
|
+
if (!plugin || plugin.installed !== true) return 'missing';
|
|
1054
|
+
return plugin.enabled === true ? 'ready' : 'disabled';
|
|
1055
|
+
} catch {
|
|
1056
|
+
return 'unknown';
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function getCodexPluginState(): CodexPluginState {
|
|
1061
|
+
const result = spawnSync('codex', ['plugin', 'list', '--json'], {
|
|
1062
|
+
encoding: 'utf-8',
|
|
1063
|
+
stdio: 'pipe',
|
|
1064
|
+
shell: process.platform === 'win32',
|
|
1065
|
+
});
|
|
1066
|
+
if (result.status !== 0) return 'unknown';
|
|
1067
|
+
return parseCodexPluginState(result.stdout || '');
|
|
1068
|
+
}
|
|
1069
|
+
|
|
911
1070
|
export function tryInstallCodexPlugin(): { ok: boolean; message: string } {
|
|
912
1071
|
const result = spawnSync('codex', ['plugin', 'add', 'memorix@personal', '--json'], {
|
|
913
1072
|
encoding: 'utf-8',
|
|
@@ -915,13 +1074,28 @@ export function tryInstallCodexPlugin(): { ok: boolean; message: string } {
|
|
|
915
1074
|
shell: process.platform === 'win32',
|
|
916
1075
|
});
|
|
917
1076
|
|
|
918
|
-
if (result.status === 0) {
|
|
919
|
-
return { ok: true, message: 'codex: plugin installed from Personal marketplace' };
|
|
920
|
-
}
|
|
921
|
-
|
|
922
1077
|
const output = `${result.stderr || ''}\n${result.stdout || ''}`.toLowerCase();
|
|
923
|
-
|
|
924
|
-
|
|
1078
|
+
const addFinished = result.status === 0 || output.includes('already');
|
|
1079
|
+
if (addFinished) {
|
|
1080
|
+
const pluginState = getCodexPluginState();
|
|
1081
|
+
if (pluginState === 'ready') {
|
|
1082
|
+
return {
|
|
1083
|
+
ok: true,
|
|
1084
|
+
message: result.status === 0
|
|
1085
|
+
? 'codex: plugin installed and enabled from Personal marketplace'
|
|
1086
|
+
: 'codex: plugin already installed and enabled from Personal marketplace',
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (pluginState === 'disabled') {
|
|
1090
|
+
return {
|
|
1091
|
+
ok: false,
|
|
1092
|
+
message: 'codex: Memorix plugin is installed but disabled. Enable it in Codex Plugins before any legacy MCP or project hooks are removed.',
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
ok: false,
|
|
1097
|
+
message: 'codex: plugin registration finished, but Codex did not confirm an enabled Memorix plugin. Legacy MCP and project hooks were kept unchanged.',
|
|
1098
|
+
};
|
|
925
1099
|
}
|
|
926
1100
|
|
|
927
1101
|
const detail = (result.stderr || result.stdout || result.error?.message || '').trim();
|
|
@@ -1106,7 +1280,9 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
|
|
|
1106
1280
|
const hasAntigravityPlugin = plan.actions.includes('antigravity-plugin') && agent === 'antigravity';
|
|
1107
1281
|
const hasHermesPlugin = plan.actions.includes('hermes-plugin') && agent === 'hermes';
|
|
1108
1282
|
const hasOmpPackage = plan.actions.includes('omp-package') && agent === 'omp';
|
|
1109
|
-
|
|
1283
|
+
// Codex's supported default is the user-level plugin. Never create a
|
|
1284
|
+
// project-local .codex/config.toml as an implicit fallback.
|
|
1285
|
+
const wantsMcpConfig = plan.mcp !== 'none' && agent !== 'pi' && !(agent === 'codex' && !global);
|
|
1110
1286
|
|
|
1111
1287
|
if (PLUGIN_PACKAGE_AGENTS.has(agent) && plan.includePlugin && !global) {
|
|
1112
1288
|
p.log.info(`${agent}: project setup leaves user-level plugins untouched. Run \`memorix setup --agent ${agent} --global\` to install its plugin, skills, and lifecycle hooks.`);
|
|
@@ -1115,7 +1291,7 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
|
|
|
1115
1291
|
if (hasPluginPackage) {
|
|
1116
1292
|
const result = await installPluginPackage({
|
|
1117
1293
|
agent: agent as PluginInstallOptions['agent'],
|
|
1118
|
-
includeHooks: plan.
|
|
1294
|
+
includeHooks: plan.includeHooks,
|
|
1119
1295
|
});
|
|
1120
1296
|
p.log.success(`${agent}: plugin package -> ${result.pluginPath}`);
|
|
1121
1297
|
if (result.marketplacePath) {
|
|
@@ -1123,17 +1299,16 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
|
|
|
1123
1299
|
}
|
|
1124
1300
|
if (agent === 'codex') {
|
|
1125
1301
|
const install = tryInstallCodexPlugin();
|
|
1126
|
-
if (install.ok) p.log.success(install.message);
|
|
1127
|
-
else p.log.warn(install.message);
|
|
1128
1302
|
if (install.ok) {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1303
|
+
p.log.success(install.message);
|
|
1304
|
+
const migration = await migrateLegacyCodexIntegration({ projectRoot: process.cwd() });
|
|
1305
|
+
if (migration.mcp === 'removed') p.log.info('codex: removed the legacy source-path MCP block; the enabled plugin now owns MCP.');
|
|
1306
|
+
if (migration.mcp === 'preserved') p.log.warn('codex: kept a customized legacy Memorix MCP block; remove it manually only after confirming plugin MCP works.');
|
|
1307
|
+
if (migration.projectHooks === 'removed') p.log.info('codex: removed a legacy project-local Memorix hooks.json; plugin hooks now own lifecycle capture.');
|
|
1308
|
+
if (migration.projectHooks === 'preserved') p.log.warn('codex: kept a project .codex/hooks.json because it contains non-Memorix commands.');
|
|
1309
|
+
if (migration.pluginFolder === 'removed') p.log.info('codex: removed the migrated legacy plugin folder under ~/.codex/plugins.');
|
|
1310
|
+
} else {
|
|
1311
|
+
p.log.warn(install.message);
|
|
1137
1312
|
}
|
|
1138
1313
|
} else if (agent === 'claude' && result.marketplaceRoot) {
|
|
1139
1314
|
const install = tryInstallClaudePlugin(result.marketplaceRoot);
|
|
@@ -1224,6 +1399,8 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
|
|
|
1224
1399
|
const result = await installMcpConfig({ agent: agent as McpConfigAgent, projectRoot: targetRoot, global, mcp });
|
|
1225
1400
|
p.log.success(`${agent}: MCP config -> ${result.configPath}`);
|
|
1226
1401
|
}
|
|
1402
|
+
} else if (agent === 'codex' && !global && plan.mcp !== 'none') {
|
|
1403
|
+
p.log.info('codex: project-local .codex config is intentionally untouched. Run `memorix setup --agent codex --global` to install the user-level plugin.');
|
|
1227
1404
|
}
|
|
1228
1405
|
|
|
1229
1406
|
if (hasPluginPackage) {
|
|
@@ -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');
|
|
@@ -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: {
|