memorix 1.2.1 → 1.2.3

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 (90) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/dist/cli/index.js +15424 -13780
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.js +1337 -536
  7. package/dist/index.js.map +1 -1
  8. package/dist/maintenance-runner.d.ts +1 -1
  9. package/dist/maintenance-runner.js +8458 -8087
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +23 -0
  12. package/dist/sdk.d.ts +7 -2
  13. package/dist/sdk.js +1365 -542
  14. package/dist/sdk.js.map +1 -1
  15. package/dist/types.d.ts +49 -1
  16. package/dist/types.js.map +1 -1
  17. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  19. package/docs/API_REFERENCE.md +24 -4
  20. package/docs/README.md +1 -1
  21. package/docs/dev-log/progress.txt +101 -11
  22. package/package.json +1 -1
  23. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  24. package/src/cli/command-guide.ts +192 -0
  25. package/src/cli/commands/audit.ts +9 -4
  26. package/src/cli/commands/cleanup.ts +5 -1
  27. package/src/cli/commands/codegraph.ts +15 -5
  28. package/src/cli/commands/context.ts +3 -2
  29. package/src/cli/commands/doctor.ts +4 -2
  30. package/src/cli/commands/explain.ts +9 -3
  31. package/src/cli/commands/handoff.ts +21 -7
  32. package/src/cli/commands/identity.ts +116 -0
  33. package/src/cli/commands/ingest-image.ts +5 -3
  34. package/src/cli/commands/lock.ts +11 -10
  35. package/src/cli/commands/memory.ts +58 -21
  36. package/src/cli/commands/message.ts +19 -14
  37. package/src/cli/commands/operator-shared.ts +98 -3
  38. package/src/cli/commands/poll.ts +16 -6
  39. package/src/cli/commands/reasoning.ts +17 -3
  40. package/src/cli/commands/retention.ts +9 -4
  41. package/src/cli/commands/serve-http.ts +8 -2
  42. package/src/cli/commands/session.ts +44 -10
  43. package/src/cli/commands/skills.ts +10 -5
  44. package/src/cli/commands/status.ts +4 -3
  45. package/src/cli/commands/task.ts +26 -17
  46. package/src/cli/commands/team.ts +14 -10
  47. package/src/cli/commands/transfer.ts +63 -10
  48. package/src/cli/identity.ts +89 -0
  49. package/src/cli/index.ts +96 -19
  50. package/src/cli/invocation.ts +115 -0
  51. package/src/cli/tui/chat-service.ts +41 -18
  52. package/src/cli/tui/data.ts +23 -44
  53. package/src/cli/tui/operator-context.ts +60 -0
  54. package/src/cli/tui/session-service.ts +3 -2
  55. package/src/cli/tui/views/MemoryView.tsx +10 -8
  56. package/src/codegraph/auto-context.ts +31 -2
  57. package/src/codegraph/context-pack.ts +1 -0
  58. package/src/codegraph/project-context.ts +2 -0
  59. package/src/compact/engine.ts +26 -10
  60. package/src/compact/index-format.ts +25 -2
  61. package/src/dashboard/server.ts +46 -9
  62. package/src/hooks/admission.ts +117 -0
  63. package/src/hooks/handler.ts +98 -91
  64. package/src/knowledge/context-assembly.ts +97 -0
  65. package/src/knowledge/workset.ts +179 -10
  66. package/src/memory/admission.ts +57 -0
  67. package/src/memory/consolidation.ts +13 -2
  68. package/src/memory/disclosure-policy.ts +6 -1
  69. package/src/memory/export-import.ts +11 -3
  70. package/src/memory/graph-context.ts +8 -2
  71. package/src/memory/observations.ts +162 -4
  72. package/src/memory/quality-audit.ts +2 -0
  73. package/src/memory/retention.ts +22 -2
  74. package/src/memory/session.ts +29 -11
  75. package/src/memory/visibility.ts +80 -0
  76. package/src/orchestrate/memorix-bridge.ts +38 -0
  77. package/src/runtime/control-plane-maintenance.ts +1 -0
  78. package/src/runtime/isolated-maintenance.ts +1 -0
  79. package/src/runtime/lifecycle.ts +18 -0
  80. package/src/runtime/maintenance-jobs.ts +1 -0
  81. package/src/runtime/maintenance-runner.ts +2 -0
  82. package/src/runtime/project-maintenance.ts +89 -0
  83. package/src/sdk.ts +35 -5
  84. package/src/server.ts +267 -83
  85. package/src/store/orama-store.ts +61 -6
  86. package/src/store/sqlite-db.ts +23 -1
  87. package/src/store/sqlite-store.ts +12 -2
  88. package/src/team/handoff.ts +7 -0
  89. package/src/types.ts +51 -0
  90. package/src/wiki/generator.ts +2 -0
@@ -6,6 +6,14 @@ import { KnowledgeWorkspaceStore } from './workspace-store.js';
6
6
  import { loadKnowledgeWorkspace } from './workspace.js';
7
7
  import { WorkflowStore } from './workflow-store.js';
8
8
  import { selectWorkflows } from './workflows.js';
9
+ import type {
10
+ ContextCandidateFreshness,
11
+ ContextCandidateKind,
12
+ ContextDeliveryTarget,
13
+ ContextReceipt,
14
+ ContextReceiptOmission,
15
+ ContextReceiptSelection,
16
+ } from './context-assembly.js';
9
17
  import type { CodeGraphProviderQuality, ExternalCodeGraphOutline } from '../codegraph/types.js';
10
18
  import type { KnowledgeClaim, ClaimEvidenceRef } from './types.js';
11
19
  import type { KnowledgePageRecord, KnowledgeWorkspace } from './workspace-types.js';
@@ -98,6 +106,8 @@ export interface TaskWorkset {
98
106
  tokenCount: number;
99
107
  omitted: string[];
100
108
  };
109
+ /** Privacy-safe selection metadata for diagnostics, never appended to the prompt. */
110
+ receipt: ContextReceipt;
101
111
  prompt: string;
102
112
  }
103
113
 
@@ -128,6 +138,8 @@ export interface BuildTaskWorksetInput {
128
138
  };
129
139
  runtimeCautions?: WorksetCaution[];
130
140
  maxTokens?: number;
141
+ /** Delivery surface controls receipt semantics without changing prompt shape. */
142
+ deliveryTarget?: ContextDeliveryTarget;
131
143
  }
132
144
 
133
145
  function unique(values: string[]): string[] {
@@ -228,35 +240,104 @@ function appendLine(
228
240
  maxTokens: number,
229
241
  omitted: string[],
230
242
  omittedKind: string,
243
+ selected?: ContextReceiptSelection[],
244
+ receiptSelection?: ContextReceiptSelection,
231
245
  ): boolean {
232
246
  const next = lines.length ? lines.join('\n') + '\n' + candidate : candidate;
233
247
  if (countTextTokens(next) <= maxTokens) {
234
248
  lines.push(candidate);
249
+ if (receiptSelection) selected?.push(receiptSelection);
235
250
  return true;
236
251
  }
237
252
  omitted.push(omittedKind);
238
253
  return false;
239
254
  }
240
255
 
256
+ function freshnessForMemory(status: WorksetMemorySource['status']): ContextCandidateFreshness {
257
+ if (status === 'current' || status === 'suspect' || status === 'stale') return status;
258
+ return 'unknown';
259
+ }
260
+
261
+ function receiptOmissionKind(raw: string): ContextCandidateKind | undefined {
262
+ if (raw.includes('task')) return 'task';
263
+ if (raw.includes('fact')) return 'current-fact';
264
+ if (raw.includes('state')) return 'code-state';
265
+ if (raw.includes('semantic')) return 'semantic-code';
266
+ if (raw.includes('start')) return 'start-here';
267
+ if (raw.includes('memory')) return 'memory';
268
+ if (raw.includes('claim')) return 'claim';
269
+ if (raw.includes('knowledge-page')) return 'knowledge-page';
270
+ if (raw.includes('workflow')) return 'workflow';
271
+ if (raw.includes('verification')) return 'verification';
272
+ if (raw.includes('caution')) return 'caution';
273
+ return undefined;
274
+ }
275
+
276
+ function receiptOmissions(omitted: string[], hiddenCautionMemoryCount: number): ContextReceiptOmission[] {
277
+ const counts = new Map<ContextCandidateKind, number>();
278
+ for (const raw of omitted) {
279
+ const kind = receiptOmissionKind(raw);
280
+ if (!kind || raw.endsWith('-heading')) continue;
281
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
282
+ }
283
+ const receipt: ContextReceiptOmission[] = [...counts.entries()].map(([kind, count]) => ({
284
+ kind,
285
+ reason: 'token-budget',
286
+ count,
287
+ }));
288
+ if (hiddenCautionMemoryCount > 0) {
289
+ receipt.push({
290
+ kind: 'caution',
291
+ reason: 'hidden-by-task-lens',
292
+ count: hiddenCautionMemoryCount,
293
+ });
294
+ }
295
+ return receipt;
296
+ }
297
+
298
+ function scheduledActions(cautions: WorksetCaution[]): string[] {
299
+ return cautions
300
+ .filter(caution => caution.kind === 'codegraph-refresh-queued')
301
+ .map(caution => short(caution.message, 28));
302
+ }
303
+
241
304
  /**
242
305
  * Render a bounded prompt from whole evidence items. It never cuts a page,
243
306
  * workflow, or claim body into an untraceable partial fragment.
244
307
  */
245
- export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'budget'> & {
308
+ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'budget' | 'receipt'> & {
246
309
  budget?: Partial<TaskWorkset['budget']>;
247
- }): { prompt: string; tokenCount: number; omitted: string[] } {
310
+ }): {
311
+ prompt: string;
312
+ tokenCount: number;
313
+ /** Compatibility summary: unique omission categories. */
314
+ omitted: string[];
315
+ /** Internal receipt input: one entry per candidate that did not fit. */
316
+ omittedItems: string[];
317
+ selected: ContextReceiptSelection[];
318
+ } {
248
319
  const maxTokens = input.budget?.maxTokens ?? 180;
249
320
  const omitted: string[] = [];
321
+ const selected: ContextReceiptSelection[] = [];
250
322
  const lines: string[] = ['Memorix Autopilot Brief'];
251
323
  const task = short(input.task || 'Continue the current task.', 34);
252
- appendLine(lines, 'Task: ' + task, maxTokens, omitted, 'task-detail');
324
+ appendLine(lines, 'Task: ' + task, maxTokens, omitted, 'task-detail', selected, {
325
+ kind: 'task',
326
+ reason: 'current task supplied by the caller',
327
+ trust: 'source-backed',
328
+ });
253
329
  appendLine(lines, 'Task lens: ' + input.lens, maxTokens, omitted, 'lens');
254
330
 
255
331
  if (input.cautions.length > 0 || input.cautionMemory.length > 0) {
256
332
  appendLine(lines, '', maxTokens, omitted, 'caution-heading');
257
333
  appendLine(lines, 'Cautions', maxTokens, omitted, 'caution-heading');
258
334
  for (const caution of input.cautions.slice(0, 6)) {
259
- appendLine(lines, '- ' + short(caution.message, 22), maxTokens, omitted, 'caution');
335
+ appendLine(lines, '- ' + short(caution.message, 22), maxTokens, omitted, 'caution', selected, {
336
+ kind: 'caution',
337
+ id: 'caution:' + caution.kind,
338
+ reason: 'current project caution',
339
+ trust: 'source-backed',
340
+ });
260
341
  }
261
342
  for (const memory of input.cautionMemory.slice(0, 3)) {
262
343
  const location = memory.path
@@ -269,6 +350,14 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
269
350
  maxTokens,
270
351
  omitted,
271
352
  'caution-memory',
353
+ selected,
354
+ {
355
+ kind: 'memory',
356
+ id: 'memory:' + memory.id,
357
+ reason: 'task-relevant memory requiring source verification',
358
+ freshness: freshnessForMemory(memory.status),
359
+ trust: 'historical',
360
+ },
272
361
  );
273
362
  }
274
363
  if (input.hiddenCautionMemoryCount > 0) {
@@ -280,14 +369,23 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
280
369
  appendLine(lines, '', maxTokens, omitted, 'facts-heading');
281
370
  appendLine(lines, 'Current project facts', maxTokens, omitted, 'facts-heading');
282
371
  for (const fact of input.currentFacts.slice(0, 4)) {
283
- appendLine(lines, '- ' + short(fact, 40), maxTokens, omitted, 'current-fact');
372
+ appendLine(lines, '- ' + short(fact, 40), maxTokens, omitted, 'current-fact', selected, {
373
+ kind: 'current-fact',
374
+ reason: 'current project state',
375
+ trust: 'source-backed',
376
+ });
284
377
  }
285
378
  }
286
379
 
287
380
  if (input.codeState) {
288
381
  appendLine(lines, '', maxTokens, omitted, 'state-heading');
289
382
  appendLine(lines, 'Project state', maxTokens, omitted, 'state-heading');
290
- appendLine(lines, input.codeState, maxTokens, omitted, 'code-state');
383
+ appendLine(lines, input.codeState, maxTokens, omitted, 'code-state', selected, {
384
+ kind: 'code-state',
385
+ ...(input.provenance.snapshotId ? { id: 'snapshot:' + input.provenance.snapshotId } : {}),
386
+ reason: 'latest available Code State snapshot',
387
+ trust: 'source-backed',
388
+ });
291
389
  }
292
390
 
293
391
  if (input.semanticCode && (input.semanticCode.entryPoints.length > 0 || input.semanticCode.relations.length > 0)) {
@@ -301,6 +399,13 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
301
399
  maxTokens,
302
400
  omitted,
303
401
  'semantic-relation',
402
+ selected,
403
+ {
404
+ kind: 'semantic-code',
405
+ id: 'code:' + location,
406
+ reason: 'validated optional semantic code relation',
407
+ trust: 'derived',
408
+ },
304
409
  );
305
410
  }
306
411
  if (input.semanticCode.relations.length === 0) {
@@ -312,6 +417,13 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
312
417
  maxTokens,
313
418
  omitted,
314
419
  'semantic-entry',
420
+ selected,
421
+ {
422
+ kind: 'semantic-code',
423
+ id: 'code:' + location,
424
+ reason: 'validated optional semantic code entry point',
425
+ trust: 'derived',
426
+ },
315
427
  );
316
428
  }
317
429
  }
@@ -321,7 +433,12 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
321
433
  appendLine(lines, '', maxTokens, omitted, 'start-heading');
322
434
  appendLine(lines, 'Start here', maxTokens, omitted, 'start-heading');
323
435
  for (const source of input.startHere.slice(0, 5)) {
324
- appendLine(lines, '- ' + source, maxTokens, omitted, 'start-here');
436
+ appendLine(lines, '- ' + source, maxTokens, omitted, 'start-here', selected, {
437
+ kind: 'start-here',
438
+ id: 'path:' + source,
439
+ reason: 'task-lensed starting point',
440
+ trust: 'derived',
441
+ });
325
442
  }
326
443
  }
327
444
 
@@ -338,6 +455,14 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
338
455
  maxTokens,
339
456
  omitted,
340
457
  'reliable-memory',
458
+ selected,
459
+ {
460
+ kind: 'memory',
461
+ id: 'memory:' + memory.id,
462
+ reason: 'current code-bound memory',
463
+ freshness: freshnessForMemory(memory.status),
464
+ trust: 'historical',
465
+ },
341
466
  );
342
467
  }
343
468
  }
@@ -346,10 +471,27 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
346
471
  appendLine(lines, '', maxTokens, omitted, 'knowledge-heading');
347
472
  appendLine(lines, 'Project knowledge', maxTokens, omitted, 'knowledge-heading');
348
473
  for (const claim of input.claims.slice(0, 3)) {
349
- appendLine(lines, '- ' + claim.assertion + ' [' + claim.id + ']', maxTokens, omitted, 'claim');
474
+ appendLine(lines, '- ' + claim.assertion + ' [' + claim.id + ']', maxTokens, omitted, 'claim', selected, {
475
+ kind: 'claim',
476
+ id: 'claim:' + claim.id,
477
+ reason: 'source-qualified task match',
478
+ trust: 'source-backed',
479
+ });
350
480
  }
351
481
  for (const page of input.pages.slice(0, 2)) {
352
- appendLine(lines, '- page: ' + page.relativePath, maxTokens, omitted, 'knowledge-page');
482
+ const supportsDeliveredClaim = page.claimIds.some(claimId => selected.some(item => (
483
+ item.kind === 'claim' && item.id === 'claim:' + claimId
484
+ )));
485
+ if (!supportsDeliveredClaim) {
486
+ omitted.push('knowledge-page-dependency');
487
+ continue;
488
+ }
489
+ appendLine(lines, '- page: ' + page.relativePath, maxTokens, omitted, 'knowledge-page', selected, {
490
+ kind: 'knowledge-page',
491
+ id: 'page:' + page.id,
492
+ reason: 'approved page linked to a selected claim',
493
+ trust: 'source-backed',
494
+ });
353
495
  }
354
496
  }
355
497
 
@@ -363,6 +505,13 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
363
505
  maxTokens,
364
506
  omitted,
365
507
  'workflow',
508
+ selected,
509
+ {
510
+ kind: 'workflow',
511
+ id: 'workflow:' + workflow.id,
512
+ reason: 'task-matching project workflow',
513
+ trust: 'source-backed',
514
+ },
366
515
  );
367
516
  }
368
517
  }
@@ -371,7 +520,11 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
371
520
  appendLine(lines, '', maxTokens, omitted, 'verification-heading');
372
521
  appendLine(lines, 'Verify', maxTokens, omitted, 'verification-heading');
373
522
  for (const check of input.verification.slice(0, 4)) {
374
- appendLine(lines, '- ' + short(check, 20), maxTokens, omitted, 'verification');
523
+ appendLine(lines, '- ' + short(check, 20), maxTokens, omitted, 'verification', selected, {
524
+ kind: 'verification',
525
+ reason: 'task-lensed verification guidance',
526
+ trust: 'derived',
527
+ });
375
528
  }
376
529
  }
377
530
 
@@ -379,6 +532,8 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
379
532
  prompt: lines.join('\n'),
380
533
  tokenCount: countTextTokens(lines.join('\n')),
381
534
  omitted: unique(omitted),
535
+ omittedItems: omitted,
536
+ selected,
382
537
  };
383
538
  }
384
539
 
@@ -388,6 +543,7 @@ export function renderTaskWorksetPrompt(input: Omit<TaskWorkset, 'prompt' | 'bud
388
543
  * context response.
389
544
  */
390
545
  export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<TaskWorkset> {
546
+ const startedAt = Date.now();
391
547
  const task = input.task?.trim() ?? '';
392
548
  const maxTokens = Math.max(96, Math.min(Math.floor(input.maxTokens ?? 180), 320));
393
549
  const cautions = [...(input.runtimeCautions ?? []), ...snapshotCautions(input)];
@@ -503,6 +659,18 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
503
659
  ...base,
504
660
  budget: { maxTokens },
505
661
  });
662
+ const receipt: ContextReceipt = {
663
+ version: '1.2.2',
664
+ target: input.deliveryTarget ?? 'project-context',
665
+ elapsedMs: Math.max(0, Date.now() - startedAt),
666
+ budget: {
667
+ maxTokens,
668
+ tokenCount: rendered.tokenCount,
669
+ },
670
+ selected: rendered.selected,
671
+ omitted: receiptOmissions(rendered.omittedItems, base.hiddenCautionMemoryCount),
672
+ scheduledActions: scheduledActions(normalizedCautions),
673
+ };
506
674
  return {
507
675
  ...base,
508
676
  budget: {
@@ -510,6 +678,7 @@ export async function buildTaskWorkset(input: BuildTaskWorksetInput): Promise<Ta
510
678
  tokenCount: rendered.tokenCount,
511
679
  omitted: rendered.omitted,
512
680
  },
681
+ receipt,
513
682
  prompt: rendered.prompt,
514
683
  };
515
684
  }
@@ -0,0 +1,57 @@
1
+ import type { Observation, ObservationAdmissionState, ObservationType } from '../types.js';
2
+ import { resolveObservationVisibility } from './visibility.js';
3
+
4
+ const DURABLE_AUTOMATIC_TYPES = new Set<ObservationType>([
5
+ 'decision',
6
+ 'gotcha',
7
+ 'problem-solution',
8
+ 'trade-off',
9
+ 'why-it-exists',
10
+ ]);
11
+
12
+ /**
13
+ * Legacy observations predate admission metadata and retain their existing
14
+ * delivery behavior. New automatic captures must explicitly earn delivery.
15
+ */
16
+ export function isEligibleForAutomaticDelivery(observation: Pick<Observation, 'admissionState'>): boolean {
17
+ return observation.admissionState !== 'ephemeral' && observation.admissionState !== 'candidate';
18
+ }
19
+
20
+ export function isCandidateObservation(observation: Pick<Observation, 'admissionState'>): boolean {
21
+ return observation.admissionState === 'candidate';
22
+ }
23
+
24
+ export function isEligibleForKnowledgePromotion(
25
+ observation: Pick<Observation, 'admissionState' | 'valueCategory' | 'visibility'>,
26
+ ): boolean {
27
+ return isEligibleForAutomaticDelivery(observation)
28
+ && observation.valueCategory !== 'ephemeral'
29
+ && resolveObservationVisibility(observation) === 'project';
30
+ }
31
+
32
+ export interface CandidateQualification {
33
+ admissionState: ObservationAdmissionState;
34
+ admissionReason: string;
35
+ }
36
+
37
+ /**
38
+ * Automatic hook records can become default-deliverable only after a current
39
+ * Code Memory link backs a non-ephemeral candidate. This deliberately does
40
+ * not create a claim: claims keep their explicit/Git source boundary.
41
+ */
42
+ export function qualifyCandidateFromCurrentCode(input: {
43
+ observation: Pick<Observation, 'admissionState' | 'valueCategory' | 'type'>;
44
+ currentCodeReferenceCount: number;
45
+ }): CandidateQualification | undefined {
46
+ if (!isCandidateObservation(input.observation)) return undefined;
47
+ if (input.observation.valueCategory === 'ephemeral') return undefined;
48
+ if (input.currentCodeReferenceCount <= 0) return undefined;
49
+
50
+ const typeLabel = DURABLE_AUTOMATIC_TYPES.has(input.observation.type)
51
+ ? 'high-value automatic record'
52
+ : 'automatic record';
53
+ return {
54
+ admissionState: 'qualified',
55
+ admissionReason: `${typeLabel} qualified against ${input.currentCodeReferenceCount} current Code Memory reference(s)`,
56
+ };
57
+ }
@@ -17,6 +17,8 @@
17
17
 
18
18
  import type { Observation } from '../types.js';
19
19
  import { getObservationStore } from '../store/obs-store.js';
20
+ import { isEligibleForAutomaticDelivery } from './admission.js';
21
+ import { resolveObservationVisibility } from './visibility.js';
20
22
 
21
23
  /** Default similarity threshold for merging (0.0-1.0) */
22
24
  const DEFAULT_SIMILARITY_THRESHOLD = 0.45;
@@ -134,10 +136,19 @@ async function loadConsolidationPage(
134
136
  }
135
137
 
136
138
  function findClusters(observations: Observation[], threshold: number): ConsolidationCluster[] {
137
- if (observations.length < MIN_CLUSTER_SIZE) return [];
139
+ // Pending automatic evidence must stay individually inspectable until its
140
+ // source-backed qualification step completes. Consolidating it first would
141
+ // erase the evidence grain the control plane still needs to audit.
142
+ // Consolidation is a project-level maintenance action. Personal notes and
143
+ // targeted handoffs must remain individually inspectable and are never
144
+ // merged by a background job or another agent's manual cleanup.
145
+ const eligible = observations
146
+ .filter(isEligibleForAutomaticDelivery)
147
+ .filter((observation) => resolveObservationVisibility(observation) === 'project');
148
+ if (eligible.length < MIN_CLUSTER_SIZE) return [];
138
149
 
139
150
  const groups = new Map<string, Observation[]>();
140
- for (const obs of observations) {
151
+ for (const obs of eligible) {
141
152
  const key = `${obs.entityName}::${obs.type}`;
142
153
  const group = groups.get(key) ?? [];
143
154
  group.push(obs);
@@ -85,6 +85,7 @@ export function evidenceBasisLine(basis: EvidenceBasis, commitHash?: string): st
85
85
  export interface ProvenanceFields {
86
86
  sourceDetail?: string;
87
87
  valueCategory?: string;
88
+ admissionState?: string;
88
89
  /** Legacy fallback: observations ingested before Phase 1 only have source='git'. */
89
90
  source?: string;
90
91
  }
@@ -112,9 +113,13 @@ export function resolveSourceDetail(
112
113
  * Classify a single observation or index entry into a disclosure layer.
113
114
  */
114
115
  export function classifyLayer(fields: ProvenanceFields): DisclosureLayer {
115
- const { valueCategory } = fields;
116
+ const { valueCategory, admissionState } = fields;
116
117
  const sd = resolveSourceDetail(fields.sourceDetail, fields.source);
117
118
 
119
+ // New automatic records must earn delivery through the control plane.
120
+ // Preserve legacy behavior for records created before admission metadata.
121
+ if (admissionState === 'candidate' || admissionState === 'ephemeral') return 'L1';
122
+
118
123
  // Core-valued memories are always promoted to L2, regardless of source.
119
124
  if (valueCategory === 'core') return 'L2';
120
125
 
@@ -10,8 +10,9 @@
10
10
  * Import: JSON format only (full fidelity restore)
11
11
  */
12
12
 
13
- import type { Observation } from '../types.js';
13
+ import type { Observation, ObservationReader } from '../types.js';
14
14
  import type { Session } from '../types.js';
15
+ import { filterReadableObservations } from './visibility.js';
15
16
  import { getObservationStore } from '../store/obs-store.js';
16
17
  import { getSessionStore } from '../store/session-store.js';
17
18
 
@@ -41,12 +42,18 @@ const OBSERVATION_ICONS: Record<string, string> = {
41
42
  export async function exportAsJson(
42
43
  projectDir: string,
43
44
  projectId: string,
45
+ reader?: ObservationReader,
44
46
  ): Promise<MemorixExport> {
45
47
  const store = getObservationStore();
46
48
  const allObs = await store.loadAll();
47
49
  const allSessions = await getSessionStore().loadAll();
48
50
 
49
- const projectObs = allObs.filter(o => o.projectId === projectId);
51
+ // Agent-facing callers supply a reader; trusted maintenance callers may
52
+ // intentionally omit it for a complete local backup or migration.
53
+ const projectObs = filterReadableObservations(
54
+ allObs.filter(o => o.projectId === projectId),
55
+ reader,
56
+ );
50
57
  const projectSessions = allSessions.filter(s => s.projectId === projectId);
51
58
 
52
59
  // Type breakdown
@@ -75,8 +82,9 @@ export async function exportAsJson(
75
82
  export async function exportAsMarkdown(
76
83
  projectDir: string,
77
84
  projectId: string,
85
+ reader?: ObservationReader,
78
86
  ): Promise<string> {
79
- const data = await exportAsJson(projectDir, projectId);
87
+ const data = await exportAsJson(projectDir, projectId, reader);
80
88
  const lines: string[] = [];
81
89
 
82
90
  lines.push(`# Memorix Export: ${projectId}`);
@@ -1,6 +1,7 @@
1
1
  import { auditMemoryQuality, type MemoryQualityAuditReport } from './quality-audit.js';
2
2
  import type { Observation } from '../types.js';
3
3
  import { getRetentionZone } from './retention.js';
4
+ import { isEligibleForAutomaticDelivery } from './admission.js';
4
5
 
5
6
  export interface GraphContextPacketOptions {
6
7
  projectId: string;
@@ -225,6 +226,8 @@ function scoreForPacket(obs: Observation, queryTokens: string[], referenceTime:
225
226
  source: obs.source ?? 'agent',
226
227
  sourceDetail: obs.sourceDetail ?? '',
227
228
  valueCategory: obs.valueCategory ?? '',
229
+ admissionState: obs.admissionState ?? '',
230
+ admissionReason: obs.admissionReason ?? '',
228
231
  }, referenceTime) !== 'active') {
229
232
  score -= 2;
230
233
  }
@@ -348,8 +351,11 @@ export function buildGraphContextPacket(
348
351
  ...audit.issues.orphans.map((entry) => entry.id),
349
352
  ...audit.issues.retentionCandidates.map((entry) => entry.id),
350
353
  ]);
351
- const filteredObservations = observations.filter((obs) => obs.projectId === options.projectId && !riskIds.has(obs.id));
352
- const baseObservations = filteredObservations.length > 0 ? filteredObservations : observations;
354
+ const deliveryEligible = observations.filter(
355
+ (obs) => obs.projectId === options.projectId && isEligibleForAutomaticDelivery(obs),
356
+ );
357
+ const filteredObservations = deliveryEligible.filter((obs) => !riskIds.has(obs.id));
358
+ const baseObservations = filteredObservations.length > 0 ? filteredObservations : deliveryEligible;
353
359
  const memories = pickMemories(baseObservations, options.projectId, options.query, options.limit ?? 5, referenceTime);
354
360
  const entities = buildEntities(memories);
355
361
  const entityNames = new Set(entities.map((entity) => entity.name));