memorix 1.3.3 → 1.3.4

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 (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli/index.js +1162 -1098
  3. package/dist/index.d.ts +513 -0
  4. package/dist/index.js +14751 -14044
  5. package/dist/maintenance-runner.js +105 -52
  6. package/dist/media-video-runner.js +105 -52
  7. package/dist/memcode-runtime/CHANGELOG.md +14 -0
  8. package/dist/sdk.d.ts +5 -513
  9. package/dist/sdk.js +186 -75
  10. package/dist/vector-backfill-runner.js +105 -52
  11. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -3
  12. package/package.json +1 -1
  13. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  14. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  15. package/plugins/copilot/memorix/plugin.json +1 -1
  16. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  17. package/plugins/omp/memorix/package.json +1 -1
  18. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  19. package/plugins/pi/memorix/package.json +1 -1
  20. package/server.json +2 -2
  21. package/src/cli/commands/media.ts +10 -2
  22. package/src/cli/commands/serve-http.ts +7 -38
  23. package/src/cli/commands/serve-shared.ts +18 -46
  24. package/src/cli/commands/serve.ts +3 -41
  25. package/src/cli/commands/setup.ts +24 -21
  26. package/src/cli/mcp-root-path.ts +36 -0
  27. package/src/index.ts +37 -55
  28. package/src/media/asset-store.ts +74 -6
  29. package/src/media/attachment.ts +12 -9
  30. package/src/media/media-store.ts +38 -4
  31. package/src/media/video-jobs.ts +39 -14
  32. package/src/memory/observations.ts +21 -2
  33. package/src/orchestrate/adapters/spawn-helper.ts +2 -0
  34. package/src/server/tool-profile.ts +7 -4
  35. package/src/server.ts +6 -2
@@ -139,12 +139,19 @@ async function finishWithAsset(input: {
139
139
  dependencies: MediaVideoGenerationDependencies;
140
140
  }): Promise<MaintenanceJobRunResult> {
141
141
  const { dataDir, projectId, store, mediaJob, request, providerTask, dependencies } = input;
142
- let asset: MediaAsset | undefined = mediaJob.assetId
143
- ? store.getAsset(projectId, mediaJob.assetId)
142
+ let activeJob = store.getJob(projectId, mediaJob.id);
143
+ if (!activeJob || terminal(activeJob)) return { action: 'complete' };
144
+ let asset: MediaAsset | undefined = activeJob.assetId
145
+ ? store.getAsset(projectId, activeJob.assetId)
144
146
  : undefined;
145
147
  if (!asset) {
146
148
  if (!providerTask.downloadUrl) throw new Error('Succeeded MiniMax video task did not include a download URL');
147
- store.updateJob(projectId, mediaJob.id, { status: 'downloading', providerTaskId: providerTask.taskId });
149
+ const downloading = store.updateJobIfNotCancelled(projectId, activeJob.id, {
150
+ status: 'downloading',
151
+ providerTaskId: providerTask.taskId,
152
+ });
153
+ if (!downloading) return { action: 'complete' };
154
+ activeJob = downloading;
148
155
  const bytes = await (dependencies.download ?? ((downloadInput) => downloadProviderMedia(downloadInput)))({
149
156
  url: providerTask.downloadUrl,
150
157
  maxBytes: request.maxBytes,
@@ -161,9 +168,20 @@ async function finishWithAsset(input: {
161
168
  maxBytes: request.maxBytes,
162
169
  });
163
170
  asset = imported.asset;
171
+ // Persist the local result before optional attachment. A later retry can
172
+ // resume attachment even if the provider's signed URL has expired.
173
+ const persistedAsset = store.updateJobIfNotCancelled(projectId, activeJob.id, {
174
+ status: 'downloading',
175
+ providerTaskId: providerTask.taskId,
176
+ assetId: asset.id,
177
+ });
178
+ if (!persistedAsset) return { action: 'complete' };
179
+ activeJob = persistedAsset;
164
180
  }
165
181
 
166
- if (mediaJob.attachOnComplete) {
182
+ if (activeJob.attachOnComplete) {
183
+ activeJob = store.getJob(projectId, activeJob.id);
184
+ if (!activeJob || terminal(activeJob)) return { action: 'complete' };
167
185
  const hasAttachment = store.listLinks(projectId, asset.id)
168
186
  .some((link) => link.role === 'attachment' && link.observationId !== undefined);
169
187
  if (!hasAttachment) {
@@ -171,13 +189,13 @@ async function finishWithAsset(input: {
171
189
  dataDir,
172
190
  projectId,
173
191
  asset,
174
- title: mediaJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
192
+ title: activeJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
175
193
  narrative: `Generated with ${request.provider}/${request.model}. Prompt: ${request.prompt}`,
176
194
  concepts: ['generated-video', 'minimax', request.model],
177
195
  });
178
196
  }
179
197
  }
180
- store.updateJob(projectId, mediaJob.id, {
198
+ store.updateJobIfNotCancelled(projectId, activeJob.id, {
181
199
  status: 'completed',
182
200
  providerTaskId: providerTask.taskId,
183
201
  assetId: asset.id,
@@ -194,10 +212,12 @@ function retryAfterProviderError(
194
212
  ): MaintenanceJobRunResult {
195
213
  const message = safeError(error);
196
214
  if (maintenanceJob.attempts >= maintenanceJob.maxAttempts) {
197
- store.updateJob(projectId, mediaJob.id, { status: 'failed', lastError: message, incrementAttempts: true });
215
+ store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: 'failed', lastError: message, incrementAttempts: true });
216
+ return { action: 'complete' };
217
+ }
218
+ if (!store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: 'retry', lastError: message, incrementAttempts: true })) {
198
219
  return { action: 'complete' };
199
220
  }
200
- store.updateJob(projectId, mediaJob.id, { status: 'retry', lastError: message, incrementAttempts: true });
201
221
  return { action: 'reschedule', delayMs: MINI_MAX_VIDEO_POLL_MS, status: 'retry', lastError: message };
202
222
  }
203
223
 
@@ -250,24 +270,27 @@ export async function runMiniMaxVideoGenerationJob(
250
270
  const request = parseStoredRequest(mediaJob.request);
251
271
 
252
272
  if (!mediaJob.providerTaskId) {
253
- store.updateJob(context.projectId, mediaJob.id, { status: 'submitting', incrementAttempts: true });
273
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: 'submitting', incrementAttempts: true })) {
274
+ return { action: 'complete' };
275
+ }
254
276
  let submitted: MiniMaxVideoTask;
255
277
  try {
256
278
  submitted = await (dependencies.createTask ?? createMiniMaxVideoTask)(toVideoRequest(request));
257
279
  } catch (error) {
258
280
  // A submit timeout can mean the provider accepted a billable request. Do
259
281
  // not retry automatically without a task ID; the operator can resubmit.
260
- store.updateJob(context.projectId, mediaJob.id, { status: 'failed', lastError: safeError(error) });
282
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: 'failed', lastError: safeError(error) });
261
283
  return { action: 'complete' };
262
284
  }
263
285
  if (submitted.status === 'failed') {
264
- store.updateJob(context.projectId, mediaJob.id, { status: 'failed', providerTaskId: submitted.taskId, lastError: submitted.error });
286
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: 'failed', providerTaskId: submitted.taskId, lastError: submitted.error });
265
287
  return { action: 'complete' };
266
288
  }
267
- const pending = store.updateJob(context.projectId, mediaJob.id, {
289
+ const pending = store.updateJobIfNotCancelled(context.projectId, mediaJob.id, {
268
290
  status: submitted.status === 'succeeded' ? 'downloading' : 'provider-pending',
269
291
  providerTaskId: submitted.taskId,
270
292
  });
293
+ if (!pending) return { action: 'complete' };
271
294
  if (submitted.status === 'succeeded') {
272
295
  try {
273
296
  return await finishWithAsset({
@@ -296,11 +319,13 @@ export async function runMiniMaxVideoGenerationJob(
296
319
  return retryAfterProviderError(store, context.projectId, mediaJob, maintenanceJob, error);
297
320
  }
298
321
  if (task.status === 'pending') {
299
- store.updateJob(context.projectId, mediaJob.id, { status: 'provider-pending', providerTaskId: task.taskId });
322
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: 'provider-pending', providerTaskId: task.taskId })) {
323
+ return { action: 'complete' };
324
+ }
300
325
  return { action: 'reschedule', delayMs: MINI_MAX_VIDEO_POLL_MS, resetAttempts: true, clearLastError: true };
301
326
  }
302
327
  if (task.status === 'failed') {
303
- store.updateJob(context.projectId, mediaJob.id, { status: 'failed', providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
328
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: 'failed', providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
304
329
  return { action: 'complete' };
305
330
  }
306
331
  try {
@@ -397,6 +397,12 @@ export async function storeObservation(input: {
397
397
  createdByAgentId?: string;
398
398
  /** Optional caller-bound write scope for topic-key upserts. */
399
399
  visibilityReader?: ObservationReader;
400
+ /**
401
+ * Internal transaction callback for a second SQLite record that must either
402
+ * commit with this observation or not exist at all. It is intentionally not
403
+ * exposed through the SDK input surface.
404
+ */
405
+ onPersisted?: (observation: Observation) => void | Promise<void>;
400
406
  }): Promise<{ observation: Observation; upserted: boolean }> {
401
407
  const now = new Date().toISOString();
402
408
 
@@ -431,7 +437,9 @@ export async function storeObservation(input: {
431
437
  if (input.visibilityReader && !canManageObservation(existing, input.visibilityReader)) {
432
438
  throw new Error('Cannot update a memory outside this session\'s write scope.');
433
439
  }
434
- return { observation: await upsertObservation(existing, input, now), upserted: true };
440
+ const observation = await upsertObservation(existing, input, now);
441
+ await input.onPersisted?.(observation);
442
+ return { observation, upserted: true };
435
443
  }
436
444
  }
437
445
 
@@ -460,6 +468,7 @@ export async function storeObservation(input: {
460
468
 
461
469
  let upsertedInsideLock = false;
462
470
  let reloadCacheAfterCommit = false;
471
+ let persistedCallbackInvoked = false;
463
472
  const assignAndPersist = async () => {
464
473
  if (projectDir) {
465
474
  const store = getObservationStore();
@@ -528,6 +537,8 @@ export async function storeObservation(input: {
528
537
  };
529
538
 
530
539
  await tx.insert(observation);
540
+ await input.onPersisted?.(observation);
541
+ persistedCallbackInvoked = true;
531
542
  await tx.saveIdCounter(id + 1);
532
543
  });
533
544
 
@@ -584,6 +595,8 @@ export async function storeObservation(input: {
584
595
  writeGeneration: 0,
585
596
  };
586
597
  observations.push(observation);
598
+ await input.onPersisted?.(observation);
599
+ persistedCallbackInvoked = true;
587
600
  }
588
601
 
589
602
  // Build Orama doc AFTER id is assigned
@@ -621,9 +634,15 @@ export async function storeObservation(input: {
621
634
 
622
635
  // If the lock discovered a topicKey duplicate on disk, delegate to upsert
623
636
  if (upsertedInsideLock) {
624
- return { observation: await upsertObservation(observation, input, now), upserted: true };
637
+ const updated = await upsertObservation(observation, input, now);
638
+ await input.onPersisted?.(updated);
639
+ return { observation: updated, upserted: true };
625
640
  }
626
641
 
642
+ // The normal persistent path invokes the callback inside the SQLite
643
+ // transaction. Keep the in-memory fallback defensively covered too.
644
+ if (!persistedCallbackInvoked) await input.onPersisted?.(observation);
645
+
627
646
  await bindObservationCodeRefsBestEffort(observation);
628
647
  queueObservationQualification(observation);
629
648
  if (resolveObservationVisibility(observation) === 'project') {
@@ -43,6 +43,7 @@ export function spawnAgent(
43
43
  stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'],
44
44
  // Windows: use shell to resolve CLI tools on PATH
45
45
  shell: process.platform === 'win32',
46
+ windowsHide: true,
46
47
  });
47
48
 
48
49
  // Write prompt via stdin to avoid shell argument escaping issues.
@@ -134,6 +135,7 @@ export function spawnAgentWithStream(
134
135
  env: { ...process.env, ...opts.env },
135
136
  stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'],
136
137
  shell: process.platform === 'win32',
138
+ windowsHide: true,
137
139
  });
138
140
 
139
141
  let stdinError: string | undefined;
@@ -6,7 +6,7 @@
6
6
  * tools/list time causes cognitive overload.
7
7
  *
8
8
  * We provide four profiles:
9
- * - "micro" (stdio default): Agent-ready project context + basic memory CRUD — 7 tools.
9
+ * - "micro" (stdio default): Agent-ready project context + bounded recovery/media controls — 9 tools.
10
10
  * Suitable for normal MCP clients where every tool schema costs context tokens.
11
11
  * - "lite": Core memory CRUD, sessions, reasoning, retention, backup — 18 tools.
12
12
  * Suitable for solo users who want the full memory surface without team tools.
@@ -35,19 +35,22 @@ export const TOOL_PROFILES: Record<string, ReadonlyArray<ToolProfile>> = Object.
35
35
  memorix_context_pack: ['micro', 'lite', 'team', 'full'],
36
36
  memorix_codegraph_status: ['micro', 'lite', 'team', 'full'],
37
37
  memorix_resolve: ['micro', 'lite', 'team', 'full'],
38
+ // A fail-closed stdio server needs an explicit recovery path when a client
39
+ // did not launch it from the workspace. The unified media tool keeps
40
+ // multimodal support to one additional schema rather than enabling lite.
41
+ memorix_session_start: ['micro', 'lite', 'team', 'full'],
42
+ memorix_media: ['micro', 'lite', 'team', 'full'],
38
43
 
39
44
  // ── lite: extended solo memory surface ────────────────────────────
40
45
  memorix_graph_context: ['lite', 'team', 'full'],
41
46
  memorix_timeline: ['lite', 'team', 'full'],
42
47
  memorix_suggest_topic_key: ['lite', 'team', 'full'],
43
- memorix_session_start: ['lite', 'team', 'full'],
44
48
  memorix_session_end: ['lite', 'team', 'full'],
45
49
  memorix_session_context: ['lite', 'team', 'full'],
46
50
  memorix_store_reasoning: ['lite', 'team', 'full'],
47
51
  memorix_search_reasoning: ['lite', 'team', 'full'],
48
52
  memorix_transfer: ['lite', 'team', 'full'],
49
53
  memorix_retention: ['lite', 'team', 'full'],
50
- memorix_media: ['lite', 'team', 'full'],
51
54
 
52
55
  // ── team: orchestration coordination surfaces — HTTP default ──────
53
56
  memorix_dashboard: ['team', 'full'],
@@ -136,7 +139,7 @@ export function resolveToolProfile(opts: ResolveToolProfileOpts): ToolProfile {
136
139
  */
137
140
  export function describeProfile(profile: ToolProfile): string {
138
141
  switch (profile) {
139
- case 'micro': return 'micro (agent-ready context + basic memory, ~7 tools)';
142
+ case 'micro': return 'micro (agent-ready context + bounded recovery/media, ~9 tools)';
140
143
  case 'lite': return 'lite (core memory + sessions, ~18 tools)';
141
144
  case 'team': return 'team (lite + coordination tools + dashboard + knowledge workspace, ~26 tools)';
142
145
  case 'full': return 'full (all tools including advanced / KG-compat)';
package/src/server.ts CHANGED
@@ -4993,10 +4993,10 @@ export async function createMemorixServer(
4993
4993
  description:
4994
4994
  'Use the controlled local media library for an explicit import, attachment, inspection, or MiniMax generation request. ' +
4995
4995
  'Assets stay outside the Git worktree and enter normal memory only when attach is explicitly true. ' +
4996
- 'Use the CLI for destructive removal, quota cleanup, job cancellation, and direct generation. ' +
4996
+ 'Use the CLI for destructive removal, quota cleanup, and direct generation. ' +
4997
4997
  'MCP generation is disabled by default and requires MEMORIX_MCP_MEDIA_GENERATION=1 after the operator reviews provider billing.',
4998
4998
  inputSchema: {
4999
- action: z.enum(['import', 'attach', 'list', 'show', 'generate-image', 'generate-video', 'status']),
4999
+ action: z.enum(['import', 'attach', 'list', 'show', 'generate-image', 'generate-video', 'status', 'cancel']),
5000
5000
  path: z.string().optional().describe('Explicit local image/audio/video/PDF path for import.'),
5001
5001
  assetId: z.string().optional().describe('Controlled MediaAsset ID for attach/show.'),
5002
5002
  jobId: z.string().optional().describe('Durable media job ID for status.'),
@@ -5069,6 +5069,10 @@ export async function createMemorixServer(
5069
5069
  if (!job) throw new Error(`Media job not found: ${jobId}`);
5070
5070
  return result({ action, projectId: project.id, job });
5071
5071
  }
5072
+ if (action === 'cancel') {
5073
+ if (!jobId?.trim()) throw new Error('jobId is required for media cancel');
5074
+ return result({ action, projectId: project.id, job: store.cancelJob(project.id, jobId) });
5075
+ }
5072
5076
  if (action === 'attach') {
5073
5077
  if (!assetId?.trim()) throw new Error('assetId is required for media attach');
5074
5078
  const asset = store.getAsset(project.id, assetId);