memorix 1.2.1 → 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 (199) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15407 -13779
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1321 -529
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8458 -8087
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +16 -0
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +1349 -535
  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 +24 -4
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/README.md +1 -1
  23. package/docs/dev-log/progress.txt +91 -11
  24. package/package.json +1 -1
  25. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  26. package/src/audit/index.ts +156 -156
  27. package/src/cli/command-guide.ts +192 -0
  28. package/src/cli/commands/audit-list.ts +89 -89
  29. package/src/cli/commands/audit.ts +9 -4
  30. package/src/cli/commands/background.ts +659 -659
  31. package/src/cli/commands/cleanup.ts +5 -1
  32. package/src/cli/commands/codegraph.ts +15 -5
  33. package/src/cli/commands/context.ts +3 -2
  34. package/src/cli/commands/doctor.ts +4 -2
  35. package/src/cli/commands/explain.ts +9 -3
  36. package/src/cli/commands/formation.ts +48 -48
  37. package/src/cli/commands/git-hook-install.ts +111 -111
  38. package/src/cli/commands/handoff.ts +75 -61
  39. package/src/cli/commands/hooks-status.ts +63 -63
  40. package/src/cli/commands/identity.ts +116 -0
  41. package/src/cli/commands/ingest-commit.ts +153 -153
  42. package/src/cli/commands/ingest-image.ts +71 -69
  43. package/src/cli/commands/ingest-log.ts +180 -180
  44. package/src/cli/commands/ingest.ts +44 -44
  45. package/src/cli/commands/integrate-shared.ts +15 -15
  46. package/src/cli/commands/lock.ts +93 -92
  47. package/src/cli/commands/memory.ts +58 -21
  48. package/src/cli/commands/message.ts +123 -118
  49. package/src/cli/commands/operator-shared.ts +98 -3
  50. package/src/cli/commands/poll.ts +74 -64
  51. package/src/cli/commands/purge-all-memory.ts +85 -85
  52. package/src/cli/commands/purge-project-memory.ts +83 -83
  53. package/src/cli/commands/reasoning.ts +135 -121
  54. package/src/cli/commands/retention.ts +9 -4
  55. package/src/cli/commands/serve-http.ts +8 -2
  56. package/src/cli/commands/serve-shared.ts +118 -118
  57. package/src/cli/commands/session.ts +29 -3
  58. package/src/cli/commands/skills.ts +124 -119
  59. package/src/cli/commands/status.ts +4 -3
  60. package/src/cli/commands/task.ts +193 -184
  61. package/src/cli/commands/team.ts +14 -10
  62. package/src/cli/commands/transfer.ts +108 -55
  63. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  64. package/src/cli/identity.ts +89 -0
  65. package/src/cli/index.ts +96 -19
  66. package/src/cli/invocation.ts +115 -0
  67. package/src/cli/tui/ChatView.tsx +234 -234
  68. package/src/cli/tui/CommandBar.tsx +312 -312
  69. package/src/cli/tui/ContextRail.tsx +118 -118
  70. package/src/cli/tui/HeaderBar.tsx +72 -72
  71. package/src/cli/tui/LogoBanner.tsx +51 -51
  72. package/src/cli/tui/Sidebar.tsx +179 -179
  73. package/src/cli/tui/chat-service.ts +41 -18
  74. package/src/cli/tui/data.ts +23 -44
  75. package/src/cli/tui/index.ts +41 -41
  76. package/src/cli/tui/markdown-render.tsx +371 -371
  77. package/src/cli/tui/operator-context.ts +60 -0
  78. package/src/cli/tui/use-mouse.ts +157 -157
  79. package/src/cli/tui/useNavigation.ts +56 -56
  80. package/src/cli/tui/views/MemoryView.tsx +10 -8
  81. package/src/cli/update-checker.ts +211 -211
  82. package/src/cli/version.ts +7 -7
  83. package/src/cli/workbench.ts +1 -1
  84. package/src/codegraph/auto-context.ts +31 -2
  85. package/src/codegraph/context-pack.ts +1 -0
  86. package/src/codegraph/project-context.ts +2 -0
  87. package/src/compact/engine.ts +26 -10
  88. package/src/compact/index-format.ts +25 -2
  89. package/src/compact/token-budget.ts +74 -74
  90. package/src/dashboard/project-classification.ts +64 -64
  91. package/src/dashboard/server.ts +46 -9
  92. package/src/embedding/fastembed-provider.ts +142 -142
  93. package/src/embedding/transformers-provider.ts +111 -111
  94. package/src/git/extractor.ts +209 -209
  95. package/src/git/hooks-path.ts +85 -85
  96. package/src/hooks/admission.ts +117 -0
  97. package/src/hooks/handler.ts +98 -91
  98. package/src/hooks/pattern-detector.ts +173 -173
  99. package/src/hooks/significance-filter.ts +250 -250
  100. package/src/knowledge/context-assembly.ts +97 -0
  101. package/src/knowledge/workset.ts +179 -10
  102. package/src/llm/memory-manager.ts +328 -328
  103. package/src/llm/provider.ts +885 -885
  104. package/src/llm/quality.ts +248 -248
  105. package/src/memory/admission.ts +57 -0
  106. package/src/memory/attribution-guard.ts +249 -249
  107. package/src/memory/consolidation.ts +13 -2
  108. package/src/memory/disclosure-policy.ts +140 -135
  109. package/src/memory/entity-extractor.ts +197 -197
  110. package/src/memory/export-import.ts +11 -3
  111. package/src/memory/formation/evaluate.ts +217 -217
  112. package/src/memory/formation/extract.ts +361 -361
  113. package/src/memory/formation/index.ts +417 -417
  114. package/src/memory/formation/resolve.ts +344 -344
  115. package/src/memory/formation/types.ts +315 -315
  116. package/src/memory/freshness.ts +122 -122
  117. package/src/memory/graph-context.ts +8 -2
  118. package/src/memory/graph.ts +197 -197
  119. package/src/memory/observations.ts +162 -4
  120. package/src/memory/quality-audit.ts +2 -0
  121. package/src/memory/refs.ts +94 -94
  122. package/src/memory/retention.ts +22 -2
  123. package/src/memory/secret-filter.ts +79 -79
  124. package/src/memory/session.ts +5 -2
  125. package/src/memory/visibility.ts +80 -0
  126. package/src/multimodal/image-loader.ts +143 -143
  127. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  128. package/src/orchestrate/adapters/claude.ts +111 -111
  129. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  130. package/src/orchestrate/adapters/codex.ts +41 -41
  131. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  132. package/src/orchestrate/adapters/gemini.ts +42 -42
  133. package/src/orchestrate/adapters/index.ts +73 -73
  134. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  135. package/src/orchestrate/adapters/opencode.ts +47 -47
  136. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  137. package/src/orchestrate/adapters/types.ts +77 -77
  138. package/src/orchestrate/capability-router.ts +284 -284
  139. package/src/orchestrate/context-compact.ts +188 -188
  140. package/src/orchestrate/cost-tracker.ts +219 -219
  141. package/src/orchestrate/error-recovery.ts +191 -191
  142. package/src/orchestrate/evidence.ts +140 -140
  143. package/src/orchestrate/ledger.ts +110 -110
  144. package/src/orchestrate/memorix-bridge.ts +378 -340
  145. package/src/orchestrate/output-budget.ts +80 -80
  146. package/src/orchestrate/permission.ts +152 -152
  147. package/src/orchestrate/pipeline-trace.ts +131 -131
  148. package/src/orchestrate/prompt-builder.ts +155 -155
  149. package/src/orchestrate/ring-buffer.ts +37 -37
  150. package/src/orchestrate/task-graph.ts +389 -389
  151. package/src/orchestrate/worktree.ts +232 -232
  152. package/src/project/aliases.ts +374 -374
  153. package/src/project/detector.ts +268 -268
  154. package/src/rules/adapters/claude-code.ts +99 -99
  155. package/src/rules/adapters/codex.ts +97 -97
  156. package/src/rules/adapters/copilot.ts +124 -124
  157. package/src/rules/adapters/cursor.ts +114 -114
  158. package/src/rules/adapters/kiro.ts +126 -126
  159. package/src/rules/adapters/trae.ts +56 -56
  160. package/src/rules/adapters/windsurf.ts +83 -83
  161. package/src/rules/syncer.ts +235 -235
  162. package/src/runtime/control-plane-maintenance.ts +1 -0
  163. package/src/runtime/isolated-maintenance.ts +1 -0
  164. package/src/runtime/lifecycle.ts +18 -0
  165. package/src/runtime/maintenance-jobs.ts +1 -0
  166. package/src/runtime/maintenance-runner.ts +2 -0
  167. package/src/runtime/project-maintenance.ts +89 -0
  168. package/src/sdk.ts +334 -304
  169. package/src/search/intent-detector.ts +289 -289
  170. package/src/search/query-expansion.ts +52 -52
  171. package/src/server/formation-timeout.ts +27 -27
  172. package/src/server.ts +260 -81
  173. package/src/skills/mini-skills.ts +386 -386
  174. package/src/store/chat-store.ts +119 -119
  175. package/src/store/graph-store.ts +249 -249
  176. package/src/store/mini-skill-store.ts +349 -349
  177. package/src/store/orama-store.ts +61 -6
  178. package/src/store/persistence-json.ts +212 -212
  179. package/src/store/persistence.ts +291 -291
  180. package/src/store/project-affinity.ts +195 -195
  181. package/src/store/sqlite-db.ts +23 -1
  182. package/src/store/sqlite-store.ts +12 -2
  183. package/src/team/event-bus.ts +76 -76
  184. package/src/team/file-locks.ts +173 -173
  185. package/src/team/handoff.ts +168 -161
  186. package/src/team/messages.ts +203 -203
  187. package/src/team/poll.ts +132 -132
  188. package/src/team/tasks.ts +211 -211
  189. package/src/types.ts +51 -0
  190. package/src/wiki/generator.ts +2 -0
  191. package/src/workspace/mcp-adapters/codex.ts +191 -191
  192. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  193. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  194. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  195. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  196. package/src/workspace/mcp-adapters/trae.ts +134 -134
  197. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  198. package/src/workspace/sanitizer.ts +60 -60
  199. 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