opencode-auto-resume 1.2.1 → 1.2.3
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 +2 -10
- package/dist/index.js +103 -87
- package/dist/v2/index.js +91 -84
- 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
|
|
package/dist/index.js
CHANGED
|
@@ -12849,6 +12849,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12849
12849
|
pendingRecoveryReason: null,
|
|
12850
12850
|
pendingRecoveryAt: 0,
|
|
12851
12851
|
recoveryAttempts: 0,
|
|
12852
|
+
recoveryContinueInFlight: false,
|
|
12852
12853
|
watchdogRetryGuard: false,
|
|
12853
12854
|
unknownToolErrors: new Map,
|
|
12854
12855
|
unknownToolSuggestionSent: false,
|
|
@@ -13092,6 +13093,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13092
13093
|
const retryBackoffMs = backoffMs(w.recoveryAttempts, baseBackoffMs, maxBackoffMs);
|
|
13093
13094
|
await log("info", `Retrying recovery on ${short(sid)}: attempt=${w.recoveryAttempts}, backoffMs=${retryBackoffMs}`);
|
|
13094
13095
|
dbg(`Retrying recovery on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts}, backoffMs=${retryBackoffMs}, pendingRecoveryReason=${w.pendingRecoveryReason}`);
|
|
13096
|
+
w.recoveryContinueInFlight = true;
|
|
13095
13097
|
try {
|
|
13096
13098
|
await sendContinuePrompt(sid, continuePrompt, w);
|
|
13097
13099
|
} catch (err) {
|
|
@@ -13559,14 +13561,13 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13559
13561
|
w.pendingRecoveryReason = null;
|
|
13560
13562
|
w.pendingRecoveryAt = 0;
|
|
13561
13563
|
w.recoveryAttempts = 0;
|
|
13564
|
+
w.recoveryContinueInFlight = false;
|
|
13562
13565
|
w.watchdogRetryGuard = false;
|
|
13563
13566
|
}
|
|
13564
13567
|
function resetBusyFlags(w) {
|
|
13565
|
-
w.resumeAttempts = 0;
|
|
13566
13568
|
w.lastRetryAt = 0;
|
|
13567
13569
|
w.pendingTools = 0;
|
|
13568
13570
|
w.pendingCommands = 0;
|
|
13569
|
-
w.gaveUp = false;
|
|
13570
13571
|
w.orphanWatchStartAt = null;
|
|
13571
13572
|
w.aborting = false;
|
|
13572
13573
|
w.toolTextRecovered = false;
|
|
@@ -13581,7 +13582,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13581
13582
|
w.pendingRecovery = false;
|
|
13582
13583
|
w.pendingRecoveryReason = null;
|
|
13583
13584
|
w.pendingRecoveryAt = 0;
|
|
13584
|
-
w.
|
|
13585
|
+
w.recoveryContinueInFlight = false;
|
|
13585
13586
|
w.watchdogRetryGuard = false;
|
|
13586
13587
|
if (w.toolTextTimer) {
|
|
13587
13588
|
clearTimeout(w.toolTextTimer);
|
|
@@ -13974,6 +13975,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13974
13975
|
return false;
|
|
13975
13976
|
if (w.oocLocked)
|
|
13976
13977
|
return false;
|
|
13978
|
+
w.aborting = true;
|
|
13977
13979
|
const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
|
|
13978
13980
|
await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
|
|
13979
13981
|
w.pluginAbortInFlight = true;
|
|
@@ -13999,6 +14001,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13999
14001
|
return false;
|
|
14000
14002
|
}
|
|
14001
14003
|
try {
|
|
14004
|
+
w.recoveryContinueInFlight = false;
|
|
14002
14005
|
await sendContinuePrompt(sid, continuePrompt, w);
|
|
14003
14006
|
await log("info", `${short(sid)} - abort+continue done`);
|
|
14004
14007
|
w.orphanWatchStartAt = null;
|
|
@@ -14234,6 +14237,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14234
14237
|
dbg(`Recovery timing on ${short(sid)}: detectionToAttemptMs=${Date.now() - w.pendingRecoveryAt}`);
|
|
14235
14238
|
w.recoveryAttempts++;
|
|
14236
14239
|
dbg(`State transition on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts - 1} -> ${w.recoveryAttempts}`);
|
|
14240
|
+
w.recoveryContinueInFlight = true;
|
|
14237
14241
|
try {
|
|
14238
14242
|
await sendContinuePrompt(sid, continuePrompt, w);
|
|
14239
14243
|
} catch (err) {
|
|
@@ -14328,6 +14332,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14328
14332
|
if (w.pendingRecovery) {
|
|
14329
14333
|
dbg(`Pending recovery cleared on ${short(sid)}: reason=session-busy`);
|
|
14330
14334
|
}
|
|
14335
|
+
if (w.recoveryContinueInFlight) {
|
|
14336
|
+
w.recoveryAttempts = 0;
|
|
14337
|
+
w.recoveryContinueInFlight = false;
|
|
14338
|
+
dbg(`Recovery succeeded on ${short(sid)}: recoveryAttempts reset to 0`);
|
|
14339
|
+
}
|
|
14331
14340
|
if (w.continuing) {
|
|
14332
14341
|
dbg(`busy on ${short(sid)} via recovery prompt - preserving resumeAttempts=${w.resumeAttempts} gaveUp=${w.gaveUp}`);
|
|
14333
14342
|
} else {
|
|
@@ -14911,7 +14920,7 @@ ${reminder}`;
|
|
|
14911
14920
|
};
|
|
14912
14921
|
|
|
14913
14922
|
// src/v2/index.ts
|
|
14914
|
-
import { readFileSync, mkdirSync,
|
|
14923
|
+
import { readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
14915
14924
|
import { dirname, join } from "path";
|
|
14916
14925
|
import { fileURLToPath } from "url";
|
|
14917
14926
|
import { readFile } from "fs/promises";
|
|
@@ -15017,7 +15026,6 @@ var DONE_CLAIM_PATTERN_SOURCES = [
|
|
|
15017
15026
|
"\\b(?:all|everything)\\s+(?:[\\w-]+\\s+)?(?:is\\s+)?(?:complete|done|finished)",
|
|
15018
15027
|
"\\bnothing\\s+(?:else\\s+)?(?:left|remaining|to do)"
|
|
15019
15028
|
];
|
|
15020
|
-
var DEFAULT_DONE_CLAIM_PATTERNS = [...DONE_CLAIM_PATTERN_SOURCES];
|
|
15021
15029
|
var DEFAULT_READY_TO_CONTINUE_PATTERNS = [
|
|
15022
15030
|
"ready to continue with task",
|
|
15023
15031
|
"continuing with task",
|
|
@@ -15055,7 +15063,8 @@ var DEFAULT_INJECT_INTERVAL_MS = 15000;
|
|
|
15055
15063
|
var OWN_PROMPT_TIME_MS = 30000;
|
|
15056
15064
|
var MAX_IDLE_SESSIONS2 = 50;
|
|
15057
15065
|
var IDLE_CLEANUP_MS2 = 10 * 60000;
|
|
15058
|
-
var COMPACTION_STALE_TTL_MS =
|
|
15066
|
+
var COMPACTION_STALE_TTL_MS = 5 * 60000;
|
|
15067
|
+
var COMPLETION_LATCH_TTL_MS = 5 * 60000;
|
|
15059
15068
|
var PERMISSION_STALE_TTL_MS = 30 * 60000;
|
|
15060
15069
|
var NEVER_INTERRUPT_STALE_TTL_MS = 30 * 60000;
|
|
15061
15070
|
var TEXT_BUFFER_TRIM_LEN = 20000;
|
|
@@ -15389,9 +15398,6 @@ function isUserHandoff(text) {
|
|
|
15389
15398
|
];
|
|
15390
15399
|
return patterns.some((p) => p.test(last));
|
|
15391
15400
|
}
|
|
15392
|
-
function backoffMs2(attempt, base, max) {
|
|
15393
|
-
return Math.min(base * Math.pow(2, attempt - 1), max);
|
|
15394
|
-
}
|
|
15395
15401
|
function isSilenceReason(reason) {
|
|
15396
15402
|
return /^no activity for \d+s/i.test(reason);
|
|
15397
15403
|
}
|
|
@@ -15582,6 +15588,7 @@ var v2_default = define({
|
|
|
15582
15588
|
const checkIntervalMs = opts.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS2;
|
|
15583
15589
|
const gracePeriodMs = opts.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS2;
|
|
15584
15590
|
const stuckToolAgeMs = opts.stuckToolAgeMs ?? DEFAULT_STUCK_TOOL_AGE_MS;
|
|
15591
|
+
const completionLatchTtlMs = opts.completionLatchTtlMs ?? COMPLETION_LATCH_TTL_MS;
|
|
15585
15592
|
const neverInterruptTools = (opts.neverInterruptTools ?? ["compress"]).map((n) => String(n).toLowerCase());
|
|
15586
15593
|
const maxRetries = opts.maxRetries ?? opts.maxRecoveryRetries ?? DEFAULT_MAX_RETRIES2;
|
|
15587
15594
|
const baseBackoff = opts.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS2;
|
|
@@ -15627,12 +15634,13 @@ var v2_default = define({
|
|
|
15627
15634
|
}
|
|
15628
15635
|
return out;
|
|
15629
15636
|
}
|
|
15630
|
-
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns,
|
|
15637
|
+
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns, DONE_CLAIM_PATTERN_SOURCES, "im");
|
|
15631
15638
|
const readyToContinuePatterns = compilePatterns(opts.readyToContinuePatterns, DEFAULT_READY_TO_CONTINUE_PATTERNS, "i");
|
|
15632
15639
|
const streamingFailureMessageRegexes = compilePatterns(streamingFailureMessagePatterns, DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS2, "i");
|
|
15633
15640
|
const FEATURE_GATED_OPTIONS = [];
|
|
15634
15641
|
const RECOGNISED_OPTIONS = new Set([
|
|
15635
15642
|
"chunkTimeoutMs",
|
|
15643
|
+
"completionLatchTtlMs",
|
|
15636
15644
|
"checkIntervalMs",
|
|
15637
15645
|
"gracePeriodMs",
|
|
15638
15646
|
"maxRetries",
|
|
@@ -15761,7 +15769,6 @@ var v2_default = define({
|
|
|
15761
15769
|
providerRetryAt: 0,
|
|
15762
15770
|
quotaNoticedAt: 0,
|
|
15763
15771
|
gaveUp: false,
|
|
15764
|
-
lastQuotaNoticeAt: 0,
|
|
15765
15772
|
paused: false,
|
|
15766
15773
|
lastGenuineUserAt: 0,
|
|
15767
15774
|
consecFailureRecovers: 0,
|
|
@@ -15795,6 +15802,8 @@ var v2_default = define({
|
|
|
15795
15802
|
intentNudgeAttempts: 0,
|
|
15796
15803
|
completionSignaled: false,
|
|
15797
15804
|
completionSignaledAt: 0,
|
|
15805
|
+
completionHard: false,
|
|
15806
|
+
latchReopened: false,
|
|
15798
15807
|
emptyIdleNudged: false,
|
|
15799
15808
|
todos: [],
|
|
15800
15809
|
todosFetchedAt: 0,
|
|
@@ -15852,14 +15861,8 @@ var v2_default = define({
|
|
|
15852
15861
|
const lifetimeMaxContinues = opts.lifetimeMaxContinues ?? base.lifetimeMaxContinues;
|
|
15853
15862
|
return { ...base, chunkTimeoutMs, gracePeriodMs, maxRetries, lifetimeMaxContinues };
|
|
15854
15863
|
}
|
|
15855
|
-
let pausedCacheMtime = 0;
|
|
15856
|
-
let pausedCacheSet = new Set;
|
|
15857
15864
|
function readPausedSet() {
|
|
15858
15865
|
try {
|
|
15859
|
-
const st = statSync(PAUSED_MARKER_FILE);
|
|
15860
|
-
if (st.mtimeMs === pausedCacheMtime)
|
|
15861
|
-
return pausedCacheSet;
|
|
15862
|
-
pausedCacheMtime = st.mtimeMs;
|
|
15863
15866
|
const raw = readFileSync(PAUSED_MARKER_FILE, "utf8");
|
|
15864
15867
|
const s = new Set;
|
|
15865
15868
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -15867,12 +15870,9 @@ var v2_default = define({
|
|
|
15867
15870
|
if (t)
|
|
15868
15871
|
s.add(t);
|
|
15869
15872
|
}
|
|
15870
|
-
pausedCacheSet = s;
|
|
15871
15873
|
return s;
|
|
15872
15874
|
} catch {
|
|
15873
|
-
|
|
15874
|
-
pausedCacheSet = new Set;
|
|
15875
|
-
return pausedCacheSet;
|
|
15875
|
+
return new Set;
|
|
15876
15876
|
}
|
|
15877
15877
|
}
|
|
15878
15878
|
function isPaused(sid) {
|
|
@@ -15892,7 +15892,6 @@ var v2_default = define({
|
|
|
15892
15892
|
writeFileSync(PAUSED_MARKER_FILE, [...s].join(`
|
|
15893
15893
|
`) + (s.size ? `
|
|
15894
15894
|
` : ""), "utf8");
|
|
15895
|
-
pausedCacheMtime = 0;
|
|
15896
15895
|
} catch (e) {
|
|
15897
15896
|
dbg(`${short(sid)} pause write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
15898
15897
|
}
|
|
@@ -15939,6 +15938,22 @@ var v2_default = define({
|
|
|
15939
15938
|
}
|
|
15940
15939
|
clearStalePermissionFlag(sid, w);
|
|
15941
15940
|
}
|
|
15941
|
+
function softLatchExpired(sid, w, now) {
|
|
15942
|
+
if (!w.completionSignaled)
|
|
15943
|
+
return false;
|
|
15944
|
+
if (w.completionHard || w.latchReopened)
|
|
15945
|
+
return false;
|
|
15946
|
+
if (!w.completionSignaledAt)
|
|
15947
|
+
return false;
|
|
15948
|
+
if (now - w.completionSignaledAt < completionLatchTtlMs)
|
|
15949
|
+
return false;
|
|
15950
|
+
w.latchReopened = true;
|
|
15951
|
+
w.completionSignaled = false;
|
|
15952
|
+
w.completionSignaledAt = 0;
|
|
15953
|
+
w.pendingRecoveryArmed = true;
|
|
15954
|
+
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`);
|
|
15955
|
+
return true;
|
|
15956
|
+
}
|
|
15942
15957
|
function clearStalePermissionFlag(sid, w) {
|
|
15943
15958
|
if (!w.permissionPending)
|
|
15944
15959
|
return;
|
|
@@ -16006,6 +16021,36 @@ var v2_default = define({
|
|
|
16006
16021
|
const cooldownMin = Math.max(1, Math.round(ladder[rung] / 60000));
|
|
16007
16022
|
recover(sid, `rate-limit cooldown served (rung ${rung + 1}/${ladder.length}, ${cooldownMin}m)`);
|
|
16008
16023
|
}
|
|
16024
|
+
function quotaGuard(sid, w, messages) {
|
|
16025
|
+
try {
|
|
16026
|
+
const last = messages.length ? messages[messages.length - 1] : null;
|
|
16027
|
+
let qtype = "";
|
|
16028
|
+
let qmsg = "";
|
|
16029
|
+
if (last && msgRole(last) === "assistant") {
|
|
16030
|
+
const qe = last.error ?? last.info?.error;
|
|
16031
|
+
if (typeof qe === "string")
|
|
16032
|
+
qmsg = qe;
|
|
16033
|
+
else if (qe && typeof qe === "object") {
|
|
16034
|
+
qtype = String(qe.name ?? qe.type ?? "");
|
|
16035
|
+
qmsg = String(qe.message ?? "");
|
|
16036
|
+
}
|
|
16037
|
+
const qtxt = lastAssistantTextFrom(messages);
|
|
16038
|
+
if (qtxt)
|
|
16039
|
+
qmsg = qmsg ? `${qmsg} ${qtxt}` : qtxt;
|
|
16040
|
+
}
|
|
16041
|
+
if (isRateLimitError(qtype, qmsg)) {
|
|
16042
|
+
handleRateLimitFailure(sid, w, qtype, qmsg);
|
|
16043
|
+
return true;
|
|
16044
|
+
}
|
|
16045
|
+
} catch (e) {
|
|
16046
|
+
dbg(`${short(sid)} quota guard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
16047
|
+
}
|
|
16048
|
+
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
16049
|
+
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
16050
|
+
return true;
|
|
16051
|
+
}
|
|
16052
|
+
return false;
|
|
16053
|
+
}
|
|
16009
16054
|
function handleRateLimitFailure(sid, w, errType, errMsg) {
|
|
16010
16055
|
if (isPaused(sid)) {
|
|
16011
16056
|
dbg(`${short(sid)} paused \u2014 not arming quota ladder`);
|
|
@@ -16018,9 +16063,8 @@ var v2_default = define({
|
|
|
16018
16063
|
if (n >= ladder.length) {
|
|
16019
16064
|
w.awaitingQuotaRetry = false;
|
|
16020
16065
|
log("warn", `${short(sid)} ${tag} budget exhausted (${ladder.length} attempts) \u2014 standing down until user turn`);
|
|
16021
|
-
if (!w.quotaNoticedAt
|
|
16066
|
+
if (!w.quotaNoticedAt) {
|
|
16022
16067
|
w.quotaNoticedAt = Date.now();
|
|
16023
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
16024
16068
|
sendQuotaNotice(sid, `Rate limited \u2014 standing down until you return (budget exhausted after ${ladder.length} attempts).`);
|
|
16025
16069
|
}
|
|
16026
16070
|
return;
|
|
@@ -16038,9 +16082,8 @@ var v2_default = define({
|
|
|
16038
16082
|
w.awaitingQuotaRetry = true;
|
|
16039
16083
|
const waitMs = Math.max(0, ladder[n] - (now - w.rateLimitedAt));
|
|
16040
16084
|
log("warn", `${short(sid)} rate limited (${errType || "error"}) \u2014 standing down, next attempt in ${Math.ceil(waitMs / 1000)}s (${n + 1}/${ladder.length})`);
|
|
16041
|
-
if (!w.quotaNoticedAt
|
|
16085
|
+
if (!w.quotaNoticedAt) {
|
|
16042
16086
|
w.quotaNoticedAt = Date.now();
|
|
16043
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
16044
16087
|
const mins = Math.max(1, Math.round(waitMs / 60000));
|
|
16045
16088
|
const when = mins >= 60 ? `~${Math.floor(mins / 60)}h ${mins % 60}m` : `~${mins}m`;
|
|
16046
16089
|
sendQuotaNotice(sid, `Rate limited, retrying in ${when} (attempt ${n + 1}/${ladder.length}).`);
|
|
@@ -16520,13 +16563,12 @@ ${lines.join(`
|
|
|
16520
16563
|
});
|
|
16521
16564
|
const elapsed = w.lastActivityAt ? Math.round((Date.now() - w.lastActivityAt) / 1000) : 0;
|
|
16522
16565
|
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`;
|
|
16523
|
-
const isRateLimitReason = /rate-limit/i.test(reason);
|
|
16524
16566
|
const isSilence = isSilenceReason(reason);
|
|
16525
16567
|
const shown = providerFault ? attempt : attempt;
|
|
16526
16568
|
const retries = prospectiveRetries ?? w.workRetries;
|
|
16527
16569
|
const head = [
|
|
16570
|
+
/rate-limit/i.test(reason) ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
16528
16571
|
isSilence ? null : reason,
|
|
16529
|
-
isRateLimitReason ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
16530
16572
|
isSilence ? `idle ${hms(elapsed)}` : null,
|
|
16531
16573
|
now
|
|
16532
16574
|
].filter((x) => typeof x === "string" && x.length > 0).join("; ");
|
|
@@ -16607,7 +16649,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
16607
16649
|
}
|
|
16608
16650
|
w.recovering = true;
|
|
16609
16651
|
const attempt = providerFault ? Math.max(1, w.resumeAttempts) : w.resumeAttempts + 1;
|
|
16610
|
-
const delay =
|
|
16652
|
+
const delay = Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff);
|
|
16611
16653
|
if (providerFault) {
|
|
16612
16654
|
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}`);
|
|
16613
16655
|
}
|
|
@@ -17094,6 +17136,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
17094
17136
|
if (w.completionSignaled) {
|
|
17095
17137
|
w.completionSignaled = false;
|
|
17096
17138
|
w.completionSignaledAt = 0;
|
|
17139
|
+
w.latchReopened = false;
|
|
17097
17140
|
dbg(`${short(sid)} genuine new user message \u2014 re-opening latched completion`);
|
|
17098
17141
|
}
|
|
17099
17142
|
if (w.resumeAttempts > 0) {
|
|
@@ -17379,6 +17422,7 @@ ${reminder}`;
|
|
|
17379
17422
|
}
|
|
17380
17423
|
w.completionSignaled = true;
|
|
17381
17424
|
w.completionSignaledAt = Date.now();
|
|
17425
|
+
w.completionHard = true;
|
|
17382
17426
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
17383
17427
|
w.taskCompleteSignals++;
|
|
17384
17428
|
if (w.taskCompleteSignals === 2)
|
|
@@ -17430,6 +17474,7 @@ ${reminder}`;
|
|
|
17430
17474
|
}
|
|
17431
17475
|
w.completionSignaled = true;
|
|
17432
17476
|
w.completionSignaledAt = Date.now();
|
|
17477
|
+
w.completionHard = true;
|
|
17433
17478
|
w.taskCompleteSignals = 0;
|
|
17434
17479
|
w.taskCompleteOverrides = 0;
|
|
17435
17480
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
@@ -17468,6 +17513,8 @@ ${reminder}`;
|
|
|
17468
17513
|
if (open.length === 0 && !w.completionSignaled) {
|
|
17469
17514
|
w.completionSignaled = true;
|
|
17470
17515
|
w.completionSignaledAt = Date.now();
|
|
17516
|
+
w.completionHard = /\btask\s+complete(d)?\b[.!]*$/i.test(stripBoundaryMarkers(tailText).split(`
|
|
17517
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0).pop() ?? "");
|
|
17471
17518
|
log("info", `${short(sid)} turn ends with a completion claim inside the activity window \u2014 latching completion, standing down`);
|
|
17472
17519
|
}
|
|
17473
17520
|
} else {
|
|
@@ -17506,6 +17553,7 @@ ${reminder}`;
|
|
|
17506
17553
|
if (!w.completionSignaled) {
|
|
17507
17554
|
w.completionSignaled = true;
|
|
17508
17555
|
w.completionSignaledAt = Date.now();
|
|
17556
|
+
w.completionHard = false;
|
|
17509
17557
|
log("info", `${short(sid)} turn ends with a celebration and no open todos \u2014 latching completion, not nudging`);
|
|
17510
17558
|
} else {
|
|
17511
17559
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -17538,6 +17586,8 @@ ${reminder}`;
|
|
|
17538
17586
|
if (!w.completionSignaled) {
|
|
17539
17587
|
w.completionSignaled = true;
|
|
17540
17588
|
w.completionSignaledAt = Date.now();
|
|
17589
|
+
w.completionHard = /\btask\s+complete(d)?\b[.!]*$/i.test(stripBoundaryMarkers(text).split(`
|
|
17590
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0).pop() ?? "");
|
|
17541
17591
|
log("info", `${short(sid)} turn ends with an explicit stop and no open todos \u2014 latching completion, not nudging`);
|
|
17542
17592
|
} else {
|
|
17543
17593
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -17884,7 +17934,10 @@ ${reminder}`;
|
|
|
17884
17934
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
17885
17935
|
w.completionSignaled = false;
|
|
17886
17936
|
w.completionSignaledAt = 0;
|
|
17937
|
+
w.latchReopened = false;
|
|
17887
17938
|
dbg(`${short(sid)} genuine new user message after latched completion \u2014 re-opening stall detection`);
|
|
17939
|
+
} else if (softLatchExpired(sid, w, now)) {
|
|
17940
|
+
dbg(`${short(sid)} soft completion latch expired \u2014 re-opening for one self-check`);
|
|
17888
17941
|
} else {
|
|
17889
17942
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
17890
17943
|
}
|
|
@@ -17904,33 +17957,8 @@ ${reminder}`;
|
|
|
17904
17957
|
const baseInterval = (profileChunkTimeoutMs + profileGracePeriodMs) * fuseMult;
|
|
17905
17958
|
const jitter = baseInterval > 60000 ? Math.floor(Math.random() * STALL_JITTER_MS * 2) - STALL_JITTER_MS : 0;
|
|
17906
17959
|
if (silence >= baseInterval + jitter) {
|
|
17907
|
-
|
|
17908
|
-
const _qlast2 = history.length ? history[history.length - 1] : null;
|
|
17909
|
-
let _qtype2 = "";
|
|
17910
|
-
let _qmsg2 = "";
|
|
17911
|
-
if (_qlast2 && msgRole(_qlast2) === "assistant") {
|
|
17912
|
-
const _qe2 = _qlast2.error ?? _qlast2.info?.error;
|
|
17913
|
-
if (typeof _qe2 === "string")
|
|
17914
|
-
_qmsg2 = _qe2;
|
|
17915
|
-
else if (_qe2 && typeof _qe2 === "object") {
|
|
17916
|
-
_qtype2 = String(_qe2.name ?? _qe2.type ?? "");
|
|
17917
|
-
_qmsg2 = String(_qe2.message ?? "");
|
|
17918
|
-
}
|
|
17919
|
-
const _qtxt2 = lastAssistantTextFrom(history);
|
|
17920
|
-
if (_qtxt2)
|
|
17921
|
-
_qmsg2 = _qmsg2 ? `${_qmsg2} ${_qtxt2}` : _qtxt2;
|
|
17922
|
-
}
|
|
17923
|
-
if (isRateLimitError(_qtype2, _qmsg2)) {
|
|
17924
|
-
handleRateLimitFailure(sid, w, _qtype2, _qmsg2);
|
|
17925
|
-
continue;
|
|
17926
|
-
}
|
|
17927
|
-
} catch (e) {
|
|
17928
|
-
dbg(`${short(sid)} quota guard (idle) failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
17929
|
-
}
|
|
17930
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
17931
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
17960
|
+
if (quotaGuard(sid, w, history))
|
|
17932
17961
|
continue;
|
|
17933
|
-
}
|
|
17934
17962
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
17935
17963
|
}
|
|
17936
17964
|
}
|
|
@@ -17954,7 +17982,10 @@ ${reminder}`;
|
|
|
17954
17982
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
17955
17983
|
w.completionSignaled = false;
|
|
17956
17984
|
w.completionSignaledAt = 0;
|
|
17985
|
+
w.latchReopened = false;
|
|
17957
17986
|
dbg(`${short(sid)} genuine new user message after latched completion \u2014 re-opening stall detection`);
|
|
17987
|
+
} else if (softLatchExpired(sid, w, now)) {
|
|
17988
|
+
dbg(`${short(sid)} soft completion latch expired \u2014 re-opening for one self-check`);
|
|
17958
17989
|
} else {
|
|
17959
17990
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
17960
17991
|
continue;
|
|
@@ -18051,34 +18082,9 @@ ${reminder}`;
|
|
|
18051
18082
|
log("info", `${short(sid)} stall (busyStallStrategy=abort): aborting before continue`);
|
|
18052
18083
|
await tryAbortAndResume(sid, w, true);
|
|
18053
18084
|
} else {
|
|
18054
|
-
|
|
18055
|
-
|
|
18056
|
-
const _qlast = _qm.length ? _qm[_qm.length - 1] : null;
|
|
18057
|
-
let _qtype = "";
|
|
18058
|
-
let _qmsg = "";
|
|
18059
|
-
if (_qlast && msgRole(_qlast) === "assistant") {
|
|
18060
|
-
const _qe = _qlast.error ?? _qlast.info?.error;
|
|
18061
|
-
if (typeof _qe === "string")
|
|
18062
|
-
_qmsg = _qe;
|
|
18063
|
-
else if (_qe && typeof _qe === "object") {
|
|
18064
|
-
_qtype = String(_qe.name ?? _qe.type ?? "");
|
|
18065
|
-
_qmsg = String(_qe.message ?? "");
|
|
18066
|
-
}
|
|
18067
|
-
const _qtxt = lastAssistantTextFrom(_qm);
|
|
18068
|
-
if (_qtxt)
|
|
18069
|
-
_qmsg = _qmsg ? `${_qmsg} ${_qtxt}` : _qtxt;
|
|
18070
|
-
}
|
|
18071
|
-
if (isRateLimitError(_qtype, _qmsg)) {
|
|
18072
|
-
handleRateLimitFailure(sid, w, _qtype, _qmsg);
|
|
18073
|
-
continue;
|
|
18074
|
-
}
|
|
18075
|
-
} catch (e) {
|
|
18076
|
-
dbg(`${short(sid)} quota guard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
18077
|
-
}
|
|
18078
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
18079
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
18085
|
+
const _qm = await loadMessages(sid);
|
|
18086
|
+
if (quotaGuard(sid, w, _qm))
|
|
18080
18087
|
continue;
|
|
18081
|
-
}
|
|
18082
18088
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
18083
18089
|
}
|
|
18084
18090
|
}
|
|
@@ -18415,11 +18421,21 @@ ${reminder}`;
|
|
|
18415
18421
|
let ownRung = !providerRetryFresh && selfInjectFresh;
|
|
18416
18422
|
if (ownRung) {
|
|
18417
18423
|
try {
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18424
|
+
const hist = await loadMessages(sid);
|
|
18425
|
+
for (const m of hist) {
|
|
18426
|
+
if (msgRole(m) !== "user")
|
|
18427
|
+
continue;
|
|
18428
|
+
if (msgAt(m) <= w.lastInjectAt)
|
|
18429
|
+
continue;
|
|
18430
|
+
const text = messageTextOf(m);
|
|
18431
|
+
const ownByText = typeof text === "string" && text.length > 0 && w.ownPromptTexts.includes(text);
|
|
18432
|
+
const ownByShape = looksLikeOwnPrompt(text);
|
|
18433
|
+
const ownByTime = msgAt(m) >= w.lastInjectAt && msgAt(m) - w.lastInjectAt < OWN_PROMPT_TIME_MS;
|
|
18434
|
+
const ownByTimeOnly = ownByTime && !(typeof text === "string" && text.length > 0);
|
|
18435
|
+
if (ownByText || ownByShape || ownByTimeOnly)
|
|
18436
|
+
continue;
|
|
18437
|
+
ownRung = false;
|
|
18438
|
+
break;
|
|
18423
18439
|
}
|
|
18424
18440
|
} catch {
|
|
18425
18441
|
ownRung = false;
|
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
|
}
|
|
@@ -671,6 +668,7 @@ var v2_default = define({
|
|
|
671
668
|
const checkIntervalMs = opts.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
|
|
672
669
|
const gracePeriodMs = opts.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
|
|
673
670
|
const stuckToolAgeMs = opts.stuckToolAgeMs ?? DEFAULT_STUCK_TOOL_AGE_MS;
|
|
671
|
+
const completionLatchTtlMs = opts.completionLatchTtlMs ?? COMPLETION_LATCH_TTL_MS;
|
|
674
672
|
const neverInterruptTools = (opts.neverInterruptTools ?? ["compress"]).map((n) => String(n).toLowerCase());
|
|
675
673
|
const maxRetries = opts.maxRetries ?? opts.maxRecoveryRetries ?? DEFAULT_MAX_RETRIES;
|
|
676
674
|
const baseBackoff = opts.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS;
|
|
@@ -716,12 +714,13 @@ var v2_default = define({
|
|
|
716
714
|
}
|
|
717
715
|
return out;
|
|
718
716
|
}
|
|
719
|
-
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns,
|
|
717
|
+
const doneClaimPatterns = compilePatterns(opts.doneClaimPatterns, DONE_CLAIM_PATTERN_SOURCES, "im");
|
|
720
718
|
const readyToContinuePatterns = compilePatterns(opts.readyToContinuePatterns, DEFAULT_READY_TO_CONTINUE_PATTERNS, "i");
|
|
721
719
|
const streamingFailureMessageRegexes = compilePatterns(streamingFailureMessagePatterns, DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS, "i");
|
|
722
720
|
const FEATURE_GATED_OPTIONS = [];
|
|
723
721
|
const RECOGNISED_OPTIONS = new Set([
|
|
724
722
|
"chunkTimeoutMs",
|
|
723
|
+
"completionLatchTtlMs",
|
|
725
724
|
"checkIntervalMs",
|
|
726
725
|
"gracePeriodMs",
|
|
727
726
|
"maxRetries",
|
|
@@ -850,7 +849,6 @@ var v2_default = define({
|
|
|
850
849
|
providerRetryAt: 0,
|
|
851
850
|
quotaNoticedAt: 0,
|
|
852
851
|
gaveUp: false,
|
|
853
|
-
lastQuotaNoticeAt: 0,
|
|
854
852
|
paused: false,
|
|
855
853
|
lastGenuineUserAt: 0,
|
|
856
854
|
consecFailureRecovers: 0,
|
|
@@ -884,6 +882,8 @@ var v2_default = define({
|
|
|
884
882
|
intentNudgeAttempts: 0,
|
|
885
883
|
completionSignaled: false,
|
|
886
884
|
completionSignaledAt: 0,
|
|
885
|
+
completionHard: false,
|
|
886
|
+
latchReopened: false,
|
|
887
887
|
emptyIdleNudged: false,
|
|
888
888
|
todos: [],
|
|
889
889
|
todosFetchedAt: 0,
|
|
@@ -941,14 +941,8 @@ var v2_default = define({
|
|
|
941
941
|
const lifetimeMaxContinues = opts.lifetimeMaxContinues ?? base.lifetimeMaxContinues;
|
|
942
942
|
return { ...base, chunkTimeoutMs, gracePeriodMs, maxRetries, lifetimeMaxContinues };
|
|
943
943
|
}
|
|
944
|
-
let pausedCacheMtime = 0;
|
|
945
|
-
let pausedCacheSet = new Set;
|
|
946
944
|
function readPausedSet() {
|
|
947
945
|
try {
|
|
948
|
-
const st = statSync(PAUSED_MARKER_FILE);
|
|
949
|
-
if (st.mtimeMs === pausedCacheMtime)
|
|
950
|
-
return pausedCacheSet;
|
|
951
|
-
pausedCacheMtime = st.mtimeMs;
|
|
952
946
|
const raw = readFileSync(PAUSED_MARKER_FILE, "utf8");
|
|
953
947
|
const s = new Set;
|
|
954
948
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -956,12 +950,9 @@ var v2_default = define({
|
|
|
956
950
|
if (t)
|
|
957
951
|
s.add(t);
|
|
958
952
|
}
|
|
959
|
-
pausedCacheSet = s;
|
|
960
953
|
return s;
|
|
961
954
|
} catch {
|
|
962
|
-
|
|
963
|
-
pausedCacheSet = new Set;
|
|
964
|
-
return pausedCacheSet;
|
|
955
|
+
return new Set;
|
|
965
956
|
}
|
|
966
957
|
}
|
|
967
958
|
function isPaused(sid) {
|
|
@@ -981,7 +972,6 @@ var v2_default = define({
|
|
|
981
972
|
writeFileSync(PAUSED_MARKER_FILE, [...s].join(`
|
|
982
973
|
`) + (s.size ? `
|
|
983
974
|
` : ""), "utf8");
|
|
984
|
-
pausedCacheMtime = 0;
|
|
985
975
|
} catch (e) {
|
|
986
976
|
dbg(`${short(sid)} pause write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
987
977
|
}
|
|
@@ -1028,6 +1018,22 @@ var v2_default = define({
|
|
|
1028
1018
|
}
|
|
1029
1019
|
clearStalePermissionFlag(sid, w);
|
|
1030
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
|
+
}
|
|
1031
1037
|
function clearStalePermissionFlag(sid, w) {
|
|
1032
1038
|
if (!w.permissionPending)
|
|
1033
1039
|
return;
|
|
@@ -1095,6 +1101,36 @@ var v2_default = define({
|
|
|
1095
1101
|
const cooldownMin = Math.max(1, Math.round(ladder[rung] / 60000));
|
|
1096
1102
|
recover(sid, `rate-limit cooldown served (rung ${rung + 1}/${ladder.length}, ${cooldownMin}m)`);
|
|
1097
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
|
+
}
|
|
1098
1134
|
function handleRateLimitFailure(sid, w, errType, errMsg) {
|
|
1099
1135
|
if (isPaused(sid)) {
|
|
1100
1136
|
dbg(`${short(sid)} paused \u2014 not arming quota ladder`);
|
|
@@ -1107,9 +1143,8 @@ var v2_default = define({
|
|
|
1107
1143
|
if (n >= ladder.length) {
|
|
1108
1144
|
w.awaitingQuotaRetry = false;
|
|
1109
1145
|
log("warn", `${short(sid)} ${tag} budget exhausted (${ladder.length} attempts) \u2014 standing down until user turn`);
|
|
1110
|
-
if (!w.quotaNoticedAt
|
|
1146
|
+
if (!w.quotaNoticedAt) {
|
|
1111
1147
|
w.quotaNoticedAt = Date.now();
|
|
1112
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
1113
1148
|
sendQuotaNotice(sid, `Rate limited \u2014 standing down until you return (budget exhausted after ${ladder.length} attempts).`);
|
|
1114
1149
|
}
|
|
1115
1150
|
return;
|
|
@@ -1127,9 +1162,8 @@ var v2_default = define({
|
|
|
1127
1162
|
w.awaitingQuotaRetry = true;
|
|
1128
1163
|
const waitMs = Math.max(0, ladder[n] - (now - w.rateLimitedAt));
|
|
1129
1164
|
log("warn", `${short(sid)} rate limited (${errType || "error"}) \u2014 standing down, next attempt in ${Math.ceil(waitMs / 1000)}s (${n + 1}/${ladder.length})`);
|
|
1130
|
-
if (!w.quotaNoticedAt
|
|
1165
|
+
if (!w.quotaNoticedAt) {
|
|
1131
1166
|
w.quotaNoticedAt = Date.now();
|
|
1132
|
-
w.lastQuotaNoticeAt = Date.now();
|
|
1133
1167
|
const mins = Math.max(1, Math.round(waitMs / 60000));
|
|
1134
1168
|
const when = mins >= 60 ? `~${Math.floor(mins / 60)}h ${mins % 60}m` : `~${mins}m`;
|
|
1135
1169
|
sendQuotaNotice(sid, `Rate limited, retrying in ${when} (attempt ${n + 1}/${ladder.length}).`);
|
|
@@ -1609,13 +1643,12 @@ ${lines.join(`
|
|
|
1609
1643
|
});
|
|
1610
1644
|
const elapsed = w.lastActivityAt ? Math.round((Date.now() - w.lastActivityAt) / 1000) : 0;
|
|
1611
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`;
|
|
1612
|
-
const isRateLimitReason = /rate-limit/i.test(reason);
|
|
1613
1646
|
const isSilence = isSilenceReason(reason);
|
|
1614
1647
|
const shown = providerFault ? attempt : attempt;
|
|
1615
1648
|
const retries = prospectiveRetries ?? w.workRetries;
|
|
1616
1649
|
const head = [
|
|
1650
|
+
/rate-limit/i.test(reason) ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
1617
1651
|
isSilence ? null : reason,
|
|
1618
|
-
isRateLimitReason ? null : `attempt ${Math.min(shown, maxRetries)}/${maxRetries} (retry #${retries})`,
|
|
1619
1652
|
isSilence ? `idle ${hms(elapsed)}` : null,
|
|
1620
1653
|
now
|
|
1621
1654
|
].filter((x) => typeof x === "string" && x.length > 0).join("; ");
|
|
@@ -1696,7 +1729,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
1696
1729
|
}
|
|
1697
1730
|
w.recovering = true;
|
|
1698
1731
|
const attempt = providerFault ? Math.max(1, w.resumeAttempts) : w.resumeAttempts + 1;
|
|
1699
|
-
const delay =
|
|
1732
|
+
const delay = Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff);
|
|
1700
1733
|
if (providerFault) {
|
|
1701
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}`);
|
|
1702
1735
|
}
|
|
@@ -2183,6 +2216,7 @@ ${suffix}`).slice(0, 2000);
|
|
|
2183
2216
|
if (w.completionSignaled) {
|
|
2184
2217
|
w.completionSignaled = false;
|
|
2185
2218
|
w.completionSignaledAt = 0;
|
|
2219
|
+
w.latchReopened = false;
|
|
2186
2220
|
dbg(`${short(sid)} genuine new user message \u2014 re-opening latched completion`);
|
|
2187
2221
|
}
|
|
2188
2222
|
if (w.resumeAttempts > 0) {
|
|
@@ -2468,6 +2502,7 @@ ${reminder}`;
|
|
|
2468
2502
|
}
|
|
2469
2503
|
w.completionSignaled = true;
|
|
2470
2504
|
w.completionSignaledAt = Date.now();
|
|
2505
|
+
w.completionHard = true;
|
|
2471
2506
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
2472
2507
|
w.taskCompleteSignals++;
|
|
2473
2508
|
if (w.taskCompleteSignals === 2)
|
|
@@ -2519,6 +2554,7 @@ ${reminder}`;
|
|
|
2519
2554
|
}
|
|
2520
2555
|
w.completionSignaled = true;
|
|
2521
2556
|
w.completionSignaledAt = Date.now();
|
|
2557
|
+
w.completionHard = true;
|
|
2522
2558
|
w.taskCompleteSignals = 0;
|
|
2523
2559
|
w.taskCompleteOverrides = 0;
|
|
2524
2560
|
log("info", `${short(sid)} task_complete called, ${await isSubAgentSession(sid) ? "subagent" : "agent"} done`);
|
|
@@ -2557,6 +2593,8 @@ ${reminder}`;
|
|
|
2557
2593
|
if (open.length === 0 && !w.completionSignaled) {
|
|
2558
2594
|
w.completionSignaled = true;
|
|
2559
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() ?? "");
|
|
2560
2598
|
log("info", `${short(sid)} turn ends with a completion claim inside the activity window \u2014 latching completion, standing down`);
|
|
2561
2599
|
}
|
|
2562
2600
|
} else {
|
|
@@ -2595,6 +2633,7 @@ ${reminder}`;
|
|
|
2595
2633
|
if (!w.completionSignaled) {
|
|
2596
2634
|
w.completionSignaled = true;
|
|
2597
2635
|
w.completionSignaledAt = Date.now();
|
|
2636
|
+
w.completionHard = false;
|
|
2598
2637
|
log("info", `${short(sid)} turn ends with a celebration and no open todos \u2014 latching completion, not nudging`);
|
|
2599
2638
|
} else {
|
|
2600
2639
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -2627,6 +2666,8 @@ ${reminder}`;
|
|
|
2627
2666
|
if (!w.completionSignaled) {
|
|
2628
2667
|
w.completionSignaled = true;
|
|
2629
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() ?? "");
|
|
2630
2671
|
log("info", `${short(sid)} turn ends with an explicit stop and no open todos \u2014 latching completion, not nudging`);
|
|
2631
2672
|
} else {
|
|
2632
2673
|
dbg(`${short(sid)} completion already latched \u2014 skipping`);
|
|
@@ -2973,7 +3014,10 @@ ${reminder}`;
|
|
|
2973
3014
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
2974
3015
|
w.completionSignaled = false;
|
|
2975
3016
|
w.completionSignaledAt = 0;
|
|
3017
|
+
w.latchReopened = false;
|
|
2976
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`);
|
|
2977
3021
|
} else {
|
|
2978
3022
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
2979
3023
|
}
|
|
@@ -2993,33 +3037,8 @@ ${reminder}`;
|
|
|
2993
3037
|
const baseInterval = (profileChunkTimeoutMs + profileGracePeriodMs) * fuseMult;
|
|
2994
3038
|
const jitter = baseInterval > 60000 ? Math.floor(Math.random() * STALL_JITTER_MS * 2) - STALL_JITTER_MS : 0;
|
|
2995
3039
|
if (silence >= baseInterval + jitter) {
|
|
2996
|
-
|
|
2997
|
-
const _qlast2 = history.length ? history[history.length - 1] : null;
|
|
2998
|
-
let _qtype2 = "";
|
|
2999
|
-
let _qmsg2 = "";
|
|
3000
|
-
if (_qlast2 && msgRole(_qlast2) === "assistant") {
|
|
3001
|
-
const _qe2 = _qlast2.error ?? _qlast2.info?.error;
|
|
3002
|
-
if (typeof _qe2 === "string")
|
|
3003
|
-
_qmsg2 = _qe2;
|
|
3004
|
-
else if (_qe2 && typeof _qe2 === "object") {
|
|
3005
|
-
_qtype2 = String(_qe2.name ?? _qe2.type ?? "");
|
|
3006
|
-
_qmsg2 = String(_qe2.message ?? "");
|
|
3007
|
-
}
|
|
3008
|
-
const _qtxt2 = lastAssistantTextFrom(history);
|
|
3009
|
-
if (_qtxt2)
|
|
3010
|
-
_qmsg2 = _qmsg2 ? `${_qmsg2} ${_qtxt2}` : _qtxt2;
|
|
3011
|
-
}
|
|
3012
|
-
if (isRateLimitError(_qtype2, _qmsg2)) {
|
|
3013
|
-
handleRateLimitFailure(sid, w, _qtype2, _qmsg2);
|
|
3014
|
-
continue;
|
|
3015
|
-
}
|
|
3016
|
-
} catch (e) {
|
|
3017
|
-
dbg(`${short(sid)} quota guard (idle) failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3018
|
-
}
|
|
3019
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
3020
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
3040
|
+
if (quotaGuard(sid, w, history))
|
|
3021
3041
|
continue;
|
|
3022
|
-
}
|
|
3023
3042
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
3024
3043
|
}
|
|
3025
3044
|
}
|
|
@@ -3043,7 +3062,10 @@ ${reminder}`;
|
|
|
3043
3062
|
if (w.lastGenuineUserAt > w.completionSignaledAt) {
|
|
3044
3063
|
w.completionSignaled = false;
|
|
3045
3064
|
w.completionSignaledAt = 0;
|
|
3065
|
+
w.latchReopened = false;
|
|
3046
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`);
|
|
3047
3069
|
} else {
|
|
3048
3070
|
dbg(`${short(sid)} completion latched \u2014 skipping stall detection`);
|
|
3049
3071
|
continue;
|
|
@@ -3140,34 +3162,9 @@ ${reminder}`;
|
|
|
3140
3162
|
log("info", `${short(sid)} stall (busyStallStrategy=abort): aborting before continue`);
|
|
3141
3163
|
await tryAbortAndResume(sid, w, true);
|
|
3142
3164
|
} else {
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
const _qlast = _qm.length ? _qm[_qm.length - 1] : null;
|
|
3146
|
-
let _qtype = "";
|
|
3147
|
-
let _qmsg = "";
|
|
3148
|
-
if (_qlast && msgRole(_qlast) === "assistant") {
|
|
3149
|
-
const _qe = _qlast.error ?? _qlast.info?.error;
|
|
3150
|
-
if (typeof _qe === "string")
|
|
3151
|
-
_qmsg = _qe;
|
|
3152
|
-
else if (_qe && typeof _qe === "object") {
|
|
3153
|
-
_qtype = String(_qe.name ?? _qe.type ?? "");
|
|
3154
|
-
_qmsg = String(_qe.message ?? "");
|
|
3155
|
-
}
|
|
3156
|
-
const _qtxt = lastAssistantTextFrom(_qm);
|
|
3157
|
-
if (_qtxt)
|
|
3158
|
-
_qmsg = _qmsg ? `${_qmsg} ${_qtxt}` : _qtxt;
|
|
3159
|
-
}
|
|
3160
|
-
if (isRateLimitError(_qtype, _qmsg)) {
|
|
3161
|
-
handleRateLimitFailure(sid, w, _qtype, _qmsg);
|
|
3162
|
-
continue;
|
|
3163
|
-
}
|
|
3164
|
-
} catch (e) {
|
|
3165
|
-
dbg(`${short(sid)} quota guard failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3166
|
-
}
|
|
3167
|
-
if (w.awaitingQuotaRetry || w.rateLimitedAt > 0) {
|
|
3168
|
-
dbg(`${short(sid)} quota-armed \u2014 skipping stall recover`);
|
|
3165
|
+
const _qm = await loadMessages(sid);
|
|
3166
|
+
if (quotaGuard(sid, w, _qm))
|
|
3169
3167
|
continue;
|
|
3170
|
-
}
|
|
3171
3168
|
await recover(sid, `no activity for ${Math.ceil(silence / 1000)}s`);
|
|
3172
3169
|
}
|
|
3173
3170
|
}
|
|
@@ -3504,11 +3501,21 @@ ${reminder}`;
|
|
|
3504
3501
|
let ownRung = !providerRetryFresh && selfInjectFresh;
|
|
3505
3502
|
if (ownRung) {
|
|
3506
3503
|
try {
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
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;
|
|
3512
3519
|
}
|
|
3513
3520
|
} catch {
|
|
3514
3521
|
ownRung = false;
|
package/package.json
CHANGED