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.
- package/dist/plugin.js +59 -12
- 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
|
-
|
|
177
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
17
|
+
"@opencode-ai/plugin": "1.4.2"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^25.5.2",
|