opencode-immune 1.0.55 → 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 +146 -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;
|
|
@@ -598,22 +609,25 @@ function getRetryableErrorType(error) {
|
|
|
598
609
|
}
|
|
599
610
|
function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
|
|
600
611
|
const fallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
612
|
+
const routerSessionID = getRouterSessionIDForChild(managedSession);
|
|
601
613
|
const request = {
|
|
602
614
|
childSessionID,
|
|
603
615
|
rootSessionID: managedSession.rootSessionID,
|
|
616
|
+
routerSessionID,
|
|
604
617
|
agent: managedSession.agent || "unknown",
|
|
618
|
+
fallbackAgent: getFallbackAgentForAgent(state, managedSession.agent || "unknown"),
|
|
605
619
|
errorType: getRetryableErrorType(error),
|
|
606
620
|
fallbackModel,
|
|
607
621
|
createdAt: Date.now(),
|
|
608
622
|
};
|
|
609
|
-
state.childFallbackRequests.set(
|
|
623
|
+
state.childFallbackRequests.set(routerSessionID, request);
|
|
610
624
|
}
|
|
611
|
-
function getChildFallbackRequest(state,
|
|
612
|
-
const request = state.childFallbackRequests.get(
|
|
625
|
+
function getChildFallbackRequest(state, sessionID, now = Date.now()) {
|
|
626
|
+
const request = state.childFallbackRequests.get(sessionID);
|
|
613
627
|
if (!request)
|
|
614
628
|
return undefined;
|
|
615
629
|
if (now - request.createdAt > CHILD_FALLBACK_REQUEST_TTL_MS) {
|
|
616
|
-
state.childFallbackRequests.delete(
|
|
630
|
+
state.childFallbackRequests.delete(sessionID);
|
|
617
631
|
return undefined;
|
|
618
632
|
}
|
|
619
633
|
return request;
|
|
@@ -831,6 +845,129 @@ function compositeChatMessage(handlers) {
|
|
|
831
845
|
}
|
|
832
846
|
};
|
|
833
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
|
+
}
|
|
834
971
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
835
972
|
// UTILITY: TASKS.MD PARSER
|
|
836
973
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1327,9 +1464,10 @@ function createSystemTransform(state) {
|
|
|
1327
1464
|
`Do not advance to the next sequential pipeline slot until this failed slot is explicitly resolved.\n` +
|
|
1328
1465
|
`- Failed child session: ${childFallbackRequest.childSessionID}\n` +
|
|
1329
1466
|
`- Agent: ${childFallbackRequest.agent}\n` +
|
|
1467
|
+
`- Runtime fallback agent: ${childFallbackRequest.fallbackAgent}\n` +
|
|
1330
1468
|
`- Error type: ${childFallbackRequest.errorType}\n` +
|
|
1331
1469
|
`- Required fallback model: ${childFallbackRequest.fallbackModel.providerID}/${childFallbackRequest.fallbackModel.modelID}\n` +
|
|
1332
|
-
`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. ` +
|
|
1333
1471
|
`Never resume the failed child session directly; create a router-owned replacement attempt instead.`);
|
|
1334
1472
|
}
|
|
1335
1473
|
}
|
|
@@ -2057,9 +2195,11 @@ async function server(input) {
|
|
|
2057
2195
|
createMultiCycleHandler(state),
|
|
2058
2196
|
];
|
|
2059
2197
|
return {
|
|
2198
|
+
config: withErrorBoundary(state, "config", createConfigHandler(state)),
|
|
2060
2199
|
event: withErrorBoundary(state, "event", createEventHandler(state)),
|
|
2061
2200
|
"chat.message": withErrorBoundary(state, "chat.message", compositeChatMessage(chatMessageHandlers)),
|
|
2062
2201
|
"chat.params": withErrorBoundary(state, "chat.params", createFallbackModels(state)),
|
|
2202
|
+
"tool.execute.before": withErrorBoundary(state, "tool.execute.before", createProviderFallbackToolBefore(state)),
|
|
2063
2203
|
"tool.execute.after": withErrorBoundary(state, "tool.execute.after", compositeToolAfter(toolAfterHandlers)),
|
|
2064
2204
|
"experimental.chat.system.transform": withErrorBoundary(state, "experimental.chat.system.transform", createSystemTransform(state)),
|
|
2065
2205
|
"experimental.session.compacting": withErrorBoundary(state, "experimental.session.compacting", createCompactionHandler(state)),
|