opencode-immune 1.0.47 → 1.0.49
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 +56 -11
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -12,7 +12,7 @@ const child_process_1 = require("child_process");
|
|
|
12
12
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
13
13
|
// PLUGIN VERSION CHECK
|
|
14
14
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
-
const PLUGIN_VERSION = "1.0.
|
|
15
|
+
const PLUGIN_VERSION = "1.0.49";
|
|
16
16
|
const PLUGIN_PACKAGE_NAME = "opencode-immune";
|
|
17
17
|
/**
|
|
18
18
|
* Read plugin version from package.json at runtime.
|
|
@@ -59,7 +59,9 @@ async function checkPluginUpdate(state) {
|
|
|
59
59
|
if (latest && latest !== currentVersion) {
|
|
60
60
|
state.pluginUpdateMessage =
|
|
61
61
|
`[PLUGIN UPDATE] opencode-immune ${currentVersion} → ${latest} is available. ` +
|
|
62
|
-
`Please inform the user:
|
|
62
|
+
`Please inform the user: run \`opencode plugin opencode-immune@latest --force\`, ` +
|
|
63
|
+
`then restart opencode. If opencode still loads the old package, clear its package cache with ` +
|
|
64
|
+
`\`rm -rf ~/.cache/opencode/packages/opencode-immune@latest\` and run the update command again.`;
|
|
63
65
|
writePluginLog(state, "warn", `[opencode-immune] Plugin update available: ${currentVersion} → ${latest}.`);
|
|
64
66
|
}
|
|
65
67
|
else if (latest) {
|
|
@@ -388,7 +390,7 @@ function isRetryableApiError(error) {
|
|
|
388
390
|
return true;
|
|
389
391
|
}
|
|
390
392
|
// HTTP status code based detection (retryable server/gateway errors + rate limits)
|
|
391
|
-
const status = maybeError.status ?? maybeError.statusCode ?? maybeError.data?.status;
|
|
393
|
+
const status = maybeError.status ?? maybeError.statusCode ?? maybeError.data?.status ?? maybeError.error?.status;
|
|
392
394
|
if (status && (status === 404 || // transient endpoint not found (API gateway issues)
|
|
393
395
|
status === 429 || // rate limit
|
|
394
396
|
status === 500 || // internal server error
|
|
@@ -400,8 +402,18 @@ function isRetryableApiError(error) {
|
|
|
400
402
|
}
|
|
401
403
|
// Text-based detection for model access errors (not marked as retryable
|
|
402
404
|
// by the API but retryable with a fallback model)
|
|
403
|
-
const message =
|
|
404
|
-
|
|
405
|
+
const message = [
|
|
406
|
+
maybeError.message,
|
|
407
|
+
maybeError.code,
|
|
408
|
+
maybeError.data?.message,
|
|
409
|
+
maybeError.data?.type,
|
|
410
|
+
maybeError.data?.code,
|
|
411
|
+
maybeError.error?.message,
|
|
412
|
+
maybeError.error?.type,
|
|
413
|
+
maybeError.error?.code,
|
|
414
|
+
].filter((value) => value !== undefined && value !== null).join(" ").toLowerCase();
|
|
415
|
+
if (message.includes("api_error") ||
|
|
416
|
+
message.includes("не разрешен") ||
|
|
405
417
|
message.includes("not allowed") ||
|
|
406
418
|
message.includes("internal error") ||
|
|
407
419
|
message.includes("internal server error") ||
|
|
@@ -437,6 +449,7 @@ function isRetryableApiError(error) {
|
|
|
437
449
|
// Certificate/TLS provider failures must pass this primary retry gate
|
|
438
450
|
// before the managed-session fallback model branch can run.
|
|
439
451
|
message.includes("unknown certificate verification error") ||
|
|
452
|
+
message.includes("unknown_certificate_verification_error") ||
|
|
440
453
|
message.includes("certificate has expired") ||
|
|
441
454
|
message.includes("certificate verification") ||
|
|
442
455
|
message.includes("tls") ||
|
|
@@ -466,23 +479,25 @@ function isRateLimitApiError(error) {
|
|
|
466
479
|
if (!error || typeof error !== "object")
|
|
467
480
|
return false;
|
|
468
481
|
const maybeError = error;
|
|
469
|
-
const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
|
|
470
|
-
const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
|
|
482
|
+
const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`.toLowerCase();
|
|
483
|
+
const type = `${maybeError.data?.type ?? ""} ${maybeError.error?.type ?? ""}`.toLowerCase();
|
|
471
484
|
return ((type.includes("rate_limit") || message.includes("too many requests") || message.includes("rate limit")));
|
|
472
485
|
}
|
|
473
486
|
function isCertificateApiError(error) {
|
|
474
487
|
if (!error || typeof error !== "object")
|
|
475
488
|
return false;
|
|
476
489
|
const maybeError = error;
|
|
477
|
-
const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
|
|
478
|
-
const code = `${maybeError.code ?? ""} ${maybeError.data?.code ?? ""}`.toLowerCase();
|
|
479
|
-
const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
|
|
490
|
+
const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`.toLowerCase();
|
|
491
|
+
const code = `${maybeError.code ?? ""} ${maybeError.data?.code ?? ""} ${maybeError.error?.code ?? ""}`.toLowerCase();
|
|
492
|
+
const type = `${maybeError.data?.type ?? ""} ${maybeError.error?.type ?? ""}`.toLowerCase();
|
|
480
493
|
return (message.includes("unknown certificate verification error") ||
|
|
494
|
+
message.includes("unknown_certificate_verification_error") ||
|
|
481
495
|
message.includes("certificate has expired") ||
|
|
482
496
|
message.includes("certificate verification") ||
|
|
483
497
|
message.includes("tls") ||
|
|
484
498
|
message.includes("ssl") ||
|
|
485
499
|
code.includes("cert_has_expired") ||
|
|
500
|
+
code.includes("unknown_certificate_verification_error") ||
|
|
486
501
|
code.includes("unable_to_verify_leaf_signature") ||
|
|
487
502
|
code.includes("self_signed_cert") ||
|
|
488
503
|
code.includes("tls") ||
|
|
@@ -558,6 +573,28 @@ async function setSessionFallbackModel(state, sessionID, model) {
|
|
|
558
573
|
fallbackModel: model,
|
|
559
574
|
});
|
|
560
575
|
}
|
|
576
|
+
async function recoverUntrackedRootSessionForRetry(state, sessionID, error) {
|
|
577
|
+
if (!isRetryableApiError(error))
|
|
578
|
+
return undefined;
|
|
579
|
+
const markerActive = await isUltraworkMarkerActive(state);
|
|
580
|
+
if (!markerActive)
|
|
581
|
+
return undefined;
|
|
582
|
+
const recovery = await parseTasksFile(state.input.directory);
|
|
583
|
+
if (!recovery || recovery.phase === "ARCHIVE: DONE")
|
|
584
|
+
return undefined;
|
|
585
|
+
state.recoveryContext = recovery;
|
|
586
|
+
await addManagedUltraworkSession(state, sessionID);
|
|
587
|
+
const recovered = getManagedSession(state, sessionID);
|
|
588
|
+
await writeDiagnosticLog(state, "session-retry:recovered-untracked-root", {
|
|
589
|
+
sessionID,
|
|
590
|
+
task: recovery.task,
|
|
591
|
+
level: recovery.level,
|
|
592
|
+
phase: recovery.phase,
|
|
593
|
+
errorType: getRetryableErrorType(error),
|
|
594
|
+
});
|
|
595
|
+
writePluginLog(state, "warn", `[opencode-immune] Recovered untracked root session ${sessionID} for retry after plugin restart or state loss.`);
|
|
596
|
+
return recovered;
|
|
597
|
+
}
|
|
561
598
|
function getManagedSessionRetryContext(state, sessionID) {
|
|
562
599
|
const managedSession = state.managedUltraworkSessions.get(sessionID);
|
|
563
600
|
if (!managedSession)
|
|
@@ -1457,10 +1494,18 @@ function createEventHandler(state) {
|
|
|
1457
1494
|
}
|
|
1458
1495
|
// ── Auto-retry on retryable API error for managed ultrawork sessions ──
|
|
1459
1496
|
if (eventType === "session.error" && sessionID) {
|
|
1460
|
-
const managedSession = getManagedSession(state, sessionID)
|
|
1497
|
+
const managedSession = getManagedSession(state, sessionID) ??
|
|
1498
|
+
await recoverUntrackedRootSessionForRetry(state, sessionID, error);
|
|
1461
1499
|
const isRoot = managedSession?.kind === "root";
|
|
1462
1500
|
const isChild = managedSession?.kind === "child";
|
|
1463
1501
|
if (!managedSession) {
|
|
1502
|
+
if (isRetryableApiError(error)) {
|
|
1503
|
+
await writeDiagnosticLog(state, "session-retry:unmanaged-retryable-error", {
|
|
1504
|
+
sessionID,
|
|
1505
|
+
eventType,
|
|
1506
|
+
errorType: getRetryableErrorType(error),
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1464
1509
|
return;
|
|
1465
1510
|
}
|
|
1466
1511
|
if (!isRetryableApiError(error)) {
|