@ykurilov/agent-chat-images 0.81.0 → 0.82.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.
package/dist/index.js CHANGED
@@ -1,20 +1,23 @@
1
1
  import {
2
2
  ImageJobKeyTakenError,
3
+ hasPendingImageFailure,
3
4
  isOpenImageJob,
4
5
  memoryImageJobStore
5
- } from "./chunk-5IORNMTX.js";
6
+ } from "./chunk-JRBKNWG3.js";
6
7
  import {
7
8
  ImageGenerationError,
8
9
  aspectRatios,
10
+ canRetryImageFailure,
9
11
  extendImageCatalog,
10
12
  hideSecrets,
11
13
  imageQualities,
12
14
  imageSizes,
13
15
  kieProvider,
16
+ providerFailureKind,
14
17
  runwareProvider,
15
18
  standardImageCatalog,
16
19
  waitForImageTask
17
- } from "./chunk-33SNN6BB.js";
20
+ } from "./chunk-W74LUWVQ.js";
18
21
 
19
22
  // src/create-image-generation.ts
20
23
  import { tool } from "ai";
@@ -110,7 +113,7 @@ function pngSize(bytes) {
110
113
  var DOWNLOAD_TIMEOUT_MS = 6e4;
111
114
  var MAX_REDIRECTS = 3;
112
115
  var imageExtensions = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
113
- var failure = (reason) => new ImageGenerationError(reason, { refusedBeforeWork: false });
116
+ var failure = (reason, kind = "unknown") => new ImageGenerationError(reason, { refusedBeforeWork: false, kind });
114
117
  var megabytes = (bytes) => `${Math.round(bytes / (1024 * 1024) * 10) / 10} \u041C\u0411`;
115
118
  async function readWithin(response, maxBytes) {
116
119
  if (!response.body) return new Uint8Array();
@@ -141,9 +144,9 @@ function checkedAddress(url, base, trustedOrigin) {
141
144
  try {
142
145
  address = new URL(url, base);
143
146
  } catch {
144
- throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441");
147
+ throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441", "configuration");
145
148
  }
146
- if (!isAllowedImageAddress(address, trustedOrigin)) throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441");
149
+ if (!isAllowedImageAddress(address, trustedOrigin)) throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441", "configuration");
147
150
  return address;
148
151
  }
149
152
  function fetchFor(input, address) {
@@ -172,15 +175,15 @@ async function downloadGeneratedImage(input) {
172
175
  let bytes;
173
176
  try {
174
177
  const response = await fetchImage(input, address, signal);
175
- if (!response.ok) throw failure(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: ${response.status}`);
178
+ if (!response.ok) throw failure(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: ${response.status}`, response.status === 408 || response.status === 429 || response.status >= 500 ? "transient" : "configuration");
176
179
  const declared = Number(response.headers.get("content-length"));
177
180
  if (Number.isFinite(declared) && declared > input.maxImageBytes) throw failure(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 ${megabytes(input.maxImageBytes)}`);
178
181
  bytes = await readWithin(response, input.maxImageBytes);
179
182
  } catch (error) {
180
183
  if (input.abortSignal?.aborted) throw input.abortSignal.reason ?? error;
181
184
  if (error instanceof ImageGenerationError) throw error;
182
- if (error instanceof WebToolError && error.kind === "bad_address") throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441");
183
- throw failure(timeout.signal.aborted ? "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435 \u0443\u0441\u043F\u0435\u043B\u0430" : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C");
185
+ if (error instanceof WebToolError && error.kind === "bad_address") throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441", "configuration");
186
+ throw failure(timeout.signal.aborted ? "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435 \u0443\u0441\u043F\u0435\u043B\u0430" : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C", "transient");
184
187
  } finally {
185
188
  clearTimeout(timer);
186
189
  }
@@ -674,9 +677,14 @@ function markRecorded(record, usageId) {
674
677
  }));
675
678
  if (!found) return null;
676
679
  if (isOpen(record)) return { steps };
677
- const left = hasUnrecorded(steps);
680
+ const left = hasUnrecorded(steps) || hasPendingImageFailure(record);
678
681
  return { steps, active: left, ...!left && { leaseOwner: void 0, leaseUntil: void 0 } };
679
682
  }
683
+ function failureOf(record) {
684
+ if (record.failure) return record.failure;
685
+ if (record.status !== "failed" || record.lateUntil !== void 0 || !record.reason) return void 0;
686
+ return { kind: providerFailureKind(void 0, void 0, record.reason), reason: record.reason };
687
+ }
680
688
  function view(record) {
681
689
  return {
682
690
  id: record.id,
@@ -691,6 +699,7 @@ function view(record) {
691
699
  ...record.data !== void 0 && { data: record.data },
692
700
  status: record.status,
693
701
  ...record.reason !== void 0 && { reason: record.reason },
702
+ ...failureOf(record) !== void 0 && { failure: structuredClone(failureOf(record)) },
694
703
  ...record.result !== void 0 && { result: record.result },
695
704
  attempt: record.attempt,
696
705
  ...record.lateUntil !== void 0 && { lateUntil: record.lateUntil },
@@ -701,7 +710,7 @@ function view(record) {
701
710
  }
702
711
  var refused = (reason) => ({ status: "refused", reason });
703
712
  function checkOptions(options) {
704
- const positive = ["pollIntervalMs", "attemptTimeoutMs", "latePollIntervalMs", "callTimeoutMs", "acceptWaitMs", "leaseMs", "forgetFinishedAfterMs", "maxImageBytes"];
713
+ const positive = ["failureRetryMs", "pollIntervalMs", "attemptTimeoutMs", "latePollIntervalMs", "callTimeoutMs", "acceptWaitMs", "leaseMs", "forgetFinishedAfterMs", "maxImageBytes"];
705
714
  for (const name of positive) {
706
715
  const value = options[name];
707
716
  if (value !== void 0 && !(Number.isFinite(value) && value > 0)) throw new Error(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: ${name} \u2014 \u043D\u0443\u0436\u043D\u043E \u0447\u0438\u0441\u043B\u043E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F`);
@@ -712,6 +721,7 @@ function checkOptions(options) {
712
721
  }
713
722
  const window = options.lateResultWindowMs;
714
723
  if (window !== void 0 && !(Number.isFinite(window) && window >= 0)) throw new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: lateResultWindowMs \u2014 \u0447\u0438\u0441\u043B\u043E \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 \u043D\u0443\u043B\u044F");
724
+ if (options.onFailed !== void 0 && typeof options.onFailed !== "function") throw new TypeError("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: onFailed \u2014 \u043D\u0443\u0436\u043D\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u044F");
715
725
  if (typeof options.deliver !== "function") throw new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: deliver \u2014 \u043D\u0443\u0436\u043D\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u044F-\u043F\u0440\u0438\u0451\u043C\u043D\u0438\u043A");
716
726
  }
717
727
  function createImageQueue(options) {
@@ -725,6 +735,7 @@ function createImageQueue(options) {
725
735
  const nowMs = () => now().getTime();
726
736
  const hide = setups.hideSecrets;
727
737
  const logError = (message) => options.logError?.(hide(message));
738
+ const failureRetryMs = options.failureRetryMs ?? 3e4;
728
739
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
729
740
  const attemptTimeoutMs = options.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;
730
741
  const attempts = options.attempts ?? 1;
@@ -861,7 +872,7 @@ function createImageQueue(options) {
861
872
  current = await updateJob(current, (fresh) => markRecorded(fresh, charge.usageId), () => true);
862
873
  if (!current) return;
863
874
  }
864
- await updateJob(current, (fresh) => isOpen(fresh) || !fresh.active && fresh.leaseOwner === void 0 ? null : { active: hasUnrecorded(fresh.steps), leaseOwner: void 0, leaseUntil: void 0 }, () => true);
875
+ await updateJob(current, (fresh) => isOpen(fresh) || !fresh.active && fresh.leaseOwner === void 0 ? null : { active: hasUnrecorded(fresh.steps) || hasPendingImageFailure(fresh), leaseOwner: void 0, leaseUntil: void 0 }, () => true);
865
876
  }
866
877
  async function chargeTask(rt, index, costUsd) {
867
878
  await write(rt, (record) => {
@@ -886,24 +897,29 @@ function createImageQueue(options) {
886
897
  (fresh) => {
887
898
  if (!isOpen(fresh)) return null;
888
899
  const steps = fresh.steps.map((step) => step.task ? { ...withoutTask(step), charges: [...step.charges, chargeOf(step.task, step.task.priceUsd)] } : step);
889
- return { status, reason, run: fresh.run + 1, steps, lateUntil: void 0, active: hasUnrecorded(steps), finishedAt: iso(nowMs()), leaseOwner: void 0, leaseUntil: void 0 };
900
+ return { status, reason, failure: status === "failed" ? { kind: "configuration", reason: reason ?? "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0441\u043C\u0435\u043D\u0438\u043B\u0430\u0441\u044C" } : void 0, failureNotification: status === "failed" && options.onFailed ? { id: `image-failed:${fresh.id}:${fresh.run + 1}`, attempts: 0, nextAttemptAt: iso(nowMs()) } : void 0, run: fresh.run + 1, steps, lateUntil: void 0, active: hasUnrecorded(steps) || status === "failed" && options.onFailed !== void 0, finishedAt: iso(nowMs()), leaseOwner: void 0, leaseUntil: void 0 };
890
901
  },
891
902
  () => true
892
903
  );
893
904
  if (!closed) return;
894
905
  byJob.get(record.id)?.controller.abort(RUN_ENDED);
895
906
  await settleClosed(closed);
907
+ if (status === "failed" && options.onFailed) {
908
+ const pending = await store.get(closed.id);
909
+ if (pending && hasPendingImageFailure(pending)) await claim(pending);
910
+ }
896
911
  }
897
912
  function openUsd(rt) {
898
913
  const record = rt.record;
899
914
  if (!record) return rt.reservedUsd;
900
- if (!isOpen(record)) return 0;
915
+ if (!isOpen(record) || !("setup" in rt)) return 0;
916
+ const setup = rt.setup;
901
917
  const late = record.lateUntil !== void 0;
902
918
  const last = record.steps.length - 1;
903
919
  return record.steps.reduce((total, step, index) => {
904
- if (step.urls) return rt.reorderOnDeliveryFailure && index === last ? total + rt.setup.steps[index].priceUsd : total;
920
+ if (step.urls) return rt.reorderOnDeliveryFailure && index === last ? total + setup.steps[index].priceUsd : total;
905
921
  if (step.task) return total + step.task.priceUsd;
906
- return late ? total : total + (rt.setup.steps[index]?.priceUsd ?? 0);
922
+ return late ? total : total + (setup.steps[index]?.priceUsd ?? 0);
907
923
  }, 0);
908
924
  }
909
925
  function pendingUsd(ownerId, chatId, before) {
@@ -949,9 +965,10 @@ function createImageQueue(options) {
949
965
  }
950
966
  async function failedSend(rt, index, error, wholeAttempt) {
951
967
  throwIfEnded(rt);
968
+ const failure2 = { kind: error instanceof ImageGenerationError ? error.kind : error instanceof CallTimedOut ? "transient" : "unknown", reason: error instanceof ImageGenerationError ? hide(error.reason) : error instanceof CallTimedOut ? error.message : "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443", ...error instanceof ImageGenerationError && error.providerCode && { providerCode: hide(error.providerCode) } };
952
969
  if (error instanceof ImageGenerationError && error.refusedBeforeWork) {
953
970
  await write(rt, (record) => ({ steps: withStep(record.steps, index, withoutTask) }));
954
- return { kind: "refused", reason: hide(error.reason) };
971
+ return { kind: "refused", reason: hide(error.reason), failure: failure2 };
955
972
  }
956
973
  let reason;
957
974
  if (error instanceof ImageGenerationError) reason = hide(error.reason);
@@ -961,7 +978,7 @@ function createImageQueue(options) {
961
978
  reason = "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443";
962
979
  }
963
980
  await chargeTask(rt, index);
964
- return { kind: "failed", reason };
981
+ return { kind: canRetryImageFailure(failure2.kind) ? "failed" : "refused", reason, failure: failure2 };
965
982
  }
966
983
  async function send(rt, index) {
967
984
  const step = rt.setup.steps[index];
@@ -1017,7 +1034,7 @@ function createImageQueue(options) {
1017
1034
  throwIfEnded(rt);
1018
1035
  if (error instanceof ImageGenerationError) {
1019
1036
  await chargeTask(rt, index);
1020
- return { kind: "failed", reason: hide(error.reason) };
1037
+ return { kind: canRetryImageFailure(error.kind) ? "failed" : "refused", reason: hide(error.reason), failure: { kind: error.kind, reason: hide(error.reason), ...error.providerCode && { providerCode: hide(error.providerCode) } } };
1021
1038
  }
1022
1039
  continue;
1023
1040
  }
@@ -1028,21 +1045,23 @@ function createImageQueue(options) {
1028
1045
  }
1029
1046
  if (state.status === "failed") {
1030
1047
  await chargeTask(rt, index, state.costUsd);
1031
- return { kind: "failed", reason: hide(state.reason) };
1048
+ return { kind: canRetryImageFailure(state.failureKind) ? "failed" : "refused", reason: hide(state.reason), failure: { kind: state.failureKind ?? "unknown", reason: hide(state.reason), ...state.providerCode && { providerCode: hide(state.providerCode) } } };
1032
1049
  }
1033
1050
  }
1034
1051
  }
1035
- async function finishFailed(rt, reason, extra = {}) {
1052
+ async function finishFailed(rt, reason, extra = {}, failure2 = { kind: "unknown", reason }) {
1036
1053
  await write(rt, (record) => ({
1037
1054
  status: "failed",
1038
1055
  reason,
1039
1056
  lateUntil: void 0,
1040
1057
  ...extra,
1041
- active: hasUnrecorded(record.steps),
1058
+ failure: failure2,
1059
+ failureNotification: options.onFailed ? { id: `image-failed:${record.id}:${record.run}`, attempts: 0, nextAttemptAt: iso(nowMs()) } : void 0,
1060
+ active: hasUnrecorded(record.steps) || options.onFailed !== void 0,
1042
1061
  finishedAt: iso(nowMs()),
1043
- leaseOwner: void 0,
1044
- leaseUntil: void 0
1062
+ ...options.onFailed ? {} : { leaseOwner: void 0, leaseUntil: void 0 }
1045
1063
  }));
1064
+ rt.sent.resolve();
1046
1065
  }
1047
1066
  const nextAttempt = (rt) => write(rt, (record) => ({ attempt: record.attempt + 1, attemptDeadline: iso(nowMs() + attemptTimeoutMs) }));
1048
1067
  async function runStep(rt, index) {
@@ -1065,7 +1084,7 @@ function createImageQueue(options) {
1065
1084
  return true;
1066
1085
  }
1067
1086
  if (outcome.kind === "refused") {
1068
- await finishFailed(rt, outcome.reason);
1087
+ await finishFailed(rt, outcome.reason, {}, outcome.failure);
1069
1088
  return false;
1070
1089
  }
1071
1090
  if (outcome.kind === "failed") {
@@ -1073,12 +1092,12 @@ function createImageQueue(options) {
1073
1092
  await nextAttempt(rt);
1074
1093
  continue;
1075
1094
  }
1076
- await finishFailed(rt, outcome.reason);
1095
+ await finishFailed(rt, outcome.reason, {}, outcome.failure);
1077
1096
  return false;
1078
1097
  }
1079
1098
  if (late) {
1080
1099
  await chargeTask(rt, index);
1081
- await finishFailed(rt, rt.record.reason ?? timeoutReason);
1100
+ await finishFailed(rt, rt.record.reason ?? timeoutReason, {}, { kind: "transient", reason: rt.record.reason ?? timeoutReason });
1082
1101
  return false;
1083
1102
  }
1084
1103
  if (rt.record.attempt < attempts) {
@@ -1091,7 +1110,7 @@ function createImageQueue(options) {
1091
1110
  continue;
1092
1111
  }
1093
1112
  await chargeTask(rt, index);
1094
- await finishFailed(rt, timeoutReason);
1113
+ await finishFailed(rt, timeoutReason, {}, { kind: "transient", reason: timeoutReason });
1095
1114
  return false;
1096
1115
  }
1097
1116
  }
@@ -1121,7 +1140,7 @@ function createImageQueue(options) {
1121
1140
  throwIfEnded(rt);
1122
1141
  if (attempt >= downloadAttempts) {
1123
1142
  if (rt.reorderOnDeliveryFailure) return "reorder";
1124
- await finishFailed(rt, error instanceof ImageGenerationError ? hide(error.reason) : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C", { deliveryFailed: true });
1143
+ await finishFailed(rt, error instanceof ImageGenerationError ? hide(error.reason) : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C", { deliveryFailed: true }, { kind: error instanceof ImageGenerationError ? error.kind : "unknown", reason: error instanceof ImageGenerationError ? hide(error.reason) : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C" });
1125
1144
  return "done";
1126
1145
  }
1127
1146
  await sleep(pollIntervalMs, rt.controller.signal);
@@ -1146,7 +1165,7 @@ function createImageQueue(options) {
1146
1165
  } catch (error) {
1147
1166
  throwIfEnded(rt);
1148
1167
  logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u043F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 ${record.id}: ${errorText2(error)}`);
1149
- await finishFailed(rt, "\u041F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443", { deliveryFailed: true });
1168
+ await finishFailed(rt, "\u041F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443", { deliveryFailed: true }, { kind: "delivery", reason: "\u041F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443" });
1150
1169
  return "done";
1151
1170
  }
1152
1171
  throwIfEnded(rt);
@@ -1182,16 +1201,62 @@ function createImageQueue(options) {
1182
1201
  try {
1183
1202
  await recordCharges(rt);
1184
1203
  if (isOpen(rt.record)) await generate(rt);
1204
+ await notifyFailure(rt);
1185
1205
  } catch (error) {
1186
1206
  if (error !== RUN_ENDED && !rt.controller.signal.aborted) logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u0437\u0430\u0434\u0430\u0447\u0430 ${rt.record?.id ?? ""} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B\u0430\u0441\u044C: ${errorText2(error)}`);
1187
1207
  } finally {
1188
- rt.ended = true;
1189
- clearTimeout(rt.leaseTimer);
1190
- runtimes.delete(rt);
1191
- if (rt.record && byJob.get(rt.record.id) === rt) byJob.delete(rt.record.id);
1192
- rt.sent.resolve();
1208
+ finishRuntime(rt);
1209
+ }
1210
+ }
1211
+ function finishRuntime(rt) {
1212
+ rt.ended = true;
1213
+ clearTimeout(rt.leaseTimer);
1214
+ runtimes.delete(rt);
1215
+ if (rt.record && byJob.get(rt.record.id) === rt) byJob.delete(rt.record.id);
1216
+ rt.sent.resolve();
1217
+ }
1218
+ async function notifyFailure(rt) {
1219
+ const record = rt.record;
1220
+ const notice = record.failureNotification;
1221
+ if (!options.onFailed || !notice || notice.deliveredAt || record.status !== "failed" || !record.failure || record.lateUntil) return;
1222
+ if (Date.parse(notice.nextAttemptAt) > nowMs()) return;
1223
+ const reserved = await write(rt, (fresh) => ({ failureNotification: { ...fresh.failureNotification, attempts: fresh.failureNotification.attempts + 1, nextAttemptAt: iso(nowMs() + failureRetryMs) } }));
1224
+ const id = notice.id;
1225
+ const isCurrent = async () => {
1226
+ if (rt.controller.signal.aborted) return false;
1227
+ const fresh = await store.get(record.id);
1228
+ const current = fresh?.status === "failed" && fresh.run === record.run && fresh.leaseOwner === me && Date.parse(fresh.leaseUntil ?? "") > nowMs() && fresh.failureNotification?.id === id && fresh.failureNotification.deliveredAt === void 0 && !rt.controller.signal.aborted;
1229
+ if (!current) rt.controller.abort(RUN_ENDED);
1230
+ return current;
1231
+ };
1232
+ if (!await isCurrent()) throw RUN_ENDED;
1233
+ try {
1234
+ await options.onFailed({ id, run: reserved.run, job: view(reserved), failure: structuredClone(reserved.failure) }, { signal: rt.controller.signal, isCurrent });
1235
+ if (!await isCurrent()) throw RUN_ENDED;
1236
+ await write(rt, (fresh) => ({ failureNotification: { ...fresh.failureNotification, deliveredAt: iso(nowMs()) }, active: hasUnrecorded(fresh.steps), leaseOwner: void 0, leaseUntil: void 0 }));
1237
+ } catch {
1238
+ throwIfEnded(rt);
1239
+ logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u0441\u043E\u0431\u044B\u0442\u0438\u0435 ${id} \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0441\u044F \u0431\u0435\u0437 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438`);
1240
+ await write(rt, () => ({ active: true, leaseOwner: void 0, leaseUntil: void 0 }));
1193
1241
  }
1194
1242
  }
1243
+ function launchFailure(record) {
1244
+ const rt = newJobRuntime(record.ownerId, record.chatId, 0);
1245
+ rt.record = record;
1246
+ runtimes.add(rt);
1247
+ byJob.set(record.id, rt);
1248
+ keepLease(rt);
1249
+ rt.done = (async () => {
1250
+ try {
1251
+ await recordCharges(rt);
1252
+ await notifyFailure(rt);
1253
+ } catch (error) {
1254
+ if (error !== RUN_ENDED && !rt.controller.signal.aborted) logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u0441\u043E\u0431\u044B\u0442\u0438\u0435 ${record.id} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B\u043E\u0441\u044C, \u0431\u0443\u0434\u0435\u0442 \u043F\u043E\u0434\u043E\u0431\u0440\u0430\u043D\u043E \u043F\u043E\u0441\u043B\u0435 \u0430\u0440\u0435\u043D\u0434\u044B`);
1255
+ } finally {
1256
+ finishRuntime(rt);
1257
+ }
1258
+ })();
1259
+ }
1195
1260
  function keepLease(rt) {
1196
1261
  const renew = () => {
1197
1262
  rt.leaseTimer = setTimeout(async () => {
@@ -1207,10 +1272,13 @@ function createImageQueue(options) {
1207
1272
  };
1208
1273
  renew();
1209
1274
  }
1210
- function newRuntime(ownerId, chatId, setup, reservedUsd) {
1275
+ function newJobRuntime(ownerId, chatId, reservedUsd) {
1211
1276
  let resolve;
1212
1277
  const promise = new Promise((done) => resolve = done);
1213
- return { ownerId, chatId, setup, reservedUsd, controller: new AbortController(), chain: Promise.resolve(), sent: { promise, resolve }, ended: false, checkNow: false, reorderOnDeliveryFailure: false };
1278
+ return { ownerId, chatId, reservedUsd, controller: new AbortController(), chain: Promise.resolve(), sent: { promise, resolve }, ended: false, checkNow: false, reorderOnDeliveryFailure: false };
1279
+ }
1280
+ function newRuntime(ownerId, chatId, setup, reservedUsd) {
1281
+ return { ...newJobRuntime(ownerId, chatId, reservedUsd), setup };
1214
1282
  }
1215
1283
  function launch(rt, record) {
1216
1284
  rt.record = record;
@@ -1237,9 +1305,13 @@ function createImageQueue(options) {
1237
1305
  }
1238
1306
  async function claim(record) {
1239
1307
  if (stopped || byJob.has(record.id)) return;
1308
+ if (!isOpen(record) && hasPendingImageFailure(record) && !hasUnrecorded(record.steps) && Date.parse(record.failureNotification.nextAttemptAt) > nowMs()) return;
1240
1309
  const claimed = await store.update(record.id, { leaseOwner: me, leaseUntil: iso(nowMs() + leaseMs), updatedAt: iso(nowMs()) }, record.version);
1241
1310
  if (!claimed) return;
1242
- if (!isOpen(claimed)) return settleClosed(claimed);
1311
+ if (!isOpen(claimed)) {
1312
+ if (options.onFailed && hasPendingImageFailure(claimed) && Date.parse(claimed.failureNotification.nextAttemptAt) <= nowMs()) return launchFailure(claimed);
1313
+ return settleClosed(claimed);
1314
+ }
1243
1315
  const setup = setupOf(claimed);
1244
1316
  if (!setup) return closeJob(claimed, "failed", `\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0441\u043C\u0435\u043D\u0438\u043B\u0430\u0441\u044C: \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \xAB${claimed.purpose}\xBB \u0432 \u043D\u0435\u0439 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442`);
1245
1317
  const rt = newRuntime(claimed.ownerId, claimed.chatId, setup, 0);
@@ -1358,11 +1430,13 @@ function createImageQueue(options) {
1358
1430
  throw error;
1359
1431
  }
1360
1432
  }
1361
- function retryJob(ownerId, key) {
1433
+ const matchesRetry = (record, guard) => guard.expectedJobId === void 0 || record?.id === guard.expectedJobId && record.run === guard.expectedRun;
1434
+ function retryJob(ownerId, key, guard) {
1362
1435
  return underKey(ownerId, key, async () => {
1363
1436
  const latest = (await store.list({ ownerId, key })).at(-1);
1364
- if (!latest) return null;
1437
+ if (!latest || !matchesRetry(latest, guard)) return null;
1365
1438
  if (latest.status !== "failed") return view(latest);
1439
+ if (failureOf(latest)?.kind === "content") return refused("\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435: \u0442\u043E\u0442 \u0436\u0435 \u0437\u0430\u043F\u0440\u043E\u0441 \u043D\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442\u0441\u044F; \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D\u043D\u0443\u044E \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u0443");
1366
1440
  const setup = setupOf(latest);
1367
1441
  if (!setup) return refused(`\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \xAB${latest.purpose}\xBB \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442`);
1368
1442
  const remaining = latest.steps.reduce((total, step, index) => step.urls ? total : total + (step.task?.priceUsd ?? setup.steps[index].priceUsd), 0);
@@ -1376,6 +1450,7 @@ function createImageQueue(options) {
1376
1450
  let current = latest;
1377
1451
  let revived = null;
1378
1452
  while (current?.status === "failed") {
1453
+ if (!matchesRetry(current, guard)) return null;
1379
1454
  if (stopped) return refused(QUEUE_STOPPED);
1380
1455
  const at = nowMs();
1381
1456
  revived = await reviveOrTaken(
@@ -1386,6 +1461,8 @@ function createImageQueue(options) {
1386
1461
  attempt: 1,
1387
1462
  attemptDeadline: iso(at + attemptTimeoutMs),
1388
1463
  reason: void 0,
1464
+ failure: void 0,
1465
+ failureNotification: void 0,
1389
1466
  lateUntil: void 0,
1390
1467
  deliveryFailed: void 0,
1391
1468
  finishedAt: void 0,
@@ -1397,13 +1474,14 @@ function createImageQueue(options) {
1397
1474
  current.version
1398
1475
  );
1399
1476
  if (revived === "taken") {
1477
+ if (guard.expectedJobId !== void 0) return null;
1400
1478
  const newest = (await store.list({ ownerId, key })).at(-1);
1401
1479
  return newest ? view(newest) : null;
1402
1480
  }
1403
1481
  if (revived) break;
1404
1482
  current = await store.get(latest.id);
1405
1483
  }
1406
- if (!revived) return current ? view(current) : null;
1484
+ if (!revived) return current && matchesRetry(current, guard) ? view(current) : null;
1407
1485
  if (stopped) {
1408
1486
  await updateJob(revived, (record) => record.leaseOwner === me ? { leaseOwner: void 0, leaseUntil: void 0 } : null, (record) => record.leaseOwner === me);
1409
1487
  return refused(QUEUE_STOPPED);
@@ -1449,12 +1527,14 @@ function createImageQueue(options) {
1449
1527
  void start();
1450
1528
  return {
1451
1529
  order,
1452
- retry({ ownerId, key }) {
1530
+ retry({ ownerId, key, expectedJobId, expectedRun }) {
1531
+ const guarded = expectedJobId !== void 0 || expectedRun !== void 0;
1532
+ if (guarded && (typeof expectedJobId !== "string" || expectedJobId.length === 0 || !Number.isInteger(expectedRun) || expectedRun < 1)) return Promise.reject(new TypeError("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: expectedJobId \u0438 expectedRun \u043F\u0435\u0440\u0435\u0434\u0430\u044E\u0442\u0441\u044F \u0432\u043C\u0435\u0441\u0442\u0435; run \u2014 \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 1"));
1453
1533
  if (stopped) return Promise.reject(new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 (stop) \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u043D\u0435 \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F"));
1454
- const flight = JSON.stringify([ownerId, key]);
1534
+ const flight = JSON.stringify([ownerId, key, expectedJobId, expectedRun]);
1455
1535
  const running = retries.get(flight);
1456
1536
  if (running) return running;
1457
- const promise = retryJob(ownerId, key).finally(() => retries.delete(flight));
1537
+ const promise = retryJob(ownerId, key, { expectedJobId, expectedRun }).finally(() => retries.delete(flight));
1458
1538
  retries.set(flight, promise);
1459
1539
  return promise;
1460
1540
  },
@@ -1541,7 +1621,7 @@ function checkNumbers(options) {
1541
1621
  const limit = options.maxImagesPerTurn;
1542
1622
  if (limit !== void 0 && !(Number.isInteger(limit) && limit >= 1)) throw new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: maxImagesPerTurn \u2014 \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 1");
1543
1623
  if (options.mode !== "background") {
1544
- const backgroundOnly = ["jobStore", "pollIntervalMs", "attempts", "onQueued"].filter((name) => options[name] !== void 0);
1624
+ const backgroundOnly = ["jobStore", "pollIntervalMs", "attempts", "onQueued", "onFailed", "failureRetryMs"].filter((name) => options[name] !== void 0);
1545
1625
  if (backgroundOnly.length > 0) throw new Error(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: ${backgroundOnly.join(", ")} \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F mode: "background"`);
1546
1626
  }
1547
1627
  }
@@ -1760,6 +1840,8 @@ function createImageGeneration(options) {
1760
1840
  ...options.purposes && { purposes: options.purposes },
1761
1841
  ...options.fetch && { fetch: options.fetch },
1762
1842
  deliver: receiveQueued,
1843
+ ...options.onFailed !== void 0 && { onFailed: options.onFailed },
1844
+ ...options.failureRetryMs !== void 0 && { failureRetryMs: options.failureRetryMs },
1763
1845
  ...options.jobStore && { store: options.jobStore },
1764
1846
  ...options.spending && { spending: options.spending },
1765
1847
  ...options.pollIntervalMs !== void 0 && { pollIntervalMs: options.pollIntervalMs },