opencode-auto-resume 1.1.9 → 1.1.10
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/index.js +159 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12439,13 +12439,17 @@ var DONE_CLAIM_PATTERNS = [
|
|
|
12439
12439
|
/^task\s+completed[.!]*$/im,
|
|
12440
12440
|
/^all\s+tasks?\s+complete[.!]*$/im,
|
|
12441
12441
|
/^all\s+tasks?\s+completed[.!]*$/im,
|
|
12442
|
-
/^(?:i['']?m\s+)?done\s+with\s+task/im
|
|
12442
|
+
/^(?:i['']?m\s+)?done\s+with\s+task/im,
|
|
12443
|
+
/\bdone\s+with\s+(?:the\s+)?(?:task|work|implementation)/im,
|
|
12444
|
+
/\bfinished\s+(?:the\s+)?(?:task|work|implementation)/im,
|
|
12445
|
+
/\b(?:all|everything)\s+(?:is\s+)?(?:complete|done|finished)/im,
|
|
12446
|
+
/\bnothing\s+(?:else\s+)?(?:left|remaining|to do)/im
|
|
12443
12447
|
];
|
|
12444
12448
|
var DONE_WITHOUT_WORK_PROMPT = "I need you to verify more carefully that you have actually completed all the required tasks. " + "Your response indicated you're done, but no work was detected. Please check your todo list " + "and complete any remaining work.";
|
|
12445
12449
|
function containsDoneClaimPattern(text) {
|
|
12446
12450
|
const lines = text.split(`
|
|
12447
12451
|
`);
|
|
12448
|
-
const lastLines = lines.slice(-
|
|
12452
|
+
const lastLines = lines.slice(-5).join(`
|
|
12449
12453
|
`);
|
|
12450
12454
|
return DONE_CLAIM_PATTERNS.some((pat) => pat.test(lastLines));
|
|
12451
12455
|
}
|
|
@@ -12592,7 +12596,22 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12592
12596
|
recordContinue(sid);
|
|
12593
12597
|
return w.continueTimestamps.length >= loopMaxContinues;
|
|
12594
12598
|
}
|
|
12599
|
+
const recentLogMsgs = new Map;
|
|
12600
|
+
const LOG_DEDUP_WINDOW_MS = 5000;
|
|
12595
12601
|
async function log(level, msg) {
|
|
12602
|
+
if (level === "debug") {
|
|
12603
|
+
const key = `${level}:${msg}`;
|
|
12604
|
+
const now = Date.now();
|
|
12605
|
+
const last = recentLogMsgs.get(key);
|
|
12606
|
+
if (last && now - last < LOG_DEDUP_WINDOW_MS)
|
|
12607
|
+
return;
|
|
12608
|
+
recentLogMsgs.set(key, now);
|
|
12609
|
+
if (recentLogMsgs.size > 200) {
|
|
12610
|
+
const oldest = [...recentLogMsgs.entries()].sort((a, b) => a[1] - b[1]);
|
|
12611
|
+
for (let i = 0;i < 100; i++)
|
|
12612
|
+
recentLogMsgs.delete(oldest[i][0]);
|
|
12613
|
+
}
|
|
12614
|
+
}
|
|
12596
12615
|
try {
|
|
12597
12616
|
await ctx.client.app.log({ body: { service: "auto-resume", level, message: msg } });
|
|
12598
12617
|
} catch (e) {
|
|
@@ -12782,6 +12801,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12782
12801
|
await log("debug", `${short(sid)} - continue already in progress, skipping`);
|
|
12783
12802
|
return;
|
|
12784
12803
|
}
|
|
12804
|
+
if (w.userCancelled || w.completionSignaled)
|
|
12805
|
+
return;
|
|
12785
12806
|
if (!w.continuing)
|
|
12786
12807
|
dbg(`State transition on ${short(sid)}: continuing=false -> true`);
|
|
12787
12808
|
w.continuing = true;
|
|
@@ -12917,9 +12938,21 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12917
12938
|
return response.messages;
|
|
12918
12939
|
return [];
|
|
12919
12940
|
}
|
|
12941
|
+
const messagesInflight = new Map;
|
|
12920
12942
|
async function getSessionMessages(sid) {
|
|
12921
|
-
const
|
|
12922
|
-
|
|
12943
|
+
const inflight = messagesInflight.get(sid);
|
|
12944
|
+
if (inflight)
|
|
12945
|
+
return inflight;
|
|
12946
|
+
const p = (async () => {
|
|
12947
|
+
try {
|
|
12948
|
+
const response = await ctx.client.session.messages({ path: { id: sid } });
|
|
12949
|
+
return extractMessages(response);
|
|
12950
|
+
} finally {
|
|
12951
|
+
messagesInflight.delete(sid);
|
|
12952
|
+
}
|
|
12953
|
+
})();
|
|
12954
|
+
messagesInflight.set(sid, p);
|
|
12955
|
+
return p;
|
|
12923
12956
|
}
|
|
12924
12957
|
function roleOf(msg) {
|
|
12925
12958
|
if (!msg)
|
|
@@ -12971,6 +13004,43 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12971
13004
|
return {};
|
|
12972
13005
|
}
|
|
12973
13006
|
}
|
|
13007
|
+
const todoCache = new Map;
|
|
13008
|
+
const todoInflight = new Map;
|
|
13009
|
+
const TODO_CACHE_TTL_MS = 2000;
|
|
13010
|
+
async function fetchSessionTodos(sid) {
|
|
13011
|
+
if (typeof sid !== "string" || !sid.startsWith("ses"))
|
|
13012
|
+
return [];
|
|
13013
|
+
const cached2 = todoCache.get(sid);
|
|
13014
|
+
if (cached2 && Date.now() - cached2.fetchedAt < TODO_CACHE_TTL_MS)
|
|
13015
|
+
return cached2.todos;
|
|
13016
|
+
const inflight = todoInflight.get(sid);
|
|
13017
|
+
if (inflight)
|
|
13018
|
+
return inflight;
|
|
13019
|
+
const p = (async () => {
|
|
13020
|
+
try {
|
|
13021
|
+
const todoFn = ctx.client.session.todo;
|
|
13022
|
+
if (typeof todoFn !== "function")
|
|
13023
|
+
return [];
|
|
13024
|
+
const response = await todoFn.call(ctx.client.session, { path: { id: sid } });
|
|
13025
|
+
const rawTodos = response.data ?? response;
|
|
13026
|
+
if (!Array.isArray(rawTodos))
|
|
13027
|
+
return [];
|
|
13028
|
+
const todos = rawTodos.map((t) => ({
|
|
13029
|
+
content: t?.content ?? "",
|
|
13030
|
+
status: t?.status ?? "pending",
|
|
13031
|
+
priority: t?.priority ?? "medium"
|
|
13032
|
+
}));
|
|
13033
|
+
todoCache.set(sid, { todos, fetchedAt: Date.now() });
|
|
13034
|
+
return todos;
|
|
13035
|
+
} catch {
|
|
13036
|
+
return [];
|
|
13037
|
+
} finally {
|
|
13038
|
+
todoInflight.delete(sid);
|
|
13039
|
+
}
|
|
13040
|
+
})();
|
|
13041
|
+
todoInflight.set(sid, p);
|
|
13042
|
+
return p;
|
|
13043
|
+
}
|
|
12974
13044
|
const SUBAGENT_STUCK_MS = 60000;
|
|
12975
13045
|
const SUBAGENT_RECOVERY_PROMPT = "It looks like you may have stalled or timed out. Please retry the last operation or continue with the task.";
|
|
12976
13046
|
async function recoverSubagent(subagentSid) {
|
|
@@ -13096,6 +13166,34 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13096
13166
|
w.recoveryAttempts = 0;
|
|
13097
13167
|
w.watchdogRetryGuard = false;
|
|
13098
13168
|
}
|
|
13169
|
+
function resetBusyFlags(w) {
|
|
13170
|
+
w.resumeAttempts = 0;
|
|
13171
|
+
w.lastRetryAt = 0;
|
|
13172
|
+
w.pendingTools = 0;
|
|
13173
|
+
w.pendingCommands = 0;
|
|
13174
|
+
w.gaveUp = false;
|
|
13175
|
+
w.orphanWatchStartAt = null;
|
|
13176
|
+
w.aborting = false;
|
|
13177
|
+
w.toolTextRecovered = false;
|
|
13178
|
+
w.toolTextAttempts = 0;
|
|
13179
|
+
w.todoCheckAttempts = 0;
|
|
13180
|
+
w.checkingToolText = false;
|
|
13181
|
+
w.interruptedContinueCount = 0;
|
|
13182
|
+
w.recentToolCalls = [];
|
|
13183
|
+
w.toolLoopAttempts = 0;
|
|
13184
|
+
w.pendingRecovery = false;
|
|
13185
|
+
w.pendingRecoveryReason = null;
|
|
13186
|
+
w.pendingRecoveryAt = 0;
|
|
13187
|
+
w.recoveryAttempts = 0;
|
|
13188
|
+
w.watchdogRetryGuard = false;
|
|
13189
|
+
if (w.toolTextTimer) {
|
|
13190
|
+
clearTimeout(w.toolTextTimer);
|
|
13191
|
+
w.toolTextTimer = null;
|
|
13192
|
+
}
|
|
13193
|
+
w.todoNudgeAttempts = 0;
|
|
13194
|
+
w.doneClaimNoTodosAttempts = 0;
|
|
13195
|
+
w.continueTimestamps = [];
|
|
13196
|
+
}
|
|
13099
13197
|
function resetIdleFlags(w) {
|
|
13100
13198
|
w.aborting = false;
|
|
13101
13199
|
w.orphanWatchStartAt = null;
|
|
@@ -13331,14 +13429,19 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13331
13429
|
const trimmedText = allAssistantText.trim();
|
|
13332
13430
|
const normalized = trimmedText.replace(/[.!?]+$/, "");
|
|
13333
13431
|
if (normalized.endsWith("\uD83C\uDF89") && (!bestCandidate || bestCandidate.priority > 0)) {
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
w.
|
|
13432
|
+
const openCount = getOpenTodos(w.todos || []).length;
|
|
13433
|
+
if (openCount > 0) {
|
|
13434
|
+
await log("info", `${short(sid)} - \uD83C\uDF89 detected but ${openCount} open todos remain, NOT latching completion`);
|
|
13435
|
+
} else {
|
|
13436
|
+
await log("info", `${short(sid)} - \uD83C\uDF89 completion detected, skipping continue`);
|
|
13437
|
+
w.toolTextRecovered = true;
|
|
13438
|
+
w.completionSignaled = true;
|
|
13439
|
+
if (w.toolTextTimer) {
|
|
13440
|
+
clearTimeout(w.toolTextTimer);
|
|
13441
|
+
w.toolTextTimer = null;
|
|
13442
|
+
}
|
|
13443
|
+
return;
|
|
13340
13444
|
}
|
|
13341
|
-
return;
|
|
13342
13445
|
}
|
|
13343
13446
|
if (!bestCandidate) {
|
|
13344
13447
|
const todos = w.todos || [];
|
|
@@ -13412,9 +13515,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13412
13515
|
await log("warn", `Invalid sid for abort: ${sid} (must start with "ses_")`);
|
|
13413
13516
|
return false;
|
|
13414
13517
|
}
|
|
13518
|
+
if (w.userCancelled || w.completionSignaled)
|
|
13519
|
+
return false;
|
|
13415
13520
|
if (w.aborting)
|
|
13416
13521
|
return false;
|
|
13417
|
-
w.aborting = true;
|
|
13418
13522
|
const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
|
|
13419
13523
|
await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
|
|
13420
13524
|
try {
|
|
@@ -13500,7 +13604,12 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13500
13604
|
w.idleSince = Date.now();
|
|
13501
13605
|
}
|
|
13502
13606
|
if (isNew) {
|
|
13503
|
-
log("debug", `Discovered session ${short(sid)} via list()`);
|
|
13607
|
+
log("debug", `Discovered session ${short(sid)} via list() \u2014 fetching todos`);
|
|
13608
|
+
const fetched = await fetchSessionTodos(sid);
|
|
13609
|
+
if (fetched.length > 0) {
|
|
13610
|
+
const w = sessions.get(sid);
|
|
13611
|
+
w.todos = fetched;
|
|
13612
|
+
}
|
|
13504
13613
|
}
|
|
13505
13614
|
}
|
|
13506
13615
|
}
|
|
@@ -13526,7 +13635,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13526
13635
|
}
|
|
13527
13636
|
if (w.status !== "busy")
|
|
13528
13637
|
continue;
|
|
13529
|
-
if (w.userCancelled)
|
|
13638
|
+
if (w.userCancelled || w.completionSignaled)
|
|
13530
13639
|
continue;
|
|
13531
13640
|
if (w.aborting)
|
|
13532
13641
|
continue;
|
|
@@ -13666,6 +13775,12 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13666
13775
|
continue;
|
|
13667
13776
|
if (busyCount() !== 0)
|
|
13668
13777
|
continue;
|
|
13778
|
+
if ((w.todos || []).length === 0) {
|
|
13779
|
+
const fetched = await fetchSessionTodos(sid);
|
|
13780
|
+
if (fetched.length > 0) {
|
|
13781
|
+
w.todos = fetched;
|
|
13782
|
+
}
|
|
13783
|
+
}
|
|
13669
13784
|
const open = getOpenTodos(w.todos || []);
|
|
13670
13785
|
if (open.length === 0)
|
|
13671
13786
|
continue;
|
|
@@ -13677,8 +13792,13 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13677
13792
|
continue;
|
|
13678
13793
|
const isCelebration = await lastAssistantEndsWithCelebration(sid);
|
|
13679
13794
|
if (isCelebration) {
|
|
13680
|
-
w.
|
|
13681
|
-
|
|
13795
|
+
const openCount = getOpenTodos(w.todos || []).length;
|
|
13796
|
+
if (openCount > 0) {
|
|
13797
|
+
await log("info", `${short(sid)} - \uD83C\uDF89 detected in periodic recheck but ${openCount} open todos remain, NOT latching completion`);
|
|
13798
|
+
} else {
|
|
13799
|
+
w.toolTextRecovered = true;
|
|
13800
|
+
w.completionSignaled = true;
|
|
13801
|
+
}
|
|
13682
13802
|
continue;
|
|
13683
13803
|
}
|
|
13684
13804
|
const reminder = buildOpenTodosReminder(w.todos || []);
|
|
@@ -13719,7 +13839,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13719
13839
|
if (w.pendingRecovery) {
|
|
13720
13840
|
dbg(`Pending recovery cleared on ${short(sid)}: reason=session-busy`);
|
|
13721
13841
|
}
|
|
13722
|
-
|
|
13842
|
+
resetBusyFlags(w);
|
|
13723
13843
|
prevBusyCount = busyCount();
|
|
13724
13844
|
log("debug", `${short(sid)} -> busy (${prevBusyCount})`);
|
|
13725
13845
|
} else if (statusType === "interrupted") {
|
|
@@ -13763,23 +13883,27 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13763
13883
|
dbg(`session.idle sid=${short(sid)}: streaming-failure check error: ${errMsg}`);
|
|
13764
13884
|
}
|
|
13765
13885
|
}
|
|
13766
|
-
|
|
13886
|
+
let todos = w.todos || [];
|
|
13887
|
+
if (todos.length === 0) {
|
|
13888
|
+
const fetched = await fetchSessionTodos(sid);
|
|
13889
|
+
if (fetched.length > 0) {
|
|
13890
|
+
w.todos = fetched;
|
|
13891
|
+
todos = fetched;
|
|
13892
|
+
}
|
|
13893
|
+
}
|
|
13767
13894
|
const open = getOpenTodos(todos);
|
|
13768
13895
|
if (open.length > 0 && currentBusy === 0 && !w.completionSignaled && !w.userCancelled && w.todoNudgeAttempts < maxRetries) {
|
|
13769
13896
|
const isCelebration = await lastAssistantEndsWithCelebration(sid);
|
|
13897
|
+
await log("info", `${short(sid)} - open todos=${open.length}, isCelebration=${isCelebration}, currentBusy=${currentBusy}`);
|
|
13770
13898
|
if (isCelebration) {
|
|
13771
|
-
await log("info", `${short(sid)} - \uD83C\uDF89 detected
|
|
13772
|
-
|
|
13773
|
-
w
|
|
13774
|
-
if (w.toolTextTimer) {
|
|
13775
|
-
clearTimeout(w.toolTextTimer);
|
|
13776
|
-
w.toolTextTimer = null;
|
|
13777
|
-
}
|
|
13778
|
-
} else {
|
|
13899
|
+
await log("info", `${short(sid)} - \uD83C\uDF89 detected but ${open.length} open todos remain, sending nudge`);
|
|
13900
|
+
const reminder = buildOpenTodosReminder(todos);
|
|
13901
|
+
await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
|
|
13779
13902
|
w.todoNudgeAttempts++;
|
|
13903
|
+
} else {
|
|
13780
13904
|
const reminder = buildOpenTodosReminder(todos);
|
|
13781
|
-
await log("info", `${short(sid)} - idle with ${open.length} open todos. Sending reminder (nudge ${w.todoNudgeAttempts}/${maxRetries})...`);
|
|
13782
13905
|
await tryResume(sid, w, "Idle with open todos", reminder);
|
|
13906
|
+
w.todoNudgeAttempts++;
|
|
13783
13907
|
}
|
|
13784
13908
|
}
|
|
13785
13909
|
}
|
|
@@ -13793,8 +13917,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13793
13917
|
dbg(`session.idle sid=${short(idleSid)}: skipping action intent, session is warming up (${Date.now() - idleW.createdAt}ms < ${warmupMs}ms)`);
|
|
13794
13918
|
return;
|
|
13795
13919
|
}
|
|
13796
|
-
const
|
|
13797
|
-
const msgs = extractMessages(response);
|
|
13920
|
+
const msgs = await getSessionMessages(idleSid);
|
|
13798
13921
|
const lastAssistantMsg = msgs.slice().reverse().find((m) => (m.role ?? m.info?.role) === "assistant");
|
|
13799
13922
|
if (lastAssistantMsg) {
|
|
13800
13923
|
let lastText = "";
|
|
@@ -13804,7 +13927,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13804
13927
|
}
|
|
13805
13928
|
if (containsActionIntent(lastText)) {
|
|
13806
13929
|
const w2 = sessions.get(idleSid);
|
|
13807
|
-
if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.status !== "idle")
|
|
13930
|
+
if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.userCancelled || w2.status !== "idle")
|
|
13808
13931
|
return;
|
|
13809
13932
|
w2.toolTextRecovered = true;
|
|
13810
13933
|
w2.toolTextAttempts++;
|
|
@@ -13858,8 +13981,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13858
13981
|
dbg(`session.idle sid=${short(sid)}: skipping action intent, session is warming up (${Date.now() - w.createdAt}ms < ${warmupMs}ms)`);
|
|
13859
13982
|
return;
|
|
13860
13983
|
}
|
|
13861
|
-
const
|
|
13862
|
-
const msgs = extractMessages(response);
|
|
13984
|
+
const msgs = await getSessionMessages(sid);
|
|
13863
13985
|
const lastAssistantMsg = msgs.slice().reverse().find((m) => (m.role ?? m.info?.role) === "assistant");
|
|
13864
13986
|
if (lastAssistantMsg) {
|
|
13865
13987
|
let lastText = "";
|
|
@@ -13869,7 +13991,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13869
13991
|
}
|
|
13870
13992
|
if (containsActionIntent(lastText)) {
|
|
13871
13993
|
const w2 = sessions.get(sid);
|
|
13872
|
-
if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.status !== "idle")
|
|
13994
|
+
if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.userCancelled || w2.status !== "idle")
|
|
13873
13995
|
return;
|
|
13874
13996
|
w2.toolTextRecovered = true;
|
|
13875
13997
|
w2.toolTextAttempts++;
|
|
@@ -13968,16 +14090,14 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13968
14090
|
break;
|
|
13969
14091
|
}
|
|
13970
14092
|
case "command.executed": {
|
|
13971
|
-
for (const [sid2, w2] of sessions) {
|
|
13972
|
-
if (w2.pendingRecovery) {
|
|
13973
|
-
dbg(`Pending recovery cleared on ${short(sid2)}: reason=user-command`);
|
|
13974
|
-
}
|
|
13975
|
-
resetSessionFlags(w2);
|
|
13976
|
-
}
|
|
13977
14093
|
if (!sid)
|
|
13978
14094
|
break;
|
|
13979
14095
|
const w = sessions.get(sid);
|
|
13980
14096
|
if (w) {
|
|
14097
|
+
if (w.pendingRecovery) {
|
|
14098
|
+
dbg(`Pending recovery cleared on ${short(sid)}: reason=user-command`);
|
|
14099
|
+
}
|
|
14100
|
+
resetBusyFlags(w);
|
|
13981
14101
|
w.pendingCommands = Math.max(0, w.pendingCommands - 1);
|
|
13982
14102
|
w.lastActivityAt = Date.now();
|
|
13983
14103
|
}
|
package/package.json
CHANGED