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
@@ -3,11 +3,11 @@ import { loadDotenv } from '../../config/dotenv-loader.js';
3
3
  import { initLLM, isLLMEnabled, getLLMConfig, callLLMWithTools } from '../../llm/provider.js';
4
4
  import type { ChatMessage, ToolDefinition, ToolCall } from '../../llm/provider.js';
5
5
  import { initObservations, prepareSearchIndex, storeObservation, resolveObservations, getObservation, getAllObservations } from '../../memory/observations.js';
6
- import type { ObservationType } from '../../types.js';
7
- import { detectProject } from '../../project/detector.js';
6
+ import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
7
+ import type { ObservationReader, ObservationType } from '../../types.js';
8
8
  import { getLastSearchMode } from '../../store/orama-store.js';
9
- import { getProjectDataDir } from '../../store/persistence.js';
10
9
  import type { MemorixDocument } from '../../types.js';
10
+ import { getTuiOperatorContext } from './operator-context.js';
11
11
 
12
12
  export interface ChatHistoryTurn {
13
13
  role: 'user' | 'assistant';
@@ -208,19 +208,29 @@ async function prepareProjectSearch(projectId: string, dataDir: string): Promise
208
208
 
209
209
  interface ToolExecutionContext {
210
210
  projectId: string;
211
+ reader: ObservationReader;
212
+ writerAgentId?: string;
211
213
  collectedSources: ChatSource[];
212
214
  }
213
215
 
216
+ async function resolveTuiContext() {
217
+ try {
218
+ return await getTuiOperatorContext();
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
223
+
214
224
  function executeSearchMemories(args: { query: string; limit?: number }, ctx: ToolExecutionContext): Promise<string> {
215
225
  const limit = Math.min(args.limit ?? SEARCH_LIMIT, 10);
216
- return compactSearch({ query: args.query, limit, projectId: ctx.projectId, status: 'active' })
226
+ return compactSearch({ query: args.query, limit, projectId: ctx.projectId, status: 'active', reader: ctx.reader })
217
227
  .then((result) => {
218
228
  const entries = result.entries.slice(0, DETAIL_LIMIT);
219
229
  if (entries.length === 0) return 'No memories found for that query.';
220
230
 
221
231
  // Collect sources for citation tracking
222
232
  const refs = entries.map((e) => ({ id: e.id, projectId: ctx.projectId }));
223
- return compactDetail(refs).then((detail) => {
233
+ return compactDetail(refs, { reader: ctx.reader }).then((detail) => {
224
234
  for (let i = 0; i < detail.documents.length; i++) {
225
235
  const doc = detail.documents[i];
226
236
  const score = entries[i]?.score ?? 0;
@@ -235,7 +245,7 @@ function executeSearchMemories(args: { query: string; limit?: number }, ctx: Too
235
245
  }
236
246
 
237
247
  function executeGetMemoryDetail(args: { id: number }, ctx: ToolExecutionContext): Promise<string> {
238
- return compactDetail([{ id: args.id, projectId: ctx.projectId }])
248
+ return compactDetail([{ id: args.id, projectId: ctx.projectId }], { reader: ctx.reader })
239
249
  .then((detail) => {
240
250
  if (detail.documents.length === 0) return `Observation ${args.id} not found.`;
241
251
  const doc = detail.documents[0];
@@ -295,6 +305,8 @@ function executeStoreMemory(
295
305
  facts: args.facts,
296
306
  projectId: ctx.projectId,
297
307
  source: 'agent',
308
+ visibilityReader: ctx.reader,
309
+ ...(ctx.writerAgentId ? { createdByAgentId: ctx.writerAgentId } : {}),
298
310
  }).then((result) => {
299
311
  const obs = result.observation;
300
312
  ctx.collectedSources.push({
@@ -314,7 +326,7 @@ function executeUpdateMemory(
314
326
  ctx: ToolExecutionContext,
315
327
  ): string {
316
328
  const obs = getObservation(args.id, ctx.projectId);
317
- if (!obs) return `Observation ${args.id} not found.`;
329
+ if (!obs || !canManageObservation(obs, ctx.reader)) return `Observation ${args.id} not found.`;
318
330
 
319
331
  const shouldAppend = args.append !== false; // default true
320
332
  const newNarrative = shouldAppend
@@ -334,6 +346,7 @@ function executeUpdateMemory(
334
346
  projectId: ctx.projectId,
335
347
  topicKey: obs.topicKey,
336
348
  source: 'agent',
349
+ visibilityReader: ctx.reader,
337
350
  }).catch(() => {/* non-blocking */});
338
351
 
339
352
  return `Updated observation [obs:${args.id}] — ${shouldAppend ? 'appended to' : 'replaced'} narrative${args.facts?.length ? ` and added ${args.facts.length} facts` : ''}. Changes will be persisted asynchronously.`;
@@ -344,7 +357,7 @@ function executeDeleteMemory(
344
357
  ctx: ToolExecutionContext,
345
358
  ): Promise<string> {
346
359
  const obs = getObservation(args.id, ctx.projectId);
347
- if (!obs) return Promise.resolve(`Observation ${args.id} not found.`);
360
+ if (!obs || !canManageObservation(obs, ctx.reader)) return Promise.resolve(`Observation ${args.id} not found.`);
348
361
 
349
362
  return resolveObservations([args.id], 'resolved')
350
363
  .then((result) => {
@@ -361,8 +374,10 @@ function executeListRecentMemories(
361
374
  ctx: ToolExecutionContext,
362
375
  ): string {
363
376
  const limit = Math.min(args.limit ?? 10, 20);
364
- let allObs = getAllObservations()
365
- .filter(o => o.projectId === ctx.projectId && o.status !== 'archived' && o.status !== 'resolved');
377
+ let allObs = filterReadableObservations(
378
+ getAllObservations().filter(o => o.projectId === ctx.projectId && o.status !== 'archived' && o.status !== 'resolved'),
379
+ ctx.reader,
380
+ );
366
381
 
367
382
  if (args.type) {
368
383
  allObs = allObs.filter(o => o.type === args.type);
@@ -399,10 +414,10 @@ export async function askMemoryQuestion(
399
414
  };
400
415
  }
401
416
 
402
- const project = detectProject(process.cwd());
417
+ const operatorContext = await resolveTuiContext();
403
418
 
404
419
  // No project + no LLM → dead end
405
- if (!project) {
420
+ if (!operatorContext) {
406
421
  loadDotenv(process.cwd());
407
422
  initLLM({ scope: 'agent' });
408
423
  if (!isLLMEnabled()) {
@@ -438,10 +453,11 @@ export async function askMemoryQuestion(
438
453
  };
439
454
  }
440
455
 
456
+ const { project, dataDir, reader, identity } = operatorContext;
457
+
441
458
  loadDotenv(project.rootPath);
442
459
  initLLM({ scope: 'agent' });
443
460
 
444
- const dataDir = await getProjectDataDir(project.id);
445
461
  await prepareProjectSearch(project.id, dataDir);
446
462
 
447
463
  const searchMode = normalizeSearchMode(getLastSearchMode(project.id) || 'fulltext');
@@ -453,11 +469,12 @@ export async function askMemoryQuestion(
453
469
  limit: SEARCH_LIMIT,
454
470
  projectId: project.id,
455
471
  status: 'active',
472
+ reader,
456
473
  });
457
474
  const topEntries = searchResult.entries.slice(0, DETAIL_LIMIT);
458
475
  const detailRefs = topEntries.map((entry) => ({ id: entry.id, projectId: project.id }));
459
476
  const detailResult = detailRefs.length > 0
460
- ? await compactDetail(detailRefs)
477
+ ? await compactDetail(detailRefs, { reader })
461
478
  : { documents: [], formatted: '', totalTokens: 0 };
462
479
  const sources = detailResult.documents.map((doc, index) => toSource(doc, topEntries[index]?.score ?? 0));
463
480
 
@@ -488,6 +505,8 @@ export async function askMemoryQuestion(
488
505
 
489
506
  const ctx: ToolExecutionContext = {
490
507
  projectId: project.id,
508
+ reader,
509
+ ...(identity ? { writerAgentId: identity.agentId } : {}),
491
510
  collectedSources: [],
492
511
  };
493
512
 
@@ -584,10 +603,10 @@ export async function askMemoryQuestionStream(
584
603
  };
585
604
  }
586
605
 
587
- const project = detectProject(process.cwd());
606
+ const operatorContext = await resolveTuiContext();
588
607
 
589
608
  // No project + no LLM → dead end
590
- if (!project) {
609
+ if (!operatorContext) {
591
610
  loadDotenv(process.cwd());
592
611
  initLLM({ scope: 'agent' });
593
612
  if (!isLLMEnabled()) {
@@ -626,10 +645,11 @@ export async function askMemoryQuestionStream(
626
645
  };
627
646
  }
628
647
 
648
+ const { project, dataDir, reader, identity } = operatorContext;
649
+
629
650
  loadDotenv(project.rootPath);
630
651
  initLLM({ scope: 'agent' });
631
652
 
632
- const dataDir = await getProjectDataDir(project.id);
633
653
  await prepareProjectSearch(project.id, dataDir);
634
654
 
635
655
  const searchMode = normalizeSearchMode(getLastSearchMode(project.id) || 'fulltext');
@@ -641,11 +661,12 @@ export async function askMemoryQuestionStream(
641
661
  limit: SEARCH_LIMIT,
642
662
  projectId: project.id,
643
663
  status: 'active',
664
+ reader,
644
665
  });
645
666
  const topEntries = searchResult.entries.slice(0, DETAIL_LIMIT);
646
667
  const detailRefs = topEntries.map((entry) => ({ id: entry.id, projectId: project.id }));
647
668
  const detailResult = detailRefs.length > 0
648
- ? await compactDetail(detailRefs)
669
+ ? await compactDetail(detailRefs, { reader })
649
670
  : { documents: [], formatted: '', totalTokens: 0 };
650
671
  const sources = detailResult.documents.map((doc, index) => toSource(doc, topEntries[index]?.score ?? 0));
651
672
 
@@ -672,6 +693,8 @@ export async function askMemoryQuestionStream(
672
693
 
673
694
  const ctx: ToolExecutionContext = {
674
695
  projectId: project.id,
696
+ reader,
697
+ ...(identity ? { writerAgentId: identity.agentId } : {}),
675
698
  collectedSources: [],
676
699
  };
677
700
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import * as fs from 'node:fs';
9
+ import { getTuiOperatorContext } from './operator-context.js';
9
10
 
10
11
  export interface ProjectInfo {
11
12
  name: string;
@@ -98,9 +99,7 @@ function truncate(text: string, max = 60): string {
98
99
 
99
100
  export async function getProjectInfo(): Promise<ProjectInfo | null> {
100
101
  try {
101
- const { detectProject } = await import('../../project/detector.js');
102
- const proj = detectProject(process.cwd());
103
- if (!proj) return null;
102
+ const { project: proj } = await getTuiOperatorContext();
104
103
  return {
105
104
  name: proj.name,
106
105
  id: proj.id,
@@ -127,12 +126,8 @@ export async function getHealthInfo(projectId?: string): Promise<HealthInfo> {
127
126
  };
128
127
 
129
128
  try {
130
- const { detectProject } = await import('../../project/detector.js');
131
- const { getProjectDataDir } = await import('../../store/persistence.js');
132
129
  const { initObservationStore: initStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
133
-
134
- const proj = detectProject(process.cwd());
135
- if (!proj) return defaults;
130
+ const { project: proj, dataDir, reader } = await getTuiOperatorContext();
136
131
 
137
132
  // Load .env BEFORE any process.env reads or provider initialization (#46)
138
133
  try {
@@ -141,11 +136,11 @@ export async function getHealthInfo(projectId?: string): Promise<HealthInfo> {
141
136
  } catch { /* best-effort */ }
142
137
 
143
138
  const effectiveProjectId = projectId || proj.id;
144
- const dataDir = await getProjectDataDir(effectiveProjectId);
139
+ if (effectiveProjectId !== proj.id) return defaults;
145
140
  await initStore(dataDir);
146
141
  const allObs = (await getStore().loadAll()) as any[];
147
- // Filter by project flat storage shares one observations.json across projects
148
- const obs = allObs.filter((o: any) => o.projectId === effectiveProjectId);
142
+ const { filterReadableObservations } = await import('../../memory/visibility.js');
143
+ const obs = filterReadableObservations(allObs, reader);
149
144
  const active = obs.filter((o: any) => (o.status ?? 'active') === 'active');
150
145
 
151
146
  defaults.totalMemories = obs.length;
@@ -219,19 +214,15 @@ export async function getHealthInfo(projectId?: string): Promise<HealthInfo> {
219
214
 
220
215
  export async function getRecentMemories(limit = 8, projectId?: string): Promise<MemoryItem[]> {
221
216
  try {
222
- const { detectProject } = await import('../../project/detector.js');
223
- const { getProjectDataDir } = await import('../../store/persistence.js');
224
217
  const { initObservationStore: initStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
225
-
226
- const proj = detectProject(process.cwd());
227
- if (!proj) return [];
218
+ const { project: proj, dataDir, reader } = await getTuiOperatorContext();
228
219
 
229
220
  const effectiveProjectId = projectId || proj.id;
230
- const dataDir = await getProjectDataDir(effectiveProjectId);
221
+ if (effectiveProjectId !== proj.id) return [];
231
222
  await initStore(dataDir);
232
223
  const allObs = (await getStore().loadAll()) as any[];
233
- // Filter by project flat storage shares one observations.json
234
- const projectObs = allObs.filter((o: any) => o.projectId === effectiveProjectId);
224
+ const { filterReadableObservations } = await import('../../memory/visibility.js');
225
+ const projectObs = filterReadableObservations(allObs, reader);
235
226
  const active = projectObs.filter((o: any) => (o.status ?? 'active') === 'active');
236
227
  const filtered = active.filter((o: any) => !/^(Ran:|Command:|Executed:)\s/i.test(o.title || ''));
237
228
 
@@ -251,18 +242,13 @@ export async function getRecentMemories(limit = 8, projectId?: string): Promise<
251
242
  export async function searchMemories(query: string, limit = 10): Promise<SearchResult[]> {
252
243
  try {
253
244
  const { searchObservations } = await import('../../store/orama-store.js');
254
- const { getProjectDataDir } = await import('../../store/persistence.js');
255
- const { detectProject } = await import('../../project/detector.js');
256
245
  const { initObservations, prepareSearchIndex } = await import('../../memory/observations.js');
246
+ const { project: proj, dataDir, reader } = await getTuiOperatorContext();
257
247
 
258
- const proj = detectProject(process.cwd());
259
- if (!proj) return [];
260
-
261
- const dataDir = await getProjectDataDir(proj.id);
262
248
  await initObservations(dataDir);
263
249
  await prepareSearchIndex();
264
250
 
265
- const results = await searchObservations({ query, limit, projectId: proj.id });
251
+ const results = await searchObservations({ query, limit, projectId: proj.id, reader });
266
252
 
267
253
  const typeIcons: Record<string, string> = {
268
254
  gotcha: '!',
@@ -290,15 +276,10 @@ export async function searchMemories(query: string, limit = 10): Promise<SearchR
290
276
 
291
277
  export async function storeQuickMemory(text: string): Promise<{ id: number; title: string } | null> {
292
278
  try {
293
- const { detectProject } = await import('../../project/detector.js');
294
- const { getProjectDataDir } = await import('../../store/persistence.js');
295
279
  const { initObservations, storeObservation } = await import('../../memory/observations.js');
296
280
  const { initObservationStore } = await import('../../store/obs-store.js');
281
+ const { project: proj, dataDir, reader, identity } = await getTuiOperatorContext();
297
282
 
298
- const proj = detectProject(process.cwd());
299
- if (!proj) return null;
300
-
301
- const dataDir = await getProjectDataDir(proj.id);
302
283
  await initObservationStore(dataDir);
303
284
  await initObservations(dataDir);
304
285
 
@@ -310,6 +291,8 @@ export async function storeQuickMemory(text: string): Promise<{ id: number; titl
310
291
  facts: [],
311
292
  projectId: proj.id,
312
293
  sourceDetail: 'explicit',
294
+ visibilityReader: reader,
295
+ ...(identity ? { createdByAgentId: identity.agentId } : {}),
313
296
  });
314
297
 
315
298
  return { id: result.observation.id, title: text.slice(0, 100) };
@@ -484,23 +467,21 @@ export async function getKnowledgeGraph(
484
467
  projectId?: string,
485
468
  ): Promise<import('../../wiki/types.js').ProjectKnowledgeGraph | null> {
486
469
  try {
487
- const { detectProject } = await import('../../project/detector.js');
488
- const { getProjectDataDir } = await import('../../store/persistence.js');
489
470
  const { initObservationStore } = await import('../../store/obs-store.js');
490
471
  const { initObservations, getAllObservations } = await import('../../memory/observations.js');
472
+ const { filterReadableObservations } = await import('../../memory/visibility.js');
491
473
  const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
492
474
  const { generateKnowledgeGraph } = await import('../../wiki/knowledge-graph.js');
493
475
 
494
- const proj = detectProject(process.cwd());
495
- if (!proj) return null;
476
+ const { project: proj, dataDir, reader } = await getTuiOperatorContext();
496
477
 
497
478
  const effectiveProjectId = projectId || proj.id;
498
- const dataDir = await getProjectDataDir(effectiveProjectId);
479
+ if (effectiveProjectId !== proj.id) return null;
499
480
  await initObservationStore(dataDir);
500
481
  await initObservations(dataDir);
501
482
  await initMiniSkillStore(dataDir);
502
483
 
503
- const observations = getAllObservations();
484
+ const observations = filterReadableObservations(getAllObservations(), reader);
504
485
  const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
505
486
 
506
487
  return generateKnowledgeGraph({
@@ -515,23 +496,21 @@ export async function getKnowledgeGraph(
515
496
 
516
497
  export async function getKnowledgeBase(projectId?: string): Promise<import('../../wiki/types.js').ProjectKnowledgeOverview | null> {
517
498
  try {
518
- const { detectProject } = await import('../../project/detector.js');
519
- const { getProjectDataDir } = await import('../../store/persistence.js');
520
499
  const { initObservationStore } = await import('../../store/obs-store.js');
521
500
  const { initObservations, getAllObservations } = await import('../../memory/observations.js');
501
+ const { filterReadableObservations } = await import('../../memory/visibility.js');
522
502
  const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
523
503
  const { generateKnowledgeBase } = await import('../../wiki/generator.js');
524
504
 
525
- const proj = detectProject(process.cwd());
526
- if (!proj) return null;
505
+ const { project: proj, dataDir, reader } = await getTuiOperatorContext();
527
506
 
528
507
  const effectiveProjectId = projectId || proj.id;
529
- const dataDir = await getProjectDataDir(effectiveProjectId);
508
+ if (effectiveProjectId !== proj.id) return null;
530
509
  await initObservationStore(dataDir);
531
510
  await initObservations(dataDir);
532
511
  await initMiniSkillStore(dataDir);
533
512
 
534
- const observations = getAllObservations();
513
+ const observations = filterReadableObservations(getAllObservations(), reader);
535
514
  const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
536
515
 
537
516
  return generateKnowledgeBase({
@@ -0,0 +1,60 @@
1
+ import { initTeamStore } from '../../team/team-store.js';
2
+ import { loadCliIdentity, resolveCliIdentity } from '../identity.js';
3
+ import { getCliInvocation } from '../invocation.js';
4
+ import type { CliProjectContext } from '../commands/operator-shared.js';
5
+
6
+ export type TuiOperatorContext = Omit<CliProjectContext, 'teamStore'>;
7
+
8
+ /**
9
+ * The Ink workbench is another CLI entry point, not an alternate security
10
+ * model. It uses the same identity resolver, but avoids opening coordination
11
+ * state until an actor was explicitly selected.
12
+ */
13
+ export async function getTuiOperatorContext(): Promise<TuiOperatorContext> {
14
+ const invocation = getCliInvocation();
15
+ const { detectProject } = await import('../../project/detector.js');
16
+ const project = detectProject(
17
+ invocation.projectRoot
18
+ ?? process.env.MEMORIX_PROJECT_ROOT
19
+ ?? process.cwd(),
20
+ );
21
+ if (!project) throw new Error('No git repository found in the current directory.');
22
+
23
+ const { getProjectDataDir } = await import('../../store/persistence.js');
24
+ const dataDir = await getProjectDataDir(project.id);
25
+ try {
26
+ const { MaintenanceTargetStore } = await import('../../runtime/maintenance-targets.js');
27
+ new MaintenanceTargetStore(dataDir).register({
28
+ projectId: project.id,
29
+ projectRoot: project.rootPath,
30
+ dataDir,
31
+ });
32
+ } catch {
33
+ // Workbench reads remain available when optional maintenance metadata fails.
34
+ }
35
+
36
+ const savedIdentity = await loadCliIdentity(dataDir, project.id);
37
+ if (!invocation.actorId && !savedIdentity) {
38
+ return {
39
+ project,
40
+ dataDir,
41
+ reader: { projectId: project.id },
42
+ identity: null,
43
+ };
44
+ }
45
+
46
+ const teamStore = await initTeamStore(dataDir);
47
+ const resolved = await resolveCliIdentity({
48
+ project,
49
+ dataDir,
50
+ teamStore,
51
+ explicitActorId: invocation.actorId,
52
+ });
53
+ return {
54
+ project,
55
+ dataDir,
56
+ reader: resolved.reader,
57
+ identity: resolved.identity,
58
+ ...(resolved.warning ? { identityWarning: resolved.warning } : {}),
59
+ };
60
+ }
@@ -53,12 +53,13 @@ export async function getSessionState(projectId?: string): Promise<SessionState>
53
53
 
54
54
  export async function bindSession(): Promise<SessionState> {
55
55
  try {
56
- const proj = await resolveProject();
57
- if (!proj) return { status: 'error', error: 'No project detected' };
56
+ const { getTuiOperatorContext } = await import('./operator-context.js');
57
+ const { project: proj, reader } = await getTuiOperatorContext();
58
58
 
59
59
  const { startSession } = await import('../../memory/session.js');
60
60
  const { session, previousContext } = await startSession(proj.rootPath, proj.id, {
61
61
  agent: 'memorix-tui',
62
+ reader,
62
63
  });
63
64
 
64
65
  return {
@@ -65,21 +65,23 @@ export function MemoryView({ projectId, focusRefId, selectedIdx, onNavigateKnowl
65
65
  (async () => {
66
66
  try {
67
67
  setLoading(true);
68
- const { getObservation, initObservations, getAllObservations } = await import('../../../memory/observations.js');
69
- const { getProjectDataDir } = await import('../../../store/persistence.js');
68
+ const { initObservations, getAllObservations } = await import('../../../memory/observations.js');
69
+ const { filterReadableObservations } = await import('../../../memory/visibility.js');
70
70
  const { initObservationStore } = await import('../../../store/obs-store.js');
71
- const { detectProject } = await import('../../../project/detector.js');
71
+ const { getTuiOperatorContext } = await import('../operator-context.js');
72
72
 
73
- const proj = detectProject(process.cwd());
74
- if (!proj) return;
73
+ const { project: proj, dataDir, reader } = await getTuiOperatorContext();
75
74
 
76
- const dataDir = await getProjectDataDir(projectId || proj.id);
75
+ const effectiveProjectId = projectId || proj.id;
76
+ if (effectiveProjectId !== proj.id) return;
77
77
  await initObservationStore(dataDir);
78
78
  await initObservations(dataDir);
79
79
 
80
80
  const obsId = parseInt(idMatch[1], 10);
81
- const allObs = getAllObservations();
82
- const obs = allObs.find((o: any) => o.id === obsId);
81
+ const obs = filterReadableObservations(
82
+ getAllObservations().filter((observation) => observation.projectId === effectiveProjectId),
83
+ reader,
84
+ ).find((observation) => observation.id === obsId);
83
85
  if (obs) {
84
86
  setDetailItem({
85
87
  id: obs.id,
@@ -2,6 +2,7 @@ import { existsSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { getResolvedConfig } from '../config/resolved-config.js';
4
4
  import { buildTaskWorkset, type TaskWorkset, type WorksetCaution } from '../knowledge/workset.js';
5
+ import type { ContextDeliveryTarget } from '../knowledge/context-assembly.js';
5
6
  import type { ProjectInfo } from '../types.js';
6
7
  import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
7
8
  import { collectCurrentProjectFacts, formatGitFact, type CurrentProjectFacts } from './current-facts.js';
@@ -19,6 +20,7 @@ import {
19
20
  type ProjectContextOverview,
20
21
  } from './project-context.js';
21
22
  import { CodeGraphStore } from './store.js';
23
+ import { isEligibleForAutomaticDelivery } from '../memory/admission.js';
22
24
  import {
23
25
  lensPathCandidates,
24
26
  lensVerificationHints,
@@ -71,6 +73,14 @@ function activeProjectObservations(
71
73
  return observations.filter(obs => obs.projectId === projectId && (obs.status ?? 'active') === 'active');
72
74
  }
73
75
 
76
+ function deliveryEligibleProjectObservations(
77
+ observations: ProjectContextObservation[],
78
+ projectId: string,
79
+ ): ProjectContextObservation[] {
80
+ return activeProjectObservations(observations, projectId)
81
+ .filter((observation) => isEligibleForAutomaticDelivery(observation));
82
+ }
83
+
74
84
  function decideRefresh(input: {
75
85
  mode: AutoContextRefreshMode;
76
86
  status: ReturnType<CodeGraphStore['status']>;
@@ -118,6 +128,8 @@ export async function buildAutoProjectContext(input: {
118
128
  * request. MCP and hook callers use this to keep their response path fast.
119
129
  */
120
130
  enqueueRefresh?: () => void | Promise<void>;
131
+ /** The caller surface is recorded in the Workset receipt, not its prompt. */
132
+ deliveryTarget?: ContextDeliveryTarget;
121
133
  }): Promise<AutoProjectContext> {
122
134
  const refreshMode = input.refresh ?? 'auto';
123
135
  const now = input.now ?? new Date();
@@ -173,13 +185,27 @@ export async function buildAutoProjectContext(input: {
173
185
  activeProjectObservations(input.observations, input.project.id) as any,
174
186
  );
175
187
  try {
176
- const { enqueueClaimRequalification } = await import('../runtime/lifecycle.js');
188
+ const { MaintenanceTargetStore } = await import('../runtime/maintenance-targets.js');
189
+ new MaintenanceTargetStore(input.dataDir).register({
190
+ projectId: input.project.id,
191
+ projectRoot: input.project.rootPath,
192
+ dataDir: input.dataDir,
193
+ });
194
+ const {
195
+ enqueueClaimRequalification,
196
+ enqueueObservationQualification,
197
+ } = await import('../runtime/lifecycle.js');
177
198
  enqueueClaimRequalification({
178
199
  dataDir: input.dataDir,
179
200
  projectId: input.project.id,
180
201
  source: 'foreground-refresh',
181
202
  snapshotId: store.latestSnapshot(input.project.id)?.id,
182
203
  });
204
+ enqueueObservationQualification({
205
+ dataDir: input.dataDir,
206
+ projectId: input.project.id,
207
+ source: 'foreground-refresh',
208
+ });
183
209
  } catch {
184
210
  // The completed scan remains useful even if its later maintenance cannot queue.
185
211
  }
@@ -198,7 +224,9 @@ export async function buildAutoProjectContext(input: {
198
224
  const explain = buildProjectContextExplain({
199
225
  project: input.project,
200
226
  store,
201
- observations: input.observations,
227
+ // Binding sees all active evidence, but automatic delivery sees only
228
+ // evidence that has passed the control-plane admission gate.
229
+ observations: deliveryEligibleProjectObservations(input.observations, input.project.id),
202
230
  exclude,
203
231
  });
204
232
  const overview = explain.overview;
@@ -300,6 +328,7 @@ export async function buildAutoProjectContext(input: {
300
328
  stale: overview.freshness.stale,
301
329
  },
302
330
  runtimeCautions,
331
+ ...(input.deliveryTarget ? { deliveryTarget: input.deliveryTarget } : {}),
303
332
  });
304
333
 
305
334
  return {
@@ -316,6 +316,7 @@ export async function attachTaskWorkset(input: {
316
316
  stale: input.pack.warnings.filter(warning => warning.status === 'stale').length,
317
317
  },
318
318
  runtimeCautions: input.runtimeCautions,
319
+ deliveryTarget: 'context-pack',
319
320
  });
320
321
  return { ...input.pack, workset };
321
322
  }
@@ -16,6 +16,8 @@ export interface ProjectContextObservation {
16
16
  title: string;
17
17
  type: string;
18
18
  status?: string;
19
+ /** Automatic capture must be qualified before it can source a project brief. */
20
+ admissionState?: 'ephemeral' | 'candidate' | 'qualified';
19
21
  createdAt?: string;
20
22
  updatedAt?: string;
21
23
  /** Explicit file paths recorded with a memory, used only before Code Memory exists. */