opencode-immune 1.0.55 → 1.0.57
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 +148 -6
- 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,
|
|
@@ -146,6 +148,7 @@ const FALLBACK_MODEL_CANDIDATES = [
|
|
|
146
148
|
RATE_LIMIT_FALLBACK_MODEL,
|
|
147
149
|
{ providerID: "codexsale", modelID: "gpt-5.5" },
|
|
148
150
|
];
|
|
151
|
+
const FALLBACK_AGENT_SUFFIX = "-provider-fallback";
|
|
149
152
|
function isManagedUltraworkSession(state, sessionID) {
|
|
150
153
|
return !!sessionID && state.managedUltraworkSessions.has(sessionID);
|
|
151
154
|
}
|
|
@@ -320,6 +323,7 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
|
|
|
320
323
|
kind: existing?.kind ?? "root",
|
|
321
324
|
agent: ULTRAWORK_AGENT,
|
|
322
325
|
rootSessionID: existing?.rootSessionID ?? sessionID,
|
|
326
|
+
parentSessionID: existing?.parentSessionID,
|
|
323
327
|
createdAt: existing?.createdAt ?? timestamp,
|
|
324
328
|
updatedAt: timestamp,
|
|
325
329
|
currentModel: existing?.currentModel,
|
|
@@ -348,6 +352,7 @@ async function addManagedChildSession(state, sessionID, parentSessionID, timesta
|
|
|
348
352
|
kind: "child",
|
|
349
353
|
agent: existing?.agent ?? "unknown",
|
|
350
354
|
rootSessionID: parent.rootSessionID,
|
|
355
|
+
parentSessionID,
|
|
351
356
|
createdAt: existing?.createdAt ?? timestamp,
|
|
352
357
|
updatedAt: timestamp,
|
|
353
358
|
currentModel: existing?.currentModel ?? parent.currentModel,
|
|
@@ -457,6 +462,12 @@ function getSessionFallbackModel(state, sessionID, preferredModel) {
|
|
|
457
462
|
}
|
|
458
463
|
return selectFallbackModel(currentModel);
|
|
459
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
|
+
}
|
|
460
471
|
function isRetryableApiError(error) {
|
|
461
472
|
if (!error || typeof error !== "object")
|
|
462
473
|
return false;
|
|
@@ -493,6 +504,7 @@ function isRetryableApiError(error) {
|
|
|
493
504
|
message.includes("не разрешен") ||
|
|
494
505
|
message.includes("not allowed") ||
|
|
495
506
|
message.includes("not supported") ||
|
|
507
|
+
message.includes("no active accounts available") ||
|
|
496
508
|
message.includes("internal error") ||
|
|
497
509
|
message.includes("internal server error") ||
|
|
498
510
|
message.includes("expected") ||
|
|
@@ -548,6 +560,7 @@ function isModelAccessError(error) {
|
|
|
548
560
|
return (message.includes("не разрешен") ||
|
|
549
561
|
message.includes("not allowed") ||
|
|
550
562
|
message.includes("not supported") ||
|
|
563
|
+
message.includes("no active accounts available") ||
|
|
551
564
|
message.includes("model not available") ||
|
|
552
565
|
message.includes("model_not_found") ||
|
|
553
566
|
message.includes("access denied") ||
|
|
@@ -598,22 +611,25 @@ function getRetryableErrorType(error) {
|
|
|
598
611
|
}
|
|
599
612
|
function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
|
|
600
613
|
const fallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
614
|
+
const routerSessionID = getRouterSessionIDForChild(managedSession);
|
|
601
615
|
const request = {
|
|
602
616
|
childSessionID,
|
|
603
617
|
rootSessionID: managedSession.rootSessionID,
|
|
618
|
+
routerSessionID,
|
|
604
619
|
agent: managedSession.agent || "unknown",
|
|
620
|
+
fallbackAgent: getFallbackAgentForAgent(state, managedSession.agent || "unknown"),
|
|
605
621
|
errorType: getRetryableErrorType(error),
|
|
606
622
|
fallbackModel,
|
|
607
623
|
createdAt: Date.now(),
|
|
608
624
|
};
|
|
609
|
-
state.childFallbackRequests.set(
|
|
625
|
+
state.childFallbackRequests.set(routerSessionID, request);
|
|
610
626
|
}
|
|
611
|
-
function getChildFallbackRequest(state,
|
|
612
|
-
const request = state.childFallbackRequests.get(
|
|
627
|
+
function getChildFallbackRequest(state, sessionID, now = Date.now()) {
|
|
628
|
+
const request = state.childFallbackRequests.get(sessionID);
|
|
613
629
|
if (!request)
|
|
614
630
|
return undefined;
|
|
615
631
|
if (now - request.createdAt > CHILD_FALLBACK_REQUEST_TTL_MS) {
|
|
616
|
-
state.childFallbackRequests.delete(
|
|
632
|
+
state.childFallbackRequests.delete(sessionID);
|
|
617
633
|
return undefined;
|
|
618
634
|
}
|
|
619
635
|
return request;
|
|
@@ -831,6 +847,129 @@ function compositeChatMessage(handlers) {
|
|
|
831
847
|
}
|
|
832
848
|
};
|
|
833
849
|
}
|
|
850
|
+
function isRecord(value) {
|
|
851
|
+
return !!value && typeof value === "object";
|
|
852
|
+
}
|
|
853
|
+
function getTaskAgentFromArgs(args) {
|
|
854
|
+
if (!isRecord(args))
|
|
855
|
+
return undefined;
|
|
856
|
+
const subagentType = args.subagent_type;
|
|
857
|
+
if (typeof subagentType === "string")
|
|
858
|
+
return subagentType;
|
|
859
|
+
const agent = args.agent;
|
|
860
|
+
if (typeof agent === "string")
|
|
861
|
+
return agent;
|
|
862
|
+
return undefined;
|
|
863
|
+
}
|
|
864
|
+
function setTaskAgentInArgs(args, agent) {
|
|
865
|
+
if (!isRecord(args))
|
|
866
|
+
return;
|
|
867
|
+
if ("subagent_type" in args) {
|
|
868
|
+
args.subagent_type = agent;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if ("agent" in args) {
|
|
872
|
+
args.agent = agent;
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
args.subagent_type = agent;
|
|
876
|
+
}
|
|
877
|
+
function createProviderFallbackToolBefore(state) {
|
|
878
|
+
return async (input, output) => {
|
|
879
|
+
if (input.tool !== "task" && input.tool !== "Task")
|
|
880
|
+
return;
|
|
881
|
+
const request = getChildFallbackRequest(state, input.sessionID);
|
|
882
|
+
if (!request)
|
|
883
|
+
return;
|
|
884
|
+
const requestedAgent = getTaskAgentFromArgs(output.args);
|
|
885
|
+
if (!requestedAgent || requestedAgent !== request.agent)
|
|
886
|
+
return;
|
|
887
|
+
if (request.fallbackAgent === request.agent)
|
|
888
|
+
return;
|
|
889
|
+
setTaskAgentInArgs(output.args, request.fallbackAgent);
|
|
890
|
+
state.childFallbackRequests.delete(input.sessionID);
|
|
891
|
+
await writeDiagnosticLog(state, "provider-fallback-task:rewrite", {
|
|
892
|
+
sessionID: input.sessionID,
|
|
893
|
+
childSessionID: request.childSessionID,
|
|
894
|
+
callID: input.callID,
|
|
895
|
+
fromAgent: request.agent,
|
|
896
|
+
toAgent: request.fallbackAgent,
|
|
897
|
+
fallbackModel: request.fallbackModel,
|
|
898
|
+
});
|
|
899
|
+
writePluginLog(state, "warn", `[opencode-immune] Rewrote provider retry Task agent ${request.agent} -> ${request.fallbackAgent} for session ${input.sessionID}.`);
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function modelRefToString(model) {
|
|
903
|
+
return `${model.providerID}/${model.modelID}`;
|
|
904
|
+
}
|
|
905
|
+
function parseModelRef(model) {
|
|
906
|
+
if (!model)
|
|
907
|
+
return undefined;
|
|
908
|
+
const [providerID, ...modelParts] = model.split("/");
|
|
909
|
+
const modelID = modelParts.join("/");
|
|
910
|
+
if (!providerID || !modelID)
|
|
911
|
+
return undefined;
|
|
912
|
+
return { providerID, modelID };
|
|
913
|
+
}
|
|
914
|
+
function canCreateFallbackForAgent(agentID, agentConfig) {
|
|
915
|
+
if (agentID.endsWith(FALLBACK_AGENT_SUFFIX))
|
|
916
|
+
return false;
|
|
917
|
+
if (!isRecord(agentConfig))
|
|
918
|
+
return false;
|
|
919
|
+
return typeof agentConfig.model === "string";
|
|
920
|
+
}
|
|
921
|
+
function globPatternMatchesAgent(pattern, agentID) {
|
|
922
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
923
|
+
const regex = new RegExp(`^${escaped.replace(/\\\*/g, ".*")}$`);
|
|
924
|
+
return regex.test(agentID);
|
|
925
|
+
}
|
|
926
|
+
function allowFallbackAgentInTaskPermissions(config, baseAgentID, fallbackAgentID) {
|
|
927
|
+
if (!config.agent)
|
|
928
|
+
return;
|
|
929
|
+
for (const agentConfig of Object.values(config.agent)) {
|
|
930
|
+
if (!isRecord(agentConfig))
|
|
931
|
+
continue;
|
|
932
|
+
const permission = agentConfig.permission;
|
|
933
|
+
if (!isRecord(permission))
|
|
934
|
+
continue;
|
|
935
|
+
const taskPermission = permission.task;
|
|
936
|
+
if (!isRecord(taskPermission))
|
|
937
|
+
continue;
|
|
938
|
+
const hasExplicitAllow = taskPermission[baseAgentID] === "allow";
|
|
939
|
+
const hasGlobAllow = Object.entries(taskPermission).some(([pattern, action]) => action === "allow" &&
|
|
940
|
+
pattern.includes("*") &&
|
|
941
|
+
globPatternMatchesAgent(pattern, baseAgentID));
|
|
942
|
+
if (hasExplicitAllow || hasGlobAllow) {
|
|
943
|
+
taskPermission[fallbackAgentID] = "allow";
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function createConfigHandler(state) {
|
|
948
|
+
return async (config) => {
|
|
949
|
+
if (!config.agent)
|
|
950
|
+
return;
|
|
951
|
+
for (const [agentID, agentConfig] of Object.entries(config.agent)) {
|
|
952
|
+
if (!canCreateFallbackForAgent(agentID, agentConfig))
|
|
953
|
+
continue;
|
|
954
|
+
const baseModel = parseModelRef(agentConfig.model);
|
|
955
|
+
const fallbackModel = selectFallbackModel(baseModel);
|
|
956
|
+
if (!baseModel || isSameModel(baseModel, fallbackModel))
|
|
957
|
+
continue;
|
|
958
|
+
const fallbackAgentID = `${agentID}${FALLBACK_AGENT_SUFFIX}`;
|
|
959
|
+
if (config.agent[fallbackAgentID])
|
|
960
|
+
continue;
|
|
961
|
+
config.agent[fallbackAgentID] = {
|
|
962
|
+
...agentConfig,
|
|
963
|
+
hidden: true,
|
|
964
|
+
model: modelRefToString(fallbackModel),
|
|
965
|
+
description: `${String(agentConfig.description ?? agentID)} (provider fallback)`,
|
|
966
|
+
};
|
|
967
|
+
state.fallbackAgentByAgent.set(agentID, fallbackAgentID);
|
|
968
|
+
state.baseAgentByFallbackAgent.set(fallbackAgentID, agentID);
|
|
969
|
+
allowFallbackAgentInTaskPermissions(config, agentID, fallbackAgentID);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
}
|
|
834
973
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
835
974
|
// UTILITY: TASKS.MD PARSER
|
|
836
975
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1327,9 +1466,10 @@ function createSystemTransform(state) {
|
|
|
1327
1466
|
`Do not advance to the next sequential pipeline slot until this failed slot is explicitly resolved.\n` +
|
|
1328
1467
|
`- Failed child session: ${childFallbackRequest.childSessionID}\n` +
|
|
1329
1468
|
`- Agent: ${childFallbackRequest.agent}\n` +
|
|
1469
|
+
`- Runtime fallback agent: ${childFallbackRequest.fallbackAgent}\n` +
|
|
1330
1470
|
`- Error type: ${childFallbackRequest.errorType}\n` +
|
|
1331
1471
|
`- Required fallback model: ${childFallbackRequest.fallbackModel.providerID}/${childFallbackRequest.fallbackModel.modelID}\n` +
|
|
1332
|
-
`Router action: retry the SAME
|
|
1472
|
+
`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. ` +
|
|
1333
1473
|
`Never resume the failed child session directly; create a router-owned replacement attempt instead.`);
|
|
1334
1474
|
}
|
|
1335
1475
|
}
|
|
@@ -2057,9 +2197,11 @@ async function server(input) {
|
|
|
2057
2197
|
createMultiCycleHandler(state),
|
|
2058
2198
|
];
|
|
2059
2199
|
return {
|
|
2200
|
+
config: withErrorBoundary(state, "config", createConfigHandler(state)),
|
|
2060
2201
|
event: withErrorBoundary(state, "event", createEventHandler(state)),
|
|
2061
2202
|
"chat.message": withErrorBoundary(state, "chat.message", compositeChatMessage(chatMessageHandlers)),
|
|
2062
2203
|
"chat.params": withErrorBoundary(state, "chat.params", createFallbackModels(state)),
|
|
2204
|
+
"tool.execute.before": withErrorBoundary(state, "tool.execute.before", createProviderFallbackToolBefore(state)),
|
|
2063
2205
|
"tool.execute.after": withErrorBoundary(state, "tool.execute.after", compositeToolAfter(toolAfterHandlers)),
|
|
2064
2206
|
"experimental.chat.system.transform": withErrorBoundary(state, "experimental.chat.system.transform", createSystemTransform(state)),
|
|
2065
2207
|
"experimental.session.compacting": withErrorBoundary(state, "experimental.session.compacting", createCompactionHandler(state)),
|