opencode-auto-resume 1.1.17 → 1.1.19
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/README.md +22 -0
- package/dist/index.js +245 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,20 @@ A separate **tool-call loop detector** catches the model calling the same tool 3
|
|
|
46
46
|
|
|
47
47
|
Loop detection runs in two places. At idle, tool names are scanned from recent assistant messages. **Live**, every `tool.execute.before` hook fingerprints the call as `tool name + arguments` — so a subagent stuck re-reading the same file/range (even alternating between two near-identical argument sets, which never produces 3 consecutive identical calls) is caught after 6+ calls in the repeating cycle. On live detection the plugin aborts the running turn immediately (this is the sanctioned exception to the never-abort-busy rule: 6+ identical name+args fingerprints prove a hallucinated loop, and the current call has not started yet) and then sends `TOOL_LOOP_RECOVERY_PROMPT`. Esc-cancelled sessions are never touched.
|
|
48
48
|
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Unknown tool suggestion
|
|
52
|
+
|
|
53
|
+
When the model calls a tool that does not exist (a typo, a hallucinated name, or a tool from a different plugin that isn't loaded), OpenCode returns a `tool` part with `state.status = "error"`. If the same wrong tool name appears **2 times** in the session's message history, the plugin:
|
|
54
|
+
|
|
55
|
+
1. Fetches the list of available tools via `ctx.client.tool.ids()` (cached for 5 minutes).
|
|
56
|
+
2. Computes the closest match by Levenshtein distance (case-insensitive, threshold = half the wrong name's length).
|
|
57
|
+
3. Sends a continue prompt that names the wrong tool, states it does not exist, suggests the closest match (if one was found), and lists the first 20 available tools for reference.
|
|
58
|
+
|
|
59
|
+
The suggestion fires once per busy cycle. A new user message resets the counter so the suggestion can fire again on a fresh cycle. Tool parts that already succeeded (`state.status = "completed"`) and parts for tools that do exist are skipped.
|
|
60
|
+
|
|
61
|
+
Threshold and cache are compile-time constants (`UNKNOWN_TOOL_THRESHOLD = 2`, `TOOL_IDS_CACHE_MS = 5 min`). The detection runs at idle, alongside the tool-text recovery check.
|
|
62
|
+
|
|
49
63
|
_Motivated by:_
|
|
50
64
|
- [#22142](https://github.com/anomalyco/opencode/issues/22142) — Repetitive tool-call loops with alibaba-coding-plan-cn/qwen3.6-plus
|
|
51
65
|
- [#16218](https://github.com/anomalyco/opencode/issues/16218) — Model repeats the same response in a loop after generating an answer
|
|
@@ -203,6 +217,8 @@ The agent can call the built-in `task_complete` tool to signal that all work is
|
|
|
203
217
|
|
|
204
218
|
If `task_complete` is called while open todos remain, the call is rejected (up to `maxRetries` times) with a message asking the agent to finish the remaining work first.
|
|
205
219
|
|
|
220
|
+
Repeat calls with no new user message in between are guarded: the first acknowledgement carries an explicit stop instruction, the second returns a repeat warning, and the third and subsequent calls are rejected as errors. This breaks the ack self-loop where the acknowledgement tool-result is fed back into the turn and a stuck model re-emits `task_complete` instead of ending with text. The counter resets on a genuine user message, on the block path above, or when any other tool runs in between.
|
|
221
|
+
|
|
206
222
|
---
|
|
207
223
|
|
|
208
224
|
### 🎉 emoji completion
|
|
@@ -312,6 +328,10 @@ session.status events:
|
|
|
312
328
|
|
|
313
329
|
todo.updated events:
|
|
314
330
|
└─ track real todo state (not regex on message text)
|
|
331
|
+
└─ if missing (no event received): idle path + task_complete
|
|
332
|
+
fetch on-demand via session.todo() API so the
|
|
333
|
+
open-todos reminder and the 🎉 completion latch see
|
|
334
|
+
real state, not a stale empty array.
|
|
315
335
|
|
|
316
336
|
Timer loop (every 5s):
|
|
317
337
|
for each busy session:
|
|
@@ -395,6 +415,8 @@ With options:
|
|
|
395
415
|
| `thinkingToolRecoveryPrompt` | `THINKING_TOOL_RECOVERY_PROMPT` | Override the thinking-tool recovery prompt |
|
|
396
416
|
| `doneWithoutWorkPrompt` | `DONE_WITHOUT_WORK_PROMPT` | Override the done-claim-with-open-todos prompt |
|
|
397
417
|
| `doneWithoutDetailsPrompt` | `DONE_WITHOUT_DETAILS_PROMPT` | Override the done-claim-with-no-todos report prompt |
|
|
418
|
+
| `doneClaimPatterns` | `DONE_CLAIM_PATTERNS` | Array of regex strings overriding the default done-claim detection patterns (case-insensitive, multiline). Invalid regexes are skipped. Empty array falls back to defaults. |
|
|
419
|
+
| `readyToContinuePatterns` | `READY_TO_CONTINUE_PATTERNS` | Array of regex strings overriding the default ready-to-continue detection patterns (case-insensitive). Invalid regexes are skipped. Empty array falls back to defaults. |
|
|
398
420
|
| `silentDeadStreamMinTokens` | `200` | Min output tokens to treat a textless `finish:"unknown"` message as a dead stream |
|
|
399
421
|
| `busyStallStrategy` | `"continue"` | Busy-stall response: `"continue"`, `"abort"` (abort-first), or `"off"` (disabled) |
|
|
400
422
|
| `contextSaturationThreshold` | `0.85` | Ratio of used/usable context that routes a saturated parent to magic-context `ctx-wrapup` (only when magic-context is installed) |
|
package/dist/index.js
CHANGED
|
@@ -12363,7 +12363,8 @@ var DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS = [
|
|
|
12363
12363
|
"streaming response failed",
|
|
12364
12364
|
"stream.*fail",
|
|
12365
12365
|
"connection.*reset",
|
|
12366
|
-
"connection.*closed"
|
|
12366
|
+
"connection.*closed",
|
|
12367
|
+
"aborted due to timeout"
|
|
12367
12368
|
];
|
|
12368
12369
|
var MAX_IDLE_SESSIONS = 50;
|
|
12369
12370
|
var IDLE_CLEANUP_MS = 10 * 60000;
|
|
@@ -12371,7 +12372,12 @@ var SESSION_DISCOVERY_INTERVAL_MS = 60000;
|
|
|
12371
12372
|
var TOOL_TEXT_RECOVERY_PROMPT = "Your last message contained a raw tool call printed as text instead of being executed. " + "Please use the proper tool calling mechanism to execute it.";
|
|
12372
12373
|
var THINKING_TOOL_RECOVERY_PROMPT = "I noticed you have a tool call generated in your thinking/reasoning. " + "Please execute it using the proper tool calling mechanism instead of keeping it in reasoning.";
|
|
12373
12374
|
var TOOL_LOOP_RECOVERY_PROMPT = "I notice you've been calling the same tool multiple times in a row without making progress. " + "Please step back and reassess your approach. Consider: " + "1) Are you stuck in a loop? 2) Do you need different information first? " + "3) Should you try a different tool or break the task into smaller steps? " + "Take a moment to think about what's blocking you and propose a different strategy.";
|
|
12375
|
+
var TASK_COMPLETE_ACK = "Task completion acknowledged. No further continuation will be sent. " + "End your turn now with a brief text reply \u2014 do not call task_complete or any other tool again " + "unless the user sends a new message.";
|
|
12376
|
+
var TASK_COMPLETE_REPEAT_WARNING = "Task completion acknowledged already on your previous call \u2014 completion is recorded. " + "End your turn now with a brief text reply. Do not call task_complete again; " + "further repeat calls are rejected as errors.";
|
|
12377
|
+
var TASK_COMPLETE_REPEAT_ERROR = "task_complete already acknowledged twice with no new user message or tool work since \u2014 " + "completion is recorded. End your turn with text and make no further tool calls.";
|
|
12374
12378
|
var CTX_WRAPUP_TRIGGER = "ctx-wrapup";
|
|
12379
|
+
var UNKNOWN_TOOL_THRESHOLD = 2;
|
|
12380
|
+
var TOOL_IDS_CACHE_MS = 5 * 60000;
|
|
12375
12381
|
var TOOL_TEXT_PATTERNS = [
|
|
12376
12382
|
/<function\s*=/i,
|
|
12377
12383
|
/<function>/i,
|
|
@@ -12422,7 +12428,7 @@ function containsToolCallAsText(text) {
|
|
|
12422
12428
|
}
|
|
12423
12429
|
return false;
|
|
12424
12430
|
}
|
|
12425
|
-
function containsReadyToContinuePattern(text) {
|
|
12431
|
+
function containsReadyToContinuePattern(text, patterns = READY_TO_CONTINUE_PATTERNS) {
|
|
12426
12432
|
const lines = text.split(`
|
|
12427
12433
|
`);
|
|
12428
12434
|
const lastLine = lines[lines.length - 1]?.trim();
|
|
@@ -12430,7 +12436,7 @@ function containsReadyToContinuePattern(text) {
|
|
|
12430
12436
|
return false;
|
|
12431
12437
|
const lastLines = lines.slice(-3).join(`
|
|
12432
12438
|
`);
|
|
12433
|
-
return
|
|
12439
|
+
return patterns.some((pat) => pat.test(lastLines));
|
|
12434
12440
|
}
|
|
12435
12441
|
var DONE_CLAIM_PATTERNS = [
|
|
12436
12442
|
/^task\s+done[.!]*$/im,
|
|
@@ -12450,12 +12456,12 @@ var DONE_CLAIM_PATTERNS = [
|
|
|
12450
12456
|
];
|
|
12451
12457
|
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.";
|
|
12452
12458
|
var DONE_WITHOUT_DETAILS_PROMPT = "Your last response claimed the task is complete but contained no work description. This is not acceptable. " + "You MUST respond now with a full, detailed report of everything you did: " + "for each file you modified, state the full path and the exact changes; " + "list every command you ran to verify and its result; state the final outcome. " + "Do NOT reply with 'done', 'task completed', or any short acknowledgment \u2014 " + "your ONLY acceptable response right now is this detailed report. Write it now.";
|
|
12453
|
-
function containsDoneClaimPattern(text) {
|
|
12459
|
+
function containsDoneClaimPattern(text, patterns = DONE_CLAIM_PATTERNS) {
|
|
12454
12460
|
const lines = text.split(`
|
|
12455
12461
|
`);
|
|
12456
12462
|
const lastLines = lines.slice(-5).join(`
|
|
12457
12463
|
`);
|
|
12458
|
-
return
|
|
12464
|
+
return patterns.some((pat) => pat.test(lastLines));
|
|
12459
12465
|
}
|
|
12460
12466
|
function containsWorkDescription(text) {
|
|
12461
12467
|
if (/`[^`\n]*\.[a-zA-Z0-9]{1,8}[^`\n]*`/.test(text))
|
|
@@ -12466,6 +12472,42 @@ function containsWorkDescription(text) {
|
|
|
12466
12472
|
return true;
|
|
12467
12473
|
return false;
|
|
12468
12474
|
}
|
|
12475
|
+
function levenshtein(a, b) {
|
|
12476
|
+
const m = a.length, n = b.length;
|
|
12477
|
+
if (m === 0)
|
|
12478
|
+
return n;
|
|
12479
|
+
if (n === 0)
|
|
12480
|
+
return m;
|
|
12481
|
+
let prev = new Array(n + 1);
|
|
12482
|
+
let curr = new Array(n + 1);
|
|
12483
|
+
for (let j = 0;j <= n; j++)
|
|
12484
|
+
prev[j] = j;
|
|
12485
|
+
for (let i = 1;i <= m; i++) {
|
|
12486
|
+
curr[0] = i;
|
|
12487
|
+
for (let j = 1;j <= n; j++) {
|
|
12488
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
12489
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
12490
|
+
}
|
|
12491
|
+
const tmp = prev;
|
|
12492
|
+
prev = curr;
|
|
12493
|
+
curr = tmp;
|
|
12494
|
+
}
|
|
12495
|
+
return prev[n];
|
|
12496
|
+
}
|
|
12497
|
+
function suggestClosestTool(wrongName, available) {
|
|
12498
|
+
const lower = wrongName.toLowerCase();
|
|
12499
|
+
let best = null;
|
|
12500
|
+
let bestDist = Infinity;
|
|
12501
|
+
for (const id of available) {
|
|
12502
|
+
const dist = levenshtein(lower, id.toLowerCase());
|
|
12503
|
+
const threshold = Math.max(2, Math.floor(lower.length / 2));
|
|
12504
|
+
if (dist < bestDist && dist <= threshold) {
|
|
12505
|
+
bestDist = dist;
|
|
12506
|
+
best = id;
|
|
12507
|
+
}
|
|
12508
|
+
}
|
|
12509
|
+
return best;
|
|
12510
|
+
}
|
|
12469
12511
|
function isStreamingFailure(errorName, errorMessage, errorNames = DEFAULT_STREAMING_FAILURE_ERROR_NAMES, messagePatterns = DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS) {
|
|
12470
12512
|
if (!errorName && !errorMessage)
|
|
12471
12513
|
return false;
|
|
@@ -12633,6 +12675,30 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12633
12675
|
const contextSaturationThreshold = options?.contextSaturationThreshold ?? 0.85;
|
|
12634
12676
|
const subagentNativeCompactionEnabled = options?.subagentNativeCompactionEnabled ?? false;
|
|
12635
12677
|
const activeUserWindowMs = options?.activeUserWindowMs ?? DEFAULT_ACTIVE_USER_WINDOW_MS;
|
|
12678
|
+
const doneClaimPatterns = (() => {
|
|
12679
|
+
const raw = options?.doneClaimPatterns;
|
|
12680
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
12681
|
+
return DONE_CLAIM_PATTERNS;
|
|
12682
|
+
return raw.map((s) => {
|
|
12683
|
+
try {
|
|
12684
|
+
return new RegExp(s, "im");
|
|
12685
|
+
} catch {
|
|
12686
|
+
return null;
|
|
12687
|
+
}
|
|
12688
|
+
}).filter((r) => r !== null);
|
|
12689
|
+
})();
|
|
12690
|
+
const readyToContinuePatterns = (() => {
|
|
12691
|
+
const raw = options?.readyToContinuePatterns;
|
|
12692
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
12693
|
+
return READY_TO_CONTINUE_PATTERNS;
|
|
12694
|
+
return raw.map((s) => {
|
|
12695
|
+
try {
|
|
12696
|
+
return new RegExp(s, "i");
|
|
12697
|
+
} catch {
|
|
12698
|
+
return null;
|
|
12699
|
+
}
|
|
12700
|
+
}).filter((r) => r !== null);
|
|
12701
|
+
})();
|
|
12636
12702
|
const dbg = (...args) => {
|
|
12637
12703
|
if (debug)
|
|
12638
12704
|
console.log("[debug]", ...args);
|
|
@@ -12707,6 +12773,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12707
12773
|
orphanWatchStartAt: null,
|
|
12708
12774
|
aborting: false,
|
|
12709
12775
|
pluginAbortInFlight: false,
|
|
12776
|
+
pluginAbortAt: 0,
|
|
12710
12777
|
toolTextRecovered: false,
|
|
12711
12778
|
toolTextAttempts: 0,
|
|
12712
12779
|
continueTimestamps: [],
|
|
@@ -12727,6 +12794,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12727
12794
|
completionSignaled: false,
|
|
12728
12795
|
todoNudgeAttempts: 0,
|
|
12729
12796
|
taskCompleteOverrides: 0,
|
|
12797
|
+
taskCompleteSignals: 0,
|
|
12730
12798
|
doneClaimNoTodosAttempts: 0,
|
|
12731
12799
|
lastUserMessageAt: 0,
|
|
12732
12800
|
pendingTools: 0,
|
|
@@ -12735,7 +12803,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12735
12803
|
pendingRecoveryReason: null,
|
|
12736
12804
|
pendingRecoveryAt: 0,
|
|
12737
12805
|
recoveryAttempts: 0,
|
|
12738
|
-
watchdogRetryGuard: false
|
|
12806
|
+
watchdogRetryGuard: false,
|
|
12807
|
+
unknownToolErrors: new Map,
|
|
12808
|
+
unknownToolSuggestionSent: false,
|
|
12809
|
+
checkedToolPartIDs: new Set
|
|
12739
12810
|
};
|
|
12740
12811
|
sessions.set(sid, w);
|
|
12741
12812
|
}
|
|
@@ -12871,6 +12942,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12871
12942
|
}
|
|
12872
12943
|
if (w.userCancelled || w.completionSignaled)
|
|
12873
12944
|
return;
|
|
12945
|
+
if (w.gaveUp) {
|
|
12946
|
+
await log("debug", `${short(sid)} - gaveUp latched, refusing further continue prompts`);
|
|
12947
|
+
return;
|
|
12948
|
+
}
|
|
12874
12949
|
if (!w.continuing)
|
|
12875
12950
|
dbg(`State transition on ${short(sid)}: continuing=false -> true`);
|
|
12876
12951
|
w.continuing = true;
|
|
@@ -12972,7 +13047,6 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12972
13047
|
} catch (err) {
|
|
12973
13048
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12974
13049
|
await log("warn", `${short(sid)} - recovery retry failed: ${errMsg}`);
|
|
12975
|
-
w.recoveryAttempts = 0;
|
|
12976
13050
|
}
|
|
12977
13051
|
w.watchdogRetryGuard = false;
|
|
12978
13052
|
} else {
|
|
@@ -12984,8 +13058,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12984
13058
|
dbg(`Watchdog check on ${short(sid)}: status=${w.status}, recoveryAttempts=${w.recoveryAttempts}, maxRetries=${maxRecoveryRetries}, watchdogLatencyMs=${Date.now() - w.lastRetryAt} -> ABORT_RESUME`);
|
|
12985
13059
|
const resumed = await tryAbortAndResume(sid, w);
|
|
12986
13060
|
if (!resumed && !w.aborting) {
|
|
12987
|
-
|
|
12988
|
-
|
|
13061
|
+
w.gaveUp = true;
|
|
13062
|
+
w.pendingRecovery = false;
|
|
13063
|
+
w.pendingRecoveryReason = null;
|
|
13064
|
+
await log("warn", `Recovery exhausted on ${short(sid)}: attempts=${w.recoveryAttempts}, lastError=abort+resume failed \u2014 GAVE UP (re-arms on next user message)`);
|
|
13065
|
+
dbg(`Watchdog check on ${short(sid)}: status=${w.status} -> GAVE_UP (latched)`);
|
|
12989
13066
|
if (w.pendingRecoveryAt > 0) {
|
|
12990
13067
|
dbg(`Total recovery cycle on ${short(sid)} (failed): totalCycleMs=${Date.now() - w.pendingRecoveryAt}`);
|
|
12991
13068
|
}
|
|
@@ -13054,6 +13131,73 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13054
13131
|
messagesInflight.set(sid, p);
|
|
13055
13132
|
return p;
|
|
13056
13133
|
}
|
|
13134
|
+
let cachedToolIds = null;
|
|
13135
|
+
let cachedToolIdsAt = 0;
|
|
13136
|
+
async function getAvailableToolIds() {
|
|
13137
|
+
if (cachedToolIds && Date.now() - cachedToolIdsAt < TOOL_IDS_CACHE_MS) {
|
|
13138
|
+
return cachedToolIds;
|
|
13139
|
+
}
|
|
13140
|
+
try {
|
|
13141
|
+
const result = await ctx.client.tool.ids();
|
|
13142
|
+
const ids = result.data ?? [];
|
|
13143
|
+
cachedToolIds = ids;
|
|
13144
|
+
cachedToolIdsAt = Date.now();
|
|
13145
|
+
return ids;
|
|
13146
|
+
} catch (e) {
|
|
13147
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13148
|
+
await log("warn", `Failed to fetch tool IDs: ${msg}`);
|
|
13149
|
+
return cachedToolIds ?? [];
|
|
13150
|
+
}
|
|
13151
|
+
}
|
|
13152
|
+
async function checkForUnknownToolCalls(sid, w) {
|
|
13153
|
+
if (w.unknownToolSuggestionSent)
|
|
13154
|
+
return false;
|
|
13155
|
+
if (w.userCancelled || w.completionSignaled)
|
|
13156
|
+
return false;
|
|
13157
|
+
try {
|
|
13158
|
+
const available = await getAvailableToolIds();
|
|
13159
|
+
if (available.length === 0)
|
|
13160
|
+
return false;
|
|
13161
|
+
const messages = await getSessionMessages(sid);
|
|
13162
|
+
for (const msg of messages) {
|
|
13163
|
+
const parts = msg.parts;
|
|
13164
|
+
if (!parts)
|
|
13165
|
+
continue;
|
|
13166
|
+
for (const part of parts) {
|
|
13167
|
+
const partType = part.type;
|
|
13168
|
+
if (partType !== "tool")
|
|
13169
|
+
continue;
|
|
13170
|
+
const partId = part.id ?? part.callID ?? "";
|
|
13171
|
+
if (!partId || w.checkedToolPartIDs.has(partId))
|
|
13172
|
+
continue;
|
|
13173
|
+
w.checkedToolPartIDs.add(partId);
|
|
13174
|
+
const state = part.state;
|
|
13175
|
+
if (!state || state.status !== "error")
|
|
13176
|
+
continue;
|
|
13177
|
+
const toolName = part.tool ?? "";
|
|
13178
|
+
if (!toolName || available.includes(toolName))
|
|
13179
|
+
continue;
|
|
13180
|
+
const count = (w.unknownToolErrors.get(toolName) ?? 0) + 1;
|
|
13181
|
+
w.unknownToolErrors.set(toolName, count);
|
|
13182
|
+
if (count < UNKNOWN_TOOL_THRESHOLD)
|
|
13183
|
+
continue;
|
|
13184
|
+
const suggestion = suggestClosestTool(toolName, available);
|
|
13185
|
+
const toolList = available.slice(0, 20).join(", ");
|
|
13186
|
+
const prompt = suggestion ? `You tried to use the tool "${toolName}" ${count} times, but it does not exist. ` + `The closest matching tool is "${suggestion}". ` + `Please use "${suggestion}" instead and adjust your arguments accordingly. ` + `Available tools include: ${toolList}.` : `You tried to use the tool "${toolName}" ${count} times, but it does not exist. ` + `Please check the available tools and use the correct one. ` + `Available tools include: ${toolList}.`;
|
|
13187
|
+
w.unknownToolSuggestionSent = true;
|
|
13188
|
+
w.toolTextRecovered = true;
|
|
13189
|
+
await log("warn", `${short(sid)} - unknown tool "${toolName}" called ${count}x, suggesting "${suggestion ?? "(none)"}"`);
|
|
13190
|
+
await sendContinuePrompt(sid, prompt, w);
|
|
13191
|
+
return true;
|
|
13192
|
+
}
|
|
13193
|
+
}
|
|
13194
|
+
return false;
|
|
13195
|
+
} catch (e) {
|
|
13196
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13197
|
+
await log("warn", `${short(sid)} - checkForUnknownToolCalls error: ${msg}`);
|
|
13198
|
+
return false;
|
|
13199
|
+
}
|
|
13200
|
+
}
|
|
13057
13201
|
function roleOf(msg) {
|
|
13058
13202
|
if (!msg)
|
|
13059
13203
|
return;
|
|
@@ -13441,6 +13585,15 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13441
13585
|
await log("info", `${short(sid)} - user recently active, skipping tool-text check`);
|
|
13442
13586
|
return;
|
|
13443
13587
|
}
|
|
13588
|
+
if ((w.todos || []).length === 0) {
|
|
13589
|
+
try {
|
|
13590
|
+
const fetched = await fetchSessionTodos(sid);
|
|
13591
|
+
if (fetched.length > 0)
|
|
13592
|
+
w.todos = fetched;
|
|
13593
|
+
} catch (e) {
|
|
13594
|
+
dbg(`checkForToolCallAsText sid=${short(sid)}: lazy todo fetch error: ${e}`);
|
|
13595
|
+
}
|
|
13596
|
+
}
|
|
13444
13597
|
const recent = messages.slice(-3);
|
|
13445
13598
|
let bestCandidate = null;
|
|
13446
13599
|
let allAssistantText = "";
|
|
@@ -13538,7 +13691,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13538
13691
|
bestCandidate = candidate;
|
|
13539
13692
|
}
|
|
13540
13693
|
}
|
|
13541
|
-
if (containsReadyToContinuePattern(text)) {
|
|
13694
|
+
if (containsReadyToContinuePattern(text, readyToContinuePatterns)) {
|
|
13542
13695
|
const todos = w.todos || [];
|
|
13543
13696
|
const hasOpenTodos = todos.some(isOpenTodo);
|
|
13544
13697
|
if (!hasOpenTodos && todos.length > 0) {
|
|
@@ -13558,16 +13711,42 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13558
13711
|
await log("info", `${short(sid)} - skipping continue, todos appear completed (attempt ${w.todoCheckAttempts}/2)`);
|
|
13559
13712
|
continue;
|
|
13560
13713
|
}
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13714
|
+
if (containsDoneClaimPattern(text, doneClaimPatterns)) {
|
|
13715
|
+
const todos = w.todos || [];
|
|
13716
|
+
const hasOpenTodos = todos.some(isOpenTodo);
|
|
13717
|
+
if (hasOpenTodos) {
|
|
13718
|
+
const candidate = {
|
|
13719
|
+
prompt: doneWithoutWorkPrompt,
|
|
13720
|
+
source: "done-claim",
|
|
13721
|
+
priority: 1
|
|
13722
|
+
};
|
|
13723
|
+
if (!bestCandidate || candidate.priority < bestCandidate.priority) {
|
|
13724
|
+
bestCandidate = candidate;
|
|
13725
|
+
}
|
|
13726
|
+
} else if (!containsWorkDescription(allAssistantText)) {
|
|
13727
|
+
if (w.doneClaimNoTodosAttempts < maxRetries) {
|
|
13728
|
+
const candidate = {
|
|
13729
|
+
prompt: doneWithoutDetailsPrompt,
|
|
13730
|
+
source: "done-claim-no-todos",
|
|
13731
|
+
priority: 1
|
|
13732
|
+
};
|
|
13733
|
+
if (!bestCandidate || candidate.priority < bestCandidate.priority) {
|
|
13734
|
+
bestCandidate = candidate;
|
|
13735
|
+
}
|
|
13736
|
+
}
|
|
13737
|
+
}
|
|
13738
|
+
} else {
|
|
13739
|
+
const candidate = {
|
|
13740
|
+
prompt: continuePrompt,
|
|
13741
|
+
source: "ready-to-continue",
|
|
13742
|
+
priority: 1
|
|
13743
|
+
};
|
|
13744
|
+
if (!bestCandidate || candidate.priority < bestCandidate.priority) {
|
|
13745
|
+
bestCandidate = candidate;
|
|
13746
|
+
}
|
|
13568
13747
|
}
|
|
13569
13748
|
}
|
|
13570
|
-
if (!bestCandidate && containsDoneClaimPattern(text)) {
|
|
13749
|
+
if (!bestCandidate && containsDoneClaimPattern(text, doneClaimPatterns)) {
|
|
13571
13750
|
const todos = w.todos || [];
|
|
13572
13751
|
const hasOpenTodos = todos.some(isOpenTodo);
|
|
13573
13752
|
if (hasOpenTodos) {
|
|
@@ -13652,6 +13831,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13652
13831
|
await log("info", `${short(sid)} - max open-todos nudges (${maxRetries}) reached, waiting for activity`);
|
|
13653
13832
|
return;
|
|
13654
13833
|
}
|
|
13834
|
+
w.todoNudgeAttempts++;
|
|
13655
13835
|
} else if (isDoneClaimNoTodos) {
|
|
13656
13836
|
w.doneClaimNoTodosAttempts++;
|
|
13657
13837
|
} else {
|
|
@@ -13707,6 +13887,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13707
13887
|
const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
|
|
13708
13888
|
await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
|
|
13709
13889
|
w.pluginAbortInFlight = true;
|
|
13890
|
+
w.pluginAbortAt = Date.now();
|
|
13710
13891
|
try {
|
|
13711
13892
|
await ctx.client.session.abort({ path: { id: sid } });
|
|
13712
13893
|
await log("info", `${short(sid)} - abort OK`);
|
|
@@ -13716,6 +13897,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13716
13897
|
await log("warn", `${short(sid)} - abort failed: ${errMsg}`);
|
|
13717
13898
|
w.aborting = false;
|
|
13718
13899
|
w.pluginAbortInFlight = false;
|
|
13900
|
+
w.pluginAbortAt = 0;
|
|
13719
13901
|
return false;
|
|
13720
13902
|
}
|
|
13721
13903
|
await new Promise((resolve) => setTimeout(resolve, ABORT_CONTINUE_DELAY_MS));
|
|
@@ -13740,6 +13922,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13740
13922
|
return false;
|
|
13741
13923
|
} finally {
|
|
13742
13924
|
w.pluginAbortInFlight = false;
|
|
13925
|
+
w.pluginAbortAt = 0;
|
|
13743
13926
|
}
|
|
13744
13927
|
}
|
|
13745
13928
|
async function tryResume(sid, w, reason, prompt) {
|
|
@@ -14283,6 +14466,9 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14283
14466
|
w.status = "idle";
|
|
14284
14467
|
resetIdleFlags(w);
|
|
14285
14468
|
dbg(`session.idle sid=${short(sid)}: resetIdleFlags done, toolTextRecovered=${w.toolTextRecovered}, toolTextAttempts=${w.toolTextAttempts}, maxRetries=${maxRetries}`);
|
|
14469
|
+
if (!w.unknownToolSuggestionSent && !w.completionSignaled && !w.userCancelled) {
|
|
14470
|
+
checkForUnknownToolCalls(sid, w);
|
|
14471
|
+
}
|
|
14286
14472
|
if (resumeOnActionIntent && !w.toolTextRecovered && !w.completionSignaled) {
|
|
14287
14473
|
setTimeout(async () => {
|
|
14288
14474
|
try {
|
|
@@ -14360,6 +14546,9 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14360
14546
|
if (w) {
|
|
14361
14547
|
w.doneClaimNoTodosAttempts = 0;
|
|
14362
14548
|
w.lastUserMessageAt = Date.now();
|
|
14549
|
+
w.unknownToolErrors.clear();
|
|
14550
|
+
w.unknownToolSuggestionSent = false;
|
|
14551
|
+
w.checkedToolPartIDs.clear();
|
|
14363
14552
|
}
|
|
14364
14553
|
break;
|
|
14365
14554
|
}
|
|
@@ -14398,10 +14587,16 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14398
14587
|
const isMessageAborted = errorName === "MessageAbortedError";
|
|
14399
14588
|
if (isMessageAborted) {
|
|
14400
14589
|
for (const [wSid, w] of sessions) {
|
|
14401
|
-
|
|
14590
|
+
const PLUGIN_ABORT_GRACE_MS = 1000;
|
|
14591
|
+
const isOwnAbort = w.pluginAbortInFlight && Date.now() - (w.pluginAbortAt || 0) < PLUGIN_ABORT_GRACE_MS;
|
|
14592
|
+
if (!isOwnAbort) {
|
|
14402
14593
|
w.userCancelled = true;
|
|
14403
14594
|
w.status = "idle";
|
|
14404
14595
|
resetIdleFlags(w);
|
|
14596
|
+
if (w.toolTextTimer) {
|
|
14597
|
+
clearTimeout(w.toolTextTimer);
|
|
14598
|
+
w.toolTextTimer = null;
|
|
14599
|
+
}
|
|
14405
14600
|
}
|
|
14406
14601
|
}
|
|
14407
14602
|
log("info", "User abort (ESC)");
|
|
@@ -14452,20 +14647,40 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14452
14647
|
}
|
|
14453
14648
|
}
|
|
14454
14649
|
const taskCompleteTool = tool({
|
|
14455
|
-
description: "Signal that all work is complete and stop automatic continuation prompts. Call this ONLY after finishing everything requested.",
|
|
14650
|
+
description: "Signal that all work is complete and stop automatic continuation prompts. Call this ONLY after finishing everything requested. Call exactly once per completed round of work \u2014 repeat calls with no new user message in between are rejected.",
|
|
14456
14651
|
args: {},
|
|
14457
14652
|
execute: async (_args, ctx) => {
|
|
14458
14653
|
const w = sessions.get(ctx.sessionID);
|
|
14459
14654
|
if (w) {
|
|
14460
14655
|
if (!w.isSubagent) {
|
|
14656
|
+
if ((w.todos || []).length === 0) {
|
|
14657
|
+
try {
|
|
14658
|
+
const fetched = await fetchSessionTodos(ctx.sessionID);
|
|
14659
|
+
if (fetched.length > 0)
|
|
14660
|
+
w.todos = fetched;
|
|
14661
|
+
} catch (e) {
|
|
14662
|
+
dbg(`task_complete sid=${short(ctx.sessionID)}: lazy todo fetch error: ${e}`);
|
|
14663
|
+
}
|
|
14664
|
+
}
|
|
14461
14665
|
const openTodos = (w.todos || []).filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
14462
14666
|
if (openTodos.length > 0 && w.taskCompleteOverrides < maxRetries) {
|
|
14463
14667
|
w.taskCompleteOverrides++;
|
|
14668
|
+
w.taskCompleteSignals = 0;
|
|
14464
14669
|
const reminder = buildOpenTodosReminder(w.todos);
|
|
14465
14670
|
const blockMsg = `Mark any finished todos complete and do not redo completed work.
|
|
14466
14671
|
${reminder}`;
|
|
14467
14672
|
await log("info", `${short(ctx.sessionID)} - task_complete blocked: ${openTodos.length} open todos remain (override ${w.taskCompleteOverrides}/${maxRetries})`);
|
|
14468
|
-
|
|
14673
|
+
let awaitingInput = false;
|
|
14674
|
+
try {
|
|
14675
|
+
awaitingInput = hasPendingUserInput(await getSessionMessages(ctx.sessionID));
|
|
14676
|
+
} catch (e) {
|
|
14677
|
+
dbg(`task_complete block: awaiting-input check error: ${e}`);
|
|
14678
|
+
}
|
|
14679
|
+
if (awaitingInput) {
|
|
14680
|
+
await log("info", `${short(ctx.sessionID)} - task_complete blocked but user input pending, skipping visible nudge`);
|
|
14681
|
+
} else {
|
|
14682
|
+
await sendContinuePrompt(ctx.sessionID, blockMsg, w);
|
|
14683
|
+
}
|
|
14469
14684
|
return blockMsg;
|
|
14470
14685
|
}
|
|
14471
14686
|
}
|
|
@@ -14476,8 +14691,13 @@ ${reminder}`;
|
|
|
14476
14691
|
w.toolTextTimer = null;
|
|
14477
14692
|
}
|
|
14478
14693
|
log("info", `${short(ctx.sessionID)} - task_complete called, ${w.isSubagent ? "subagent" : "agent"} done`);
|
|
14694
|
+
w.taskCompleteSignals++;
|
|
14695
|
+
if (w.taskCompleteSignals === 2)
|
|
14696
|
+
return TASK_COMPLETE_REPEAT_WARNING;
|
|
14697
|
+
if (w.taskCompleteSignals > 2)
|
|
14698
|
+
throw new Error(TASK_COMPLETE_REPEAT_ERROR);
|
|
14479
14699
|
}
|
|
14480
|
-
return
|
|
14700
|
+
return TASK_COMPLETE_ACK;
|
|
14481
14701
|
}
|
|
14482
14702
|
});
|
|
14483
14703
|
return {
|
|
@@ -14509,6 +14729,7 @@ ${reminder}`;
|
|
|
14509
14729
|
if (w.userCancelled || w.completionSignaled) {
|
|
14510
14730
|
w.userCancelled = false;
|
|
14511
14731
|
w.completionSignaled = false;
|
|
14732
|
+
w.taskCompleteSignals = 0;
|
|
14512
14733
|
await log("info", `${short(sid)} - new user message, re-arming auto-resume`);
|
|
14513
14734
|
}
|
|
14514
14735
|
},
|
|
@@ -14519,6 +14740,8 @@ ${reminder}`;
|
|
|
14519
14740
|
w.pendingTools++;
|
|
14520
14741
|
w.lastActivityAt = Date.now();
|
|
14521
14742
|
const toolName = input.tool ?? "unknown";
|
|
14743
|
+
if (toolName !== "task_complete")
|
|
14744
|
+
w.taskCompleteSignals = 0;
|
|
14522
14745
|
const rawArgs = hookArgs?.args ?? input.args;
|
|
14523
14746
|
w.liveToolSigs.push(toolCallSignature(toolName, rawArgs));
|
|
14524
14747
|
if (w.liveToolSigs.length > 15)
|
package/package.json
CHANGED