opencode-immune 1.0.56 → 1.0.58
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 +107 -134
- 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.58";
|
|
15
15
|
const PLUGIN_PACKAGE_NAME = "opencode-immune";
|
|
16
16
|
const PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
function getServerAuthHeaders() {
|
|
@@ -468,144 +468,114 @@ function getFallbackAgentForAgent(state, agent) {
|
|
|
468
468
|
function getRouterSessionIDForChild(record) {
|
|
469
469
|
return record.parentSessionID ?? record.rootSessionID;
|
|
470
470
|
}
|
|
471
|
-
function
|
|
472
|
-
if (
|
|
473
|
-
return
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
471
|
+
function stringifyErrorField(value) {
|
|
472
|
+
if (value === undefined || value === null)
|
|
473
|
+
return undefined;
|
|
474
|
+
if (typeof value === "string")
|
|
475
|
+
return value;
|
|
476
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
477
|
+
return String(value);
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
function numberFromErrorField(value) {
|
|
481
|
+
if (typeof value === "number")
|
|
482
|
+
return value;
|
|
483
|
+
if (typeof value !== "string")
|
|
484
|
+
return undefined;
|
|
485
|
+
const parsed = Number(value);
|
|
486
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
487
|
+
}
|
|
488
|
+
function firstDefinedNumber(...values) {
|
|
489
|
+
for (const value of values) {
|
|
490
|
+
const parsed = numberFromErrorField(value);
|
|
491
|
+
if (parsed !== undefined)
|
|
492
|
+
return parsed;
|
|
490
493
|
}
|
|
491
|
-
|
|
492
|
-
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
function extractManagedErrorDetails(error) {
|
|
497
|
+
if (!isRecord(error))
|
|
498
|
+
return undefined;
|
|
499
|
+
const data = isRecord(error.data) ? error.data : undefined;
|
|
500
|
+
const nestedError = isRecord(error.error) ? error.error : undefined;
|
|
501
|
+
const name = stringifyErrorField(error.name) ?? "";
|
|
502
|
+
const status = firstDefinedNumber(error.status, error.statusCode, data?.status, nestedError?.status);
|
|
503
|
+
const code = [error.code, data?.code, nestedError?.code]
|
|
504
|
+
.map(stringifyErrorField)
|
|
505
|
+
.find(Boolean);
|
|
506
|
+
const type = [error.type, data?.type, nestedError?.type]
|
|
507
|
+
.map(stringifyErrorField)
|
|
508
|
+
.find(Boolean);
|
|
509
|
+
const isRetryable = error.isRetryable === true || data?.isRetryable === true || nestedError?.isRetryable === true
|
|
510
|
+
? true
|
|
511
|
+
: undefined;
|
|
493
512
|
const message = [
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
message.includes("schema") ||
|
|
514
|
-
message.includes("zod") ||
|
|
515
|
-
message.includes("parse error") ||
|
|
516
|
-
message.includes("invalid id") ||
|
|
517
|
-
message.includes("expected id") ||
|
|
518
|
-
message.includes("model not available") ||
|
|
519
|
-
message.includes("model_not_found") ||
|
|
520
|
-
message.includes("access denied") ||
|
|
521
|
-
message.includes("404") ||
|
|
522
|
-
message.includes("not found") ||
|
|
523
|
-
message.includes("page not found") ||
|
|
524
|
-
message.includes("502") ||
|
|
525
|
-
message.includes("bad gateway") ||
|
|
526
|
-
message.includes("503") ||
|
|
527
|
-
message.includes("service unavailable") ||
|
|
528
|
-
message.includes("504") ||
|
|
529
|
-
message.includes("gateway timeout") ||
|
|
530
|
-
message.includes("econnrefused") ||
|
|
531
|
-
message.includes("econnreset") ||
|
|
532
|
-
message.includes("etimedout") ||
|
|
533
|
-
message.includes("fetch failed") ||
|
|
534
|
-
message.includes("timed out") ||
|
|
535
|
-
message.includes("timeout") ||
|
|
536
|
-
message.includes("sse read") ||
|
|
537
|
-
message.includes("stream error") ||
|
|
538
|
-
// Certificate/TLS provider failures must pass this primary retry gate
|
|
539
|
-
// before the managed-session fallback model branch can run.
|
|
540
|
-
message.includes("unknown certificate verification error") ||
|
|
541
|
-
message.includes("unknown_certificate_verification_error") ||
|
|
542
|
-
message.includes("certificate has expired") ||
|
|
543
|
-
message.includes("certificate verification") ||
|
|
544
|
-
message.includes("tls") ||
|
|
545
|
-
message.includes("ssl") ||
|
|
546
|
-
message.includes("connection reset") ||
|
|
547
|
-
message.includes("socket hang up") ||
|
|
548
|
-
message.includes("aborted")) {
|
|
549
|
-
return true;
|
|
550
|
-
}
|
|
551
|
-
return false;
|
|
513
|
+
error.message,
|
|
514
|
+
data?.message,
|
|
515
|
+
nestedError?.message,
|
|
516
|
+
code,
|
|
517
|
+
type,
|
|
518
|
+
].map(stringifyErrorField).filter(Boolean).join(" ").toLowerCase();
|
|
519
|
+
return {
|
|
520
|
+
name,
|
|
521
|
+
message,
|
|
522
|
+
status,
|
|
523
|
+
code,
|
|
524
|
+
type,
|
|
525
|
+
isRetryable,
|
|
526
|
+
hasApiShape: name === "APIError" ||
|
|
527
|
+
status !== undefined ||
|
|
528
|
+
isRetryable !== undefined ||
|
|
529
|
+
data !== undefined ||
|
|
530
|
+
nestedError !== undefined,
|
|
531
|
+
};
|
|
552
532
|
}
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
533
|
+
function getManagedSessionErrorAction(error) {
|
|
534
|
+
const details = extractManagedErrorDetails(error);
|
|
535
|
+
if (!details)
|
|
536
|
+
return "none";
|
|
537
|
+
if (details.name === "AbortError" || details.code === "ABORT_ERR") {
|
|
538
|
+
return "none";
|
|
539
|
+
}
|
|
540
|
+
if (details.status !== undefined) {
|
|
541
|
+
if (details.status === 408 || details.status === 409 || details.status === 425 || details.status >= 500) {
|
|
542
|
+
return "retry";
|
|
543
|
+
}
|
|
544
|
+
if (details.status >= 400 && details.status < 500) {
|
|
545
|
+
return "fallback";
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (details.isRetryable === true)
|
|
549
|
+
return "retry";
|
|
550
|
+
if (details.code && /^E[A-Z0-9_]+$/.test(details.code))
|
|
551
|
+
return "retry";
|
|
552
|
+
// Managed session errors originate from the active model/provider request.
|
|
553
|
+
// If the server emitted an API-shaped error but did not expose a stable status
|
|
554
|
+
// or retryability flag, prefer one fallback-model attempt over hard-stopping.
|
|
555
|
+
if (details.hasApiShape || details.message)
|
|
556
|
+
return "fallback";
|
|
557
|
+
return "none";
|
|
558
|
+
}
|
|
559
|
+
function shouldUseFallbackForManagedError(error, managedSession, retryCount) {
|
|
560
|
+
if (managedSession.kind === "child")
|
|
561
|
+
return true;
|
|
562
|
+
return getManagedSessionErrorAction(error) === "fallback" || retryCount > 0;
|
|
575
563
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
return false;
|
|
579
|
-
const maybeError = error;
|
|
580
|
-
const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`.toLowerCase();
|
|
581
|
-
const code = `${maybeError.code ?? ""} ${maybeError.data?.code ?? ""} ${maybeError.error?.code ?? ""}`.toLowerCase();
|
|
582
|
-
const type = `${maybeError.data?.type ?? ""} ${maybeError.error?.type ?? ""}`.toLowerCase();
|
|
583
|
-
return (message.includes("unknown certificate verification error") ||
|
|
584
|
-
message.includes("unknown_certificate_verification_error") ||
|
|
585
|
-
message.includes("certificate has expired") ||
|
|
586
|
-
message.includes("certificate verification") ||
|
|
587
|
-
message.includes("tls") ||
|
|
588
|
-
message.includes("ssl") ||
|
|
589
|
-
code.includes("cert_has_expired") ||
|
|
590
|
-
code.includes("unknown_certificate_verification_error") ||
|
|
591
|
-
code.includes("unable_to_verify_leaf_signature") ||
|
|
592
|
-
code.includes("self_signed_cert") ||
|
|
593
|
-
code.includes("tls") ||
|
|
594
|
-
type.includes("certificate") ||
|
|
595
|
-
type.includes("tls") ||
|
|
596
|
-
type.includes("ssl"));
|
|
564
|
+
function isRetryableApiError(error) {
|
|
565
|
+
return getManagedSessionErrorAction(error) !== "none";
|
|
597
566
|
}
|
|
598
567
|
function isProviderRetryBanner(text) {
|
|
599
568
|
return /(?:<none>\s*)?retrying in \d+s\s*-\s*attempt #\d+/i.test(text);
|
|
600
569
|
}
|
|
601
570
|
function getRetryableErrorType(error) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
571
|
+
const action = getManagedSessionErrorAction(error);
|
|
572
|
+
const status = extractManagedErrorDetails(error)?.status;
|
|
573
|
+
const statusSuffix = status !== undefined ? ` (${status})` : "";
|
|
574
|
+
if (action === "fallback")
|
|
575
|
+
return `provider fallback error${statusSuffix}`;
|
|
576
|
+
if (action === "retry")
|
|
577
|
+
return `transient provider error${statusSuffix}`;
|
|
578
|
+
return "non-retryable provider error";
|
|
609
579
|
}
|
|
610
580
|
function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
|
|
611
581
|
const fallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
@@ -1716,14 +1686,17 @@ function createEventHandler(state) {
|
|
|
1716
1686
|
// active managed root session, retry it as a best-effort recovery path.
|
|
1717
1687
|
if (eventType === "session.error" && !sessionID && isRetryableApiError(error)) {
|
|
1718
1688
|
const rootCandidates = Array.from(state.managedUltraworkSessions.entries())
|
|
1719
|
-
.filter(([, record]) => record.kind === "root")
|
|
1720
|
-
.map(([id]) => id);
|
|
1689
|
+
.filter(([, record]) => record.kind === "root");
|
|
1721
1690
|
if (rootCandidates.length === 1) {
|
|
1722
|
-
const fallbackSessionID = rootCandidates[0];
|
|
1691
|
+
const [fallbackSessionID, managedSession] = rootCandidates[0];
|
|
1723
1692
|
const count = state.sessionErrorRetryCount.get(fallbackSessionID) ?? 0;
|
|
1724
1693
|
if (count < MAX_RETRIES && !state.sessionRetryTimers.has(fallbackSessionID)) {
|
|
1725
1694
|
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
|
|
1726
1695
|
state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
|
|
1696
|
+
if (shouldUseFallbackForManagedError(error, managedSession, count)) {
|
|
1697
|
+
const fallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
1698
|
+
await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
|
|
1699
|
+
}
|
|
1727
1700
|
pluginLog.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
|
|
1728
1701
|
`Retrying sole managed root session ${fallbackSessionID}.`);
|
|
1729
1702
|
scheduleManagedSessionRetry(state, fallbackSessionID, {
|
|
@@ -1773,7 +1746,7 @@ function createEventHandler(state) {
|
|
|
1773
1746
|
state.sessionErrorRetryCount.set(sessionID, count);
|
|
1774
1747
|
return;
|
|
1775
1748
|
}
|
|
1776
|
-
else if (isRoot && (
|
|
1749
|
+
else if (isRoot && shouldUseFallbackForManagedError(error, managedSession, count)) {
|
|
1777
1750
|
const selectedFallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
1778
1751
|
await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
|
|
1779
1752
|
const errorType = getRetryableErrorType(error);
|