opencode-immune 1.0.54 → 1.0.56
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/plugin.js +235 -22
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -11,7 +11,7 @@ import { execFile } from "child_process";
|
|
|
11
11
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
12
12
|
// PLUGIN VERSION CHECK
|
|
13
13
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
14
|
-
const PLUGIN_VERSION = "1.0.
|
|
14
|
+
const PLUGIN_VERSION = "1.0.56";
|
|
15
15
|
const PLUGIN_PACKAGE_NAME = "opencode-immune";
|
|
16
16
|
const PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
function getServerAuthHeaders() {
|
|
@@ -96,6 +96,8 @@ function createState(input) {
|
|
|
96
96
|
providerRetryWatchdogs: new Map(),
|
|
97
97
|
childFallbackRequests: new Map(),
|
|
98
98
|
sessionErrorRetryCount: new Map(),
|
|
99
|
+
fallbackAgentByAgent: new Map(),
|
|
100
|
+
baseAgentByFallbackAgent: new Map(),
|
|
99
101
|
ultraworkMarkerPath: join(input.directory, ".opencode", "state", "ultrawork-active.json"),
|
|
100
102
|
diagnosticsLogPath: join(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
|
|
101
103
|
lastEditAttempt: null,
|
|
@@ -135,12 +137,18 @@ const RETRY_PROMPT_DELIVERY_ATTEMPTS = 3;
|
|
|
135
137
|
const CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
136
138
|
const RATE_LIMIT_FALLBACK_MODEL = {
|
|
137
139
|
providerID: "codexsale",
|
|
138
|
-
modelID: "gpt-5.
|
|
140
|
+
modelID: "gpt-5.4-mini",
|
|
139
141
|
};
|
|
140
142
|
const CHILD_SESSION_FALLBACK_MODEL = {
|
|
141
|
-
providerID: "
|
|
142
|
-
modelID: "
|
|
143
|
+
providerID: "claudehub",
|
|
144
|
+
modelID: "claude-opus-4-7",
|
|
143
145
|
};
|
|
146
|
+
const FALLBACK_MODEL_CANDIDATES = [
|
|
147
|
+
CHILD_SESSION_FALLBACK_MODEL,
|
|
148
|
+
RATE_LIMIT_FALLBACK_MODEL,
|
|
149
|
+
{ providerID: "codexsale", modelID: "gpt-5.5" },
|
|
150
|
+
];
|
|
151
|
+
const FALLBACK_AGENT_SUFFIX = "-provider-fallback";
|
|
144
152
|
function isManagedUltraworkSession(state, sessionID) {
|
|
145
153
|
return !!sessionID && state.managedUltraworkSessions.has(sessionID);
|
|
146
154
|
}
|
|
@@ -315,8 +323,10 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
|
|
|
315
323
|
kind: existing?.kind ?? "root",
|
|
316
324
|
agent: ULTRAWORK_AGENT,
|
|
317
325
|
rootSessionID: existing?.rootSessionID ?? sessionID,
|
|
326
|
+
parentSessionID: existing?.parentSessionID,
|
|
318
327
|
createdAt: existing?.createdAt ?? timestamp,
|
|
319
328
|
updatedAt: timestamp,
|
|
329
|
+
currentModel: existing?.currentModel,
|
|
320
330
|
fallbackModel: existing?.fallbackModel,
|
|
321
331
|
};
|
|
322
332
|
if (existing &&
|
|
@@ -342,8 +352,10 @@ async function addManagedChildSession(state, sessionID, parentSessionID, timesta
|
|
|
342
352
|
kind: "child",
|
|
343
353
|
agent: existing?.agent ?? "unknown",
|
|
344
354
|
rootSessionID: parent.rootSessionID,
|
|
355
|
+
parentSessionID,
|
|
345
356
|
createdAt: existing?.createdAt ?? timestamp,
|
|
346
357
|
updatedAt: timestamp,
|
|
358
|
+
currentModel: existing?.currentModel ?? parent.currentModel,
|
|
347
359
|
fallbackModel: existing?.fallbackModel ?? parent.fallbackModel,
|
|
348
360
|
});
|
|
349
361
|
}
|
|
@@ -396,6 +408,66 @@ function markUltraworkSessionActive(state, sessionID) {
|
|
|
396
408
|
});
|
|
397
409
|
return true;
|
|
398
410
|
}
|
|
411
|
+
function getModelProviderID(model) {
|
|
412
|
+
return model?.providerID || model?.modelID.split("/")[0];
|
|
413
|
+
}
|
|
414
|
+
function isSameModel(a, b) {
|
|
415
|
+
if (!a || !b)
|
|
416
|
+
return false;
|
|
417
|
+
return a.providerID === b.providerID && a.modelID === b.modelID;
|
|
418
|
+
}
|
|
419
|
+
function selectFallbackModel(currentModel) {
|
|
420
|
+
const currentProviderID = getModelProviderID(currentModel);
|
|
421
|
+
const otherProviderModel = FALLBACK_MODEL_CANDIDATES.find((candidate) => !isSameModel(candidate, currentModel) &&
|
|
422
|
+
getModelProviderID(candidate) !== currentProviderID);
|
|
423
|
+
if (otherProviderModel)
|
|
424
|
+
return otherProviderModel;
|
|
425
|
+
return (FALLBACK_MODEL_CANDIDATES.find((candidate) => !isSameModel(candidate, currentModel)) ?? RATE_LIMIT_FALLBACK_MODEL);
|
|
426
|
+
}
|
|
427
|
+
function getFailedModelFromError(error) {
|
|
428
|
+
if (!error || typeof error !== "object")
|
|
429
|
+
return undefined;
|
|
430
|
+
const maybeError = error;
|
|
431
|
+
const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`;
|
|
432
|
+
const providerModelMatch = message.match(/\b([a-z0-9_-]+)\/([a-z0-9._-]+)\b/i);
|
|
433
|
+
if (providerModelMatch?.[1] && providerModelMatch[2]) {
|
|
434
|
+
return {
|
|
435
|
+
providerID: providerModelMatch[1].toLowerCase(),
|
|
436
|
+
modelID: providerModelMatch[2].toLowerCase(),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const unsupportedModelMatch = message.match(/["']([^"']+)["']\s+model\s+is\s+not\s+supported/i);
|
|
440
|
+
if (unsupportedModelMatch?.[1] && /\bcodex\b/i.test(message)) {
|
|
441
|
+
return {
|
|
442
|
+
providerID: "codexsale",
|
|
443
|
+
modelID: unsupportedModelMatch[1].toLowerCase(),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
async function updateManagedSessionModel(state, sessionID, model) {
|
|
449
|
+
const existing = state.managedUltraworkSessions.get(sessionID);
|
|
450
|
+
if (!existing || isSameModel(existing.currentModel, model))
|
|
451
|
+
return;
|
|
452
|
+
state.managedUltraworkSessions.set(sessionID, {
|
|
453
|
+
...existing,
|
|
454
|
+
currentModel: model,
|
|
455
|
+
updatedAt: Date.now(),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function getSessionFallbackModel(state, sessionID, preferredModel) {
|
|
459
|
+
const currentModel = state.managedUltraworkSessions.get(sessionID)?.currentModel;
|
|
460
|
+
if (preferredModel && !isSameModel(preferredModel, currentModel)) {
|
|
461
|
+
return preferredModel;
|
|
462
|
+
}
|
|
463
|
+
return selectFallbackModel(currentModel);
|
|
464
|
+
}
|
|
465
|
+
function getFallbackAgentForAgent(state, agent) {
|
|
466
|
+
return state.fallbackAgentByAgent.get(agent) ?? agent;
|
|
467
|
+
}
|
|
468
|
+
function getRouterSessionIDForChild(record) {
|
|
469
|
+
return record.parentSessionID ?? record.rootSessionID;
|
|
470
|
+
}
|
|
399
471
|
function isRetryableApiError(error) {
|
|
400
472
|
if (!error || typeof error !== "object")
|
|
401
473
|
return false;
|
|
@@ -431,6 +503,7 @@ function isRetryableApiError(error) {
|
|
|
431
503
|
if (message.includes("api_error") ||
|
|
432
504
|
message.includes("не разрешен") ||
|
|
433
505
|
message.includes("not allowed") ||
|
|
506
|
+
message.includes("not supported") ||
|
|
434
507
|
message.includes("internal error") ||
|
|
435
508
|
message.includes("internal server error") ||
|
|
436
509
|
message.includes("expected") ||
|
|
@@ -485,6 +558,7 @@ function isModelAccessError(error) {
|
|
|
485
558
|
const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
|
|
486
559
|
return (message.includes("не разрешен") ||
|
|
487
560
|
message.includes("not allowed") ||
|
|
561
|
+
message.includes("not supported") ||
|
|
488
562
|
message.includes("model not available") ||
|
|
489
563
|
message.includes("model_not_found") ||
|
|
490
564
|
message.includes("access denied") ||
|
|
@@ -534,22 +608,26 @@ function getRetryableErrorType(error) {
|
|
|
534
608
|
return "retryable provider error";
|
|
535
609
|
}
|
|
536
610
|
function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
|
|
611
|
+
const fallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
612
|
+
const routerSessionID = getRouterSessionIDForChild(managedSession);
|
|
537
613
|
const request = {
|
|
538
614
|
childSessionID,
|
|
539
615
|
rootSessionID: managedSession.rootSessionID,
|
|
616
|
+
routerSessionID,
|
|
540
617
|
agent: managedSession.agent || "unknown",
|
|
618
|
+
fallbackAgent: getFallbackAgentForAgent(state, managedSession.agent || "unknown"),
|
|
541
619
|
errorType: getRetryableErrorType(error),
|
|
542
|
-
fallbackModel
|
|
620
|
+
fallbackModel,
|
|
543
621
|
createdAt: Date.now(),
|
|
544
622
|
};
|
|
545
|
-
state.childFallbackRequests.set(
|
|
623
|
+
state.childFallbackRequests.set(routerSessionID, request);
|
|
546
624
|
}
|
|
547
|
-
function getChildFallbackRequest(state,
|
|
548
|
-
const request = state.childFallbackRequests.get(
|
|
625
|
+
function getChildFallbackRequest(state, sessionID, now = Date.now()) {
|
|
626
|
+
const request = state.childFallbackRequests.get(sessionID);
|
|
549
627
|
if (!request)
|
|
550
628
|
return undefined;
|
|
551
629
|
if (now - request.createdAt > CHILD_FALLBACK_REQUEST_TTL_MS) {
|
|
552
|
-
state.childFallbackRequests.delete(
|
|
630
|
+
state.childFallbackRequests.delete(sessionID);
|
|
553
631
|
return undefined;
|
|
554
632
|
}
|
|
555
633
|
return request;
|
|
@@ -565,10 +643,11 @@ function scheduleProviderRetryWatchdog(state, sessionID, model) {
|
|
|
565
643
|
return;
|
|
566
644
|
if (state.sessionRetryTimers.has(sessionID))
|
|
567
645
|
return;
|
|
568
|
-
|
|
646
|
+
const fallbackModel = getSessionFallbackModel(state, sessionID, model);
|
|
647
|
+
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
569
648
|
await writeDiagnosticLog(state, "provider-retry-watchdog:fired", {
|
|
570
649
|
sessionID,
|
|
571
|
-
fallbackModel
|
|
650
|
+
fallbackModel,
|
|
572
651
|
});
|
|
573
652
|
scheduleManagedSessionRetry(state, sessionID, {
|
|
574
653
|
delayMs: 1_000,
|
|
@@ -766,6 +845,129 @@ function compositeChatMessage(handlers) {
|
|
|
766
845
|
}
|
|
767
846
|
};
|
|
768
847
|
}
|
|
848
|
+
function isRecord(value) {
|
|
849
|
+
return !!value && typeof value === "object";
|
|
850
|
+
}
|
|
851
|
+
function getTaskAgentFromArgs(args) {
|
|
852
|
+
if (!isRecord(args))
|
|
853
|
+
return undefined;
|
|
854
|
+
const subagentType = args.subagent_type;
|
|
855
|
+
if (typeof subagentType === "string")
|
|
856
|
+
return subagentType;
|
|
857
|
+
const agent = args.agent;
|
|
858
|
+
if (typeof agent === "string")
|
|
859
|
+
return agent;
|
|
860
|
+
return undefined;
|
|
861
|
+
}
|
|
862
|
+
function setTaskAgentInArgs(args, agent) {
|
|
863
|
+
if (!isRecord(args))
|
|
864
|
+
return;
|
|
865
|
+
if ("subagent_type" in args) {
|
|
866
|
+
args.subagent_type = agent;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if ("agent" in args) {
|
|
870
|
+
args.agent = agent;
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
args.subagent_type = agent;
|
|
874
|
+
}
|
|
875
|
+
function createProviderFallbackToolBefore(state) {
|
|
876
|
+
return async (input, output) => {
|
|
877
|
+
if (input.tool !== "task" && input.tool !== "Task")
|
|
878
|
+
return;
|
|
879
|
+
const request = getChildFallbackRequest(state, input.sessionID);
|
|
880
|
+
if (!request)
|
|
881
|
+
return;
|
|
882
|
+
const requestedAgent = getTaskAgentFromArgs(output.args);
|
|
883
|
+
if (!requestedAgent || requestedAgent !== request.agent)
|
|
884
|
+
return;
|
|
885
|
+
if (request.fallbackAgent === request.agent)
|
|
886
|
+
return;
|
|
887
|
+
setTaskAgentInArgs(output.args, request.fallbackAgent);
|
|
888
|
+
state.childFallbackRequests.delete(input.sessionID);
|
|
889
|
+
await writeDiagnosticLog(state, "provider-fallback-task:rewrite", {
|
|
890
|
+
sessionID: input.sessionID,
|
|
891
|
+
childSessionID: request.childSessionID,
|
|
892
|
+
callID: input.callID,
|
|
893
|
+
fromAgent: request.agent,
|
|
894
|
+
toAgent: request.fallbackAgent,
|
|
895
|
+
fallbackModel: request.fallbackModel,
|
|
896
|
+
});
|
|
897
|
+
writePluginLog(state, "warn", `[opencode-immune] Rewrote provider retry Task agent ${request.agent} -> ${request.fallbackAgent} for session ${input.sessionID}.`);
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function modelRefToString(model) {
|
|
901
|
+
return `${model.providerID}/${model.modelID}`;
|
|
902
|
+
}
|
|
903
|
+
function parseModelRef(model) {
|
|
904
|
+
if (!model)
|
|
905
|
+
return undefined;
|
|
906
|
+
const [providerID, ...modelParts] = model.split("/");
|
|
907
|
+
const modelID = modelParts.join("/");
|
|
908
|
+
if (!providerID || !modelID)
|
|
909
|
+
return undefined;
|
|
910
|
+
return { providerID, modelID };
|
|
911
|
+
}
|
|
912
|
+
function canCreateFallbackForAgent(agentID, agentConfig) {
|
|
913
|
+
if (agentID.endsWith(FALLBACK_AGENT_SUFFIX))
|
|
914
|
+
return false;
|
|
915
|
+
if (!isRecord(agentConfig))
|
|
916
|
+
return false;
|
|
917
|
+
return typeof agentConfig.model === "string";
|
|
918
|
+
}
|
|
919
|
+
function globPatternMatchesAgent(pattern, agentID) {
|
|
920
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
921
|
+
const regex = new RegExp(`^${escaped.replace(/\\\*/g, ".*")}$`);
|
|
922
|
+
return regex.test(agentID);
|
|
923
|
+
}
|
|
924
|
+
function allowFallbackAgentInTaskPermissions(config, baseAgentID, fallbackAgentID) {
|
|
925
|
+
if (!config.agent)
|
|
926
|
+
return;
|
|
927
|
+
for (const agentConfig of Object.values(config.agent)) {
|
|
928
|
+
if (!isRecord(agentConfig))
|
|
929
|
+
continue;
|
|
930
|
+
const permission = agentConfig.permission;
|
|
931
|
+
if (!isRecord(permission))
|
|
932
|
+
continue;
|
|
933
|
+
const taskPermission = permission.task;
|
|
934
|
+
if (!isRecord(taskPermission))
|
|
935
|
+
continue;
|
|
936
|
+
const hasExplicitAllow = taskPermission[baseAgentID] === "allow";
|
|
937
|
+
const hasGlobAllow = Object.entries(taskPermission).some(([pattern, action]) => action === "allow" &&
|
|
938
|
+
pattern.includes("*") &&
|
|
939
|
+
globPatternMatchesAgent(pattern, baseAgentID));
|
|
940
|
+
if (hasExplicitAllow || hasGlobAllow) {
|
|
941
|
+
taskPermission[fallbackAgentID] = "allow";
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function createConfigHandler(state) {
|
|
946
|
+
return async (config) => {
|
|
947
|
+
if (!config.agent)
|
|
948
|
+
return;
|
|
949
|
+
for (const [agentID, agentConfig] of Object.entries(config.agent)) {
|
|
950
|
+
if (!canCreateFallbackForAgent(agentID, agentConfig))
|
|
951
|
+
continue;
|
|
952
|
+
const baseModel = parseModelRef(agentConfig.model);
|
|
953
|
+
const fallbackModel = selectFallbackModel(baseModel);
|
|
954
|
+
if (!baseModel || isSameModel(baseModel, fallbackModel))
|
|
955
|
+
continue;
|
|
956
|
+
const fallbackAgentID = `${agentID}${FALLBACK_AGENT_SUFFIX}`;
|
|
957
|
+
if (config.agent[fallbackAgentID])
|
|
958
|
+
continue;
|
|
959
|
+
config.agent[fallbackAgentID] = {
|
|
960
|
+
...agentConfig,
|
|
961
|
+
hidden: true,
|
|
962
|
+
model: modelRefToString(fallbackModel),
|
|
963
|
+
description: `${String(agentConfig.description ?? agentID)} (provider fallback)`,
|
|
964
|
+
};
|
|
965
|
+
state.fallbackAgentByAgent.set(agentID, fallbackAgentID);
|
|
966
|
+
state.baseAgentByFallbackAgent.set(fallbackAgentID, agentID);
|
|
967
|
+
allowFallbackAgentInTaskPermissions(config, agentID, fallbackAgentID);
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
769
971
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
770
972
|
// UTILITY: TASKS.MD PARSER
|
|
771
973
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1262,9 +1464,10 @@ function createSystemTransform(state) {
|
|
|
1262
1464
|
`Do not advance to the next sequential pipeline slot until this failed slot is explicitly resolved.\n` +
|
|
1263
1465
|
`- Failed child session: ${childFallbackRequest.childSessionID}\n` +
|
|
1264
1466
|
`- Agent: ${childFallbackRequest.agent}\n` +
|
|
1467
|
+
`- Runtime fallback agent: ${childFallbackRequest.fallbackAgent}\n` +
|
|
1265
1468
|
`- Error type: ${childFallbackRequest.errorType}\n` +
|
|
1266
1469
|
`- Required fallback model: ${childFallbackRequest.fallbackModel.providerID}/${childFallbackRequest.fallbackModel.modelID}\n` +
|
|
1267
|
-
`Router action: retry the SAME
|
|
1470
|
+
`Router action: retry the SAME pipeline slot once by creating a NEW Task call to the same original agent. Do not resume the failed task_id. The plugin will rewrite that one Task call to the runtime fallback agent above so the model actually changes. If it still fails, record DECLINE/failed-provider-retry for that slot before continuing. ` +
|
|
1268
1471
|
`Never resume the failed child session directly; create a router-owned replacement attempt instead.`);
|
|
1269
1472
|
}
|
|
1270
1473
|
}
|
|
@@ -1438,7 +1641,7 @@ function createFallbackModels(state) {
|
|
|
1438
1641
|
const wasAlreadyManaged = isManagedUltraworkSession(state, input.sessionID);
|
|
1439
1642
|
await addManagedUltraworkSession(state, input.sessionID);
|
|
1440
1643
|
await writeUltraworkMarker(state);
|
|
1441
|
-
scheduleProviderRetryWatchdog(state, input.sessionID
|
|
1644
|
+
scheduleProviderRetryWatchdog(state, input.sessionID);
|
|
1442
1645
|
// First contact with 0-ultrawork after plugin restart:
|
|
1443
1646
|
// if marker is active and tasks.md has incomplete work, send AUTO-RESUME prompt.
|
|
1444
1647
|
if (!wasAlreadyManaged && !state.autoResumeAttempted) {
|
|
@@ -1472,11 +1675,17 @@ function createFallbackModels(state) {
|
|
|
1472
1675
|
// Subagent calls from 0-ultrawork (1-van, 7-backlog, etc.) use the same session.
|
|
1473
1676
|
// Log model and agent for observability
|
|
1474
1677
|
const modelId = input.model && "id" in input.model
|
|
1475
|
-
? input.model.id
|
|
1678
|
+
? input.model.id ?? "unknown"
|
|
1476
1679
|
: "unknown";
|
|
1477
1680
|
const providerId = input.provider?.info && "id" in input.provider.info
|
|
1478
|
-
? input.provider.info.id
|
|
1681
|
+
? input.provider.info.id ?? "unknown"
|
|
1479
1682
|
: "unknown";
|
|
1683
|
+
if (input.sessionID && modelId !== "unknown" && providerId !== "unknown") {
|
|
1684
|
+
await updateManagedSessionModel(state, input.sessionID, {
|
|
1685
|
+
providerID: providerId,
|
|
1686
|
+
modelID: modelId,
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1480
1689
|
pluginLog.info(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
|
|
1481
1690
|
`model="${modelId}", provider="${providerId}"`);
|
|
1482
1691
|
};
|
|
@@ -1564,11 +1773,12 @@ function createEventHandler(state) {
|
|
|
1564
1773
|
state.sessionErrorRetryCount.set(sessionID, count);
|
|
1565
1774
|
return;
|
|
1566
1775
|
}
|
|
1567
|
-
else if (isRoot && (isRateLimitApiError(error) || isCertificateApiError(error))) {
|
|
1568
|
-
|
|
1569
|
-
|
|
1776
|
+
else if (isRoot && (isModelAccessError(error) || isRateLimitApiError(error) || isCertificateApiError(error))) {
|
|
1777
|
+
const selectedFallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
1778
|
+
await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
|
|
1779
|
+
const errorType = getRetryableErrorType(error);
|
|
1570
1780
|
pluginLog.info(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
|
|
1571
|
-
`Retry will use fallback model ${
|
|
1781
|
+
`Retry will use fallback model ${selectedFallbackModel.providerID}/${selectedFallbackModel.modelID}.`);
|
|
1572
1782
|
}
|
|
1573
1783
|
const scheduled = scheduleManagedSessionRetry(state, sessionID, {
|
|
1574
1784
|
delayMs: delay,
|
|
@@ -1746,7 +1956,7 @@ function createTextCompleteHandler(state) {
|
|
|
1746
1956
|
// Some provider/SDK failures render as a retry banner instead of a
|
|
1747
1957
|
// session.error event, leaving the UI waiting for long internal backoff.
|
|
1748
1958
|
if (isProviderRetryBanner(text) && isManagedRootUltraworkSession(state, sessionID)) {
|
|
1749
|
-
const fallbackModel =
|
|
1959
|
+
const fallbackModel = getSessionFallbackModel(state, sessionID);
|
|
1750
1960
|
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
1751
1961
|
scheduleManagedSessionRetry(state, sessionID, {
|
|
1752
1962
|
delayMs: 1_000,
|
|
@@ -1857,9 +2067,10 @@ function createMultiCycleHandler(state) {
|
|
|
1857
2067
|
if (sessionID &&
|
|
1858
2068
|
RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
|
|
1859
2069
|
if (managedSession && !managedSession.fallbackModel) {
|
|
1860
|
-
|
|
2070
|
+
const fallbackModel = getSessionFallbackModel(state, sessionID);
|
|
2071
|
+
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
1861
2072
|
pluginLog.info(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
|
|
1862
|
-
`Fallback model pinned to ${
|
|
2073
|
+
`Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`);
|
|
1863
2074
|
}
|
|
1864
2075
|
if (managedSession) {
|
|
1865
2076
|
scheduleManagedSessionRetry(state, sessionID, {
|
|
@@ -1984,9 +2195,11 @@ async function server(input) {
|
|
|
1984
2195
|
createMultiCycleHandler(state),
|
|
1985
2196
|
];
|
|
1986
2197
|
return {
|
|
2198
|
+
config: withErrorBoundary(state, "config", createConfigHandler(state)),
|
|
1987
2199
|
event: withErrorBoundary(state, "event", createEventHandler(state)),
|
|
1988
2200
|
"chat.message": withErrorBoundary(state, "chat.message", compositeChatMessage(chatMessageHandlers)),
|
|
1989
2201
|
"chat.params": withErrorBoundary(state, "chat.params", createFallbackModels(state)),
|
|
2202
|
+
"tool.execute.before": withErrorBoundary(state, "tool.execute.before", createProviderFallbackToolBefore(state)),
|
|
1990
2203
|
"tool.execute.after": withErrorBoundary(state, "tool.execute.after", compositeToolAfter(toolAfterHandlers)),
|
|
1991
2204
|
"experimental.chat.system.transform": withErrorBoundary(state, "experimental.chat.system.transform", createSystemTransform(state)),
|
|
1992
2205
|
"experimental.session.compacting": withErrorBoundary(state, "experimental.session.compacting", createCompactionHandler(state)),
|