memorix 1.2.6 → 1.2.8
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 +20 -0
- package/README.md +6 -3
- package/README.zh-CN.md +5 -2
- package/dist/cli/index.js +1272 -105
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +7 -2
- package/dist/index.js +809 -52
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +6 -0
- package/dist/maintenance-runner.js +204 -25
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +20 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +809 -52
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +38 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.7-CONTEXT-CONTINUITY.md +68 -0
- package/docs/API_REFERENCE.md +6 -2
- package/docs/dev-log/progress.txt +48 -0
- package/docs/hooks-architecture.md +2 -1
- package/package.json +3 -3
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/extensions/memorix.js +17 -0
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/extensions/memorix.js +17 -0
- package/plugins/pi/memorix/package.json +1 -1
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/command-guide.ts +10 -0
- package/src/cli/commands/agent-integrations.ts +102 -5
- package/src/cli/commands/checkpoint.ts +144 -0
- package/src/cli/commands/serve-http.ts +14 -3
- package/src/cli/commands/setup.ts +44 -0
- package/src/cli/index.ts +3 -1
- package/src/codegraph/auto-context.ts +51 -5
- package/src/dashboard/server.ts +11 -0
- package/src/hooks/handler.ts +103 -11
- package/src/hooks/normalizer.ts +85 -1
- package/src/hooks/types.ts +16 -0
- package/src/knowledge/workset.ts +43 -2
- package/src/memory/compaction.ts +165 -0
- package/src/memory/observations.ts +67 -20
- package/src/runtime/maintenance-jobs.ts +84 -4
- package/src/runtime/project-maintenance.ts +63 -6
- package/src/server/tool-profile.ts +1 -0
- package/src/server.ts +94 -0
- package/src/store/compaction-checkpoint-store.ts +397 -0
- package/src/store/sqlite-db.ts +41 -0
- package/src/types.ts +46 -0
|
@@ -26,6 +26,8 @@ export interface AgentMcpCheck {
|
|
|
26
26
|
exists: boolean;
|
|
27
27
|
status: AgentIntegrationStatus;
|
|
28
28
|
issues: string[];
|
|
29
|
+
/** This endpoint is supplied by an enabled Codex plugin, not config.toml. */
|
|
30
|
+
managedByPlugin?: boolean;
|
|
29
31
|
server?: {
|
|
30
32
|
transport: 'stdio' | 'http';
|
|
31
33
|
command?: string;
|
|
@@ -229,6 +231,10 @@ function codexPluginPath(): string {
|
|
|
229
231
|
return `${homedir()}/.codex/plugins/${CODEX_PLUGIN_NAME}`;
|
|
230
232
|
}
|
|
231
233
|
|
|
234
|
+
function codexPluginMcpPath(): string {
|
|
235
|
+
return `${codexPluginPath()}/.mcp.json`;
|
|
236
|
+
}
|
|
237
|
+
|
|
232
238
|
function codexMarketplacePath(): string {
|
|
233
239
|
return `${homedir()}/.agents/plugins/marketplace.json`;
|
|
234
240
|
}
|
|
@@ -267,6 +273,7 @@ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
|
|
|
267
273
|
const pluginPath = codexPluginPath();
|
|
268
274
|
const manifestPath = `${pluginPath}/.codex-plugin/plugin.json`;
|
|
269
275
|
const hooksPath = `${pluginPath}/hooks/hooks.json`;
|
|
276
|
+
const mcpPath = codexPluginMcpPath();
|
|
270
277
|
if (!existsSync(pluginPath)) {
|
|
271
278
|
return {
|
|
272
279
|
scope: 'global',
|
|
@@ -317,6 +324,20 @@ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
|
|
|
317
324
|
if (version !== getCliVersion()) issues.push('codex-plugin-version-mismatch');
|
|
318
325
|
if (manifest.hooks !== './hooks/hooks.json') issues.push('codex-hook-manifest-missing');
|
|
319
326
|
|
|
327
|
+
if (!existsSync(mcpPath)) {
|
|
328
|
+
issues.push('codex-plugin-mcp-manifest-missing');
|
|
329
|
+
} else {
|
|
330
|
+
try {
|
|
331
|
+
const mcpConfig = asRecord(JSON.parse(await readFile(mcpPath, 'utf-8')));
|
|
332
|
+
const server = coerceMcpServer('memorix', asRecord(mcpConfig?.mcpServers)?.memorix);
|
|
333
|
+
if (!server || !isRecommendedStdioServer(server)) {
|
|
334
|
+
issues.push('codex-plugin-mcp-manifest-invalid');
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
issues.push('codex-plugin-mcp-manifest-unreadable');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
320
341
|
let declared: string[] = [];
|
|
321
342
|
if (!existsSync(hooksPath)) {
|
|
322
343
|
issues.push('codex-hook-manifest-missing');
|
|
@@ -479,7 +500,9 @@ async function inspectCodexHookTrust(): Promise<AgentPluginCheck> {
|
|
|
479
500
|
kind: 'hook-trust',
|
|
480
501
|
path: configPath,
|
|
481
502
|
exists: true,
|
|
482
|
-
|
|
503
|
+
// Codex records hook approval only after the user has reviewed it. Memorix
|
|
504
|
+
// must not treat an intentional, host-owned consent step as a broken setup.
|
|
505
|
+
status: issues.length > 0 ? 'skipped' : 'ok',
|
|
483
506
|
issues,
|
|
484
507
|
hookTrust: { trusted, expected: [...CODEX_HOOK_STATE_NAMES] },
|
|
485
508
|
};
|
|
@@ -562,7 +585,7 @@ function getClaudeLocalConfigPath(): string {
|
|
|
562
585
|
return `${homedir()}/.claude.json`;
|
|
563
586
|
}
|
|
564
587
|
|
|
565
|
-
function
|
|
588
|
+
function coerceMcpServer(name: string, value: unknown): MCPServerEntry | null {
|
|
566
589
|
const entry = asRecord(value);
|
|
567
590
|
if (!entry) return null;
|
|
568
591
|
const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
@@ -626,7 +649,7 @@ async function inspectClaudeLocalMcp(projectRoot: string): Promise<AgentMcpCheck
|
|
|
626
649
|
}
|
|
627
650
|
|
|
628
651
|
const servers = asRecord(localProject.project.mcpServers);
|
|
629
|
-
const server =
|
|
652
|
+
const server = coerceMcpServer('memorix', servers?.memorix);
|
|
630
653
|
if (!server) {
|
|
631
654
|
return {
|
|
632
655
|
scope: 'local',
|
|
@@ -683,6 +706,59 @@ async function installClaudeLocalMcpConfig(projectRoot: string): Promise<void> {
|
|
|
683
706
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
684
707
|
}
|
|
685
708
|
|
|
709
|
+
async function inspectCodexPluginMcp(): Promise<AgentMcpCheck | null> {
|
|
710
|
+
const runtime = inspectCodexPluginRuntime();
|
|
711
|
+
if (!runtime.runtime?.installed || !runtime.runtime.enabled) return null;
|
|
712
|
+
|
|
713
|
+
const mcpPath = codexPluginMcpPath();
|
|
714
|
+
if (!existsSync(mcpPath)) {
|
|
715
|
+
return {
|
|
716
|
+
scope: 'global',
|
|
717
|
+
path: mcpPath,
|
|
718
|
+
exists: false,
|
|
719
|
+
status: 'repairable',
|
|
720
|
+
issues: ['codex-plugin-mcp-manifest-missing'],
|
|
721
|
+
managedByPlugin: true,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
const config = asRecord(JSON.parse(await readFile(mcpPath, 'utf-8')));
|
|
727
|
+
const server = coerceMcpServer('memorix', asRecord(config?.mcpServers)?.memorix);
|
|
728
|
+
if (!server) {
|
|
729
|
+
return {
|
|
730
|
+
scope: 'global',
|
|
731
|
+
path: mcpPath,
|
|
732
|
+
exists: true,
|
|
733
|
+
status: 'repairable',
|
|
734
|
+
issues: ['codex-plugin-mcp-manifest-invalid'],
|
|
735
|
+
managedByPlugin: true,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
const issues: string[] = [];
|
|
739
|
+
if (looksLikeStaleMemorixCommand(server)) issues.push('stale-command-path');
|
|
740
|
+
if (!isRecommendedStdioServer(server)) issues.push('nonstandard-mcp-command');
|
|
741
|
+
return {
|
|
742
|
+
scope: 'global',
|
|
743
|
+
path: mcpPath,
|
|
744
|
+
exists: true,
|
|
745
|
+
status: issues.length > 0 ? 'repairable' : 'ok',
|
|
746
|
+
issues,
|
|
747
|
+
managedByPlugin: true,
|
|
748
|
+
server: sanitizeServer(server),
|
|
749
|
+
};
|
|
750
|
+
} catch {
|
|
751
|
+
return {
|
|
752
|
+
scope: 'global',
|
|
753
|
+
path: mcpPath,
|
|
754
|
+
exists: true,
|
|
755
|
+
status: 'repairable',
|
|
756
|
+
issues: ['codex-plugin-mcp-manifest-unreadable'],
|
|
757
|
+
managedByPlugin: true,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
686
762
|
async function inspectMcp(agent: AgentName, projectRoot: string, scope: AgentIntegrationScope): Promise<AgentIntegrationEntry['mcp']> {
|
|
687
763
|
if (!isMcpConfigAgent(agent)) {
|
|
688
764
|
return { status: 'skipped', issues: ['mcp-managed-by-package'], checks: [] };
|
|
@@ -697,6 +773,14 @@ async function inspectMcp(agent: AgentName, projectRoot: string, scope: AgentInt
|
|
|
697
773
|
continue;
|
|
698
774
|
}
|
|
699
775
|
|
|
776
|
+
if (agent === 'codex' && targetScope === 'global') {
|
|
777
|
+
const pluginCheck = await inspectCodexPluginMcp();
|
|
778
|
+
if (pluginCheck) {
|
|
779
|
+
checks.push(pluginCheck);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
700
784
|
const configPath = adapter.getConfigPath(targetScope === 'project' ? projectRoot : undefined);
|
|
701
785
|
if (targetScope === 'global' && configPath === adapter.getConfigPath(projectRoot)) continue;
|
|
702
786
|
|
|
@@ -872,11 +956,20 @@ export function formatAgentIntegrationReport(report: AgentIntegrationReport): st
|
|
|
872
956
|
lines.push(`${entry.agent}: ${status}`);
|
|
873
957
|
if (entry.mcp.issues.length > 0) lines.push(` MCP: ${entry.mcp.issues.join(', ')}`);
|
|
874
958
|
if (entry.guidance.issues.length > 0) lines.push(` Guidance: ${entry.guidance.issues.join(', ')}`);
|
|
875
|
-
|
|
959
|
+
const hookTrustPending = entry.plugin.issues.includes('codex-hook-trust-pending');
|
|
960
|
+
const pluginIssues = entry.plugin.issues.filter((issue) => issue !== 'codex-hook-trust-pending');
|
|
961
|
+
if (pluginIssues.length > 0) lines.push(` Plugin: ${pluginIssues.join(', ')}`);
|
|
962
|
+
if (hookTrustPending) {
|
|
963
|
+
lines.push(' Hooks: waiting for Codex approval on first use; MCP remains available.');
|
|
964
|
+
}
|
|
876
965
|
}
|
|
877
966
|
|
|
878
967
|
lines.push('');
|
|
879
|
-
|
|
968
|
+
if (report.summary.missing > 0 || report.summary.repairable > 0) {
|
|
969
|
+
lines.push(`Repair: ${report.repairCommand}`);
|
|
970
|
+
} else {
|
|
971
|
+
lines.push('No file repair needed.');
|
|
972
|
+
}
|
|
880
973
|
return lines.join('\n');
|
|
881
974
|
}
|
|
882
975
|
|
|
@@ -899,6 +992,10 @@ export async function repairAgentIntegrations(options: {
|
|
|
899
992
|
if (isMcpConfigAgent(entry.agent)) {
|
|
900
993
|
for (const check of entry.mcp.checks) {
|
|
901
994
|
if (check.status === 'ok' || check.status === 'skipped') continue;
|
|
995
|
+
if (check.managedByPlugin) {
|
|
996
|
+
skipped.push(`${entry.agent}:mcp:${check.scope}:plugin-managed`);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
902
999
|
if (check.status === 'missing' && !canInstallMissing) {
|
|
903
1000
|
skipped.push(`${entry.agent}:mcp:${check.scope}:missing`);
|
|
904
1001
|
continue;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { buildCompactionWorkset } from '../../memory/compaction.js';
|
|
3
|
+
import { CompactionCheckpointStore } from '../../store/compaction-checkpoint-store.js';
|
|
4
|
+
import type { CompactionCheckpoint } from '../../types.js';
|
|
5
|
+
import {
|
|
6
|
+
emitError,
|
|
7
|
+
emitResult,
|
|
8
|
+
getCliProjectContext,
|
|
9
|
+
parsePositiveInt,
|
|
10
|
+
shortId,
|
|
11
|
+
} from './operator-shared.js';
|
|
12
|
+
|
|
13
|
+
async function getCheckpointContext() {
|
|
14
|
+
const context = await getCliProjectContext();
|
|
15
|
+
const { initAliasRegistry, registerAlias } = await import('../../project/aliases.js');
|
|
16
|
+
initAliasRegistry(context.dataDir);
|
|
17
|
+
const canonicalId = await registerAlias(context.project);
|
|
18
|
+
return {
|
|
19
|
+
...context,
|
|
20
|
+
project: { ...context.project, id: canonicalId },
|
|
21
|
+
store: new CompactionCheckpointStore(context.dataDir),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertProjectCheckpoint(
|
|
26
|
+
checkpoint: CompactionCheckpoint | undefined,
|
|
27
|
+
projectId: string,
|
|
28
|
+
id: string,
|
|
29
|
+
): CompactionCheckpoint {
|
|
30
|
+
if (!checkpoint || checkpoint.projectId !== projectId) {
|
|
31
|
+
throw new Error(`No compact checkpoint "${id}" exists for this project.`);
|
|
32
|
+
}
|
|
33
|
+
return checkpoint;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatCheckpoint(checkpoint: CompactionCheckpoint): string {
|
|
37
|
+
const completion = checkpoint.completedAt ? `, completed ${checkpoint.completedAt}` : '';
|
|
38
|
+
const delivery = checkpoint.deliveredAt ? `, delivered ${checkpoint.deliveryCount}x` : '';
|
|
39
|
+
return [
|
|
40
|
+
`${shortId(checkpoint.id)} ${checkpoint.phase} ${checkpoint.agent} (${checkpoint.captureKind}, ${checkpoint.reason})`,
|
|
41
|
+
` session: ${checkpoint.sessionId}`,
|
|
42
|
+
` source: ${checkpoint.sourceEvent}${completion}${delivery}`,
|
|
43
|
+
` status: ${checkpoint.status}${checkpoint.transcriptAvailable ? ', transcript marker available' : ''}`,
|
|
44
|
+
...(checkpoint.summary ? [` summary: ${checkpoint.summary.replace(/\s+/g, ' ').slice(0, 220)}`] : []),
|
|
45
|
+
].join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function usage(): void {
|
|
49
|
+
console.log('Memorix Compact Checkpoints');
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log('Usage:');
|
|
52
|
+
console.log(' memorix checkpoint list [--session <id>] [--agent <name>] [--all]');
|
|
53
|
+
console.log(' memorix checkpoint show --id <checkpoint-id>');
|
|
54
|
+
console.log(' memorix checkpoint context [--id <checkpoint-id>] [--task "..."] [--budget 420]');
|
|
55
|
+
console.log(' memorix checkpoint archive --id <checkpoint-id>');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default defineCommand({
|
|
59
|
+
meta: {
|
|
60
|
+
name: 'checkpoint',
|
|
61
|
+
description: 'Inspect and manage native compact continuity checkpoints',
|
|
62
|
+
},
|
|
63
|
+
args: {
|
|
64
|
+
id: { type: 'string', description: 'Checkpoint ID' },
|
|
65
|
+
session: { type: 'string', description: 'Filter by host session ID' },
|
|
66
|
+
agent: { type: 'string', description: 'Filter by host agent name' },
|
|
67
|
+
task: { type: 'string', description: 'Current task for a bounded continuation workset' },
|
|
68
|
+
budget: { type: 'string', description: 'Maximum workset token budget (default: 420)' },
|
|
69
|
+
limit: { type: 'string', description: 'Maximum checkpoints to list (default: 20)' },
|
|
70
|
+
all: { type: 'boolean', description: 'Include archived checkpoints in list output' },
|
|
71
|
+
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
72
|
+
},
|
|
73
|
+
run: async ({ args }) => {
|
|
74
|
+
const positional = (args._ as string[]) ?? [];
|
|
75
|
+
const action = (positional[0] ?? 'list').toLowerCase();
|
|
76
|
+
const asJson = Boolean(args.json);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const { project, store } = await getCheckpointContext();
|
|
80
|
+
const id = (args.id as string | undefined)?.trim();
|
|
81
|
+
|
|
82
|
+
switch (action) {
|
|
83
|
+
case 'list': {
|
|
84
|
+
const checkpoints = store.list({
|
|
85
|
+
projectId: project.id,
|
|
86
|
+
sessionId: (args.session as string | undefined)?.trim() || undefined,
|
|
87
|
+
agent: (args.agent as string | undefined)?.trim() || undefined,
|
|
88
|
+
includeArchived: Boolean(args.all),
|
|
89
|
+
limit: parsePositiveInt(args.limit as string | undefined, 20),
|
|
90
|
+
});
|
|
91
|
+
emitResult(
|
|
92
|
+
{ project, checkpoints },
|
|
93
|
+
checkpoints.length
|
|
94
|
+
? checkpoints.map(formatCheckpoint).join('\n\n')
|
|
95
|
+
: 'No compact checkpoints for this project.',
|
|
96
|
+
asJson,
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'show': {
|
|
102
|
+
if (!id) throw new Error('id is required for "memorix checkpoint show".');
|
|
103
|
+
const checkpoint = assertProjectCheckpoint(store.get(id), project.id, id);
|
|
104
|
+
emitResult({ project, checkpoint }, formatCheckpoint(checkpoint), asJson);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'context': {
|
|
109
|
+
const checkpoint = id
|
|
110
|
+
? assertProjectCheckpoint(store.get(id), project.id, id)
|
|
111
|
+
: store.list({
|
|
112
|
+
projectId: project.id,
|
|
113
|
+
sessionId: (args.session as string | undefined)?.trim() || undefined,
|
|
114
|
+
agent: (args.agent as string | undefined)?.trim() || undefined,
|
|
115
|
+
limit: parsePositiveInt(args.limit as string | undefined, 20),
|
|
116
|
+
}).find((entry) => entry.phase === 'complete');
|
|
117
|
+
if (!checkpoint) {
|
|
118
|
+
throw new Error('No completed compact checkpoint is available for this project and filter.');
|
|
119
|
+
}
|
|
120
|
+
const workset = buildCompactionWorkset(checkpoint, {
|
|
121
|
+
task: (args.task as string | undefined)?.trim(),
|
|
122
|
+
maxTokens: parsePositiveInt(args.budget as string | undefined, 420),
|
|
123
|
+
});
|
|
124
|
+
emitResult({ project, checkpoint, workset }, workset.text, asJson);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'archive': {
|
|
129
|
+
if (!id) throw new Error('id is required for "memorix checkpoint archive".');
|
|
130
|
+
assertProjectCheckpoint(store.get(id), project.id, id);
|
|
131
|
+
const checkpoint = store.archive(id);
|
|
132
|
+
if (!checkpoint) throw new Error(`Compact checkpoint "${id}" is already archived.`);
|
|
133
|
+
emitResult({ project, checkpoint }, `Archived compact checkpoint ${shortId(checkpoint.id)}.`, asJson);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
usage();
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
});
|
|
@@ -839,10 +839,21 @@ export default defineCommand({
|
|
|
839
839
|
};
|
|
840
840
|
} catch { /* best effort */ }
|
|
841
841
|
|
|
842
|
-
let vectorStatus = {
|
|
842
|
+
let vectorStatus = {
|
|
843
|
+
available: false,
|
|
844
|
+
total: 0,
|
|
845
|
+
missing: 0,
|
|
846
|
+
missingIds: [] as number[],
|
|
847
|
+
backfillRunning: false,
|
|
848
|
+
};
|
|
843
849
|
try {
|
|
844
|
-
const { getVectorStatus } = await import('../../memory/observations.js');
|
|
845
|
-
|
|
850
|
+
const { getSearchIndexStatus, getVectorStatus } = await import('../../memory/observations.js');
|
|
851
|
+
// The HTTP control plane has a different module graph from stdio MCP.
|
|
852
|
+
// Only report vector counts when this process actually owns a hydrated
|
|
853
|
+
// index; an empty singleton must not look like a healthy 0/0 state.
|
|
854
|
+
if (getSearchIndexStatus(statsProjectId).prepared) {
|
|
855
|
+
vectorStatus = { available: true, ...getVectorStatus(statsProjectId) };
|
|
856
|
+
}
|
|
846
857
|
} catch { /* best effort */ }
|
|
847
858
|
|
|
848
859
|
// Real search mode from the last actual search execution
|
|
@@ -484,6 +484,40 @@ function mergeTomlMcpConfig(existingContent: string | null, generatedContent: st
|
|
|
484
484
|
return `${parts.join('\n\n')}\n`;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
function isLegacyCodexMemorixNodeServer(server: MCPServerEntry): boolean {
|
|
488
|
+
if (server.name !== 'memorix' || server.command.trim().toLowerCase() !== 'node') return false;
|
|
489
|
+
const startsMemorixStdio = server.args.some((arg) => arg.trim().toLowerCase() === 'serve');
|
|
490
|
+
const sourceEntry = server.args.some((arg) => {
|
|
491
|
+
const normalized = arg.replace(/\\/g, '/').toLowerCase();
|
|
492
|
+
return /\/memorix\/dist\/cli\/index\.(?:[cm]?js)$/.test(normalized);
|
|
493
|
+
});
|
|
494
|
+
return startsMemorixStdio && sourceEntry;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Codex plugins now own Memorix stdio MCP registration. Remove only the old
|
|
499
|
+
* source-path form emitted by pre-plugin setup, never a user's custom server.
|
|
500
|
+
*/
|
|
501
|
+
export async function removeLegacyCodexMemorixMcpConfig(
|
|
502
|
+
configPath = path.join(homedir(), '.codex', 'config.toml'),
|
|
503
|
+
): Promise<{ configPath: string; removed: boolean }> {
|
|
504
|
+
let existingContent: string;
|
|
505
|
+
try {
|
|
506
|
+
existingContent = await readFile(configPath, 'utf-8');
|
|
507
|
+
} catch {
|
|
508
|
+
return { configPath, removed: false };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const adapter = new CodexMCPAdapter();
|
|
512
|
+
const legacyServer = adapter.parse(existingContent).find(isLegacyCodexMemorixNodeServer);
|
|
513
|
+
if (!legacyServer) return { configPath, removed: false };
|
|
514
|
+
|
|
515
|
+
let nextContent = removeTomlSection(existingContent, 'mcp_servers.memorix.env');
|
|
516
|
+
nextContent = removeTomlSection(nextContent, 'mcp_servers.memorix');
|
|
517
|
+
await writeFile(configPath, nextContent ? `${nextContent}\n` : '', 'utf-8');
|
|
518
|
+
return { configPath, removed: true };
|
|
519
|
+
}
|
|
520
|
+
|
|
487
521
|
function mergeYamlMcpConfig(existingContent: string | null, generatedContent: string): string {
|
|
488
522
|
let existing: Record<string, unknown> = {};
|
|
489
523
|
try {
|
|
@@ -1091,6 +1125,16 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
|
|
|
1091
1125
|
const install = tryInstallCodexPlugin();
|
|
1092
1126
|
if (install.ok) p.log.success(install.message);
|
|
1093
1127
|
else p.log.warn(install.message);
|
|
1128
|
+
if (install.ok) {
|
|
1129
|
+
try {
|
|
1130
|
+
const migration = await removeLegacyCodexMemorixMcpConfig();
|
|
1131
|
+
if (migration.removed) {
|
|
1132
|
+
p.log.info(`codex: removed legacy source-path MCP config from ${migration.configPath}`);
|
|
1133
|
+
}
|
|
1134
|
+
} catch {
|
|
1135
|
+
p.log.warn('codex: plugin installed, but the legacy MCP config could not be checked');
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1094
1138
|
} else if (agent === 'claude' && result.marketplaceRoot) {
|
|
1095
1139
|
const install = tryInstallClaudePlugin(result.marketplaceRoot);
|
|
1096
1140
|
if (install.ok) p.log.success(install.message);
|
package/src/cli/index.ts
CHANGED
|
@@ -1050,6 +1050,7 @@ const main = defineCommand({
|
|
|
1050
1050
|
resume: () => import('./commands/resume.js').then(m => m.default),
|
|
1051
1051
|
explain: () => import('./commands/explain.js').then(m => m.default),
|
|
1052
1052
|
codegraph: () => import('./commands/codegraph.js').then(m => m.default),
|
|
1053
|
+
checkpoint: () => import('./commands/checkpoint.js').then(m => m.default),
|
|
1053
1054
|
knowledge: () => import('./commands/knowledge.js').then(m => m.default),
|
|
1054
1055
|
reasoning: () => import('./commands/reasoning.js').then(m => m.default),
|
|
1055
1056
|
retention: () => import('./commands/retention.js').then(m => m.default),
|
|
@@ -1118,7 +1119,7 @@ const main = defineCommand({
|
|
|
1118
1119
|
// Detect by checking if the first CLI arg matches a registered subcommand name.
|
|
1119
1120
|
const firstArg = process.argv[2];
|
|
1120
1121
|
const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
|
|
1121
|
-
'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
|
|
1122
|
+
'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'checkpoint', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
|
|
1122
1123
|
'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
|
|
1123
1124
|
'receipt',
|
|
1124
1125
|
'serve', 'serve-http', 'status', 'sync',
|
|
@@ -1161,6 +1162,7 @@ const main = defineCommand({
|
|
|
1161
1162
|
console.error(' resume Resume prior work with one bounded project brief');
|
|
1162
1163
|
console.error(' explain Explain where Memorix project context comes from');
|
|
1163
1164
|
console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
|
|
1165
|
+
console.error(' checkpoint Inspect native compact continuity checkpoints');
|
|
1164
1166
|
console.error(' knowledge Review source-backed knowledge pages and project workflows');
|
|
1165
1167
|
console.error(' reasoning Store/search decision rationale');
|
|
1166
1168
|
console.error(' retention Inspect stale/archive status');
|
|
@@ -2,7 +2,12 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { truncateToTokenBudget } from '../compact/token-budget.js';
|
|
4
4
|
import { getResolvedConfig } from '../config/resolved-config.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildTaskWorkset,
|
|
7
|
+
type TaskWorkset,
|
|
8
|
+
type WorksetCaution,
|
|
9
|
+
type WorksetContinuation,
|
|
10
|
+
} from '../knowledge/workset.js';
|
|
6
11
|
import type { ContextDeliveryTarget } from '../knowledge/context-assembly.js';
|
|
7
12
|
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
8
13
|
import type { ObservationReader, ProjectInfo } from '../types.js';
|
|
@@ -23,7 +28,7 @@ import {
|
|
|
23
28
|
} from './project-context.js';
|
|
24
29
|
import { CodeGraphStore } from './store.js';
|
|
25
30
|
import { isEligibleForAutomaticDelivery } from '../memory/admission.js';
|
|
26
|
-
import { getSessionResumeBrief
|
|
31
|
+
import { getSessionResumeBrief } from '../memory/session.js';
|
|
27
32
|
import { initSessionStore } from '../store/session-store.js';
|
|
28
33
|
import {
|
|
29
34
|
isContinuationTask,
|
|
@@ -56,7 +61,7 @@ export interface AutoProjectContext {
|
|
|
56
61
|
refresh: AutoContextRefreshResult;
|
|
57
62
|
providerQuality: CodeGraphProviderQuality;
|
|
58
63
|
/** Present only when the caller asked to continue prior work. */
|
|
59
|
-
continuation?:
|
|
64
|
+
continuation?: WorksetContinuation;
|
|
60
65
|
workset: TaskWorkset;
|
|
61
66
|
}
|
|
62
67
|
|
|
@@ -72,6 +77,7 @@ export interface AutoProjectBrief {
|
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
const DEFAULT_MAX_AGE_MS = 10 * 60 * 1000;
|
|
80
|
+
const COMPACT_CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
75
81
|
|
|
76
82
|
function activeProjectObservations(
|
|
77
83
|
observations: ProjectContextObservation[],
|
|
@@ -134,6 +140,14 @@ export async function buildAutoProjectContext(input: {
|
|
|
134
140
|
reader?: ObservationReader;
|
|
135
141
|
/** Auto detects continuation language; always is used by the explicit resume path. */
|
|
136
142
|
continuation?: 'auto' | 'always' | 'never';
|
|
143
|
+
/**
|
|
144
|
+
* Suppress a checkpoint already delivered through a host-native channel in
|
|
145
|
+
* this exact session. Other agents and later sessions remain eligible.
|
|
146
|
+
*/
|
|
147
|
+
excludeCompactionCheckpointFor?: {
|
|
148
|
+
sessionId: string;
|
|
149
|
+
agent: string;
|
|
150
|
+
};
|
|
137
151
|
/**
|
|
138
152
|
* When supplied, a needed refresh is queued instead of running in this
|
|
139
153
|
* request. MCP and hook callers use this to keep their response path fast.
|
|
@@ -294,10 +308,34 @@ export async function buildAutoProjectContext(input: {
|
|
|
294
308
|
// Project Context is also used by lightweight callers that have not touched
|
|
295
309
|
// session APIs yet. Initialize only when continuation was requested so a
|
|
296
310
|
// normal Workset remains independent of session persistence.
|
|
297
|
-
let continuation:
|
|
311
|
+
let continuation: WorksetContinuation | undefined;
|
|
298
312
|
if (continuationRequested) {
|
|
299
313
|
await initSessionStore(input.dataDir);
|
|
300
314
|
continuation = await getSessionResumeBrief(input.project.id, task, input.reader);
|
|
315
|
+
const { CompactionCheckpointStore } = await import('../store/compaction-checkpoint-store.js');
|
|
316
|
+
const checkpoint = new CompactionCheckpointStore(input.dataDir).findLatestCompleted(
|
|
317
|
+
input.project.id,
|
|
318
|
+
input.excludeCompactionCheckpointFor
|
|
319
|
+
? { excludeSession: input.excludeCompactionCheckpointFor }
|
|
320
|
+
: undefined,
|
|
321
|
+
);
|
|
322
|
+
const completedAtMs = checkpoint ? Date.parse(checkpoint.completedAt ?? checkpoint.preCapturedAt) : Number.NaN;
|
|
323
|
+
if (
|
|
324
|
+
checkpoint
|
|
325
|
+
&& Number.isFinite(completedAtMs)
|
|
326
|
+
&& completedAtMs <= now.getTime()
|
|
327
|
+
&& now.getTime() - completedAtMs <= COMPACT_CHECKPOINT_MAX_AGE_MS
|
|
328
|
+
) {
|
|
329
|
+
continuation.compactCheckpoint = {
|
|
330
|
+
id: checkpoint.id,
|
|
331
|
+
agent: checkpoint.agent,
|
|
332
|
+
captureKind: checkpoint.captureKind === 'native-summary' ? 'native-summary' : 'lifecycle',
|
|
333
|
+
reason: checkpoint.reason,
|
|
334
|
+
...(checkpoint.completedAt ? { completedAt: checkpoint.completedAt } : {}),
|
|
335
|
+
summary: checkpoint.summary
|
|
336
|
+
?? 'The host completed context compaction without exposing a native summary. Reconstruct only what the current task needs from current code and durable evidence.',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
301
339
|
}
|
|
302
340
|
const workset = await buildTaskWorkset({
|
|
303
341
|
projectId: input.project.id,
|
|
@@ -564,7 +602,7 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
|
|
|
564
602
|
);
|
|
565
603
|
|
|
566
604
|
const continuation = context.workset.continuation;
|
|
567
|
-
if (continuation?.previousSession || continuation?.memories.length) {
|
|
605
|
+
if (continuation?.previousSession || continuation?.memories.length || continuation?.compactCheckpoint) {
|
|
568
606
|
lines.push('', 'Resume from prior work');
|
|
569
607
|
if (continuation.previousSession) {
|
|
570
608
|
const session = continuation.previousSession;
|
|
@@ -584,6 +622,14 @@ export function formatAutoProjectContextSummary(context: AutoProjectContext): st
|
|
|
584
622
|
+ detail,
|
|
585
623
|
);
|
|
586
624
|
}
|
|
625
|
+
if (continuation.compactCheckpoint) {
|
|
626
|
+
const checkpoint = continuation.compactCheckpoint;
|
|
627
|
+
lines.push(
|
|
628
|
+
'- Recent host compact checkpoint ('
|
|
629
|
+
+ [checkpoint.agent, checkpoint.captureKind, checkpoint.reason].join(', ')
|
|
630
|
+
+ '): ' + compactContinuationText(checkpoint.summary, 36),
|
|
631
|
+
);
|
|
632
|
+
}
|
|
587
633
|
}
|
|
588
634
|
|
|
589
635
|
return lines.join('\n');
|
package/src/dashboard/server.ts
CHANGED
|
@@ -351,6 +351,17 @@ async function handleApi(
|
|
|
351
351
|
sourceCounts,
|
|
352
352
|
recentObservations: sorted,
|
|
353
353
|
embedding: embeddingStatus,
|
|
354
|
+
// A standalone dashboard has no access to an active MCP
|
|
355
|
+
// process's in-memory Orama index. Keep this explicit so the
|
|
356
|
+
// UI never presents an empty local singleton as a healthy
|
|
357
|
+
// all-vectors-indexed result.
|
|
358
|
+
vectorStatus: {
|
|
359
|
+
available: false,
|
|
360
|
+
total: 0,
|
|
361
|
+
missing: 0,
|
|
362
|
+
missingIds: [],
|
|
363
|
+
backfillRunning: false,
|
|
364
|
+
},
|
|
354
365
|
storage: storageInfo,
|
|
355
366
|
maintenance,
|
|
356
367
|
...(lifecycle ? { lifecycle } : {}),
|