opencode-immune 1.0.72 → 1.0.75

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/server.js +161 -55
  2. package/package.json +1 -1
@@ -3777,7 +3777,7 @@ import { fileURLToPath } from "url";
3777
3777
  import { createHash } from "crypto";
3778
3778
  import { tmpdir } from "os";
3779
3779
  import { execFile } from "child_process";
3780
- var PLUGIN_VERSION = "1.0.72";
3780
+ var PLUGIN_VERSION = "1.0.75";
3781
3781
  var PLUGIN_PACKAGE_NAME = "opencode-immune";
3782
3782
  var PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
3783
3783
  function getServerAuthHeaders() {
@@ -3855,6 +3855,7 @@ function createState(input) {
3855
3855
  ultraworkPermissionSessions: /* @__PURE__ */ new Set(),
3856
3856
  fallbackAgentByAgent: /* @__PURE__ */ new Map(),
3857
3857
  baseAgentByFallbackAgent: /* @__PURE__ */ new Map(),
3858
+ fallbackModelCandidates: [],
3858
3859
  ultraworkMarkerPath: join(
3859
3860
  input.directory,
3860
3861
  ".opencode",
@@ -3911,24 +3912,15 @@ var MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
3911
3912
  var PROVIDER_RETRY_WATCHDOG_MS = 3e4;
3912
3913
  var RETRY_PROMPT_DELIVERY_ATTEMPTS = 3;
3913
3914
  var CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1e3;
3914
- var RATE_LIMIT_FALLBACK_MODEL = {
3915
- providerID: "codexsale",
3916
- modelID: "gpt-5.4-mini"
3917
- };
3918
- var CHILD_SESSION_FALLBACK_MODEL = {
3919
- providerID: "claudehub",
3920
- modelID: "claude-opus-4-7"
3921
- };
3922
- var HIGH_CAPABILITY_FALLBACK_MODELS = [
3923
- { providerID: "codexsale", modelID: "gpt-5.5" },
3924
- { providerID: "codexsale", modelID: "gpt-5.4" }
3925
- ];
3926
3915
  var AUTO_CYCLE_LOCK_TTL_MS = 30 * 60 * 1e3;
3927
- var FALLBACK_MODEL_CANDIDATES = [
3928
- CHILD_SESSION_FALLBACK_MODEL,
3929
- ...HIGH_CAPABILITY_FALLBACK_MODELS,
3930
- RATE_LIMIT_FALLBACK_MODEL
3931
- ];
3916
+ var MODEL_NAME_CAPABILITY_SCORE = {
3917
+ "claude-opus-4-7": 100,
3918
+ "gpt-5.5": 100,
3919
+ "gpt-5.4": 90,
3920
+ "claude-sonnet-4-6": 90,
3921
+ "gpt-5.4-mini": 50,
3922
+ "claude-haiku-4-5": 50
3923
+ };
3932
3924
  var FALLBACK_AGENT_SUFFIX = "-provider-fallback";
3933
3925
  function isManagedUltraworkSession(state, sessionID) {
3934
3926
  return !!sessionID && state.managedUltraworkSessions.has(sessionID);
@@ -3956,6 +3948,42 @@ async function createManagedUltraworkSession(state, title) {
3956
3948
  await addManagedUltraworkSession(state, sessionID);
3957
3949
  return sessionID;
3958
3950
  }
3951
+ async function startAutoCycleInNewSession(state, options) {
3952
+ const nextTask = options.nextTask?.trim() || "Continue processing task backlog";
3953
+ const title = `AUTO-CYCLE: ${nextTask}`;
3954
+ state.autoResumeInFlight = true;
3955
+ try {
3956
+ const newSessionID = await createManagedUltraworkSession(state, title);
3957
+ if (!newSessionID) {
3958
+ throw new Error("session.create returned no session ID");
3959
+ }
3960
+ await refreshAutoCycleLock(state, newSessionID);
3961
+ await promptManagedSession(
3962
+ state,
3963
+ newSessionID,
3964
+ `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`
3965
+ );
3966
+ state.autoResumeAttempted = true;
3967
+ if (options.retireSourceSession && isManagedUltraworkSession(state, options.sourceSessionID)) {
3968
+ await removeManagedUltraworkSession(
3969
+ state,
3970
+ options.sourceSessionID,
3971
+ "auto-cycle moved to a new session"
3972
+ );
3973
+ }
3974
+ pluginLog.info(
3975
+ `[opencode-immune] ${options.logContext}: Bootstrap prompt sent to ${newSessionID}`
3976
+ );
3977
+ return newSessionID;
3978
+ } catch (err) {
3979
+ if (options.clearLockOnFailureReason) {
3980
+ await clearAutoCycleLock(state, options.clearLockOnFailureReason);
3981
+ }
3982
+ throw err;
3983
+ } finally {
3984
+ state.autoResumeInFlight = false;
3985
+ }
3986
+ }
3959
3987
  async function applyUltraworkSessionPermissions(state, sessionID) {
3960
3988
  if (state.ultraworkPermissionSessions.has(sessionID)) return;
3961
3989
  try {
@@ -4212,15 +4240,57 @@ function isSameModel(a, b) {
4212
4240
  if (!a || !b) return false;
4213
4241
  return a.providerID === b.providerID && a.modelID === b.modelID;
4214
4242
  }
4215
- function selectFallbackModel(currentModel) {
4243
+ function getModelCapabilityScore(model) {
4244
+ if (!model) return 90;
4245
+ const modelID = model.modelID.toLowerCase();
4246
+ const explicitScore = MODEL_NAME_CAPABILITY_SCORE[modelID];
4247
+ if (explicitScore !== void 0) return explicitScore;
4248
+ if (/\b(mini|nano|small|lite|haiku|flash)\b/i.test(modelID)) return 50;
4249
+ if (/\b(opus|gpt-5\.5|gpt-5\.4|o3|o4|gpt-4\.1|gpt-4o)\b/i.test(modelID)) return 90;
4250
+ return 70;
4251
+ }
4252
+ function getMinimumFallbackCapabilityScore(currentModel) {
4253
+ const currentScore = getModelCapabilityScore(currentModel);
4254
+ if (currentScore >= 90) return 90;
4255
+ if (currentScore >= 70) return 70;
4256
+ return 0;
4257
+ }
4258
+ function sortFallbackModelsByCapability(candidates, currentModel) {
4259
+ const currentScore = getModelCapabilityScore(currentModel);
4260
+ const minimumScore = getMinimumFallbackCapabilityScore(currentModel);
4261
+ return [...candidates].sort((a, b) => {
4262
+ const aScore = getModelCapabilityScore(a);
4263
+ const bScore = getModelCapabilityScore(b);
4264
+ const aAllowed = aScore >= minimumScore ? 0 : 1;
4265
+ const bAllowed = bScore >= minimumScore ? 0 : 1;
4266
+ if (aAllowed !== bAllowed) return aAllowed - bAllowed;
4267
+ const aDirectionPenalty = aScore >= currentScore ? 0 : 1;
4268
+ const bDirectionPenalty = bScore >= currentScore ? 0 : 1;
4269
+ if (aDirectionPenalty !== bDirectionPenalty) {
4270
+ return aDirectionPenalty - bDirectionPenalty;
4271
+ }
4272
+ const aSameModelName = currentModel && a.modelID === currentModel.modelID ? 0 : 1;
4273
+ const bSameModelName = currentModel && b.modelID === currentModel.modelID ? 0 : 1;
4274
+ if (aSameModelName !== bSameModelName) return aSameModelName - bSameModelName;
4275
+ const aDistance = Math.abs(aScore - currentScore);
4276
+ const bDistance = Math.abs(bScore - currentScore);
4277
+ if (aDistance !== bDistance) return aDistance - bDistance;
4278
+ return bScore - aScore;
4279
+ });
4280
+ }
4281
+ function selectFallbackModel(state, currentModel) {
4216
4282
  const currentProviderID = getModelProviderID(currentModel);
4217
- const otherProviderModel = FALLBACK_MODEL_CANDIDATES.find(
4218
- (candidate) => !isSameModel(candidate, currentModel) && getModelProviderID(candidate) !== currentProviderID
4283
+ const fallbackCandidates = state.fallbackModelCandidates.filter(
4284
+ (candidate) => !isSameModel(candidate, currentModel)
4219
4285
  );
4286
+ const otherProviderModel = sortFallbackModelsByCapability(
4287
+ fallbackCandidates.filter(
4288
+ (candidate) => getModelProviderID(candidate) !== currentProviderID
4289
+ ),
4290
+ currentModel
4291
+ )[0];
4220
4292
  if (otherProviderModel) return otherProviderModel;
4221
- return FALLBACK_MODEL_CANDIDATES.find(
4222
- (candidate) => !isSameModel(candidate, currentModel)
4223
- ) ?? RATE_LIMIT_FALLBACK_MODEL;
4293
+ return sortFallbackModelsByCapability(fallbackCandidates, currentModel)[0];
4224
4294
  }
4225
4295
  function getFailedModelFromError(error) {
4226
4296
  if (!error || typeof error !== "object") return void 0;
@@ -4233,13 +4303,6 @@ function getFailedModelFromError(error) {
4233
4303
  modelID: providerModelMatch[2].toLowerCase()
4234
4304
  };
4235
4305
  }
4236
- const unsupportedModelMatch = message.match(/["']([^"']+)["']\s+model\s+is\s+not\s+supported/i);
4237
- if (unsupportedModelMatch?.[1] && /\bcodex\b/i.test(message)) {
4238
- return {
4239
- providerID: "codexsale",
4240
- modelID: unsupportedModelMatch[1].toLowerCase()
4241
- };
4242
- }
4243
4306
  return void 0;
4244
4307
  }
4245
4308
  async function updateManagedSessionModel(state, sessionID, model) {
@@ -4256,7 +4319,7 @@ function getSessionFallbackModel(state, sessionID, preferredModel) {
4256
4319
  if (preferredModel && !isSameModel(preferredModel, currentModel)) {
4257
4320
  return preferredModel;
4258
4321
  }
4259
- return selectFallbackModel(currentModel);
4322
+ return selectFallbackModel(state, currentModel);
4260
4323
  }
4261
4324
  function getFallbackAgentForAgent(state, agent) {
4262
4325
  return state.fallbackAgentByAgent.get(agent) ?? agent;
@@ -4393,8 +4456,10 @@ async function recoverUntrackedRootSessionForActiveTask(state, sessionID, reason
4393
4456
  }
4394
4457
  function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
4395
4458
  const fallbackModel = selectFallbackModel(
4459
+ state,
4396
4460
  getFailedModelFromError(error) ?? managedSession.currentModel
4397
4461
  );
4462
+ if (!fallbackModel) return;
4398
4463
  const routerSessionID = getRouterSessionIDForChild(managedSession);
4399
4464
  const request = {
4400
4465
  childSessionID,
@@ -4425,6 +4490,7 @@ function scheduleProviderRetryWatchdog(state, sessionID, model) {
4425
4490
  if (!isManagedRootUltraworkSession(state, sessionID)) return;
4426
4491
  if (state.sessionRetryTimers.has(sessionID)) return;
4427
4492
  const fallbackModel = getSessionFallbackModel(state, sessionID, model);
4493
+ if (!fallbackModel) return;
4428
4494
  await setSessionFallbackModel(state, sessionID, fallbackModel);
4429
4495
  await writeDiagnosticLog(state, "provider-retry-watchdog:fired", {
4430
4496
  sessionID,
@@ -4656,6 +4722,36 @@ function parseModelRef(model) {
4656
4722
  if (!providerID || !modelID) return void 0;
4657
4723
  return { providerID, modelID };
4658
4724
  }
4725
+ function addFallbackModelCandidate(candidates, model) {
4726
+ const parsed = parseModelRef(model);
4727
+ if (!parsed) return;
4728
+ candidates.set(modelRefToString(parsed).toLowerCase(), parsed);
4729
+ }
4730
+ function collectFallbackModelCandidates(config) {
4731
+ const candidates = /* @__PURE__ */ new Map();
4732
+ addFallbackModelCandidate(candidates, config.model);
4733
+ addFallbackModelCandidate(candidates, config.small_model);
4734
+ for (const agentConfig of Object.values(config.agent ?? {})) {
4735
+ if (!isRecord(agentConfig)) continue;
4736
+ const model = agentConfig.model;
4737
+ if (typeof model === "string") addFallbackModelCandidate(candidates, model);
4738
+ }
4739
+ const enabledProviders = new Set(config.enabled_providers ?? []);
4740
+ const disabledProviders = new Set(config.disabled_providers ?? []);
4741
+ const isProviderEnabled = (providerID) => {
4742
+ if (enabledProviders.size > 0 && !enabledProviders.has(providerID)) return false;
4743
+ return !disabledProviders.has(providerID);
4744
+ };
4745
+ for (const [providerID, providerConfig] of Object.entries(config.provider ?? {})) {
4746
+ if (!isProviderEnabled(providerID) || !isRecord(providerConfig)) continue;
4747
+ const models = providerConfig.models;
4748
+ if (!isRecord(models)) continue;
4749
+ for (const modelID of Object.keys(models)) {
4750
+ addFallbackModelCandidate(candidates, `${providerID}/${modelID}`);
4751
+ }
4752
+ }
4753
+ return sortFallbackModelsByCapability([...candidates.values()]);
4754
+ }
4659
4755
  function canCreateFallbackForAgent(agentID, agentConfig) {
4660
4756
  if (agentID.endsWith(FALLBACK_AGENT_SUFFIX)) return false;
4661
4757
  if (!isRecord(agentConfig)) return false;
@@ -4686,11 +4782,12 @@ function allowFallbackAgentInTaskPermissions(config, baseAgentID, fallbackAgentI
4686
4782
  function createConfigHandler(state) {
4687
4783
  return async (config) => {
4688
4784
  if (!config.agent) return;
4785
+ state.fallbackModelCandidates = collectFallbackModelCandidates(config);
4689
4786
  for (const [agentID, agentConfig] of Object.entries(config.agent)) {
4690
4787
  if (!canCreateFallbackForAgent(agentID, agentConfig)) continue;
4691
4788
  const baseModel = parseModelRef(agentConfig.model);
4692
- const fallbackModel = selectFallbackModel(baseModel);
4693
- if (!baseModel || isSameModel(baseModel, fallbackModel)) continue;
4789
+ const fallbackModel = selectFallbackModel(state, baseModel);
4790
+ if (!baseModel || !fallbackModel || isSameModel(baseModel, fallbackModel)) continue;
4694
4791
  const fallbackAgentID = `${agentID}${FALLBACK_AGENT_SUFFIX}`;
4695
4792
  if (config.agent[fallbackAgentID]) continue;
4696
4793
  config.agent[fallbackAgentID] = {
@@ -5204,28 +5301,25 @@ function createSessionRecoveryEvent(state) {
5204
5301
  sessionID
5205
5302
  );
5206
5303
  if (!lockAcquired) return;
5207
- await addManagedUltraworkSession(state, sessionID);
5208
5304
  await refreshAutoCycleLock(state, sessionID);
5209
- state.autoResumeInFlight = true;
5210
5305
  setTimeout(async () => {
5211
5306
  try {
5212
- await promptManagedSession(
5307
+ const newSessionID = await startAutoCycleInNewSession(
5213
5308
  state,
5214
- sessionID,
5215
- `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`
5309
+ {
5310
+ sourceSessionID: sessionID,
5311
+ logContext: "Auto-cycle",
5312
+ clearLockOnFailureReason: "root session auto-cycle failed"
5313
+ }
5216
5314
  );
5217
- state.autoResumeAttempted = true;
5218
5315
  pluginLog.info(
5219
- `[opencode-immune] Auto-cycle prompt sent to root ultrawork session ${sessionID}`
5316
+ `[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`
5220
5317
  );
5221
5318
  } catch (err) {
5222
- await clearAutoCycleLock(state, "root session auto-cycle failed");
5223
5319
  pluginLog.error(
5224
5320
  `[opencode-immune] Auto-cycle prompt failed for root session ${sessionID}:`,
5225
5321
  err
5226
5322
  );
5227
- } finally {
5228
- state.autoResumeInFlight = false;
5229
5323
  }
5230
5324
  }, 3e3);
5231
5325
  }
@@ -5503,9 +5597,12 @@ function createEventHandler(state) {
5503
5597
  state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
5504
5598
  if (shouldUseFallbackForManagedError(error, managedSession, count)) {
5505
5599
  const fallbackModel = selectFallbackModel(
5600
+ state,
5506
5601
  getFailedModelFromError(error) ?? managedSession.currentModel
5507
5602
  );
5508
- await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
5603
+ if (fallbackModel) {
5604
+ await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
5605
+ }
5509
5606
  }
5510
5607
  pluginLog.warn(
5511
5608
  `[opencode-immune] session.error without sessionID matched retryable error. Retrying sole managed root session ${fallbackSessionID}.`
@@ -5561,8 +5658,10 @@ function createEventHandler(state) {
5561
5658
  return;
5562
5659
  } else if (isRoot && shouldUseFallbackForManagedError(error, managedSession, count)) {
5563
5660
  const selectedFallbackModel = selectFallbackModel(
5661
+ state,
5564
5662
  getFailedModelFromError(error) ?? managedSession.currentModel
5565
5663
  );
5664
+ if (!selectedFallbackModel) return;
5566
5665
  await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
5567
5666
  const errorType = getRetryableErrorType(error);
5568
5667
  pluginLog.info(
@@ -5709,6 +5808,7 @@ function createTextCompleteHandler(state) {
5709
5808
  cancelProviderRetryWatchdog(state, sessionID, "assistant text completed");
5710
5809
  if (isProviderRetryBanner(text) && isManagedRootSession) {
5711
5810
  const fallbackModel = getSessionFallbackModel(state, sessionID);
5811
+ if (!fallbackModel) return;
5712
5812
  await setSessionFallbackModel(state, sessionID, fallbackModel);
5713
5813
  scheduleManagedSessionRetry(state, sessionID, {
5714
5814
  delayMs: 1e3,
@@ -5769,15 +5869,18 @@ function createTextCompleteHandler(state) {
5769
5869
  const taskMatch = text.match(NEXT_TASK_PATTERN);
5770
5870
  const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
5771
5871
  pluginLog.info(
5772
- `[opencode-immune] Multi-Cycle: Starting next cycle in current session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`
5872
+ `[opencode-immune] Multi-Cycle: Starting next cycle in a new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`
5773
5873
  );
5774
5874
  try {
5775
- await promptManagedSession(
5875
+ await startAutoCycleInNewSession(
5776
5876
  state,
5777
- sessionID,
5778
- `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`
5877
+ {
5878
+ sourceSessionID: sessionID,
5879
+ nextTask,
5880
+ logContext: "Multi-Cycle",
5881
+ retireSourceSession: true
5882
+ }
5779
5883
  );
5780
- pluginLog.info(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${sessionID}`);
5781
5884
  } catch (err) {
5782
5885
  pluginLog.error("[opencode-immune] Multi-Cycle: Failed to send prompt:", err);
5783
5886
  }
@@ -5802,15 +5905,17 @@ function createTextCompleteHandler(state) {
5802
5905
  );
5803
5906
  try {
5804
5907
  await refreshAutoCycleLock(state, sessionID);
5805
- await promptManagedSession(
5908
+ await startAutoCycleInNewSession(
5806
5909
  state,
5807
- sessionID,
5808
- `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`
5910
+ {
5911
+ sourceSessionID: sessionID,
5912
+ logContext: "Multi-Cycle fallback",
5913
+ clearLockOnFailureReason: "fallback bootstrap failed",
5914
+ retireSourceSession: true
5915
+ }
5809
5916
  );
5810
- pluginLog.info(`[opencode-immune] Multi-Cycle fallback: Bootstrap prompt sent to ${sessionID}`);
5811
5917
  } catch (err) {
5812
5918
  state.autoCycleSourceSessions.delete(sessionID);
5813
- await clearAutoCycleLock(state, "fallback bootstrap failed");
5814
5919
  pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to send prompt:", err);
5815
5920
  } finally {
5816
5921
  state.autoCycleInFlight = false;
@@ -5835,6 +5940,7 @@ function createMultiCycleHandler(state) {
5835
5940
  if (sessionID && RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
5836
5941
  if (managedSession && !managedSession.fallbackModel) {
5837
5942
  const fallbackModel = getSessionFallbackModel(state, sessionID);
5943
+ if (!fallbackModel) return;
5838
5944
  await setSessionFallbackModel(state, sessionID, fallbackModel);
5839
5945
  pluginLog.info(
5840
5946
  `[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.72",
3
+ "version": "1.0.75",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {