opencode-immune 1.0.41 → 1.0.43

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 +59 -14
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -11,7 +11,7 @@ const child_process_1 = require("child_process");
11
11
  // ═══════════════════════════════════════════════════════════════════════════════
12
12
  // PLUGIN VERSION CHECK
13
13
  // ═══════════════════════════════════════════════════════════════════════════════
14
- const PLUGIN_VERSION = "1.0.41";
14
+ const PLUGIN_VERSION = "1.0.43";
15
15
  const PLUGIN_PACKAGE_NAME = "opencode-immune";
16
16
  /**
17
17
  * Read plugin version from package.json at runtime.
@@ -76,6 +76,7 @@ function createState(input) {
76
76
  managedUltraworkSessions: new Map(),
77
77
  sessionRetryTimers: new Map(),
78
78
  providerRetryWatchdogs: new Map(),
79
+ childFallbackRequests: new Map(),
79
80
  sessionErrorRetryCount: new Map(),
80
81
  ultraworkMarkerPath: (0, path_1.join)(input.directory, ".opencode", "state", "ultrawork-active.json"),
81
82
  diagnosticsLogPath: (0, path_1.join)(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
@@ -93,6 +94,7 @@ function createState(input) {
93
94
  const ULTRAWORK_AGENT = "0-ultrawork";
94
95
  const MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
95
96
  const PROVIDER_RETRY_WATCHDOG_MS = 30_000;
97
+ const CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1000;
96
98
  const RATE_LIMIT_FALLBACK_MODEL = {
97
99
  providerID: "externcash",
98
100
  modelID: "gpt-5.5",
@@ -364,14 +366,44 @@ function isCertificateApiError(error) {
364
366
  function isProviderRetryBanner(text) {
365
367
  return /(?:<none>\s*)?retrying in \d+s\s*-\s*attempt #\d+/i.test(text);
366
368
  }
369
+ function getRetryableErrorType(error) {
370
+ if (isModelAccessError(error))
371
+ return "model access error";
372
+ if (isRateLimitApiError(error))
373
+ return "rate limit";
374
+ if (isCertificateApiError(error))
375
+ return "certificate error";
376
+ return "retryable provider error";
377
+ }
378
+ function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
379
+ const request = {
380
+ childSessionID,
381
+ rootSessionID: managedSession.rootSessionID,
382
+ agent: managedSession.agent || "unknown",
383
+ errorType: getRetryableErrorType(error),
384
+ fallbackModel: CHILD_SESSION_FALLBACK_MODEL,
385
+ createdAt: Date.now(),
386
+ };
387
+ state.childFallbackRequests.set(managedSession.rootSessionID, request);
388
+ }
389
+ function getChildFallbackRequest(state, rootSessionID, now = Date.now()) {
390
+ const request = state.childFallbackRequests.get(rootSessionID);
391
+ if (!request)
392
+ return undefined;
393
+ if (now - request.createdAt > CHILD_FALLBACK_REQUEST_TTL_MS) {
394
+ state.childFallbackRequests.delete(rootSessionID);
395
+ return undefined;
396
+ }
397
+ return request;
398
+ }
367
399
  function scheduleProviderRetryWatchdog(state, sessionID, model) {
368
- if (!isManagedUltraworkSession(state, sessionID))
400
+ if (!isManagedRootUltraworkSession(state, sessionID))
369
401
  return;
370
402
  if (state.providerRetryWatchdogs.has(sessionID))
371
403
  return;
372
404
  const timer = setTimeout(async () => {
373
405
  state.providerRetryWatchdogs.delete(sessionID);
374
- if (!isManagedUltraworkSession(state, sessionID))
406
+ if (!isManagedRootUltraworkSession(state, sessionID))
375
407
  return;
376
408
  if (state.sessionRetryTimers.has(sessionID))
377
409
  return;
@@ -1041,6 +1073,21 @@ function createSystemTransform(state) {
1041
1073
  `Read memory-bank/tasks.md and memory-bank/activeContext.md to resume work.`);
1042
1074
  }
1043
1075
  }
1076
+ const rootSessionID = input.sessionID;
1077
+ if (rootSessionID && isManagedRootUltraworkSession(state, rootSessionID)) {
1078
+ const childFallbackRequest = getChildFallbackRequest(state, rootSessionID);
1079
+ if (childFallbackRequest) {
1080
+ output.system.push(`[Router-Owned Child Retry Required]\n` +
1081
+ `A child/subagent session failed with a retryable provider/model error. ` +
1082
+ `Do not advance to the next sequential pipeline slot until this failed slot is explicitly resolved.\n` +
1083
+ `- Failed child session: ${childFallbackRequest.childSessionID}\n` +
1084
+ `- Agent: ${childFallbackRequest.agent}\n` +
1085
+ `- Error type: ${childFallbackRequest.errorType}\n` +
1086
+ `- Required fallback model: ${childFallbackRequest.fallbackModel.providerID}/${childFallbackRequest.fallbackModel.modelID}\n` +
1087
+ `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. ` +
1088
+ `Never resume the failed child session directly; create a router-owned replacement attempt instead.`);
1089
+ }
1090
+ }
1044
1091
  // Ralph Loop injection
1045
1092
  if (state.lastEditAttempt) {
1046
1093
  const edit = state.lastEditAttempt;
@@ -1251,7 +1298,6 @@ function createFallbackModels(state) {
1251
1298
  }
1252
1299
  else if (getManagedSession(state, input.sessionID)?.kind === "child") {
1253
1300
  await updateManagedSessionAgent(state, input.sessionID, input.agent);
1254
- scheduleProviderRetryWatchdog(state, input.sessionID, CHILD_SESSION_FALLBACK_MODEL);
1255
1301
  }
1256
1302
  // NOTE: Do NOT remove managed root sessions when agent changes in chat.params.
1257
1303
  // Subagent calls from 0-ultrawork (1-van, 7-backlog, etc.) use the same session.
@@ -1332,12 +1378,14 @@ function createEventHandler(state) {
1332
1378
  if (count < MAX_RETRIES) {
1333
1379
  const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
1334
1380
  state.sessionErrorRetryCount.set(sessionID, count + 1);
1335
- // Pin fallback model: for child sessions always, for root provider-level failures.
1381
+ // Child/subagent failures are owned by the router. Auto-resuming a failed
1382
+ // child after the router advances can create two writers in one pipeline.
1336
1383
  if (isChild) {
1337
- await setSessionFallbackModel(state, sessionID, CHILD_SESSION_FALLBACK_MODEL);
1338
- const errorType = isModelAccessError(error) ? "model access error" : isRateLimitApiError(error) ? "rate limit" : isCertificateApiError(error) ? "certificate error" : "retryable error";
1339
- console.log(`[opencode-immune] Child session ${sessionID}: ${errorType} detected. ` +
1340
- `Retry will use fallback model ${CHILD_SESSION_FALLBACK_MODEL.providerID}/${CHILD_SESSION_FALLBACK_MODEL.modelID}.`);
1384
+ recordChildFallbackRequest(state, managedSession, sessionID, error);
1385
+ console.log(`[opencode-immune] Child session ${sessionID}: retryable error detected. ` +
1386
+ `Recorded router-owned fallback request and skipped plugin auto-retry.`);
1387
+ state.sessionErrorRetryCount.set(sessionID, count);
1388
+ return;
1341
1389
  }
1342
1390
  else if (isRoot && (isRateLimitApiError(error) || isCertificateApiError(error))) {
1343
1391
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
@@ -1515,11 +1563,8 @@ function createTextCompleteHandler(state) {
1515
1563
  cancelProviderRetryWatchdog(state, sessionID, "assistant text completed");
1516
1564
  // Some provider/SDK failures render as a retry banner instead of a
1517
1565
  // session.error event, leaving the UI waiting for long internal backoff.
1518
- if (isProviderRetryBanner(text) && isManagedUltraworkSession(state, sessionID)) {
1519
- const managedSession = getManagedSession(state, sessionID);
1520
- const fallbackModel = managedSession?.kind === "child"
1521
- ? CHILD_SESSION_FALLBACK_MODEL
1522
- : RATE_LIMIT_FALLBACK_MODEL;
1566
+ if (isProviderRetryBanner(text) && isManagedRootUltraworkSession(state, sessionID)) {
1567
+ const fallbackModel = RATE_LIMIT_FALLBACK_MODEL;
1523
1568
  await setSessionFallbackModel(state, sessionID, fallbackModel);
1524
1569
  scheduleManagedSessionRetry(state, sessionID, {
1525
1570
  delayMs: 1_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"