memorix 1.1.13 → 1.2.1

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 (97) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +6 -3
  3. package/README.zh-CN.md +6 -3
  4. package/dist/cli/index.js +36639 -31279
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +50 -30
  7. package/dist/index.js +6636 -1778
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +3775 -302
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +33 -1
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +6634 -1776
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
  17. package/docs/1.2.0-CODE-STATE.md +61 -0
  18. package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
  19. package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
  20. package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
  21. package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
  22. package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
  23. package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
  24. package/docs/1.2.0-PRODUCT-STORY.md +234 -0
  25. package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
  26. package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
  27. package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
  28. package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
  29. package/docs/API_REFERENCE.md +27 -6
  30. package/docs/CONFIGURATION.md +21 -3
  31. package/docs/DEVELOPMENT.md +4 -0
  32. package/docs/README.md +17 -2
  33. package/docs/SETUP.md +7 -1
  34. package/docs/dev-log/progress.txt +120 -40
  35. package/docs/knowledge/workflows/memorix-release.md +57 -0
  36. package/llms-full.txt +16 -2
  37. package/llms.txt +9 -4
  38. package/package.json +3 -2
  39. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  40. package/src/cli/capability-map.ts +1 -0
  41. package/src/cli/commands/codegraph.ts +111 -9
  42. package/src/cli/commands/context.ts +2 -0
  43. package/src/cli/commands/doctor.ts +73 -4
  44. package/src/cli/commands/knowledge.ts +322 -0
  45. package/src/cli/commands/serve-http.ts +26 -42
  46. package/src/cli/commands/setup.ts +9 -3
  47. package/src/cli/index.ts +3 -1
  48. package/src/cli/tui/App.tsx +1 -1
  49. package/src/cli/tui/Panels.tsx +8 -8
  50. package/src/cli/tui/theme.ts +1 -1
  51. package/src/cli/tui/views/GraphView.tsx +8 -7
  52. package/src/cli/tui/views/KnowledgeView.tsx +9 -9
  53. package/src/codegraph/auto-context.ts +169 -19
  54. package/src/codegraph/code-state.ts +95 -0
  55. package/src/codegraph/context-pack.ts +82 -1
  56. package/src/codegraph/current-facts.ts +19 -1
  57. package/src/codegraph/external-provider.ts +581 -0
  58. package/src/codegraph/lite-provider.ts +64 -19
  59. package/src/codegraph/project-context.ts +9 -1
  60. package/src/codegraph/store.ts +154 -6
  61. package/src/codegraph/task-lens.ts +49 -5
  62. package/src/codegraph/types.ts +117 -0
  63. package/src/config/resolved-config.ts +28 -0
  64. package/src/config/toml-loader.ts +3 -0
  65. package/src/config/yaml-loader.ts +6 -0
  66. package/src/dashboard/server.ts +30 -47
  67. package/src/evaluation/workset-evaluation.ts +120 -0
  68. package/src/hooks/handler.ts +48 -6
  69. package/src/knowledge/claim-store.ts +267 -0
  70. package/src/knowledge/claims.ts +587 -0
  71. package/src/knowledge/markdown.ts +129 -0
  72. package/src/knowledge/types.ts +158 -0
  73. package/src/knowledge/wiki.ts +524 -0
  74. package/src/knowledge/workflow-store.ts +168 -0
  75. package/src/knowledge/workflow-types.ts +95 -0
  76. package/src/knowledge/workflows.ts +774 -0
  77. package/src/knowledge/workset.ts +515 -0
  78. package/src/knowledge/workspace-store.ts +220 -0
  79. package/src/knowledge/workspace-types.ts +106 -0
  80. package/src/knowledge/workspace.ts +220 -0
  81. package/src/memory/auto-relations.ts +21 -0
  82. package/src/memory/graph-scope.ts +46 -0
  83. package/src/memory/observations.ts +19 -0
  84. package/src/orchestrate/verify-gate.ts +33 -10
  85. package/src/runtime/control-plane-maintenance.ts +5 -0
  86. package/src/runtime/isolated-maintenance.ts +5 -0
  87. package/src/runtime/lifecycle-status.ts +102 -0
  88. package/src/runtime/lifecycle.ts +107 -0
  89. package/src/runtime/maintenance-jobs.ts +5 -0
  90. package/src/runtime/project-maintenance.ts +190 -0
  91. package/src/server/tool-profile.ts +3 -2
  92. package/src/server.ts +424 -22
  93. package/src/store/file-lock.ts +24 -4
  94. package/src/store/sqlite-db.ts +307 -0
  95. package/src/wiki/generator.ts +4 -2
  96. package/src/wiki/knowledge-graph.ts +7 -4
  97. package/src/wiki/types.ts +16 -4
@@ -55,6 +55,9 @@ export interface ResolvedMemorixConfig {
55
55
  codegraph: {
56
56
  excludePatterns?: string[];
57
57
  maxFileBytes?: number;
58
+ externalContext: 'auto' | 'off';
59
+ externalCommand?: string;
60
+ externalTimeoutMs?: number;
58
61
  };
59
62
  server: {
60
63
  transport?: 'stdio' | 'http';
@@ -156,6 +159,22 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
156
159
  codegraph: {
157
160
  excludePatterns: firstArray(toml.codegraph?.exclude_patterns, yaml.codegraph?.excludePatterns),
158
161
  maxFileBytes: firstNumber(toml.codegraph?.max_file_bytes, yaml.codegraph?.maxFileBytes),
162
+ externalContext: first(
163
+ normalizeExternalContext(process.env.MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT),
164
+ toml.codegraph?.external_context,
165
+ yaml.codegraph?.externalContext,
166
+ 'auto',
167
+ )!,
168
+ externalCommand: first(
169
+ process.env.MEMORIX_CODEGRAPH_EXTERNAL_COMMAND,
170
+ toml.codegraph?.external_command,
171
+ yaml.codegraph?.externalCommand,
172
+ ),
173
+ externalTimeoutMs: firstNumber(
174
+ parseNumber(process.env.MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS),
175
+ toml.codegraph?.external_timeout_ms,
176
+ yaml.codegraph?.externalTimeoutMs,
177
+ ),
159
178
  },
160
179
  server: {
161
180
  transport: first(toml.server?.transport, yaml.server?.transport),
@@ -254,6 +273,9 @@ function getEnvSourceNames(): string[] {
254
273
  'MEMORIX_EMBEDDING_BASE_URL',
255
274
  'MEMORIX_EMBEDDING_MODEL',
256
275
  'MEMORIX_EMBEDDING_DIMENSIONS',
276
+ 'MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT',
277
+ 'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
278
+ 'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
257
279
  'OPENROUTER_API_KEY',
258
280
  ].filter((name) => process.env[name]);
259
281
  }
@@ -267,3 +289,9 @@ function isOpenRouterUrl(value: string | undefined): boolean {
267
289
  return /(^|\.)openrouter\.ai(?::|\/|$)/i.test(value);
268
290
  }
269
291
  }
292
+
293
+ function normalizeExternalContext(value: string | undefined): 'auto' | 'off' | undefined {
294
+ const normalized = value?.trim().toLowerCase();
295
+ if (normalized === 'auto' || normalized === 'off') return normalized;
296
+ return undefined;
297
+ }
@@ -43,6 +43,9 @@ export interface MemorixTomlConfig {
43
43
  codegraph?: {
44
44
  exclude_patterns?: string[];
45
45
  max_file_bytes?: number;
46
+ external_context?: 'auto' | 'off';
47
+ external_command?: string;
48
+ external_timeout_ms?: number;
46
49
  };
47
50
  server?: {
48
51
  transport?: 'stdio' | 'http';
@@ -70,6 +70,12 @@ export interface MemorixYamlConfig {
70
70
  excludePatterns?: string[];
71
71
  /** Maximum source file size to parse into CodeGraph Memory */
72
72
  maxFileBytes?: number;
73
+ /** Use a healthy pre-existing local CodeGraph index when present (default: auto). */
74
+ externalContext?: 'auto' | 'off';
75
+ /** Optional CodeGraph executable path when it is not available on PATH. */
76
+ externalCommand?: string;
77
+ /** Bound for one local semantic context request. */
78
+ externalTimeoutMs?: number;
73
79
  };
74
80
 
75
81
  /** Behavior settings */
@@ -22,6 +22,7 @@ import { loadDotenv } from '../config/dotenv-loader.js';
22
22
  import { resetDotenv } from '../config/dotenv-loader.js';
23
23
  import { initProjectRoot } from '../config/yaml-loader.js';
24
24
  import { clearProjectRoot } from '../config/yaml-loader.js';
25
+ import { scopeKnowledgeGraphToProject } from '../memory/graph-scope.js';
25
26
 
26
27
  // MIME types for static file serving
27
28
  const MIME_TYPES: Record<string, string> = {
@@ -92,22 +93,15 @@ export function prepareDashboardConfig(projectRoot: string | null): void {
92
93
 
93
94
  /**
94
95
  * Compute project-scoped graph counts from observations.
95
- * Only entities referenced by this project's active observations are counted.
96
+ * Active observation entities and their explicit cross-references are counted.
96
97
  */
97
98
  function computeProjectGraphCounts(
98
99
  allEntities: Array<{ name: string }>,
99
100
  allRelations: Array<{ from: string; to: string }>,
100
- projectObs: Array<{ entityName?: string; status?: string }>,
101
+ projectObs: Array<{ entityName?: string; relatedEntities?: string[]; status?: string }>,
101
102
  ): { entities: number; relations: number; entityNames: Set<string> } {
102
- const entityNames = new Set(
103
- projectObs
104
- .filter(o => (o.status ?? 'active') === 'active' && o.entityName)
105
- .map(o => o.entityName!),
106
- );
107
- const entities = allEntities.filter(e => entityNames.has(e.name));
108
- const entityNameSet = new Set(entities.map(e => e.name));
109
- const relations = allRelations.filter(r => entityNameSet.has(r.from) && entityNameSet.has(r.to));
110
- return { entities: entities.length, relations: relations.length, entityNames };
103
+ const scoped = scopeKnowledgeGraphToProject({ entities: allEntities, relations: allRelations }, projectObs);
104
+ return { entities: scoped.entities.length, relations: scoped.relations.length, entityNames: scoped.entityNames };
111
105
  }
112
106
 
113
107
  /**
@@ -210,17 +204,9 @@ async function handleApi(
210
204
  await initGraphStore(effectiveDataDir);
211
205
  const gStore = getGraphStore();
212
206
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
213
- // Project-scope the graph: only include entities that have observations in this project
214
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ entityName?: string }>;
215
- const projectEntityNames = new Set(
216
- graphObs
217
- .filter(o => o.entityName)
218
- .map(o => o.entityName!),
219
- );
220
- const entities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
221
- const entityNameSet = new Set(entities.map((e: any) => e.name));
222
- const relations = graph.relations.filter((r: any) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
223
- sendJson(res, { entities, relations });
207
+ const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
208
+ const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
209
+ sendJson(res, { entities: scoped.entities, relations: scoped.relations });
224
210
  break;
225
211
  }
226
212
 
@@ -240,11 +226,11 @@ async function handleApi(
240
226
  case '/stats': {
241
227
  await initGraphStore(effectiveDataDir);
242
228
  const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
243
- const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; status?: string }>;
229
+ const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; relatedEntities?: string[]; status?: string }>;
244
230
  const nextId = await getObservationStore().loadIdCounter();
245
231
 
246
232
  // Project-scoped graph counts (must match /api/graph and /api/export)
247
- const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations as Array<{ entityName?: string; status?: string }>);
233
+ const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations);
248
234
 
249
235
  // Type counts (exclude probe -- operational heartbeats, not durable knowledge)
250
236
  const typeCounts: Record<string, number> = {};
@@ -322,9 +308,15 @@ async function handleApi(
322
308
  };
323
309
 
324
310
  let maintenance = { total: 0, pending: 0, running: 0, retrying: 0, completed: 0, failed: 0 };
311
+ let lifecycle: unknown;
325
312
  try {
326
313
  const { MaintenanceJobStore } = await import('../runtime/maintenance-jobs.js');
327
314
  maintenance = new MaintenanceJobStore(effectiveDataDir).summary(effectiveProjectId);
315
+ const { collectLifecycleDiagnostics } = await import('../runtime/lifecycle-status.js');
316
+ lifecycle = await collectLifecycleDiagnostics({
317
+ dataDir: effectiveDataDir,
318
+ projectId: effectiveProjectId,
319
+ });
328
320
  } catch { /* optional maintenance diagnostics */ }
329
321
 
330
322
  sendJson(res, {
@@ -338,6 +330,7 @@ async function handleApi(
338
330
  embedding: embeddingStatus,
339
331
  storage: storageInfo,
340
332
  maintenance,
333
+ ...(lifecycle ? { lifecycle } : {}),
341
334
  gitSummary: {
342
335
  total: gitMemories.length,
343
336
  recentWeek: recentGitCount,
@@ -349,7 +342,10 @@ async function handleApi(
349
342
  }
350
343
 
351
344
  case '/maintenance': {
352
- const { MaintenanceJobStore } = await import('../runtime/maintenance-jobs.js');
345
+ const [{ MaintenanceJobStore }, { collectLifecycleDiagnostics }] = await Promise.all([
346
+ import('../runtime/maintenance-jobs.js'),
347
+ import('../runtime/lifecycle-status.js'),
348
+ ]);
353
349
  const jobs = new MaintenanceJobStore(effectiveDataDir).list({
354
350
  projectId: effectiveProjectId,
355
351
  limit: 50,
@@ -357,6 +353,10 @@ async function handleApi(
357
353
  sendJson(res, {
358
354
  summary: new MaintenanceJobStore(effectiveDataDir).summary(effectiveProjectId),
359
355
  jobs,
356
+ lifecycle: await collectLifecycleDiagnostics({
357
+ dataDir: effectiveDataDir,
358
+ projectId: effectiveProjectId,
359
+ }),
360
360
  });
361
361
  break;
362
362
  }
@@ -447,24 +447,15 @@ async function handleApi(
447
447
  const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
448
448
  const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
449
449
 
450
- // Project-scope graph store (same logic as /api/graph)
451
450
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
452
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ entityName?: string }>;
453
- const projectEntityNames = new Set(
454
- graphObs
455
- .filter(o => o.entityName)
456
- .map(o => o.entityName!),
457
- );
458
- const scopedEntities = fullGraph.entities.filter((e: any) => projectEntityNames.has(e.name));
459
- const scopedEntityNameSet = new Set(scopedEntities.map((e: any) => e.name));
460
- const scopedRelations = fullGraph.relations.filter((r: any) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
451
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
461
452
 
462
453
  const graph = generateKnowledgeGraph({
463
454
  projectId: effectiveProjectId,
464
455
  observations: allObs,
465
456
  miniSkills: skills,
466
- graphEntities: scopedEntities,
467
- graphRelations: scopedRelations,
457
+ graphEntities: scoped.entities,
458
+ graphRelations: scoped.relations,
468
459
  });
469
460
 
470
461
  sendJson(res, graph);
@@ -733,19 +724,11 @@ async function handleApi(
733
724
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
734
725
  const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
735
726
  const nextId = await getObservationStore().loadIdCounter();
736
- // Project-scope the graph: only entities referenced by this project's observations
737
- const exportEntityNames = new Set(
738
- observations
739
- .filter(o => (o.status ?? 'active') === 'active' && o.entityName)
740
- .map(o => o.entityName!),
741
- );
742
- const exportEntities = fullGraph.entities.filter((e: any) => exportEntityNames.has(e.name));
743
- const exportEntitySet = new Set(exportEntities.map((e: any) => e.name));
744
- const exportRelations = fullGraph.relations.filter((r: any) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
727
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, observations);
745
728
  const exportData = {
746
729
  project: { id: effectiveProjectId, name: effectiveProjectName },
747
730
  exportedAt: new Date().toISOString(),
748
- graph: { entities: exportEntities, relations: exportRelations },
731
+ graph: { entities: scoped.entities, relations: scoped.relations },
749
732
  observations,
750
733
  nextId,
751
734
  };
@@ -0,0 +1,120 @@
1
+ import { countTextTokens } from '../compact/token-budget.js';
2
+
3
+ export const WORKSET_VARIANTS = [
4
+ 'memory-only',
5
+ 'current-context',
6
+ 'candidate-workset',
7
+ ] as const;
8
+
9
+ export type WorksetVariant = typeof WORKSET_VARIANTS[number];
10
+
11
+ /**
12
+ * A normalized result shape for evaluating a task context. The production
13
+ * Workset builder will populate this shape in Phase 5; Phase 0 uses it to
14
+ * establish deterministic baseline fixtures without an LLM dependency.
15
+ */
16
+ export interface EvaluatedWorkset {
17
+ variant: WorksetVariant;
18
+ prompt: string;
19
+ startHere: string[];
20
+ evidenceIds: string[];
21
+ cautions: string[];
22
+ }
23
+
24
+ export interface WorksetExpectation {
25
+ requiredStartHere?: string[];
26
+ requiredEvidenceIds?: string[];
27
+ requiredCautions?: string[];
28
+ maxTokens: number;
29
+ maxTokensByVariant?: Partial<Record<WorksetVariant, number>>;
30
+ }
31
+
32
+ export interface WorksetEvaluationFixture {
33
+ id: string;
34
+ title: string;
35
+ task: string;
36
+ expectation: WorksetExpectation;
37
+ }
38
+
39
+ export interface WorksetEvaluationResult {
40
+ fixtureId: string;
41
+ variant: WorksetVariant;
42
+ tokenCount: number;
43
+ tokenBudget: number;
44
+ withinBudget: boolean;
45
+ missingStartHere: string[];
46
+ missingEvidenceIds: string[];
47
+ missingCautions: string[];
48
+ passed: boolean;
49
+ }
50
+
51
+ export interface WorksetComparison {
52
+ fixtureId: string;
53
+ results: WorksetEvaluationResult[];
54
+ byVariant: Partial<Record<WorksetVariant, WorksetEvaluationResult>>;
55
+ }
56
+
57
+ function unique(values: string[]): string[] {
58
+ return [...new Set(values)];
59
+ }
60
+
61
+ function missing(required: string[] | undefined, actual: string[]): string[] {
62
+ const available = new Set(actual);
63
+ return unique(required ?? []).filter(value => !available.has(value));
64
+ }
65
+
66
+ function tokenBudgetFor(
67
+ expectation: WorksetExpectation,
68
+ variant: WorksetVariant,
69
+ ): number {
70
+ return expectation.maxTokensByVariant?.[variant] ?? expectation.maxTokens;
71
+ }
72
+
73
+ /**
74
+ * Evaluate one context against source-backed fixture requirements. This is
75
+ * deliberately mechanical: it reports evidence, safety, and token gaps
76
+ * instead of asking a model to grade another model's answer.
77
+ */
78
+ export function evaluateWorkset(
79
+ fixture: WorksetEvaluationFixture,
80
+ workset: EvaluatedWorkset,
81
+ ): WorksetEvaluationResult {
82
+ const tokenCount = countTextTokens(workset.prompt);
83
+ const missingStartHere = missing(fixture.expectation.requiredStartHere, workset.startHere);
84
+ const missingEvidenceIds = missing(fixture.expectation.requiredEvidenceIds, workset.evidenceIds);
85
+ const missingCautions = missing(fixture.expectation.requiredCautions, workset.cautions);
86
+ const tokenBudget = tokenBudgetFor(fixture.expectation, workset.variant);
87
+ const withinBudget = tokenCount <= tokenBudget;
88
+
89
+ return {
90
+ fixtureId: fixture.id,
91
+ variant: workset.variant,
92
+ tokenCount,
93
+ tokenBudget,
94
+ withinBudget,
95
+ missingStartHere,
96
+ missingEvidenceIds,
97
+ missingCautions,
98
+ passed: withinBudget
99
+ && missingStartHere.length === 0
100
+ && missingEvidenceIds.length === 0
101
+ && missingCautions.length === 0,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Compare memory-only, current-context, and candidate Worksets against the
107
+ * same task requirements. Missing variants are intentionally allowed so a
108
+ * new retrieval implementation can be introduced incrementally.
109
+ */
110
+ export function compareWorksets(
111
+ fixture: WorksetEvaluationFixture,
112
+ worksets: EvaluatedWorkset[],
113
+ ): WorksetComparison {
114
+ const results = worksets.map(workset => evaluateWorkset(fixture, workset));
115
+ const byVariant: Partial<Record<WorksetVariant, WorksetEvaluationResult>> = {};
116
+ for (const result of results) {
117
+ byVariant[result.variant] = result;
118
+ }
119
+ return { fixtureId: fixture.id, results, byVariant };
120
+ }
@@ -298,12 +298,12 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
298
298
  dataDir,
299
299
  observations: activeObservations,
300
300
  refresh: 'auto',
301
- enqueueRefresh: () => import('../runtime/maintenance-jobs.js').then(({ MaintenanceJobStore }) => {
302
- new MaintenanceJobStore(dataDir).enqueue({
301
+ enqueueRefresh: () => import('../runtime/lifecycle.js').then(({ enqueueCodegraphRefresh }) => {
302
+ enqueueCodegraphRefresh({
303
+ dataDir,
303
304
  projectId: canonicalId,
304
- kind: 'codegraph-refresh',
305
- dedupeKey: 'hook-codegraph-refresh',
306
- payload: { maxFiles: 5_000 },
305
+ source: 'hook-session-start',
306
+ maxFiles: 5_000,
307
307
  });
308
308
  }),
309
309
  });
@@ -337,7 +337,7 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
337
337
  * Pipeline: Classify → Policy check → Store → Respond
338
338
  * Pattern detection is used for classification only, not storage gating.
339
339
  */
340
- export async function handleHookEvent(input: NormalizedHookInput): Promise<{
340
+ async function handleHookEventCore(input: NormalizedHookInput): Promise<{
341
341
  observation: ReturnType<typeof buildObservation> | null;
342
342
  output: HookOutput;
343
343
  }> {
@@ -431,6 +431,48 @@ export async function handleHookEvent(input: NormalizedHookInput): Promise<{
431
431
  };
432
432
  }
433
433
 
434
+ /** Queue a later scan after an actual file mutation without scanning in the hook. */
435
+ async function queueCodegraphRefreshForMutation(input: NormalizedHookInput): Promise<void> {
436
+ if (classifyTool(input) !== 'file_modify') return;
437
+ try {
438
+ const [
439
+ { detectProject },
440
+ { getProjectDataDir },
441
+ { initAliasRegistry, registerAlias },
442
+ { enqueueCodegraphRefresh },
443
+ ] = await Promise.all([
444
+ import('../project/detector.js'),
445
+ import('../store/persistence.js'),
446
+ import('../project/aliases.js'),
447
+ import('../runtime/lifecycle.js'),
448
+ ]);
449
+ const project = detectProject(input.cwd || process.cwd());
450
+ if (!project) return;
451
+ const dataDir = await getProjectDataDir(project.id);
452
+ initAliasRegistry(dataDir);
453
+ const projectId = await registerAlias(project);
454
+ enqueueCodegraphRefresh({
455
+ dataDir,
456
+ projectId,
457
+ source: 'hook-file-mutation',
458
+ maxFiles: 5_000,
459
+ });
460
+ } catch {
461
+ // Hooks remain capture-first even if optional Code Memory scheduling fails.
462
+ }
463
+ }
464
+
465
+ export async function handleHookEvent(input: NormalizedHookInput): Promise<{
466
+ observation: ReturnType<typeof buildObservation> | null;
467
+ output: HookOutput;
468
+ }> {
469
+ try {
470
+ return await handleHookEventCore(input);
471
+ } finally {
472
+ await queueCodegraphRefreshForMutation(input);
473
+ }
474
+ }
475
+
434
476
  /**
435
477
  * Convert Memorix's neutral hook response into the host-specific response
436
478
  * schema expected by each supported agent.