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
@@ -9,9 +9,10 @@
9
9
  */
10
10
 
11
11
  import { create, insert, search, remove, update, count, getByID, type AnyOrama } from '@orama/orama';
12
- import type { MemorixDocument, SearchOptions, IndexEntry, KnowledgeLayer } from '../types.js';
12
+ import type { MemorixDocument, SearchOptions, IndexEntry, KnowledgeLayer, ObservationReader } from '../types.js';
13
13
  import { OBSERVATION_ICONS, type ObservationType } from '../types.js';
14
14
  import { resolveKnowledgeLayer } from '../skills/mini-skills.js';
15
+ import { canReadObservation } from '../memory/visibility.js';
15
16
  import { getEmbeddingProvider, type EmbeddingProvider } from '../embedding/provider.js';
16
17
  import { calculateProjectAffinity, extractProjectKeywords, type AffinityContext, type MemoryContent } from './project-affinity.js';
17
18
  import { detectQueryIntent, applyIntentBoost } from '../search/intent-detector.js';
@@ -175,6 +176,11 @@ async function initializeDb(
175
176
  source: 'string' as const,
176
177
  sourceDetail: 'string' as const,
177
178
  valueCategory: 'string' as const,
179
+ admissionState: 'string' as const,
180
+ admissionReason: 'string' as const,
181
+ visibility: 'string' as const,
182
+ createdByAgentId: 'string' as const,
183
+ sharedWithAgentIds: 'string' as const,
178
184
  documentType: 'string' as const,
179
185
  knowledgeLayer: 'string' as const,
180
186
  };
@@ -390,6 +396,13 @@ export async function hydrateIndex(
390
396
  lastAccessedAt: obs.lastAccessedAt || '',
391
397
  status: obs.status ?? 'active',
392
398
  source: obs.source || 'agent',
399
+ sourceDetail: obs.sourceDetail ?? '',
400
+ valueCategory: obs.valueCategory ?? '',
401
+ admissionState: obs.admissionState ?? '',
402
+ admissionReason: obs.admissionReason ?? '',
403
+ visibility: obs.visibility ?? 'project',
404
+ createdByAgentId: obs.createdByAgentId ?? '',
405
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
393
406
  documentType: 'observation',
394
407
  knowledgeLayer: resolveKnowledgeLayer('observation', obs.sourceDetail, obs.source),
395
408
  ...(compatibleVector ? { embedding: compatibleVector } : {}),
@@ -443,6 +456,26 @@ export async function insertObservation(doc: MemorixDocument): Promise<void> {
443
456
  rememberObservationDoc(doc);
444
457
  }
445
458
 
459
+ /**
460
+ * Update retrieval metadata without rebuilding an embedding. Admission is
461
+ * deliberately metadata-only: promotion must not create another embedding
462
+ * request or delay a background qualification job.
463
+ */
464
+ export async function updateObservationMetadata(
465
+ projectId: string,
466
+ observationId: number,
467
+ patch: Partial<Pick<MemorixDocument, 'admissionState' | 'admissionReason' | 'visibility' | 'createdByAgentId' | 'sharedWithAgentIds'>>,
468
+ ): Promise<boolean> {
469
+ const database = await getDb();
470
+ const id = makeOramaObservationId(projectId, observationId);
471
+ const existing = getByID(database, id) as MemorixDocument | undefined;
472
+ if (!existing) return false;
473
+ const next = { ...existing, ...patch };
474
+ await update(database, id, next);
475
+ rememberObservationDoc(next);
476
+ return true;
477
+ }
478
+
446
479
  /**
447
480
  * Remove an observation document by its Orama internal ID.
448
481
  */
@@ -643,6 +676,9 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
643
676
  const doc = hit.document as unknown as MemorixDocument;
644
677
  return projectIds.includes(doc.projectId);
645
678
  })
679
+ // Visibility is enforced before scoring/ranking so private records cannot
680
+ // affect result order, fallback behavior, or downstream detail reads.
681
+ .filter((hit) => canReadObservation(hit.document as unknown as MemorixDocument, options.reader))
646
682
  // Post-filter by status (active/resolved/archived)
647
683
  .filter((hit) => {
648
684
  if (statusFilter === 'all') return true;
@@ -682,6 +718,8 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
682
718
  source: (doc.source || 'agent') as 'agent' | 'git' | 'manual',
683
719
  sourceDetail: (doc.sourceDetail || undefined) as 'explicit' | 'hook' | 'git-ingest' | undefined,
684
720
  valueCategory: (doc.valueCategory || undefined) as 'core' | 'contextual' | 'ephemeral' | undefined,
721
+ admissionState: (doc.admissionState || undefined) as IndexEntry['admissionState'],
722
+ visibility: (doc.visibility || undefined) as IndexEntry['visibility'],
685
723
  entityName: doc.entityName || undefined,
686
724
  documentType: (doc.documentType || 'observation') as 'observation' | 'mini-skill',
687
725
  knowledgeLayer: (doc.knowledgeLayer || 'project-truth') as KnowledgeLayer,
@@ -752,6 +790,16 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
752
790
  }));
753
791
  }
754
792
 
793
+ // Automatic capture is useful as an audit trail, but unqualified evidence
794
+ // should not crowd out durable project knowledge in ordinary retrieval.
795
+ // Keep it available as a fallback when it is all the project has.
796
+ if (hasQuery) {
797
+ const qualifiedEntries = intermediate.filter(
798
+ (entry) => entry.admissionState !== 'candidate' && entry.admissionState !== 'ephemeral',
799
+ );
800
+ if (qualifiedEntries.length > 0) intermediate = qualifiedEntries;
801
+ }
802
+
755
803
  // Re-sort: chronological for WHEN queries, relevance for others
756
804
  if (intentResult?.preferChronological) {
757
805
  intermediate.sort((a, b) => new Date(b.rawTime).getTime() - new Date(a.rawTime).getTime());
@@ -960,7 +1008,8 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
960
1008
  let entries: IndexEntry[] = intermediate.map(({ rawTime: _, _isCommandLog: _c, ...rest }: any) => rest);
961
1009
 
962
1010
  for (const hit of results.hits) {
963
- rememberObservationDoc(hit.document as unknown as MemorixDocument);
1011
+ const doc = hit.document as unknown as MemorixDocument;
1012
+ if (canReadObservation(doc, options.reader)) rememberObservationDoc(doc);
964
1013
  }
965
1014
 
966
1015
  // Explainable recall: annotate entries with match reasons (O(1) lookup via Map)
@@ -1013,7 +1062,10 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
1013
1062
 
1014
1063
  // Record access for returned results (fire-and-forget, non-blocking).
1015
1064
  if (options.trackAccess !== false) {
1016
- const hitDocs = results.hits.map((h) => ({ id: h.id, doc: h.document as unknown as MemorixDocument }));
1065
+ const returnedKeys = new Set(entries.map((entry) => makeEntryKey(entry.projectId, entry.id)));
1066
+ const hitDocs = results.hits
1067
+ .map((hit) => ({ id: hit.id, doc: hit.document as unknown as MemorixDocument }))
1068
+ .filter(({ doc }) => returnedKeys.has(makeEntryKey(doc.projectId, doc.observationId)));
1017
1069
  recordAccessBatch(hitDocs).catch(() => {});
1018
1070
  }
1019
1071
 
@@ -1071,6 +1123,7 @@ export async function getTimeline(
1071
1123
  projectId?: string,
1072
1124
  depthBefore = 3,
1073
1125
  depthAfter = 3,
1126
+ reader?: ObservationReader,
1074
1127
  ): Promise<{ before: IndexEntry[]; anchor: IndexEntry | null; after: IndexEntry[] }> {
1075
1128
  // Use in-memory observations for reliable lookup
1076
1129
  // (Orama search with empty term is unreliable — same fix as compactDetail)
@@ -1079,9 +1132,9 @@ export async function getTimeline(
1079
1132
  const rawObs = await withFreshIndex(() => getAllObservations());
1080
1133
 
1081
1134
  // Filter by project if specified — prevents cross-project context leaking
1082
- const allObs = projectId
1135
+ const allObs = (projectId
1083
1136
  ? rawObs.filter((o) => o.projectId === projectId)
1084
- : rawObs;
1137
+ : rawObs).filter((observation) => canReadObservation(observation, reader));
1085
1138
 
1086
1139
  // Sort by creation time
1087
1140
  const sorted = allObs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
@@ -1093,7 +1146,7 @@ export async function getTimeline(
1093
1146
 
1094
1147
  const toIndexEntry = (obs: {
1095
1148
  id: number; type: string; title: string; tokens: number; createdAt: string;
1096
- source?: string; sourceDetail?: string; valueCategory?: string;
1149
+ source?: string; sourceDetail?: string; valueCategory?: string; admissionState?: string; visibility?: string;
1097
1150
  }): IndexEntry => {
1098
1151
  const obsType = obs.type as ObservationType;
1099
1152
  return {
@@ -1106,6 +1159,8 @@ export async function getTimeline(
1106
1159
  source: (obs.source as IndexEntry['source']) || undefined,
1107
1160
  sourceDetail: (obs.sourceDetail as IndexEntry['sourceDetail']) || undefined,
1108
1161
  valueCategory: (obs.valueCategory as IndexEntry['valueCategory']) || undefined,
1162
+ admissionState: (obs.admissionState as IndexEntry['admissionState']) || undefined,
1163
+ visibility: (obs.visibility as IndexEntry['visibility']) || undefined,
1109
1164
  };
1110
1165
  };
1111
1166
 
@@ -57,7 +57,11 @@ CREATE TABLE IF NOT EXISTS observations (
57
57
  relatedCommits TEXT,
58
58
  relatedEntities TEXT,
59
59
  sourceDetail TEXT,
60
- valueCategory TEXT
60
+ valueCategory TEXT,
61
+ admissionState TEXT,
62
+ admissionReason TEXT,
63
+ visibility TEXT,
64
+ sharedWithAgentIds TEXT
61
65
  );
62
66
  `;
63
67
 
@@ -529,6 +533,8 @@ CREATE INDEX IF NOT EXISTS idx_observations_projectId ON observations(projectId)
529
533
  CREATE INDEX IF NOT EXISTS idx_observations_topicKey ON observations(projectId, topicKey);
530
534
  CREATE INDEX IF NOT EXISTS idx_observations_status ON observations(status);
531
535
  CREATE INDEX IF NOT EXISTS idx_observations_project_status_id ON observations(projectId, status, id);
536
+ CREATE INDEX IF NOT EXISTS idx_observations_project_admission ON observations(projectId, status, admissionState, id);
537
+ CREATE INDEX IF NOT EXISTS idx_observations_project_visibility ON observations(projectId, status, visibility, id);
532
538
  CREATE INDEX IF NOT EXISTS idx_mini_skills_projectId ON mini_skills(projectId);
533
539
  CREATE INDEX IF NOT EXISTS idx_sessions_projectId ON sessions(projectId);
534
540
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(projectId, status);
@@ -590,6 +596,22 @@ function addColumnIfMissing(db: any, table: string, column: string, definition:
590
596
  }
591
597
 
592
598
  const SCHEMA_MIGRATIONS: SchemaMigration[] = [
599
+ {
600
+ id: '1.2.2-observation-admission',
601
+ apply: (db) => {
602
+ addColumnIfMissing(db, 'observations', 'admissionState', 'admissionState TEXT');
603
+ addColumnIfMissing(db, 'observations', 'admissionReason', 'admissionReason TEXT');
604
+ db.exec('CREATE INDEX IF NOT EXISTS idx_observations_project_admission ON observations(projectId, status, admissionState, id)');
605
+ },
606
+ },
607
+ {
608
+ id: '1.2.2-observation-visibility',
609
+ apply: (db) => {
610
+ addColumnIfMissing(db, 'observations', 'visibility', 'visibility TEXT');
611
+ addColumnIfMissing(db, 'observations', 'sharedWithAgentIds', 'sharedWithAgentIds TEXT');
612
+ db.exec('CREATE INDEX IF NOT EXISTS idx_observations_project_visibility ON observations(projectId, status, visibility, id)');
613
+ },
614
+ },
593
615
  {
594
616
  id: '1.2-code-state-snapshots',
595
617
  apply: (db) => {
@@ -49,6 +49,10 @@ function obsToRow(obs: Observation): Record<string, unknown> {
49
49
  relatedEntities: obs.relatedEntities ? JSON.stringify(obs.relatedEntities) : null,
50
50
  sourceDetail: obs.sourceDetail ?? null,
51
51
  valueCategory: obs.valueCategory ?? null,
52
+ admissionState: obs.admissionState ?? null,
53
+ admissionReason: obs.admissionReason ?? null,
54
+ visibility: obs.visibility ?? null,
55
+ sharedWithAgentIds: obs.sharedWithAgentIds ? JSON.stringify(obs.sharedWithAgentIds) : null,
52
56
  createdByAgentId: obs.createdByAgentId ?? null,
53
57
  writeGeneration: obs.writeGeneration ?? 0,
54
58
  };
@@ -80,6 +84,10 @@ function rowToObs(row: any): Observation {
80
84
  ...(row.relatedEntities ? { relatedEntities: safeJsonParse(row.relatedEntities, []) } : {}),
81
85
  ...(row.sourceDetail ? { sourceDetail: row.sourceDetail } : {}),
82
86
  ...(row.valueCategory ? { valueCategory: row.valueCategory } : {}),
87
+ ...(row.admissionState ? { admissionState: row.admissionState } : {}),
88
+ ...(row.admissionReason ? { admissionReason: row.admissionReason } : {}),
89
+ ...(row.visibility ? { visibility: row.visibility } : {}),
90
+ ...(row.sharedWithAgentIds ? { sharedWithAgentIds: safeJsonParse(row.sharedWithAgentIds, []) } : {}),
83
91
  ...(row.createdByAgentId ? { createdByAgentId: row.createdByAgentId } : {}),
84
92
  ...(row.writeGeneration ? { writeGeneration: row.writeGeneration } : {}),
85
93
  } as Observation;
@@ -132,12 +140,14 @@ export class SqliteBackend implements ObservationStore {
132
140
  (id, entityName, type, title, narrative, facts, filesModified, concepts, tokens,
133
141
  createdAt, updatedAt, projectId, hasCausalLanguage, topicKey, revisionCount,
134
142
  sessionId, status, progress, source, commitHash, relatedCommits, relatedEntities,
135
- sourceDetail, valueCategory, createdByAgentId, writeGeneration)
143
+ sourceDetail, valueCategory, admissionState, admissionReason, visibility, sharedWithAgentIds,
144
+ createdByAgentId, writeGeneration)
136
145
  VALUES
137
146
  (@id, @entityName, @type, @title, @narrative, @facts, @filesModified, @concepts, @tokens,
138
147
  @createdAt, @updatedAt, @projectId, @hasCausalLanguage, @topicKey, @revisionCount,
139
148
  @sessionId, @status, @progress, @source, @commitHash, @relatedCommits, @relatedEntities,
140
- @sourceDetail, @valueCategory, @createdByAgentId, @writeGeneration)
149
+ @sourceDetail, @valueCategory, @admissionState, @admissionReason, @visibility, @sharedWithAgentIds,
150
+ @createdByAgentId, @writeGeneration)
141
151
  `);
142
152
  this.stmtUpdate = this.stmtInsert; // INSERT OR REPLACE works for both
143
153
  this.stmtSetStatus = this.db.prepare(`UPDATE observations SET status = ? WHERE id = ?`);
@@ -57,6 +57,8 @@ export async function createHandoffArtifact(
57
57
  topicKey?: string;
58
58
  sourceDetail?: 'explicit' | 'hook' | 'git-ingest';
59
59
  valueCategory?: 'core' | 'contextual' | 'ephemeral';
60
+ visibility?: 'personal' | 'project' | 'team';
61
+ sharedWithAgentIds?: string[];
60
62
  createdByAgentId?: string;
61
63
  }) => Promise<{ observation: { id: number }; upserted: boolean }>,
62
64
  teamStore: TeamStore,
@@ -82,6 +84,11 @@ export async function createHandoffArtifact(
82
84
  topicKey: input.taskId ? `handoff:${input.taskId}:${input.fromAgentId}` : undefined,
83
85
  sourceDetail: 'explicit',
84
86
  valueCategory: 'core', // immune to retention archival
87
+ // A direct handoff is not a project-wide memo. The sender retains access
88
+ // and the explicit recipient receives a read grant. Broadcast handoffs
89
+ // are deliberately visible only to active team members.
90
+ visibility: input.toAgentId ? 'personal' : 'team',
91
+ ...(input.toAgentId ? { sharedWithAgentIds: [input.toAgentId] } : {}),
85
92
  createdByAgentId: input.fromAgentId,
86
93
  });
87
94
 
package/src/types.ts CHANGED
@@ -85,6 +85,33 @@ export const OBSERVATION_ICONS: Record<ObservationType, string> = {
85
85
  /** Observation lifecycle status */
86
86
  export type ObservationStatus = 'active' | 'resolved' | 'archived';
87
87
 
88
+ /**
89
+ * Control-plane admission state for automatically captured observations.
90
+ * Candidate and ephemeral records remain inspectable, but only qualified
91
+ * records are eligible for automatic context delivery. Missing state preserves
92
+ * legacy behavior for observations written before this policy existed.
93
+ */
94
+ export type ObservationAdmissionState = 'ephemeral' | 'candidate' | 'qualified';
95
+
96
+ /**
97
+ * Who may retrieve an observation through agent-facing memory surfaces.
98
+ * Missing visibility is a legacy record and resolves to `project`.
99
+ */
100
+ export type ObservationVisibility = 'personal' | 'project' | 'team';
101
+
102
+ /**
103
+ * Identity available to an agent-facing retrieval call. Internal maintenance
104
+ * deliberately omits this and may inspect all project records for lifecycle work.
105
+ */
106
+ export interface ObservationReader {
107
+ /** Bound project for a normal project-scoped call. Omit only for explicit global search. */
108
+ projectId?: string;
109
+ /** Stable coordination identity, available after an explicit team join. */
110
+ agentId?: string;
111
+ /** True only when agentId is an active member of the bound project team. */
112
+ isTeamMember?: boolean;
113
+ }
114
+
88
115
  /** Progress tracking for task/feature observations */
89
116
  export interface ProgressInfo {
90
117
  feature: string;
@@ -132,6 +159,14 @@ export interface Observation {
132
159
  sourceDetail?: 'explicit' | 'hook' | 'git-ingest';
133
160
  /** Value category from formation pipeline evaluation */
134
161
  valueCategory?: 'core' | 'contextual' | 'ephemeral';
162
+ /** Control-plane admission state for automatic capture and delivery. */
163
+ admissionState?: ObservationAdmissionState;
164
+ /** Short, sanitized explanation for the latest admission decision. */
165
+ admissionReason?: string;
166
+ /** Retrieval scope. Legacy records without a value remain project-visible. */
167
+ visibility?: ObservationVisibility;
168
+ /** Explicit additional readers for a personal record, used by targeted handoffs. */
169
+ sharedWithAgentIds?: string[];
135
170
  /** Phase 4a: Agent ID that created this observation (team attribution) */
136
171
  createdByAgentId?: string;
137
172
  /** Phase 4a: Monotonic write generation — snapshot of storage_generation at write time (watermark coherence) */
@@ -176,6 +211,10 @@ export interface IndexEntry {
176
211
  sourceDetail?: 'explicit' | 'hook' | 'git-ingest';
177
212
  /** Value category for source-aware ranking */
178
213
  valueCategory?: 'core' | 'contextual' | 'ephemeral';
214
+ /** Control-plane admission state when known. */
215
+ admissionState?: ObservationAdmissionState;
216
+ /** Retrieval scope when known. */
217
+ visibility?: ObservationVisibility;
179
218
  /** Explainable recall: why this result matched. */
180
219
  matchedFields?: string[];
181
220
  /** Entity name — used for entity-affinity scoring and workstream deduplication. */
@@ -216,6 +255,8 @@ export interface SearchOptions {
216
255
  source?: 'agent' | 'git' | 'manual';
217
256
  /** Internal observability probes can disable access tracking without affecting normal search behavior. */
218
257
  trackAccess?: boolean;
258
+ /** Internal reader context for agent-facing visibility filtering. */
259
+ reader?: ObservationReader;
219
260
  }
220
261
 
221
262
  /** Topic key family heuristics for suggesting stable topic keys */
@@ -258,6 +299,16 @@ export interface MemorixDocument {
258
299
  sourceDetail?: string;
259
300
  /** Value category from formation evaluation */
260
301
  valueCategory?: string;
302
+ /** Control-plane admission state for automatic capture and delivery. */
303
+ admissionState?: string;
304
+ /** Short, sanitized explanation for the latest admission decision. */
305
+ admissionReason?: string;
306
+ /** Retrieval scope for agent-facing filtering. */
307
+ visibility?: string;
308
+ /** Agent that owns a personal record, or authored a shared record. */
309
+ createdByAgentId?: string;
310
+ /** Explicit readers for a targeted personal record. */
311
+ sharedWithAgentIds?: string;
261
312
  /** Optional vector embedding for semantic/hybrid retrieval */
262
313
  embedding?: number[];
263
314
  /** Document type: observation or mini-skill (Phase 3a) */
@@ -1,4 +1,5 @@
1
1
  import type { Observation, MiniSkill } from '../types.js';
2
+ import { isEligibleForKnowledgePromotion } from '../memory/admission.js';
2
3
  import type { KnowledgeSourceRef, KnowledgeItem, KnowledgeSection, ProjectKnowledgeOverview } from './types.js';
3
4
 
4
5
  const COMMAND_LOG_TITLE = /^(Ran:|Command:|Executed:)\s/i;
@@ -63,6 +64,7 @@ function isEligible(o: Observation, projectId: string): boolean {
63
64
  if (isCommandLog(o)) return false;
64
65
  if (isInactive(o)) return false;
65
66
  if (isOtherProject(o, projectId)) return false;
67
+ if (!isEligibleForKnowledgePromotion(o)) return false;
66
68
  if (o.valueCategory === 'ephemeral') return false;
67
69
  if (!contextualHasSubstance(o)) return false;
68
70
  return true;