opencode-immune 1.0.72 → 1.0.75
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/server.js +161 -55
- package/package.json +1 -1
package/dist/plugin/server.js
CHANGED
|
@@ -3777,7 +3777,7 @@ import { fileURLToPath } from "url";
|
|
|
3777
3777
|
import { createHash } from "crypto";
|
|
3778
3778
|
import { tmpdir } from "os";
|
|
3779
3779
|
import { execFile } from "child_process";
|
|
3780
|
-
var PLUGIN_VERSION = "1.0.
|
|
3780
|
+
var PLUGIN_VERSION = "1.0.75";
|
|
3781
3781
|
var PLUGIN_PACKAGE_NAME = "opencode-immune";
|
|
3782
3782
|
var PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
|
|
3783
3783
|
function getServerAuthHeaders() {
|
|
@@ -3855,6 +3855,7 @@ function createState(input) {
|
|
|
3855
3855
|
ultraworkPermissionSessions: /* @__PURE__ */ new Set(),
|
|
3856
3856
|
fallbackAgentByAgent: /* @__PURE__ */ new Map(),
|
|
3857
3857
|
baseAgentByFallbackAgent: /* @__PURE__ */ new Map(),
|
|
3858
|
+
fallbackModelCandidates: [],
|
|
3858
3859
|
ultraworkMarkerPath: join(
|
|
3859
3860
|
input.directory,
|
|
3860
3861
|
".opencode",
|
|
@@ -3911,24 +3912,15 @@ var MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
3911
3912
|
var PROVIDER_RETRY_WATCHDOG_MS = 3e4;
|
|
3912
3913
|
var RETRY_PROMPT_DELIVERY_ATTEMPTS = 3;
|
|
3913
3914
|
var CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1e3;
|
|
3914
|
-
var RATE_LIMIT_FALLBACK_MODEL = {
|
|
3915
|
-
providerID: "codexsale",
|
|
3916
|
-
modelID: "gpt-5.4-mini"
|
|
3917
|
-
};
|
|
3918
|
-
var CHILD_SESSION_FALLBACK_MODEL = {
|
|
3919
|
-
providerID: "claudehub",
|
|
3920
|
-
modelID: "claude-opus-4-7"
|
|
3921
|
-
};
|
|
3922
|
-
var HIGH_CAPABILITY_FALLBACK_MODELS = [
|
|
3923
|
-
{ providerID: "codexsale", modelID: "gpt-5.5" },
|
|
3924
|
-
{ providerID: "codexsale", modelID: "gpt-5.4" }
|
|
3925
|
-
];
|
|
3926
3915
|
var AUTO_CYCLE_LOCK_TTL_MS = 30 * 60 * 1e3;
|
|
3927
|
-
var
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3916
|
+
var MODEL_NAME_CAPABILITY_SCORE = {
|
|
3917
|
+
"claude-opus-4-7": 100,
|
|
3918
|
+
"gpt-5.5": 100,
|
|
3919
|
+
"gpt-5.4": 90,
|
|
3920
|
+
"claude-sonnet-4-6": 90,
|
|
3921
|
+
"gpt-5.4-mini": 50,
|
|
3922
|
+
"claude-haiku-4-5": 50
|
|
3923
|
+
};
|
|
3932
3924
|
var FALLBACK_AGENT_SUFFIX = "-provider-fallback";
|
|
3933
3925
|
function isManagedUltraworkSession(state, sessionID) {
|
|
3934
3926
|
return !!sessionID && state.managedUltraworkSessions.has(sessionID);
|
|
@@ -3956,6 +3948,42 @@ async function createManagedUltraworkSession(state, title) {
|
|
|
3956
3948
|
await addManagedUltraworkSession(state, sessionID);
|
|
3957
3949
|
return sessionID;
|
|
3958
3950
|
}
|
|
3951
|
+
async function startAutoCycleInNewSession(state, options) {
|
|
3952
|
+
const nextTask = options.nextTask?.trim() || "Continue processing task backlog";
|
|
3953
|
+
const title = `AUTO-CYCLE: ${nextTask}`;
|
|
3954
|
+
state.autoResumeInFlight = true;
|
|
3955
|
+
try {
|
|
3956
|
+
const newSessionID = await createManagedUltraworkSession(state, title);
|
|
3957
|
+
if (!newSessionID) {
|
|
3958
|
+
throw new Error("session.create returned no session ID");
|
|
3959
|
+
}
|
|
3960
|
+
await refreshAutoCycleLock(state, newSessionID);
|
|
3961
|
+
await promptManagedSession(
|
|
3962
|
+
state,
|
|
3963
|
+
newSessionID,
|
|
3964
|
+
`[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`
|
|
3965
|
+
);
|
|
3966
|
+
state.autoResumeAttempted = true;
|
|
3967
|
+
if (options.retireSourceSession && isManagedUltraworkSession(state, options.sourceSessionID)) {
|
|
3968
|
+
await removeManagedUltraworkSession(
|
|
3969
|
+
state,
|
|
3970
|
+
options.sourceSessionID,
|
|
3971
|
+
"auto-cycle moved to a new session"
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
pluginLog.info(
|
|
3975
|
+
`[opencode-immune] ${options.logContext}: Bootstrap prompt sent to ${newSessionID}`
|
|
3976
|
+
);
|
|
3977
|
+
return newSessionID;
|
|
3978
|
+
} catch (err) {
|
|
3979
|
+
if (options.clearLockOnFailureReason) {
|
|
3980
|
+
await clearAutoCycleLock(state, options.clearLockOnFailureReason);
|
|
3981
|
+
}
|
|
3982
|
+
throw err;
|
|
3983
|
+
} finally {
|
|
3984
|
+
state.autoResumeInFlight = false;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3959
3987
|
async function applyUltraworkSessionPermissions(state, sessionID) {
|
|
3960
3988
|
if (state.ultraworkPermissionSessions.has(sessionID)) return;
|
|
3961
3989
|
try {
|
|
@@ -4212,15 +4240,57 @@ function isSameModel(a, b) {
|
|
|
4212
4240
|
if (!a || !b) return false;
|
|
4213
4241
|
return a.providerID === b.providerID && a.modelID === b.modelID;
|
|
4214
4242
|
}
|
|
4215
|
-
function
|
|
4243
|
+
function getModelCapabilityScore(model) {
|
|
4244
|
+
if (!model) return 90;
|
|
4245
|
+
const modelID = model.modelID.toLowerCase();
|
|
4246
|
+
const explicitScore = MODEL_NAME_CAPABILITY_SCORE[modelID];
|
|
4247
|
+
if (explicitScore !== void 0) return explicitScore;
|
|
4248
|
+
if (/\b(mini|nano|small|lite|haiku|flash)\b/i.test(modelID)) return 50;
|
|
4249
|
+
if (/\b(opus|gpt-5\.5|gpt-5\.4|o3|o4|gpt-4\.1|gpt-4o)\b/i.test(modelID)) return 90;
|
|
4250
|
+
return 70;
|
|
4251
|
+
}
|
|
4252
|
+
function getMinimumFallbackCapabilityScore(currentModel) {
|
|
4253
|
+
const currentScore = getModelCapabilityScore(currentModel);
|
|
4254
|
+
if (currentScore >= 90) return 90;
|
|
4255
|
+
if (currentScore >= 70) return 70;
|
|
4256
|
+
return 0;
|
|
4257
|
+
}
|
|
4258
|
+
function sortFallbackModelsByCapability(candidates, currentModel) {
|
|
4259
|
+
const currentScore = getModelCapabilityScore(currentModel);
|
|
4260
|
+
const minimumScore = getMinimumFallbackCapabilityScore(currentModel);
|
|
4261
|
+
return [...candidates].sort((a, b) => {
|
|
4262
|
+
const aScore = getModelCapabilityScore(a);
|
|
4263
|
+
const bScore = getModelCapabilityScore(b);
|
|
4264
|
+
const aAllowed = aScore >= minimumScore ? 0 : 1;
|
|
4265
|
+
const bAllowed = bScore >= minimumScore ? 0 : 1;
|
|
4266
|
+
if (aAllowed !== bAllowed) return aAllowed - bAllowed;
|
|
4267
|
+
const aDirectionPenalty = aScore >= currentScore ? 0 : 1;
|
|
4268
|
+
const bDirectionPenalty = bScore >= currentScore ? 0 : 1;
|
|
4269
|
+
if (aDirectionPenalty !== bDirectionPenalty) {
|
|
4270
|
+
return aDirectionPenalty - bDirectionPenalty;
|
|
4271
|
+
}
|
|
4272
|
+
const aSameModelName = currentModel && a.modelID === currentModel.modelID ? 0 : 1;
|
|
4273
|
+
const bSameModelName = currentModel && b.modelID === currentModel.modelID ? 0 : 1;
|
|
4274
|
+
if (aSameModelName !== bSameModelName) return aSameModelName - bSameModelName;
|
|
4275
|
+
const aDistance = Math.abs(aScore - currentScore);
|
|
4276
|
+
const bDistance = Math.abs(bScore - currentScore);
|
|
4277
|
+
if (aDistance !== bDistance) return aDistance - bDistance;
|
|
4278
|
+
return bScore - aScore;
|
|
4279
|
+
});
|
|
4280
|
+
}
|
|
4281
|
+
function selectFallbackModel(state, currentModel) {
|
|
4216
4282
|
const currentProviderID = getModelProviderID(currentModel);
|
|
4217
|
-
const
|
|
4218
|
-
(candidate) => !isSameModel(candidate, currentModel)
|
|
4283
|
+
const fallbackCandidates = state.fallbackModelCandidates.filter(
|
|
4284
|
+
(candidate) => !isSameModel(candidate, currentModel)
|
|
4219
4285
|
);
|
|
4286
|
+
const otherProviderModel = sortFallbackModelsByCapability(
|
|
4287
|
+
fallbackCandidates.filter(
|
|
4288
|
+
(candidate) => getModelProviderID(candidate) !== currentProviderID
|
|
4289
|
+
),
|
|
4290
|
+
currentModel
|
|
4291
|
+
)[0];
|
|
4220
4292
|
if (otherProviderModel) return otherProviderModel;
|
|
4221
|
-
return
|
|
4222
|
-
(candidate) => !isSameModel(candidate, currentModel)
|
|
4223
|
-
) ?? RATE_LIMIT_FALLBACK_MODEL;
|
|
4293
|
+
return sortFallbackModelsByCapability(fallbackCandidates, currentModel)[0];
|
|
4224
4294
|
}
|
|
4225
4295
|
function getFailedModelFromError(error) {
|
|
4226
4296
|
if (!error || typeof error !== "object") return void 0;
|
|
@@ -4233,13 +4303,6 @@ function getFailedModelFromError(error) {
|
|
|
4233
4303
|
modelID: providerModelMatch[2].toLowerCase()
|
|
4234
4304
|
};
|
|
4235
4305
|
}
|
|
4236
|
-
const unsupportedModelMatch = message.match(/["']([^"']+)["']\s+model\s+is\s+not\s+supported/i);
|
|
4237
|
-
if (unsupportedModelMatch?.[1] && /\bcodex\b/i.test(message)) {
|
|
4238
|
-
return {
|
|
4239
|
-
providerID: "codexsale",
|
|
4240
|
-
modelID: unsupportedModelMatch[1].toLowerCase()
|
|
4241
|
-
};
|
|
4242
|
-
}
|
|
4243
4306
|
return void 0;
|
|
4244
4307
|
}
|
|
4245
4308
|
async function updateManagedSessionModel(state, sessionID, model) {
|
|
@@ -4256,7 +4319,7 @@ function getSessionFallbackModel(state, sessionID, preferredModel) {
|
|
|
4256
4319
|
if (preferredModel && !isSameModel(preferredModel, currentModel)) {
|
|
4257
4320
|
return preferredModel;
|
|
4258
4321
|
}
|
|
4259
|
-
return selectFallbackModel(currentModel);
|
|
4322
|
+
return selectFallbackModel(state, currentModel);
|
|
4260
4323
|
}
|
|
4261
4324
|
function getFallbackAgentForAgent(state, agent) {
|
|
4262
4325
|
return state.fallbackAgentByAgent.get(agent) ?? agent;
|
|
@@ -4393,8 +4456,10 @@ async function recoverUntrackedRootSessionForActiveTask(state, sessionID, reason
|
|
|
4393
4456
|
}
|
|
4394
4457
|
function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
|
|
4395
4458
|
const fallbackModel = selectFallbackModel(
|
|
4459
|
+
state,
|
|
4396
4460
|
getFailedModelFromError(error) ?? managedSession.currentModel
|
|
4397
4461
|
);
|
|
4462
|
+
if (!fallbackModel) return;
|
|
4398
4463
|
const routerSessionID = getRouterSessionIDForChild(managedSession);
|
|
4399
4464
|
const request = {
|
|
4400
4465
|
childSessionID,
|
|
@@ -4425,6 +4490,7 @@ function scheduleProviderRetryWatchdog(state, sessionID, model) {
|
|
|
4425
4490
|
if (!isManagedRootUltraworkSession(state, sessionID)) return;
|
|
4426
4491
|
if (state.sessionRetryTimers.has(sessionID)) return;
|
|
4427
4492
|
const fallbackModel = getSessionFallbackModel(state, sessionID, model);
|
|
4493
|
+
if (!fallbackModel) return;
|
|
4428
4494
|
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
4429
4495
|
await writeDiagnosticLog(state, "provider-retry-watchdog:fired", {
|
|
4430
4496
|
sessionID,
|
|
@@ -4656,6 +4722,36 @@ function parseModelRef(model) {
|
|
|
4656
4722
|
if (!providerID || !modelID) return void 0;
|
|
4657
4723
|
return { providerID, modelID };
|
|
4658
4724
|
}
|
|
4725
|
+
function addFallbackModelCandidate(candidates, model) {
|
|
4726
|
+
const parsed = parseModelRef(model);
|
|
4727
|
+
if (!parsed) return;
|
|
4728
|
+
candidates.set(modelRefToString(parsed).toLowerCase(), parsed);
|
|
4729
|
+
}
|
|
4730
|
+
function collectFallbackModelCandidates(config) {
|
|
4731
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
4732
|
+
addFallbackModelCandidate(candidates, config.model);
|
|
4733
|
+
addFallbackModelCandidate(candidates, config.small_model);
|
|
4734
|
+
for (const agentConfig of Object.values(config.agent ?? {})) {
|
|
4735
|
+
if (!isRecord(agentConfig)) continue;
|
|
4736
|
+
const model = agentConfig.model;
|
|
4737
|
+
if (typeof model === "string") addFallbackModelCandidate(candidates, model);
|
|
4738
|
+
}
|
|
4739
|
+
const enabledProviders = new Set(config.enabled_providers ?? []);
|
|
4740
|
+
const disabledProviders = new Set(config.disabled_providers ?? []);
|
|
4741
|
+
const isProviderEnabled = (providerID) => {
|
|
4742
|
+
if (enabledProviders.size > 0 && !enabledProviders.has(providerID)) return false;
|
|
4743
|
+
return !disabledProviders.has(providerID);
|
|
4744
|
+
};
|
|
4745
|
+
for (const [providerID, providerConfig] of Object.entries(config.provider ?? {})) {
|
|
4746
|
+
if (!isProviderEnabled(providerID) || !isRecord(providerConfig)) continue;
|
|
4747
|
+
const models = providerConfig.models;
|
|
4748
|
+
if (!isRecord(models)) continue;
|
|
4749
|
+
for (const modelID of Object.keys(models)) {
|
|
4750
|
+
addFallbackModelCandidate(candidates, `${providerID}/${modelID}`);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
return sortFallbackModelsByCapability([...candidates.values()]);
|
|
4754
|
+
}
|
|
4659
4755
|
function canCreateFallbackForAgent(agentID, agentConfig) {
|
|
4660
4756
|
if (agentID.endsWith(FALLBACK_AGENT_SUFFIX)) return false;
|
|
4661
4757
|
if (!isRecord(agentConfig)) return false;
|
|
@@ -4686,11 +4782,12 @@ function allowFallbackAgentInTaskPermissions(config, baseAgentID, fallbackAgentI
|
|
|
4686
4782
|
function createConfigHandler(state) {
|
|
4687
4783
|
return async (config) => {
|
|
4688
4784
|
if (!config.agent) return;
|
|
4785
|
+
state.fallbackModelCandidates = collectFallbackModelCandidates(config);
|
|
4689
4786
|
for (const [agentID, agentConfig] of Object.entries(config.agent)) {
|
|
4690
4787
|
if (!canCreateFallbackForAgent(agentID, agentConfig)) continue;
|
|
4691
4788
|
const baseModel = parseModelRef(agentConfig.model);
|
|
4692
|
-
const fallbackModel = selectFallbackModel(baseModel);
|
|
4693
|
-
if (!baseModel || isSameModel(baseModel, fallbackModel)) continue;
|
|
4789
|
+
const fallbackModel = selectFallbackModel(state, baseModel);
|
|
4790
|
+
if (!baseModel || !fallbackModel || isSameModel(baseModel, fallbackModel)) continue;
|
|
4694
4791
|
const fallbackAgentID = `${agentID}${FALLBACK_AGENT_SUFFIX}`;
|
|
4695
4792
|
if (config.agent[fallbackAgentID]) continue;
|
|
4696
4793
|
config.agent[fallbackAgentID] = {
|
|
@@ -5204,28 +5301,25 @@ function createSessionRecoveryEvent(state) {
|
|
|
5204
5301
|
sessionID
|
|
5205
5302
|
);
|
|
5206
5303
|
if (!lockAcquired) return;
|
|
5207
|
-
await addManagedUltraworkSession(state, sessionID);
|
|
5208
5304
|
await refreshAutoCycleLock(state, sessionID);
|
|
5209
|
-
state.autoResumeInFlight = true;
|
|
5210
5305
|
setTimeout(async () => {
|
|
5211
5306
|
try {
|
|
5212
|
-
await
|
|
5307
|
+
const newSessionID = await startAutoCycleInNewSession(
|
|
5213
5308
|
state,
|
|
5214
|
-
|
|
5215
|
-
|
|
5309
|
+
{
|
|
5310
|
+
sourceSessionID: sessionID,
|
|
5311
|
+
logContext: "Auto-cycle",
|
|
5312
|
+
clearLockOnFailureReason: "root session auto-cycle failed"
|
|
5313
|
+
}
|
|
5216
5314
|
);
|
|
5217
|
-
state.autoResumeAttempted = true;
|
|
5218
5315
|
pluginLog.info(
|
|
5219
|
-
`[opencode-immune] Auto-cycle prompt sent to
|
|
5316
|
+
`[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`
|
|
5220
5317
|
);
|
|
5221
5318
|
} catch (err) {
|
|
5222
|
-
await clearAutoCycleLock(state, "root session auto-cycle failed");
|
|
5223
5319
|
pluginLog.error(
|
|
5224
5320
|
`[opencode-immune] Auto-cycle prompt failed for root session ${sessionID}:`,
|
|
5225
5321
|
err
|
|
5226
5322
|
);
|
|
5227
|
-
} finally {
|
|
5228
|
-
state.autoResumeInFlight = false;
|
|
5229
5323
|
}
|
|
5230
5324
|
}, 3e3);
|
|
5231
5325
|
}
|
|
@@ -5503,9 +5597,12 @@ function createEventHandler(state) {
|
|
|
5503
5597
|
state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
|
|
5504
5598
|
if (shouldUseFallbackForManagedError(error, managedSession, count)) {
|
|
5505
5599
|
const fallbackModel = selectFallbackModel(
|
|
5600
|
+
state,
|
|
5506
5601
|
getFailedModelFromError(error) ?? managedSession.currentModel
|
|
5507
5602
|
);
|
|
5508
|
-
|
|
5603
|
+
if (fallbackModel) {
|
|
5604
|
+
await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
|
|
5605
|
+
}
|
|
5509
5606
|
}
|
|
5510
5607
|
pluginLog.warn(
|
|
5511
5608
|
`[opencode-immune] session.error without sessionID matched retryable error. Retrying sole managed root session ${fallbackSessionID}.`
|
|
@@ -5561,8 +5658,10 @@ function createEventHandler(state) {
|
|
|
5561
5658
|
return;
|
|
5562
5659
|
} else if (isRoot && shouldUseFallbackForManagedError(error, managedSession, count)) {
|
|
5563
5660
|
const selectedFallbackModel = selectFallbackModel(
|
|
5661
|
+
state,
|
|
5564
5662
|
getFailedModelFromError(error) ?? managedSession.currentModel
|
|
5565
5663
|
);
|
|
5664
|
+
if (!selectedFallbackModel) return;
|
|
5566
5665
|
await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
|
|
5567
5666
|
const errorType = getRetryableErrorType(error);
|
|
5568
5667
|
pluginLog.info(
|
|
@@ -5709,6 +5808,7 @@ function createTextCompleteHandler(state) {
|
|
|
5709
5808
|
cancelProviderRetryWatchdog(state, sessionID, "assistant text completed");
|
|
5710
5809
|
if (isProviderRetryBanner(text) && isManagedRootSession) {
|
|
5711
5810
|
const fallbackModel = getSessionFallbackModel(state, sessionID);
|
|
5811
|
+
if (!fallbackModel) return;
|
|
5712
5812
|
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
5713
5813
|
scheduleManagedSessionRetry(state, sessionID, {
|
|
5714
5814
|
delayMs: 1e3,
|
|
@@ -5769,15 +5869,18 @@ function createTextCompleteHandler(state) {
|
|
|
5769
5869
|
const taskMatch = text.match(NEXT_TASK_PATTERN);
|
|
5770
5870
|
const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
|
|
5771
5871
|
pluginLog.info(
|
|
5772
|
-
`[opencode-immune] Multi-Cycle: Starting next cycle in
|
|
5872
|
+
`[opencode-immune] Multi-Cycle: Starting next cycle in a new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`
|
|
5773
5873
|
);
|
|
5774
5874
|
try {
|
|
5775
|
-
await
|
|
5875
|
+
await startAutoCycleInNewSession(
|
|
5776
5876
|
state,
|
|
5777
|
-
|
|
5778
|
-
|
|
5877
|
+
{
|
|
5878
|
+
sourceSessionID: sessionID,
|
|
5879
|
+
nextTask,
|
|
5880
|
+
logContext: "Multi-Cycle",
|
|
5881
|
+
retireSourceSession: true
|
|
5882
|
+
}
|
|
5779
5883
|
);
|
|
5780
|
-
pluginLog.info(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${sessionID}`);
|
|
5781
5884
|
} catch (err) {
|
|
5782
5885
|
pluginLog.error("[opencode-immune] Multi-Cycle: Failed to send prompt:", err);
|
|
5783
5886
|
}
|
|
@@ -5802,15 +5905,17 @@ function createTextCompleteHandler(state) {
|
|
|
5802
5905
|
);
|
|
5803
5906
|
try {
|
|
5804
5907
|
await refreshAutoCycleLock(state, sessionID);
|
|
5805
|
-
await
|
|
5908
|
+
await startAutoCycleInNewSession(
|
|
5806
5909
|
state,
|
|
5807
|
-
|
|
5808
|
-
|
|
5910
|
+
{
|
|
5911
|
+
sourceSessionID: sessionID,
|
|
5912
|
+
logContext: "Multi-Cycle fallback",
|
|
5913
|
+
clearLockOnFailureReason: "fallback bootstrap failed",
|
|
5914
|
+
retireSourceSession: true
|
|
5915
|
+
}
|
|
5809
5916
|
);
|
|
5810
|
-
pluginLog.info(`[opencode-immune] Multi-Cycle fallback: Bootstrap prompt sent to ${sessionID}`);
|
|
5811
5917
|
} catch (err) {
|
|
5812
5918
|
state.autoCycleSourceSessions.delete(sessionID);
|
|
5813
|
-
await clearAutoCycleLock(state, "fallback bootstrap failed");
|
|
5814
5919
|
pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to send prompt:", err);
|
|
5815
5920
|
} finally {
|
|
5816
5921
|
state.autoCycleInFlight = false;
|
|
@@ -5835,6 +5940,7 @@ function createMultiCycleHandler(state) {
|
|
|
5835
5940
|
if (sessionID && RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
|
|
5836
5941
|
if (managedSession && !managedSession.fallbackModel) {
|
|
5837
5942
|
const fallbackModel = getSessionFallbackModel(state, sessionID);
|
|
5943
|
+
if (!fallbackModel) return;
|
|
5838
5944
|
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
5839
5945
|
pluginLog.info(
|
|
5840
5946
|
`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`
|