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
@@ -90,6 +90,14 @@ async function getSearchObservations() {
90
90
  return _searchObservations;
91
91
  }
92
92
 
93
+ function automaticActorId(...parts: string[]): string {
94
+ return `orchestrator:${createHash('sha256').update(parts.join('\u0000')).digest('hex').slice(0, 24)}`;
95
+ }
96
+
97
+ function isEligibleAutomaticLesson(entry: { admissionState?: string }): boolean {
98
+ return entry.admissionState !== 'candidate' && entry.admissionState !== 'ephemeral';
99
+ }
100
+
93
101
  // ── 5a. Fix Loop Memory (CORE — always on) ────────────────────────
94
102
 
95
103
  /**
@@ -104,6 +112,7 @@ export function storeVerifiedFix(fix: FixResult): void {
104
112
 
105
113
  const errorHash = hashErrorPattern(fix.errorOutput);
106
114
  const confidence = fix.fixAttempt === 1 ? 'high' : fix.fixAttempt === 2 ? 'medium' : 'low';
115
+ const createdByAgentId = automaticActorId(fix.projectId, 'verified-fix', fix.gate, errorHash);
107
116
 
108
117
  // Fire-and-forget — catches all errors silently
109
118
  void (async () => {
@@ -126,6 +135,11 @@ export function storeVerifiedFix(fix: FixResult): void {
126
135
  source: 'agent',
127
136
  sourceDetail: 'hook',
128
137
  valueCategory: confidence === 'high' ? 'core' : 'contextual',
138
+ admissionState: 'candidate',
139
+ admissionReason: 'verified orchestration fix awaits current Code Memory qualification',
140
+ visibility: 'personal',
141
+ createdByAgentId,
142
+ visibilityReader: { projectId: fix.projectId, agentId: createdByAgentId },
129
143
  });
130
144
  } catch { /* fire-and-forget */ }
131
145
  })();
@@ -138,6 +152,7 @@ export function storeVerifiedFix(fix: FixResult): void {
138
152
  */
139
153
  export function storeFixExhausted(fix: FixResult): void {
140
154
  const errorHash = hashErrorPattern(fix.errorOutput);
155
+ const createdByAgentId = automaticActorId(fix.projectId, 'fix-exhausted', fix.gate, errorHash);
141
156
 
142
157
  void (async () => {
143
158
  try {
@@ -158,6 +173,11 @@ export function storeFixExhausted(fix: FixResult): void {
158
173
  source: 'agent',
159
174
  sourceDetail: 'hook',
160
175
  valueCategory: 'contextual',
176
+ admissionState: 'candidate',
177
+ admissionReason: 'automatic orchestration failure awaits current Code Memory qualification',
178
+ visibility: 'personal',
179
+ createdByAgentId,
180
+ visibilityReader: { projectId: fix.projectId, agentId: createdByAgentId },
161
181
  });
162
182
  } catch { /* fire-and-forget */ }
163
183
  })();
@@ -182,6 +202,7 @@ export async function searchKnownFixes(
182
202
  search({
183
203
  query: searchQuery,
184
204
  projectId,
205
+ reader: { projectId },
185
206
  type: 'problem-solution',
186
207
  limit: 3,
187
208
  status: 'active',
@@ -190,6 +211,7 @@ export async function searchKnownFixes(
190
211
  ]);
191
212
 
192
213
  return (results as any[])
214
+ .filter(isEligibleAutomaticLesson)
193
215
  .filter((r: any) => (r.score ?? 1) > 0.3) // Only reasonably relevant
194
216
  .map((r: any) => ({
195
217
  id: r.id ?? 0,
@@ -226,6 +248,7 @@ export async function searchLessons(
226
248
  search({
227
249
  query,
228
250
  projectId,
251
+ reader: { projectId },
229
252
  limit: config.maxLessons + 2, // Fetch extra for filtering
230
253
  status: 'active',
231
254
  }),
@@ -233,6 +256,7 @@ export async function searchLessons(
233
256
  ]);
234
257
 
235
258
  const lessons = (results as any[])
259
+ .filter(isEligibleAutomaticLesson)
236
260
  .filter((r: any) =>
237
261
  (r.type === 'problem-solution' || r.type === 'gotcha') &&
238
262
  (r.score ?? 1) > 0.5, // Only high-relevance (Safeguard 5: project isolation already handled by projectId filter)
@@ -282,6 +306,8 @@ export function storeTaskCompletion(opts: {
282
306
  durationMs: number;
283
307
  tailOutput: string;
284
308
  }): void {
309
+ const createdByAgentId = automaticActorId(opts.projectId, opts.pipelineId, opts.taskId, opts.agentName);
310
+
285
311
  void (async () => {
286
312
  try {
287
313
  const store = await getStoreObservation();
@@ -300,6 +326,11 @@ export function storeTaskCompletion(opts: {
300
326
  source: 'agent',
301
327
  sourceDetail: 'hook',
302
328
  valueCategory: 'ephemeral',
329
+ admissionState: 'ephemeral',
330
+ admissionReason: 'pipeline completion retained only as a short-lived trace',
331
+ visibility: 'personal',
332
+ createdByAgentId,
333
+ visibilityReader: { projectId: opts.projectId, agentId: createdByAgentId },
303
334
  });
304
335
  } catch { /* fire-and-forget */ }
305
336
  })();
@@ -318,6 +349,8 @@ export function storePipelineSummary(opts: {
318
349
  failed: number;
319
350
  elapsedMs: number;
320
351
  }): void {
352
+ const createdByAgentId = automaticActorId(opts.projectId, opts.pipelineId, 'summary');
353
+
321
354
  void (async () => {
322
355
  try {
323
356
  const store = await getStoreObservation();
@@ -338,6 +371,11 @@ export function storePipelineSummary(opts: {
338
371
  source: 'agent',
339
372
  sourceDetail: 'hook',
340
373
  valueCategory: 'contextual',
374
+ admissionState: 'candidate',
375
+ admissionReason: 'automatic pipeline summary awaits current Code Memory qualification',
376
+ visibility: 'personal',
377
+ createdByAgentId,
378
+ visibilityReader: { projectId: opts.projectId, agentId: createdByAgentId },
341
379
  });
342
380
  } catch { /* fire-and-forget */ }
343
381
  })();
@@ -11,6 +11,7 @@ export const CONTROL_PLANE_MAINTENANCE_KINDS: readonly MaintenanceJobKind[] = [
11
11
  'retention-archive',
12
12
  'consolidation',
13
13
  'codegraph-refresh',
14
+ 'observation-qualify',
14
15
  'claim-derive',
15
16
  'claim-requalification',
16
17
  'knowledge-compile',
@@ -12,6 +12,7 @@ export const ISOLATED_MAINTENANCE_JOB_KINDS: readonly MaintenanceJobKind[] = [
12
12
  'retention-archive',
13
13
  'consolidation',
14
14
  'codegraph-refresh',
15
+ 'observation-qualify',
15
16
  'claim-derive',
16
17
  'claim-requalification',
17
18
  'knowledge-compile',
@@ -65,6 +65,24 @@ export function enqueueClaimDerivation(input: {
65
65
  });
66
66
  }
67
67
 
68
+ /**
69
+ * Automatic captures are initially only candidates. Qualification runs in the
70
+ * durable maintenance lane after Code Memory has a chance to refresh.
71
+ */
72
+ export function enqueueObservationQualification(input: {
73
+ dataDir: string;
74
+ projectId: string;
75
+ source: string;
76
+ queue?: MaintenanceQueue;
77
+ }): void {
78
+ queueFor(input).enqueue({
79
+ projectId: input.projectId,
80
+ kind: 'observation-qualify',
81
+ dedupeKey: 'observation-qualify',
82
+ payload: { source: input.source, limit: 100 },
83
+ });
84
+ }
85
+
68
86
  /**
69
87
  * The follow-up jobs operate on the same workspace the Workset reads: a
70
88
  * versioned workspace when present, otherwise the local workspace.
@@ -8,6 +8,7 @@ export const MAINTENANCE_JOB_KINDS = [
8
8
  'retention-archive',
9
9
  'consolidation',
10
10
  'codegraph-refresh',
11
+ 'observation-qualify',
11
12
  'claim-derive',
12
13
  'claim-requalification',
13
14
  'knowledge-compile',
@@ -3,6 +3,7 @@ import { loadDotenv } from '../config/dotenv-loader.js';
3
3
  import { initProjectRoot } from '../config/yaml-loader.js';
4
4
  import { initLLM } from '../llm/provider.js';
5
5
  import { initObservationStore } from '../store/obs-store.js';
6
+ import { initObservations } from '../memory/observations.js';
6
7
  import {
7
8
  MAINTENANCE_JOB_KINDS,
8
9
  type MaintenanceJob,
@@ -58,6 +59,7 @@ export async function executeMaintenanceRequest(
58
59
  initProjectRoot(request.projectRoot);
59
60
  loadDotenv(request.projectRoot);
60
61
  await initObservationStore(request.dataDir);
62
+ await initObservations(request.dataDir);
61
63
  if (request.job.kind === 'consolidation') initLLM();
62
64
 
63
65
  const handler = createProjectMaintenanceHandler(
@@ -6,6 +6,7 @@ import { runMaintenanceInChildProcess } from './isolated-maintenance.js';
6
6
  import {
7
7
  enqueueClaimRequalification,
8
8
  enqueueKnowledgeFollowups,
9
+ enqueueObservationQualification,
9
10
  type MaintenanceQueue,
10
11
  } from './lifecycle.js';
11
12
 
@@ -14,6 +15,7 @@ const DEFAULT_RETENTION_BATCH_SIZE = 100;
14
15
  const DEFAULT_CONSOLIDATION_BATCH_SIZE = 200;
15
16
  const DEFAULT_CODEGRAPH_MAX_FILES = 5_000;
16
17
  const DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
18
+ const DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
17
19
  const VECTOR_RETRY_DELAY_MS = 5_000;
18
20
  const DEDUP_PER_PAIR_TIMEOUT_MS = 5_000;
19
21
 
@@ -68,6 +70,19 @@ function claimDerivationCursor(payload: Record<string, unknown>): number {
68
70
  : 0;
69
71
  }
70
72
 
73
+ function observationQualificationBatchSize(payload: Record<string, unknown>): number {
74
+ const value = payload.limit;
75
+ if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE;
76
+ return Math.min(500, Math.max(1, Math.floor(value)));
77
+ }
78
+
79
+ function observationQualificationCursor(payload: Record<string, unknown>): number {
80
+ const value = payload.cursor;
81
+ return typeof value === 'number' && Number.isFinite(value)
82
+ ? Math.max(0, Math.floor(value))
83
+ : 0;
84
+ }
85
+
71
86
  function observationId(payload: Record<string, unknown>): number | undefined {
72
87
  const value = payload.observationId;
73
88
  return typeof value === 'number' && Number.isSafeInteger(value) && value > 0
@@ -275,6 +290,80 @@ export function createProjectMaintenanceHandler(
275
290
  ...(snapshot?.id ? { snapshotId: snapshot.id } : {}),
276
291
  ...(options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}),
277
292
  });
293
+ enqueueObservationQualification({
294
+ projectId,
295
+ dataDir: projectDir,
296
+ source: 'codegraph-refresh',
297
+ ...(options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}),
298
+ });
299
+ return { action: 'complete' };
300
+ }
301
+
302
+ if (job.kind === 'observation-qualify') {
303
+ const [
304
+ { CodeGraphStore },
305
+ { bindObservationToCode },
306
+ { getObservationStore },
307
+ { qualifyCandidateFromCurrentCode },
308
+ { updateObservationAdmission },
309
+ ] = await Promise.all([
310
+ import('../codegraph/store.js'),
311
+ import('../codegraph/binder.js'),
312
+ import('../store/obs-store.js'),
313
+ import('../memory/admission.js'),
314
+ import('../memory/observations.js'),
315
+ ]);
316
+ const codeStore = new CodeGraphStore();
317
+ await codeStore.init(projectDir);
318
+ const limit = observationQualificationBatchSize(job.payload);
319
+ const page = await getObservationStore().loadByProject(projectId, {
320
+ status: 'active',
321
+ afterId: observationQualificationCursor(job.payload),
322
+ limit: limit + 1,
323
+ });
324
+ const hasMore = page.length > limit;
325
+ const observations = hasMore ? page.slice(0, limit) : page;
326
+ const indexedAtMs = Date.parse(codeStore.status(projectId).indexedAt ?? '');
327
+
328
+ for (const observation of observations) {
329
+ if (observation.admissionState !== 'candidate') continue;
330
+ // A candidate may only earn delivery after a Code Memory scan that
331
+ // happened at or after it was captured. A stale snapshot is evidence
332
+ // about an earlier project state, not a reason to inject this record.
333
+ if (!Number.isFinite(indexedAtMs) || indexedAtMs < Date.parse(observation.createdAt)) continue;
334
+ await bindObservationToCode(codeStore, observation);
335
+ const currentCodeReferenceCount = codeStore
336
+ .listObservationRefs(projectId, observation.id)
337
+ .filter((reference) => reference.status === 'current')
338
+ .length;
339
+ const qualification = qualifyCandidateFromCurrentCode({
340
+ observation,
341
+ currentCodeReferenceCount,
342
+ });
343
+ if (!qualification) continue;
344
+ await updateObservationAdmission({
345
+ observationId: observation.id,
346
+ projectId,
347
+ expectedState: 'candidate',
348
+ // Automatic capture earns project visibility only after a current
349
+ // Code Memory link exists. Explicit/private records retain scope.
350
+ ...(observation.sourceDetail === 'hook' ? { visibility: 'project' as const } : {}),
351
+ ...qualification,
352
+ });
353
+ }
354
+
355
+ if (hasMore && observations.length > 0) {
356
+ return {
357
+ action: 'reschedule',
358
+ delayMs: 0,
359
+ resetAttempts: true,
360
+ payload: {
361
+ ...job.payload,
362
+ cursor: observations[observations.length - 1].id,
363
+ limit,
364
+ },
365
+ };
366
+ }
278
367
  return { action: 'complete' };
279
368
  }
280
369
 
package/src/sdk.ts CHANGED
@@ -25,7 +25,13 @@ import type {
25
25
  ProgressInfo,
26
26
  ProjectInfo,
27
27
  DetectionResult,
28
+ ObservationReader,
28
29
  } from './types.js';
30
+ import {
31
+ canManageObservation,
32
+ canReadObservation,
33
+ filterReadableObservations,
34
+ } from './memory/visibility.js';
29
35
 
30
36
  // ── Re-exports for convenience ──────────────────────────────────────
31
37
 
@@ -97,12 +103,17 @@ export interface ResolveResult {
97
103
  * Memorix observations without MCP overhead.
98
104
  *
99
105
  * Each client initializes its own SQLite backend and Orama search index,
100
- * scoped to a single project. Call `close()` when done to release resources.
106
+ * scoped to a single project. It is an unbound project reader by default, so
107
+ * it exposes only project-shared records and cannot alter personal or team
108
+ * records. Use an identity-bound MCP session for private coordination data.
109
+ * Call `close()` when done to release resources.
101
110
  */
102
111
  export class MemoryClient {
103
112
  private _projectId: string;
104
113
  private _projectRoot: string;
105
114
  private _dataDir: string;
115
+ /** SDK calls are unbound by default, so they see only project-shared records. */
116
+ private _reader: ObservationReader;
106
117
  private _closed = false;
107
118
 
108
119
  // Internal module references — loaded lazily to avoid top-level side effects
@@ -116,6 +127,7 @@ export class MemoryClient {
116
127
  this._projectId = projectId;
117
128
  this._projectRoot = projectRoot;
118
129
  this._dataDir = dataDir;
130
+ this._reader = { projectId };
119
131
  }
120
132
 
121
133
  /** The canonical project ID (derived from Git remote or local path) */
@@ -182,6 +194,7 @@ export class MemoryClient {
182
194
  return this._observations.storeObservation({
183
195
  ...input,
184
196
  projectId: this._projectId,
197
+ visibilityReader: this._reader,
185
198
  });
186
199
  }
187
200
 
@@ -207,6 +220,7 @@ export class MemoryClient {
207
220
  type: options.type,
208
221
  source: options.source,
209
222
  status: options.status === 'all' ? undefined : (options.status ?? 'active'),
223
+ reader: this._reader,
210
224
  };
211
225
 
212
226
  return this._oramaStore.searchObservations(searchOpts);
@@ -218,7 +232,8 @@ export class MemoryClient {
218
232
  async get(id: number): Promise<Observation | undefined> {
219
233
  this._ensureOpen();
220
234
  await this._freshness.withFreshIndex(() => {});
221
- return this._observations.getObservation(id, this._projectId);
235
+ const observation = this._observations.getObservation(id, this._projectId);
236
+ return observation && canReadObservation(observation, this._reader) ? observation : undefined;
222
237
  }
223
238
 
224
239
  /**
@@ -227,7 +242,10 @@ export class MemoryClient {
227
242
  async getAll(): Promise<Observation[]> {
228
243
  this._ensureOpen();
229
244
  await this._freshness.withFreshIndex(() => {});
230
- return this._observations.getProjectObservations(this._projectId);
245
+ return filterReadableObservations(
246
+ this._observations.getProjectObservations(this._projectId),
247
+ this._reader,
248
+ );
231
249
  }
232
250
 
233
251
  /**
@@ -236,7 +254,10 @@ export class MemoryClient {
236
254
  async count(): Promise<number> {
237
255
  this._ensureOpen();
238
256
  await this._freshness.withFreshIndex(() => {});
239
- return this._observations.getProjectObservations(this._projectId).length;
257
+ return filterReadableObservations(
258
+ this._observations.getProjectObservations(this._projectId),
259
+ this._reader,
260
+ ).length;
240
261
  }
241
262
 
242
263
  /**
@@ -253,7 +274,16 @@ export class MemoryClient {
253
274
  */
254
275
  async resolve(ids: number[], status: ObservationStatus = 'resolved'): Promise<ResolveResult> {
255
276
  this._ensureOpen();
256
- return this._observations.resolveObservations(ids, status);
277
+ const manageableIds = ids.filter((id) => {
278
+ const observation = this._observations.getObservation(id, this._projectId);
279
+ return observation && canManageObservation(observation, this._reader);
280
+ });
281
+ const deniedIds = ids.filter((id) => !manageableIds.includes(id));
282
+ const result = await this._observations.resolveObservations(manageableIds, status);
283
+ return {
284
+ resolved: result.resolved,
285
+ notFound: [...new Set([...result.notFound, ...deniedIds])],
286
+ };
257
287
  }
258
288
 
259
289
  /**