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.
Files changed (2) hide show
  1. package/dist/plugin.js +146 -6
  2. 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.55";
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(managedSession.rootSessionID, request);
623
+ state.childFallbackRequests.set(routerSessionID, request);
610
624
  }
611
- function getChildFallbackRequest(state, rootSessionID, now = Date.now()) {
612
- const request = state.childFallbackRequests.get(rootSessionID);
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(rootSessionID);
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 agent/slot once using the required fallback model from a different provider; never reuse the failed model for this retry. If it still fails or cannot be retried, record DECLINE/failed-provider-retry for that slot before continuing. ` +
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)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.55",
3
+ "version": "1.0.56",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {