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.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +6 -3
  3. package/README.zh-CN.md +5 -2
  4. package/dist/cli/index.js +1272 -105
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +7 -2
  7. package/dist/index.js +809 -52
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +6 -0
  10. package/dist/maintenance-runner.js +204 -25
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +20 -0
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +809 -52
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +38 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.7-CONTEXT-CONTINUITY.md +68 -0
  19. package/docs/API_REFERENCE.md +6 -2
  20. package/docs/dev-log/progress.txt +48 -0
  21. package/docs/hooks-architecture.md +2 -1
  22. package/package.json +3 -3
  23. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  24. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/copilot/memorix/plugin.json +1 -1
  26. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  27. package/plugins/omp/memorix/extensions/memorix.js +17 -0
  28. package/plugins/omp/memorix/package.json +1 -1
  29. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  30. package/plugins/pi/memorix/extensions/memorix.js +17 -0
  31. package/plugins/pi/memorix/package.json +1 -1
  32. package/src/cli/capability-map.ts +1 -0
  33. package/src/cli/command-guide.ts +10 -0
  34. package/src/cli/commands/agent-integrations.ts +102 -5
  35. package/src/cli/commands/checkpoint.ts +144 -0
  36. package/src/cli/commands/serve-http.ts +14 -3
  37. package/src/cli/commands/setup.ts +44 -0
  38. package/src/cli/index.ts +3 -1
  39. package/src/codegraph/auto-context.ts +51 -5
  40. package/src/dashboard/server.ts +11 -0
  41. package/src/hooks/handler.ts +103 -11
  42. package/src/hooks/normalizer.ts +85 -1
  43. package/src/hooks/types.ts +16 -0
  44. package/src/knowledge/workset.ts +43 -2
  45. package/src/memory/compaction.ts +165 -0
  46. package/src/memory/observations.ts +67 -20
  47. package/src/runtime/maintenance-jobs.ts +84 -4
  48. package/src/runtime/project-maintenance.ts +63 -6
  49. package/src/server/tool-profile.ts +1 -0
  50. package/src/server.ts +94 -0
  51. package/src/store/compaction-checkpoint-store.ts +397 -0
  52. package/src/store/sqlite-db.ts +41 -0
  53. 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
- status: issues.length > 0 ? 'repairable' : 'ok',
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 coerceClaudeLocalServer(name: string, value: unknown): MCPServerEntry | null {
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 = coerceClaudeLocalServer('memorix', servers?.memorix);
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
- if (entry.plugin.issues.length > 0) lines.push(` Plugin: ${entry.plugin.issues.join(', ')}`);
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
- lines.push(`Repair: ${report.repairCommand}`);
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 = { total: 0, missing: 0, missingIds: [] as number[], backfillRunning: false };
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
- vectorStatus = getVectorStatus(statsProjectId);
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 { buildTaskWorkset, type TaskWorkset, type WorksetCaution } from '../knowledge/workset.js';
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, type SessionResumeBrief } from '../memory/session.js';
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?: SessionResumeBrief;
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: SessionResumeBrief | undefined;
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');
@@ -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 } : {}),