opencode-auto-resume 1.2.0 → 1.2.2
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 +3 -11
- package/dist/index.js +162 -89
- package/dist/v2/index.js +97 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -424,17 +424,9 @@ Aborts the active run, then continues. The only family that can unblock a live r
|
|
|
424
424
|
|
|
425
425
|
Paths: orphan parent, subagent stuck (parent side), hallucination loop, streaming-failure escalation. All aborts pass the active-tool guards first, and plugin-initiated aborts are marked so they are not mistaken for user ESC (`session.error` → `MessageAbortedError` race).
|
|
426
426
|
|
|
427
|
-
|
|
427
|
+
<<<<<<< HEAD
|
|
428
|
+
## Architecture
|
|
428
429
|
|
|
429
|
-
Auto-resume works on its own, but two companion plugins make it sharper. Both are optional — auto-resume detects their presence and degrades gracefully when they are absent.
|
|
430
|
-
|
|
431
|
-
| Plugin | Required? | If absent |
|
|
432
|
-
|---|---|---|
|
|
433
|
-
| opencode-todolist | No | No task-aware nudges; other paths unaffected |
|
|
434
|
-
| opencode-task-complete | No | Built-in fallback `task_complete` tool; text/emoji completion |
|
|
435
|
-
|
|
436
|
-
- [opencode-todolist](https://github.com/aiev/opencode-todolist) — Restores `todowrite` / `todoread` in opencode v2
|
|
437
|
-
- [opencode-task-complete](https://github.com/famewolf/opencode-task-complete) — Provides the `task_complete` tool for explicit completion signalling
|
|
438
430
|
|
|
439
431
|
## Verification & troubleshooting
|
|
440
432
|
|
|
@@ -453,7 +445,7 @@ On v2 a clean load writes nothing at startup: warnings and recovery lines go to
|
|
|
453
445
|
| Tool-text not detected | Check server logs — requires SDK message fetching (v1). On v2 the history comes from the client message surface or loopback HTTP — check the server process output instead |
|
|
454
446
|
| Long-running tool killed | Should not happen — active-tool guard prevents it. Report a bug. |
|
|
455
447
|
| No plugin activity in the log | The entry is not in a location opencode scans, or not listed in `plugins`; fix the config and restart opencode. |
|
|
456
|
-
| Log says `ctx.app.version is missing` (or `ctx.serverUrl is missing` on v1) | The build was loaded on the wrong server generation. Update the npm package (the universal entry auto-selects the branch) or match the build file to the server. |
|
|
448
|
+
| Log says `ctx.app.version is missing` (or `ctx.serverUrl is missing` on v1) | The build was loaded on the wrong server generation. The plugin disables itself with this diagnostic instead of crashing. Update the npm package (the universal entry auto-selects the branch by context shape) or match the build file to the server. |
|
|
457
449
|
| Never recovers a parent waiting on a subagent | Intentional: parent sessions blocked on a running subagent are left alone. |
|
|
458
450
|
| Never recovers while a permission dialog is open | Intentional: recovery is held until the permission is answered. |
|
|
459
451
|
| Recovers but you don't see a notice (v2) | Your model/provider may reject `session.synthetic()`; the plugin falls back to `session.prompt()` (no TUI banner). |
|
package/dist/index.js
CHANGED
|
@@ -12389,6 +12389,12 @@ var TASK_COMPLETE_REPEAT_ERROR = "task_complete already acknowledged twice with
|
|
|
12389
12389
|
var CTX_WRAPUP_TRIGGER = "ctx-wrapup";
|
|
12390
12390
|
var UNKNOWN_TOOL_THRESHOLD = 2;
|
|
12391
12391
|
var TOOL_IDS_CACHE_MS = 5 * 60000;
|
|
12392
|
+
var OWN_PROMPT_SHAPES = [
|
|
12393
|
+
/^continue\s*\u2014\s*stalled\s*\(/,
|
|
12394
|
+
/^continue\s*\nOnly continue work already in progress\./,
|
|
12395
|
+
/^continue\s*\nIf work was already in progress,/,
|
|
12396
|
+
/^You tried to use the tool "/
|
|
12397
|
+
];
|
|
12392
12398
|
var TOOL_TEXT_PATTERNS = [
|
|
12393
12399
|
/<function\s*=/i,
|
|
12394
12400
|
/<function>/i,
|
|
@@ -12677,6 +12683,10 @@ Please continue working on ${thisWord} ${taskWord}.`;
|
|
|
12677
12683
|
var AutoResumePlugin = async (ctx, options) => {
|
|
12678
12684
|
if (!ctx.serverUrl) {
|
|
12679
12685
|
console.warn("[auto-resume] this looks like the opencode v1 build running on the wrong server: ctx.serverUrl is missing. The v1 bundle requires opencode v1 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).");
|
|
12686
|
+
return {
|
|
12687
|
+
event: async () => {},
|
|
12688
|
+
config: async () => {}
|
|
12689
|
+
};
|
|
12680
12690
|
}
|
|
12681
12691
|
const chunkTimeoutMs = options?.chunkTimeoutMs ?? DEFAULT_CHUNK_TIMEOUT_MS;
|
|
12682
12692
|
const checkIntervalMs = options?.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
|
|
@@ -13201,7 +13211,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13201
13211
|
if (available.length === 0)
|
|
13202
13212
|
return false;
|
|
13203
13213
|
const messages = await getSessionMessages(sid);
|
|
13214
|
+
const lastUserAt = lastUserMessageTimeOf(messages);
|
|
13204
13215
|
for (const msg of messages) {
|
|
13216
|
+
if (lastUserAt !== undefined && messageTimeOf(msg) !== undefined && messageTimeOf(msg) <= lastUserAt) {
|
|
13217
|
+
continue;
|
|
13218
|
+
}
|
|
13205
13219
|
const parts = msg.parts;
|
|
13206
13220
|
if (!parts)
|
|
13207
13221
|
continue;
|
|
@@ -13245,6 +13259,39 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13245
13259
|
return;
|
|
13246
13260
|
return msg.role ?? msg.info?.role;
|
|
13247
13261
|
}
|
|
13262
|
+
function messageTimeOf(msg) {
|
|
13263
|
+
if (!msg)
|
|
13264
|
+
return;
|
|
13265
|
+
const direct = msg.time;
|
|
13266
|
+
const viaInfo = msg.info?.time;
|
|
13267
|
+
const raw = typeof direct === "number" ? direct : direct?.created ?? (typeof viaInfo === "number" ? viaInfo : viaInfo?.created);
|
|
13268
|
+
return typeof raw === "number" && raw > 0 ? raw : undefined;
|
|
13269
|
+
}
|
|
13270
|
+
function lastUserMessageTimeOf(messages) {
|
|
13271
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
13272
|
+
const msg = messages[i];
|
|
13273
|
+
if (roleOf(msg) !== "user")
|
|
13274
|
+
continue;
|
|
13275
|
+
const t = messageTimeOf(msg);
|
|
13276
|
+
if (t !== undefined)
|
|
13277
|
+
return t;
|
|
13278
|
+
}
|
|
13279
|
+
return;
|
|
13280
|
+
}
|
|
13281
|
+
function looksLikeOwnPrompt(props) {
|
|
13282
|
+
if (!props)
|
|
13283
|
+
return false;
|
|
13284
|
+
const parts = props.parts ?? props.info?.parts;
|
|
13285
|
+
let text;
|
|
13286
|
+
if (Array.isArray(parts)) {
|
|
13287
|
+
text = parts.filter((p) => p?.type === "text").map((p) => p.text).join(`
|
|
13288
|
+
`);
|
|
13289
|
+
}
|
|
13290
|
+
text = text ?? props.text ?? props.info?.text;
|
|
13291
|
+
if (!text)
|
|
13292
|
+
return false;
|
|
13293
|
+
return OWN_PROMPT_SHAPES.some((re) => re.test(text));
|
|
13294
|
+
}
|
|
13248
13295
|
async function lastAssistantEndsWithCelebration(sid) {
|
|
13249
13296
|
try {
|
|
13250
13297
|
const msgs = await getSessionMessages(sid);
|
|
@@ -14600,13 +14647,15 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14600
14647
|
const info = props?.info;
|
|
14601
14648
|
const role = info?.role ?? props?.role;
|
|
14602
14649
|
if (role === "user") {
|
|
14650
|
+
if (looksLikeOwnPrompt(props)) {
|
|
14651
|
+
break;
|
|
14652
|
+
}
|
|
14603
14653
|
const w = sessions.get(sid);
|
|
14604
14654
|
if (w) {
|
|
14605
14655
|
w.doneClaimNoTodosAttempts = 0;
|
|
14606
14656
|
w.lastUserMessageAt = Date.now();
|
|
14607
14657
|
w.unknownToolErrors.clear();
|
|
14608
14658
|
w.unknownToolSuggestionSent = false;
|
|
14609
|
-
w.checkedToolPartIDs.clear();
|
|
14610
14659
|
}
|
|
14611
14660
|
break;
|
|
14612
14661
|
}
|
|
@@ -14862,7 +14911,7 @@ ${reminder}`;
|
|
|
14862
14911
|
};
|
|
14863
14912
|
|
|
14864
14913
|
// src/v2/index.ts
|
|
14865
|
-
import { readFileSync, mkdirSync,
|
|
14914
|
+
import { readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
14866
14915
|
import { dirname, join } from "path";
|
|
14867
14916
|
import { fileURLToPath } from "url";
|
|
14868
14917
|
import { readFile } from "fs/promises";
|
|
@@ -14968,7 +15017,6 @@ var DONE_CLAIM_PATTERN_SOURCES = [
|
|
|
14968
15017
|
"\\b(?:all|everything)\\s+(?:[\\w-]+\\s+)?(?:is\\s+)?(?:complete|done|finished)",
|
|
14969
15018
|
"\\bnothing\\s+(?:else\\s+)?(?:left|remaining|to do)"
|
|
14970
15019
|
];
|
|
14971
|
-
var DEFAULT_DONE_CLAIM_PATTERNS = [...DONE_CLAIM_PATTERN_SOURCES];
|
|
14972
15020
|
var DEFAULT_READY_TO_CONTINUE_PATTERNS = [
|
|
14973
15021
|
"ready to continue with task",
|
|
14974
15022
|
"continuing with task",
|
|
@@ -15006,7 +15054,8 @@ var DEFAULT_INJECT_INTERVAL_MS = 15000;
|
|
|
15006
15054
|
var OWN_PROMPT_TIME_MS = 30000;
|
|
15007
15055
|
var MAX_IDLE_SESSIONS2 = 50;
|
|
15008
15056
|
var IDLE_CLEANUP_MS2 = 10 * 60000;
|
|
15009
|
-
var COMPACTION_STALE_TTL_MS =
|
|
15057
|
+
var COMPACTION_STALE_TTL_MS = 5 * 60000;
|
|
15058
|
+
var COMPLETION_LATCH_TTL_MS = 5 * 60000;
|
|
15010
15059
|
var PERMISSION_STALE_TTL_MS = 30 * 60000;
|
|
15011
15060
|
var NEVER_INTERRUPT_STALE_TTL_MS = 30 * 60000;
|
|
15012
15061
|
var TEXT_BUFFER_TRIM_LEN = 20000;
|
|
@@ -15340,9 +15389,6 @@ function isUserHandoff(text) {
|
|
|
15340
15389
|
];
|
|
15341
15390
|
return patterns.some((p) => p.test(last));
|
|
15342
15391
|
}
|
|
15343
|
-
function backoffMs2(attempt, base, max) {
|
|
15344
|
-
return Math.min(base * Math.pow(2, attempt - 1), max);
|
|
15345
|
-
}
|
|
15346
15392
|
function isSilenceReason(reason) {
|
|
15347
15393
|
return /^no activity for \d+s/i.test(reason);
|
|
15348
15394
|
}
|
|
@@ -15479,15 +15525,16 @@ function messageTextOf(m) {
|
|
|
15479
15525
|
return mm.info.content;
|
|
15480
15526
|
return;
|
|
15481
15527
|
}
|
|
15482
|
-
var
|
|
15528
|
+
var OWN_PROMPT_SHAPES2 = [
|
|
15483
15529
|
/^continue\s*\u2014\s*stalled\s*\(/,
|
|
15484
15530
|
/^continue\s*\nOnly continue work already in progress\./,
|
|
15485
|
-
/^continue\s*\nIf work was already in progress
|
|
15531
|
+
/^continue\s*\nIf work was already in progress,/,
|
|
15532
|
+
/^You tried to use the tool "/
|
|
15486
15533
|
];
|
|
15487
15534
|
function looksLikeOwnPrompt(text) {
|
|
15488
15535
|
if (typeof text !== "string" || text.length === 0)
|
|
15489
15536
|
return false;
|
|
15490
|
-
return
|
|
15537
|
+
return OWN_PROMPT_SHAPES2.some((re) => re.test(text));
|
|
15491
15538
|
}
|
|
15492
15539
|
function isTaskToolCall(ev) {
|
|
15493
15540
|
const tool = ev.data?.tool ?? ev.data?.toolName;
|
|
@@ -15532,6 +15579,7 @@ var v2_default = define({
|
|
|
15532
15579
|
const checkIntervalMs = opts.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS2;
|
|
15533
15580
|
const gracePeriodMs = opts.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS2;
|
|
15534
15581
|
const stuckToolAgeMs = opts.stuckToolAgeMs ?? DEFAULT_STUCK_TOOL_AGE_MS;
|
|
15582
|
+
const completionLatchTtlMs = opts.completionLatchTtlMs ?? COMPLETION_LATCH_TTL_MS;
|
|
15535
15583
|
const neverInterruptTools = (opts.neverInterruptTools ?? ["compress"]).map((n) => String(n).toLowerCase());
|
|
15536
15584
|
const maxRetries = opts.maxRetries ?? opts.maxRecoveryRetries ?? DEFAULT_MAX_RETRIES2;
|
|
15537
15585
|
const baseBackoff = opts.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS2;
|
|
@@ -15577,12 +15625,13 @@ var v2_default = define({
|
|
|
15577
15625
|
}
|
|
15578
15626
|
return out;
|
|
15579
15627
|
}
|
|
15580
|
-
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns,
|
|
15628
|
+
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns, DONE_CLAIM_PATTERN_SOURCES, "im");
|
|
15581
15629
|
const readyToContinuePatterns = compilePatterns(opts.readyToContinuePatterns, DEFAULT_READY_TO_CONTINUE_PATTERNS, "i");
|
|
15582
15630
|
const streamingFailureMessageRegexes = compilePatterns(streamingFailureMessagePatterns, DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS2, "i");
|
|
15583
15631
|
const FEATURE_GATED_OPTIONS = [];
|
|
15584
15632
|
const RECOGNISED_OPTIONS = new Set([
|
|
15585
15633
|
"chunkTimeoutMs",
|
|
15634
|
+
"completionLatchTtlMs",
|
|
15586
15635
|
"checkIntervalMs",
|
|
15587
15636
|
"gracePeriodMs",
|
|
15588
15637
|
"maxRetries",
|
|
@@ -15636,6 +15685,7 @@ var v2_default = define({
|
|
|
15636
15685
|
}
|
|
15637
15686
|
if (typeof ctx.app?.version !== "string") {
|
|
15638
15687
|
log("warn", "this looks like the opencode v1 build running on the wrong server: ctx.app.version is missing. The v2 bundle requires opencode v2 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).");
|
|
15688
|
+
return () => {};
|
|
15639
15689
|
}
|
|
15640
15690
|
const gatedInUse = Object.keys(opts).filter((key) => FEATURE_GATED_OPTIONS.includes(key));
|
|
15641
15691
|
const dbg = (...args) => {
|
|
@@ -15710,7 +15760,6 @@ var v2_default = define({
|
|
|
15710
15760
|
providerRetryAt: 0,
|
|
15711
15761
|
quotaNoticedAt: 0,
|
|
15712
15762
|
gaveUp: false,
|
|
15713
|
-
lastQuotaNoticeAt: 0,
|
|
15714
15763
|
paused: false,
|
|
15715
15764
|
lastGenuineUserAt: 0,
|
|
15716
15765
|
consecFailureRecovers: 0,
|
|
@@ -15744,6 +15793,8 @@ var v2_default = define({
|
|
|
15744
15793
|
intentNudgeAttempts: 0,
|
|
15745
15794
|
completionSignaled: false,
|
|
15746
15795
|
completionSignaledAt: 0,
|
|
15796
|
+
completionHard: false,
|
|
15797
|
+
latchReopened: false,
|
|
15747
15798
|
emptyIdleNudged: false,
|
|
15748
15799
|
todos: [],
|
|
15749
15800
|
todosFetchedAt: 0,
|
|
@@ -15801,14 +15852,8 @@ var v2_default = define({
|
|
|
15801
15852
|
const lifetimeMaxContinues = opts.lifetimeMaxContinues ?? base.lifetimeMaxContinues;
|
|
15802
15853
|
return { ...base, chunkTimeoutMs, gracePeriodMs, maxRetries, lifetimeMaxContinues };
|
|
15803
15854
|
}
|
|
15804
|
-
let pausedCacheMtime = 0;
|
|
15805
|
-
let pausedCacheSet = new Set;
|
|
15806
15855
|
function readPausedSet() {
|
|
15807
15856
|
try {
|
|
15808
|
-
const st = statSync(PAUSED_MARKER_FILE);
|
|
15809
|
-
if (st.mtimeMs === pausedCacheMtime)
|
|
15810
|
-
return pausedCacheSet;
|
|
15811
|
-
pausedCacheMtime = st.mtimeMs;
|
|
15812
15857
|
const raw = readFileSync(PAUSED_MARKER_FILE, "utf8");
|
|
15813
15858
|
const s = new Set;
|
|
15814
15859
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -15816,12 +15861,9 @@ var v2_default = define({
|
|
|
15816
15861
|
if (t)
|
|
15817
15862
|
s.add(t);
|
|
15818
15863
|
}
|
|
15819
|
-
pausedCacheSet = s;
|
|
15820
15864
|
return s;
|
|
15821
15865
|
} catch {
|
|
15822
|
-
|
|
15823
|
-
pausedCacheSet = new Set;
|
|
15824
|
-
return pausedCacheSet;
|
|
15866
|
+
return new Set;
|
|
15825
15867
|
}
|
|
15826
15868
|
}
|
|
15827
15869
|
function isPaused(sid) {
|
|
@@ -15841,7 +15883,6 @@ var v2_default = define({
|
|
|
15841
15883
|
writeFileSync(PAUSED_MARKER_FILE, [...s].join(`
|
|
15842
15884
|
`) + (s.size ? `
|
|
15843
15885
|
` : ""), "utf8");
|
|
15844
|
-
pausedCacheMtime = 0;
|
|
15845
15886
|
} catch (e) {
|
|
15846
15887
|
dbg(`${short(sid)} pause write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
15847
15888
|
}
|
|
@@ -15888,6 +15929,22 @@ var v2_default = define({
|
|
|
15888
15929
|
}
|
|
15889
15930
|
clearStalePermissionFlag(sid, w);
|
|
15890
15931
|
}
|
|
15932
|
+
function softLatchExpired(sid, w, now) {
|
|
15933
|
+
if (!w.completionSignaled)
|
|
15934
|
+
return false;
|
|
15935
|
+
if (w.completionHard || w.latchReopened)
|
|
15936
|
+
return false;
|
|
15937
|
+
if (!w.completionSignaledAt)
|
|
15938
|
+
return false;
|
|
15939
|
+
if (now - w.completionSignaledAt < completionLatchTtlMs)
|
|
15940
|
+
return false;
|
|
15941
|
+
w.latchReopened = true;
|
|
15942
|
+
w.completionSignaled = false;
|
|
15943
|
+
w.completionSignaledAt = 0;
|
|
15944
|
+
w.pendingRecoveryArmed = true;
|
|
15945
|
+
log("warn", `${short(sid)} soft completion latch older than ${Math.round(completionLatchTtlMs / 1000)}s with no user message \u2014 re-opening once for a self-check`);
|
|
15946
|
+
return true;
|
|
15947
|
+
}
|
|
15891
15948
|
function clearStalePermissionFlag(sid, w) {
|
|
15892
15949
|
if (!w.permissionPending)
|
|
15893
15950
|
return;
|
|
@@ -15955,6 +16012,36 @@ var v2_default = define({
|
|
|
15955
16012
|
const cooldownMin = Math.max(1, Math.round(ladder[rung] / 60000));
|
|
15956
16013
|
recover(sid, `rate-limit cooldown served (rung ${rung + 1}/${ladder.length}, ${cooldownMin}m)`);
|
|
15957
16014
|
}
|
|
16015
|
+
function quotaGuard(sid, w, messages) {
|
|
16016
|
+
try {
|
|
16017
|
+
const last = messages.length ? messages[messages.length - 1] : null;
|
|
16018
|
+
let qtype = "";
|
|
16019
|
+
let qmsg = "";
|
|
16020
|
+
if (last && msgRole(last) === "assistant") {
|
|
16021
|
+
const qe = last.error ?? last.info?.error;
|
|
16022
|
+
if (typeof qe === "string")
|
|
16023
|
+
qmsg = qe;
|
|
16024
|
+
else if (qe && typeof qe === "object") {
|
|
16025
|
+
qtype = String(qe.name ?? qe.type ?? "");
|
|
16026
|
+
qmsg = String(qe.message ?? "");
|
|
16027
|
+
}
|
|
16028
|
+
const qtxt = lastAssistantTextFrom(messages);
|
|
16029
|
+
if (qtxt)
|
|
16030
|
+
qmsg = qmsg ? `${qmsg} ${qtxt}` : qtxt;
|
|
16031
|
+
}
|
|
16032
|
+
if (isRateLimitError(qtype, qmsg)) {
|
|
16033
|
+
handleRateLimitFailure(sid, w, qtype, qmsg);
|
|
16034
|
+
return true;
|
|
16035
|
+
}
|
|
16036
|
+
} catch (e) {
|
|
16037
|
+
dbg(`${short(sid)} quota guard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
16038
|
+
}
|
|
16039
|
+
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
16040
|
+
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
16041
|
+
return true;
|
|
16042
|
+
}
|
|
16043
|
+
return false;
|
|
16044
|
+
}
|
|
15958
16045
|
function handleRateLimitFailure(sid, w, errType, errMsg) {
|
|
15959
16046
|
if (isPaused(sid)) {
|
|
15960
16047
|
dbg(`${short(sid)} paused \u2014 not arming quota ladder`);
|
|
@@ -15967,9 +16054,8 @@ var v2_default = define({
|
|
|
15967
16054
|
if (n >= ladder.length) {
|
|
15968
16055
|
w.awaitingQuotaRetry = false;
|
|
15969
16056
|
log("warn", `${short(sid)} ${tag} budget exhausted (${ladder.length} attempts) \u2014 standing down until user turn`);
|
|
15970
|
-
if (!w.quotaNoticedAt
|
|
16057
|
+
if (!w.quotaNoticedAt) {
|
|
15971
16058
|
w.quotaNoticedAt = Date.now();
|
|
15972
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
15973
16059
|
sendQuotaNotice(sid, `Rate limited \u2014 standing down until you return (budget exhausted after ${ladder.length} attempts).`);
|
|
15974
16060
|
}
|
|
15975
16061
|
return;
|
|
@@ -15987,9 +16073,8 @@ var v2_default = define({
|
|
|
15987
16073
|
w.awaitingQuotaRetry = true;
|
|
15988
16074
|
const waitMs = Math.max(0, ladder[n] - (now - w.rateLimitedAt));
|
|
15989
16075
|
log("warn", `${short(sid)} rate limited (${errType || "error"}) \u2014 standing down, next attempt in ${Math.ceil(waitMs / 1000)}s (${n + 1}/${ladder.length})`);
|
|
15990
|
-
if (!w.quotaNoticedAt
|
|
16076
|
+
if (!w.quotaNoticedAt) {
|
|
15991
16077
|
w.quotaNoticedAt = Date.now();
|
|
15992
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
15993
16078
|
const mins = Math.max(1, Math.round(waitMs / 60000));
|
|
15994
16079
|
const when = mins >= 60 ? `~${Math.floor(mins / 60)}h ${mins % 60}m` : `~${mins}m`;
|
|
15995
16080
|
sendQuotaNotice(sid, `Rate limited, retrying in ${when} (attempt ${n + 1}/${ladder.length}).`);
|
|
@@ -16469,13 +16554,12 @@ ${lines.join(`
|
|
|
16469
16554
|
});
|
|
16470
16555
|
const elapsed = w.lastActivityAt ? Math.round((Date.now() - w.lastActivityAt) / 1000) : 0;
|
|
16471
16556
|
const hms = (n) => n < 60 ? `${n}s` : n < 3600 ? `${Math.floor(n / 60)}m ${n % 60}s` : `${Math.floor(n / 3600)}h ${Math.floor(n % 3600 / 60)}m`;
|
|
16472
|
-
const isRateLimitReason = /rate-limit/i.test(reason);
|
|
16473
16557
|
const isSilence = isSilenceReason(reason);
|
|
16474
16558
|
const shown = providerFault ? attempt : attempt;
|
|
16475
16559
|
const retries = prospectiveRetries ?? w.workRetries;
|
|
16476
16560
|
const head = [
|
|
16561
|
+
/rate-limit/i.test(reason) ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
16477
16562
|
isSilence ? null : reason,
|
|
16478
|
-
isRateLimitReason ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
16479
16563
|
isSilence ? `idle ${hms(elapsed)}` : null,
|
|
16480
16564
|
now
|
|
16481
16565
|
].filter((x) => typeof x === "string" && x.length > 0).join("; ");
|
|
@@ -16556,7 +16640,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
16556
16640
|
}
|
|
16557
16641
|
w.recovering = true;
|
|
16558
16642
|
const attempt = providerFault ? Math.max(1, w.resumeAttempts) : w.resumeAttempts + 1;
|
|
16559
|
-
const delay =
|
|
16643
|
+
const delay = Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff);
|
|
16560
16644
|
if (providerFault) {
|
|
16561
16645
|
log("info", `${short(sid)} provider-fault stall (${reason}) \u2014 retry scheduled ${Math.round((Date.now() - w.providerRetryAt) / 1000)}s ago, holding the ladder at ${attempt}/${profileMaxRetries}`);
|
|
16562
16646
|
}
|
|
@@ -17043,6 +17127,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
17043
17127
|
if (w.completionSignaled) {
|
|
17044
17128
|
w.completionSignaled = false;
|
|
17045
17129
|
w.completionSignaledAt = 0;
|
|
17130
|
+
w.latchReopened = false;
|
|
17046
17131
|
dbg(`${short(sid)} genuine new user message \u2014 re-opening latched completion`);
|
|
17047
17132
|
}
|
|
17048
17133
|
if (w.resumeAttempts > 0) {
|
|
@@ -17223,7 +17308,10 @@ ${suffix}`).slice(0, 2000);
|
|
|
17223
17308
|
const available = await getAvailableToolIds();
|
|
17224
17309
|
if (available.length === 0)
|
|
17225
17310
|
return false;
|
|
17311
|
+
const lastUserAt = lastUserMessageAt(messages);
|
|
17226
17312
|
for (const msg of messages) {
|
|
17313
|
+
if (lastUserAt !== undefined && msgAt(msg) <= lastUserAt)
|
|
17314
|
+
continue;
|
|
17227
17315
|
const content = msg?.content;
|
|
17228
17316
|
if (!Array.isArray(content))
|
|
17229
17317
|
continue;
|
|
@@ -17325,6 +17413,7 @@ ${reminder}`;
|
|
|
17325
17413
|
}
|
|
17326
17414
|
w.completionSignaled = true;
|
|
17327
17415
|
w.completionSignaledAt = Date.now();
|
|
17416
|
+
w.completionHard = true;
|
|
17328
17417
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
17329
17418
|
w.taskCompleteSignals++;
|
|
17330
17419
|
if (w.taskCompleteSignals === 2)
|
|
@@ -17376,6 +17465,7 @@ ${reminder}`;
|
|
|
17376
17465
|
}
|
|
17377
17466
|
w.completionSignaled = true;
|
|
17378
17467
|
w.completionSignaledAt = Date.now();
|
|
17468
|
+
w.completionHard = true;
|
|
17379
17469
|
w.taskCompleteSignals = 0;
|
|
17380
17470
|
w.taskCompleteOverrides = 0;
|
|
17381
17471
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
@@ -17414,6 +17504,8 @@ ${reminder}`;
|
|
|
17414
17504
|
if (open.length === 0 && !w.completionSignaled) {
|
|
17415
17505
|
w.completionSignaled = true;
|
|
17416
17506
|
w.completionSignaledAt = Date.now();
|
|
17507
|
+
w.completionHard = /\btask\s+complete(d)?\b[.!]*$/i.test(stripBoundaryMarkers(tailText).split(`
|
|
17508
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0).pop() ?? "");
|
|
17417
17509
|
log("info", `${short(sid)} turn ends with a completion claim inside the activity window \u2014 latching completion, standing down`);
|
|
17418
17510
|
}
|
|
17419
17511
|
} else {
|
|
@@ -17452,6 +17544,7 @@ ${reminder}`;
|
|
|
17452
17544
|
if (!w.completionSignaled) {
|
|
17453
17545
|
w.completionSignaled = true;
|
|
17454
17546
|
w.completionSignaledAt = Date.now();
|
|
17547
|
+
w.completionHard = false;
|
|
17455
17548
|
log("info", `${short(sid)} turn ends with a celebration and no open todos \u2014 latching completion, not nudging`);
|
|
17456
17549
|
} else {
|
|
17457
17550
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -17484,6 +17577,8 @@ ${reminder}`;
|
|
|
17484
17577
|
if (!w.completionSignaled) {
|
|
17485
17578
|
w.completionSignaled = true;
|
|
17486
17579
|
w.completionSignaledAt = Date.now();
|
|
17580
|
+
w.completionHard = /\btask\s+complete(d)?\b[.!]*$/i.test(stripBoundaryMarkers(text).split(`
|
|
17581
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0).pop() ?? "");
|
|
17487
17582
|
log("info", `${short(sid)} turn ends with an explicit stop and no open todos \u2014 latching completion, not nudging`);
|
|
17488
17583
|
} else {
|
|
17489
17584
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -17830,7 +17925,10 @@ ${reminder}`;
|
|
|
17830
17925
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
17831
17926
|
w.completionSignaled = false;
|
|
17832
17927
|
w.completionSignaledAt = 0;
|
|
17928
|
+
w.latchReopened = false;
|
|
17833
17929
|
dbg(`${short(sid)} genuine new user message after latched completion \u2014 re-opening stall detection`);
|
|
17930
|
+
} else if (softLatchExpired(sid, w, now)) {
|
|
17931
|
+
dbg(`${short(sid)} soft completion latch expired \u2014 re-opening for one self-check`);
|
|
17834
17932
|
} else {
|
|
17835
17933
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
17836
17934
|
}
|
|
@@ -17850,33 +17948,8 @@ ${reminder}`;
|
|
|
17850
17948
|
const baseInterval = (profileChunkTimeoutMs + profileGracePeriodMs) * fuseMult;
|
|
17851
17949
|
const jitter = baseInterval > 60000 ? Math.floor(Math.random() * STALL_JITTER_MS * 2) - STALL_JITTER_MS : 0;
|
|
17852
17950
|
if (silence >= baseInterval + jitter) {
|
|
17853
|
-
|
|
17854
|
-
const _qlast2 = history.length ? history[history.length - 1] : null;
|
|
17855
|
-
let _qtype2 = "";
|
|
17856
|
-
let _qmsg2 = "";
|
|
17857
|
-
if (_qlast2 && msgRole(_qlast2) === "assistant") {
|
|
17858
|
-
const _qe2 = _qlast2.error ?? _qlast2.info?.error;
|
|
17859
|
-
if (typeof _qe2 === "string")
|
|
17860
|
-
_qmsg2 = _qe2;
|
|
17861
|
-
else if (_qe2 && typeof _qe2 === "object") {
|
|
17862
|
-
_qtype2 = String(_qe2.name ?? _qe2.type ?? "");
|
|
17863
|
-
_qmsg2 = String(_qe2.message ?? "");
|
|
17864
|
-
}
|
|
17865
|
-
const _qtxt2 = lastAssistantTextFrom(history);
|
|
17866
|
-
if (_qtxt2)
|
|
17867
|
-
_qmsg2 = _qmsg2 ? `${_qmsg2} ${_qtxt2}` : _qtxt2;
|
|
17868
|
-
}
|
|
17869
|
-
if (isRateLimitError(_qtype2, _qmsg2)) {
|
|
17870
|
-
handleRateLimitFailure(sid, w, _qtype2, _qmsg2);
|
|
17871
|
-
continue;
|
|
17872
|
-
}
|
|
17873
|
-
} catch (e) {
|
|
17874
|
-
dbg(`${short(sid)} quota guard (idle) failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
17875
|
-
}
|
|
17876
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
17877
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
17951
|
+
if (quotaGuard(sid, w, history))
|
|
17878
17952
|
continue;
|
|
17879
|
-
}
|
|
17880
17953
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
17881
17954
|
}
|
|
17882
17955
|
}
|
|
@@ -17900,7 +17973,10 @@ ${reminder}`;
|
|
|
17900
17973
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
17901
17974
|
w.completionSignaled = false;
|
|
17902
17975
|
w.completionSignaledAt = 0;
|
|
17976
|
+
w.latchReopened = false;
|
|
17903
17977
|
dbg(`${short(sid)} genuine new user message after latched completion \u2014 re-opening stall detection`);
|
|
17978
|
+
} else if (softLatchExpired(sid, w, now)) {
|
|
17979
|
+
dbg(`${short(sid)} soft completion latch expired \u2014 re-opening for one self-check`);
|
|
17904
17980
|
} else {
|
|
17905
17981
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
17906
17982
|
continue;
|
|
@@ -17997,34 +18073,9 @@ ${reminder}`;
|
|
|
17997
18073
|
log("info", `${short(sid)} stall (busyStallStrategy=abort): aborting before continue`);
|
|
17998
18074
|
await tryAbortAndResume(sid, w, true);
|
|
17999
18075
|
} else {
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
const _qlast = _qm.length ? _qm[_qm.length - 1] : null;
|
|
18003
|
-
let _qtype = "";
|
|
18004
|
-
let _qmsg = "";
|
|
18005
|
-
if (_qlast && msgRole(_qlast) === "assistant") {
|
|
18006
|
-
const _qe = _qlast.error ?? _qlast.info?.error;
|
|
18007
|
-
if (typeof _qe === "string")
|
|
18008
|
-
_qmsg = _qe;
|
|
18009
|
-
else if (_qe && typeof _qe === "object") {
|
|
18010
|
-
_qtype = String(_qe.name ?? _qe.type ?? "");
|
|
18011
|
-
_qmsg = String(_qe.message ?? "");
|
|
18012
|
-
}
|
|
18013
|
-
const _qtxt = lastAssistantTextFrom(_qm);
|
|
18014
|
-
if (_qtxt)
|
|
18015
|
-
_qmsg = _qmsg ? `${_qmsg} ${_qtxt}` : _qtxt;
|
|
18016
|
-
}
|
|
18017
|
-
if (isRateLimitError(_qtype, _qmsg)) {
|
|
18018
|
-
handleRateLimitFailure(sid, w, _qtype, _qmsg);
|
|
18019
|
-
continue;
|
|
18020
|
-
}
|
|
18021
|
-
} catch (e) {
|
|
18022
|
-
dbg(`${short(sid)} quota guard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
18023
|
-
}
|
|
18024
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
18025
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
18076
|
+
const _qm = await loadMessages(sid);
|
|
18077
|
+
if (quotaGuard(sid, w, _qm))
|
|
18026
18078
|
continue;
|
|
18027
|
-
}
|
|
18028
18079
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
18029
18080
|
}
|
|
18030
18081
|
}
|
|
@@ -18361,11 +18412,21 @@ ${reminder}`;
|
|
|
18361
18412
|
let ownRung = !providerRetryFresh && selfInjectFresh;
|
|
18362
18413
|
if (ownRung) {
|
|
18363
18414
|
try {
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
18367
|
-
|
|
18368
|
-
|
|
18415
|
+
const hist = await loadMessages(sid);
|
|
18416
|
+
for (const m of hist) {
|
|
18417
|
+
if (msgRole(m) !== "user")
|
|
18418
|
+
continue;
|
|
18419
|
+
if (msgAt(m) <= w.lastInjectAt)
|
|
18420
|
+
continue;
|
|
18421
|
+
const text = messageTextOf(m);
|
|
18422
|
+
const ownByText = typeof text === "string" && text.length > 0 && w.ownPromptTexts.includes(text);
|
|
18423
|
+
const ownByShape = looksLikeOwnPrompt(text);
|
|
18424
|
+
const ownByTime = msgAt(m) >= w.lastInjectAt && msgAt(m) - w.lastInjectAt < OWN_PROMPT_TIME_MS;
|
|
18425
|
+
const ownByTimeOnly = ownByTime && !(typeof text === "string" && text.length > 0);
|
|
18426
|
+
if (ownByText || ownByShape || ownByTimeOnly)
|
|
18427
|
+
continue;
|
|
18428
|
+
ownRung = false;
|
|
18429
|
+
break;
|
|
18369
18430
|
}
|
|
18370
18431
|
} catch {
|
|
18371
18432
|
ownRung = false;
|
|
@@ -18868,10 +18929,22 @@ ${reminder}`;
|
|
|
18868
18929
|
});
|
|
18869
18930
|
|
|
18870
18931
|
// src/dispatch.ts
|
|
18932
|
+
var noopDispose = () => {};
|
|
18871
18933
|
var dispatcher = {
|
|
18872
18934
|
id: "auto-resume.v2",
|
|
18873
18935
|
server: AutoResumePlugin,
|
|
18874
|
-
setup:
|
|
18936
|
+
setup: async (ctx) => {
|
|
18937
|
+
if (ctx?.app?.version)
|
|
18938
|
+
return v2_default.setup(ctx);
|
|
18939
|
+
if (typeof ctx?.event?.subscribe === "function") {
|
|
18940
|
+
return v2_default.setup(ctx);
|
|
18941
|
+
}
|
|
18942
|
+
if (ctx?.serverUrl || ctx?.client?.session) {
|
|
18943
|
+
return AutoResumePlugin(ctx, ctx.options);
|
|
18944
|
+
}
|
|
18945
|
+
console.warn("[auto-resume] unrecognised host context \u2014 no ctx.app.version (opencode v2), no ctx.event.subscribe (v2 loader), no ctx.serverUrl/client (opencode v1). Plugin disabled; see the README on npm (opencode-auto-resume).");
|
|
18946
|
+
return noopDispose;
|
|
18947
|
+
}
|
|
18875
18948
|
};
|
|
18876
18949
|
var dispatch_default = dispatcher;
|
|
18877
18950
|
export {
|
package/dist/v2/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/v2/index.ts
|
|
3
|
-
import { readFileSync, mkdirSync,
|
|
3
|
+
import { readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { readFile } from "fs/promises";
|
|
@@ -106,7 +106,6 @@ var DONE_CLAIM_PATTERN_SOURCES = [
|
|
|
106
106
|
"\\b(?:all|everything)\\s+(?:[\\w-]+\\s+)?(?:is\\s+)?(?:complete|done|finished)",
|
|
107
107
|
"\\bnothing\\s+(?:else\\s+)?(?:left|remaining|to do)"
|
|
108
108
|
];
|
|
109
|
-
var DEFAULT_DONE_CLAIM_PATTERNS = [...DONE_CLAIM_PATTERN_SOURCES];
|
|
110
109
|
var DEFAULT_READY_TO_CONTINUE_PATTERNS = [
|
|
111
110
|
"ready to continue with task",
|
|
112
111
|
"continuing with task",
|
|
@@ -144,7 +143,8 @@ var DEFAULT_INJECT_INTERVAL_MS = 15000;
|
|
|
144
143
|
var OWN_PROMPT_TIME_MS = 30000;
|
|
145
144
|
var MAX_IDLE_SESSIONS = 50;
|
|
146
145
|
var IDLE_CLEANUP_MS = 10 * 60000;
|
|
147
|
-
var COMPACTION_STALE_TTL_MS =
|
|
146
|
+
var COMPACTION_STALE_TTL_MS = 5 * 60000;
|
|
147
|
+
var COMPLETION_LATCH_TTL_MS = 5 * 60000;
|
|
148
148
|
var PERMISSION_STALE_TTL_MS = 30 * 60000;
|
|
149
149
|
var NEVER_INTERRUPT_STALE_TTL_MS = 30 * 60000;
|
|
150
150
|
var TEXT_BUFFER_TRIM_LEN = 20000;
|
|
@@ -478,9 +478,6 @@ function isUserHandoff(text) {
|
|
|
478
478
|
];
|
|
479
479
|
return patterns.some((p) => p.test(last));
|
|
480
480
|
}
|
|
481
|
-
function backoffMs(attempt, base, max) {
|
|
482
|
-
return Math.min(base * Math.pow(2, attempt - 1), max);
|
|
483
|
-
}
|
|
484
481
|
function isSilenceReason(reason) {
|
|
485
482
|
return /^no activity for \d+s/i.test(reason);
|
|
486
483
|
}
|
|
@@ -620,7 +617,8 @@ function messageTextOf(m) {
|
|
|
620
617
|
var OWN_PROMPT_SHAPES = [
|
|
621
618
|
/^continue\s*\u2014\s*stalled\s*\(/,
|
|
622
619
|
/^continue\s*\nOnly continue work already in progress\./,
|
|
623
|
-
/^continue\s*\nIf work was already in progress
|
|
620
|
+
/^continue\s*\nIf work was already in progress,/,
|
|
621
|
+
/^You tried to use the tool "/
|
|
624
622
|
];
|
|
625
623
|
function looksLikeOwnPrompt(text) {
|
|
626
624
|
if (typeof text !== "string" || text.length === 0)
|
|
@@ -670,6 +668,7 @@ var v2_default = define({
|
|
|
670
668
|
const checkIntervalMs = opts.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
|
|
671
669
|
const gracePeriodMs = opts.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
|
|
672
670
|
const stuckToolAgeMs = opts.stuckToolAgeMs ?? DEFAULT_STUCK_TOOL_AGE_MS;
|
|
671
|
+
const completionLatchTtlMs = opts.completionLatchTtlMs ?? COMPLETION_LATCH_TTL_MS;
|
|
673
672
|
const neverInterruptTools = (opts.neverInterruptTools ?? ["compress"]).map((n) => String(n).toLowerCase());
|
|
674
673
|
const maxRetries = opts.maxRetries ?? opts.maxRecoveryRetries ?? DEFAULT_MAX_RETRIES;
|
|
675
674
|
const baseBackoff = opts.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS;
|
|
@@ -715,12 +714,13 @@ var v2_default = define({
|
|
|
715
714
|
}
|
|
716
715
|
return out;
|
|
717
716
|
}
|
|
718
|
-
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns,
|
|
717
|
+
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns, DONE_CLAIM_PATTERN_SOURCES, "im");
|
|
719
718
|
const readyToContinuePatterns = compilePatterns(opts.readyToContinuePatterns, DEFAULT_READY_TO_CONTINUE_PATTERNS, "i");
|
|
720
719
|
const streamingFailureMessageRegexes = compilePatterns(streamingFailureMessagePatterns, DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS, "i");
|
|
721
720
|
const FEATURE_GATED_OPTIONS = [];
|
|
722
721
|
const RECOGNISED_OPTIONS = new Set([
|
|
723
722
|
"chunkTimeoutMs",
|
|
723
|
+
"completionLatchTtlMs",
|
|
724
724
|
"checkIntervalMs",
|
|
725
725
|
"gracePeriodMs",
|
|
726
726
|
"maxRetries",
|
|
@@ -774,6 +774,7 @@ var v2_default = define({
|
|
|
774
774
|
}
|
|
775
775
|
if (typeof ctx.app?.version !== "string") {
|
|
776
776
|
log("warn", "this looks like the opencode v1 build running on the wrong server: ctx.app.version is missing. The v2 bundle requires opencode v2 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).");
|
|
777
|
+
return () => {};
|
|
777
778
|
}
|
|
778
779
|
const gatedInUse = Object.keys(opts).filter((key) => FEATURE_GATED_OPTIONS.includes(key));
|
|
779
780
|
const dbg = (...args) => {
|
|
@@ -848,7 +849,6 @@ var v2_default = define({
|
|
|
848
849
|
providerRetryAt: 0,
|
|
849
850
|
quotaNoticedAt: 0,
|
|
850
851
|
gaveUp: false,
|
|
851
|
-
lastQuotaNoticeAt: 0,
|
|
852
852
|
paused: false,
|
|
853
853
|
lastGenuineUserAt: 0,
|
|
854
854
|
consecFailureRecovers: 0,
|
|
@@ -882,6 +882,8 @@ var v2_default = define({
|
|
|
882
882
|
intentNudgeAttempts: 0,
|
|
883
883
|
completionSignaled: false,
|
|
884
884
|
completionSignaledAt: 0,
|
|
885
|
+
completionHard: false,
|
|
886
|
+
latchReopened: false,
|
|
885
887
|
emptyIdleNudged: false,
|
|
886
888
|
todos: [],
|
|
887
889
|
todosFetchedAt: 0,
|
|
@@ -939,14 +941,8 @@ var v2_default = define({
|
|
|
939
941
|
const lifetimeMaxContinues = opts.lifetimeMaxContinues ?? base.lifetimeMaxContinues;
|
|
940
942
|
return { ...base, chunkTimeoutMs, gracePeriodMs, maxRetries, lifetimeMaxContinues };
|
|
941
943
|
}
|
|
942
|
-
let pausedCacheMtime = 0;
|
|
943
|
-
let pausedCacheSet = new Set;
|
|
944
944
|
function readPausedSet() {
|
|
945
945
|
try {
|
|
946
|
-
const st = statSync(PAUSED_MARKER_FILE);
|
|
947
|
-
if (st.mtimeMs === pausedCacheMtime)
|
|
948
|
-
return pausedCacheSet;
|
|
949
|
-
pausedCacheMtime = st.mtimeMs;
|
|
950
946
|
const raw = readFileSync(PAUSED_MARKER_FILE, "utf8");
|
|
951
947
|
const s = new Set;
|
|
952
948
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -954,12 +950,9 @@ var v2_default = define({
|
|
|
954
950
|
if (t)
|
|
955
951
|
s.add(t);
|
|
956
952
|
}
|
|
957
|
-
pausedCacheSet = s;
|
|
958
953
|
return s;
|
|
959
954
|
} catch {
|
|
960
|
-
|
|
961
|
-
pausedCacheSet = new Set;
|
|
962
|
-
return pausedCacheSet;
|
|
955
|
+
return new Set;
|
|
963
956
|
}
|
|
964
957
|
}
|
|
965
958
|
function isPaused(sid) {
|
|
@@ -979,7 +972,6 @@ var v2_default = define({
|
|
|
979
972
|
writeFileSync(PAUSED_MARKER_FILE, [...s].join(`
|
|
980
973
|
`) + (s.size ? `
|
|
981
974
|
` : ""), "utf8");
|
|
982
|
-
pausedCacheMtime = 0;
|
|
983
975
|
} catch (e) {
|
|
984
976
|
dbg(`${short(sid)} pause write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
985
977
|
}
|
|
@@ -1026,6 +1018,22 @@ var v2_default = define({
|
|
|
1026
1018
|
}
|
|
1027
1019
|
clearStalePermissionFlag(sid, w);
|
|
1028
1020
|
}
|
|
1021
|
+
function softLatchExpired(sid, w, now) {
|
|
1022
|
+
if (!w.completionSignaled)
|
|
1023
|
+
return false;
|
|
1024
|
+
if (w.completionHard || w.latchReopened)
|
|
1025
|
+
return false;
|
|
1026
|
+
if (!w.completionSignaledAt)
|
|
1027
|
+
return false;
|
|
1028
|
+
if (now - w.completionSignaledAt < completionLatchTtlMs)
|
|
1029
|
+
return false;
|
|
1030
|
+
w.latchReopened = true;
|
|
1031
|
+
w.completionSignaled = false;
|
|
1032
|
+
w.completionSignaledAt = 0;
|
|
1033
|
+
w.pendingRecoveryArmed = true;
|
|
1034
|
+
log("warn", `${short(sid)} soft completion latch older than ${Math.round(completionLatchTtlMs / 1000)}s with no user message \u2014 re-opening once for a self-check`);
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1029
1037
|
function clearStalePermissionFlag(sid, w) {
|
|
1030
1038
|
if (!w.permissionPending)
|
|
1031
1039
|
return;
|
|
@@ -1093,6 +1101,36 @@ var v2_default = define({
|
|
|
1093
1101
|
const cooldownMin = Math.max(1, Math.round(ladder[rung] / 60000));
|
|
1094
1102
|
recover(sid, `rate-limit cooldown served (rung ${rung + 1}/${ladder.length}, ${cooldownMin}m)`);
|
|
1095
1103
|
}
|
|
1104
|
+
function quotaGuard(sid, w, messages) {
|
|
1105
|
+
try {
|
|
1106
|
+
const last = messages.length ? messages[messages.length - 1] : null;
|
|
1107
|
+
let qtype = "";
|
|
1108
|
+
let qmsg = "";
|
|
1109
|
+
if (last && msgRole(last) === "assistant") {
|
|
1110
|
+
const qe = last.error ?? last.info?.error;
|
|
1111
|
+
if (typeof qe === "string")
|
|
1112
|
+
qmsg = qe;
|
|
1113
|
+
else if (qe && typeof qe === "object") {
|
|
1114
|
+
qtype = String(qe.name ?? qe.type ?? "");
|
|
1115
|
+
qmsg = String(qe.message ?? "");
|
|
1116
|
+
}
|
|
1117
|
+
const qtxt = lastAssistantTextFrom(messages);
|
|
1118
|
+
if (qtxt)
|
|
1119
|
+
qmsg = qmsg ? `${qmsg} ${qtxt}` : qtxt;
|
|
1120
|
+
}
|
|
1121
|
+
if (isRateLimitError(qtype, qmsg)) {
|
|
1122
|
+
handleRateLimitFailure(sid, w, qtype, qmsg);
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
dbg(`${short(sid)} quota guard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1127
|
+
}
|
|
1128
|
+
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
1129
|
+
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1096
1134
|
function handleRateLimitFailure(sid, w, errType, errMsg) {
|
|
1097
1135
|
if (isPaused(sid)) {
|
|
1098
1136
|
dbg(`${short(sid)} paused \u2014 not arming quota ladder`);
|
|
@@ -1105,9 +1143,8 @@ var v2_default = define({
|
|
|
1105
1143
|
if (n >= ladder.length) {
|
|
1106
1144
|
w.awaitingQuotaRetry = false;
|
|
1107
1145
|
log("warn", `${short(sid)} ${tag} budget exhausted (${ladder.length} attempts) \u2014 standing down until user turn`);
|
|
1108
|
-
if (!w.quotaNoticedAt
|
|
1146
|
+
if (!w.quotaNoticedAt) {
|
|
1109
1147
|
w.quotaNoticedAt = Date.now();
|
|
1110
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
1111
1148
|
sendQuotaNotice(sid, `Rate limited \u2014 standing down until you return (budget exhausted after ${ladder.length} attempts).`);
|
|
1112
1149
|
}
|
|
1113
1150
|
return;
|
|
@@ -1125,9 +1162,8 @@ var v2_default = define({
|
|
|
1125
1162
|
w.awaitingQuotaRetry = true;
|
|
1126
1163
|
const waitMs = Math.max(0, ladder[n] - (now - w.rateLimitedAt));
|
|
1127
1164
|
log("warn", `${short(sid)} rate limited (${errType || "error"}) \u2014 standing down, next attempt in ${Math.ceil(waitMs / 1000)}s (${n + 1}/${ladder.length})`);
|
|
1128
|
-
if (!w.quotaNoticedAt
|
|
1165
|
+
if (!w.quotaNoticedAt) {
|
|
1129
1166
|
w.quotaNoticedAt = Date.now();
|
|
1130
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
1131
1167
|
const mins = Math.max(1, Math.round(waitMs / 60000));
|
|
1132
1168
|
const when = mins >= 60 ? `~${Math.floor(mins / 60)}h ${mins % 60}m` : `~${mins}m`;
|
|
1133
1169
|
sendQuotaNotice(sid, `Rate limited, retrying in ${when} (attempt ${n + 1}/${ladder.length}).`);
|
|
@@ -1607,13 +1643,12 @@ ${lines.join(`
|
|
|
1607
1643
|
});
|
|
1608
1644
|
const elapsed = w.lastActivityAt ? Math.round((Date.now() - w.lastActivityAt) / 1000) : 0;
|
|
1609
1645
|
const hms = (n) => n < 60 ? `${n}s` : n < 3600 ? `${Math.floor(n / 60)}m ${n % 60}s` : `${Math.floor(n / 3600)}h ${Math.floor(n % 3600 / 60)}m`;
|
|
1610
|
-
const isRateLimitReason = /rate-limit/i.test(reason);
|
|
1611
1646
|
const isSilence = isSilenceReason(reason);
|
|
1612
1647
|
const shown = providerFault ? attempt : attempt;
|
|
1613
1648
|
const retries = prospectiveRetries ?? w.workRetries;
|
|
1614
1649
|
const head = [
|
|
1650
|
+
/rate-limit/i.test(reason) ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
1615
1651
|
isSilence ? null : reason,
|
|
1616
|
-
isRateLimitReason ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
1617
1652
|
isSilence ? `idle ${hms(elapsed)}` : null,
|
|
1618
1653
|
now
|
|
1619
1654
|
].filter((x) => typeof x === "string" && x.length > 0).join("; ");
|
|
@@ -1694,7 +1729,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
1694
1729
|
}
|
|
1695
1730
|
w.recovering = true;
|
|
1696
1731
|
const attempt = providerFault ? Math.max(1, w.resumeAttempts) : w.resumeAttempts + 1;
|
|
1697
|
-
const delay =
|
|
1732
|
+
const delay = Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff);
|
|
1698
1733
|
if (providerFault) {
|
|
1699
1734
|
log("info", `${short(sid)} provider-fault stall (${reason}) \u2014 retry scheduled ${Math.round((Date.now() - w.providerRetryAt) / 1000)}s ago, holding the ladder at ${attempt}/${profileMaxRetries}`);
|
|
1700
1735
|
}
|
|
@@ -2181,6 +2216,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
2181
2216
|
if (w.completionSignaled) {
|
|
2182
2217
|
w.completionSignaled = false;
|
|
2183
2218
|
w.completionSignaledAt = 0;
|
|
2219
|
+
w.latchReopened = false;
|
|
2184
2220
|
dbg(`${short(sid)} genuine new user message \u2014 re-opening latched completion`);
|
|
2185
2221
|
}
|
|
2186
2222
|
if (w.resumeAttempts > 0) {
|
|
@@ -2361,7 +2397,10 @@ ${suffix}`).slice(0, 2000);
|
|
|
2361
2397
|
const available = await getAvailableToolIds();
|
|
2362
2398
|
if (available.length === 0)
|
|
2363
2399
|
return false;
|
|
2400
|
+
const lastUserAt = lastUserMessageAt(messages);
|
|
2364
2401
|
for (const msg of messages) {
|
|
2402
|
+
if (lastUserAt !== undefined && msgAt(msg) <= lastUserAt)
|
|
2403
|
+
continue;
|
|
2365
2404
|
const content = msg?.content;
|
|
2366
2405
|
if (!Array.isArray(content))
|
|
2367
2406
|
continue;
|
|
@@ -2463,6 +2502,7 @@ ${reminder}`;
|
|
|
2463
2502
|
}
|
|
2464
2503
|
w.completionSignaled = true;
|
|
2465
2504
|
w.completionSignaledAt = Date.now();
|
|
2505
|
+
w.completionHard = true;
|
|
2466
2506
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
2467
2507
|
w.taskCompleteSignals++;
|
|
2468
2508
|
if (w.taskCompleteSignals === 2)
|
|
@@ -2514,6 +2554,7 @@ ${reminder}`;
|
|
|
2514
2554
|
}
|
|
2515
2555
|
w.completionSignaled = true;
|
|
2516
2556
|
w.completionSignaledAt = Date.now();
|
|
2557
|
+
w.completionHard = true;
|
|
2517
2558
|
w.taskCompleteSignals = 0;
|
|
2518
2559
|
w.taskCompleteOverrides = 0;
|
|
2519
2560
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
@@ -2552,6 +2593,8 @@ ${reminder}`;
|
|
|
2552
2593
|
if (open.length === 0 && !w.completionSignaled) {
|
|
2553
2594
|
w.completionSignaled = true;
|
|
2554
2595
|
w.completionSignaledAt = Date.now();
|
|
2596
|
+
w.completionHard = /\btask\s+complete(d)?\b[.!]*$/i.test(stripBoundaryMarkers(tailText).split(`
|
|
2597
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0).pop() ?? "");
|
|
2555
2598
|
log("info", `${short(sid)} turn ends with a completion claim inside the activity window \u2014 latching completion, standing down`);
|
|
2556
2599
|
}
|
|
2557
2600
|
} else {
|
|
@@ -2590,6 +2633,7 @@ ${reminder}`;
|
|
|
2590
2633
|
if (!w.completionSignaled) {
|
|
2591
2634
|
w.completionSignaled = true;
|
|
2592
2635
|
w.completionSignaledAt = Date.now();
|
|
2636
|
+
w.completionHard = false;
|
|
2593
2637
|
log("info", `${short(sid)} turn ends with a celebration and no open todos \u2014 latching completion, not nudging`);
|
|
2594
2638
|
} else {
|
|
2595
2639
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -2622,6 +2666,8 @@ ${reminder}`;
|
|
|
2622
2666
|
if (!w.completionSignaled) {
|
|
2623
2667
|
w.completionSignaled = true;
|
|
2624
2668
|
w.completionSignaledAt = Date.now();
|
|
2669
|
+
w.completionHard = /\btask\s+complete(d)?\b[.!]*$/i.test(stripBoundaryMarkers(text).split(`
|
|
2670
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0).pop() ?? "");
|
|
2625
2671
|
log("info", `${short(sid)} turn ends with an explicit stop and no open todos \u2014 latching completion, not nudging`);
|
|
2626
2672
|
} else {
|
|
2627
2673
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -2968,7 +3014,10 @@ ${reminder}`;
|
|
|
2968
3014
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
2969
3015
|
w.completionSignaled = false;
|
|
2970
3016
|
w.completionSignaledAt = 0;
|
|
3017
|
+
w.latchReopened = false;
|
|
2971
3018
|
dbg(`${short(sid)} genuine new user message after latched completion \u2014 re-opening stall detection`);
|
|
3019
|
+
} else if (softLatchExpired(sid, w, now)) {
|
|
3020
|
+
dbg(`${short(sid)} soft completion latch expired \u2014 re-opening for one self-check`);
|
|
2972
3021
|
} else {
|
|
2973
3022
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
2974
3023
|
}
|
|
@@ -2988,33 +3037,8 @@ ${reminder}`;
|
|
|
2988
3037
|
const baseInterval = (profileChunkTimeoutMs + profileGracePeriodMs) * fuseMult;
|
|
2989
3038
|
const jitter = baseInterval > 60000 ? Math.floor(Math.random() * STALL_JITTER_MS * 2) - STALL_JITTER_MS : 0;
|
|
2990
3039
|
if (silence >= baseInterval + jitter) {
|
|
2991
|
-
|
|
2992
|
-
const _qlast2 = history.length ? history[history.length - 1] : null;
|
|
2993
|
-
let _qtype2 = "";
|
|
2994
|
-
let _qmsg2 = "";
|
|
2995
|
-
if (_qlast2 && msgRole(_qlast2) === "assistant") {
|
|
2996
|
-
const _qe2 = _qlast2.error ?? _qlast2.info?.error;
|
|
2997
|
-
if (typeof _qe2 === "string")
|
|
2998
|
-
_qmsg2 = _qe2;
|
|
2999
|
-
else if (_qe2 && typeof _qe2 === "object") {
|
|
3000
|
-
_qtype2 = String(_qe2.name ?? _qe2.type ?? "");
|
|
3001
|
-
_qmsg2 = String(_qe2.message ?? "");
|
|
3002
|
-
}
|
|
3003
|
-
const _qtxt2 = lastAssistantTextFrom(history);
|
|
3004
|
-
if (_qtxt2)
|
|
3005
|
-
_qmsg2 = _qmsg2 ? `${_qmsg2} ${_qtxt2}` : _qtxt2;
|
|
3006
|
-
}
|
|
3007
|
-
if (isRateLimitError(_qtype2, _qmsg2)) {
|
|
3008
|
-
handleRateLimitFailure(sid, w, _qtype2, _qmsg2);
|
|
3009
|
-
continue;
|
|
3010
|
-
}
|
|
3011
|
-
} catch (e) {
|
|
3012
|
-
dbg(`${short(sid)} quota guard (idle) failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3013
|
-
}
|
|
3014
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
3015
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
3040
|
+
if (quotaGuard(sid, w, history))
|
|
3016
3041
|
continue;
|
|
3017
|
-
}
|
|
3018
3042
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
3019
3043
|
}
|
|
3020
3044
|
}
|
|
@@ -3038,7 +3062,10 @@ ${reminder}`;
|
|
|
3038
3062
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
3039
3063
|
w.completionSignaled = false;
|
|
3040
3064
|
w.completionSignaledAt = 0;
|
|
3065
|
+
w.latchReopened = false;
|
|
3041
3066
|
dbg(`${short(sid)} genuine new user message after latched completion \u2014 re-opening stall detection`);
|
|
3067
|
+
} else if (softLatchExpired(sid, w, now)) {
|
|
3068
|
+
dbg(`${short(sid)} soft completion latch expired \u2014 re-opening for one self-check`);
|
|
3042
3069
|
} else {
|
|
3043
3070
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
3044
3071
|
continue;
|
|
@@ -3135,34 +3162,9 @@ ${reminder}`;
|
|
|
3135
3162
|
log("info", `${short(sid)} stall (busyStallStrategy=abort): aborting before continue`);
|
|
3136
3163
|
await tryAbortAndResume(sid, w, true);
|
|
3137
3164
|
} else {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
const _qlast = _qm.length ? _qm[_qm.length - 1] : null;
|
|
3141
|
-
let _qtype = "";
|
|
3142
|
-
let _qmsg = "";
|
|
3143
|
-
if (_qlast && msgRole(_qlast) === "assistant") {
|
|
3144
|
-
const _qe = _qlast.error ?? _qlast.info?.error;
|
|
3145
|
-
if (typeof _qe === "string")
|
|
3146
|
-
_qmsg = _qe;
|
|
3147
|
-
else if (_qe && typeof _qe === "object") {
|
|
3148
|
-
_qtype = String(_qe.name ?? _qe.type ?? "");
|
|
3149
|
-
_qmsg = String(_qe.message ?? "");
|
|
3150
|
-
}
|
|
3151
|
-
const _qtxt = lastAssistantTextFrom(_qm);
|
|
3152
|
-
if (_qtxt)
|
|
3153
|
-
_qmsg = _qmsg ? `${_qmsg} ${_qtxt}` : _qtxt;
|
|
3154
|
-
}
|
|
3155
|
-
if (isRateLimitError(_qtype, _qmsg)) {
|
|
3156
|
-
handleRateLimitFailure(sid, w, _qtype, _qmsg);
|
|
3157
|
-
continue;
|
|
3158
|
-
}
|
|
3159
|
-
} catch (e) {
|
|
3160
|
-
dbg(`${short(sid)} quota guard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3161
|
-
}
|
|
3162
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
3163
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
3165
|
+
const _qm = await loadMessages(sid);
|
|
3166
|
+
if (quotaGuard(sid, w, _qm))
|
|
3164
3167
|
continue;
|
|
3165
|
-
}
|
|
3166
3168
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
3167
3169
|
}
|
|
3168
3170
|
}
|
|
@@ -3499,11 +3501,21 @@ ${reminder}`;
|
|
|
3499
3501
|
let ownRung = !providerRetryFresh && selfInjectFresh;
|
|
3500
3502
|
if (ownRung) {
|
|
3501
3503
|
try {
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3504
|
+
const hist = await loadMessages(sid);
|
|
3505
|
+
for (const m of hist) {
|
|
3506
|
+
if (msgRole(m) !== "user")
|
|
3507
|
+
continue;
|
|
3508
|
+
if (msgAt(m) <= w.lastInjectAt)
|
|
3509
|
+
continue;
|
|
3510
|
+
const text = messageTextOf(m);
|
|
3511
|
+
const ownByText = typeof text === "string" && text.length > 0 && w.ownPromptTexts.includes(text);
|
|
3512
|
+
const ownByShape = looksLikeOwnPrompt(text);
|
|
3513
|
+
const ownByTime = msgAt(m) >= w.lastInjectAt && msgAt(m) - w.lastInjectAt < OWN_PROMPT_TIME_MS;
|
|
3514
|
+
const ownByTimeOnly = ownByTime && !(typeof text === "string" && text.length > 0);
|
|
3515
|
+
if (ownByText || ownByShape || ownByTimeOnly)
|
|
3516
|
+
continue;
|
|
3517
|
+
ownRung = false;
|
|
3518
|
+
break;
|
|
3507
3519
|
}
|
|
3508
3520
|
} catch {
|
|
3509
3521
|
ownRung = false;
|
package/package.json
CHANGED