memorix 1.2.0 → 1.2.2

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 (212) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15919 -14055
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1997 -1021
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8481 -8005
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +30 -1
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +2022 -1024
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +27 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +7 -1
  25. package/docs/dev-log/progress.txt +91 -11
  26. package/docs/knowledge/workflows/memorix-release.md +57 -0
  27. package/package.json +1 -1
  28. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/command-guide.ts +192 -0
  31. package/src/cli/commands/audit-list.ts +89 -89
  32. package/src/cli/commands/audit.ts +9 -4
  33. package/src/cli/commands/background.ts +659 -659
  34. package/src/cli/commands/cleanup.ts +5 -1
  35. package/src/cli/commands/codegraph.ts +17 -8
  36. package/src/cli/commands/context.ts +3 -2
  37. package/src/cli/commands/doctor.ts +4 -2
  38. package/src/cli/commands/explain.ts +9 -3
  39. package/src/cli/commands/formation.ts +48 -48
  40. package/src/cli/commands/git-hook-install.ts +111 -111
  41. package/src/cli/commands/handoff.ts +75 -61
  42. package/src/cli/commands/hooks-status.ts +63 -63
  43. package/src/cli/commands/identity.ts +116 -0
  44. package/src/cli/commands/ingest-commit.ts +153 -153
  45. package/src/cli/commands/ingest-image.ts +71 -69
  46. package/src/cli/commands/ingest-log.ts +180 -180
  47. package/src/cli/commands/ingest.ts +44 -44
  48. package/src/cli/commands/integrate-shared.ts +15 -15
  49. package/src/cli/commands/knowledge.ts +40 -0
  50. package/src/cli/commands/lock.ts +93 -92
  51. package/src/cli/commands/memory.ts +58 -21
  52. package/src/cli/commands/message.ts +123 -118
  53. package/src/cli/commands/operator-shared.ts +98 -3
  54. package/src/cli/commands/poll.ts +74 -64
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +135 -121
  58. package/src/cli/commands/retention.ts +9 -4
  59. package/src/cli/commands/serve-http.ts +22 -43
  60. package/src/cli/commands/serve-shared.ts +118 -118
  61. package/src/cli/commands/session.ts +29 -3
  62. package/src/cli/commands/setup.ts +9 -3
  63. package/src/cli/commands/skills.ts +124 -119
  64. package/src/cli/commands/status.ts +4 -3
  65. package/src/cli/commands/task.ts +193 -184
  66. package/src/cli/commands/team.ts +14 -10
  67. package/src/cli/commands/transfer.ts +108 -55
  68. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  69. package/src/cli/identity.ts +89 -0
  70. package/src/cli/index.ts +96 -19
  71. package/src/cli/invocation.ts +115 -0
  72. package/src/cli/tui/ChatView.tsx +234 -234
  73. package/src/cli/tui/CommandBar.tsx +312 -312
  74. package/src/cli/tui/ContextRail.tsx +118 -118
  75. package/src/cli/tui/HeaderBar.tsx +72 -72
  76. package/src/cli/tui/LogoBanner.tsx +51 -51
  77. package/src/cli/tui/Sidebar.tsx +179 -179
  78. package/src/cli/tui/chat-service.ts +41 -18
  79. package/src/cli/tui/data.ts +23 -44
  80. package/src/cli/tui/index.ts +41 -41
  81. package/src/cli/tui/markdown-render.tsx +371 -371
  82. package/src/cli/tui/operator-context.ts +60 -0
  83. package/src/cli/tui/use-mouse.ts +157 -157
  84. package/src/cli/tui/useNavigation.ts +56 -56
  85. package/src/cli/tui/views/MemoryView.tsx +10 -8
  86. package/src/cli/update-checker.ts +211 -211
  87. package/src/cli/version.ts +7 -7
  88. package/src/cli/workbench.ts +1 -1
  89. package/src/codegraph/auto-context.ts +34 -17
  90. package/src/codegraph/context-pack.ts +1 -0
  91. package/src/codegraph/current-facts.ts +19 -1
  92. package/src/codegraph/project-context.ts +2 -0
  93. package/src/codegraph/task-lens.ts +49 -5
  94. package/src/compact/engine.ts +26 -10
  95. package/src/compact/index-format.ts +25 -2
  96. package/src/compact/token-budget.ts +74 -74
  97. package/src/dashboard/project-classification.ts +64 -64
  98. package/src/dashboard/server.ts +58 -52
  99. package/src/embedding/fastembed-provider.ts +142 -142
  100. package/src/embedding/transformers-provider.ts +111 -111
  101. package/src/git/extractor.ts +209 -209
  102. package/src/git/hooks-path.ts +85 -85
  103. package/src/hooks/admission.ts +117 -0
  104. package/src/hooks/handler.ts +98 -91
  105. package/src/hooks/pattern-detector.ts +173 -173
  106. package/src/hooks/significance-filter.ts +250 -250
  107. package/src/knowledge/claims.ts +51 -1
  108. package/src/knowledge/context-assembly.ts +97 -0
  109. package/src/knowledge/types.ts +1 -0
  110. package/src/knowledge/workflows.ts +34 -3
  111. package/src/knowledge/workset.ts +179 -10
  112. package/src/llm/memory-manager.ts +328 -328
  113. package/src/llm/provider.ts +885 -885
  114. package/src/llm/quality.ts +248 -248
  115. package/src/memory/admission.ts +57 -0
  116. package/src/memory/attribution-guard.ts +249 -249
  117. package/src/memory/auto-relations.ts +21 -0
  118. package/src/memory/consolidation.ts +13 -2
  119. package/src/memory/disclosure-policy.ts +140 -135
  120. package/src/memory/entity-extractor.ts +197 -197
  121. package/src/memory/export-import.ts +11 -3
  122. package/src/memory/formation/evaluate.ts +217 -217
  123. package/src/memory/formation/extract.ts +361 -361
  124. package/src/memory/formation/index.ts +417 -417
  125. package/src/memory/formation/resolve.ts +344 -344
  126. package/src/memory/formation/types.ts +315 -315
  127. package/src/memory/freshness.ts +122 -122
  128. package/src/memory/graph-context.ts +8 -2
  129. package/src/memory/graph-scope.ts +46 -0
  130. package/src/memory/graph.ts +197 -197
  131. package/src/memory/observations.ts +162 -4
  132. package/src/memory/quality-audit.ts +2 -0
  133. package/src/memory/refs.ts +94 -94
  134. package/src/memory/retention.ts +22 -2
  135. package/src/memory/secret-filter.ts +79 -79
  136. package/src/memory/session.ts +5 -2
  137. package/src/memory/visibility.ts +80 -0
  138. package/src/multimodal/image-loader.ts +143 -143
  139. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  140. package/src/orchestrate/adapters/claude.ts +111 -111
  141. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  142. package/src/orchestrate/adapters/codex.ts +41 -41
  143. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  144. package/src/orchestrate/adapters/gemini.ts +42 -42
  145. package/src/orchestrate/adapters/index.ts +73 -73
  146. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  147. package/src/orchestrate/adapters/opencode.ts +47 -47
  148. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  149. package/src/orchestrate/adapters/types.ts +77 -77
  150. package/src/orchestrate/capability-router.ts +284 -284
  151. package/src/orchestrate/context-compact.ts +188 -188
  152. package/src/orchestrate/cost-tracker.ts +219 -219
  153. package/src/orchestrate/error-recovery.ts +191 -191
  154. package/src/orchestrate/evidence.ts +140 -140
  155. package/src/orchestrate/ledger.ts +110 -110
  156. package/src/orchestrate/memorix-bridge.ts +378 -340
  157. package/src/orchestrate/output-budget.ts +80 -80
  158. package/src/orchestrate/permission.ts +152 -152
  159. package/src/orchestrate/pipeline-trace.ts +131 -131
  160. package/src/orchestrate/prompt-builder.ts +155 -155
  161. package/src/orchestrate/ring-buffer.ts +37 -37
  162. package/src/orchestrate/task-graph.ts +389 -389
  163. package/src/orchestrate/verify-gate.ts +33 -10
  164. package/src/orchestrate/worktree.ts +232 -232
  165. package/src/project/aliases.ts +374 -374
  166. package/src/project/detector.ts +268 -268
  167. package/src/rules/adapters/claude-code.ts +99 -99
  168. package/src/rules/adapters/codex.ts +97 -97
  169. package/src/rules/adapters/copilot.ts +124 -124
  170. package/src/rules/adapters/cursor.ts +114 -114
  171. package/src/rules/adapters/kiro.ts +126 -126
  172. package/src/rules/adapters/trae.ts +56 -56
  173. package/src/rules/adapters/windsurf.ts +83 -83
  174. package/src/rules/syncer.ts +235 -235
  175. package/src/runtime/control-plane-maintenance.ts +1 -0
  176. package/src/runtime/isolated-maintenance.ts +1 -0
  177. package/src/runtime/lifecycle.ts +18 -0
  178. package/src/runtime/maintenance-jobs.ts +1 -0
  179. package/src/runtime/maintenance-runner.ts +2 -0
  180. package/src/runtime/project-maintenance.ts +89 -0
  181. package/src/sdk.ts +334 -304
  182. package/src/search/intent-detector.ts +289 -289
  183. package/src/search/query-expansion.ts +52 -52
  184. package/src/server/formation-timeout.ts +27 -27
  185. package/src/server.ts +334 -93
  186. package/src/skills/mini-skills.ts +386 -386
  187. package/src/store/chat-store.ts +119 -119
  188. package/src/store/graph-store.ts +249 -249
  189. package/src/store/mini-skill-store.ts +349 -349
  190. package/src/store/orama-store.ts +61 -6
  191. package/src/store/persistence-json.ts +212 -212
  192. package/src/store/persistence.ts +291 -291
  193. package/src/store/project-affinity.ts +195 -195
  194. package/src/store/sqlite-db.ts +23 -1
  195. package/src/store/sqlite-store.ts +12 -2
  196. package/src/team/event-bus.ts +76 -76
  197. package/src/team/file-locks.ts +173 -173
  198. package/src/team/handoff.ts +168 -161
  199. package/src/team/messages.ts +203 -203
  200. package/src/team/poll.ts +132 -132
  201. package/src/team/tasks.ts +211 -211
  202. package/src/types.ts +51 -0
  203. package/src/wiki/generator.ts +2 -0
  204. package/src/workspace/mcp-adapters/codex.ts +191 -191
  205. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  206. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  207. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  208. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  209. package/src/workspace/mcp-adapters/trae.ts +134 -134
  210. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  211. package/src/workspace/sanitizer.ts +60 -60
  212. package/src/workspace/workflow-sync.ts +131 -131
@@ -8,7 +8,16 @@
8
8
  * and in the Orama search index (for full-text + vector search).
9
9
  */
10
10
 
11
- import type { Observation, ObservationType, ObservationStatus, MemorixDocument, ProgressInfo } from '../types.js';
11
+ import type {
12
+ Observation,
13
+ ObservationAdmissionState,
14
+ ObservationReader,
15
+ ObservationVisibility,
16
+ ObservationType,
17
+ ObservationStatus,
18
+ MemorixDocument,
19
+ ProgressInfo,
20
+ } from '../types.js';
12
21
  import { TOPIC_KEY_FAMILIES } from '../types.js';
13
22
  import {
14
23
  insertObservation,
@@ -25,13 +34,15 @@ import {
25
34
  makeOramaObservationId,
26
35
  getLastSearchMode,
27
36
  searchObservations,
37
+ updateObservationMetadata,
28
38
  } from '../store/orama-store.js';
29
39
  import { getObservationStore, initObservationStore } from '../store/obs-store.js';
30
40
  import { countTextTokens } from '../compact/token-budget.js';
31
41
  import { extractEntities, enrichConcepts } from './entity-extractor.js';
32
42
  import { getEmbeddingProvider, isEmbeddingExplicitlyDisabled } from '../embedding/provider.js';
33
43
  import { sanitizeCredentials } from './secret-filter.js';
34
- import { enqueueClaimDerivation } from '../runtime/lifecycle.js';
44
+ import { enqueueClaimDerivation, enqueueObservationQualification } from '../runtime/lifecycle.js';
45
+ import { canManageObservation, resolveObservationVisibility } from './visibility.js';
35
46
 
36
47
  /** In-memory observation list (loaded from persistence on init) */
37
48
  let observations: Observation[] = [];
@@ -137,6 +148,20 @@ function queueClaimDerivation(observation: Observation): void {
137
148
  }
138
149
  }
139
150
 
151
+ function queueObservationQualification(observation: Observation): void {
152
+ const dataDir = projectDir;
153
+ if (!dataDir || observation.admissionState !== 'candidate') return;
154
+ try {
155
+ enqueueObservationQualification({
156
+ dataDir,
157
+ projectId: observation.projectId,
158
+ source: 'automatic-capture:' + observation.id,
159
+ });
160
+ } catch {
161
+ // Candidate evidence remains available for a later qualification scan.
162
+ }
163
+ }
164
+
140
165
  function isVectorCompatibleWithCurrentIndex(embedding: number[] | null): boolean {
141
166
  if (!embedding) return false;
142
167
  const vectorDimensions = getVectorDimensions();
@@ -253,7 +278,13 @@ export async function storeObservation(input: {
253
278
  relatedEntities?: string[];
254
279
  sourceDetail?: 'explicit' | 'hook' | 'git-ingest';
255
280
  valueCategory?: 'core' | 'contextual' | 'ephemeral';
281
+ admissionState?: ObservationAdmissionState;
282
+ admissionReason?: string;
283
+ visibility?: ObservationVisibility;
284
+ sharedWithAgentIds?: string[];
256
285
  createdByAgentId?: string;
286
+ /** Optional caller-bound write scope for topic-key upserts. */
287
+ visibilityReader?: ObservationReader;
257
288
  }): Promise<{ observation: Observation; upserted: boolean }> {
258
289
  const now = new Date().toISOString();
259
290
 
@@ -273,6 +304,9 @@ export async function storeObservation(input: {
273
304
  o => o.topicKey === input.topicKey && o.projectId === input.projectId,
274
305
  );
275
306
  if (existing) {
307
+ if (input.visibilityReader && !canManageObservation(existing, input.visibilityReader)) {
308
+ throw new Error('Cannot update a memory outside this session\'s write scope.');
309
+ }
276
310
  return { observation: await upsertObservation(existing, input, now), upserted: true };
277
311
  }
278
312
  }
@@ -320,6 +354,9 @@ export async function storeObservation(input: {
320
354
  if (input.topicKey) {
321
355
  const diskExisting = await tx.findByTopicKey(input.projectId, input.topicKey);
322
356
  if (diskExisting) {
357
+ if (input.visibilityReader && !canManageObservation(diskExisting, input.visibilityReader)) {
358
+ throw new Error('Cannot update a memory outside this session\'s write scope.');
359
+ }
323
360
  // Switch to upsert path — update the existing observation after this
324
361
  // short transaction has released its lock.
325
362
  upsertedInsideLock = true;
@@ -355,6 +392,10 @@ export async function storeObservation(input: {
355
392
  relatedEntities: input.relatedEntities,
356
393
  sourceDetail: input.sourceDetail,
357
394
  valueCategory: input.valueCategory,
395
+ admissionState: input.admissionState,
396
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : undefined,
397
+ visibility: input.visibility ?? 'project',
398
+ sharedWithAgentIds: input.sharedWithAgentIds,
358
399
  createdByAgentId: input.createdByAgentId,
359
400
  // Predict the generation that atomic() will commit after this callback.
360
401
  // bumpGeneration() runs after fn(tx) returns, incrementing by 1.
@@ -409,6 +450,10 @@ export async function storeObservation(input: {
409
450
  relatedEntities: input.relatedEntities,
410
451
  sourceDetail: input.sourceDetail,
411
452
  valueCategory: input.valueCategory,
453
+ admissionState: input.admissionState,
454
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : undefined,
455
+ visibility: input.visibility ?? 'project',
456
+ sharedWithAgentIds: input.sharedWithAgentIds,
412
457
  createdByAgentId: input.createdByAgentId,
413
458
  writeGeneration: 0,
414
459
  };
@@ -435,6 +480,11 @@ export async function storeObservation(input: {
435
480
  source: input.source ?? 'agent',
436
481
  sourceDetail: input.sourceDetail ?? '',
437
482
  valueCategory: input.valueCategory ?? '',
483
+ admissionState: input.admissionState ?? '',
484
+ admissionReason: input.admissionReason ? sanitizeCredentials(input.admissionReason) : '',
485
+ visibility: input.visibility ?? 'project',
486
+ createdByAgentId: input.createdByAgentId ?? '',
487
+ sharedWithAgentIds: JSON.stringify(input.sharedWithAgentIds ?? []),
438
488
  };
439
489
 
440
490
  await insertObservation(doc);
@@ -448,7 +498,10 @@ export async function storeObservation(input: {
448
498
  }
449
499
 
450
500
  await bindObservationCodeRefsBestEffort(observation);
451
- queueClaimDerivation(observation);
501
+ queueObservationQualification(observation);
502
+ if (resolveObservationVisibility(observation) === 'project') {
503
+ queueClaimDerivation(observation);
504
+ }
452
505
 
453
506
  // Generate embedding async (fire-and-forget) — never blocks MCP response
454
507
  // Track in vectorMissingIds until embedding is successfully written.
@@ -518,6 +571,10 @@ async function upsertObservation(
518
571
  progress?: ProgressInfo;
519
572
  sourceDetail?: 'explicit' | 'hook' | 'git-ingest';
520
573
  valueCategory?: 'core' | 'contextual' | 'ephemeral';
574
+ admissionState?: ObservationAdmissionState;
575
+ admissionReason?: string;
576
+ visibility?: ObservationVisibility;
577
+ sharedWithAgentIds?: string[];
521
578
  },
522
579
  now: string,
523
580
  ): Promise<Observation> {
@@ -556,6 +613,10 @@ async function upsertObservation(
556
613
  if (input.progress) existing.progress = input.progress;
557
614
  if (input.sourceDetail !== undefined) existing.sourceDetail = input.sourceDetail;
558
615
  if (input.valueCategory !== undefined) existing.valueCategory = input.valueCategory;
616
+ if (input.admissionState !== undefined) existing.admissionState = input.admissionState;
617
+ if (input.admissionReason !== undefined) existing.admissionReason = sanitizeCredentials(input.admissionReason);
618
+ if (input.visibility !== undefined) existing.visibility = input.visibility;
619
+ if (input.sharedWithAgentIds !== undefined) existing.sharedWithAgentIds = input.sharedWithAgentIds;
559
620
 
560
621
  // Re-index in Orama WITHOUT embedding first (non-blocking)
561
622
  const doc: MemorixDocument = {
@@ -577,6 +638,11 @@ async function upsertObservation(
577
638
  source: existing.source ?? 'agent',
578
639
  sourceDetail: existing.sourceDetail ?? '',
579
640
  valueCategory: existing.valueCategory ?? '',
641
+ admissionState: existing.admissionState ?? '',
642
+ admissionReason: existing.admissionReason ?? '',
643
+ visibility: existing.visibility ?? 'project',
644
+ createdByAgentId: existing.createdByAgentId ?? '',
645
+ sharedWithAgentIds: JSON.stringify(existing.sharedWithAgentIds ?? []),
580
646
  };
581
647
 
582
648
  // Remove old doc and insert updated one (with retry for concurrent upsert race)
@@ -603,7 +669,10 @@ async function upsertObservation(
603
669
  }
604
670
 
605
671
  await bindObservationCodeRefsBestEffort(existing);
606
- queueClaimDerivation(existing);
672
+ queueObservationQualification(existing);
673
+ if (resolveObservationVisibility(existing) === 'project') {
674
+ queueClaimDerivation(existing);
675
+ }
607
676
 
608
677
  // Generate embedding async (fire-and-forget) — never blocks MCP response
609
678
  const searchableText = [input.title, input.narrative, ...(input.facts ?? [])].join(' ');
@@ -651,6 +720,79 @@ export function getObservation(id: number, projectId?: string): Observation | un
651
720
  return observations.find((o) => o.id === id && (projectId ? o.projectId === projectId : true));
652
721
  }
653
722
 
723
+ /**
724
+ * Promote or demote an automatic observation without changing its content.
725
+ * The expected-state guard keeps concurrent maintenance workers from
726
+ * resurrecting an observation that another writer has already changed.
727
+ */
728
+ export async function updateObservationAdmission(input: {
729
+ observationId: number;
730
+ projectId?: string;
731
+ expectedState?: ObservationAdmissionState;
732
+ admissionState: ObservationAdmissionState;
733
+ admissionReason: string;
734
+ visibility?: ObservationVisibility;
735
+ }): Promise<Observation | undefined> {
736
+ await ensureFreshObservations();
737
+ const cached = observations.find((observation) =>
738
+ observation.id === input.observationId &&
739
+ (!input.projectId || observation.projectId === input.projectId),
740
+ );
741
+ if (!cached) return undefined;
742
+
743
+ const now = new Date().toISOString();
744
+ const admissionReason = sanitizeCredentials(input.admissionReason).slice(0, 240);
745
+ const apply = (current: Observation): Observation | undefined => {
746
+ if (input.projectId && current.projectId !== input.projectId) return undefined;
747
+ if (input.expectedState && current.admissionState !== input.expectedState) return undefined;
748
+ return {
749
+ ...current,
750
+ admissionState: input.admissionState,
751
+ admissionReason,
752
+ ...(input.visibility !== undefined ? { visibility: input.visibility } : {}),
753
+ updatedAt: now,
754
+ };
755
+ };
756
+
757
+ let updated: Observation | undefined;
758
+ if (projectDir) {
759
+ const store = getObservationStore();
760
+ await store.atomic(async (tx) => {
761
+ const current = await tx.getById(input.observationId);
762
+ if (!current) return;
763
+ const next = apply(current);
764
+ if (!next) return;
765
+ await tx.update(next);
766
+ updated = next;
767
+ });
768
+ } else {
769
+ updated = apply(cached);
770
+ }
771
+
772
+ if (!updated) return undefined;
773
+ observations = observations.map((observation) =>
774
+ observation.id === updated!.id && observation.projectId === updated!.projectId
775
+ ? updated!
776
+ : observation,
777
+ );
778
+
779
+ try {
780
+ await updateObservationMetadata(updated.projectId, updated.id, {
781
+ admissionState: updated.admissionState,
782
+ admissionReason: updated.admissionReason,
783
+ ...(updated.visibility !== undefined ? { visibility: updated.visibility } : {}),
784
+ });
785
+ } catch {
786
+ // SQLite is canonical. The next index hydration repairs a missed update.
787
+ }
788
+
789
+ if (updated.admissionState === 'qualified' && resolveObservationVisibility(updated) === 'project') {
790
+ queueClaimDerivation(updated);
791
+ }
792
+
793
+ return updated;
794
+ }
795
+
654
796
  /**
655
797
  * Resolve observations — mark them as resolved (completed/no longer active).
656
798
  * This prevents resolved memories from appearing in default search results.
@@ -701,6 +843,11 @@ export async function resolveObservations(
701
843
  source: obs.source ?? 'agent',
702
844
  sourceDetail: obs.sourceDetail ?? '',
703
845
  valueCategory: obs.valueCategory ?? '',
846
+ admissionState: obs.admissionState ?? '',
847
+ admissionReason: obs.admissionReason ?? '',
848
+ visibility: obs.visibility ?? 'project',
849
+ createdByAgentId: obs.createdByAgentId ?? '',
850
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
704
851
  };
705
852
  await insertObservation(doc);
706
853
  // Async embedding update (fire-and-forget)
@@ -904,6 +1051,11 @@ export async function reindexObservations(): Promise<number> {
904
1051
  source: obs.source ?? 'agent',
905
1052
  sourceDetail: obs.sourceDetail ?? '',
906
1053
  valueCategory: obs.valueCategory ?? '',
1054
+ admissionState: obs.admissionState ?? '',
1055
+ admissionReason: obs.admissionReason ?? '',
1056
+ visibility: obs.visibility ?? 'project',
1057
+ createdByAgentId: obs.createdByAgentId ?? '',
1058
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
907
1059
  ...(compatibleEmbedding ? { embedding: compatibleEmbedding } : {}),
908
1060
  };
909
1061
  await insertObservation(doc);
@@ -1028,6 +1180,7 @@ export async function probeSearchIndex(projectId: string): Promise<string> {
1028
1180
  await searchObservations({
1029
1181
  query: 'semantic memory retrieval status',
1030
1182
  projectId,
1183
+ reader: { projectId },
1031
1184
  limit: 1,
1032
1185
  status: 'all',
1033
1186
  trackAccess: false,
@@ -1114,6 +1267,11 @@ export async function backfillVectorEmbeddings(options: {
1114
1267
  source: obs.source ?? 'agent',
1115
1268
  sourceDetail: obs.sourceDetail ?? '',
1116
1269
  valueCategory: obs.valueCategory ?? '',
1270
+ admissionState: obs.admissionState ?? '',
1271
+ admissionReason: obs.admissionReason ?? '',
1272
+ visibility: obs.visibility ?? 'project',
1273
+ createdByAgentId: obs.createdByAgentId ?? '',
1274
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
1117
1275
  embedding,
1118
1276
  };
1119
1277
  await insertObservation(doc);
@@ -137,6 +137,8 @@ export function auditMemoryQuality(
137
137
  source: obs.source ?? 'agent',
138
138
  sourceDetail: obs.sourceDetail ?? '',
139
139
  valueCategory: obs.valueCategory ?? '',
140
+ admissionState: obs.admissionState ?? '',
141
+ admissionReason: obs.admissionReason ?? '',
140
142
  }, options.referenceTime) !== 'active')
141
143
  .map((obs) => makeEntry(obs, 'Outside active retention zone'));
142
144
 
@@ -1,94 +1,94 @@
1
- /**
2
- * Typed Memory Reference Protocol (Phase 3a)
3
- *
4
- * Provides a formal, unambiguous way to reference memory objects
5
- * (observations and mini-skills) across internal code and the MCP API.
6
- *
7
- * String format:
8
- * obs:42 — observation #42
9
- * skill:3 — mini-skill #3
10
- * obs:42@org/proj — observation #42 in project org/proj
11
- *
12
- * Legacy support:
13
- * 42 (bare number) → obs:42
14
- * "42" (bare string) → obs:42
15
- *
16
- * Display short forms (presentation only):
17
- * #42 — observation
18
- * S3 — mini-skill
19
- */
20
-
21
- import type { MemoryRef } from '../types.js';
22
-
23
- // ── Parsing ──────────────────────────────────────────────────────
24
-
25
- const TYPED_REF_RE = /^(obs|skill):(\d+)(?:@(.+))?$/;
26
-
27
- /**
28
- * Parse a typed memory reference from a string or number.
29
- *
30
- * Accepts:
31
- * - "obs:42", "skill:3", "obs:42@org/proj"
32
- * - 42 (bare number → obs:42)
33
- * - "42" (bare numeric string → obs:42)
34
- *
35
- * Throws on invalid input.
36
- */
37
- export function parseMemoryRef(input: string | number): MemoryRef {
38
- // Bare number → legacy observation ref
39
- if (typeof input === 'number') {
40
- if (!Number.isInteger(input) || input < 0) {
41
- throw new Error(`Invalid memory ref: ${input} (must be a non-negative integer)`);
42
- }
43
- return { kind: 'obs', id: input };
44
- }
45
-
46
- const trimmed = input.trim();
47
-
48
- // Bare numeric string → legacy observation ref
49
- if (/^\d+$/.test(trimmed)) {
50
- return { kind: 'obs', id: parseInt(trimmed, 10) };
51
- }
52
-
53
- // Typed ref: obs:42 or skill:3 or obs:42@org/proj
54
- const match = trimmed.match(TYPED_REF_RE);
55
- if (!match) {
56
- throw new Error(
57
- `Invalid memory ref: "${input}". Expected format: obs:<id>, skill:<id>, obs:<id>@<projectId>, or a bare number.`,
58
- );
59
- }
60
-
61
- const kind = match[1] as 'obs' | 'skill';
62
- const id = parseInt(match[2], 10);
63
- const projectId = match[3] || undefined;
64
-
65
- return { kind, id, projectId };
66
- }
67
-
68
- // ── Serialization ────────────────────────────────────────────────
69
-
70
- /**
71
- * Serialize a MemoryRef to its canonical string form.
72
- *
73
- * Examples:
74
- * { kind: 'obs', id: 42 } → "obs:42"
75
- * { kind: 'skill', id: 3 } → "skill:3"
76
- * { kind: 'obs', id: 42, projectId: 'o/p' } → "obs:42@o/p"
77
- */
78
- export function serializeMemoryRef(ref: MemoryRef): string {
79
- const base = `${ref.kind}:${ref.id}`;
80
- return ref.projectId ? `${base}@${ref.projectId}` : base;
81
- }
82
-
83
- // ── Display ──────────────────────────────────────────────────────
84
-
85
- /**
86
- * Format a MemoryRef for human-readable display.
87
- *
88
- * Short forms:
89
- * obs:42 → "#42"
90
- * skill:3 → "S3"
91
- */
92
- export function displayRef(ref: MemoryRef): string {
93
- return ref.kind === 'obs' ? `#${ref.id}` : `S${ref.id}`;
94
- }
1
+ /**
2
+ * Typed Memory Reference Protocol (Phase 3a)
3
+ *
4
+ * Provides a formal, unambiguous way to reference memory objects
5
+ * (observations and mini-skills) across internal code and the MCP API.
6
+ *
7
+ * String format:
8
+ * obs:42 — observation #42
9
+ * skill:3 — mini-skill #3
10
+ * obs:42@org/proj — observation #42 in project org/proj
11
+ *
12
+ * Legacy support:
13
+ * 42 (bare number) → obs:42
14
+ * "42" (bare string) → obs:42
15
+ *
16
+ * Display short forms (presentation only):
17
+ * #42 — observation
18
+ * S3 — mini-skill
19
+ */
20
+
21
+ import type { MemoryRef } from '../types.js';
22
+
23
+ // ── Parsing ──────────────────────────────────────────────────────
24
+
25
+ const TYPED_REF_RE = /^(obs|skill):(\d+)(?:@(.+))?$/;
26
+
27
+ /**
28
+ * Parse a typed memory reference from a string or number.
29
+ *
30
+ * Accepts:
31
+ * - "obs:42", "skill:3", "obs:42@org/proj"
32
+ * - 42 (bare number → obs:42)
33
+ * - "42" (bare numeric string → obs:42)
34
+ *
35
+ * Throws on invalid input.
36
+ */
37
+ export function parseMemoryRef(input: string | number): MemoryRef {
38
+ // Bare number → legacy observation ref
39
+ if (typeof input === 'number') {
40
+ if (!Number.isInteger(input) || input < 0) {
41
+ throw new Error(`Invalid memory ref: ${input} (must be a non-negative integer)`);
42
+ }
43
+ return { kind: 'obs', id: input };
44
+ }
45
+
46
+ const trimmed = input.trim();
47
+
48
+ // Bare numeric string → legacy observation ref
49
+ if (/^\d+$/.test(trimmed)) {
50
+ return { kind: 'obs', id: parseInt(trimmed, 10) };
51
+ }
52
+
53
+ // Typed ref: obs:42 or skill:3 or obs:42@org/proj
54
+ const match = trimmed.match(TYPED_REF_RE);
55
+ if (!match) {
56
+ throw new Error(
57
+ `Invalid memory ref: "${input}". Expected format: obs:<id>, skill:<id>, obs:<id>@<projectId>, or a bare number.`,
58
+ );
59
+ }
60
+
61
+ const kind = match[1] as 'obs' | 'skill';
62
+ const id = parseInt(match[2], 10);
63
+ const projectId = match[3] || undefined;
64
+
65
+ return { kind, id, projectId };
66
+ }
67
+
68
+ // ── Serialization ────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Serialize a MemoryRef to its canonical string form.
72
+ *
73
+ * Examples:
74
+ * { kind: 'obs', id: 42 } → "obs:42"
75
+ * { kind: 'skill', id: 3 } → "skill:3"
76
+ * { kind: 'obs', id: 42, projectId: 'o/p' } → "obs:42@o/p"
77
+ */
78
+ export function serializeMemoryRef(ref: MemoryRef): string {
79
+ const base = `${ref.kind}:${ref.id}`;
80
+ return ref.projectId ? `${base}@${ref.projectId}` : base;
81
+ }
82
+
83
+ // ── Display ──────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Format a MemoryRef for human-readable display.
87
+ *
88
+ * Short forms:
89
+ * obs:42 → "#42"
90
+ * skill:3 → "S3"
91
+ */
92
+ export function displayRef(ref: MemoryRef): string {
93
+ return ref.kind === 'obs' ? `#${ref.id}` : `S${ref.id}`;
94
+ }
@@ -15,8 +15,9 @@
15
15
  * retention period but are no longer permanently immune — they decay normally.
16
16
  */
17
17
 
18
- import type { MemorixDocument, Observation } from '../types.js';
18
+ import type { MemorixDocument, Observation, ObservationReader } from '../types.js';
19
19
  import { getObservationStore } from '../store/obs-store.js';
20
+ import { canManageObservation } from './visibility.js';
20
21
 
21
22
  // ── Importance → Retention Period mapping ────────────────────────────
22
23
 
@@ -113,6 +114,10 @@ export function isImmune(doc: MemorixDocument): boolean {
113
114
  // Probe observations are operational heartbeats -- never immune, regardless of valueCategory or access.
114
115
  if (doc.type === 'probe') return false;
115
116
 
117
+ // A candidate has not earned durable status. Never let an automatic core
118
+ // classification turn unqualified capture into permanently retained memory.
119
+ if (doc.admissionState === 'candidate' || doc.admissionState === 'ephemeral') return false;
120
+
116
121
  // formation-classified core memories are immune regardless of type
117
122
  if (doc.valueCategory === 'core') return true;
118
123
 
@@ -135,6 +140,11 @@ export function getImmunityReason(doc: MemorixDocument): string | null {
135
140
  // Probe observations are never immune
136
141
  if (doc.type === 'probe') return null;
137
142
 
143
+ // Automatic traces and candidates have not earned durable status. Keep the
144
+ // explanation aligned with isImmune() even when an earlier classifier gave
145
+ // a candidate the core value category.
146
+ if (doc.admissionState === 'candidate' || doc.admissionState === 'ephemeral') return null;
147
+
138
148
  if (doc.valueCategory === 'core') return 'core valueCategory (formation-classified)';
139
149
  const importance = getImportanceLevel(doc);
140
150
  if (importance === 'critical') return 'critical importance';
@@ -369,6 +379,8 @@ export interface ArchiveExpiredBatchOptions {
369
379
  limit?: number;
370
380
  referenceTime?: Date;
371
381
  accessMap?: Map<number, { accessCount: number; lastAccessedAt: string }>;
382
+ /** Omit only for trusted background maintenance. */
383
+ reader?: ObservationReader;
372
384
  }
373
385
 
374
386
  export interface ArchiveExpiredBatchResult {
@@ -401,6 +413,8 @@ function toRetentionDocument(
401
413
  source: obs.source ?? 'agent',
402
414
  sourceDetail: obs.sourceDetail ?? '',
403
415
  valueCategory: obs.valueCategory ?? '',
416
+ admissionState: obs.admissionState ?? '',
417
+ admissionReason: obs.admissionReason ?? '',
404
418
  };
405
419
  }
406
420
 
@@ -423,6 +437,7 @@ export async function archiveExpiredBatch(
423
437
  const hasMore = page.length > limit;
424
438
  const scanned = hasMore ? page.slice(0, limit) : page;
425
439
  const candidateIds = scanned
440
+ .filter((observation) => !options.reader || canManageObservation(observation, options.reader))
426
441
  .filter((observation) => getRetentionZone(
427
442
  toRetentionDocument(observation, options.accessMap),
428
443
  options.referenceTime,
@@ -461,6 +476,7 @@ export async function archiveExpired(
461
476
  referenceTime?: Date,
462
477
  accessMap?: Map<number, { accessCount: number; lastAccessedAt: string }>,
463
478
  projectId?: string,
479
+ reader?: ObservationReader,
464
480
  ): Promise<{ archived: number; remaining: number }> {
465
481
  const store = getObservationStore();
466
482
  if (projectId) {
@@ -472,12 +488,16 @@ export async function archiveExpired(
472
488
  afterId,
473
489
  referenceTime,
474
490
  accessMap,
491
+ reader,
475
492
  });
476
493
  archived += batch.archived;
477
494
  afterId = batch.nextCursor;
478
495
  } while (afterId !== undefined);
479
496
 
480
- const remaining = await store.countByProject(projectId, { status: 'active' });
497
+ const remainingObservations = await store.loadByProject(projectId, { status: 'active' });
498
+ const remaining = reader
499
+ ? remainingObservations.filter((observation) => canManageObservation(observation, reader)).length
500
+ : remainingObservations.length;
481
501
  return { archived, remaining };
482
502
  }
483
503