@ykurilov/agent-chat-core 0.56.0 → 0.58.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 (34) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +2 -0
  3. package/dist/access.js +2 -2
  4. package/dist/{chunk-SNPF4BPB.js → chunk-FBULM4V6.js} +2 -2
  5. package/dist/{chunk-PWGI6NH6.js → chunk-FOAIG4DY.js} +1 -1
  6. package/dist/chunk-FOAIG4DY.js.map +1 -0
  7. package/dist/{chunk-7M7M7DAN.js → chunk-KAG3QTZC.js} +661 -46
  8. package/dist/chunk-KAG3QTZC.js.map +1 -0
  9. package/dist/index.js +77 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/storage-contract.js +87 -5
  12. package/dist/storage-contract.js.map +1 -1
  13. package/dist/types/agent-run/garbled-tail.d.ts +69 -0
  14. package/dist/types/agent-run/next-attempt.d.ts +12 -1
  15. package/dist/types/agent-run/reply-checks.d.ts +3 -1
  16. package/dist/types/agent-run/run-agent-turn.d.ts +6 -0
  17. package/dist/types/agent-run/staff-replies.d.ts +7 -0
  18. package/dist/types/bursts/message-bursts.d.ts +12 -0
  19. package/dist/types/chat-events/chat-events.d.ts +7 -0
  20. package/dist/types/chat-service.d.ts +37 -0
  21. package/dist/types/helpers/run-helper.d.ts +3 -0
  22. package/dist/types/index.d.ts +5 -3
  23. package/dist/types/provider-errors/classify-provider-error.d.ts +5 -0
  24. package/dist/types/provider-errors/error-signs.d.ts +2 -2
  25. package/dist/types/provider-errors/provider-outages.d.ts +41 -0
  26. package/dist/types/storage/chat-storage.d.ts +47 -0
  27. package/dist/types/storage/postgres-migrations.d.ts +28 -0
  28. package/dist/types/storage/storage-contract.d.ts +5 -0
  29. package/dist/types/turn-hooks/turn-gate.d.ts +2 -0
  30. package/dist/types/turn-hooks/turn-hooks.d.ts +7 -0
  31. package/package.json +1 -1
  32. package/dist/chunk-7M7M7DAN.js.map +0 -1
  33. package/dist/chunk-PWGI6NH6.js.map +0 -1
  34. /package/dist/{chunk-SNPF4BPB.js.map → chunk-FBULM4V6.js.map} +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  TurnRefusedError,
4
4
  normalizeTurnRefusal,
5
5
  validateAppEvent
6
- } from "./chunk-PWGI6NH6.js";
6
+ } from "./chunk-FOAIG4DY.js";
7
7
 
8
8
  // src/conversation/types.ts
9
9
  var CURRENT_FORMAT_VERSION = 1;
@@ -1718,6 +1718,9 @@ var DEFAULT_BURST_QUIET_MS = 3e3;
1718
1718
  var MAX_BURST_QUIET_MS = 6e5;
1719
1719
  var BURST_PENDING_GRACE_MS = 12e4;
1720
1720
  var BURST_BUSY_RETRY_MS = 1e3;
1721
+ var BURST_HOLD_MS = 3e4;
1722
+ var BURST_HOLD_RENEW_MS = 1e4;
1723
+ var BURST_SWEEP_LIMIT = 10;
1721
1724
  function burstPart(quietMs, at) {
1722
1725
  return { type: BURST_PART_TYPE, data: { quietMs, at } };
1723
1726
  }
@@ -1826,6 +1829,9 @@ function createBurstWaits() {
1826
1829
  wake(chatId) {
1827
1830
  for (const entry of [...timers]) if (entry.chatId === chatId) entry.done();
1828
1831
  },
1832
+ wakeAll() {
1833
+ for (const entry of [...timers]) entry.done();
1834
+ },
1829
1835
  has: (chatId) => waiting.has(chatId),
1830
1836
  async holding(chatId, work) {
1831
1837
  enter(chatId);
@@ -2478,10 +2484,9 @@ var NOT_RETRYABLE_SIGNS = signs([
2478
2484
  "available balance",
2479
2485
  "insufficient_quota",
2480
2486
  "out of budget",
2481
- "quota exceeded",
2482
- "billing",
2483
2487
  "subscription_sharing_usage_limit_exceeded"
2484
2488
  ]);
2489
+ var QUOTA_WORDS = signs(["quota exceeded", "billing"]);
2485
2490
  var RETRYABLE_SIGNS = signs([
2486
2491
  "overloaded",
2487
2492
  "currently experiencing high demand",
@@ -2528,8 +2533,8 @@ function isContextOverflow(text, statusCode) {
2528
2533
  if (NOT_OVERFLOW_SIGNS.some((sign) => sign.test(text))) return false;
2529
2534
  return OVERFLOW_SIGNS.some((sign) => sign.test(text));
2530
2535
  }
2531
- function isNotRetryable(text) {
2532
- return NOT_RETRYABLE_SIGNS.test(text);
2536
+ function isNotRetryable(text, statusCode) {
2537
+ return NOT_RETRYABLE_SIGNS.test(text) || statusCode !== 429 && QUOTA_WORDS.test(text);
2533
2538
  }
2534
2539
  function looksTransient(text) {
2535
2540
  return RETRYABLE_SIGNS.test(text);
@@ -2567,7 +2572,7 @@ function isTransient(error) {
2567
2572
  if (verdict === "false") return false;
2568
2573
  const status = errorStatusCode(error);
2569
2574
  const text = errorText(error);
2570
- if (isNotRetryable(text) || isContextOverflow(text, status)) return false;
2575
+ if (isNotRetryable(text, status) || isContextOverflow(text, status)) return false;
2571
2576
  if (status !== void 0) return RETRYABLE_STATUS_CODES.has(status) || status >= 500;
2572
2577
  if (APICallError2.isInstance(error) && error.isRetryable) return true;
2573
2578
  return looksTransient(text);
@@ -2741,6 +2746,9 @@ function sleep(ms, signal) {
2741
2746
  }
2742
2747
 
2743
2748
  // src/provider-errors/classify-provider-error.ts
2749
+ var KEY_SIGNS = /api[ _-]?key|authenticat|unauthori[sz]ed|credential|access[ _-]?token|key (?:is )?(?:invalid|revoked|expired|disabled)/i;
2750
+ var FUNDS_SIGNS = /key limit exceeded|insufficient (?:credits|funds|balance)/i;
2751
+ var MODERATION_SIGNS = /flagged|moderation|content[ _-]?policy|safety/i;
2744
2752
  var RATE_LIMIT_SIGNS = /rate.?limit|too many requests|throttl|please wait|ResourceExhausted/i;
2745
2753
  function classifyProviderError(error, provider) {
2746
2754
  if (error instanceof MissingProviderKeyError) return "invalid_key";
@@ -2750,8 +2758,12 @@ function classifyProviderError(error, provider) {
2750
2758
  for (const hint of provider?.errorHints ?? []) {
2751
2759
  if (hint.match.test(text)) return hint.kind;
2752
2760
  }
2753
- if (status === 401 || status === 403) return "invalid_key";
2754
- if (status === 402 || isNotRetryable(text)) return "insufficient_funds";
2761
+ if (status === 401) return "invalid_key";
2762
+ if (status === 403) {
2763
+ if (KEY_SIGNS.test(text)) return "invalid_key";
2764
+ return FUNDS_SIGNS.test(text) || isNotRetryable(text) ? "insufficient_funds" : "other";
2765
+ }
2766
+ if (status === 402 || isNotRetryable(text, status)) return "insufficient_funds";
2755
2767
  if (isContextOverflow(text, status)) return "context_overflow";
2756
2768
  if (status === 429) return "rate_limited";
2757
2769
  if (isProviderSilence(cause)) return "overloaded";
@@ -2759,6 +2771,10 @@ function classifyProviderError(error, provider) {
2759
2771
  if (RATE_LIMIT_SIGNS.test(text)) return "rate_limited";
2760
2772
  return looksTransient(text) ? "overloaded" : "other";
2761
2773
  }
2774
+ function isAccessRefusal(error) {
2775
+ const cause = underlyingError(error);
2776
+ return errorStatusCode(cause) === 403 && !MODERATION_SIGNS.test(errorText(cause));
2777
+ }
2762
2778
 
2763
2779
  // src/provider-errors/provider-failure-log.ts
2764
2780
  import { APICallError as APICallError3 } from "ai";
@@ -2784,6 +2800,49 @@ function oneLine(text) {
2784
2800
  return text.replace(/\s*[\r\n]+\s*/g, " ");
2785
2801
  }
2786
2802
 
2803
+ // src/provider-errors/provider-outages.ts
2804
+ function isProviderOutage(kind) {
2805
+ return kind === "insufficient_funds" || kind === "invalid_key";
2806
+ }
2807
+ var PROVIDER_OUTAGE_PAUSE_MS = 5 * 6e4;
2808
+ var PROVIDER_OUTAGE_ALERT_EVERY_MS = 15 * 6e4;
2809
+ var PROVIDER_OUTAGE_PROBE_MS = 6e4;
2810
+ var SAID = {
2811
+ insufficient_funds: "\u043A\u043E\u043D\u0447\u0438\u043B\u0438\u0441\u044C \u0434\u0435\u043D\u044C\u0433\u0438",
2812
+ invalid_key: "\u043A\u043B\u044E\u0447 \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u0442"
2813
+ };
2814
+ function createProviderOutages(options2) {
2815
+ const outages = /* @__PURE__ */ new Map();
2816
+ const alertedAt = /* @__PURE__ */ new Map();
2817
+ const pausedKind = (providerId) => {
2818
+ const outage = outages.get(providerId);
2819
+ if (!outage) return void 0;
2820
+ const time = options2.now();
2821
+ return time < outage.pausedUntil || time < (outage.probeUntil ?? 0) ? outage.kind : void 0;
2822
+ };
2823
+ return {
2824
+ report({ providerId, kind, fallbackProviderId, fallbacksPaused }) {
2825
+ const time = options2.now();
2826
+ outages.set(providerId, { kind, pausedUntil: time + PROVIDER_OUTAGE_PAUSE_MS });
2827
+ const last = alertedAt.get(providerId);
2828
+ if (last !== void 0 && time - last < PROVIDER_OUTAGE_ALERT_EVERY_MS) return;
2829
+ alertedAt.set(providerId, time);
2830
+ const route = fallbackProviderId !== void 0 ? `\u0445\u043E\u0434\u044B \u0438\u0434\u0443\u0442 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0439 ${fallbackProviderId}` : `${fallbacksPaused ? "\u0437\u0430\u043F\u0430\u0441\u043D\u044B\u0435 \u0442\u043E\u0436\u0435 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435" : "\u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u043D\u0435\u0442"}, \u0445\u043E\u0434\u044B \u043F\u0430\u0434\u0430\u044E\u0442 \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439`;
2831
+ options2.logError(`[\u0412\u041D\u0418\u041C\u0410\u041D\u0418\u0415 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440] ${providerId}: ${SAID[kind]} \u2014 ${route}`);
2832
+ },
2833
+ pausedKind,
2834
+ admit(providerId) {
2835
+ const kind = pausedKind(providerId);
2836
+ const outage = outages.get(providerId);
2837
+ if (kind === void 0 && outage) outage.probeUntil = options2.now() + PROVIDER_OUTAGE_PROBE_MS;
2838
+ return kind;
2839
+ },
2840
+ recovered(providerId) {
2841
+ outages.delete(providerId);
2842
+ }
2843
+ };
2844
+ }
2845
+
2787
2846
  // src/tool-approval/tool-approval-policy.ts
2788
2847
  var needsApprovalOf = (tool4) => tool4.needsApproval;
2789
2848
  function prepareToolApproval(tools, alwaysAllowed) {
@@ -2859,7 +2918,7 @@ function usageMinusJournaled(usage, journaled) {
2859
2918
  }
2860
2919
 
2861
2920
  // src/agent-run/build-model-request.ts
2862
- import { convertToModelMessages, wrapLanguageModel as wrapLanguageModel3 } from "ai";
2921
+ import { convertToModelMessages, wrapLanguageModel as wrapLanguageModel4 } from "ai";
2863
2922
 
2864
2923
  // src/conversation/hanging-tool-calls.ts
2865
2924
  var PRELIMINARY_OUTPUT_MAX_LENGTH = 2e3;
@@ -3535,6 +3594,37 @@ function withUnansweredMarked(messages) {
3535
3594
  );
3536
3595
  }
3537
3596
 
3597
+ // src/agent-run/staff-replies.ts
3598
+ function isStaffReply(message) {
3599
+ if (message.role !== "user") return false;
3600
+ const notices = message.parts.flatMap(noticesOfPart);
3601
+ return notices.length > 0 && notices.every((notice) => notice.kind === "staff");
3602
+ }
3603
+ function staffPartsJoined(parts) {
3604
+ return parts.flatMap((part) => {
3605
+ if (part.type !== AGENT_NOTICE_PART_TYPE) return joinedParts([part]);
3606
+ const text = noticePartForModel(part);
3607
+ return text ? [{ type: "text", text: `
3608
+
3609
+ ${text.text}` }] : [];
3610
+ });
3611
+ }
3612
+ function withStaffRepliesJoined(history) {
3613
+ const joined = [];
3614
+ let afterStaff = false;
3615
+ let changed = false;
3616
+ for (const message of history) {
3617
+ const previous = joined.at(-1);
3618
+ const staff = isStaffReply(message);
3619
+ if (previous?.role === "user" && message.role === "user" && (staff || afterStaff)) {
3620
+ joined[joined.length - 1] = { ...previous, parts: [...previous.parts, ...staff ? staffPartsJoined(message.parts) : joinedParts(message.parts)] };
3621
+ changed = true;
3622
+ } else joined.push(message);
3623
+ afterStaff = staff;
3624
+ }
3625
+ return changed ? joined : history;
3626
+ }
3627
+
3538
3628
  // src/frozen-context/frozen-context-messages.ts
3539
3629
  function frozenContextBase(history) {
3540
3630
  for (let index = history.length - 1; index >= 0; index -= 1) {
@@ -3660,6 +3750,298 @@ function withoutUndelivered(history) {
3660
3750
  return dropped.size === 0 ? history : history.filter((_, index) => !dropped.has(index));
3661
3751
  }
3662
3752
 
3753
+ // src/agent-run/garbled-tail.ts
3754
+ import { wrapLanguageModel as wrapLanguageModel3 } from "ai";
3755
+ var SIGN_ORDER = ["think", "role", "restart", "tool"];
3756
+ var TOKEN_SOURCE = String.raw`<\/?think>|<\|[^|\n<>]{1,40}\|>|<|[^|\n<>]{1,40}|>`;
3757
+ var TEMPLATE_TOKEN = new RegExp(TOKEN_SOURCE, "g");
3758
+ var ROLE_LINE = /(?<=^|\n)(user|assistant)(?=[ \t]*(?:\n|$)|[А-ЯЁа-яё])/g;
3759
+ var TOKEN_ROLE_LINE = /(?<=^|\n)(?:(?:<\|im_start\|>|<\|start_header_id\|>)(?:user|assistant)\b|<\|(?:user|assistant)\|>|<|(?:User|Assistant)|>)/g;
3760
+ var ANCHOR_HINT = /(?:^|\n)(?:(?:user|assistant)(?:[ \t]*(?:\n|$)|[А-ЯЁа-яё])|(?:<\|im_start\|>|<\|start_header_id\|>)(?:user|assistant)|<\|(?:user|assistant)\|>|<|(?:User|Assistant)|>)/;
3761
+ var ROLE_TOKEN = /(?:<\|im_start\|>|<\|start_header_id\|>)(user|assistant)\b|<\|(user|assistant)\|>|<|(User|Assistant)|>/g;
3762
+ var GLUED_SENTENCES = /[а-яё]{2}[.!?…][А-ЯЁ][а-яё]/;
3763
+ var changesVerdict = (lines) => ANCHOR_HINT.test(lines) || GLUED_SENTENCES.test(lines) || new RegExp(TOKEN_SOURCE).test(lines);
3764
+ var FINISHED = /[.!?…»)"]$/;
3765
+ var TRAILING_TOKENS = new RegExp(String.raw`(?:\s|${TOKEN_SOURCE})+$`);
3766
+ function findGarbledTail(parts) {
3767
+ if (!parts.some((part) => part.type === "text" && ANCHOR_HINT.test(part.text))) return null;
3768
+ const steps = stepsOf(parts);
3769
+ const markersByStep = steps.map((step) => step.texts.flatMap((text, at) => markersIn(text, step.texts.slice(0, at))));
3770
+ const bareRoles = new Set(markersByStep.flat().flatMap((marker) => marker.bareRole ? [marker.bareRole.name] : []));
3771
+ const tokenRoles = new Set(
3772
+ steps.flatMap((step) => step.texts.flatMap((text) => [...text.visible.matchAll(ROLE_TOKEN)].map((match) => (match[1] ?? match[2] ?? match[3]).toLowerCase())))
3773
+ );
3774
+ const dialog = bareRoles.size > 1;
3775
+ const template = tokenRoles.size > 1;
3776
+ for (const [index, step] of steps.entries()) {
3777
+ const markers = markersByStep[index].filter((marker) => !(dialog && marker.bareRole) && !(template && marker.roleToken));
3778
+ const found = garbleInStep(parts, step, markers);
3779
+ if (found) return found;
3780
+ }
3781
+ return null;
3782
+ }
3783
+ function cutGarbledTail(parts) {
3784
+ const found = findGarbledTail(parts);
3785
+ if (!found) return null;
3786
+ const stepEnd = parts.findIndex((part, index) => index > found.partIndex && part.type === "step-start");
3787
+ const end = stepEnd === -1 ? parts.length : stepEnd;
3788
+ const cutPart = parts[found.partIndex];
3789
+ const answer = cutPart.text.slice(0, found.offset).replace(TRAILING_TOKENS, "");
3790
+ let removedChars = cutPart.text.length - answer.length;
3791
+ const kept = parts.slice(0, found.partIndex);
3792
+ if (answer) kept.push({ ...cutPart, text: answer, state: "done" });
3793
+ for (const part of parts.slice(found.partIndex + 1, end)) {
3794
+ if (part.type === "text") removedChars += part.text.length;
3795
+ else if (part.type === "reasoning") continue;
3796
+ else if (!(stepEnd === -1 && part.type === LENGTH_CUT_PART_TYPE)) kept.push(part);
3797
+ }
3798
+ kept.push(...parts.slice(end));
3799
+ if (!kept.some((part) => part.type === "text" && /[\p{L}\p{N}]/u.test(part.text.replace(TEMPLATE_TOKEN, "")))) return null;
3800
+ const textIndex = answer ? found.partIndex : parts.slice(0, found.partIndex).findLastIndex((part) => part.type === "text");
3801
+ return { parts: kept, signs: found.signs, removedChars, textIndex };
3802
+ }
3803
+ function withoutGarbledTails(history) {
3804
+ let changed = false;
3805
+ const cleaned = history.map((message) => {
3806
+ if (message.role !== "assistant") return message;
3807
+ const parts = rememberedCut(message);
3808
+ if (!parts) return message;
3809
+ changed = true;
3810
+ return { ...message, parts };
3811
+ });
3812
+ return changed ? cleaned : history;
3813
+ }
3814
+ var rememberedCuts = /* @__PURE__ */ new Map();
3815
+ var REMEMBERED_CUTS_LIMIT = 500;
3816
+ function rememberedCut(message) {
3817
+ if (!message.parts.some((part) => part.type === "text" && ANCHOR_HINT.test(part.text))) return null;
3818
+ const source = JSON.stringify(message.parts);
3819
+ const known = rememberedCuts.get(message.id);
3820
+ if (known?.source === source) return known.parts;
3821
+ const parts = cutGarbledTail(message.parts)?.parts ?? null;
3822
+ rememberedCuts.delete(message.id);
3823
+ rememberedCuts.set(message.id, { source, parts });
3824
+ if (rememberedCuts.size > REMEMBERED_CUTS_LIMIT) rememberedCuts.delete(rememberedCuts.keys().next().value);
3825
+ return parts;
3826
+ }
3827
+ var DROPPED_CALL = { type: "tool-dropped", toolCallId: "", state: "input-available", input: {} };
3828
+ function createGarbledTailGuard() {
3829
+ let parts = [];
3830
+ let garbled = false;
3831
+ let droppedCalls = 0;
3832
+ let hinted = false;
3833
+ const worthChecking = (lines) => {
3834
+ hinted ||= ANCHOR_HINT.test(lines);
3835
+ return hinted && changesVerdict(lines);
3836
+ };
3837
+ const textAt = /* @__PURE__ */ new Map();
3838
+ const open = /* @__PURE__ */ new Set();
3839
+ const droppedIds = /* @__PURE__ */ new Set();
3840
+ const garbledNow = (withCall) => {
3841
+ if (!hinted) return false;
3842
+ const known = parts.map(
3843
+ (part, index) => part.type === "text" && open.has(index) ? { ...part, text: part.text.slice(0, part.text.lastIndexOf("\n") + 1) } : part
3844
+ );
3845
+ return cutGarbledTail(withCall ? [...known, DROPPED_CALL] : known) !== null;
3846
+ };
3847
+ const transform = (part, controller) => {
3848
+ switch (part.type) {
3849
+ case "text-start": {
3850
+ const index = parts.push({ type: "text", text: "" }) - 1;
3851
+ textAt.set(part.id, index);
3852
+ open.add(index);
3853
+ break;
3854
+ }
3855
+ case "text-delta": {
3856
+ if (garbled) return;
3857
+ const index = textAt.get(part.id);
3858
+ if (index === void 0) break;
3859
+ const text = parts[index];
3860
+ const before = text.text.length;
3861
+ const lineStart = text.text.lastIndexOf("\n") + 1;
3862
+ text.text += part.delta;
3863
+ if (!part.delta.includes("\n")) break;
3864
+ if (worthChecking(text.text.slice(lineStart, text.text.lastIndexOf("\n") + 1)) && garbledNow(false)) {
3865
+ garbled = true;
3866
+ const keep = text.text.lastIndexOf("\n") + 1 - before;
3867
+ text.text = text.text.slice(0, before + Math.max(keep, 0));
3868
+ if (keep > 0) controller.enqueue({ ...part, delta: part.delta.slice(0, keep) });
3869
+ return;
3870
+ }
3871
+ break;
3872
+ }
3873
+ case "text-end": {
3874
+ const index = textAt.get(part.id);
3875
+ if (index === void 0 || garbled) break;
3876
+ open.delete(index);
3877
+ const { text } = parts[index];
3878
+ if (worthChecking(text.slice(text.lastIndexOf("\n") + 1)) && garbledNow(false)) garbled = true;
3879
+ break;
3880
+ }
3881
+ case "reasoning-delta":
3882
+ if (garbled) return;
3883
+ break;
3884
+ case "tool-input-start":
3885
+ case "tool-call": {
3886
+ const callId = part.type === "tool-call" ? part.toolCallId : part.id;
3887
+ if (droppedIds.has(callId)) return;
3888
+ const started = part.type === "tool-call" && parts.some((known) => known.toolCallId === callId);
3889
+ if (started) break;
3890
+ if (garbled || garbledNow(true)) {
3891
+ garbled = true;
3892
+ droppedIds.add(callId);
3893
+ droppedCalls += 1;
3894
+ return;
3895
+ }
3896
+ parts.push({ type: "tool-call", toolCallId: callId, state: "input-streaming" });
3897
+ break;
3898
+ }
3899
+ case "tool-input-delta":
3900
+ case "tool-input-end":
3901
+ if (droppedIds.has(part.id)) return;
3902
+ break;
3903
+ case "finish":
3904
+ if (droppedCalls > 0 && part.finishReason.unified === "tool-calls") {
3905
+ controller.enqueue({ ...part, finishReason: { ...part.finishReason, unified: "stop" } });
3906
+ return;
3907
+ }
3908
+ break;
3909
+ }
3910
+ controller.enqueue(part);
3911
+ };
3912
+ return {
3913
+ /** Новая попытка: шаги прежней стёрты — всё сначала. */
3914
+ reset() {
3915
+ parts = [];
3916
+ garbled = false;
3917
+ droppedCalls = 0;
3918
+ hinted = false;
3919
+ textAt.clear();
3920
+ open.clear();
3921
+ droppedIds.clear();
3922
+ },
3923
+ /** Модель попытки под присмотром: каждый запрос — новый шаг. */
3924
+ wrap(model) {
3925
+ if (typeof model === "string") return model;
3926
+ return wrapLanguageModel3({
3927
+ model,
3928
+ middleware: {
3929
+ specificationVersion: "v4",
3930
+ wrapStream: async ({ doStream }) => {
3931
+ const result = await doStream();
3932
+ parts.push({ type: "step-start" });
3933
+ return { ...result, stream: result.stream.pipeThrough(new TransformStream({ transform })) };
3934
+ }
3935
+ }
3936
+ });
3937
+ },
3938
+ /** Срыв уже замечен в этой попытке: нового шага не нужно. */
3939
+ garbled: () => garbled,
3940
+ /** Сколько вызовов отброшено до запуска. */
3941
+ droppedCalls: () => droppedCalls,
3942
+ /** Части ответа для правила при сохранении: отброшенный вызов — на своём месте, в конце. */
3943
+ forCut: (saved) => droppedCalls > 0 ? [...saved, DROPPED_CALL] : saved,
3944
+ /** Заглушка отброшенного вызова из `forCut`: в запись не идёт. */
3945
+ isDroppedCall: (part) => part === DROPPED_CALL
3946
+ };
3947
+ }
3948
+ var isToolPart6 = (part) => part.type.startsWith("tool-") || part.type === "dynamic-tool";
3949
+ function stepsOf(parts) {
3950
+ const steps = [];
3951
+ let current = { from: 0, to: 0, texts: [] };
3952
+ parts.forEach((part, partIndex) => {
3953
+ if (part.type === "step-start") {
3954
+ current.to = partIndex;
3955
+ steps.push(current);
3956
+ current = { from: partIndex, to: partIndex, texts: [] };
3957
+ } else if (part.type === "text") current.texts.push({ partIndex, visible: withoutCodeAndQuotes(part.text) });
3958
+ });
3959
+ current.to = parts.length;
3960
+ steps.push(current);
3961
+ return steps.filter((step) => step.texts.length > 0);
3962
+ }
3963
+ function garbleInStep(parts, step, markers) {
3964
+ const roles = markers.filter((marker) => marker.kind === "role");
3965
+ const role = roles.find((marker) => finishedBefore(step.texts, marker));
3966
+ if (!role) return null;
3967
+ const after = (marker) => byPlace(marker, role) > 0;
3968
+ const tokens2 = markers.filter((marker) => marker.kind !== "role");
3969
+ const gluedAfter = () => textAfter(step.texts, role).some((text) => GLUED_SENTENCES.test(text));
3970
+ const others = /* @__PURE__ */ new Set();
3971
+ if (role.roleToken) {
3972
+ if (tokens2.some((marker) => !after(marker))) others.add("think");
3973
+ if (roles.some(after) || gluedAfter()) others.add("restart");
3974
+ } else {
3975
+ if (tokens2.length > 0) others.add("think");
3976
+ if (roles.some(after)) others.add("restart");
3977
+ if (role.bareRole.glued) {
3978
+ if (gluedAfter()) others.add("restart");
3979
+ if (parts.slice(role.partIndex + 1, step.to).some(isToolPart6)) others.add("tool");
3980
+ }
3981
+ }
3982
+ if (others.size === 0) return null;
3983
+ const closeThinks = markers.filter((marker) => marker.kind === "close-think" && !after(marker));
3984
+ const anchor = closeThinks.length >= 2 ? closeThinks[0] : role;
3985
+ const signs2 = /* @__PURE__ */ new Set(["role", ...others]);
3986
+ return { partIndex: anchor.partIndex, offset: anchor.offset, signs: SIGN_ORDER.filter((sign) => signs2.has(sign)) };
3987
+ }
3988
+ var byPlace = (a, b) => a.partIndex - b.partIndex || a.offset - b.offset;
3989
+ function textAfter(texts, anchor) {
3990
+ return texts.flatMap(
3991
+ (text) => text.partIndex < anchor.partIndex ? [] : [text.partIndex === anchor.partIndex ? text.visible.slice(anchor.offset) : text.visible]
3992
+ );
3993
+ }
3994
+ function markersIn(text, earlier) {
3995
+ const { partIndex, visible } = text;
3996
+ const markers = [];
3997
+ const hasTextBefore = (offset) => visibleBefore(earlier, visible, offset) !== "";
3998
+ const roleTokens = /* @__PURE__ */ new Set();
3999
+ for (const match of visible.matchAll(TOKEN_ROLE_LINE)) {
4000
+ roleTokens.add(match.index);
4001
+ if (hasTextBefore(match.index)) markers.push({ partIndex, offset: match.index, kind: "role", roleToken: true });
4002
+ }
4003
+ for (const match of visible.matchAll(TEMPLATE_TOKEN)) {
4004
+ if (roleTokens.has(match.index) || !hasTextBefore(match.index)) continue;
4005
+ markers.push({ partIndex, offset: match.index, kind: match[0] === "</think>" ? "close-think" : "token" });
4006
+ }
4007
+ for (const match of visible.matchAll(ROLE_LINE)) {
4008
+ if (!hasTextBefore(match.index)) continue;
4009
+ const name = match[1];
4010
+ const glued = /[А-ЯЁа-яё]/.test(visible[match.index + name.length] ?? "");
4011
+ markers.push({ partIndex, offset: match.index, kind: "role", bareRole: { name, glued } });
4012
+ }
4013
+ return markers.sort(byPlace);
4014
+ }
4015
+ function finishedBefore(texts, marker) {
4016
+ const at = texts.findIndex((text) => text.partIndex === marker.partIndex);
4017
+ return FINISHED.test(visibleBefore(texts.slice(0, at), texts[at].visible, marker.offset));
4018
+ }
4019
+ function visibleBefore(earlier, visible, offset) {
4020
+ const own = visible.slice(0, offset).replace(TRAILING_TOKENS, "");
4021
+ if (own) return own;
4022
+ for (let index = earlier.length - 1; index >= 0; index -= 1) {
4023
+ const text = earlier[index].visible.replace(TRAILING_TOKENS, "");
4024
+ if (text) return text;
4025
+ }
4026
+ return "";
4027
+ }
4028
+ function withoutCodeAndQuotes(text) {
4029
+ let fence = null;
4030
+ return text.split("\n").map((line) => {
4031
+ const opener = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
4032
+ if (fence !== null) {
4033
+ if (opener && opener[0] === fence[0] && opener.length >= fence.length && line.trim() === opener) fence = null;
4034
+ return " ".repeat(line.length);
4035
+ }
4036
+ if (opener) {
4037
+ fence = opener;
4038
+ return " ".repeat(line.length);
4039
+ }
4040
+ if (/^(?: {4}|\t)/.test(line) || /^\s*>/.test(line)) return " ".repeat(line.length);
4041
+ return line.replace(/(`+)[^`\n]*?\1/g, (code) => " ".repeat(code.length));
4042
+ }).join("\n");
4043
+ }
4044
+
3663
4045
  // src/agent-run/build-model-request.ts
3664
4046
  async function buildModelRequest(input) {
3665
4047
  const found = findModel(input.catalog, input.modelId, input.providerId);
@@ -3684,7 +4066,7 @@ async function buildModelRequest(input) {
3684
4066
  });
3685
4067
  const tools = input.tools ? sortToolsByName(input.tools) : void 0;
3686
4068
  const sendable = input.switchableTools ? withSwitchedOffCallsAsText(sendableHistory(input.history), input.switchableTools, tools) : sendableHistory(input.history);
3687
- const noted = withChannelNotes(withModeNotes(withBurstsJoined(withCorrectionsJoined(sendable)), input.modeNotes));
4069
+ const noted = withStaffRepliesJoined(withChannelNotes(withModeNotes(withBurstsJoined(withCorrectionsJoined(sendable)), input.modeNotes)));
3688
4070
  const held = input.frozenContext ? holdNoteAfterApprovals(withFrozenContext(noted, input.frozenContext)) : { messages: noted };
3689
4071
  const history = selectReasoningForModel(held.messages, {
3690
4072
  modelId: profile.id,
@@ -3720,7 +4102,7 @@ async function buildModelRequest(input) {
3720
4102
  };
3721
4103
  }
3722
4104
  function sendableHistory(history) {
3723
- return withoutUndelivered(history).filter((message) => (message.role !== "assistant" || hasAnswerContent(message.parts)) && !isStateMessage(message)).map(withAssistantFilesAsText).map((message) => {
4105
+ return withoutGarbledTails(withoutUndelivered(history)).filter((message) => (message.role !== "assistant" || hasAnswerContent(message.parts)) && !isStateMessage(message)).map(withAssistantFilesAsText).map((message) => {
3724
4106
  const closed = message.role === "assistant" ? closeHangingToolCalls(message.parts) : message.parts;
3725
4107
  const parts = withoutLoneSurrogatesInParts(closed);
3726
4108
  return parts === message.parts ? message : { ...message, parts };
@@ -3768,7 +4150,7 @@ function withoutLoneSurrogatesDeep(value) {
3768
4150
  }
3769
4151
  function withRequestWatch(model, provider, watch) {
3770
4152
  if (!watch || typeof model === "string") return model;
3771
- return wrapLanguageModel3({
4153
+ return wrapLanguageModel4({
3772
4154
  model,
3773
4155
  middleware: {
3774
4156
  specificationVersion: "v4",
@@ -3788,7 +4170,7 @@ function withRequestWatch(model, provider, watch) {
3788
4170
  });
3789
4171
  }
3790
4172
  function withTransientRetries(model, context) {
3791
- return typeof model === "string" ? model : wrapLanguageModel3({ model, middleware: createTransientRetryMiddleware(context) });
4173
+ return typeof model === "string" ? model : wrapLanguageModel4({ model, middleware: createTransientRetryMiddleware(context) });
3792
4174
  }
3793
4175
  function withLastMessageMark(messages, options2) {
3794
4176
  if (!options2 || messages.length === 0) return messages;
@@ -4019,8 +4401,14 @@ function decideNextAttempt(result, state) {
4019
4401
  if (state.overflowCompaction?.answerSteps) return { action: "compact-and-continue", scope: "answer-steps" };
4020
4402
  return { action: "fail" };
4021
4403
  }
4022
- if (!TRANSIENT_KINDS.has(result.kind)) return { action: "fail" };
4404
+ if (isProviderOutage(result.kind)) {
4405
+ const next2 = state.fallbackProviderIds[0];
4406
+ return next2 === void 0 ? { action: "fail" } : { action: "fallback", providerId: next2 };
4407
+ }
4408
+ const accessRefused = result.kind === "other" && isAccessRefusal(result.error);
4409
+ if (!TRANSIENT_KINDS.has(result.kind) && !accessRefused) return { action: "fail" };
4023
4410
  if (result.contentSeen) {
4411
+ if (accessRefused) return { action: "fail" };
4024
4412
  const auto = state.autoContinue;
4025
4413
  if (!auto || auto.approvalRequested || auto.done >= AUTO_CONTINUES_PER_TURN) return { action: "fail" };
4026
4414
  return { action: "auto-continue", pauseMs: autoContinuePauseMs({ index: auto.done, error: result.error, random: auto.random ?? Math.random }) };
@@ -4029,6 +4417,10 @@ function decideNextAttempt(result, state) {
4029
4417
  if (state.fallbackUsed || next === void 0) return { action: "fail" };
4030
4418
  return { action: "fallback", providerId: next };
4031
4419
  }
4420
+ function errorKindForPerson(lastKind, firstOutage) {
4421
+ if (firstOutage === void 0 || TRANSIENT_KINDS.has(lastKind) || lastKind === "context_overflow") return lastKind;
4422
+ return firstOutage;
4423
+ }
4032
4424
  function checkProviderFallback(setting, catalog) {
4033
4425
  if (setting === void 0 || setting === false) return;
4034
4426
  if (typeof setting !== "object" || setting === null || Array.isArray(setting)) {
@@ -5052,6 +5444,10 @@ function withTextAt(parts, index, text) {
5052
5444
  function createShownSteps() {
5053
5445
  let steps = 0;
5054
5446
  let textSteps = [];
5447
+ const resetsToText = (fromEnd) => {
5448
+ const step = textSteps.at(-1 - fromEnd);
5449
+ return step === void 0 || step === 0 ? null : steps - step + 1;
5450
+ };
5055
5451
  return {
5056
5452
  observe(chunk) {
5057
5453
  if (chunk.type === "start-step") steps += 1;
@@ -5062,10 +5458,9 @@ function createShownSteps() {
5062
5458
  }
5063
5459
  },
5064
5460
  /** Сколько шагов стереть с конца, чтобы убрать шаг последнего текста; `null` — текста на экране нет. */
5065
- resetsToLastText() {
5066
- const last = textSteps.at(-1);
5067
- return last === void 0 || last === 0 ? null : steps - last + 1;
5068
- }
5461
+ resetsToLastText: () => resetsToText(0),
5462
+ /** То же для текста `fromEnd`-го с конца (0 — последний); `null` — такого текста на экране нет. */
5463
+ resetsToText
5069
5464
  };
5070
5465
  }
5071
5466
  function replacementChunks(corrected, index, resets, providerId) {
@@ -5531,6 +5926,7 @@ async function runTurn(turnInput, emitRaw) {
5531
5926
  const externalContent = createExternalContentWatch();
5532
5927
  const emitShown = (chunk) => {
5533
5928
  shownSteps?.observe(chunk);
5929
+ screenSteps.observe(chunk);
5534
5930
  emitRaw(chunk);
5535
5931
  if (turnInput.liveDemos && externalContent.observe(chunk)) emitRaw(externalContentChunk());
5536
5932
  };
@@ -5538,6 +5934,7 @@ async function runTurn(turnInput, emitRaw) {
5538
5934
  const seenTexts = replyChecks && createSeenTexts();
5539
5935
  const heldText = replyChecks && (replyChecks.web === "hold" || (turnInput.channel ?? "web") !== "web") ? createHeldReplyText() : void 0;
5540
5936
  const shownSteps = replyChecks && createShownSteps();
5937
+ const screenSteps = createShownSteps();
5541
5938
  const emitToClient = (chunk) => {
5542
5939
  for (const passed of heldText ? heldText.pass(chunk) : [chunk]) emitShown(passed);
5543
5940
  };
@@ -5572,6 +5969,7 @@ async function runTurn(turnInput, emitRaw) {
5572
5969
  }
5573
5970
  );
5574
5971
  const turnUsage = createTurnUsage();
5972
+ const garbledTail = createGarbledTailGuard();
5575
5973
  const turnBudget = createTurnBudget(input.turnRules, { catalog: input.catalog, modelId: input.modelId, turnUsage });
5576
5974
  const messageUsage = () => {
5577
5975
  const spent = turnUsage.total();
@@ -5660,13 +6058,21 @@ async function runTurn(turnInput, emitRaw) {
5660
6058
  const seen = latestFrozenContextOf(resume.message.parts);
5661
6059
  if (seen) resumeNote = { ...resumeNote ?? { blocks: seen.blocks, sent: { changed: [], removed: [] }, base: messageId }, notices: turnNotices };
5662
6060
  }
5663
- const fallbackProviderIds = fallbackProviders({
6061
+ const outages = input.providerOutages ?? createProviderOutages({ now: () => (input.now ?? (() => /* @__PURE__ */ new Date()))().getTime(), logError });
6062
+ const allFallbackIds = fallbackProviders({
5664
6063
  catalog: input.catalog,
5665
6064
  modelId,
5666
6065
  chosenProviderId: input.providerId,
5667
6066
  env: input.env,
5668
6067
  ...input.providerFallback !== void 0 && { setting: input.providerFallback }
5669
6068
  });
6069
+ const fallbackProviderIds = allFallbackIds.filter((candidate) => outages.pausedKind(candidate) === void 0);
6070
+ let firstOutage;
6071
+ const primaryPaused = outages.admit(input.providerId);
6072
+ if (primaryPaused !== void 0 && fallbackProviderIds[0] !== void 0) {
6073
+ providerId = fallbackProviderIds[0];
6074
+ logError(`[\u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440] \u0447\u0430\u0442=${input.chatId} \u043C\u043E\u0434\u0435\u043B\u044C=${modelId} \u0441=${input.providerId} \u043D\u0430=${providerId} \u043F\u0440\u0438\u0447\u0438\u043D\u0430=${primaryPaused} \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435`);
6075
+ }
5670
6076
  emit({ type: "start", messageId, messageMetadata: { modelId, providerId: input.providerId } });
5671
6077
  if (input.liveDemos && chatHasExternalContent(await storage.listMessages(input.chatId)) && externalContent.announce()) emitRaw(externalContentChunk());
5672
6078
  const silenceSetting = resolveSilenceSwitch(input.providerFallback);
@@ -5739,6 +6145,7 @@ async function runTurn(turnInput, emitRaw) {
5739
6145
  emit,
5740
6146
  turnUsage,
5741
6147
  turnBudget,
6148
+ garbledTail,
5742
6149
  // Есть куда перейти при молчании — у того же провайдера оно не повторяется.
5743
6150
  repeatSilence: () => silenceTarget() === void 0,
5744
6151
  ...silenceSwitch.scope === "step" && switchedBySilence && {
@@ -5756,13 +6163,14 @@ async function runTurn(turnInput, emitRaw) {
5756
6163
  } catch (error2) {
5757
6164
  if (error2 instanceof MissingProviderKeyError) logError(error2.message);
5758
6165
  if (!input.abortSignal.aborted) {
5759
- errorKind = classify(error2);
6166
+ errorKind = errorKindForPerson(classify(error2), firstOutage);
5760
6167
  errors = [{ type: "error", errorText: errorKind }];
5761
6168
  }
5762
6169
  break;
5763
6170
  }
5764
6171
  const { result } = attempt;
5765
6172
  historyRewritten = false;
6173
+ if (result.outcome === "ok") outages.recovered(providerId);
5766
6174
  if (result.outcome !== "failed" || result.contentSeen) answeredProviderId = providerId;
5767
6175
  if (result.outcome === "ok" && backToPrimary) {
5768
6176
  backToPrimary = false;
@@ -5797,12 +6205,24 @@ async function runTurn(turnInput, emitRaw) {
5797
6205
  silenceSwitch: { done: silenceSwitches, max: silenceSwitch.maxSwitches, nextProviderId: silenceTarget() }
5798
6206
  });
5799
6207
  let next = stepsDone < maxSteps ? decideNextAttempt(result, nextState()) : { action: "fail" };
6208
+ const outage = isProviderOutage(result.kind) ? result.kind : void 0;
6209
+ if (outage) {
6210
+ if (providerId === input.providerId) firstOutage ??= outage;
6211
+ outages.report({
6212
+ providerId,
6213
+ kind: outage,
6214
+ ...next.action === "fallback" ? { fallbackProviderId: next.providerId } : { fallbacksPaused: allFallbackIds.some((candidate) => candidate !== providerId && outages.pausedKind(candidate) !== void 0) }
6215
+ });
6216
+ }
5800
6217
  if (next.action === "fallback") {
5801
6218
  logError(`[\u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440] \u0447\u0430\u0442=${input.chatId} \u043C\u043E\u0434\u0435\u043B\u044C=${modelId} \u0441=${providerId} \u043D\u0430=${next.providerId} \u043F\u0440\u0438\u0447\u0438\u043D\u0430=${result.kind} \u043A\u044D\u0448 \u043F\u043E\u0442\u0435\u0440\u044F\u043D`);
5802
- fallbackUsed = true;
6219
+ if (!outage) fallbackUsed = true;
5803
6220
  switchedBySilence = false;
5804
6221
  providerId = next.providerId;
5805
- answer.update(parts2, { modelId, providerId: answeredProviderId });
6222
+ const closing2 = result.contentSeen ? [...openParts.closingChunks(), ...closingChunksForHangingCalls(parts2)] : [];
6223
+ for (const chunk of closing2) emit(chunk);
6224
+ if (closing2.length > 0) answer.continueAfterBreak(parts2, { modelId, providerId: answeredProviderId });
6225
+ else answer.update(parts2, { modelId, providerId: answeredProviderId });
5806
6226
  turnUsage.closeAttempt();
5807
6227
  if (input.abortSignal.aborted) break;
5808
6228
  continue;
@@ -5874,8 +6294,8 @@ async function runTurn(turnInput, emitRaw) {
5874
6294
  turnUsage.closeAttempt();
5875
6295
  continue;
5876
6296
  }
5877
- errorKind = result.kind;
5878
- errors = attempt.errors;
6297
+ errorKind = errorKindForPerson(result.kind, firstOutage);
6298
+ errors = errorKind === result.kind ? attempt.errors : [{ type: "error", errorText: errorKind }];
5879
6299
  terminal = attempt.terminal;
5880
6300
  logError(
5881
6301
  describeProviderFailure(result.error, { chatId: input.chatId, modelId, providerId, kind: result.kind, env: input.env, catalog: input.catalog })
@@ -5942,6 +6362,15 @@ async function runTurn(turnInput, emitRaw) {
5942
6362
  const failed = input.abortSignal.aborted || errors.length > 0;
5943
6363
  const closing = !failed && pendingHumanInput(savedParts, input.waitingWidgets).length > 0 && (closure !== void 0 || await chatClosedNow());
5944
6364
  let parts = failed || closing ? closeHangingToolCalls(denyPendingApprovals(savedParts, INTERRUPTED_REASON, RESULT_UNKNOWN_REASON), { notStarted: toolQueue.notStarted }) : savedParts;
6365
+ const garbled = cutGarbledTail(garbledTail.forCut(parts));
6366
+ const droppedCalls = garbledTail.droppedCalls();
6367
+ const garbleLog = `\u0447\u0430\u0442=${input.chatId} \u043E\u0442\u0432\u0435\u0442=${messageId} \u043C\u043E\u0434\u0435\u043B\u044C=${modelId} \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440=${answeredProviderId}`;
6368
+ if (garbled) {
6369
+ logError(`[\u0445\u0432\u043E\u0441\u0442 \u043E\u0431\u0440\u0435\u0437\u0430\u043D] ${garbleLog} \u043F\u0440\u0438\u0437\u043D\u0430\u043A\u0438=${garbled.signs.join(",")} \u0437\u043D\u0430\u043A\u043E\u0432=${garbled.removedChars} \u0432\u044B\u0437\u043E\u0432\u043E\u0432_\u043D\u0435_\u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E=${droppedCalls}`);
6370
+ const cut = garbled.parts.filter((part) => !garbledTail.isDroppedCall(part));
6371
+ if (!takeover.signal.aborted) showCutAnswer(parts, cut, garbled.textIndex, { heldText, screenSteps, providerId: answeredProviderId, emit: emitShown });
6372
+ parts = cut;
6373
+ } else if (findGarbledTail(garbledTail.forCut(parts))) logError(`[\u0445\u0432\u043E\u0441\u0442 \u043D\u0435 \u043E\u0431\u0440\u0435\u0437\u0430\u043D: \u043E\u0442\u0432\u0435\u0442 \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u0431\u044B \u043F\u0443\u0441\u0442\u044B\u043C] ${garbleLog}`);
5945
6374
  const status = input.abortSignal.aborted ? "stopped" : errors.length > 0 ? "error" : pendingHumanInput(parts, input.waitingWidgets).length > 0 ? "awaiting_approval" : "complete";
5946
6375
  if (endTurns.hasText()) logError(`[endTurn] \u0447\u0430\u0442=${input.chatId} \u0433\u043E\u0442\u043E\u0432\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u043D\u0435 \u043F\u043E\u043A\u0430\u0437\u0430\u043D: \u0445\u043E\u0434 \u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u0441\u043E \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u043C ${status}`);
5947
6376
  const textIndex = lastTextIndex(parts);
@@ -6255,8 +6684,10 @@ async function openAttempt(input, attempt, attemptSignal) {
6255
6684
  return true;
6256
6685
  };
6257
6686
  const { noteAfterApprovals, ...modelRequest } = request;
6687
+ attempt.garbledTail.reset();
6258
6688
  const result = streamText({
6259
6689
  ...modelRequest,
6690
+ model: attempt.garbledTail.wrap(modelRequest.model),
6260
6691
  ...approval.toolApproval && { toolApproval: gateToolApproval(approval.toolApproval, () => toolsClosed) },
6261
6692
  // Попытке — остаток шагов хода.
6262
6693
  // Помощник встал на подтверждении человека — ход ждёт, модель основного агента его незаконченным не видит.
@@ -6264,6 +6695,7 @@ async function openAttempt(input, attempt, attemptSignal) {
6264
6695
  // Инструмент кончил ход: без текста — стоп сразу; с текстом — ещё шаг подставной модели, его держит только помощник
6265
6696
  // на подтверждении (ход ждёт человека). Потолок шагов и прочие стопы готовый текст не теряют.
6266
6697
  stopWhen: async (options2) => {
6698
+ if (attempt.garbledTail.garbled()) return true;
6267
6699
  if (attempt.endTurns.requested()) {
6268
6700
  if (attempt.endTurns.hasText()) return helperAwaitsApproval(options2);
6269
6701
  attempt.endTurns.stopWithoutText();
@@ -6475,6 +6907,7 @@ function helperRunContext(input, toolQueue, logError) {
6475
6907
  ...input.toolTimeout && { toolTimeout: input.toolTimeout },
6476
6908
  ...input.sequentialTools && { sequentialTools: input.sequentialTools },
6477
6909
  ...input.providerFallback !== void 0 && { providerFallback: input.providerFallback },
6910
+ ...input.providerOutages && { providerOutages: input.providerOutages },
6478
6911
  ...input.modelSilence && { modelSilence: input.modelSilence },
6479
6912
  ...input.turnObserver && { turnObserver: input.turnObserver },
6480
6913
  ...input.assertWithinLimit && { assertWithinLimit: input.assertWithinLimit }
@@ -6599,6 +7032,19 @@ function withLastUserPart(history, part) {
6599
7032
  return history.map((message, position) => position === index ? { ...message, parts: [...message.parts, part] } : message);
6600
7033
  }
6601
7034
  var FINISH_RETRY_PAUSES_MS = [1e3, 3e3];
7035
+ function showCutAnswer(original, cut, anchor, screen) {
7036
+ if (anchor === -1) return;
7037
+ const textsAfter = original.slice(anchor + 1).filter((part) => part.type === "text").length;
7038
+ if (screen.heldText?.holdsText()) {
7039
+ if (textsAfter === 0 && lastTextIndex(cut) === anchor) {
7040
+ for (const chunk of screen.heldText.release(cut[anchor].text)) screen.emit(chunk);
7041
+ return;
7042
+ }
7043
+ for (const chunk of screen.heldText.release()) screen.emit(chunk);
7044
+ }
7045
+ const resets = screen.screenSteps.resetsToText(textsAfter);
7046
+ for (const chunk of resets && replacementChunks(cut, anchor, resets, screen.providerId) || []) screen.emit(chunk);
7047
+ }
6602
7048
  async function finishWithRetries(storage, messageId, final, logError) {
6603
7049
  for (let attempt = 0; ; attempt += 1) {
6604
7050
  let failure;
@@ -6692,16 +7138,26 @@ async function* helperSteps(input) {
6692
7138
  const messages = [...input.priorMessages ?? []];
6693
7139
  const maxSteps = helper.maxSteps ?? DEFAULT_HELPER_MAX_STEPS;
6694
7140
  const { modelId } = helper.model;
6695
- const fallbackProviderIds = fallbackProviders({
7141
+ const outages = context.providerOutages ?? createProviderOutages({ now: Date.now, logError: context.logError });
7142
+ const allFallbackIds = fallbackProviders({
6696
7143
  catalog: context.catalog,
6697
7144
  modelId,
6698
7145
  chosenProviderId: helper.model.providerId,
6699
7146
  env: context.env,
6700
7147
  ...context.providerFallback !== void 0 && { setting: context.providerFallback }
6701
7148
  });
7149
+ const fallbackProviderIds = allFallbackIds.filter((candidate) => outages.pausedKind(candidate) === void 0);
6702
7150
  const toolQueue = context.toolQueue ?? createToolQueue();
6703
7151
  let providerId = helper.model.providerId;
6704
7152
  let fallbackUsed = false;
7153
+ let firstOutage;
7154
+ const primaryPaused = outages.admit(providerId);
7155
+ if (primaryPaused !== void 0 && fallbackProviderIds[0] !== void 0) {
7156
+ context.logError(
7157
+ `[\u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440] \u0447\u0430\u0442=${context.chatId} \u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A=${input.name} \u043C\u043E\u0434\u0435\u043B\u044C=${modelId} \u0441=${providerId} \u043D\u0430=${fallbackProviderIds[0]} \u043F\u0440\u0438\u0447\u0438\u043D\u0430=${primaryPaused} \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435`
7158
+ );
7159
+ providerId = fallbackProviderIds[0];
7160
+ }
6705
7161
  let externalContent = false;
6706
7162
  const report = (status, extra = {}) => ({
6707
7163
  messages: [...messages],
@@ -6728,17 +7184,28 @@ async function* helperSteps(input) {
6728
7184
  } catch (error) {
6729
7185
  if (abortSignal.aborted) throw error;
6730
7186
  const kind = classifyProviderError(error, provider);
6731
- const next = decideNextAttempt({ outcome: "failed", kind, error, contentSeen: false }, { fallbackUsed, fallbackProviderIds });
7187
+ const candidates = providerId === helper.model.providerId ? fallbackProviderIds : fallbackProviderIds.slice(fallbackProviderIds.indexOf(providerId) + 1);
7188
+ const next = decideNextAttempt({ outcome: "failed", kind, error, contentSeen: false }, { fallbackUsed, fallbackProviderIds: candidates });
7189
+ const outage = isProviderOutage(kind) ? kind : void 0;
7190
+ if (outage) {
7191
+ if (providerId === helper.model.providerId) firstOutage ??= outage;
7192
+ outages.report({
7193
+ providerId,
7194
+ kind: outage,
7195
+ ...next.action === "fallback" ? { fallbackProviderId: next.providerId } : { fallbacksPaused: allFallbackIds.some((candidate) => candidate !== providerId && outages.pausedKind(candidate) !== void 0) }
7196
+ });
7197
+ }
6732
7198
  if (next.action === "fallback") {
6733
7199
  context.logError(`[\u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440] \u0447\u0430\u0442=${context.chatId} \u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A=${input.name} \u043C\u043E\u0434\u0435\u043B\u044C=${modelId} \u0441=${providerId} \u043D\u0430=${next.providerId} \u043F\u0440\u0438\u0447\u0438\u043D\u0430=${kind}`);
6734
- fallbackUsed = true;
7200
+ if (!outage) fallbackUsed = true;
6735
7201
  providerId = next.providerId;
6736
7202
  continue;
6737
7203
  }
6738
7204
  context.logError(describeProviderFailure(error, { chatId: context.chatId, modelId, providerId, kind, env: context.env, catalog: context.catalog }));
6739
- yield report("failed", { error: FAILURE_REASONS[kind] });
7205
+ yield report("failed", { error: FAILURE_REASONS[errorKindForPerson(kind, firstOutage)] });
6740
7206
  return;
6741
7207
  }
7208
+ outages.recovered(providerId);
6742
7209
  messages.push(...response);
6743
7210
  externalContent ||= callsExternalContentTool(messages, tools);
6744
7211
  const pendingApproval = pendingApprovalOf2(messages, tools);
@@ -6951,6 +7418,7 @@ function createChatEvents(logError = () => {
6951
7418
  listeners.delete(listener);
6952
7419
  };
6953
7420
  },
7421
+ hasListeners: () => listeners.size > 0,
6954
7422
  emit(event) {
6955
7423
  for (const listener of [...listeners]) {
6956
7424
  try {
@@ -7033,9 +7501,9 @@ function editAnswerParts(parts, edit, uneditable) {
7033
7501
  return { ok: true, parts: textPlaced ? edited : [...edited, textPart] };
7034
7502
  }
7035
7503
  function toolCallIdOf(part) {
7036
- return isToolPart6(part) ? part.toolCallId : void 0;
7504
+ return isToolPart7(part) ? part.toolCallId : void 0;
7037
7505
  }
7038
- var isToolPart6 = (part) => part.type.startsWith("tool-") || part.type === "dynamic-tool";
7506
+ var isToolPart7 = (part) => part.type.startsWith("tool-") || part.type === "dynamic-tool";
7039
7507
  function toolNameOf2(part) {
7040
7508
  return part.type === "dynamic-tool" ? part.toolName : part.type.slice("tool-".length);
7041
7509
  }
@@ -7329,7 +7797,7 @@ var fireworksProvider = createFireworksProvider();
7329
7797
 
7330
7798
  // src/models/providers/openrouter.ts
7331
7799
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
7332
- import { wrapLanguageModel as wrapLanguageModel4 } from "ai";
7800
+ import { wrapLanguageModel as wrapLanguageModel5 } from "ai";
7333
7801
 
7334
7802
  // src/models/providers/openrouter-cache-mark-limit.ts
7335
7803
  function openrouterCacheMarkLimit(providerModelId) {
@@ -7579,7 +8047,7 @@ function createOpenRouterProvider(options2 = {}) {
7579
8047
  createLanguageModel: (providerModelId, apiKey, requestOptions) => {
7580
8048
  const markLimit = openrouterCacheMarkLimit(providerModelId);
7581
8049
  const fetch = markLimit === void 0 ? requestOptions?.fetch : fetchWithCacheMarkLimit(requestOptions?.fetch, markLimit);
7582
- return wrapLanguageModel4({
8050
+ return wrapLanguageModel5({
7583
8051
  model: createOpenRouter({ apiKey, fetch, ...connection }).chat(providerModelId, routingOptions),
7584
8052
  middleware: [
7585
8053
  openrouterRoutingMiddleware(routing),
@@ -9481,7 +9949,7 @@ function isPlainObject3(value) {
9481
9949
  }
9482
9950
 
9483
9951
  // src/memory/memory-after-answer.ts
9484
- import { wrapLanguageModel as wrapLanguageModel5 } from "ai";
9952
+ import { wrapLanguageModel as wrapLanguageModel6 } from "ai";
9485
9953
  var DRAIN_WAIT_MS = 5e3;
9486
9954
  function createMemoryAfterAnswer({ memory, storage, catalog, env, logError, turnObserver }) {
9487
9955
  const running = /* @__PURE__ */ new Set();
@@ -9526,7 +9994,7 @@ function createMemoryAfterAnswer({ memory, storage, catalog, env, logError, turn
9526
9994
  };
9527
9995
  }
9528
9996
  function withRetries(model, context) {
9529
- return typeof model === "string" ? model : wrapLanguageModel5({ model, middleware: createTransientRetryMiddleware(context) });
9997
+ return typeof model === "string" ? model : wrapLanguageModel6({ model, middleware: createTransientRetryMiddleware(context) });
9530
9998
  }
9531
9999
 
9532
10000
  // src/conversation/to-agent-chat-message.ts
@@ -10248,6 +10716,7 @@ function createTurnGate(hooks) {
10248
10716
  ...input.text !== void 0 && { text: input.text },
10249
10717
  ...input.messageId !== void 0 && { messageId: input.messageId },
10250
10718
  ...input.burstMessageIds && { burstMessageIds: [...input.burstMessageIds] },
10719
+ ...input.unattended && { unattended: true },
10251
10720
  ...abandonedOf(input)
10252
10721
  });
10253
10722
  if (!decision) return TURN;
@@ -10280,6 +10749,7 @@ function createTurnGate(hooks) {
10280
10749
  ...input.messageId !== void 0 && { messageId: input.messageId },
10281
10750
  ...input.text !== void 0 && { text: input.text },
10282
10751
  ...input.burstMessageIds && { burstMessageIds: [...input.burstMessageIds] },
10752
+ ...input.unattended && { unattended: true },
10283
10753
  reason,
10284
10754
  ...abandonedOf(input)
10285
10755
  });
@@ -10670,6 +11140,8 @@ var InvalidWidgetAnswerError = class extends Error {
10670
11140
  this.name = "InvalidWidgetAnswerError";
10671
11141
  }
10672
11142
  };
11143
+ var FAR_FUTURE_ISO = "9999-12-31T23:59:59.999Z";
11144
+ var abandonedBurstReceivers = /* @__PURE__ */ new WeakMap();
10673
11145
  var TurnEndedBeforeSubscribeError = class extends Error {
10674
11146
  constructor(chatId) {
10675
11147
  super(`\u0425\u043E\u0434 \u0432 \u0447\u0430\u0442\u0435 ${chatId} \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u0434\u043E \u043F\u043E\u0434\u043F\u0438\u0441\u043A\u0438`);
@@ -10867,6 +11339,7 @@ function createChatService(input) {
10867
11339
  const modelSilence = resolveModelSilence(modelSilenceTimeout);
10868
11340
  const cacheMissWatch = cacheMissLog === false ? void 0 : createCacheMissWatch({ logError });
10869
11341
  checkProviderFallback(providerFallback, options2.catalog);
11342
+ const providerOutages = createProviderOutages({ now: () => now().getTime(), logError });
10870
11343
  const placeMaxLength = resolvePlaceMaxLength(placeMaxLengthSetting);
10871
11344
  const browserToolTimeoutMs = resolveBrowserToolTimeoutMs(browserToolTimeoutSetting);
10872
11345
  const pendingBrowserCalls = createPendingBrowserCalls();
@@ -10913,7 +11386,9 @@ function createChatService(input) {
10913
11386
  const steersInFlight = /* @__PURE__ */ new Map();
10914
11387
  const startingByChat = /* @__PURE__ */ new Map();
10915
11388
  const burstWaits = createBurstWaits();
10916
- const turnLeases = createChatTurnLeases({ storage, holder: crypto.randomUUID(), logError });
11389
+ const copyId = crypto.randomUUID();
11390
+ const turnLeases = createChatTurnLeases({ storage, holder: copyId, logError });
11391
+ const burstSweeps = /* @__PURE__ */ new Set();
10917
11392
  const burstWrites = /* @__PURE__ */ new Map();
10918
11393
  const chatToolQueues = /* @__PURE__ */ new Map();
10919
11394
  let staleSweep = null;
@@ -11540,6 +12015,7 @@ function createChatService(input) {
11540
12015
  modelSilence,
11541
12016
  ...limits.maxOutputTokens !== void 0 && { maxOutputTokens: limits.maxOutputTokens },
11542
12017
  ...providerFallback !== void 0 && { providerFallback },
12018
+ providerOutages,
11543
12019
  ...cacheMissWatch && { cacheMissWatch },
11544
12020
  ...options2.turnObserver && { turnObserver: options2.turnObserver },
11545
12021
  ...options2.limits && { assertWithinLimit: () => assertWithinSpendingLimit(chat.ownerId, chatId) },
@@ -11661,7 +12137,15 @@ function createChatService(input) {
11661
12137
  steeringTurn = steering.begin(chatId, answerId);
11662
12138
  if (stopsWhileStarting.delete(chatId)) registry.abort(chatId);
11663
12139
  const turnEvent = { ownerId: chat.ownerId, chatId, messageId: answerId, trigger, channel };
11664
- chatEvents.emit({ type: "turn-started", ...turnEvent, ...launch.deliveredByApp && { deliveredByApp: true } });
12140
+ chatEvents.emit({
12141
+ type: "turn-started",
12142
+ ...turnEvent,
12143
+ ...launch.deliveredByApp && { deliveredByApp: true },
12144
+ ...launch.unattended && { unattended: true }
12145
+ });
12146
+ if (launch.unattended && chatEvents.hasListeners?.() === false) {
12147
+ logError(`\u0425\u043E\u0434 \u0431\u0435\u0437 \u0432\u044B\u0437\u043E\u0432\u0430 \u043D\u0430 \u043F\u0430\u0447\u043A\u0443 \u0447\u0430\u0442\u0430 ${chatId}: \u043D\u0430 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u0447\u0430\u0442\u0430 \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u043D \u2014 \u043E\u0442\u0432\u0435\u0442 \u0431\u0443\u0434\u0435\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438`);
12148
+ }
11665
12149
  launch.onStarted?.({ answerId, finished });
11666
12150
  liveAnswerIds.add(answerId);
11667
12151
  const slot = startingSlots.get(chatId);
@@ -11779,6 +12263,7 @@ function createChatService(input) {
11779
12263
  await assertStillOpen(chatId);
11780
12264
  const earlier = await storage.getMessage(message.id);
11781
12265
  if (earlier?.chatId === chatId && earlier.role === "user") return earlier;
12266
+ if (sendOptions?.answerAfterRestart) await savingPendingBurst(userId, chatId, message.id, now().getTime() + quietMs, sendOptions.page);
11782
12267
  const written = await writingHumanMessage(userId, chatId, await currentBranch(chatId), message, text, files, sendOptions, { quietMs });
11783
12268
  chatEvents.emit({ type: "message-added", ownerId: userId, chatId, messageId: written.id });
11784
12269
  return written;
@@ -11797,11 +12282,11 @@ function createChatService(input) {
11797
12282
  return true;
11798
12283
  };
11799
12284
  }
11800
- async function answeringBurst(userId, chatId, question2, sendOptions) {
12285
+ async function answeringBurst(userId, chatId, questionId, page, unattended = false) {
11801
12286
  const otherCopy = waitingForOtherCopy(chatId);
11802
12287
  for (; ; ) {
11803
12288
  if (closing) throw new ChatBusyError(chatId);
11804
- const verdict = burstVerdict(await currentBranch(chatId), question2.id, now().getTime());
12289
+ const verdict = burstVerdict(await currentBranch(chatId), questionId, now().getTime());
11805
12290
  if (verdict.kind === "skip") return null;
11806
12291
  if (verdict.kind === "wait") {
11807
12292
  if (closing) throw new ChatBusyError(chatId);
@@ -11818,16 +12303,20 @@ function createChatService(input) {
11818
12303
  const burstMessageIds = verdict.burst.map((message) => message.id);
11819
12304
  try {
11820
12305
  const stream = await startingTurn(
11821
- { userId, chat, action: "send", text: burstText(verdict.burst), messageId: question2.id, burstMessageIds },
12306
+ { userId, chat, action: "send", text: burstText(verdict.burst), messageId: questionId, burstMessageIds, ...unattended && { unattended: true } },
11822
12307
  async (pass) => {
11823
12308
  const branch = await currentBranch(chatId);
11824
- if (burstVerdict(branch, question2.id, now().getTime()).kind !== "start") return null;
11825
- if (pass.kind === "hold") return heldTurn(userId, chatId, question2.id);
12309
+ if (burstVerdict(branch, questionId, now().getTime()).kind !== "start") return null;
12310
+ if (pass.kind === "hold") return unattended ? (heldTurn(userId, chatId, questionId), "unattended") : heldTurn(userId, chatId, questionId);
11826
12311
  takeTurnSlot(userId, chatId);
11827
- return startAnswer("send", userId, chatId, branch, sendOptions?.page, void 0, void 0, replyOf(pass));
12312
+ if (!unattended) return startAnswer("send", userId, chatId, branch, page, void 0, void 0, replyOf(pass));
12313
+ const reply = replyOf(pass);
12314
+ await launchTurn(userId, chatId, branch, { parentId: branch.at(-1).id, page, action: "send", detached: true, unattended: true, ...reply !== void 0 && { reply } });
12315
+ return "unattended";
11828
12316
  },
11829
12317
  (result) => result === null ? "busy" : void 0
11830
12318
  );
12319
+ if (stream === "unattended") return null;
11831
12320
  return stream && viewerStream(userId, chat, {}, async () => stream);
11832
12321
  } catch (error) {
11833
12322
  if (error instanceof ChatBusyError && !closing && localTurnOf(chatId)) continue;
@@ -11836,6 +12325,115 @@ function createChatService(input) {
11836
12325
  }
11837
12326
  }
11838
12327
  }
12328
+ const keepsPendingBursts = Boolean(storage.savePendingBurst && storage.takeAbandonedBursts && storage.renewPendingBurst && storage.removePendingBurst);
12329
+ async function savingPendingBurst(ownerId, chatId, messageId, dueAt, page) {
12330
+ if (!keepsPendingBursts) return;
12331
+ await storage.savePendingBurst({
12332
+ chatId,
12333
+ ownerId,
12334
+ messageId,
12335
+ dueAt: new Date(dueAt).toISOString(),
12336
+ holder: copyId,
12337
+ heldUntil: new Date(dueAt + BURST_HOLD_MS).toISOString(),
12338
+ ...page && { page }
12339
+ });
12340
+ }
12341
+ function renewingBurstHold(chatId, messageId, dueAt, holder) {
12342
+ const timer = setInterval(() => {
12343
+ const heldUntil = new Date(Math.max(now().getTime(), dueAt) + BURST_HOLD_MS).toISOString();
12344
+ storage.renewPendingBurst({ chatId, messageId, holder, heldUntil }).then(
12345
+ (held) => {
12346
+ if (!held) clearInterval(timer);
12347
+ },
12348
+ (error) => logError(`\u0423\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435 \u043F\u0430\u0447\u043A\u0438 \u0447\u0430\u0442\u0430 ${chatId} \u043D\u0435 \u043F\u0440\u043E\u0434\u043B\u0438\u043B\u043E\u0441\u044C: ${error instanceof Error ? error.message : String(error)}`)
12349
+ );
12350
+ }, BURST_HOLD_RENEW_MS);
12351
+ timer.unref?.();
12352
+ return () => clearInterval(timer);
12353
+ }
12354
+ const burstWaitsForNextSweep = (error) => error instanceof ChatBusyError || error instanceof TooManyTurnsError;
12355
+ function answeringRecordedBurst(ownerId, chatId, questionId, page, burst) {
12356
+ const unattended = burst.kept && burst.unattended === true;
12357
+ if (!burst.kept || !keepsPendingBursts) return burstWaits.holding(chatId, () => answeringBurst(ownerId, chatId, questionId, page, unattended));
12358
+ const stopRenewing = renewingBurstHold(chatId, questionId, burst.dueAt, burst.holder ?? copyId);
12359
+ const answering = burstWaits.holding(chatId, () => answeringBurst(ownerId, chatId, questionId, page, unattended)).finally(stopRenewing);
12360
+ void answering.then(
12361
+ () => true,
12362
+ (error) => !burstWaitsForNextSweep(error)
12363
+ ).then((decided) => {
12364
+ if (decided)
12365
+ return storage.removePendingBurst(chatId, questionId).catch((error) => logError(`\u0417\u0430\u043F\u0438\u0441\u044C \u043F\u0430\u0447\u043A\u0438 \u0447\u0430\u0442\u0430 ${chatId} \u043D\u0435 \u0441\u043D\u044F\u043B\u0430\u0441\u044C: ${error instanceof Error ? error.message : String(error)}`));
12366
+ });
12367
+ return answering;
12368
+ }
12369
+ let burstDecisions = 0;
12370
+ const burstDecisionWaiters = /* @__PURE__ */ new Set();
12371
+ const burstDecisionFreed = () => new Promise((resolve) => burstDecisionWaiters.add(resolve));
12372
+ function releasingBurstDecision() {
12373
+ burstDecisions--;
12374
+ for (const waiter of [...burstDecisionWaiters]) {
12375
+ burstDecisionWaiters.delete(waiter);
12376
+ waiter();
12377
+ }
12378
+ }
12379
+ function reportingAbandonedBurst(chatId, error) {
12380
+ if (burstWaitsForNextSweep(error)) return;
12381
+ const why = error instanceof TurnRefusedError ? `\u043E\u0442\u043A\u0430\u0437 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F ${error.code}: ${error.message}` : error instanceof Error ? error.message : String(error);
12382
+ logError(`\u0425\u043E\u0434 \u043D\u0430 \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u0443\u044E \u043F\u0430\u0447\u043A\u0443 \u0447\u0430\u0442\u0430 ${chatId} \u043D\u0435 \u043D\u0430\u0447\u0430\u043B\u0441\u044F: ${why}`);
12383
+ }
12384
+ async function receivingAbandonedBurst(burst) {
12385
+ if (burstWaits.has(burst.chatId)) return;
12386
+ await answeringRecordedBurst(burst.ownerId, burst.chatId, burst.messageId, burst.page, { kept: true, dueAt: Date.parse(burst.dueAt), unattended: true, holder: burst.holder }).then(
12387
+ () => {
12388
+ },
12389
+ (error) => reportingAbandonedBurst(burst.chatId, error)
12390
+ );
12391
+ }
12392
+ async function routingAbandonedBurst(burst) {
12393
+ const serviceFor = options2.abandonedBursts?.serviceFor;
12394
+ if (!serviceFor) return receivingAbandonedBurst(burst);
12395
+ let target;
12396
+ try {
12397
+ const chat = await storage.getChat(burst.chatId);
12398
+ if (!chat) return receivingAbandonedBurst(burst);
12399
+ target = await serviceFor({ ownerId: burst.ownerId, chatId: burst.chatId, chat });
12400
+ } catch (error) {
12401
+ logError(`\u0421\u043B\u0443\u0436\u0431\u0430 \u0434\u043B\u044F \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u043E\u0439 \u043F\u0430\u0447\u043A\u0438 \u0447\u0430\u0442\u0430 ${burst.chatId} \u043D\u0435 \u043D\u0430\u0448\u043B\u0430\u0441\u044C (serviceFor): ${error instanceof Error ? error.message : String(error)} \u2014 \u043F\u0430\u0447\u043A\u0430 \u0436\u0434\u0451\u0442 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043E\u0431\u0445\u043E\u0434\u0430`);
12402
+ return;
12403
+ }
12404
+ if (!target) {
12405
+ logError(`\u0414\u043B\u044F \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u043E\u0439 \u043F\u0430\u0447\u043A\u0438 \u0447\u0430\u0442\u0430 ${burst.chatId} \u043D\u0435\u0442 \u0441\u043B\u0443\u0436\u0431\u044B (serviceFor \u2192 ${String(target)}): \u043F\u0430\u0447\u043A\u0430 \u0436\u0434\u0451\u0442 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043E\u0431\u0445\u043E\u0434\u0430`);
12406
+ return;
12407
+ }
12408
+ const receive = abandonedBurstReceivers.get(target);
12409
+ if (!receive) {
12410
+ logError(`serviceFor \u0434\u043B\u044F \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u043E\u0439 \u043F\u0430\u0447\u043A\u0438 \u0447\u0430\u0442\u0430 ${burst.chatId} \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435 \u0441\u043B\u0443\u0436\u0431\u0443 \u0447\u0430\u0442\u0430 SDK: \u043F\u0430\u0447\u043A\u0430 \u0436\u0434\u0451\u0442 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043E\u0431\u0445\u043E\u0434\u0430`);
12411
+ return;
12412
+ }
12413
+ await receive(burst);
12414
+ }
12415
+ function sweepingBursts(take) {
12416
+ if (!keepsPendingBursts || closing) return Promise.resolve();
12417
+ const sweep = (async () => {
12418
+ for (; ; ) {
12419
+ if (closing) return;
12420
+ const free = BURST_SWEEP_LIMIT - burstDecisions;
12421
+ if (free <= 0) {
12422
+ await burstDecisionFreed();
12423
+ continue;
12424
+ }
12425
+ const heldUntil = new Date(now().getTime() + BURST_HOLD_MS).toISOString();
12426
+ const taken = await storage.takeAbandonedBursts({ ...take, holder: copyId, heldUntil, limit: free });
12427
+ if (taken.length === 0) return;
12428
+ for (const burst of taken) {
12429
+ burstDecisions++;
12430
+ void routingAbandonedBurst(burst).finally(releasingBurstDecision);
12431
+ }
12432
+ }
12433
+ })().catch((error) => logError(`\u041E\u0431\u0445\u043E\u0434 \u0436\u0434\u0443\u0449\u0438\u0445 \u043F\u0430\u0447\u0435\u043A \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F: ${error instanceof Error ? error.message : String(error)}`)).finally(() => burstSweeps.delete(sweep));
12434
+ burstSweeps.add(sweep);
12435
+ return sweep;
12436
+ }
11839
12437
  async function stoppingPendingBurst(userId, chatId) {
11840
12438
  if (!burstWaits.has(chatId) && !isBurstPending(await currentBranch(chatId), now().getTime())) return false;
11841
12439
  try {
@@ -12261,7 +12859,13 @@ function createChatService(input) {
12261
12859
  }).catch(() => {
12262
12860
  });
12263
12861
  const question2 = await recording;
12264
- const turn = burstWaits.holding(chatId, () => answeringBurst(userId, chatId, question2, burstOptions));
12862
+ const turn = answeringRecordedBurst(
12863
+ userId,
12864
+ chatId,
12865
+ question2.id,
12866
+ burstOptions?.page,
12867
+ burstOptions?.answerAfterRestart ? { kept: true, dueAt: Date.parse(question2.createdAt) + quietMs } : { kept: false }
12868
+ );
12265
12869
  turn.catch(() => {
12266
12870
  });
12267
12871
  return { messageId: question2.id, turn };
@@ -12546,9 +13150,15 @@ function createChatService(input) {
12546
13150
  },
12547
13151
  async recoverInterruptedAnswers(recoverOptions) {
12548
13152
  await afterTurn?.recoverPending(liveAnswerIds);
12549
- const [count] = await Promise.all([recoverAnswersOnStart(recoverOptions?.soleServer === true), options2.background?.recover()]);
13153
+ const soleServer = recoverOptions?.soleServer === true;
13154
+ const [count] = await Promise.all([recoverAnswersOnStart(soleServer), options2.background?.recover()]);
13155
+ await sweepingBursts(soleServer ? { abandonedBefore: FAR_FUTURE_ISO, exceptHolder: copyId } : { abandonedBefore: now().toISOString() });
12550
13156
  return count;
12551
13157
  },
13158
+ async tickBursts() {
13159
+ burstWaits.wakeAll();
13160
+ await sweepingBursts({ abandonedBefore: now().toISOString() });
13161
+ },
12552
13162
  async closeChat({ ownerId, chatId, code, message }) {
12553
13163
  const closed = normalizeChatClosure({ code, message });
12554
13164
  await changeClosure(await ownChat(ownerId, chatId), closed);
@@ -12576,6 +13186,7 @@ function createChatService(input) {
12576
13186
  burstWaits.cancelAll();
12577
13187
  try {
12578
13188
  await sweeping;
13189
+ while (burstSweeps.size > 0) await Promise.allSettled([...burstSweeps]);
12579
13190
  while (startingWork.size > 0) await Promise.allSettled([...startingWork]);
12580
13191
  const unfinished = await liveTurns.drain(waitForAnswersMs);
12581
13192
  await burstWaits.settled();
@@ -12614,7 +13225,7 @@ function createChatService(input) {
12614
13225
  const next = () => {
12615
13226
  if (sweep.stopped) return;
12616
13227
  sweep.timer = setTimeout(() => {
12617
- sweep.running = Promise.all([recoverStaleAnswers(), afterTurn?.recoverPending(liveAnswerIds)]).then(() => void 0).catch(
13228
+ sweep.running = Promise.all([recoverStaleAnswers(), afterTurn?.recoverPending(liveAnswerIds), sweepingBursts({ abandonedBefore: now().toISOString() })]).then(() => void 0).catch(
12618
13229
  (failure) => logError(`\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u044B\u0445 \u043E\u0442\u0432\u0435\u0442\u043E\u0432 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C: ${failure instanceof Error ? failure.message : "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"}`)
12619
13230
  ).finally(() => {
12620
13231
  sweep.running = void 0;
@@ -12731,6 +13342,7 @@ function createChatService(input) {
12731
13342
  modelSilence,
12732
13343
  logError,
12733
13344
  ...providerFallback !== void 0 && { providerFallback },
13345
+ providerOutages,
12734
13346
  ...options2.turnObserver && { turnObserver: options2.turnObserver },
12735
13347
  ...options2.limits && { assertWithinLimit: () => assertWithinSpendingLimit(ownerId, chatId) }
12736
13348
  });
@@ -12740,6 +13352,7 @@ function createChatService(input) {
12740
13352
  helpers: backgroundHelpers
12741
13353
  };
12742
13354
  }
13355
+ abandonedBurstReceivers.set(service, receivingAbandonedBurst);
12743
13356
  return service;
12744
13357
  function isDetachedAnswer(last, runningId) {
12745
13358
  return runningId === null && last?.status === "streaming" && !liveAnswerIds.has(last.id);
@@ -12899,6 +13512,8 @@ export {
12899
13512
  isCorrection,
12900
13513
  BURST_PART_TYPE,
12901
13514
  DEFAULT_BURST_QUIET_MS,
13515
+ BURST_HOLD_MS,
13516
+ BURST_SWEEP_LIMIT,
12902
13517
  isBurstMessage,
12903
13518
  RELEASED_ANSWER_ALIVE_AT,
12904
13519
  aliveAtByLocalClock,
@@ -12999,4 +13614,4 @@ export {
12999
13614
  InvalidWidgetAnswerError,
13000
13615
  createChatService
13001
13616
  };
13002
- //# sourceMappingURL=chunk-7M7M7DAN.js.map
13617
+ //# sourceMappingURL=chunk-KAG3QTZC.js.map