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.
- package/dist/plugin.js +91 -18
- 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.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.
|
|
138
|
+
modelID: "gpt-5.4-mini",
|
|
139
139
|
};
|
|
140
140
|
const CHILD_SESSION_FALLBACK_MODEL = {
|
|
141
|
-
providerID: "
|
|
142
|
-
modelID: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1569
|
-
|
|
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 ${
|
|
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 =
|
|
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
|
-
|
|
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 ${
|
|
1935
|
+
`Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`);
|
|
1863
1936
|
}
|
|
1864
1937
|
if (managedSession) {
|
|
1865
1938
|
scheduleManagedSessionRetry(state, sessionID, {
|