@skj1724/oh-my-opencode 3.19.7 → 3.19.9

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
@@ -5140,7 +5140,7 @@ function getChain(agent, category) {
5140
5140
  if (category && CATEGORY_MODEL_REQUIREMENTS[category]) {
5141
5141
  return CATEGORY_MODEL_REQUIREMENTS[category].fallbackChain;
5142
5142
  }
5143
- throw new Error(`No fallback chain found for agent="${agent ?? ""}" category="${category ?? ""}"`);
5143
+ return;
5144
5144
  }
5145
5145
  function resolveNextFallbackModel(input) {
5146
5146
  const {
@@ -5149,40 +5149,58 @@ function resolveNextFallbackModel(input) {
5149
5149
  currentModel,
5150
5150
  attempts,
5151
5151
  availableModels,
5152
- lastErrorClassification
5152
+ lastErrorClassification,
5153
+ configuredFallbackModels,
5154
+ maxAttempts
5153
5155
  } = input;
5156
+ if (maxAttempts !== undefined && attempts.length >= maxAttempts) {
5157
+ return {
5158
+ kind: "exhausted",
5159
+ attempts,
5160
+ reason: "max fallback attempts reached",
5161
+ lastErrorClassification
5162
+ };
5163
+ }
5154
5164
  const chain = getChain(agent, category);
5155
- const candidates = expandChain(chain);
5165
+ const candidates = configuredFallbackModels && configuredFallbackModels.length > 0 ? configuredFallbackModels : chain ? expandChain(chain) : undefined;
5166
+ if (!candidates) {
5167
+ return {
5168
+ kind: "unconfigured",
5169
+ reason: `No fallback chain found for agent="${agent ?? ""}" category="${category ?? ""}"`
5170
+ };
5171
+ }
5156
5172
  const skipKeys = new Set;
5157
5173
  skipKeys.add(modelKey(currentModel));
5158
5174
  for (const a of attempts) {
5159
5175
  skipKeys.add(modelKey(a.model));
5160
5176
  }
5161
- const resultAttempts = [...attempts];
5162
- const currentKey = modelKey(currentModel);
5163
- const isInAttempts = attempts.some((a) => modelKey(a.model) === currentKey);
5164
- if (!isInAttempts) {
5165
- resultAttempts.push({ model: currentModel });
5166
- }
5177
+ const skipped = [];
5167
5178
  const hasAvailabilityFilter = availableModels != null && availableModels.size > 0;
5168
5179
  for (const candidate of candidates) {
5169
5180
  const key = modelKey(candidate);
5170
- if (skipKeys.has(key))
5181
+ if (skipKeys.has(key)) {
5182
+ skipped.push({ model: candidate, reason: "already attempted or current model" });
5171
5183
  continue;
5184
+ }
5172
5185
  if (hasAvailabilityFilter) {
5173
5186
  const match = fuzzyMatchModel(key, availableModels, [candidate.providerID]);
5174
- if (!match)
5187
+ if (!match) {
5188
+ skipped.push({ model: candidate, reason: "model unavailable" });
5175
5189
  continue;
5190
+ }
5176
5191
  }
5177
5192
  return {
5178
5193
  kind: "next",
5179
5194
  model: candidate,
5180
- attempts: resultAttempts
5195
+ attempts,
5196
+ skipped
5181
5197
  };
5182
5198
  }
5183
5199
  return {
5184
5200
  kind: "exhausted",
5185
- attempts: resultAttempts,
5201
+ attempts,
5202
+ reason: "No fallback candidates available",
5203
+ skipped,
5186
5204
  lastErrorClassification
5187
5205
  };
5188
5206
  }
@@ -24629,6 +24647,464 @@ function createPerfProfilerHook(options) {
24629
24647
  "chat.message": async () => {}
24630
24648
  };
24631
24649
  }
24650
+ // src/shared/provider-error-classifier.ts
24651
+ function extractErrorInfo(error) {
24652
+ if (typeof error === "string") {
24653
+ return { message: error };
24654
+ }
24655
+ if (error instanceof Error) {
24656
+ const anyErr = error;
24657
+ return {
24658
+ statusCode: anyErr.status ?? anyErr.statusCode ?? anyErr.httpStatus,
24659
+ code: anyErr.code ?? anyErr.error?.code,
24660
+ type: anyErr.type ?? anyErr.error?.type,
24661
+ message: error.message,
24662
+ status: anyErr.status ?? anyErr.error?.status,
24663
+ headers: anyErr.headers
24664
+ };
24665
+ }
24666
+ if (typeof error === "object" && error !== null) {
24667
+ const obj = error;
24668
+ const inner = obj.error ?? {};
24669
+ return {
24670
+ statusCode: obj.status ?? obj.statusCode ?? inner.status,
24671
+ code: inner.code ?? obj.code,
24672
+ type: inner.type ?? obj.type,
24673
+ message: inner.message ?? obj.message ?? String(error),
24674
+ status: inner.status ?? obj.status,
24675
+ headers: obj.headers
24676
+ };
24677
+ }
24678
+ return { message: String(error) };
24679
+ }
24680
+ function parseRetryAfterMs(headers) {
24681
+ if (!headers)
24682
+ return;
24683
+ const retryAfter = headers["retry-after"] ?? headers["Retry-After"];
24684
+ if (retryAfter) {
24685
+ const seconds = Number(retryAfter);
24686
+ if (!isNaN(seconds) && seconds > 0) {
24687
+ return seconds * 1000;
24688
+ }
24689
+ }
24690
+ const reset = headers["x-ratelimit-reset"] ?? headers["X-Ratelimit-Reset"];
24691
+ if (reset) {
24692
+ const resetTimestamp = Number(reset);
24693
+ if (!isNaN(resetTimestamp)) {
24694
+ const resetMs = resetTimestamp > 1000000000000 ? resetTimestamp : resetTimestamp * 1000;
24695
+ const delayMs = resetMs - Date.now();
24696
+ return delayMs > 0 ? delayMs : 0;
24697
+ }
24698
+ }
24699
+ return;
24700
+ }
24701
+ function isContextOverflow(message, code) {
24702
+ const lowerMessage = message.toLowerCase();
24703
+ return lowerMessage.includes("context_length_exceeded") || lowerMessage.includes("prompt is too long") || lowerMessage.includes("maximum context length") || code === "context_length_exceeded";
24704
+ }
24705
+ function isZhipuQuotaCode(code) {
24706
+ if (typeof code !== "number")
24707
+ return false;
24708
+ return [1113, 1304, 1308, 1309].includes(code);
24709
+ }
24710
+ function isGenericQuotaMessage(message) {
24711
+ const m = message.toLowerCase();
24712
+ return m.includes("usage quota") || m.includes("quota exceeded") || m.includes("exceeded your quota") || m.includes("quota") && m.includes("exceeded") || m.includes("upgrade your plan");
24713
+ }
24714
+ function isZhipuRateLimitCode(code) {
24715
+ if (typeof code !== "number")
24716
+ return false;
24717
+ return [1302, 1303].includes(code);
24718
+ }
24719
+ function isZhipuOverloadedCode(code) {
24720
+ if (typeof code !== "number")
24721
+ return false;
24722
+ return code === 1312;
24723
+ }
24724
+ function isGeminiQuotaDetails(details) {
24725
+ if (!Array.isArray(details))
24726
+ return false;
24727
+ return details.some((d) => d?.["@type"]?.includes("QuotaFailure") || d?.["@type"]?.includes("google.rpc.QuotaFailure"));
24728
+ }
24729
+ function isGeminiPerMinuteLimit(message, details) {
24730
+ const lowerMessage = message.toLowerCase();
24731
+ if (lowerMessage.includes("per_minute") || lowerMessage.includes("per minute")) {
24732
+ return true;
24733
+ }
24734
+ if (Array.isArray(details)) {
24735
+ return details.some((d) => d?.["@type"]?.includes("RetryInfo"));
24736
+ }
24737
+ return false;
24738
+ }
24739
+ function classifyProviderError(error) {
24740
+ const info = extractErrorInfo(error);
24741
+ const { statusCode, code, type: type2, message, status, headers } = info;
24742
+ if (isContextOverflow(message, code)) {
24743
+ return {
24744
+ category: "context_overflow",
24745
+ retryable: false,
24746
+ shouldFallback: false,
24747
+ statusCode,
24748
+ reason: "Context length exceeded, prompt too long for model"
24749
+ };
24750
+ }
24751
+ if (statusCode === 401 || statusCode === 403) {
24752
+ return {
24753
+ category: "auth",
24754
+ retryable: false,
24755
+ shouldFallback: false,
24756
+ statusCode,
24757
+ reason: statusCode === 401 ? "Invalid API key or authentication" : "Permission denied"
24758
+ };
24759
+ }
24760
+ const lowerMessage = message.toLowerCase();
24761
+ const isModelUnavailableMessage = lowerMessage.includes("model not found") || lowerMessage.includes("model unavailable") || lowerMessage.includes("unsupported model") || lowerMessage.includes("invalid model") || lowerMessage.includes("unknown model");
24762
+ const isProviderUnavailableMessage = lowerMessage.includes("provider not found") || lowerMessage.includes("unknown provider") || lowerMessage.includes("invalid provider");
24763
+ if (isProviderUnavailableMessage && (statusCode === 400 || statusCode === 404 || statusCode === 422)) {
24764
+ return {
24765
+ category: "provider_unavailable",
24766
+ retryable: false,
24767
+ shouldFallback: true,
24768
+ statusCode,
24769
+ reason: `Provider unavailable: ${message.substring(0, 100)}`
24770
+ };
24771
+ }
24772
+ if (isModelUnavailableMessage && (statusCode === 400 || statusCode === 404 || statusCode === 422)) {
24773
+ return {
24774
+ category: "model_unavailable",
24775
+ retryable: false,
24776
+ shouldFallback: true,
24777
+ statusCode,
24778
+ reason: `Model unavailable: ${message.substring(0, 100)}`
24779
+ };
24780
+ }
24781
+ if (statusCode === 404) {
24782
+ return {
24783
+ category: "model_unavailable",
24784
+ retryable: false,
24785
+ shouldFallback: true,
24786
+ statusCode,
24787
+ reason: `Model not found (404): ${message.substring(0, 100)}`
24788
+ };
24789
+ }
24790
+ if (statusCode === 400) {
24791
+ return {
24792
+ category: "bad_request",
24793
+ retryable: false,
24794
+ shouldFallback: false,
24795
+ statusCode,
24796
+ reason: "Invalid request parameters"
24797
+ };
24798
+ }
24799
+ if (statusCode === 429 && (code === "insufficient_quota" || type2 === "insufficient_quota")) {
24800
+ return {
24801
+ category: "quota",
24802
+ retryable: false,
24803
+ shouldFallback: true,
24804
+ statusCode,
24805
+ providerGuess: "openai",
24806
+ reason: "OpenAI quota exceeded, billing issue"
24807
+ };
24808
+ }
24809
+ if (statusCode === 402 && type2 === "billing_error") {
24810
+ return {
24811
+ category: "quota",
24812
+ retryable: false,
24813
+ shouldFallback: true,
24814
+ statusCode,
24815
+ providerGuess: "anthropic",
24816
+ reason: "Anthropic billing error, payment required"
24817
+ };
24818
+ }
24819
+ if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiQuotaDetails(info.headers ? undefined : error?.error?.details)) {
24820
+ return {
24821
+ category: "quota",
24822
+ retryable: false,
24823
+ shouldFallback: true,
24824
+ statusCode,
24825
+ providerGuess: "gemini",
24826
+ reason: "Gemini daily quota exceeded"
24827
+ };
24828
+ }
24829
+ if (statusCode === 429 && isZhipuQuotaCode(code)) {
24830
+ const quotaReasons = {
24831
+ 1113: "\u8D26\u6237\u6B20\u8D39",
24832
+ 1304: "\u8C03\u7528\u6B21\u6570\u8D85\u8FC7\u9650\u989D",
24833
+ 1308: "\u4F7F\u7528\u91CF\u8D85\u8FC7\u4E0A\u9650",
24834
+ 1309: "\u5957\u9910\u5DF2\u5230\u671F"
24835
+ };
24836
+ return {
24837
+ category: "quota",
24838
+ retryable: false,
24839
+ shouldFallback: true,
24840
+ statusCode,
24841
+ providerGuess: "zhipu",
24842
+ reason: `Zhipu/GLM: ${quotaReasons[code] ?? "quota exceeded"}`
24843
+ };
24844
+ }
24845
+ if (statusCode === 529 && type2 === "overloaded_error") {
24846
+ return {
24847
+ category: "overloaded",
24848
+ retryable: true,
24849
+ shouldFallback: false,
24850
+ statusCode,
24851
+ providerGuess: "anthropic",
24852
+ reason: "Anthropic API overloaded"
24853
+ };
24854
+ }
24855
+ if (statusCode === 429 && isZhipuOverloadedCode(code)) {
24856
+ return {
24857
+ category: "overloaded",
24858
+ retryable: true,
24859
+ shouldFallback: false,
24860
+ statusCode,
24861
+ providerGuess: "zhipu",
24862
+ reason: "Zhipu/GLM: \u5F53\u524D\u8D1F\u8F7D\u8FC7\u9AD8"
24863
+ };
24864
+ }
24865
+ if (statusCode === 429 && type2 === "rate_limit_error") {
24866
+ return {
24867
+ category: "rate_limit",
24868
+ retryable: true,
24869
+ shouldFallback: false,
24870
+ statusCode,
24871
+ providerGuess: "anthropic",
24872
+ retryAfterMs: parseRetryAfterMs(headers),
24873
+ reason: "Anthropic rate limit exceeded"
24874
+ };
24875
+ }
24876
+ if (statusCode === 429 && code === "rate_limit_exceeded") {
24877
+ return {
24878
+ category: "rate_limit",
24879
+ retryable: true,
24880
+ shouldFallback: false,
24881
+ statusCode,
24882
+ providerGuess: "openai",
24883
+ retryAfterMs: parseRetryAfterMs(headers),
24884
+ reason: "OpenAI rate limit exceeded"
24885
+ };
24886
+ }
24887
+ if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiPerMinuteLimit(message, error?.error?.details)) {
24888
+ return {
24889
+ category: "rate_limit",
24890
+ retryable: true,
24891
+ shouldFallback: false,
24892
+ statusCode,
24893
+ providerGuess: "gemini",
24894
+ retryAfterMs: parseRetryAfterMs(headers),
24895
+ reason: "Gemini per-minute rate limit exceeded"
24896
+ };
24897
+ }
24898
+ if (statusCode === 429 && isZhipuRateLimitCode(code)) {
24899
+ const rateLimitReasons = {
24900
+ 1302: "\u5E76\u53D1\u8BF7\u6C42\u8D85\u8FC7\u9650\u5236",
24901
+ 1303: "\u8BF7\u6C42\u9891\u7387\u8D85\u8FC7\u9650\u5236"
24902
+ };
24903
+ return {
24904
+ category: "rate_limit",
24905
+ retryable: true,
24906
+ shouldFallback: false,
24907
+ statusCode,
24908
+ providerGuess: "zhipu",
24909
+ retryAfterMs: parseRetryAfterMs(headers),
24910
+ reason: `Zhipu/GLM: ${rateLimitReasons[code] ?? "rate limit exceeded"}`
24911
+ };
24912
+ }
24913
+ if (statusCode === 429) {
24914
+ return {
24915
+ category: "rate_limit",
24916
+ retryable: true,
24917
+ shouldFallback: false,
24918
+ statusCode,
24919
+ retryAfterMs: parseRetryAfterMs(headers),
24920
+ reason: "Rate limit exceeded (generic 429)"
24921
+ };
24922
+ }
24923
+ if (isGenericQuotaMessage(message)) {
24924
+ return {
24925
+ category: "quota",
24926
+ retryable: false,
24927
+ shouldFallback: true,
24928
+ statusCode,
24929
+ reason: `Quota exceeded: ${message.substring(0, 100)}`
24930
+ };
24931
+ }
24932
+ return {
24933
+ category: "unknown",
24934
+ retryable: false,
24935
+ shouldFallback: false,
24936
+ statusCode,
24937
+ reason: `Unknown error: ${message.substring(0, 100)}`
24938
+ };
24939
+ }
24940
+
24941
+ // src/shared/retry-strategy.ts
24942
+ var DEFAULT_RETRY_CONFIG = {
24943
+ max_attempts: 3,
24944
+ initial_delay_ms: 1000,
24945
+ backoff_factor: 2,
24946
+ max_delay_ms: 30000,
24947
+ jitter: true,
24948
+ respect_retry_after: true
24949
+ };
24950
+ function calculateRetryDelay(attempt, config, retryAfterMs) {
24951
+ if (attempt >= config.max_attempts) {
24952
+ return {
24953
+ retryable: false,
24954
+ delay_ms: 0,
24955
+ attempt,
24956
+ reason: `max attempts (${config.max_attempts}) reached`
24957
+ };
24958
+ }
24959
+ const exponentialDelay = config.initial_delay_ms * Math.pow(config.backoff_factor, attempt);
24960
+ let baseDelay;
24961
+ let reason;
24962
+ if (config.respect_retry_after && retryAfterMs !== undefined && retryAfterMs > 0) {
24963
+ baseDelay = retryAfterMs;
24964
+ reason = "Retry-After";
24965
+ } else {
24966
+ baseDelay = exponentialDelay;
24967
+ reason = "exponential backoff";
24968
+ }
24969
+ baseDelay = Math.min(baseDelay, config.max_delay_ms);
24970
+ let finalDelay;
24971
+ if (config.jitter) {
24972
+ const jitterRange = baseDelay * 0.5;
24973
+ finalDelay = baseDelay + (Math.random() * 2 - 1) * jitterRange;
24974
+ finalDelay = Math.max(0, finalDelay);
24975
+ } else {
24976
+ finalDelay = baseDelay;
24977
+ }
24978
+ return {
24979
+ retryable: true,
24980
+ delay_ms: Math.round(finalDelay),
24981
+ attempt,
24982
+ reason
24983
+ };
24984
+ }
24985
+
24986
+ // src/hooks/runtime-fallback/index.ts
24987
+ init_logger();
24988
+ init_runtime_fallback();
24989
+ function createRuntimeFallbackHook(ctx, options) {
24990
+ const retryStates = new Map;
24991
+ const fallbackAttempts = new Map;
24992
+ const config = options?.config ?? {
24993
+ enabled: true,
24994
+ max_attempts: 3,
24995
+ max_retries_before_fallback: 2,
24996
+ initial_delay_ms: DEFAULT_RETRY_CONFIG.initial_delay_ms,
24997
+ backoff_factor: DEFAULT_RETRY_CONFIG.backoff_factor,
24998
+ max_delay_ms: DEFAULT_RETRY_CONFIG.max_delay_ms,
24999
+ respect_retry_after: DEFAULT_RETRY_CONFIG.respect_retry_after,
25000
+ jitter: DEFAULT_RETRY_CONFIG.jitter
25001
+ };
25002
+ const handler = async ({
25003
+ event
25004
+ }) => {
25005
+ if (!config.enabled)
25006
+ return false;
25007
+ if (event.type === "session.deleted") {
25008
+ const props2 = event.properties;
25009
+ const info = props2?.info;
25010
+ const sessionID2 = info?.id ?? props2?.sessionID;
25011
+ if (sessionID2) {
25012
+ retryStates.delete(sessionID2);
25013
+ fallbackAttempts.delete(sessionID2);
25014
+ }
25015
+ return false;
25016
+ }
25017
+ if (event.type !== "session.error")
25018
+ return false;
25019
+ const props = event.properties;
25020
+ const sessionID = props?.sessionID;
25021
+ const error = props?.error;
25022
+ if (!sessionID || error === undefined || error === null)
25023
+ return false;
25024
+ if (options?.sessionRecovery?.isRecoverableError(error)) {
25025
+ return false;
25026
+ }
25027
+ const classification = classifyProviderError(error);
25028
+ if (classification.category === "context_overflow") {
25029
+ return false;
25030
+ }
25031
+ if (classification.category === "auth" || classification.category === "bad_request") {
25032
+ return false;
25033
+ }
25034
+ if (classification.category === "rate_limit") {
25035
+ const state2 = retryStates.get(sessionID) ?? { attempt: 0, lastAttemptTime: Date.now() };
25036
+ const decision = calculateRetryDelay(state2.attempt, config, classification.retryAfterMs);
25037
+ if (decision.retryable && state2.attempt < config.max_retries_before_fallback) {
25038
+ retryStates.set(sessionID, {
25039
+ attempt: state2.attempt + 1,
25040
+ lastAttemptTime: Date.now()
25041
+ });
25042
+ await new Promise((resolve8) => setTimeout(resolve8, decision.delay_ms));
25043
+ return false;
25044
+ }
25045
+ retryStates.delete(sessionID);
25046
+ }
25047
+ if (classification.category !== "quota" && classification.category !== "rate_limit") {
25048
+ return false;
25049
+ }
25050
+ let currentModel = { providerID: "", modelID: "" };
25051
+ let agent = props?.agent;
25052
+ const category = props?.category;
25053
+ try {
25054
+ const messagesResp = await ctx.client.session.messages?.({ path: { id: sessionID } });
25055
+ const messages = messagesResp?.data ?? [];
25056
+ for (let i2 = messages.length - 1;i2 >= 0; i2--) {
25057
+ const info = messages[i2].info;
25058
+ if (!agent && info?.agent)
25059
+ agent = info.agent;
25060
+ const msgModel = info?.model;
25061
+ if (msgModel?.providerID && msgModel?.modelID) {
25062
+ currentModel = { providerID: msgModel.providerID, modelID: msgModel.modelID };
25063
+ break;
25064
+ }
25065
+ if (info?.providerID && info?.modelID) {
25066
+ currentModel = { providerID: info.providerID, modelID: info.modelID };
25067
+ break;
25068
+ }
25069
+ }
25070
+ } catch (messageReadError) {
25071
+ log("[runtime-fallback] failed to read session messages", { sessionID, error: String(messageReadError) });
25072
+ }
25073
+ agent ??= "sisyphus";
25074
+ const attempts = fallbackAttempts.get(sessionID) ?? [];
25075
+ const fallbackResult = resolveNextFallbackModel({
25076
+ agent,
25077
+ category,
25078
+ currentModel,
25079
+ attempts,
25080
+ configuredFallbackModels: options?.getConfiguredFallbackModels?.(agent, category),
25081
+ maxAttempts: config.max_attempts,
25082
+ lastErrorClassification: classification
25083
+ });
25084
+ if (fallbackResult.kind !== "next") {
25085
+ return false;
25086
+ }
25087
+ try {
25088
+ await ctx.client.session.prompt({
25089
+ path: { id: sessionID },
25090
+ body: {
25091
+ model: fallbackResult.model,
25092
+ parts: [{ type: "text", text: "continue" }]
25093
+ },
25094
+ query: { directory: ctx.directory }
25095
+ });
25096
+ fallbackAttempts.delete(sessionID);
25097
+ return true;
25098
+ } catch (fallbackError) {
25099
+ fallbackAttempts.set(sessionID, [
25100
+ ...attempts,
25101
+ { model: fallbackResult.model, error: classifyProviderError(fallbackError) }
25102
+ ]);
25103
+ return false;
25104
+ }
25105
+ };
25106
+ return { handler };
25107
+ }
24632
25108
  // src/features/context-injector/collector.ts
24633
25109
  var PRIORITY_ORDER = {
24634
25110
  critical: 0,
@@ -43385,256 +43861,7 @@ function initTaskToastManager(client2, concurrencyManager) {
43385
43861
  }
43386
43862
  // src/tools/delegate-task/tools.ts
43387
43863
  init_shared();
43388
-
43389
- // src/shared/provider-error-classifier.ts
43390
- function extractErrorInfo(error45) {
43391
- if (typeof error45 === "string") {
43392
- return { message: error45 };
43393
- }
43394
- if (error45 instanceof Error) {
43395
- const anyErr = error45;
43396
- return {
43397
- statusCode: anyErr.status ?? anyErr.statusCode ?? anyErr.httpStatus,
43398
- code: anyErr.code ?? anyErr.error?.code,
43399
- type: anyErr.type ?? anyErr.error?.type,
43400
- message: error45.message,
43401
- status: anyErr.status ?? anyErr.error?.status,
43402
- headers: anyErr.headers
43403
- };
43404
- }
43405
- if (typeof error45 === "object" && error45 !== null) {
43406
- const obj = error45;
43407
- const inner = obj.error ?? {};
43408
- return {
43409
- statusCode: obj.status ?? obj.statusCode ?? inner.status,
43410
- code: inner.code ?? obj.code,
43411
- type: inner.type ?? obj.type,
43412
- message: inner.message ?? obj.message ?? String(error45),
43413
- status: inner.status ?? obj.status,
43414
- headers: obj.headers
43415
- };
43416
- }
43417
- return { message: String(error45) };
43418
- }
43419
- function parseRetryAfterMs(headers) {
43420
- if (!headers)
43421
- return;
43422
- const retryAfter = headers["retry-after"] ?? headers["Retry-After"];
43423
- if (retryAfter) {
43424
- const seconds = Number(retryAfter);
43425
- if (!isNaN(seconds) && seconds > 0) {
43426
- return seconds * 1000;
43427
- }
43428
- }
43429
- const reset = headers["x-ratelimit-reset"] ?? headers["X-Ratelimit-Reset"];
43430
- if (reset) {
43431
- const resetTimestamp = Number(reset);
43432
- if (!isNaN(resetTimestamp)) {
43433
- const resetMs = resetTimestamp > 1000000000000 ? resetTimestamp : resetTimestamp * 1000;
43434
- const delayMs = resetMs - Date.now();
43435
- return delayMs > 0 ? delayMs : 0;
43436
- }
43437
- }
43438
- return;
43439
- }
43440
- function isContextOverflow(message, code) {
43441
- const lowerMessage = message.toLowerCase();
43442
- return lowerMessage.includes("context_length_exceeded") || lowerMessage.includes("prompt is too long") || lowerMessage.includes("maximum context length") || code === "context_length_exceeded";
43443
- }
43444
- function isZhipuQuotaCode(code) {
43445
- if (typeof code !== "number")
43446
- return false;
43447
- return [1113, 1304, 1308, 1309].includes(code);
43448
- }
43449
- function isZhipuRateLimitCode(code) {
43450
- if (typeof code !== "number")
43451
- return false;
43452
- return [1302, 1303].includes(code);
43453
- }
43454
- function isZhipuOverloadedCode(code) {
43455
- if (typeof code !== "number")
43456
- return false;
43457
- return code === 1312;
43458
- }
43459
- function isGeminiQuotaDetails(details) {
43460
- if (!Array.isArray(details))
43461
- return false;
43462
- return details.some((d) => d?.["@type"]?.includes("QuotaFailure") || d?.["@type"]?.includes("google.rpc.QuotaFailure"));
43463
- }
43464
- function isGeminiPerMinuteLimit(message, details) {
43465
- const lowerMessage = message.toLowerCase();
43466
- if (lowerMessage.includes("per_minute") || lowerMessage.includes("per minute")) {
43467
- return true;
43468
- }
43469
- if (Array.isArray(details)) {
43470
- return details.some((d) => d?.["@type"]?.includes("RetryInfo"));
43471
- }
43472
- return false;
43473
- }
43474
- function classifyProviderError(error45) {
43475
- const info = extractErrorInfo(error45);
43476
- const { statusCode, code, type: type2, message, status, headers } = info;
43477
- if (isContextOverflow(message, code)) {
43478
- return {
43479
- category: "context_overflow",
43480
- retryable: false,
43481
- shouldFallback: false,
43482
- statusCode,
43483
- reason: "Context length exceeded, prompt too long for model"
43484
- };
43485
- }
43486
- if (statusCode === 401 || statusCode === 403) {
43487
- return {
43488
- category: "auth",
43489
- retryable: false,
43490
- shouldFallback: false,
43491
- statusCode,
43492
- reason: statusCode === 401 ? "Invalid API key or authentication" : "Permission denied"
43493
- };
43494
- }
43495
- if (statusCode === 400) {
43496
- return {
43497
- category: "bad_request",
43498
- retryable: false,
43499
- shouldFallback: false,
43500
- statusCode,
43501
- reason: "Invalid request parameters"
43502
- };
43503
- }
43504
- if (statusCode === 429 && (code === "insufficient_quota" || type2 === "insufficient_quota")) {
43505
- return {
43506
- category: "quota",
43507
- retryable: false,
43508
- shouldFallback: true,
43509
- statusCode,
43510
- providerGuess: "openai",
43511
- reason: "OpenAI quota exceeded, billing issue"
43512
- };
43513
- }
43514
- if (statusCode === 402 && type2 === "billing_error") {
43515
- return {
43516
- category: "quota",
43517
- retryable: false,
43518
- shouldFallback: true,
43519
- statusCode,
43520
- providerGuess: "anthropic",
43521
- reason: "Anthropic billing error, payment required"
43522
- };
43523
- }
43524
- if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiQuotaDetails(info.headers ? undefined : error45?.error?.details)) {
43525
- return {
43526
- category: "quota",
43527
- retryable: false,
43528
- shouldFallback: true,
43529
- statusCode,
43530
- providerGuess: "gemini",
43531
- reason: "Gemini daily quota exceeded"
43532
- };
43533
- }
43534
- if (statusCode === 429 && isZhipuQuotaCode(code)) {
43535
- const quotaReasons = {
43536
- 1113: "\u8D26\u6237\u6B20\u8D39",
43537
- 1304: "\u8C03\u7528\u6B21\u6570\u8D85\u8FC7\u9650\u989D",
43538
- 1308: "\u4F7F\u7528\u91CF\u8D85\u8FC7\u4E0A\u9650",
43539
- 1309: "\u5957\u9910\u5DF2\u5230\u671F"
43540
- };
43541
- return {
43542
- category: "quota",
43543
- retryable: false,
43544
- shouldFallback: true,
43545
- statusCode,
43546
- providerGuess: "zhipu",
43547
- reason: `Zhipu/GLM: ${quotaReasons[code] ?? "quota exceeded"}`
43548
- };
43549
- }
43550
- if (statusCode === 529 && type2 === "overloaded_error") {
43551
- return {
43552
- category: "overloaded",
43553
- retryable: true,
43554
- shouldFallback: false,
43555
- statusCode,
43556
- providerGuess: "anthropic",
43557
- reason: "Anthropic API overloaded"
43558
- };
43559
- }
43560
- if (statusCode === 429 && isZhipuOverloadedCode(code)) {
43561
- return {
43562
- category: "overloaded",
43563
- retryable: true,
43564
- shouldFallback: false,
43565
- statusCode,
43566
- providerGuess: "zhipu",
43567
- reason: "Zhipu/GLM: \u5F53\u524D\u8D1F\u8F7D\u8FC7\u9AD8"
43568
- };
43569
- }
43570
- if (statusCode === 429 && type2 === "rate_limit_error") {
43571
- return {
43572
- category: "rate_limit",
43573
- retryable: true,
43574
- shouldFallback: false,
43575
- statusCode,
43576
- providerGuess: "anthropic",
43577
- retryAfterMs: parseRetryAfterMs(headers),
43578
- reason: "Anthropic rate limit exceeded"
43579
- };
43580
- }
43581
- if (statusCode === 429 && code === "rate_limit_exceeded") {
43582
- return {
43583
- category: "rate_limit",
43584
- retryable: true,
43585
- shouldFallback: false,
43586
- statusCode,
43587
- providerGuess: "openai",
43588
- retryAfterMs: parseRetryAfterMs(headers),
43589
- reason: "OpenAI rate limit exceeded"
43590
- };
43591
- }
43592
- if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiPerMinuteLimit(message, error45?.error?.details)) {
43593
- return {
43594
- category: "rate_limit",
43595
- retryable: true,
43596
- shouldFallback: false,
43597
- statusCode,
43598
- providerGuess: "gemini",
43599
- retryAfterMs: parseRetryAfterMs(headers),
43600
- reason: "Gemini per-minute rate limit exceeded"
43601
- };
43602
- }
43603
- if (statusCode === 429 && isZhipuRateLimitCode(code)) {
43604
- const rateLimitReasons = {
43605
- 1302: "\u5E76\u53D1\u8BF7\u6C42\u8D85\u8FC7\u9650\u5236",
43606
- 1303: "\u8BF7\u6C42\u9891\u7387\u8D85\u8FC7\u9650\u5236"
43607
- };
43608
- return {
43609
- category: "rate_limit",
43610
- retryable: true,
43611
- shouldFallback: false,
43612
- statusCode,
43613
- providerGuess: "zhipu",
43614
- retryAfterMs: parseRetryAfterMs(headers),
43615
- reason: `Zhipu/GLM: ${rateLimitReasons[code] ?? "rate limit exceeded"}`
43616
- };
43617
- }
43618
- if (statusCode === 429) {
43619
- return {
43620
- category: "rate_limit",
43621
- retryable: true,
43622
- shouldFallback: false,
43623
- statusCode,
43624
- retryAfterMs: parseRetryAfterMs(headers),
43625
- reason: "Rate limit exceeded (generic 429)"
43626
- };
43627
- }
43628
- return {
43629
- category: "unknown",
43630
- retryable: false,
43631
- shouldFallback: false,
43632
- statusCode,
43633
- reason: `Unknown error: ${message.substring(0, 100)}`
43634
- };
43635
- }
43636
-
43637
- // src/tools/delegate-task/tools.ts
43864
+ init_runtime_fallback();
43638
43865
  var SISYPHUS_JUNIOR_AGENT = "sisyphus-junior";
43639
43866
  function parseModelString(model) {
43640
43867
  const parts = model.split("/");
@@ -43643,6 +43870,19 @@ function parseModelString(model) {
43643
43870
  }
43644
43871
  return;
43645
43872
  }
43873
+ function parseFallbackModelEntries(entries) {
43874
+ return entries?.map((entry) => {
43875
+ if ("model" in entry) {
43876
+ const parsed = parseModelString(entry.model);
43877
+ return {
43878
+ providerID: parsed?.providerID ?? "",
43879
+ modelID: parsed?.modelID ?? entry.model,
43880
+ variant: entry.variant
43881
+ };
43882
+ }
43883
+ return entry;
43884
+ });
43885
+ }
43646
43886
  function getMessageDir9(sessionID) {
43647
43887
  if (!existsSync48(MESSAGE_STORAGE))
43648
43888
  return null;
@@ -43732,8 +43972,15 @@ ${categoryPromptAppend}`;
43732
43972
  return skillContent || categoryPromptAppend;
43733
43973
  }
43734
43974
  function createDelegateTask(options) {
43735
- const { manager, client: client2, directory, userCategories, gitMasterConfig, sisyphusJuniorModel } = options;
43975
+ const { manager, client: client2, directory, userCategories, gitMasterConfig, sisyphusJuniorModel, runtimeFallbackConfig, agentFallbackModels } = options;
43736
43976
  const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories };
43977
+ const getConfiguredFallbackModels = (agent, category) => {
43978
+ const agentModels = agent ? agentFallbackModels?.[agent] : undefined;
43979
+ if (agentModels)
43980
+ return parseFallbackModelEntries(agentModels);
43981
+ const categoryModels = category ? userCategories?.[category]?.fallback_models : undefined;
43982
+ return parseFallbackModelEntries(categoryModels);
43983
+ };
43737
43984
  const categoryNames = Object.keys(allCategories);
43738
43985
  const categoryExamples = categoryNames.map((k) => `'${k}'`).join(", ");
43739
43986
  const categoryList = categoryNames.map((name) => {
@@ -44062,6 +44309,7 @@ ${textContent || "(\u65E0\u6587\u672C\u8F93\u51FA)"}
44062
44309
  parentMessageID: ctx.messageID,
44063
44310
  parentModel,
44064
44311
  parentAgent,
44312
+ category: args.category,
44065
44313
  model: categoryModel,
44066
44314
  skills: args.load_skills.length > 0 ? args.load_skills : undefined,
44067
44315
  skillContent: systemContent
@@ -44162,26 +44410,85 @@ Status: ${task.status}
44162
44410
  }
44163
44411
  });
44164
44412
  } catch (promptError) {
44165
- if (toastManager && taskId !== undefined) {
44166
- toastManager.removeTask(taskId);
44167
- }
44168
- const errorMessage = promptError instanceof Error ? promptError.message : String(promptError);
44169
- if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
44170
- return formatDetailedError(new Error(`Agent "${agentToUse}" \u672A\u627E\u5230\u3002\u8BF7\u786E\u8BA4\u8BE5 agent \u5DF2\u5728 opencode.json \u4E2D\u6CE8\u518C\u6216\u7531\u63D2\u4EF6\u63D0\u4F9B\u3002`), {
44171
- operation: "\u53D1\u9001 prompt \u7ED9 agent",
44413
+ const classification = classifyProviderError(promptError);
44414
+ const canFallback = runtimeFallbackConfig?.enabled !== false && (classification.retryable || classification.shouldFallback);
44415
+ if (canFallback) {
44416
+ const attempts = [];
44417
+ let fallbackSucceeded = false;
44418
+ let currentModel = categoryModel ?? { providerID: "", modelID: "" };
44419
+ let currentClassification = classification;
44420
+ while (true) {
44421
+ const fallbackResult = resolveNextFallbackModel({
44422
+ agent: agentToUse,
44423
+ category: args.category,
44424
+ currentModel,
44425
+ attempts,
44426
+ configuredFallbackModels: getConfiguredFallbackModels(agentToUse, args.category),
44427
+ maxAttempts: runtimeFallbackConfig?.max_attempts,
44428
+ lastErrorClassification: currentClassification
44429
+ });
44430
+ if (fallbackResult.kind !== "next")
44431
+ break;
44432
+ try {
44433
+ await client2.session.prompt({
44434
+ path: { id: sessionID },
44435
+ body: {
44436
+ agent: agentToUse,
44437
+ system: systemContent,
44438
+ tools: {
44439
+ task: false,
44440
+ delegate_task: false,
44441
+ call_omo_agent: true
44442
+ },
44443
+ parts: [{ type: "text", text: args.prompt }],
44444
+ model: fallbackResult.model
44445
+ }
44446
+ });
44447
+ fallbackSucceeded = true;
44448
+ break;
44449
+ } catch (fallbackError) {
44450
+ currentClassification = classifyProviderError(fallbackError);
44451
+ attempts.push({ model: fallbackResult.model, error: currentClassification });
44452
+ currentModel = fallbackResult.model;
44453
+ if (!currentClassification.retryable && !currentClassification.shouldFallback) {
44454
+ throw fallbackError;
44455
+ }
44456
+ }
44457
+ }
44458
+ if (!fallbackSucceeded) {
44459
+ if (toastManager && taskId !== undefined) {
44460
+ toastManager.removeTask(taskId);
44461
+ }
44462
+ return formatDetailedError(promptError, {
44463
+ operation: "\u53D1\u9001 prompt",
44464
+ args,
44465
+ sessionID,
44466
+ agent: agentToUse,
44467
+ category: args.category
44468
+ });
44469
+ }
44470
+ } else {
44471
+ if (toastManager && taskId !== undefined) {
44472
+ toastManager.removeTask(taskId);
44473
+ }
44474
+ const errorMessage = promptError instanceof Error ? promptError.message : String(promptError);
44475
+ if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
44476
+ return formatDetailedError(new Error(`Agent "${agentToUse}" \u672A\u627E\u5230\u3002\u8BF7\u786E\u8BA4\u8BE5 agent \u5DF2\u5728 opencode.json \u4E2D\u6CE8\u518C\u6216\u7531\u63D2\u4EF6\u63D0\u4F9B\u3002`), {
44477
+ operation: "\u53D1\u9001 prompt \u7ED9 agent",
44478
+ args,
44479
+ sessionID,
44480
+ agent: agentToUse,
44481
+ category: args.category
44482
+ });
44483
+ }
44484
+ return formatDetailedError(promptError, {
44485
+ operation: "\u53D1\u9001 prompt",
44172
44486
  args,
44173
44487
  sessionID,
44174
44488
  agent: agentToUse,
44175
44489
  category: args.category
44176
44490
  });
44177
44491
  }
44178
- return formatDetailedError(promptError, {
44179
- operation: "\u53D1\u9001 prompt",
44180
- args,
44181
- sessionID,
44182
- agent: agentToUse,
44183
- category: args.category
44184
- });
44185
44492
  }
44186
44493
  const POLL_INTERVAL_MS = 500;
44187
44494
  const MAX_POLL_TIME_MS = 10 * 60 * 1000;
@@ -44250,7 +44557,7 @@ Session ID: ${sessionID}`;
44250
44557
  const diagnosis = classification.category !== "unknown" ? `
44251
44558
 
44252
44559
  \uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
44253
- ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u53EF\u901A\u8FC7 runtime fallback \u81EA\u52A8\u5904\u7406\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44560
+ ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u7B26\u5408 runtime fallback \u6761\u4EF6\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44254
44561
  return `Error fetching result: ${messagesResult.error}${diagnosis}
44255
44562
 
44256
44563
  Session ID: ${sessionID}`;
@@ -44293,7 +44600,7 @@ ${textContent || "(\u65E0\u6587\u672C\u8F93\u51FA)"}
44293
44600
  const diagnosis = classification.category !== "unknown" ? `
44294
44601
 
44295
44602
  \uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
44296
- ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u53EF\u901A\u8FC7 runtime fallback \u81EA\u52A8\u5904\u7406\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44603
+ ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u7B26\u5408 runtime fallback \u6761\u4EF6\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44297
44604
  return `\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error45 instanceof Error ? error45.message : String(error45)}${diagnosis}
44298
44605
 
44299
44606
  Session ID: ${syncSessionID ?? "unknown"}`;
@@ -44548,6 +44855,7 @@ class BackgroundManager {
44548
44855
  parentMessageID: input.parentMessageID,
44549
44856
  parentModel: input.parentModel,
44550
44857
  parentAgent: input.parentAgent,
44858
+ category: input.category,
44551
44859
  model: input.model,
44552
44860
  maxSteps: this.config?.maxSteps,
44553
44861
  maxRuntimeMs: this.config?.maxRuntimeMs,
@@ -44676,72 +44984,79 @@ class BackgroundManager {
44676
44984
  },
44677
44985
  parts: [{ type: "text", text: input.prompt }]
44678
44986
  }
44679
- }).catch((error45) => {
44680
- log("[background-agent] promptAsync error:", error45);
44681
- const existingTask = this.findBySession(sessionID);
44682
- if (existingTask) {
44683
- const classification = classifyProviderError(error45);
44684
- if (classification.retryable || classification.shouldFallback) {
44685
- const attempts = existingTask.attempts ?? [];
44686
- const currentModel = input.model ?? { providerID: "", modelID: "" };
44687
- const fallbackResult = resolveNextFallbackModel({
44688
- agent: input.agent,
44689
- currentModel,
44690
- attempts,
44691
- lastErrorClassification: classification
44692
- });
44693
- if (fallbackResult.kind === "next") {
44694
- existingTask.attempts = [...attempts, {
44987
+ }).catch(async (error45) => {
44988
+ await this.handlePromptFailure(sessionID, input, error45);
44989
+ });
44990
+ }
44991
+ async handlePromptFailure(sessionID, input, error45) {
44992
+ log("[background-agent] promptAsync error:", error45);
44993
+ const existingTask = this.findBySession(sessionID);
44994
+ if (!existingTask)
44995
+ return;
44996
+ const runtimeFallback = this.config?.runtimeFallback;
44997
+ const classification = classifyProviderError(error45);
44998
+ const canFallback = runtimeFallback?.enabled !== false && (classification.retryable || classification.shouldFallback);
44999
+ if (canFallback) {
45000
+ let currentError = error45;
45001
+ let currentClassification = classification;
45002
+ let currentModel = input.model ?? { providerID: "", modelID: "" };
45003
+ while (true) {
45004
+ const attempts = existingTask.attempts ?? [];
45005
+ const fallbackResult = resolveNextFallbackModel({
45006
+ agent: input.agent,
45007
+ category: input.category,
45008
+ currentModel,
45009
+ attempts,
45010
+ maxAttempts: runtimeFallback?.max_attempts,
45011
+ lastErrorClassification: currentClassification
45012
+ });
45013
+ if (fallbackResult.kind !== "next") {
45014
+ existingTask.error = fallbackResult.kind === "exhausted" ? `All fallback models exhausted. Last error: ${currentClassification.reason}` : fallbackResult.reason;
45015
+ break;
45016
+ }
45017
+ log("[background-agent] Fallback to model:", fallbackResult.model);
45018
+ try {
45019
+ await this.client.session.prompt({
45020
+ path: { id: sessionID },
45021
+ body: {
45022
+ agent: input.agent,
44695
45023
  model: fallbackResult.model,
44696
- error: classification
44697
- }];
44698
- log("[background-agent] Fallback to model:", fallbackResult.model);
44699
- this.client.session.prompt({
44700
- path: { id: sessionID },
44701
- body: {
44702
- agent: input.agent,
44703
- model: fallbackResult.model,
44704
- parts: [{ type: "text", text: input.prompt }]
44705
- }
44706
- }).catch((retryError) => {
44707
- log("[background-agent] Fallback prompt error:", retryError);
44708
- const task2 = this.findBySession(sessionID);
44709
- if (task2) {
44710
- task2.status = "error";
44711
- task2.error = `Fallback failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`;
44712
- task2.completedAt = new Date;
44713
- if (task2.concurrencyKey) {
44714
- this.concurrencyManager.release(task2.concurrencyKey);
44715
- task2.concurrencyKey = undefined;
44716
- }
44717
- this.markForNotification(task2);
44718
- this.notifyParentSession(task2).catch((err) => {
44719
- log("[background-agent] Failed to notify on fallback error:", err);
44720
- });
44721
- }
44722
- });
44723
- return;
44724
- }
44725
- existingTask.error = `All fallback models exhausted. Last error: ${classification.reason}`;
44726
- } else {
44727
- const errorMessage = error45 instanceof Error ? error45.message : String(error45);
44728
- if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
44729
- existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`;
44730
- } else {
44731
- existingTask.error = errorMessage;
45024
+ parts: [{ type: "text", text: input.prompt }]
45025
+ }
45026
+ });
45027
+ existingTask.attempts = attempts;
45028
+ return;
45029
+ } catch (retryError) {
45030
+ currentError = retryError;
45031
+ currentClassification = classifyProviderError(currentError);
45032
+ existingTask.attempts = [...attempts, {
45033
+ model: fallbackResult.model,
45034
+ error: currentClassification
45035
+ }];
45036
+ currentModel = fallbackResult.model;
45037
+ if (!currentClassification.retryable && !currentClassification.shouldFallback) {
45038
+ existingTask.error = `Fallback failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`;
45039
+ break;
44732
45040
  }
44733
45041
  }
44734
- existingTask.status = "error";
44735
- existingTask.completedAt = new Date;
44736
- if (existingTask.concurrencyKey) {
44737
- this.concurrencyManager.release(existingTask.concurrencyKey);
44738
- existingTask.concurrencyKey = undefined;
44739
- }
44740
- this.markForNotification(existingTask);
44741
- this.notifyParentSession(existingTask).catch((err) => {
44742
- log("[background-agent] Failed to notify on error:", err);
44743
- });
44744
45042
  }
45043
+ } else {
45044
+ const errorMessage = error45 instanceof Error ? error45.message : String(error45);
45045
+ if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
45046
+ existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`;
45047
+ } else {
45048
+ existingTask.error = errorMessage;
45049
+ }
45050
+ }
45051
+ existingTask.status = "error";
45052
+ existingTask.completedAt = new Date;
45053
+ if (existingTask.concurrencyKey) {
45054
+ this.concurrencyManager.release(existingTask.concurrencyKey);
45055
+ existingTask.concurrencyKey = undefined;
45056
+ }
45057
+ this.markForNotification(existingTask);
45058
+ this.notifyParentSession(existingTask).catch((err) => {
45059
+ log("[background-agent] Failed to notify on error:", err);
44745
45060
  });
44746
45061
  }
44747
45062
  getTask(id) {
@@ -63568,6 +63883,23 @@ var BuiltinCommandNameSchema = exports_external2.enum([
63568
63883
  "init-deep",
63569
63884
  "start-work"
63570
63885
  ]);
63886
+ var ProviderModelStringSchema = exports_external2.string().refine((value) => {
63887
+ const separatorIndex = value.indexOf("/");
63888
+ return separatorIndex > 0 && separatorIndex < value.length - 1;
63889
+ }, "Expected provider/model format");
63890
+ var NonEmptyStringSchema = exports_external2.string().min(1);
63891
+ var FallbackModelEntrySchema = exports_external2.union([
63892
+ exports_external2.object({
63893
+ model: ProviderModelStringSchema,
63894
+ variant: exports_external2.string().optional()
63895
+ }).strict(),
63896
+ exports_external2.object({
63897
+ providerID: NonEmptyStringSchema,
63898
+ modelID: NonEmptyStringSchema,
63899
+ variant: exports_external2.string().optional()
63900
+ }).strict()
63901
+ ]);
63902
+ var FallbackModelsSchema = exports_external2.array(FallbackModelEntrySchema);
63571
63903
  var AgentOverrideConfigSchema = exports_external2.object({
63572
63904
  model: exports_external2.string().optional(),
63573
63905
  variant: exports_external2.string().optional(),
@@ -63582,7 +63914,8 @@ var AgentOverrideConfigSchema = exports_external2.object({
63582
63914
  description: exports_external2.string().optional(),
63583
63915
  mode: exports_external2.enum(["subagent", "primary", "all"]).optional(),
63584
63916
  color: exports_external2.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
63585
- permission: AgentPermissionSchema.optional()
63917
+ permission: AgentPermissionSchema.optional(),
63918
+ fallback_models: FallbackModelsSchema.optional()
63586
63919
  });
63587
63920
  var AgentOverridesSchema = exports_external2.object({
63588
63921
  build: AgentOverrideConfigSchema.optional(),
@@ -63618,6 +63951,7 @@ var CategoryConfigSchema = exports_external2.object({
63618
63951
  description: exports_external2.string().optional(),
63619
63952
  model: exports_external2.string().optional(),
63620
63953
  variant: exports_external2.string().optional(),
63954
+ fallback_models: FallbackModelsSchema.optional(),
63621
63955
  temperature: exports_external2.number().min(0).max(2).optional(),
63622
63956
  top_p: exports_external2.number().min(0).max(1).optional(),
63623
63957
  maxTokens: exports_external2.number().optional(),
@@ -63748,6 +64082,7 @@ var GitMasterConfigSchema = exports_external2.object({
63748
64082
  var RuntimeFallbackConfigSchema = exports_external2.object({
63749
64083
  enabled: exports_external2.boolean().default(true),
63750
64084
  max_attempts: exports_external2.number().min(0).default(3),
64085
+ max_retries_before_fallback: exports_external2.number().min(0).default(2),
63751
64086
  initial_delay_ms: exports_external2.number().min(0).default(2000),
63752
64087
  backoff_factor: exports_external2.number().min(1).default(2),
63753
64088
  max_delay_ms: exports_external2.number().min(0).default(30000),
@@ -68829,9 +69164,31 @@ var OhMyOpenCodePlugin = async (ctx) => {
68829
69164
  const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
68830
69165
  const firstMessageVariantGate = createFirstMessageVariantGate();
68831
69166
  const isHookEnabled = (hookName) => !disabledHooks.has(hookName);
69167
+ const parseConfiguredFallbackModels = (entries) => entries?.map((entry) => {
69168
+ if ("model" in entry) {
69169
+ const separatorIndex = entry.model.indexOf("/");
69170
+ return {
69171
+ providerID: entry.model.slice(0, separatorIndex),
69172
+ modelID: entry.model.slice(separatorIndex + 1),
69173
+ variant: entry.variant
69174
+ };
69175
+ }
69176
+ return entry;
69177
+ });
68832
69178
  const modelCacheState = createModelCacheState();
68833
69179
  const contextWindowMonitor = isHookEnabled("context-window-monitor") ? createContextWindowMonitorHook(ctx) : null;
68834
69180
  const sessionRecovery = isHookEnabled("session-recovery") ? createSessionRecoveryHook(ctx, { experimental: pluginConfig.experimental }) : null;
69181
+ const runtimeFallback = isHookEnabled("runtime-fallback") && pluginConfig.runtime_fallback?.enabled !== false ? createRuntimeFallbackHook(ctx, {
69182
+ config: pluginConfig.runtime_fallback,
69183
+ sessionRecovery: sessionRecovery ?? undefined,
69184
+ getConfiguredFallbackModels: (agent, category) => {
69185
+ const agentModels = agent && pluginConfig.agents?.[agent]?.fallback_models;
69186
+ if (agentModels)
69187
+ return parseConfiguredFallbackModels(agentModels);
69188
+ const categoryModels = category ? pluginConfig.categories?.[category]?.fallback_models : undefined;
69189
+ return parseConfiguredFallbackModels(categoryModels);
69190
+ }
69191
+ }) : null;
68835
69192
  let sessionNotification = null;
68836
69193
  if (isHookEnabled("session-notification")) {
68837
69194
  const forceEnable = pluginConfig.notification?.force_enable ?? false;
@@ -68884,7 +69241,10 @@ var OhMyOpenCodePlugin = async (ctx) => {
68884
69241
  const prometheusMdOnly = isHookEnabled("prometheus-md-only") ? createPrometheusMdOnlyHook(ctx) : null;
68885
69242
  const questionLabelTruncator = createQuestionLabelTruncatorHook();
68886
69243
  const taskResumeInfo = createTaskResumeInfoHook();
68887
- const backgroundManager = new BackgroundManager(ctx, pluginConfig.background_task);
69244
+ const backgroundManager = new BackgroundManager(ctx, {
69245
+ ...pluginConfig.background_task,
69246
+ runtimeFallback: pluginConfig.runtime_fallback
69247
+ });
68888
69248
  const atlasHook = isHookEnabled("atlas") ? createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }) : null;
68889
69249
  const perfTracer = pluginConfig.experimental?.profiling?.enabled ? new PerfTracer({
68890
69250
  enabled: true,
@@ -68931,7 +69291,9 @@ var OhMyOpenCodePlugin = async (ctx) => {
68931
69291
  directory: ctx.directory,
68932
69292
  userCategories: pluginConfig.categories,
68933
69293
  gitMasterConfig: pluginConfig.git_master,
68934
- sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model
69294
+ sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model,
69295
+ runtimeFallbackConfig: pluginConfig.runtime_fallback,
69296
+ agentFallbackModels: Object.fromEntries(Object.entries(pluginConfig.agents ?? {}).map(([agent, config4]) => [agent, config4?.fallback_models]))
68935
69297
  });
68936
69298
  const disabledSkills = new Set(pluginConfig.disabled_skills ?? []);
68937
69299
  const systemMcpNames = getSystemMcpServerNames();
@@ -69100,6 +69462,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
69100
69462
  hookCount++;
69101
69463
  await wrapWithTiming(perfTracer, "event", "anthropicContextWindowLimitRecovery", () => anthropicContextWindowLimitRecovery?.event(input), evtSessionID);
69102
69464
  hookCount++;
69465
+ await wrapWithTiming(perfTracer, "event", "runtimeFallback", () => runtimeFallback?.handler(input), evtSessionID);
69466
+ hookCount++;
69103
69467
  await wrapWithTiming(perfTracer, "event", "agentUsageReminder", () => agentUsageReminder?.event(input), evtSessionID);
69104
69468
  hookCount++;
69105
69469
  await wrapWithTiming(perfTracer, "event", "interactiveBashSession", () => interactiveBashSession?.event(input), evtSessionID);