opencode-immune 1.0.54 → 1.0.55

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 +91 -18
  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.54";
14
+ const PLUGIN_VERSION = "1.0.55";
15
15
  const PLUGIN_PACKAGE_NAME = "opencode-immune";
16
16
  const PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
17
17
  function getServerAuthHeaders() {
@@ -135,12 +135,17 @@ const RETRY_PROMPT_DELIVERY_ATTEMPTS = 3;
135
135
  const CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1000;
136
136
  const RATE_LIMIT_FALLBACK_MODEL = {
137
137
  providerID: "codexsale",
138
- modelID: "gpt-5.5",
138
+ modelID: "gpt-5.4-mini",
139
139
  };
140
140
  const CHILD_SESSION_FALLBACK_MODEL = {
141
- providerID: "codexsale",
142
- modelID: "gpt-5.5",
141
+ providerID: "claudehub",
142
+ modelID: "claude-opus-4-7",
143
143
  };
144
+ const FALLBACK_MODEL_CANDIDATES = [
145
+ CHILD_SESSION_FALLBACK_MODEL,
146
+ RATE_LIMIT_FALLBACK_MODEL,
147
+ { providerID: "codexsale", modelID: "gpt-5.5" },
148
+ ];
144
149
  function isManagedUltraworkSession(state, sessionID) {
145
150
  return !!sessionID && state.managedUltraworkSessions.has(sessionID);
146
151
  }
@@ -317,6 +322,7 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
317
322
  rootSessionID: existing?.rootSessionID ?? sessionID,
318
323
  createdAt: existing?.createdAt ?? timestamp,
319
324
  updatedAt: timestamp,
325
+ currentModel: existing?.currentModel,
320
326
  fallbackModel: existing?.fallbackModel,
321
327
  };
322
328
  if (existing &&
@@ -344,6 +350,7 @@ async function addManagedChildSession(state, sessionID, parentSessionID, timesta
344
350
  rootSessionID: parent.rootSessionID,
345
351
  createdAt: existing?.createdAt ?? timestamp,
346
352
  updatedAt: timestamp,
353
+ currentModel: existing?.currentModel ?? parent.currentModel,
347
354
  fallbackModel: existing?.fallbackModel ?? parent.fallbackModel,
348
355
  });
349
356
  }
@@ -396,6 +403,60 @@ function markUltraworkSessionActive(state, sessionID) {
396
403
  });
397
404
  return true;
398
405
  }
406
+ function getModelProviderID(model) {
407
+ return model?.providerID || model?.modelID.split("/")[0];
408
+ }
409
+ function isSameModel(a, b) {
410
+ if (!a || !b)
411
+ return false;
412
+ return a.providerID === b.providerID && a.modelID === b.modelID;
413
+ }
414
+ function selectFallbackModel(currentModel) {
415
+ const currentProviderID = getModelProviderID(currentModel);
416
+ const otherProviderModel = FALLBACK_MODEL_CANDIDATES.find((candidate) => !isSameModel(candidate, currentModel) &&
417
+ getModelProviderID(candidate) !== currentProviderID);
418
+ if (otherProviderModel)
419
+ return otherProviderModel;
420
+ return (FALLBACK_MODEL_CANDIDATES.find((candidate) => !isSameModel(candidate, currentModel)) ?? RATE_LIMIT_FALLBACK_MODEL);
421
+ }
422
+ function getFailedModelFromError(error) {
423
+ if (!error || typeof error !== "object")
424
+ return undefined;
425
+ const maybeError = error;
426
+ const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`;
427
+ const providerModelMatch = message.match(/\b([a-z0-9_-]+)\/([a-z0-9._-]+)\b/i);
428
+ if (providerModelMatch?.[1] && providerModelMatch[2]) {
429
+ return {
430
+ providerID: providerModelMatch[1].toLowerCase(),
431
+ modelID: providerModelMatch[2].toLowerCase(),
432
+ };
433
+ }
434
+ const unsupportedModelMatch = message.match(/["']([^"']+)["']\s+model\s+is\s+not\s+supported/i);
435
+ if (unsupportedModelMatch?.[1] && /\bcodex\b/i.test(message)) {
436
+ return {
437
+ providerID: "codexsale",
438
+ modelID: unsupportedModelMatch[1].toLowerCase(),
439
+ };
440
+ }
441
+ return undefined;
442
+ }
443
+ async function updateManagedSessionModel(state, sessionID, model) {
444
+ const existing = state.managedUltraworkSessions.get(sessionID);
445
+ if (!existing || isSameModel(existing.currentModel, model))
446
+ return;
447
+ state.managedUltraworkSessions.set(sessionID, {
448
+ ...existing,
449
+ currentModel: model,
450
+ updatedAt: Date.now(),
451
+ });
452
+ }
453
+ function getSessionFallbackModel(state, sessionID, preferredModel) {
454
+ const currentModel = state.managedUltraworkSessions.get(sessionID)?.currentModel;
455
+ if (preferredModel && !isSameModel(preferredModel, currentModel)) {
456
+ return preferredModel;
457
+ }
458
+ return selectFallbackModel(currentModel);
459
+ }
399
460
  function isRetryableApiError(error) {
400
461
  if (!error || typeof error !== "object")
401
462
  return false;
@@ -431,6 +492,7 @@ function isRetryableApiError(error) {
431
492
  if (message.includes("api_error") ||
432
493
  message.includes("не разрешен") ||
433
494
  message.includes("not allowed") ||
495
+ message.includes("not supported") ||
434
496
  message.includes("internal error") ||
435
497
  message.includes("internal server error") ||
436
498
  message.includes("expected") ||
@@ -485,6 +547,7 @@ function isModelAccessError(error) {
485
547
  const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
486
548
  return (message.includes("не разрешен") ||
487
549
  message.includes("not allowed") ||
550
+ message.includes("not supported") ||
488
551
  message.includes("model not available") ||
489
552
  message.includes("model_not_found") ||
490
553
  message.includes("access denied") ||
@@ -534,12 +597,13 @@ function getRetryableErrorType(error) {
534
597
  return "retryable provider error";
535
598
  }
536
599
  function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
600
+ const fallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
537
601
  const request = {
538
602
  childSessionID,
539
603
  rootSessionID: managedSession.rootSessionID,
540
604
  agent: managedSession.agent || "unknown",
541
605
  errorType: getRetryableErrorType(error),
542
- fallbackModel: CHILD_SESSION_FALLBACK_MODEL,
606
+ fallbackModel,
543
607
  createdAt: Date.now(),
544
608
  };
545
609
  state.childFallbackRequests.set(managedSession.rootSessionID, request);
@@ -565,10 +629,11 @@ function scheduleProviderRetryWatchdog(state, sessionID, model) {
565
629
  return;
566
630
  if (state.sessionRetryTimers.has(sessionID))
567
631
  return;
568
- await setSessionFallbackModel(state, sessionID, model);
632
+ const fallbackModel = getSessionFallbackModel(state, sessionID, model);
633
+ await setSessionFallbackModel(state, sessionID, fallbackModel);
569
634
  await writeDiagnosticLog(state, "provider-retry-watchdog:fired", {
570
635
  sessionID,
571
- fallbackModel: model,
636
+ fallbackModel,
572
637
  });
573
638
  scheduleManagedSessionRetry(state, sessionID, {
574
639
  delayMs: 1_000,
@@ -1264,7 +1329,7 @@ function createSystemTransform(state) {
1264
1329
  `- Agent: ${childFallbackRequest.agent}\n` +
1265
1330
  `- Error type: ${childFallbackRequest.errorType}\n` +
1266
1331
  `- Required fallback model: ${childFallbackRequest.fallbackModel.providerID}/${childFallbackRequest.fallbackModel.modelID}\n` +
1267
- `Router action: retry the SAME agent/slot once using the fallback model if available; if it still fails or cannot be retried, record DECLINE/failed-provider-retry for that slot before continuing. ` +
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. ` +
1268
1333
  `Never resume the failed child session directly; create a router-owned replacement attempt instead.`);
1269
1334
  }
1270
1335
  }
@@ -1438,7 +1503,7 @@ function createFallbackModels(state) {
1438
1503
  const wasAlreadyManaged = isManagedUltraworkSession(state, input.sessionID);
1439
1504
  await addManagedUltraworkSession(state, input.sessionID);
1440
1505
  await writeUltraworkMarker(state);
1441
- scheduleProviderRetryWatchdog(state, input.sessionID, RATE_LIMIT_FALLBACK_MODEL);
1506
+ scheduleProviderRetryWatchdog(state, input.sessionID);
1442
1507
  // First contact with 0-ultrawork after plugin restart:
1443
1508
  // if marker is active and tasks.md has incomplete work, send AUTO-RESUME prompt.
1444
1509
  if (!wasAlreadyManaged && !state.autoResumeAttempted) {
@@ -1472,11 +1537,17 @@ function createFallbackModels(state) {
1472
1537
  // Subagent calls from 0-ultrawork (1-van, 7-backlog, etc.) use the same session.
1473
1538
  // Log model and agent for observability
1474
1539
  const modelId = input.model && "id" in input.model
1475
- ? input.model.id
1540
+ ? input.model.id ?? "unknown"
1476
1541
  : "unknown";
1477
1542
  const providerId = input.provider?.info && "id" in input.provider.info
1478
- ? input.provider.info.id
1543
+ ? input.provider.info.id ?? "unknown"
1479
1544
  : "unknown";
1545
+ if (input.sessionID && modelId !== "unknown" && providerId !== "unknown") {
1546
+ await updateManagedSessionModel(state, input.sessionID, {
1547
+ providerID: providerId,
1548
+ modelID: modelId,
1549
+ });
1550
+ }
1480
1551
  pluginLog.info(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
1481
1552
  `model="${modelId}", provider="${providerId}"`);
1482
1553
  };
@@ -1564,11 +1635,12 @@ function createEventHandler(state) {
1564
1635
  state.sessionErrorRetryCount.set(sessionID, count);
1565
1636
  return;
1566
1637
  }
1567
- else if (isRoot && (isRateLimitApiError(error) || isCertificateApiError(error))) {
1568
- await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1569
- const errorType = isCertificateApiError(error) ? "certificate error" : "rate limit";
1638
+ else if (isRoot && (isModelAccessError(error) || isRateLimitApiError(error) || isCertificateApiError(error))) {
1639
+ const selectedFallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
1640
+ await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
1641
+ const errorType = getRetryableErrorType(error);
1570
1642
  pluginLog.info(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
1571
- `Retry will use fallback model ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1643
+ `Retry will use fallback model ${selectedFallbackModel.providerID}/${selectedFallbackModel.modelID}.`);
1572
1644
  }
1573
1645
  const scheduled = scheduleManagedSessionRetry(state, sessionID, {
1574
1646
  delayMs: delay,
@@ -1746,7 +1818,7 @@ function createTextCompleteHandler(state) {
1746
1818
  // Some provider/SDK failures render as a retry banner instead of a
1747
1819
  // session.error event, leaving the UI waiting for long internal backoff.
1748
1820
  if (isProviderRetryBanner(text) && isManagedRootUltraworkSession(state, sessionID)) {
1749
- const fallbackModel = RATE_LIMIT_FALLBACK_MODEL;
1821
+ const fallbackModel = getSessionFallbackModel(state, sessionID);
1750
1822
  await setSessionFallbackModel(state, sessionID, fallbackModel);
1751
1823
  scheduleManagedSessionRetry(state, sessionID, {
1752
1824
  delayMs: 1_000,
@@ -1857,9 +1929,10 @@ function createMultiCycleHandler(state) {
1857
1929
  if (sessionID &&
1858
1930
  RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
1859
1931
  if (managedSession && !managedSession.fallbackModel) {
1860
- await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1932
+ const fallbackModel = getSessionFallbackModel(state, sessionID);
1933
+ await setSessionFallbackModel(state, sessionID, fallbackModel);
1861
1934
  pluginLog.info(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1862
- `Fallback model pinned to ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1935
+ `Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`);
1863
1936
  }
1864
1937
  if (managedSession) {
1865
1938
  scheduleManagedSessionRetry(state, sessionID, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.54",
3
+ "version": "1.0.55",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {