memorix 1.3.3 → 1.4.0

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 (36) hide show
  1. package/CHANGELOG.md +23 -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 +23 -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/1.3.3-MULTIMODAL-MEDIA-AND-MCP-COMPAT.md +13 -13
  12. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -3
  13. package/package.json +1 -1
  14. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  15. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  16. package/plugins/copilot/memorix/plugin.json +1 -1
  17. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  18. package/plugins/omp/memorix/package.json +1 -1
  19. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  20. package/plugins/pi/memorix/package.json +1 -1
  21. package/server.json +2 -2
  22. package/src/cli/commands/media.ts +10 -2
  23. package/src/cli/commands/serve-http.ts +7 -38
  24. package/src/cli/commands/serve-shared.ts +18 -46
  25. package/src/cli/commands/serve.ts +3 -41
  26. package/src/cli/commands/setup.ts +24 -21
  27. package/src/cli/mcp-root-path.ts +36 -0
  28. package/src/index.ts +37 -55
  29. package/src/media/asset-store.ts +74 -6
  30. package/src/media/attachment.ts +12 -9
  31. package/src/media/media-store.ts +38 -4
  32. package/src/media/video-jobs.ts +39 -14
  33. package/src/memory/observations.ts +21 -2
  34. package/src/orchestrate/adapters/spawn-helper.ts +2 -0
  35. package/src/server/tool-profile.ts +7 -4
  36. package/src/server.ts +6 -2
@@ -3065,8 +3065,8 @@ async function acquireLock(lockPath) {
3065
3065
  const code = err instanceof Error && "code" in err ? err.code : void 0;
3066
3066
  if (code === "EEXIST" || code === "EPERM") {
3067
3067
  try {
3068
- const stat3 = await fs3.stat(lockPath);
3069
- if (Date.now() - stat3.mtimeMs > LOCK_STALE_MS) {
3068
+ const stat4 = await fs3.stat(lockPath);
3069
+ if (Date.now() - stat4.mtimeMs > LOCK_STALE_MS) {
3070
3070
  await fs3.unlink(lockPath).catch(() => {
3071
3071
  });
3072
3072
  continue;
@@ -7922,6 +7922,10 @@ var init_media_store = __esm({
7922
7922
  `).run(projectId, assetId).changes ?? 0);
7923
7923
  this.db.prepare("DELETE FROM media_derivations WHERE project_id = ? AND asset_id = ?").run(projectId, assetId);
7924
7924
  this.db.prepare("DELETE FROM media_embeddings WHERE project_id = ? AND asset_id = ?").run(projectId, assetId);
7925
+ this.db.prepare(`
7926
+ UPDATE media_jobs SET asset_id = NULL, updated_at = ?
7927
+ WHERE project_id = ? AND asset_id = ?
7928
+ `).run(Date.now(), projectId, assetId);
7925
7929
  const result = this.db.prepare(`
7926
7930
  UPDATE media_assets SET deleted_at = ?, updated_at = ?
7927
7931
  WHERE project_id = ? AND id = ? AND deleted_at IS NULL
@@ -8096,16 +8100,28 @@ var init_media_store = __esm({
8096
8100
  return row ? rowToJob2(row) : void 0;
8097
8101
  }
8098
8102
  updateJob(projectId, id, input) {
8103
+ const updated = this.updateJobInternal(projectId, id, input, false);
8104
+ if (!updated) throw new Error(`Media job not found: ${id}`);
8105
+ return updated;
8106
+ }
8107
+ /**
8108
+ * Atomically preserve a user cancellation against a worker that was already
8109
+ * downloading or attaching media when the cancellation arrived.
8110
+ */
8111
+ updateJobIfNotCancelled(projectId, id, input) {
8112
+ return this.updateJobInternal(projectId, id, input, true);
8113
+ }
8114
+ updateJobInternal(projectId, id, input, rejectCancelled) {
8099
8115
  const existing = this.getJob(projectId, id);
8100
- if (!existing) throw new Error(`Media job not found: ${id}`);
8116
+ if (!existing) return void 0;
8101
8117
  const now3 = Date.now();
8102
8118
  const attempts = existing.attempts + (input.incrementAttempts ? 1 : 0);
8103
8119
  const completedAt = input.status === "completed" ? now3 : null;
8104
- this.db.prepare(`
8120
+ const result = this.db.prepare(`
8105
8121
  UPDATE media_jobs
8106
8122
  SET status = ?, provider_task_id = COALESCE(?, provider_task_id), asset_id = COALESCE(?, asset_id),
8107
8123
  last_error = ?, attempts = ?, updated_at = ?, completed_at = ?
8108
- WHERE project_id = ? AND id = ?
8124
+ WHERE project_id = ? AND id = ? ${rejectCancelled ? "AND status <> 'cancelled'" : ""}
8109
8125
  `).run(
8110
8126
  input.status,
8111
8127
  input.providerTaskId ?? null,
@@ -8117,6 +8133,7 @@ var init_media_store = __esm({
8117
8133
  projectId,
8118
8134
  id
8119
8135
  );
8136
+ if (Number(result.changes ?? 0) !== 1) return void 0;
8120
8137
  return this.getJob(projectId, id);
8121
8138
  }
8122
8139
  cancelJob(projectId, id) {
@@ -8162,6 +8179,7 @@ async function attachMediaAssetToObservation(input) {
8162
8179
  `MIME type: ${asset.mimeType}`,
8163
8180
  ...input.facts ?? []
8164
8181
  ];
8182
+ const store = new MediaStore(input.dataDir);
8165
8183
  const result = await storeObservation({
8166
8184
  entityName: input.entityName?.trim() || sourceLabel2.replace(/\.[^.]+$/, "") || "media-asset",
8167
8185
  type: "discovery",
@@ -8174,13 +8192,15 @@ async function attachMediaAssetToObservation(input) {
8174
8192
  sourceDetail: "explicit",
8175
8193
  ...input.visibility ? { visibility: input.visibility } : {},
8176
8194
  ...input.createdByAgentId ? { createdByAgentId: input.createdByAgentId } : {},
8177
- ...input.visibilityReader ? { visibilityReader: input.visibilityReader } : {}
8178
- });
8179
- new MediaStore(input.dataDir).linkAsset({
8180
- projectId: input.projectId,
8181
- assetId: asset.id,
8182
- observationId: result.observation.id,
8183
- role: input.role ?? "attachment"
8195
+ ...input.visibilityReader ? { visibilityReader: input.visibilityReader } : {},
8196
+ onPersisted: (observation) => {
8197
+ store.linkAsset({
8198
+ projectId: input.projectId,
8199
+ assetId: asset.id,
8200
+ observationId: observation.id,
8201
+ role: input.role ?? "attachment"
8202
+ });
8203
+ }
8184
8204
  });
8185
8205
  return result.observation;
8186
8206
  }
@@ -8206,7 +8226,7 @@ var init_types2 = __esm({
8206
8226
 
8207
8227
  // src/media/asset-store.ts
8208
8228
  import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
8209
- import { copyFile, lstat, mkdir as mkdir3, open, readFile as readFile3, rename, rm, writeFile as writeFile3 } from "fs/promises";
8229
+ import { copyFile, lstat, mkdir as mkdir3, open, readFile as readFile3, readdir, rename, rm, stat, writeFile as writeFile3 } from "fs/promises";
8210
8230
  import path10 from "path";
8211
8231
  function mediaRoot(dataDir) {
8212
8232
  return path10.resolve(dataDir, "media");
@@ -59319,7 +59339,7 @@ var require_node_domexception = __commonJS({
59319
59339
  // node_modules/fetch-blob/from.js
59320
59340
  import { statSync as statSync2, createReadStream, promises as fs7 } from "fs";
59321
59341
  import { basename } from "path";
59322
- var import_node_domexception, stat, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
59342
+ var import_node_domexception, stat2, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
59323
59343
  var init_from = __esm({
59324
59344
  "node_modules/fetch-blob/from.js"() {
59325
59345
  "use strict";
@@ -59327,23 +59347,23 @@ var init_from = __esm({
59327
59347
  import_node_domexception = __toESM(require_node_domexception(), 1);
59328
59348
  init_file();
59329
59349
  init_fetch_blob();
59330
- ({ stat } = fs7);
59350
+ ({ stat: stat2 } = fs7);
59331
59351
  blobFromSync = (path19, type) => fromBlob(statSync2(path19), path19, type);
59332
- blobFrom = (path19, type) => stat(path19).then((stat3) => fromBlob(stat3, path19, type));
59333
- fileFrom = (path19, type) => stat(path19).then((stat3) => fromFile(stat3, path19, type));
59352
+ blobFrom = (path19, type) => stat2(path19).then((stat4) => fromBlob(stat4, path19, type));
59353
+ fileFrom = (path19, type) => stat2(path19).then((stat4) => fromFile(stat4, path19, type));
59334
59354
  fileFromSync = (path19, type) => fromFile(statSync2(path19), path19, type);
59335
- fromBlob = (stat3, path19, type = "") => new fetch_blob_default([new BlobDataItem({
59355
+ fromBlob = (stat4, path19, type = "") => new fetch_blob_default([new BlobDataItem({
59336
59356
  path: path19,
59337
- size: stat3.size,
59338
- lastModified: stat3.mtimeMs,
59357
+ size: stat4.size,
59358
+ lastModified: stat4.mtimeMs,
59339
59359
  start: 0
59340
59360
  })], { type });
59341
- fromFile = (stat3, path19, type = "") => new file_default([new BlobDataItem({
59361
+ fromFile = (stat4, path19, type = "") => new file_default([new BlobDataItem({
59342
59362
  path: path19,
59343
- size: stat3.size,
59344
- lastModified: stat3.mtimeMs,
59363
+ size: stat4.size,
59364
+ lastModified: stat4.mtimeMs,
59345
59365
  start: 0
59346
- })], basename(path19), { type, lastModified: stat3.mtimeMs });
59366
+ })], basename(path19), { type, lastModified: stat4.mtimeMs });
59347
59367
  BlobDataItem = class _BlobDataItem {
59348
59368
  #path;
59349
59369
  #start;
@@ -59366,7 +59386,7 @@ var init_from = __esm({
59366
59386
  });
59367
59387
  }
59368
59388
  async *stream() {
59369
- const { mtimeMs } = await stat(this.#path);
59389
+ const { mtimeMs } = await stat2(this.#path);
59370
59390
  if (mtimeMs > this.lastModified) {
59371
59391
  throw new import_node_domexception.default("The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.", "NotReadableError");
59372
59392
  }
@@ -141532,10 +141552,17 @@ function sourceLabel(request, providerTaskId) {
141532
141552
  }
141533
141553
  async function finishWithAsset(input) {
141534
141554
  const { dataDir, projectId, store, mediaJob, request, providerTask, dependencies } = input;
141535
- let asset = mediaJob.assetId ? store.getAsset(projectId, mediaJob.assetId) : void 0;
141555
+ let activeJob = store.getJob(projectId, mediaJob.id);
141556
+ if (!activeJob || terminal(activeJob)) return { action: "complete" };
141557
+ let asset = activeJob.assetId ? store.getAsset(projectId, activeJob.assetId) : void 0;
141536
141558
  if (!asset) {
141537
141559
  if (!providerTask.downloadUrl) throw new Error("Succeeded MiniMax video task did not include a download URL");
141538
- store.updateJob(projectId, mediaJob.id, { status: "downloading", providerTaskId: providerTask.taskId });
141560
+ const downloading = store.updateJobIfNotCancelled(projectId, activeJob.id, {
141561
+ status: "downloading",
141562
+ providerTaskId: providerTask.taskId
141563
+ });
141564
+ if (!downloading) return { action: "complete" };
141565
+ activeJob = downloading;
141539
141566
  const bytes = await (dependencies.download ?? ((downloadInput) => downloadProviderMedia(downloadInput)))({
141540
141567
  url: providerTask.downloadUrl,
141541
141568
  maxBytes: request.maxBytes
@@ -141552,21 +141579,30 @@ async function finishWithAsset(input) {
141552
141579
  maxBytes: request.maxBytes
141553
141580
  });
141554
141581
  asset = imported.asset;
141582
+ const persistedAsset = store.updateJobIfNotCancelled(projectId, activeJob.id, {
141583
+ status: "downloading",
141584
+ providerTaskId: providerTask.taskId,
141585
+ assetId: asset.id
141586
+ });
141587
+ if (!persistedAsset) return { action: "complete" };
141588
+ activeJob = persistedAsset;
141555
141589
  }
141556
- if (mediaJob.attachOnComplete) {
141590
+ if (activeJob.attachOnComplete) {
141591
+ activeJob = store.getJob(projectId, activeJob.id);
141592
+ if (!activeJob || terminal(activeJob)) return { action: "complete" };
141557
141593
  const hasAttachment = store.listLinks(projectId, asset.id).some((link) => link.role === "attachment" && link.observationId !== void 0);
141558
141594
  if (!hasAttachment) {
141559
141595
  await (dependencies.attach ?? attachMediaAssetToObservation)({
141560
141596
  dataDir,
141561
141597
  projectId,
141562
141598
  asset,
141563
- title: mediaJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
141599
+ title: activeJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
141564
141600
  narrative: `Generated with ${request.provider}/${request.model}. Prompt: ${request.prompt}`,
141565
141601
  concepts: ["generated-video", "minimax", request.model]
141566
141602
  });
141567
141603
  }
141568
141604
  }
141569
- store.updateJob(projectId, mediaJob.id, {
141605
+ store.updateJobIfNotCancelled(projectId, activeJob.id, {
141570
141606
  status: "completed",
141571
141607
  providerTaskId: providerTask.taskId,
141572
141608
  assetId: asset.id
@@ -141576,10 +141612,12 @@ async function finishWithAsset(input) {
141576
141612
  function retryAfterProviderError(store, projectId, mediaJob, maintenanceJob, error) {
141577
141613
  const message = safeError2(error);
141578
141614
  if (maintenanceJob.attempts >= maintenanceJob.maxAttempts) {
141579
- store.updateJob(projectId, mediaJob.id, { status: "failed", lastError: message, incrementAttempts: true });
141615
+ store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: "failed", lastError: message, incrementAttempts: true });
141616
+ return { action: "complete" };
141617
+ }
141618
+ if (!store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: "retry", lastError: message, incrementAttempts: true })) {
141580
141619
  return { action: "complete" };
141581
141620
  }
141582
- store.updateJob(projectId, mediaJob.id, { status: "retry", lastError: message, incrementAttempts: true });
141583
141621
  return { action: "reschedule", delayMs: MINI_MAX_VIDEO_POLL_MS, status: "retry", lastError: message };
141584
141622
  }
141585
141623
  function queueMiniMaxVideoGeneration(input) {
@@ -141620,22 +141658,25 @@ async function runMiniMaxVideoGenerationJob(maintenanceJob, context, dependencie
141620
141658
  if (mediaJob.kind !== "minimax-video-generation") throw new Error(`Unsupported media job kind: ${mediaJob.kind}`);
141621
141659
  const request = parseStoredRequest(mediaJob.request);
141622
141660
  if (!mediaJob.providerTaskId) {
141623
- store.updateJob(context.projectId, mediaJob.id, { status: "submitting", incrementAttempts: true });
141661
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "submitting", incrementAttempts: true })) {
141662
+ return { action: "complete" };
141663
+ }
141624
141664
  let submitted;
141625
141665
  try {
141626
141666
  submitted = await (dependencies.createTask ?? createMiniMaxVideoTask)(toVideoRequest(request));
141627
141667
  } catch (error) {
141628
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", lastError: safeError2(error) });
141668
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", lastError: safeError2(error) });
141629
141669
  return { action: "complete" };
141630
141670
  }
141631
141671
  if (submitted.status === "failed") {
141632
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", providerTaskId: submitted.taskId, lastError: submitted.error });
141672
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", providerTaskId: submitted.taskId, lastError: submitted.error });
141633
141673
  return { action: "complete" };
141634
141674
  }
141635
- const pending = store.updateJob(context.projectId, mediaJob.id, {
141675
+ const pending = store.updateJobIfNotCancelled(context.projectId, mediaJob.id, {
141636
141676
  status: submitted.status === "succeeded" ? "downloading" : "provider-pending",
141637
141677
  providerTaskId: submitted.taskId
141638
141678
  });
141679
+ if (!pending) return { action: "complete" };
141639
141680
  if (submitted.status === "succeeded") {
141640
141681
  try {
141641
141682
  return await finishWithAsset({
@@ -141663,11 +141704,13 @@ async function runMiniMaxVideoGenerationJob(maintenanceJob, context, dependencie
141663
141704
  return retryAfterProviderError(store, context.projectId, mediaJob, maintenanceJob, error);
141664
141705
  }
141665
141706
  if (task.status === "pending") {
141666
- store.updateJob(context.projectId, mediaJob.id, { status: "provider-pending", providerTaskId: task.taskId });
141707
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "provider-pending", providerTaskId: task.taskId })) {
141708
+ return { action: "complete" };
141709
+ }
141667
141710
  return { action: "reschedule", delayMs: MINI_MAX_VIDEO_POLL_MS, resetAttempts: true, clearLastError: true };
141668
141711
  }
141669
141712
  if (task.status === "failed") {
141670
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
141713
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
141671
141714
  return { action: "complete" };
141672
141715
  }
141673
141716
  try {
@@ -142950,7 +142993,7 @@ function indexFileLite(projectId, projectRoot, abs, indexedAt, fileStat) {
142950
142993
  const rel = normalizeCodePath(relative(projectRoot, abs));
142951
142994
  try {
142952
142995
  const text = readFileSync6(abs, "utf-8");
142953
- const stat3 = fileStat ?? statSync3(abs);
142996
+ const stat4 = fileStat ?? statSync3(abs);
142954
142997
  const file = {
142955
142998
  id: makeCodeFileId(projectId, rel),
142956
142999
  projectId,
@@ -142959,8 +143002,8 @@ function indexFileLite(projectId, projectRoot, abs, indexedAt, fileStat) {
142959
143002
  contentHash: hashText(text),
142960
143003
  // Preserve filesystem precision. Rounding here made same-size edits in
142961
143004
  // a short Windows timestamp window indistinguishable to refresh.
142962
- mtimeMs: Number(stat3.mtimeMs),
142963
- sizeBytes: Number(stat3.size),
143005
+ mtimeMs: Number(stat4.mtimeMs),
143006
+ sizeBytes: Number(stat4.size),
142964
143007
  indexedAt
142965
143008
  };
142966
143009
  return {
@@ -142987,18 +143030,18 @@ async function indexProjectLite(options) {
142987
143030
  let skippedOversizedFiles = 0;
142988
143031
  let unreadableFiles = 0;
142989
143032
  for (const abs of paths) {
142990
- let stat3;
143033
+ let stat4;
142991
143034
  try {
142992
- stat3 = statSync3(abs);
143035
+ stat4 = statSync3(abs);
142993
143036
  } catch {
142994
143037
  unreadableFiles++;
142995
143038
  continue;
142996
143039
  }
142997
- if (Number(stat3.size) > maxFileBytes) {
143040
+ if (Number(stat4.size) > maxFileBytes) {
142998
143041
  skippedOversizedFiles++;
142999
143042
  continue;
143000
143043
  }
143001
- const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat3);
143044
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat4);
143002
143045
  if (!attempt.delta) {
143003
143046
  if (attempt.unreadable) unreadableFiles++;
143004
143047
  continue;
@@ -143026,16 +143069,16 @@ async function refreshProjectLite(store, options) {
143026
143069
  for (const abs of paths) {
143027
143070
  const rel = normalizeCodePath(relative(options.projectRoot, abs));
143028
143071
  seenPaths.add(rel);
143029
- let stat3;
143072
+ let stat4;
143030
143073
  try {
143031
- stat3 = statSync3(abs);
143074
+ stat4 = statSync3(abs);
143032
143075
  } catch {
143033
143076
  unreadableFiles++;
143034
143077
  continue;
143035
143078
  }
143036
143079
  const existing = existingByPath.get(rel);
143037
- const mtimeMs = Number(stat3.mtimeMs);
143038
- const sizeBytes = Number(stat3.size);
143080
+ const mtimeMs = Number(stat4.mtimeMs);
143081
+ const sizeBytes = Number(stat4.size);
143039
143082
  if (sizeBytes > maxFileBytes) {
143040
143083
  skippedOversizedFiles++;
143041
143084
  if (existing) oversizedExistingFileIds.add(existing.id);
@@ -143045,7 +143088,7 @@ async function refreshProjectLite(store, options) {
143045
143088
  unchangedFiles++;
143046
143089
  continue;
143047
143090
  }
143048
- const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat3);
143091
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat4);
143049
143092
  if (!attempt.delta) {
143050
143093
  if (attempt.unreadable) unreadableFiles++;
143051
143094
  continue;
@@ -146486,7 +146529,9 @@ async function storeObservation(input) {
146486
146529
  if (input.visibilityReader && !canManageObservation(existing, input.visibilityReader)) {
146487
146530
  throw new Error("Cannot update a memory outside this session's write scope.");
146488
146531
  }
146489
- return { observation: await upsertObservation(existing, input, now3), upserted: true };
146532
+ const observation2 = await upsertObservation(existing, input, now3);
146533
+ await input.onPersisted?.(observation2);
146534
+ return { observation: observation2, upserted: true };
146490
146535
  }
146491
146536
  }
146492
146537
  const contentForExtraction = [input.title, input.narrative, ...input.facts ?? []].join(" ");
@@ -146511,6 +146556,7 @@ async function storeObservation(input) {
146511
146556
  let doc;
146512
146557
  let upsertedInsideLock = false;
146513
146558
  let reloadCacheAfterCommit = false;
146559
+ let persistedCallbackInvoked = false;
146514
146560
  const assignAndPersist = async () => {
146515
146561
  if (projectDir) {
146516
146562
  const store = getObservationStore();
@@ -146566,6 +146612,8 @@ async function storeObservation(input) {
146566
146612
  writeGeneration: transactionGeneration + 1
146567
146613
  };
146568
146614
  await tx.insert(observation);
146615
+ await input.onPersisted?.(observation);
146616
+ persistedCallbackInvoked = true;
146569
146617
  await tx.saveIdCounter(id + 1);
146570
146618
  });
146571
146619
  observation.writeGeneration = store.getGeneration();
@@ -146615,6 +146663,8 @@ async function storeObservation(input) {
146615
146663
  writeGeneration: 0
146616
146664
  };
146617
146665
  observations.push(observation);
146666
+ await input.onPersisted?.(observation);
146667
+ persistedCallbackInvoked = true;
146618
146668
  }
146619
146669
  doc = {
146620
146670
  id: makeOramaObservationId(input.projectId, observation.id),
@@ -146646,8 +146696,11 @@ async function storeObservation(input) {
146646
146696
  };
146647
146697
  await assignAndPersist();
146648
146698
  if (upsertedInsideLock) {
146649
- return { observation: await upsertObservation(observation, input, now3), upserted: true };
146699
+ const updated = await upsertObservation(observation, input, now3);
146700
+ await input.onPersisted?.(updated);
146701
+ return { observation: updated, upserted: true };
146650
146702
  }
146703
+ if (!persistedCallbackInvoked) await input.onPersisted?.(observation);
146651
146704
  await bindObservationCodeRefsBestEffort(observation);
146652
146705
  queueObservationQualification(observation);
146653
146706
  if (resolveObservationVisibility(observation) === "project") {