opencode-immune 1.0.14 → 1.0.16

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 -12
  2. package/package.json +2 -2
package/dist/plugin.js CHANGED
@@ -30,6 +30,10 @@ const RATE_LIMIT_FALLBACK_MODEL = {
30
30
  providerID: "externcash",
31
31
  modelID: "gpt-5.4",
32
32
  };
33
+ const CHILD_SESSION_FALLBACK_MODEL = {
34
+ providerID: "externcash",
35
+ modelID: "gpt-5.4",
36
+ };
33
37
  function isManagedUltraworkSession(state, sessionID) {
34
38
  return !!sessionID && state.managedUltraworkSessions.has(sessionID);
35
39
  }
@@ -173,8 +177,36 @@ function isRetryableApiError(error) {
173
177
  if (!error || typeof error !== "object")
174
178
  return false;
175
179
  const maybeError = error;
176
- return (maybeError.name === "APIError" &&
177
- maybeError.data?.isRetryable === true);
180
+ // Structured retryable flag
181
+ if (maybeError.name === "APIError" &&
182
+ maybeError.data?.isRetryable === true) {
183
+ return true;
184
+ }
185
+ // Text-based detection for model access errors (not marked as retryable
186
+ // by the API but retryable with a fallback model)
187
+ const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
188
+ if (message.includes("не разрешен") ||
189
+ message.includes("not allowed") ||
190
+ message.includes("model not available") ||
191
+ message.includes("model_not_found") ||
192
+ message.includes("access denied")) {
193
+ return true;
194
+ }
195
+ return false;
196
+ }
197
+ function isModelAccessError(error) {
198
+ if (!error || typeof error !== "object")
199
+ return false;
200
+ const maybeError = error;
201
+ const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
202
+ const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
203
+ return (message.includes("не разрешен") ||
204
+ message.includes("not allowed") ||
205
+ message.includes("model not available") ||
206
+ message.includes("model_not_found") ||
207
+ message.includes("access denied") ||
208
+ type.includes("model_not_found") ||
209
+ type.includes("invalid_model"));
178
210
  }
179
211
  function isRateLimitApiError(error) {
180
212
  if (!error || typeof error !== "object")
@@ -263,7 +295,7 @@ async function sendManagedSessionRetryPrompt(state, sessionID, reason, options =
263
295
  return true;
264
296
  }
265
297
  function scheduleManagedSessionRetry(state, sessionID, options) {
266
- if (!isManagedRootUltraworkSession(state, sessionID)) {
298
+ if (!isManagedUltraworkSession(state, sessionID)) {
267
299
  return false;
268
300
  }
269
301
  if (state.sessionRetryTimers.has(sessionID)) {
@@ -275,7 +307,7 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
275
307
  `Waiting ${options.delayMs / 1000}s before retry...`);
276
308
  const timer = setTimeout(async () => {
277
309
  state.sessionRetryTimers.delete(sessionID);
278
- if (!isManagedRootUltraworkSession(state, sessionID)) {
310
+ if (!isManagedUltraworkSession(state, sessionID)) {
279
311
  return;
280
312
  }
281
313
  try {
@@ -497,6 +529,11 @@ function createSessionRecoveryEvent(state) {
497
529
  if (recovery.phase !== "ARCHIVE: DONE") {
498
530
  // Register this root session as managed so retry/recovery works
499
531
  await addManagedUltraworkSession(state, sessionID);
532
+ // Skip sending AUTO-RESUME if already sent from plugin init
533
+ if (state.autoResumeAttempted) {
534
+ console.log(`[opencode-immune] Auto-resume already sent from plugin init, skipping duplicate for session ${sessionID}.`);
535
+ return;
536
+ }
500
537
  setTimeout(async () => {
501
538
  try {
502
539
  await state.input.client.session.promptAsync({
@@ -791,10 +828,13 @@ function createEventHandler(state) {
791
828
  const eventType = event.type ?? "unknown";
792
829
  const info = event.properties?.info;
793
830
  const sessionID = event.properties?.sessionID ?? info?.id;
794
- // ── Auto-retry on retryable API error for managed root ultrawork sessions ──
831
+ // ── Auto-retry on retryable API error for managed ultrawork sessions ──
795
832
  if (eventType === "session.error" && sessionID) {
796
833
  const error = event.properties?.error;
797
- if (!isManagedRootUltraworkSession(state, sessionID)) {
834
+ const managedSession = getManagedSession(state, sessionID);
835
+ const isRoot = managedSession?.kind === "root";
836
+ const isChild = managedSession?.kind === "child";
837
+ if (!managedSession) {
798
838
  return;
799
839
  }
800
840
  if (!isRetryableApiError(error)) {
@@ -803,22 +843,29 @@ function createEventHandler(state) {
803
843
  return;
804
844
  }
805
845
  if (state.sessionRetryTimers.has(sessionID)) {
806
- console.log(`[opencode-immune] Retry already pending for session ${sessionID}, skipping duplicate.`);
846
+ console.log(`[opencode-immune] Retry already pending for ${isChild ? "child" : "root"} session ${sessionID}, skipping duplicate.`);
807
847
  return;
808
848
  }
809
849
  const count = state.sessionErrorRetryCount.get(sessionID) ?? 0;
810
850
  if (count < MAX_RETRIES) {
811
851
  const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
812
852
  state.sessionErrorRetryCount.set(sessionID, count + 1);
813
- if (isRateLimitApiError(error)) {
853
+ // Pin fallback model: for child sessions always, for root on rate-limit
854
+ if (isChild) {
855
+ await setSessionFallbackModel(state, sessionID, CHILD_SESSION_FALLBACK_MODEL);
856
+ const errorType = isModelAccessError(error) ? "model access error" : isRateLimitApiError(error) ? "rate limit" : "retryable error";
857
+ console.log(`[opencode-immune] Child session ${sessionID}: ${errorType} detected. ` +
858
+ `Retry will use fallback model ${CHILD_SESSION_FALLBACK_MODEL.providerID}/${CHILD_SESSION_FALLBACK_MODEL.modelID}.`);
859
+ }
860
+ else if (isRoot && isRateLimitApiError(error)) {
814
861
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
815
- console.log(`[opencode-immune] Rate limit detected for session ${sessionID}. ` +
862
+ console.log(`[opencode-immune] Rate limit detected for root session ${sessionID}. ` +
816
863
  `Retry will use fallback model ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
817
864
  }
818
865
  const scheduled = scheduleManagedSessionRetry(state, sessionID, {
819
866
  delayMs: delay,
820
- reason: "session.error",
821
- attemptLabel: `attempt ${count + 1}/${MAX_RETRIES}`,
867
+ reason: isChild ? "child session.error" : "session.error",
868
+ attemptLabel: `${isChild ? "child " : ""}attempt ${count + 1}/${MAX_RETRIES}`,
822
869
  countAgainstBudget: true,
823
870
  });
824
871
  if (!scheduled) {
@@ -826,7 +873,7 @@ function createEventHandler(state) {
826
873
  }
827
874
  }
828
875
  else {
829
- console.log(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for session ${sessionID}. Not retrying.`);
876
+ console.log(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for ${isChild ? "child" : "root"} session ${sessionID}. Not retrying.`);
830
877
  }
831
878
  }
832
879
  // Reset retry counter on successful activity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"
@@ -14,7 +14,7 @@
14
14
  "prepublishOnly": "npm run build"
15
15
  },
16
16
  "dependencies": {
17
- "@opencode-ai/plugin": "1.4.1"
17
+ "@opencode-ai/plugin": "1.4.2"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^25.5.2",