opencode-immune 1.0.57 → 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 -136
- 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,146 +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("validation") ||
|
|
514
|
-
message.includes("schema") ||
|
|
515
|
-
message.includes("zod") ||
|
|
516
|
-
message.includes("parse error") ||
|
|
517
|
-
message.includes("invalid id") ||
|
|
518
|
-
message.includes("expected id") ||
|
|
519
|
-
message.includes("model not available") ||
|
|
520
|
-
message.includes("model_not_found") ||
|
|
521
|
-
message.includes("access denied") ||
|
|
522
|
-
message.includes("404") ||
|
|
523
|
-
message.includes("not found") ||
|
|
524
|
-
message.includes("page not found") ||
|
|
525
|
-
message.includes("502") ||
|
|
526
|
-
message.includes("bad gateway") ||
|
|
527
|
-
message.includes("503") ||
|
|
528
|
-
message.includes("service unavailable") ||
|
|
529
|
-
message.includes("504") ||
|
|
530
|
-
message.includes("gateway timeout") ||
|
|
531
|
-
message.includes("econnrefused") ||
|
|
532
|
-
message.includes("econnreset") ||
|
|
533
|
-
message.includes("etimedout") ||
|
|
534
|
-
message.includes("fetch failed") ||
|
|
535
|
-
message.includes("timed out") ||
|
|
536
|
-
message.includes("timeout") ||
|
|
537
|
-
message.includes("sse read") ||
|
|
538
|
-
message.includes("stream error") ||
|
|
539
|
-
// Certificate/TLS provider failures must pass this primary retry gate
|
|
540
|
-
// before the managed-session fallback model branch can run.
|
|
541
|
-
message.includes("unknown certificate verification error") ||
|
|
542
|
-
message.includes("unknown_certificate_verification_error") ||
|
|
543
|
-
message.includes("certificate has expired") ||
|
|
544
|
-
message.includes("certificate verification") ||
|
|
545
|
-
message.includes("tls") ||
|
|
546
|
-
message.includes("ssl") ||
|
|
547
|
-
message.includes("connection reset") ||
|
|
548
|
-
message.includes("socket hang up") ||
|
|
549
|
-
message.includes("aborted")) {
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
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
|
+
};
|
|
553
532
|
}
|
|
554
|
-
function
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
return
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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;
|
|
577
563
|
}
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
return false;
|
|
581
|
-
const maybeError = error;
|
|
582
|
-
const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`.toLowerCase();
|
|
583
|
-
const code = `${maybeError.code ?? ""} ${maybeError.data?.code ?? ""} ${maybeError.error?.code ?? ""}`.toLowerCase();
|
|
584
|
-
const type = `${maybeError.data?.type ?? ""} ${maybeError.error?.type ?? ""}`.toLowerCase();
|
|
585
|
-
return (message.includes("unknown certificate verification error") ||
|
|
586
|
-
message.includes("unknown_certificate_verification_error") ||
|
|
587
|
-
message.includes("certificate has expired") ||
|
|
588
|
-
message.includes("certificate verification") ||
|
|
589
|
-
message.includes("tls") ||
|
|
590
|
-
message.includes("ssl") ||
|
|
591
|
-
code.includes("cert_has_expired") ||
|
|
592
|
-
code.includes("unknown_certificate_verification_error") ||
|
|
593
|
-
code.includes("unable_to_verify_leaf_signature") ||
|
|
594
|
-
code.includes("self_signed_cert") ||
|
|
595
|
-
code.includes("tls") ||
|
|
596
|
-
type.includes("certificate") ||
|
|
597
|
-
type.includes("tls") ||
|
|
598
|
-
type.includes("ssl"));
|
|
564
|
+
function isRetryableApiError(error) {
|
|
565
|
+
return getManagedSessionErrorAction(error) !== "none";
|
|
599
566
|
}
|
|
600
567
|
function isProviderRetryBanner(text) {
|
|
601
568
|
return /(?:<none>\s*)?retrying in \d+s\s*-\s*attempt #\d+/i.test(text);
|
|
602
569
|
}
|
|
603
570
|
function getRetryableErrorType(error) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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";
|
|
611
579
|
}
|
|
612
580
|
function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
|
|
613
581
|
const fallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
@@ -1718,14 +1686,17 @@ function createEventHandler(state) {
|
|
|
1718
1686
|
// active managed root session, retry it as a best-effort recovery path.
|
|
1719
1687
|
if (eventType === "session.error" && !sessionID && isRetryableApiError(error)) {
|
|
1720
1688
|
const rootCandidates = Array.from(state.managedUltraworkSessions.entries())
|
|
1721
|
-
.filter(([, record]) => record.kind === "root")
|
|
1722
|
-
.map(([id]) => id);
|
|
1689
|
+
.filter(([, record]) => record.kind === "root");
|
|
1723
1690
|
if (rootCandidates.length === 1) {
|
|
1724
|
-
const fallbackSessionID = rootCandidates[0];
|
|
1691
|
+
const [fallbackSessionID, managedSession] = rootCandidates[0];
|
|
1725
1692
|
const count = state.sessionErrorRetryCount.get(fallbackSessionID) ?? 0;
|
|
1726
1693
|
if (count < MAX_RETRIES && !state.sessionRetryTimers.has(fallbackSessionID)) {
|
|
1727
1694
|
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
|
|
1728
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
|
+
}
|
|
1729
1700
|
pluginLog.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
|
|
1730
1701
|
`Retrying sole managed root session ${fallbackSessionID}.`);
|
|
1731
1702
|
scheduleManagedSessionRetry(state, fallbackSessionID, {
|
|
@@ -1775,7 +1746,7 @@ function createEventHandler(state) {
|
|
|
1775
1746
|
state.sessionErrorRetryCount.set(sessionID, count);
|
|
1776
1747
|
return;
|
|
1777
1748
|
}
|
|
1778
|
-
else if (isRoot && (
|
|
1749
|
+
else if (isRoot && shouldUseFallbackForManagedError(error, managedSession, count)) {
|
|
1779
1750
|
const selectedFallbackModel = selectFallbackModel(getFailedModelFromError(error) ?? managedSession.currentModel);
|
|
1780
1751
|
await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
|
|
1781
1752
|
const errorType = getRetryableErrorType(error);
|