@bybrawe/opencode-loop 0.5.34 → 0.5.36
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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/package.json +3 -3
- package/scripts/empty-turn-recovery-test.mjs +150 -0
- package/scripts/host-adapter-contract-test.mjs +2 -2
- package/scripts/host-goal-coexistence-canary.mjs +419 -0
- package/scripts/network-recovery-test.mjs +218 -0
- package/scripts/session-status-test.mjs +18 -4
- package/src/index.js +422 -33
- package/src/source/core/continuation.js +53 -2
- package/src/source/opencode/host.js +23 -2
- package/src/source/runtime/empty-turn.js +46 -0
- package/src/source/runtime/executor.js +173 -16
- package/src/source/runtime/network-recovery.js +40 -0
- package/src/source/runtime/run-finalization.js +28 -0
- package/src/source/runtime/session-status.js +37 -32
- package/src/source/runtime/terminal-guard.js +56 -0
package/src/index.js
CHANGED
|
@@ -801,11 +801,25 @@ function orderedSessionMessages(messages) {
|
|
|
801
801
|
return { message, index, created: Number.isFinite(created) ? created : 0 };
|
|
802
802
|
}).sort((a, b) => a.created - b.created || a.index - b.index).map((entry) => entry.message);
|
|
803
803
|
}
|
|
804
|
+
function assistantMessageHasMeaningfulActivity(message) {
|
|
805
|
+
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
806
|
+
for (const part of parts) {
|
|
807
|
+
if (!part || typeof part !== "object")
|
|
808
|
+
continue;
|
|
809
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.trim())
|
|
810
|
+
return true;
|
|
811
|
+
if (["tool", "file", "patch", "artifact"].includes(String(part.type || "")))
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
const info = message?.info || message || {};
|
|
815
|
+
return [info.text, info.content, info.summary].some((value) => typeof value === "string" && value.trim());
|
|
816
|
+
}
|
|
804
817
|
async function activeRunCompletionFromMessages(directory, client, sessionID, active) {
|
|
805
818
|
const messages = await readRecentSessionMessages(client, sessionID, directory);
|
|
806
819
|
if (!messages)
|
|
807
820
|
return "unknown";
|
|
808
|
-
const
|
|
821
|
+
const ordered = orderedSessionMessages(messages);
|
|
822
|
+
const tail = ordered.at(-1);
|
|
809
823
|
const info = tail?.info || tail;
|
|
810
824
|
if (!info || info.role !== "assistant")
|
|
811
825
|
return "incomplete";
|
|
@@ -816,7 +830,17 @@ async function activeRunCompletionFromMessages(directory, client, sessionID, act
|
|
|
816
830
|
const startedAt = Number(active?.startedAt || 0);
|
|
817
831
|
if (startedAt > 0 && completed < startedAt && (!Number.isFinite(created) || created < startedAt))
|
|
818
832
|
return "incomplete";
|
|
819
|
-
|
|
833
|
+
const relevant = ordered.filter((message) => {
|
|
834
|
+
const candidate = message?.info || message || {};
|
|
835
|
+
if (candidate.role !== "assistant")
|
|
836
|
+
return false;
|
|
837
|
+
if (startedAt <= 0)
|
|
838
|
+
return true;
|
|
839
|
+
const candidateCreated = Number(candidate?.time?.created || 0);
|
|
840
|
+
const candidateCompleted = Number(candidate?.time?.completed || 0);
|
|
841
|
+
return candidateCreated >= startedAt || candidateCompleted >= startedAt;
|
|
842
|
+
});
|
|
843
|
+
return relevant.some(assistantMessageHasMeaningfulActivity) ? "completed" : "empty";
|
|
820
844
|
}
|
|
821
845
|
async function resolveCompactionModel(directory, client, sessionID, preferredModel) {
|
|
822
846
|
const preferred = normalizedModelRef(preferredModel);
|
|
@@ -1033,13 +1057,55 @@ var CONTINUATION_SHORTHANDS = new Set([
|
|
|
1033
1057
|
"devam et.",
|
|
1034
1058
|
"devam et bakal\u0131m"
|
|
1035
1059
|
]);
|
|
1060
|
+
var COMPLETION_BOUNDED_PATTERNS = [
|
|
1061
|
+
/\bbitene kadar\b/i,
|
|
1062
|
+
/\b(?:tamamen|komple) projeyi bitir\b/i,
|
|
1063
|
+
/\bi\u015Fi bitir\b/i,
|
|
1064
|
+
/\buntil (?:it(?:'s| is) )?(?:done|complete|completed|finished)\b/i,
|
|
1065
|
+
/\bfinish (?:the )?(?:project|task|work)\b/i,
|
|
1066
|
+
/\bkeep going until\b/i
|
|
1067
|
+
];
|
|
1068
|
+
var TERMINAL_COMPLETION_PATTERNS = [
|
|
1069
|
+
/proje tamamland[\u0131i](?=\s|[.,;:!\u2014-]|$)/i,
|
|
1070
|
+
/\bproject (?:is )?(?:complete|completed|finished|done)\b/i,
|
|
1071
|
+
/\b(?:task|work) (?:is )?(?:complete|completed|finished|done)\b/i
|
|
1072
|
+
];
|
|
1073
|
+
var TERMINAL_NO_WORK_PATTERNS = [
|
|
1074
|
+
/yap[\u0131i]lacak (?:ba\u015Fka )?i\u015F yok(?=\s|[.,;:!\u2014-]|$)/i,
|
|
1075
|
+
/ba\u015Fka (?:bir )?i\u015F (?:kalmad[\u0131i]|yok)(?=\s|[.,;:!\u2014-]|$)/i,
|
|
1076
|
+
/\bnothing (?:else )?left to do\b/i,
|
|
1077
|
+
/\bno (?:more|remaining) work\b/i,
|
|
1078
|
+
/\bno known (?:bugs|issues)\b/i,
|
|
1079
|
+
/\bzero known (?:bugs|issues)\b/i
|
|
1080
|
+
];
|
|
1081
|
+
var NEXT_WORK_PATTERNS = [
|
|
1082
|
+
/\bnext(?: step| task)?\b/i,
|
|
1083
|
+
/s[\u0131i]radaki(?=\s|[.,;:!\u2014-]|$)/i,
|
|
1084
|
+
/sonraki(?=\s|[.,;:!\u2014-]|$)/i,
|
|
1085
|
+
/kalan (?:i\u015F|i\u015Fler|todo|ad\u0131m)(?=\s|[.,;:!\u2014-]|$)/i,
|
|
1086
|
+
/\bremaining (?:work|task|todo|step)/i,
|
|
1087
|
+
/devam (?:edece\u011Fim|ediyorum|etmek gerek)(?=\s|[.,;:!\u2014-]|$)/i
|
|
1088
|
+
];
|
|
1036
1089
|
function isContinuationShorthand(value) {
|
|
1037
1090
|
return CONTINUATION_SHORTHANDS.has(String(value || "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
1038
1091
|
}
|
|
1092
|
+
function isCompletionBoundedContinuation(value) {
|
|
1093
|
+
const text = String(value || "").trim();
|
|
1094
|
+
return COMPLETION_BOUNDED_PATTERNS.some((pattern) => pattern.test(text));
|
|
1095
|
+
}
|
|
1096
|
+
function isTerminalNoWorkReply(value) {
|
|
1097
|
+
const text = String(value || "").trim();
|
|
1098
|
+
if (!text || NEXT_WORK_PATTERNS.some((pattern) => pattern.test(text)))
|
|
1099
|
+
return false;
|
|
1100
|
+
const completed = TERMINAL_COMPLETION_PATTERNS.some((pattern) => pattern.test(text));
|
|
1101
|
+
const noWork = TERMINAL_NO_WORK_PATTERNS.some((pattern) => pattern.test(text));
|
|
1102
|
+
return completed && noWork;
|
|
1103
|
+
}
|
|
1039
1104
|
function continuationProjectInstruction(value) {
|
|
1040
|
-
if (!isContinuationShorthand(value))
|
|
1105
|
+
if (!isContinuationShorthand(value) && !isCompletionBoundedContinuation(value))
|
|
1041
1106
|
return "";
|
|
1042
|
-
|
|
1107
|
+
const finish = isCompletionBoundedContinuation(value) ? " If you believe the project is finished, perform a fresh verification pass before declaring completion; report both that the project is complete and that no work remains only when you have concrete current evidence." : "";
|
|
1108
|
+
return `Treat this as continuation of the current project and conversation, not a fresh task. Inspect the repository state, relevant files, TODO/progress notes, recent changes, and git status as needed to identify the next unfinished step. Continue from existing work, do not redo completed work, and verify meaningful changes when practical.${finish}`;
|
|
1043
1109
|
}
|
|
1044
1110
|
|
|
1045
1111
|
// src/source/core/jobs.js
|
|
@@ -2811,6 +2877,9 @@ function nonNegativeNumber(value, fallback) {
|
|
|
2811
2877
|
const number = Number(value);
|
|
2812
2878
|
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
2813
2879
|
}
|
|
2880
|
+
function settledAssistantCompletion(value) {
|
|
2881
|
+
return value === "completed" || value === "empty";
|
|
2882
|
+
}
|
|
2814
2883
|
function createSessionStatusRuntime(options = {}) {
|
|
2815
2884
|
const activeRuns = options.activeRuns;
|
|
2816
2885
|
if (!(activeRuns instanceof Map))
|
|
@@ -2865,7 +2934,9 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2865
2934
|
if (directory)
|
|
2866
2935
|
argsList.push({ query: { directory } }, { directory }, { workspace: directory });
|
|
2867
2936
|
argsList.push({});
|
|
2937
|
+
let attempted = false;
|
|
2868
2938
|
for (const args of argsList) {
|
|
2939
|
+
attempted = true;
|
|
2869
2940
|
try {
|
|
2870
2941
|
const result = await statusMethod.call(client.session, args);
|
|
2871
2942
|
const error = sdkError(result);
|
|
@@ -2895,7 +2966,7 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2895
2966
|
return { type: "idle", source: "sdk" };
|
|
2896
2967
|
} catch {}
|
|
2897
2968
|
}
|
|
2898
|
-
return;
|
|
2969
|
+
return attempted ? { type: "unknown", source: "sdk-error" } : undefined;
|
|
2899
2970
|
}
|
|
2900
2971
|
async function canFinalizeActiveRun(directory, client, sessionID, active, options2 = {}) {
|
|
2901
2972
|
if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID))
|
|
@@ -2903,33 +2974,37 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2903
2974
|
if (!options2.requireIdle && !options2.forceStale)
|
|
2904
2975
|
return true;
|
|
2905
2976
|
const completion = options2.forceStale ? await activeRunCompletionFromMessages2(directory, client, sessionID, active) : undefined;
|
|
2906
|
-
if (completion
|
|
2977
|
+
if (settledAssistantCompletion(completion))
|
|
2907
2978
|
return true;
|
|
2908
2979
|
if (!options2.requireIdle)
|
|
2909
2980
|
return completion === "unknown" && staleActiveRun(sessionID);
|
|
2981
|
+
const cached = sessionStatuses.get(sessionID);
|
|
2982
|
+
const seenAt = sessionStatusSeenAt.get(sessionID) || 0;
|
|
2983
|
+
const cachedIdleAfterRun = cached === "idle" && seenAt > (active.startedAt || 0);
|
|
2910
2984
|
const live = await readLiveSessionStatus(client, sessionID, directory);
|
|
2911
2985
|
if (live?.type === "idle")
|
|
2912
2986
|
return true;
|
|
2987
|
+
if (live?.type === "unknown" && cachedIdleAfterRun) {
|
|
2988
|
+
return true;
|
|
2989
|
+
}
|
|
2913
2990
|
if (live?.type) {
|
|
2914
|
-
if (
|
|
2991
|
+
if (live.type === "busy" && options2.forceStale && completion === "unknown" && staleActiveRun(sessionID))
|
|
2915
2992
|
return true;
|
|
2916
2993
|
return false;
|
|
2917
2994
|
}
|
|
2918
2995
|
if (options2.forceStale && completion === "unknown" && staleActiveRun(sessionID))
|
|
2919
2996
|
return true;
|
|
2920
|
-
|
|
2921
|
-
const seenAt = sessionStatusSeenAt.get(sessionID) || 0;
|
|
2922
|
-
return cached === "idle" && seenAt > (active.startedAt || 0);
|
|
2997
|
+
return cachedIdleAfterRun;
|
|
2923
2998
|
}
|
|
2924
2999
|
async function recoverCompletedTailWithoutActiveRun(directory, client, sessionID, liveType, seenAt) {
|
|
2925
3000
|
if (activeRuns.has(sessionID))
|
|
2926
3001
|
return false;
|
|
2927
|
-
if (liveType !== "busy"
|
|
3002
|
+
if (liveType !== "busy")
|
|
2928
3003
|
return false;
|
|
2929
3004
|
if (!seenAt || now2() - seenAt < sessionStatusCacheMs)
|
|
2930
3005
|
return false;
|
|
2931
3006
|
const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, { startedAt: 0 });
|
|
2932
|
-
if (completion
|
|
3007
|
+
if (!settledAssistantCompletion(completion))
|
|
2933
3008
|
return false;
|
|
2934
3009
|
markSessionStatus(sessionID, "idle");
|
|
2935
3010
|
await appendLoopLog2(directory, "status-message-idle-recovery", {
|
|
@@ -2952,6 +3027,11 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2952
3027
|
if (cached && now2() - seenAt < sessionStatusCacheMs)
|
|
2953
3028
|
return cached;
|
|
2954
3029
|
const live = await readLiveSessionStatus(client, sessionID, directory);
|
|
3030
|
+
if (live?.type === "unknown") {
|
|
3031
|
+
const conservative = cached === "retry" ? "retry" : "busy";
|
|
3032
|
+
markSessionStatus(sessionID, conservative);
|
|
3033
|
+
return conservative;
|
|
3034
|
+
}
|
|
2955
3035
|
if (live?.type) {
|
|
2956
3036
|
if (await recoverCompletedTailWithoutActiveRun(directory, client, sessionID, live.type, seenAt))
|
|
2957
3037
|
return "idle";
|
|
@@ -2959,13 +3039,16 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2959
3039
|
const active = activeRuns.get(sessionID);
|
|
2960
3040
|
if (active) {
|
|
2961
3041
|
const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, active);
|
|
2962
|
-
if (completion === "
|
|
3042
|
+
if (settledAssistantCompletion(completion) || live.type === "busy" && completion === "unknown" && staleActiveRun(sessionID)) {
|
|
2963
3043
|
markSessionStatus(sessionID, "idle");
|
|
2964
|
-
|
|
3044
|
+
const logDetails = {
|
|
2965
3045
|
sessionID,
|
|
2966
3046
|
job: active.job?.name || active.jobId,
|
|
2967
|
-
startedAt: active.startedAt
|
|
2968
|
-
|
|
3047
|
+
startedAt: active.startedAt,
|
|
3048
|
+
...completion === "completed" ? {} : { staleStatus: live.type }
|
|
3049
|
+
};
|
|
3050
|
+
const recoveryEvent = completion === "empty" ? "status-message-empty-recovery" : completion === "completed" ? "status-message-complete-recovery" : "status-stale-recovery";
|
|
3051
|
+
await appendLoopLog2(directory, recoveryEvent, logDetails);
|
|
2969
3052
|
return "idle";
|
|
2970
3053
|
}
|
|
2971
3054
|
}
|
|
@@ -2987,6 +3070,7 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2987
3070
|
staleActiveRun,
|
|
2988
3071
|
canFinalizeActiveRun,
|
|
2989
3072
|
readLiveSessionStatus,
|
|
3073
|
+
activeRunCompletion: activeRunCompletionFromMessages2,
|
|
2990
3074
|
sessionStatusType,
|
|
2991
3075
|
sessionIsIdle
|
|
2992
3076
|
};
|
|
@@ -3181,6 +3265,58 @@ ${prompt}`;
|
|
|
3181
3265
|
return { fireAction };
|
|
3182
3266
|
}
|
|
3183
3267
|
|
|
3268
|
+
// src/source/runtime/terminal-guard.js
|
|
3269
|
+
function messageText(message) {
|
|
3270
|
+
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
3271
|
+
const fromParts = parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join(`
|
|
3272
|
+
`).trim();
|
|
3273
|
+
if (fromParts)
|
|
3274
|
+
return fromParts;
|
|
3275
|
+
const info = message?.info || message || {};
|
|
3276
|
+
for (const value of [info.text, info.content, info.summary]) {
|
|
3277
|
+
if (typeof value === "string" && value.trim())
|
|
3278
|
+
return value.trim();
|
|
3279
|
+
}
|
|
3280
|
+
return "";
|
|
3281
|
+
}
|
|
3282
|
+
async function applyTerminalContinuationGuard(directory, client, sessionID, job, options = {}) {
|
|
3283
|
+
if (job?.scheduleMode !== "idle" || !isCompletionBoundedContinuation(job?.action)) {
|
|
3284
|
+
return { job, terminal: false, pausedNow: false };
|
|
3285
|
+
}
|
|
3286
|
+
const messages = await readRecentSessionMessages(client, sessionID, directory, options.messageLimit || 8);
|
|
3287
|
+
if (!messages)
|
|
3288
|
+
return { job, terminal: false, pausedNow: false };
|
|
3289
|
+
const tail = orderedSessionMessages(messages).at(-1);
|
|
3290
|
+
const info = tail?.info || tail || {};
|
|
3291
|
+
if (info.role !== "assistant")
|
|
3292
|
+
return { job, terminal: false, pausedNow: false };
|
|
3293
|
+
const completed = Number(info?.time?.completed || 0);
|
|
3294
|
+
const created = Number(info?.time?.created || 0);
|
|
3295
|
+
const runStarted = Number(job?.lastRunAt || 0);
|
|
3296
|
+
if (!Number.isFinite(completed) || completed <= 0)
|
|
3297
|
+
return { job, terminal: false, pausedNow: false };
|
|
3298
|
+
if (runStarted > 0 && completed < runStarted && (!Number.isFinite(created) || created < runStarted)) {
|
|
3299
|
+
return { job, terminal: false, pausedNow: false };
|
|
3300
|
+
}
|
|
3301
|
+
const text = messageText(tail);
|
|
3302
|
+
const terminal = isTerminalNoWorkReply(text);
|
|
3303
|
+
if (!terminal) {
|
|
3304
|
+
if (job.terminalNoWorkCount)
|
|
3305
|
+
job.terminalNoWorkCount = 0;
|
|
3306
|
+
return { job, terminal: false, pausedNow: false, text };
|
|
3307
|
+
}
|
|
3308
|
+
job.terminalNoWorkCount = (job.terminalNoWorkCount || 0) + 1;
|
|
3309
|
+
job.lastTerminalNoWorkAt = Date.now();
|
|
3310
|
+
job.lastTerminalNoWorkSummary = text.slice(0, 1000);
|
|
3311
|
+
const threshold = Math.max(2, Number(options.threshold) || 2);
|
|
3312
|
+
const pausedNow = job.terminalNoWorkCount >= threshold && !job.paused;
|
|
3313
|
+
if (pausedNow) {
|
|
3314
|
+
job.paused = true;
|
|
3315
|
+
job.lastFailureReason = "terminal_no_work";
|
|
3316
|
+
}
|
|
3317
|
+
return { job, terminal: true, pausedNow, text };
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3184
3320
|
// src/source/runtime/run-finalization.js
|
|
3185
3321
|
function requireFunction8(value, label) {
|
|
3186
3322
|
if (typeof value !== "function")
|
|
@@ -3200,7 +3336,14 @@ function createRunFinalizationRuntime(options = {}) {
|
|
|
3200
3336
|
const toast2 = typeof options.toast === "function" ? options.toast : toast;
|
|
3201
3337
|
const writeGoalReport2 = typeof options.writeGoalReport === "function" ? options.writeGoalReport : writeGoalReport;
|
|
3202
3338
|
const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
|
|
3339
|
+
const applyTerminalContinuationGuard2 = typeof options.applyTerminalContinuationGuard === "function" ? options.applyTerminalContinuationGuard : applyTerminalContinuationGuard;
|
|
3203
3340
|
async function finalizeJob(directory, client, sessionID, state, job, previousJob) {
|
|
3341
|
+
if (job.infrastructureFailureCount) {
|
|
3342
|
+
job.infrastructureFailureCount = 0;
|
|
3343
|
+
job.lastInfrastructureFailure = "";
|
|
3344
|
+
job.lastInfrastructureError = "";
|
|
3345
|
+
job.lastInfrastructureFailureAt = 0;
|
|
3346
|
+
}
|
|
3204
3347
|
if (job.verifyCommand) {
|
|
3205
3348
|
const verify = await runShellCommand2(job.verifyCommand, directory, job.timeoutMs || 300000);
|
|
3206
3349
|
job.lastVerifyAt = now2();
|
|
@@ -3263,6 +3406,18 @@ exit=` + postrun.code + `
|
|
|
3263
3406
|
job = await runGoalChecks(directory, sessionID, job, client);
|
|
3264
3407
|
job = await applyGoalNoProgressGuard(directory, client, sessionID, job, previousJob);
|
|
3265
3408
|
}
|
|
3409
|
+
const terminal = await applyTerminalContinuationGuard2(directory, client, sessionID, job);
|
|
3410
|
+
job = terminal.job;
|
|
3411
|
+
if (terminal.pausedNow) {
|
|
3412
|
+
await appendLoopLog2(directory, "terminal-no-work", {
|
|
3413
|
+
sessionID,
|
|
3414
|
+
job: job.name || job.id,
|
|
3415
|
+
count: job.terminalNoWorkCount,
|
|
3416
|
+
summary: String(terminal.text || "").slice(0, 1000)
|
|
3417
|
+
});
|
|
3418
|
+
await notifyJob2(directory, job, "terminal_no_work");
|
|
3419
|
+
await toast2(client, "Loop paused: completion-bounded task reported complete with no work remaining twice.", "success");
|
|
3420
|
+
}
|
|
3266
3421
|
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate).filter((candidate) => candidate.enabled !== false || isGoalJob(candidate));
|
|
3267
3422
|
await writeState2(directory, sessionID, state);
|
|
3268
3423
|
if (isGoalJob(job))
|
|
@@ -3359,9 +3514,102 @@ exit=` + preflight.code + `
|
|
|
3359
3514
|
return { dueJobs: dueJobs2, admitJob };
|
|
3360
3515
|
}
|
|
3361
3516
|
|
|
3517
|
+
// src/source/runtime/network-recovery.js
|
|
3518
|
+
var TRANSIENT_NETWORK_PATTERNS = [
|
|
3519
|
+
/\b(?:408|425|429|500|502|503|504|524)\b/i,
|
|
3520
|
+
/rate[\s_-]?limit|too many requests|overloaded|service[\s_-]?unavailable|provider[_ -]?unavailable/i,
|
|
3521
|
+
/terminated|fetch failed|failed to fetch|network[\s_-]?error|network connection lost/i,
|
|
3522
|
+
/connection (?:error|refused|lost)|socket (?:hang up|connection was closed)|reset before headers/i,
|
|
3523
|
+
/\b(?:enotfound|eai_again|econnrefused|econnreset|etimedout|ehostunreach|enetunreach|epipe)\b/i,
|
|
3524
|
+
/\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i,
|
|
3525
|
+
/\btimeout(?:error)?\b/i
|
|
3526
|
+
];
|
|
3527
|
+
function errorText(value) {
|
|
3528
|
+
if (value instanceof Error)
|
|
3529
|
+
return `${value.name}: ${value.message}`;
|
|
3530
|
+
if (typeof value === "string")
|
|
3531
|
+
return value;
|
|
3532
|
+
try {
|
|
3533
|
+
return JSON.stringify(value);
|
|
3534
|
+
} catch {
|
|
3535
|
+
return String(value);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
function isTransientNetworkError(value) {
|
|
3539
|
+
const text = errorText(value);
|
|
3540
|
+
return TRANSIENT_NETWORK_PATTERNS.some((pattern) => pattern.test(text));
|
|
3541
|
+
}
|
|
3542
|
+
function networkRetryDelayMs(attempt, baseMs = 5000, maxMs = 60000) {
|
|
3543
|
+
const safeAttempt = Math.max(1, Math.floor(Number(attempt) || 1));
|
|
3544
|
+
const safeBase = Math.max(1, Math.floor(Number(baseMs) || 5000));
|
|
3545
|
+
const safeMax = Math.max(safeBase, Math.floor(Number(maxMs) || 60000));
|
|
3546
|
+
return Math.min(safeMax, safeBase * 2 ** Math.min(8, safeAttempt - 1));
|
|
3547
|
+
}
|
|
3548
|
+
function refundInfrastructureRun(job, snapshot = {}, input = {}) {
|
|
3549
|
+
const chargedCount = Number(snapshot.runCount ?? job.runCount ?? 0);
|
|
3550
|
+
const currentCount = Number(job.runCount || 0);
|
|
3551
|
+
if (chargedCount > 0 && currentCount >= chargedCount)
|
|
3552
|
+
job.runCount = Math.max(0, currentCount - 1);
|
|
3553
|
+
if (Number.isFinite(Number(snapshot.previousLastRunAt)))
|
|
3554
|
+
job.lastRunAt = Number(snapshot.previousLastRunAt);
|
|
3555
|
+
if (snapshot.disabledByMaxRuns && job.maxRuns > 0 && job.runCount < job.maxRuns)
|
|
3556
|
+
job.enabled = true;
|
|
3557
|
+
job.infrastructureFailureCount = (job.infrastructureFailureCount || 0) + 1;
|
|
3558
|
+
job.lastInfrastructureFailure = String(input.reason || "transient_network_failure").slice(0, 120);
|
|
3559
|
+
job.lastInfrastructureError = errorText(input.error).slice(0, 4000);
|
|
3560
|
+
job.lastInfrastructureFailureAt = Number(input.now || Date.now());
|
|
3561
|
+
return job;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
// src/source/runtime/empty-turn.js
|
|
3565
|
+
var DEFAULT_MAX_EMPTY_TURNS = 2;
|
|
3566
|
+
function guardsEmptyAssistantTurn(job) {
|
|
3567
|
+
const kind = actionKind(job?.action, job || {});
|
|
3568
|
+
return kind === "prompt" || kind === "goal";
|
|
3569
|
+
}
|
|
3570
|
+
function emptyTurnLimit(job) {
|
|
3571
|
+
const configured = Number(job?.maxEmptyTurns || 0);
|
|
3572
|
+
if (Number.isFinite(configured) && configured > 0)
|
|
3573
|
+
return Math.max(1, Math.floor(configured));
|
|
3574
|
+
return DEFAULT_MAX_EMPTY_TURNS;
|
|
3575
|
+
}
|
|
3576
|
+
function refundEmptyAssistantTurn(job, active = {}, timestamp = Date.now()) {
|
|
3577
|
+
const chargedCount = Number(active?.job?.runCount ?? job?.runCount ?? 0);
|
|
3578
|
+
const currentCount = Number(job?.runCount || 0);
|
|
3579
|
+
if (chargedCount > 0 && currentCount >= chargedCount)
|
|
3580
|
+
job.runCount = Math.max(0, currentCount - 1);
|
|
3581
|
+
if (Number.isFinite(Number(active?.previousLastRunAt)))
|
|
3582
|
+
job.lastRunAt = Number(active.previousLastRunAt);
|
|
3583
|
+
if (active?.disabledByMaxRuns && Number(job?.maxRuns || 0) > 0 && Number(job?.runCount || 0) < Number(job.maxRuns)) {
|
|
3584
|
+
job.enabled = true;
|
|
3585
|
+
}
|
|
3586
|
+
job.emptyTurnCount = Number(job.emptyTurnCount || 0) + 1;
|
|
3587
|
+
job.lastEmptyTurnAt = Number(timestamp) || Date.now();
|
|
3588
|
+
job.lastFailureReason = "empty_turn";
|
|
3589
|
+
const limit = emptyTurnLimit(job);
|
|
3590
|
+
const paused = job.emptyTurnCount >= limit;
|
|
3591
|
+
if (paused) {
|
|
3592
|
+
job.paused = true;
|
|
3593
|
+
delete job.runNowRequestedAt;
|
|
3594
|
+
} else {
|
|
3595
|
+
job.runNowRequestedAt = Math.max(1, Number(timestamp) || Date.now());
|
|
3596
|
+
}
|
|
3597
|
+
return { job, paused, count: job.emptyTurnCount, limit };
|
|
3598
|
+
}
|
|
3599
|
+
function clearEmptyAssistantTurnStreak(job) {
|
|
3600
|
+
if (!job)
|
|
3601
|
+
return job;
|
|
3602
|
+
job.emptyTurnCount = 0;
|
|
3603
|
+
if (job.lastFailureReason === "empty_turn")
|
|
3604
|
+
delete job.lastFailureReason;
|
|
3605
|
+
return job;
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3362
3608
|
// src/source/runtime/executor.js
|
|
3363
3609
|
var DEFAULT_ACTIVE_GUARD_MS = 45000;
|
|
3364
3610
|
var DEFAULT_BUSY_RETRY_MS2 = 5000;
|
|
3611
|
+
var DEFAULT_PROVIDER_RETRY_WATCHDOG_MS = 2 * 60000;
|
|
3612
|
+
var DEFAULT_NETWORK_RETRY_MAX_MS = 60000;
|
|
3365
3613
|
function requireFunction10(value, label) {
|
|
3366
3614
|
if (typeof value !== "function")
|
|
3367
3615
|
throw new TypeError(`createLoopExecutor requires ${label}`);
|
|
@@ -3393,8 +3641,11 @@ function createLoopExecutor(options = {}) {
|
|
|
3393
3641
|
const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
|
|
3394
3642
|
const activeGuardMs = Number.isFinite(Number(options.activeGuardMs)) && Number(options.activeGuardMs) > 0 ? Number(options.activeGuardMs) : DEFAULT_ACTIVE_GUARD_MS;
|
|
3395
3643
|
const busyRetryMs = Number.isFinite(Number(options.busyRetryMs)) && Number(options.busyRetryMs) > 0 ? Number(options.busyRetryMs) : DEFAULT_BUSY_RETRY_MS2;
|
|
3644
|
+
const providerRetryWatchdogMs = Number.isFinite(Number(options.providerRetryWatchdogMs)) && Number(options.providerRetryWatchdogMs) > 0 ? Number(options.providerRetryWatchdogMs) : DEFAULT_PROVIDER_RETRY_WATCHDOG_MS;
|
|
3645
|
+
const networkRetryMaxMs = Number.isFinite(Number(options.networkRetryMaxMs)) && Number(options.networkRetryMaxMs) > 0 ? Number(options.networkRetryMaxMs) : DEFAULT_NETWORK_RETRY_MAX_MS;
|
|
3396
3646
|
const activeRuns = new Map;
|
|
3397
3647
|
const runLocks = new Map;
|
|
3648
|
+
const retryRuns = new Map;
|
|
3398
3649
|
const statusRuntime = createSessionStatusRuntime({
|
|
3399
3650
|
activeRuns,
|
|
3400
3651
|
appendLoopLog: appendLoopLog2,
|
|
@@ -3407,6 +3658,8 @@ function createLoopExecutor(options = {}) {
|
|
|
3407
3658
|
updateSessionStatusFromEvent,
|
|
3408
3659
|
staleActiveRun,
|
|
3409
3660
|
canFinalizeActiveRun,
|
|
3661
|
+
activeRunCompletion,
|
|
3662
|
+
sessionStatusType,
|
|
3410
3663
|
sessionIsIdle,
|
|
3411
3664
|
markSessionStatus,
|
|
3412
3665
|
clearSessionStatus
|
|
@@ -3465,6 +3718,7 @@ function createLoopExecutor(options = {}) {
|
|
|
3465
3718
|
clearTimeout(active.timer);
|
|
3466
3719
|
compactionRuntime.clearForActiveRun(sessionID, active);
|
|
3467
3720
|
activeRuns.delete(sessionID);
|
|
3721
|
+
retryRuns.delete(sessionID);
|
|
3468
3722
|
}
|
|
3469
3723
|
function disposeSession(sessionID) {
|
|
3470
3724
|
clearActiveRun(sessionID);
|
|
@@ -3472,30 +3726,106 @@ function createLoopExecutor(options = {}) {
|
|
|
3472
3726
|
compactionRuntime.clear(sessionID);
|
|
3473
3727
|
clearSessionStatus(sessionID);
|
|
3474
3728
|
}
|
|
3729
|
+
async function persistInfrastructureRefund(directory, sessionID, active, input = {}) {
|
|
3730
|
+
const state = await readState2(directory, sessionID);
|
|
3731
|
+
const job = (state.jobs || []).find((candidate) => candidate.id === active.jobId);
|
|
3732
|
+
if (!job)
|
|
3733
|
+
return { job: undefined, delayMs: busyRetryMs };
|
|
3734
|
+
refundInfrastructureRun(job, {
|
|
3735
|
+
runCount: active.job?.runCount,
|
|
3736
|
+
previousLastRunAt: active.previousLastRunAt,
|
|
3737
|
+
disabledByMaxRuns: active.disabledByMaxRuns
|
|
3738
|
+
}, {
|
|
3739
|
+
reason: input.reason,
|
|
3740
|
+
error: input.error,
|
|
3741
|
+
now: now2()
|
|
3742
|
+
});
|
|
3743
|
+
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3744
|
+
await writeState2(directory, sessionID, state);
|
|
3745
|
+
const delayMs = networkRetryDelayMs(job.infrastructureFailureCount || 1, busyRetryMs, networkRetryMaxMs);
|
|
3746
|
+
return { job, delayMs };
|
|
3747
|
+
}
|
|
3475
3748
|
async function recoverActiveDispatchFailure(directory, client, sessionID, jobId, runToken, error) {
|
|
3476
3749
|
const active = activeRuns.get(sessionID);
|
|
3477
3750
|
if (!active || active.jobId !== jobId || active.runToken !== runToken)
|
|
3478
3751
|
return false;
|
|
3752
|
+
const transient = isTransientNetworkError(error);
|
|
3753
|
+
const snapshot = active;
|
|
3479
3754
|
clearActiveRun(sessionID);
|
|
3480
3755
|
clearSessionStatus(sessionID);
|
|
3481
3756
|
const message = errorMessage(error);
|
|
3482
|
-
|
|
3483
|
-
|
|
3757
|
+
let state = await readState2(directory, sessionID);
|
|
3758
|
+
let job = (state.jobs || []).find((candidate) => candidate.id === jobId);
|
|
3759
|
+
let retryDelay = busyRetryMs;
|
|
3484
3760
|
if (job) {
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
job
|
|
3491
|
-
|
|
3761
|
+
if (transient) {
|
|
3762
|
+
const refunded = await persistInfrastructureRefund(directory, sessionID, snapshot, {
|
|
3763
|
+
reason: "dispatch_failed_transient",
|
|
3764
|
+
error
|
|
3765
|
+
});
|
|
3766
|
+
job = refunded.job;
|
|
3767
|
+
retryDelay = refunded.delayMs;
|
|
3768
|
+
state = await readState2(directory, sessionID);
|
|
3769
|
+
} else {
|
|
3770
|
+
job.failureCount = (job.failureCount || 0) + 1;
|
|
3771
|
+
job.lastFailureReason = "dispatch_failed";
|
|
3772
|
+
job.lastDispatchFailure = message.slice(0, 4000);
|
|
3773
|
+
job.lastDispatchFailureAt = now2();
|
|
3774
|
+
if (job.maxFailures > 0 && job.failureCount >= job.maxFailures) {
|
|
3775
|
+
job.paused = true;
|
|
3776
|
+
await notifyJob2(directory, job, "dispatch_failed");
|
|
3777
|
+
}
|
|
3778
|
+
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3779
|
+
await writeState2(directory, sessionID, state);
|
|
3492
3780
|
}
|
|
3493
|
-
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3494
|
-
await writeState2(directory, sessionID, state);
|
|
3495
3781
|
}
|
|
3496
|
-
await appendLoopLog2(directory, "dispatch-error"
|
|
3497
|
-
|
|
3498
|
-
|
|
3782
|
+
await appendLoopLog2(directory, transient ? "network-dispatch-error" : "dispatch-error", {
|
|
3783
|
+
sessionID,
|
|
3784
|
+
job: job?.name || jobId,
|
|
3785
|
+
error: message,
|
|
3786
|
+
...transient ? { retryInMs: retryDelay } : {}
|
|
3787
|
+
});
|
|
3788
|
+
if (transient) {
|
|
3789
|
+
await toast2(client, `Loop network dispatch failed; retrying when the session recovers: ${message}`, "warning");
|
|
3790
|
+
} else {
|
|
3791
|
+
await toast2(client, `Loop dispatch failed${job?.paused ? " and paused" : ""}: ${message}`, job?.paused ? "error" : "warning");
|
|
3792
|
+
}
|
|
3793
|
+
await scheduleDueWork(directory, client, sessionID, retryDelay);
|
|
3794
|
+
return true;
|
|
3795
|
+
}
|
|
3796
|
+
async function recoverStuckProviderRetry(directory, client, sessionID) {
|
|
3797
|
+
const active = activeRuns.get(sessionID);
|
|
3798
|
+
if (!active || active.compactionOnly) {
|
|
3799
|
+
retryRuns.delete(sessionID);
|
|
3800
|
+
return false;
|
|
3801
|
+
}
|
|
3802
|
+
const current = retryRuns.get(sessionID);
|
|
3803
|
+
if (!current || current.runToken !== active.runToken) {
|
|
3804
|
+
retryRuns.set(sessionID, { runToken: active.runToken, since: now2() });
|
|
3805
|
+
return false;
|
|
3806
|
+
}
|
|
3807
|
+
if (now2() - current.since < providerRetryWatchdogMs)
|
|
3808
|
+
return false;
|
|
3809
|
+
const snapshot = active;
|
|
3810
|
+
try {
|
|
3811
|
+
if (client?.session?.abort) {
|
|
3812
|
+
await fireSdk2(client, "session.abort provider retry watchdog", client.session.abort.bind(client.session), { path: { id: sessionID }, body: {} }, { path: { sessionID }, body: {} }, { sessionID });
|
|
3813
|
+
}
|
|
3814
|
+
} catch {}
|
|
3815
|
+
clearActiveRun(sessionID);
|
|
3816
|
+
clearSessionStatus(sessionID);
|
|
3817
|
+
const refunded = await persistInfrastructureRefund(directory, sessionID, snapshot, {
|
|
3818
|
+
reason: "provider_retry_watchdog",
|
|
3819
|
+
error: `OpenCode session.status stayed retry for ${providerRetryWatchdogMs}ms`
|
|
3820
|
+
});
|
|
3821
|
+
await appendLoopLog2(directory, "provider-retry-recovery", {
|
|
3822
|
+
sessionID,
|
|
3823
|
+
job: refunded.job?.name || snapshot.jobId,
|
|
3824
|
+
retryForMs: now2() - current.since,
|
|
3825
|
+
retryInMs: refunded.delayMs
|
|
3826
|
+
});
|
|
3827
|
+
await toast2(client, "Loop provider retry exceeded the watchdog; the Loop-owned turn was released and will retry with backoff.", "warning");
|
|
3828
|
+
await scheduleDueWork(directory, client, sessionID, refunded.delayMs);
|
|
3499
3829
|
return true;
|
|
3500
3830
|
}
|
|
3501
3831
|
async function finalizeActiveRun(directory, client, sessionID, finalizeOptions = {}) {
|
|
@@ -3504,6 +3834,7 @@ function createLoopExecutor(options = {}) {
|
|
|
3504
3834
|
return;
|
|
3505
3835
|
if (!await canFinalizeActiveRun(directory, client, sessionID, active, finalizeOptions))
|
|
3506
3836
|
return false;
|
|
3837
|
+
const completion = await activeRunCompletion(directory, client, sessionID, active);
|
|
3507
3838
|
const recoveredStale = staleActiveRun(sessionID);
|
|
3508
3839
|
if (active.compactionOnly) {
|
|
3509
3840
|
const pending = compactionRuntime.getPending(sessionID);
|
|
@@ -3524,6 +3855,30 @@ function createLoopExecutor(options = {}) {
|
|
|
3524
3855
|
if (!job)
|
|
3525
3856
|
return;
|
|
3526
3857
|
job.lastFinishedAt = now2();
|
|
3858
|
+
if (completion === "empty" && guardsEmptyAssistantTurn(job)) {
|
|
3859
|
+
const empty = refundEmptyAssistantTurn(job, active, now2());
|
|
3860
|
+
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3861
|
+
await writeState2(directory, sessionID, state);
|
|
3862
|
+
await appendLoopLog2(directory, "empty-assistant-turn", {
|
|
3863
|
+
sessionID,
|
|
3864
|
+
job: job.name || job.id,
|
|
3865
|
+
count: empty.count,
|
|
3866
|
+
limit: empty.limit,
|
|
3867
|
+
paused: empty.paused,
|
|
3868
|
+
refunded: true
|
|
3869
|
+
});
|
|
3870
|
+
if (empty.paused) {
|
|
3871
|
+
await notifyJob2(directory, job, "empty_turn");
|
|
3872
|
+
await toast2(client, "Loop paused after " + empty.count + " consecutive completed assistant turns with no visible output or tool activity. Resume after changing the model/prompt or use /loop-resume.", "warning");
|
|
3873
|
+
await scheduleDueWork(directory, client, sessionID);
|
|
3874
|
+
} else {
|
|
3875
|
+
await toast2(client, "Loop received an empty completed assistant turn; the logical run was refunded and will retry once.", "warning");
|
|
3876
|
+
await scheduleDueWork(directory, client, sessionID, busyRetryMs);
|
|
3877
|
+
}
|
|
3878
|
+
return true;
|
|
3879
|
+
}
|
|
3880
|
+
if (completion === "completed")
|
|
3881
|
+
clearEmptyAssistantTurnStreak(job);
|
|
3527
3882
|
if (recoveredStale) {
|
|
3528
3883
|
await appendLoopLog2(directory, "active-stale-recovery", {
|
|
3529
3884
|
sessionID,
|
|
@@ -3546,14 +3901,24 @@ function createLoopExecutor(options = {}) {
|
|
|
3546
3901
|
}
|
|
3547
3902
|
runLocks.set(sessionID, now2());
|
|
3548
3903
|
let job;
|
|
3904
|
+
let previousLastRunAt = 0;
|
|
3905
|
+
let disabledByMaxRuns = false;
|
|
3549
3906
|
try {
|
|
3550
3907
|
await finalizeActiveRun(directory, client, sessionID, { requireIdle: true, forceStale: true });
|
|
3551
|
-
|
|
3908
|
+
const statusType = await sessionStatusType(client, sessionID, directory);
|
|
3909
|
+
if (statusType !== "idle") {
|
|
3910
|
+
if (statusType === "retry") {
|
|
3911
|
+
if (await recoverStuckProviderRetry(directory, client, sessionID))
|
|
3912
|
+
return;
|
|
3913
|
+
} else {
|
|
3914
|
+
retryRuns.delete(sessionID);
|
|
3915
|
+
}
|
|
3552
3916
|
if (runOptions.force)
|
|
3553
3917
|
await toast2(client, "Loop queued: session is busy; it will run on the next idle check.", "info");
|
|
3554
3918
|
await reschedule(busyRetryMs);
|
|
3555
3919
|
return;
|
|
3556
3920
|
}
|
|
3921
|
+
retryRuns.delete(sessionID);
|
|
3557
3922
|
const active = activeRuns.get(sessionID);
|
|
3558
3923
|
const activeAge = active ? now2() - (active.startedAt || 0) : 0;
|
|
3559
3924
|
const activeGuard = active?.job?.timeoutMs || active?.job?.activeRecoveryMs || activeGuardMs;
|
|
@@ -3607,9 +3972,11 @@ function createLoopExecutor(options = {}) {
|
|
|
3607
3972
|
if (runNowRequested)
|
|
3608
3973
|
delete job.runNowRequestedAt;
|
|
3609
3974
|
job.watchTriggered = false;
|
|
3975
|
+
previousLastRunAt = Number(job.lastRunAt || 0);
|
|
3610
3976
|
job.lastRunAt = now2();
|
|
3611
3977
|
job.runCount = (job.runCount || 0) + 1;
|
|
3612
|
-
|
|
3978
|
+
disabledByMaxRuns = job.maxRuns > 0 && job.runCount >= job.maxRuns;
|
|
3979
|
+
if (disabledByMaxRuns) {
|
|
3613
3980
|
job.enabled = false;
|
|
3614
3981
|
await notifyJob2(directory, job, "max_runs_reached");
|
|
3615
3982
|
}
|
|
@@ -3648,7 +4015,9 @@ function createLoopExecutor(options = {}) {
|
|
|
3648
4015
|
startedAt: now2(),
|
|
3649
4016
|
timer,
|
|
3650
4017
|
runToken,
|
|
3651
|
-
compactionAction: result.compaction === true
|
|
4018
|
+
compactionAction: result.compaction === true,
|
|
4019
|
+
previousLastRunAt,
|
|
4020
|
+
disabledByMaxRuns
|
|
3652
4021
|
});
|
|
3653
4022
|
if (result.compaction && compactionRuntime.isCompleted(sessionID, job.id)) {
|
|
3654
4023
|
await compactionRuntime.finalize(directory, client, sessionID);
|
|
@@ -3663,6 +4032,24 @@ function createLoopExecutor(options = {}) {
|
|
|
3663
4032
|
await reschedule(busyRetryMs);
|
|
3664
4033
|
} catch (error) {
|
|
3665
4034
|
clearActiveRun(sessionID);
|
|
4035
|
+
if (isTransientNetworkError(error) && job) {
|
|
4036
|
+
const stateAfterFailure = await readState2(directory, sessionID);
|
|
4037
|
+
const persisted = (stateAfterFailure.jobs || []).find((candidate) => candidate.id === job.id);
|
|
4038
|
+
if (persisted) {
|
|
4039
|
+
refundInfrastructureRun(persisted, {
|
|
4040
|
+
runCount: job.runCount,
|
|
4041
|
+
previousLastRunAt,
|
|
4042
|
+
disabledByMaxRuns
|
|
4043
|
+
}, { reason: "action_dispatch_transient", error, now: now2() });
|
|
4044
|
+
stateAfterFailure.jobs = (stateAfterFailure.jobs || []).map((candidate) => candidate.id === persisted.id ? persisted : candidate);
|
|
4045
|
+
await writeState2(directory, sessionID, stateAfterFailure);
|
|
4046
|
+
const delayMs = networkRetryDelayMs(persisted.infrastructureFailureCount || 1, busyRetryMs, networkRetryMaxMs);
|
|
4047
|
+
await appendLoopLog2(directory, "network-action-error", { sessionID, job: persisted.name || persisted.id, error: errorMessage(error), retryInMs: delayMs });
|
|
4048
|
+
await toast2(client, "Loop action hit a transient network failure; the logical run was refunded and will retry.", "warning");
|
|
4049
|
+
await reschedule(delayMs);
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
3666
4053
|
await toast2(client, `Loop job failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3667
4054
|
await appendLoopLog2(directory, "error", {
|
|
3668
4055
|
sessionID,
|
|
@@ -3680,9 +4067,11 @@ function createLoopExecutor(options = {}) {
|
|
|
3680
4067
|
clearActiveRun,
|
|
3681
4068
|
disposeSession,
|
|
3682
4069
|
recoverActiveDispatchFailure,
|
|
4070
|
+
recoverStuckProviderRetry,
|
|
3683
4071
|
finalizeActiveRun,
|
|
3684
4072
|
fireAction,
|
|
3685
4073
|
maybeRunDueJobs,
|
|
4074
|
+
sessionStatusType,
|
|
3686
4075
|
sessionIsIdle,
|
|
3687
4076
|
updateSessionStatusFromEvent,
|
|
3688
4077
|
markSessionStatus,
|