@ykurilov/agent-chat-images 0.80.1 → 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";
@@ -23,6 +26,7 @@ import { SpendingLimitReachedError as SpendingLimitReachedError2, declareToolTim
23
26
 
24
27
  // src/download-image.ts
25
28
  import { detectFileKind } from "@ykurilov/agent-chat-attachments";
29
+ import { dnsHostResolver, sharedPinnedFetch, WebToolError } from "@ykurilov/agent-chat-web-tools/network";
26
30
 
27
31
  // src/download-address.ts
28
32
  var PRIVATE_IPV4 = [
@@ -109,7 +113,7 @@ function pngSize(bytes) {
109
113
  var DOWNLOAD_TIMEOUT_MS = 6e4;
110
114
  var MAX_REDIRECTS = 3;
111
115
  var imageExtensions = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
112
- var failure = (reason) => new ImageGenerationError(reason, { refusedBeforeWork: false });
116
+ var failure = (reason, kind = "unknown") => new ImageGenerationError(reason, { refusedBeforeWork: false, kind });
113
117
  var megabytes = (bytes) => `${Math.round(bytes / (1024 * 1024) * 10) / 10} \u041C\u0411`;
114
118
  async function readWithin(response, maxBytes) {
115
119
  if (!response.body) return new Uint8Array();
@@ -140,15 +144,20 @@ function checkedAddress(url, base, trustedOrigin) {
140
144
  try {
141
145
  address = new URL(url, base);
142
146
  } catch {
143
- 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");
144
148
  }
145
- 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");
146
150
  return address;
147
151
  }
152
+ function fetchFor(input, address) {
153
+ if (input.fetch) return input.fetch;
154
+ if (input.trustedOrigin !== void 0 && address.origin === input.trustedOrigin) return globalThis.fetch;
155
+ return sharedPinnedFetch(input.resolveHost ?? dnsHostResolver);
156
+ }
148
157
  async function fetchImage(input, first, signal) {
149
158
  let address = first;
150
159
  for (let hop = 0; ; hop++) {
151
- const response = await input.fetch(address.href, { signal, redirect: "manual" });
160
+ const response = await fetchFor(input, address)(address.href, { signal, redirect: "manual" });
152
161
  const location = response.status >= 300 && response.status < 400 ? response.headers.get("location") : null;
153
162
  if (location === null) return response;
154
163
  await response.body?.cancel().catch(() => {
@@ -166,14 +175,15 @@ async function downloadGeneratedImage(input) {
166
175
  let bytes;
167
176
  try {
168
177
  const response = await fetchImage(input, address, signal);
169
- 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");
170
179
  const declared = Number(response.headers.get("content-length"));
171
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)}`);
172
181
  bytes = await readWithin(response, input.maxImageBytes);
173
182
  } catch (error) {
174
183
  if (input.abortSignal?.aborted) throw input.abortSignal.reason ?? error;
175
184
  if (error instanceof ImageGenerationError) throw error;
176
- 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");
177
187
  } finally {
178
188
  clearTimeout(timer);
179
189
  }
@@ -302,7 +312,7 @@ function createImageJobs(options) {
302
312
  const output = input.setup.output;
303
313
  const file = await downloadGeneratedImage({
304
314
  url,
305
- fetch: last.connection.fetch,
315
+ ...last.downloadFetch && { fetch: last.downloadFetch },
306
316
  maxImageBytes: options.maxImageBytes,
307
317
  ...last.trustedOrigin !== void 0 && { trustedOrigin: last.trustedOrigin },
308
318
  ...output && { transform: (bytes, mediaType) => postprocessImage(bytes, mediaType, output) },
@@ -534,6 +544,7 @@ function resolveImageSetups(choice) {
534
544
  sizes,
535
545
  priceUsd: roundUsd(sizePriceUsd * qualityFactor),
536
546
  ...provider.baseURL !== void 0 && { trustedOrigin: new URL(provider.baseURL).origin },
547
+ ...choice.fetch && { downloadFetch: choice.fetch },
537
548
  ...providerSettings && { providerSettings },
538
549
  ...settings.transparentBackground && { outputFormat: "png" }
539
550
  };
@@ -666,9 +677,14 @@ function markRecorded(record, usageId) {
666
677
  }));
667
678
  if (!found) return null;
668
679
  if (isOpen(record)) return { steps };
669
- const left = hasUnrecorded(steps);
680
+ const left = hasUnrecorded(steps) || hasPendingImageFailure(record);
670
681
  return { steps, active: left, ...!left && { leaseOwner: void 0, leaseUntil: void 0 } };
671
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
+ }
672
688
  function view(record) {
673
689
  return {
674
690
  id: record.id,
@@ -683,6 +699,7 @@ function view(record) {
683
699
  ...record.data !== void 0 && { data: record.data },
684
700
  status: record.status,
685
701
  ...record.reason !== void 0 && { reason: record.reason },
702
+ ...failureOf(record) !== void 0 && { failure: structuredClone(failureOf(record)) },
686
703
  ...record.result !== void 0 && { result: record.result },
687
704
  attempt: record.attempt,
688
705
  ...record.lateUntil !== void 0 && { lateUntil: record.lateUntil },
@@ -693,7 +710,7 @@ function view(record) {
693
710
  }
694
711
  var refused = (reason) => ({ status: "refused", reason });
695
712
  function checkOptions(options) {
696
- const positive = ["pollIntervalMs", "attemptTimeoutMs", "latePollIntervalMs", "callTimeoutMs", "acceptWaitMs", "leaseMs", "forgetFinishedAfterMs", "maxImageBytes"];
713
+ const positive = ["failureRetryMs", "pollIntervalMs", "attemptTimeoutMs", "latePollIntervalMs", "callTimeoutMs", "acceptWaitMs", "leaseMs", "forgetFinishedAfterMs", "maxImageBytes"];
697
714
  for (const name of positive) {
698
715
  const value = options[name];
699
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`);
@@ -704,6 +721,7 @@ function checkOptions(options) {
704
721
  }
705
722
  const window = options.lateResultWindowMs;
706
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");
707
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");
708
726
  }
709
727
  function createImageQueue(options) {
@@ -717,6 +735,7 @@ function createImageQueue(options) {
717
735
  const nowMs = () => now().getTime();
718
736
  const hide = setups.hideSecrets;
719
737
  const logError = (message) => options.logError?.(hide(message));
738
+ const failureRetryMs = options.failureRetryMs ?? 3e4;
720
739
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
721
740
  const attemptTimeoutMs = options.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;
722
741
  const attempts = options.attempts ?? 1;
@@ -853,7 +872,7 @@ function createImageQueue(options) {
853
872
  current = await updateJob(current, (fresh) => markRecorded(fresh, charge.usageId), () => true);
854
873
  if (!current) return;
855
874
  }
856
- 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);
857
876
  }
858
877
  async function chargeTask(rt, index, costUsd) {
859
878
  await write(rt, (record) => {
@@ -878,24 +897,29 @@ function createImageQueue(options) {
878
897
  (fresh) => {
879
898
  if (!isOpen(fresh)) return null;
880
899
  const steps = fresh.steps.map((step) => step.task ? { ...withoutTask(step), charges: [...step.charges, chargeOf(step.task, step.task.priceUsd)] } : step);
881
- 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 };
882
901
  },
883
902
  () => true
884
903
  );
885
904
  if (!closed) return;
886
905
  byJob.get(record.id)?.controller.abort(RUN_ENDED);
887
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
+ }
888
911
  }
889
912
  function openUsd(rt) {
890
913
  const record = rt.record;
891
914
  if (!record) return rt.reservedUsd;
892
- if (!isOpen(record)) return 0;
915
+ if (!isOpen(record) || !("setup" in rt)) return 0;
916
+ const setup = rt.setup;
893
917
  const late = record.lateUntil !== void 0;
894
918
  const last = record.steps.length - 1;
895
919
  return record.steps.reduce((total, step, index) => {
896
- 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;
897
921
  if (step.task) return total + step.task.priceUsd;
898
- return late ? total : total + (rt.setup.steps[index]?.priceUsd ?? 0);
922
+ return late ? total : total + (setup.steps[index]?.priceUsd ?? 0);
899
923
  }, 0);
900
924
  }
901
925
  function pendingUsd(ownerId, chatId, before) {
@@ -941,9 +965,10 @@ function createImageQueue(options) {
941
965
  }
942
966
  async function failedSend(rt, index, error, wholeAttempt) {
943
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) } };
944
969
  if (error instanceof ImageGenerationError && error.refusedBeforeWork) {
945
970
  await write(rt, (record) => ({ steps: withStep(record.steps, index, withoutTask) }));
946
- return { kind: "refused", reason: hide(error.reason) };
971
+ return { kind: "refused", reason: hide(error.reason), failure: failure2 };
947
972
  }
948
973
  let reason;
949
974
  if (error instanceof ImageGenerationError) reason = hide(error.reason);
@@ -953,7 +978,7 @@ function createImageQueue(options) {
953
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";
954
979
  }
955
980
  await chargeTask(rt, index);
956
- return { kind: "failed", reason };
981
+ return { kind: canRetryImageFailure(failure2.kind) ? "failed" : "refused", reason, failure: failure2 };
957
982
  }
958
983
  async function send(rt, index) {
959
984
  const step = rt.setup.steps[index];
@@ -1009,7 +1034,7 @@ function createImageQueue(options) {
1009
1034
  throwIfEnded(rt);
1010
1035
  if (error instanceof ImageGenerationError) {
1011
1036
  await chargeTask(rt, index);
1012
- 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) } } };
1013
1038
  }
1014
1039
  continue;
1015
1040
  }
@@ -1020,21 +1045,23 @@ function createImageQueue(options) {
1020
1045
  }
1021
1046
  if (state.status === "failed") {
1022
1047
  await chargeTask(rt, index, state.costUsd);
1023
- 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) } } };
1024
1049
  }
1025
1050
  }
1026
1051
  }
1027
- async function finishFailed(rt, reason, extra = {}) {
1052
+ async function finishFailed(rt, reason, extra = {}, failure2 = { kind: "unknown", reason }) {
1028
1053
  await write(rt, (record) => ({
1029
1054
  status: "failed",
1030
1055
  reason,
1031
1056
  lateUntil: void 0,
1032
1057
  ...extra,
1033
- 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,
1034
1061
  finishedAt: iso(nowMs()),
1035
- leaseOwner: void 0,
1036
- leaseUntil: void 0
1062
+ ...options.onFailed ? {} : { leaseOwner: void 0, leaseUntil: void 0 }
1037
1063
  }));
1064
+ rt.sent.resolve();
1038
1065
  }
1039
1066
  const nextAttempt = (rt) => write(rt, (record) => ({ attempt: record.attempt + 1, attemptDeadline: iso(nowMs() + attemptTimeoutMs) }));
1040
1067
  async function runStep(rt, index) {
@@ -1057,7 +1084,7 @@ function createImageQueue(options) {
1057
1084
  return true;
1058
1085
  }
1059
1086
  if (outcome.kind === "refused") {
1060
- await finishFailed(rt, outcome.reason);
1087
+ await finishFailed(rt, outcome.reason, {}, outcome.failure);
1061
1088
  return false;
1062
1089
  }
1063
1090
  if (outcome.kind === "failed") {
@@ -1065,12 +1092,12 @@ function createImageQueue(options) {
1065
1092
  await nextAttempt(rt);
1066
1093
  continue;
1067
1094
  }
1068
- await finishFailed(rt, outcome.reason);
1095
+ await finishFailed(rt, outcome.reason, {}, outcome.failure);
1069
1096
  return false;
1070
1097
  }
1071
1098
  if (late) {
1072
1099
  await chargeTask(rt, index);
1073
- await finishFailed(rt, rt.record.reason ?? timeoutReason);
1100
+ await finishFailed(rt, rt.record.reason ?? timeoutReason, {}, { kind: "transient", reason: rt.record.reason ?? timeoutReason });
1074
1101
  return false;
1075
1102
  }
1076
1103
  if (rt.record.attempt < attempts) {
@@ -1083,7 +1110,7 @@ function createImageQueue(options) {
1083
1110
  continue;
1084
1111
  }
1085
1112
  await chargeTask(rt, index);
1086
- await finishFailed(rt, timeoutReason);
1113
+ await finishFailed(rt, timeoutReason, {}, { kind: "transient", reason: timeoutReason });
1087
1114
  return false;
1088
1115
  }
1089
1116
  }
@@ -1102,7 +1129,7 @@ function createImageQueue(options) {
1102
1129
  try {
1103
1130
  file = await downloadGeneratedImage({
1104
1131
  url,
1105
- fetch: last.connection.fetch,
1132
+ ...last.downloadFetch && { fetch: last.downloadFetch },
1106
1133
  maxImageBytes,
1107
1134
  ...last.trustedOrigin !== void 0 && { trustedOrigin: last.trustedOrigin },
1108
1135
  abortSignal: rt.controller.signal,
@@ -1113,7 +1140,7 @@ function createImageQueue(options) {
1113
1140
  throwIfEnded(rt);
1114
1141
  if (attempt >= downloadAttempts) {
1115
1142
  if (rt.reorderOnDeliveryFailure) return "reorder";
1116
- 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" });
1117
1144
  return "done";
1118
1145
  }
1119
1146
  await sleep(pollIntervalMs, rt.controller.signal);
@@ -1138,7 +1165,7 @@ function createImageQueue(options) {
1138
1165
  } catch (error) {
1139
1166
  throwIfEnded(rt);
1140
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)}`);
1141
- 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" });
1142
1169
  return "done";
1143
1170
  }
1144
1171
  throwIfEnded(rt);
@@ -1174,16 +1201,62 @@ function createImageQueue(options) {
1174
1201
  try {
1175
1202
  await recordCharges(rt);
1176
1203
  if (isOpen(rt.record)) await generate(rt);
1204
+ await notifyFailure(rt);
1177
1205
  } catch (error) {
1178
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)}`);
1179
1207
  } finally {
1180
- rt.ended = true;
1181
- clearTimeout(rt.leaseTimer);
1182
- runtimes.delete(rt);
1183
- if (rt.record && byJob.get(rt.record.id) === rt) byJob.delete(rt.record.id);
1184
- 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 }));
1185
1241
  }
1186
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
+ }
1187
1260
  function keepLease(rt) {
1188
1261
  const renew = () => {
1189
1262
  rt.leaseTimer = setTimeout(async () => {
@@ -1199,10 +1272,13 @@ function createImageQueue(options) {
1199
1272
  };
1200
1273
  renew();
1201
1274
  }
1202
- function newRuntime(ownerId, chatId, setup, reservedUsd) {
1275
+ function newJobRuntime(ownerId, chatId, reservedUsd) {
1203
1276
  let resolve;
1204
1277
  const promise = new Promise((done) => resolve = done);
1205
- 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 };
1206
1282
  }
1207
1283
  function launch(rt, record) {
1208
1284
  rt.record = record;
@@ -1229,9 +1305,13 @@ function createImageQueue(options) {
1229
1305
  }
1230
1306
  async function claim(record) {
1231
1307
  if (stopped || byJob.has(record.id)) return;
1308
+ if (!isOpen(record) && hasPendingImageFailure(record) && !hasUnrecorded(record.steps) && Date.parse(record.failureNotification.nextAttemptAt) > nowMs()) return;
1232
1309
  const claimed = await store.update(record.id, { leaseOwner: me, leaseUntil: iso(nowMs() + leaseMs), updatedAt: iso(nowMs()) }, record.version);
1233
1310
  if (!claimed) return;
1234
- 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
+ }
1235
1315
  const setup = setupOf(claimed);
1236
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`);
1237
1317
  const rt = newRuntime(claimed.ownerId, claimed.chatId, setup, 0);
@@ -1350,11 +1430,13 @@ function createImageQueue(options) {
1350
1430
  throw error;
1351
1431
  }
1352
1432
  }
1353
- 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) {
1354
1435
  return underKey(ownerId, key, async () => {
1355
1436
  const latest = (await store.list({ ownerId, key })).at(-1);
1356
- if (!latest) return null;
1437
+ if (!latest || !matchesRetry(latest, guard)) return null;
1357
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");
1358
1440
  const setup = setupOf(latest);
1359
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`);
1360
1442
  const remaining = latest.steps.reduce((total, step, index) => step.urls ? total : total + (step.task?.priceUsd ?? setup.steps[index].priceUsd), 0);
@@ -1368,6 +1450,7 @@ function createImageQueue(options) {
1368
1450
  let current = latest;
1369
1451
  let revived = null;
1370
1452
  while (current?.status === "failed") {
1453
+ if (!matchesRetry(current, guard)) return null;
1371
1454
  if (stopped) return refused(QUEUE_STOPPED);
1372
1455
  const at = nowMs();
1373
1456
  revived = await reviveOrTaken(
@@ -1378,6 +1461,8 @@ function createImageQueue(options) {
1378
1461
  attempt: 1,
1379
1462
  attemptDeadline: iso(at + attemptTimeoutMs),
1380
1463
  reason: void 0,
1464
+ failure: void 0,
1465
+ failureNotification: void 0,
1381
1466
  lateUntil: void 0,
1382
1467
  deliveryFailed: void 0,
1383
1468
  finishedAt: void 0,
@@ -1389,13 +1474,14 @@ function createImageQueue(options) {
1389
1474
  current.version
1390
1475
  );
1391
1476
  if (revived === "taken") {
1477
+ if (guard.expectedJobId !== void 0) return null;
1392
1478
  const newest = (await store.list({ ownerId, key })).at(-1);
1393
1479
  return newest ? view(newest) : null;
1394
1480
  }
1395
1481
  if (revived) break;
1396
1482
  current = await store.get(latest.id);
1397
1483
  }
1398
- if (!revived) return current ? view(current) : null;
1484
+ if (!revived) return current && matchesRetry(current, guard) ? view(current) : null;
1399
1485
  if (stopped) {
1400
1486
  await updateJob(revived, (record) => record.leaseOwner === me ? { leaseOwner: void 0, leaseUntil: void 0 } : null, (record) => record.leaseOwner === me);
1401
1487
  return refused(QUEUE_STOPPED);
@@ -1441,12 +1527,14 @@ function createImageQueue(options) {
1441
1527
  void start();
1442
1528
  return {
1443
1529
  order,
1444
- 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"));
1445
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"));
1446
- const flight = JSON.stringify([ownerId, key]);
1534
+ const flight = JSON.stringify([ownerId, key, expectedJobId, expectedRun]);
1447
1535
  const running = retries.get(flight);
1448
1536
  if (running) return running;
1449
- const promise = retryJob(ownerId, key).finally(() => retries.delete(flight));
1537
+ const promise = retryJob(ownerId, key, { expectedJobId, expectedRun }).finally(() => retries.delete(flight));
1450
1538
  retries.set(flight, promise);
1451
1539
  return promise;
1452
1540
  },
@@ -1533,7 +1621,7 @@ function checkNumbers(options) {
1533
1621
  const limit = options.maxImagesPerTurn;
1534
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");
1535
1623
  if (options.mode !== "background") {
1536
- 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);
1537
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"`);
1538
1626
  }
1539
1627
  }
@@ -1752,6 +1840,8 @@ function createImageGeneration(options) {
1752
1840
  ...options.purposes && { purposes: options.purposes },
1753
1841
  ...options.fetch && { fetch: options.fetch },
1754
1842
  deliver: receiveQueued,
1843
+ ...options.onFailed !== void 0 && { onFailed: options.onFailed },
1844
+ ...options.failureRetryMs !== void 0 && { failureRetryMs: options.failureRetryMs },
1755
1845
  ...options.jobStore && { store: options.jobStore },
1756
1846
  ...options.spending && { spending: options.spending },
1757
1847
  ...options.pollIntervalMs !== void 0 && { pollIntervalMs: options.pollIntervalMs },