opencode-auto-resume 1.1.14 → 1.1.16
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 +18 -2
- package/dist/index.js +251 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,6 +156,10 @@ _Motivated by:_
|
|
|
156
156
|
|
|
157
157
|
The model stream can die after emitting only reasoning — no text part, no tool call — finalizing with `finish: "unknown"`. OpenCode treats the message as completed and the session goes idle, so no error or stall path triggers. On idle, if the **newest** assistant message has a finish reason, zero text parts, and at least `silentDeadStreamMinTokens` output tokens, the plugin sends a recovery prompt. Only the newest assistant message is evaluated — a delivered text answer means normal completion, and older tool-call steps are never misread as dead streams. Recovery is also skipped if the session has gone busy/retry again before the prompt is sent (race guard).
|
|
158
158
|
|
|
159
|
+
### Context saturation → magic-context wrapup
|
|
160
|
+
|
|
161
|
+
A session can fill its usable context window without stalling — it just keeps working until it chokes. The plugin tracks token usage from `message.updated` events and computes the ratio against the model's usable window (`context − min(20k, maxOutput)`, mirroring OpenCode's own overflow math). On idle, when the ratio crosses `contextSaturationThreshold` (default 0.85), routing depends on session kind. For parent sessions, only when magic-context is detected in the host's configured plugin list (`config.get().plugin`), the plugin invokes the registered `ctx-wrapup` command through `client.session.command()` — it does not send `/ctx-wrapup` as prompt text, because prompt text is not expanded into a command. For subagent sessions identified by `parentID` on `session.created`, the default is no intervention: magic-context does run bounded cleanup for subagents (structural-noise/cleared-reasoning strips, heuristic drops, ceiling nudges), but its hard protections — historian compartments, emergency fail-closed abort, caveman compression — skip subagents, and on true overflow the error just propagates to the parent with no deterministic reclaim. Terminal reclamation therefore depends on the agent calling `ctx_reduce` when nudged. If `subagentNativeCompactionEnabled` is `true`, the plugin calls native `session.summarize()` as an opt-in safety net. Fail-safe: provider lookup errors, unknown model limits, user cancellation, or completion signals mean no intervention. Magic-context absence additionally disables only the parent path (the `ctx-wrapup` command would not be registered); the opt-in subagent path needs no magic-context detection because `session.summarize()` is native. This path is magic-context-gated on purpose: magic-context's setup disables OpenCode's native compaction, so unconditionally summarizing a magic-context-managed parent would double-compress and fight its cache-aware historian. One intervention per busy cycle.
|
|
162
|
+
|
|
159
163
|
---
|
|
160
164
|
|
|
161
165
|
### Active-tool safety guard
|
|
@@ -221,7 +225,16 @@ When the assistant ends a line with `:` ("Next, I will edit the file:") — anno
|
|
|
221
225
|
|
|
222
226
|
### Done-claim verification
|
|
223
227
|
|
|
224
|
-
If the assistant claims the task is done ("task done", "finished", "all complete") but open todos remain, the plugin sends `DONE_WITHOUT_WORK_PROMPT` asking the agent to verify and finish remaining work. If it claims done with **no** open todos
|
|
228
|
+
If the assistant claims the task is done ("task done", "finished", "all complete") but open todos remain, the plugin sends `DONE_WITHOUT_WORK_PROMPT` asking the agent to verify and finish remaining work. If it claims done with **no** open todos and the response carries no work description, the plugin sends `DONE_WITHOUT_DETAILS_PROMPT` — an imperative prompt demanding a concrete report (files changed, commands run, results). A response that already contains file paths, verification output, or result sections satisfies the demand on its own and never triggers the prompt. The budget is capped at `maxRetries` across busy cycles (going busy no longer re-arms it); only a genuinely new inbound user message re-arms it. Both use the real `todo.updated` event state — not regex on the message text.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### User-input awareness
|
|
233
|
+
|
|
234
|
+
The session is not stalled while the ball is in the user's court. Two gates stand down idle nudges:
|
|
235
|
+
|
|
236
|
+
- **Awaiting input**: the newest assistant message holds a `tool_use` part with `state.status: "pending"` (e.g. an open `question` tool call). All idle checks, the periodic recheck, delayed action-intent callbacks, and the tool-text scan skip prompting until a newer user message clears the gate. Completed tool calls never engage it.
|
|
237
|
+
- **Recently active user**: any inbound user message within `activeUserWindowMs` (default 15 minutes) means the user is engaged — likely composing a reply, which leaves no pending tool call behind. Open-todos nudges (idle + periodic), the tool-text reminder fallback, and action-intent callbacks stand down until the window expires.
|
|
225
238
|
|
|
226
239
|
---
|
|
227
240
|
|
|
@@ -261,7 +274,7 @@ All recovery paths fall into three families. Which family fires determines what
|
|
|
261
274
|
|
|
262
275
|
Fire only after OpenCode reports the session **idle** — the runner has exited. The prompt starts a new run.
|
|
263
276
|
|
|
264
|
-
Paths: todo nudges, tool-call-as-text recovery, thinking-tool recovery, action-intent nudge, ready-to-continue, done-claim verification, streaming-failure recovery, silent dead-stream recovery.
|
|
277
|
+
Paths: todo nudges, tool-call-as-text recovery, thinking-tool recovery, action-intent nudge, ready-to-continue, done-claim verification, streaming-failure recovery, silent dead-stream recovery, context-saturation routing (magic-context-gated; parent sessions use `session.command`, subagents use opt-in native `session.summarize`).
|
|
265
278
|
|
|
266
279
|
### 2. Busy-silence continue (stream stall)
|
|
267
280
|
|
|
@@ -384,6 +397,9 @@ With options:
|
|
|
384
397
|
| `doneWithoutDetailsPrompt` | `DONE_WITHOUT_DETAILS_PROMPT` | Override the done-claim-with-no-todos report prompt |
|
|
385
398
|
| `silentDeadStreamMinTokens` | `200` | Min output tokens to treat a textless `finish:"unknown"` message as a dead stream |
|
|
386
399
|
| `busyStallStrategy` | `"continue"` | Busy-stall response: `"continue"`, `"abort"` (abort-first), or `"off"` (disabled) |
|
|
400
|
+
| `contextSaturationThreshold` | `0.85` | Ratio of used/usable context that routes a saturated parent to magic-context `ctx-wrapup` (only when magic-context is installed) |
|
|
401
|
+
| `activeUserWindowMs` | `900000` | Inbound-user-message recency window (15 min) during which idle nudges stand down (user likely composing) |
|
|
402
|
+
| `subagentNativeCompactionEnabled` | `false` | Opt-in native `session.summarize()` for saturated subagent sessions (no magic-context detection required) |
|
|
387
403
|
|
|
388
404
|
Message patterns are matched case-insensitively. Error names use exact match.
|
|
389
405
|
|
package/dist/index.js
CHANGED
|
@@ -12337,6 +12337,7 @@ tool.schema = exports_external;
|
|
|
12337
12337
|
// src/index.ts
|
|
12338
12338
|
var DEFAULT_CHUNK_TIMEOUT_MS = 45000;
|
|
12339
12339
|
var DEFAULT_CHECK_INTERVAL_MS = 5000;
|
|
12340
|
+
var DEFAULT_ACTIVE_USER_WINDOW_MS = 15 * 60000;
|
|
12340
12341
|
var DEFAULT_GRACE_PERIOD_MS = 3000;
|
|
12341
12342
|
var DEFAULT_MAX_RETRIES = 3;
|
|
12342
12343
|
var DEFAULT_MAX_BACKOFF_MS = 8000;
|
|
@@ -12370,6 +12371,7 @@ var SESSION_DISCOVERY_INTERVAL_MS = 60000;
|
|
|
12370
12371
|
var TOOL_TEXT_RECOVERY_PROMPT = "Your last message contained a raw tool call printed as text instead of being executed. " + "Please use the proper tool calling mechanism to execute it.";
|
|
12371
12372
|
var THINKING_TOOL_RECOVERY_PROMPT = "I noticed you have a tool call generated in your thinking/reasoning. " + "Please execute it using the proper tool calling mechanism instead of keeping it in reasoning.";
|
|
12372
12373
|
var TOOL_LOOP_RECOVERY_PROMPT = "I notice you've been calling the same tool multiple times in a row without making progress. " + "Please step back and reassess your approach. Consider: " + "1) Are you stuck in a loop? 2) Do you need different information first? " + "3) Should you try a different tool or break the task into smaller steps? " + "Take a moment to think about what's blocking you and propose a different strategy.";
|
|
12374
|
+
var CTX_WRAPUP_TRIGGER = "ctx-wrapup";
|
|
12373
12375
|
var TOOL_TEXT_PATTERNS = [
|
|
12374
12376
|
/<function\s*=/i,
|
|
12375
12377
|
/<function>/i,
|
|
@@ -12455,6 +12457,15 @@ function containsDoneClaimPattern(text) {
|
|
|
12455
12457
|
`);
|
|
12456
12458
|
return DONE_CLAIM_PATTERNS.some((pat) => pat.test(lastLines));
|
|
12457
12459
|
}
|
|
12460
|
+
function containsWorkDescription(text) {
|
|
12461
|
+
if (/`[^`\n]*\.[a-zA-Z0-9]{1,8}[^`\n]*`/.test(text))
|
|
12462
|
+
return true;
|
|
12463
|
+
if (/[\w\-~.][\w\-.~\/]*\/[\w\-.~]*\.[a-zA-Z]{1,8}\b/.test(text))
|
|
12464
|
+
return true;
|
|
12465
|
+
if (/^(changed|modified|deleted|created|updated|renamed|moved|files?\s+changed|verification|verified|tests?(?:\s+run|\s+passing|\s+pass)?|results?|outcome|commands?\s+(?:run|executed))/im.test(text))
|
|
12466
|
+
return true;
|
|
12467
|
+
return false;
|
|
12468
|
+
}
|
|
12458
12469
|
function isStreamingFailure(errorName, errorMessage, errorNames = DEFAULT_STREAMING_FAILURE_ERROR_NAMES, messagePatterns = DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS) {
|
|
12459
12470
|
if (!errorName && !errorMessage)
|
|
12460
12471
|
return false;
|
|
@@ -12619,6 +12630,9 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12619
12630
|
const silentDeadStreamMinTokens = options?.silentDeadStreamMinTokens ?? DEFAULT_SILENT_DEAD_STREAM_MIN_TOKENS;
|
|
12620
12631
|
const rawBusyStallStrategy = options?.busyStallStrategy ?? "continue";
|
|
12621
12632
|
const busyStallStrategy = rawBusyStallStrategy === "abort" || rawBusyStallStrategy === "off" ? rawBusyStallStrategy : "continue";
|
|
12633
|
+
const contextSaturationThreshold = options?.contextSaturationThreshold ?? 0.85;
|
|
12634
|
+
const subagentNativeCompactionEnabled = options?.subagentNativeCompactionEnabled ?? false;
|
|
12635
|
+
const activeUserWindowMs = options?.activeUserWindowMs ?? DEFAULT_ACTIVE_USER_WINDOW_MS;
|
|
12622
12636
|
const dbg = (...args) => {
|
|
12623
12637
|
if (debug)
|
|
12624
12638
|
console.log("[debug]", ...args);
|
|
@@ -12706,12 +12720,15 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12706
12720
|
interruptedContinueCount: 0,
|
|
12707
12721
|
recentToolCalls: [],
|
|
12708
12722
|
liveToolSigs: [],
|
|
12723
|
+
lastTokenTotal: 0,
|
|
12724
|
+
contextWrapupAttempts: 0,
|
|
12709
12725
|
toolLoopAttempts: 0,
|
|
12710
12726
|
isSubagent: false,
|
|
12711
12727
|
completionSignaled: false,
|
|
12712
12728
|
todoNudgeAttempts: 0,
|
|
12713
12729
|
taskCompleteOverrides: 0,
|
|
12714
12730
|
doneClaimNoTodosAttempts: 0,
|
|
12731
|
+
lastUserMessageAt: 0,
|
|
12715
12732
|
pendingTools: 0,
|
|
12716
12733
|
pendingCommands: 0,
|
|
12717
12734
|
pendingRecovery: false,
|
|
@@ -12892,6 +12909,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12892
12909
|
}
|
|
12893
12910
|
}
|
|
12894
12911
|
dbg(`Recovery prompt sent to ${short(sid)}: prompt="${text.length > 80 ? `${text.slice(0, 80)}...` : text}", agent=${agent ?? "(default)"}, model=${model ? `${model.providerID}/${model.modelID}` : "(default)"}`);
|
|
12912
|
+
if (w.userCancelled || w.completionSignaled)
|
|
12913
|
+
return;
|
|
12895
12914
|
const response = await ctx.client.session.prompt({
|
|
12896
12915
|
path: { id: sid },
|
|
12897
12916
|
body: {
|
|
@@ -12933,7 +12952,12 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12933
12952
|
setTimeout(async () => {
|
|
12934
12953
|
if (w.status !== "busy") {
|
|
12935
12954
|
if (w.pendingRecovery) {
|
|
12936
|
-
if (w.
|
|
12955
|
+
if (w.userCancelled || w.completionSignaled) {
|
|
12956
|
+
w.pendingRecovery = false;
|
|
12957
|
+
w.pendingRecoveryReason = null;
|
|
12958
|
+
w.recoveryAttempts = 0;
|
|
12959
|
+
await log("info", `${short(sid)} - recovery disarmed: session cancelled/completed while awaiting watchdog`);
|
|
12960
|
+
} else if (w.recoveryAttempts < maxRecoveryRetries) {
|
|
12937
12961
|
w.recoveryAttempts++;
|
|
12938
12962
|
dbg(`State transition on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts - 1} -> ${w.recoveryAttempts}`);
|
|
12939
12963
|
w.watchdogRetryGuard = true;
|
|
@@ -12989,6 +13013,31 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12989
13013
|
return response.messages;
|
|
12990
13014
|
return [];
|
|
12991
13015
|
}
|
|
13016
|
+
function hasPendingUserInput(messages) {
|
|
13017
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
13018
|
+
const msg = messages[i];
|
|
13019
|
+
const rawRole = msg.role ?? msg.info?.role;
|
|
13020
|
+
if (rawRole === "user")
|
|
13021
|
+
return false;
|
|
13022
|
+
if (rawRole !== "assistant")
|
|
13023
|
+
continue;
|
|
13024
|
+
const parts = msg.parts;
|
|
13025
|
+
if (!parts)
|
|
13026
|
+
return false;
|
|
13027
|
+
for (const part of parts) {
|
|
13028
|
+
if (part.type !== "tool_use")
|
|
13029
|
+
continue;
|
|
13030
|
+
const state = part.state;
|
|
13031
|
+
if (state?.status === "pending")
|
|
13032
|
+
return true;
|
|
13033
|
+
}
|
|
13034
|
+
return false;
|
|
13035
|
+
}
|
|
13036
|
+
return false;
|
|
13037
|
+
}
|
|
13038
|
+
function userRecentlyActive(w) {
|
|
13039
|
+
return w.lastUserMessageAt > 0 && Date.now() - w.lastUserMessageAt < activeUserWindowMs;
|
|
13040
|
+
}
|
|
12992
13041
|
const messagesInflight = new Map;
|
|
12993
13042
|
async function getSessionMessages(sid) {
|
|
12994
13043
|
const inflight = messagesInflight.get(sid);
|
|
@@ -13124,6 +13173,67 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13124
13173
|
return false;
|
|
13125
13174
|
}
|
|
13126
13175
|
}
|
|
13176
|
+
let magicContextDetected = null;
|
|
13177
|
+
async function isMagicContextInstalled() {
|
|
13178
|
+
if (magicContextDetected !== null)
|
|
13179
|
+
return magicContextDetected;
|
|
13180
|
+
try {
|
|
13181
|
+
const res = await ctx.client.config?.get?.();
|
|
13182
|
+
const cfg = res?.data;
|
|
13183
|
+
const plugins = cfg?.plugin;
|
|
13184
|
+
if (!Array.isArray(plugins)) {
|
|
13185
|
+
dbg("magic-context detection: plugin list unavailable, treating as not installed");
|
|
13186
|
+
return false;
|
|
13187
|
+
}
|
|
13188
|
+
magicContextDetected = plugins.some((p) => {
|
|
13189
|
+
const spec = typeof p === "string" ? p : Array.isArray(p) ? String(p[0]) : "";
|
|
13190
|
+
return spec.toLowerCase().includes("magic-context");
|
|
13191
|
+
});
|
|
13192
|
+
return magicContextDetected;
|
|
13193
|
+
} catch (e) {
|
|
13194
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
13195
|
+
dbg(`magic-context detection failed, treating as not installed: ${errMsg}`);
|
|
13196
|
+
return false;
|
|
13197
|
+
}
|
|
13198
|
+
}
|
|
13199
|
+
const usableLimitCache = new Map;
|
|
13200
|
+
async function getUsableContextLimit(sid) {
|
|
13201
|
+
try {
|
|
13202
|
+
const msgs = await getSessionMessages(sid);
|
|
13203
|
+
let model;
|
|
13204
|
+
for (let i = msgs.length - 1;i >= 0; i--) {
|
|
13205
|
+
const m = msgs[i];
|
|
13206
|
+
if (m.role === "user") {
|
|
13207
|
+
model = m.model ?? m.info?.model;
|
|
13208
|
+
break;
|
|
13209
|
+
}
|
|
13210
|
+
}
|
|
13211
|
+
if (!model || typeof model.providerID !== "string" || typeof model.modelID !== "string") {
|
|
13212
|
+
return null;
|
|
13213
|
+
}
|
|
13214
|
+
const key = `${model.providerID}/${model.modelID}`;
|
|
13215
|
+
const cached = usableLimitCache.get(key);
|
|
13216
|
+
if (cached !== undefined)
|
|
13217
|
+
return cached;
|
|
13218
|
+
const res = await ctx.client.provider?.get?.();
|
|
13219
|
+
const providers = res?.data ?? res;
|
|
13220
|
+
if (!Array.isArray(providers))
|
|
13221
|
+
return null;
|
|
13222
|
+
const prov = providers.find((p) => p.id === model.providerID);
|
|
13223
|
+
const models = prov?.models;
|
|
13224
|
+
const entry = models?.find((x) => x.id === model.modelID);
|
|
13225
|
+
const limit = entry?.limit;
|
|
13226
|
+
if (!limit || typeof limit.context !== "number" || limit.context === 0)
|
|
13227
|
+
return null;
|
|
13228
|
+
const usable = limit.context - Math.min(20000, limit.output ?? 0);
|
|
13229
|
+
usableLimitCache.set(key, usable);
|
|
13230
|
+
return usable;
|
|
13231
|
+
} catch (e) {
|
|
13232
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
13233
|
+
dbg(`usable-context-limit lookup failed for ${short(sid)}: ${errMsg}`);
|
|
13234
|
+
return null;
|
|
13235
|
+
}
|
|
13236
|
+
}
|
|
13127
13237
|
async function checkSessionHasActiveTool(sid) {
|
|
13128
13238
|
try {
|
|
13129
13239
|
const statusMap = await getSessionStatusMap();
|
|
@@ -13234,6 +13344,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13234
13344
|
w.recentToolCalls = [];
|
|
13235
13345
|
w.liveToolSigs = [];
|
|
13236
13346
|
w.toolLoopAttempts = 0;
|
|
13347
|
+
w.contextWrapupAttempts = 0;
|
|
13237
13348
|
w.pendingRecovery = false;
|
|
13238
13349
|
w.pendingRecoveryReason = null;
|
|
13239
13350
|
w.pendingRecoveryAt = 0;
|
|
@@ -13244,7 +13355,6 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13244
13355
|
w.toolTextTimer = null;
|
|
13245
13356
|
}
|
|
13246
13357
|
w.todoNudgeAttempts = 0;
|
|
13247
|
-
w.doneClaimNoTodosAttempts = 0;
|
|
13248
13358
|
w.continueTimestamps = [];
|
|
13249
13359
|
}
|
|
13250
13360
|
function resetIdleFlags(w) {
|
|
@@ -13321,6 +13431,16 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13321
13431
|
await log("debug", `${short(sid)} - checking for tool-call-as-text (attempt ${w.toolTextAttempts + 1})`);
|
|
13322
13432
|
try {
|
|
13323
13433
|
const messages = await getSessionMessages(sid);
|
|
13434
|
+
if (hasPendingUserInput(messages)) {
|
|
13435
|
+
w.checkingToolText = false;
|
|
13436
|
+
await log("info", `${short(sid)} - awaiting user input (pending tool_use), skipping tool-text check`);
|
|
13437
|
+
return;
|
|
13438
|
+
}
|
|
13439
|
+
if (userRecentlyActive(w)) {
|
|
13440
|
+
w.checkingToolText = false;
|
|
13441
|
+
await log("info", `${short(sid)} - user recently active, skipping tool-text check`);
|
|
13442
|
+
return;
|
|
13443
|
+
}
|
|
13324
13444
|
const recent = messages.slice(-3);
|
|
13325
13445
|
let bestCandidate = null;
|
|
13326
13446
|
let allAssistantText = "";
|
|
@@ -13457,6 +13577,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13457
13577
|
source: "done-claim-no-emoji",
|
|
13458
13578
|
priority: 1
|
|
13459
13579
|
};
|
|
13580
|
+
} else if (containsWorkDescription(allAssistantText)) {
|
|
13581
|
+
await log("info", `${short(sid)} - model claims done with no open todos, but the response already contains a work description. Skipping details prompt...`);
|
|
13460
13582
|
} else if (w.doneClaimNoTodosAttempts < maxRetries) {
|
|
13461
13583
|
await log("info", `${short(sid)} - model claims done with no open todos. Sending details prompt (attempt ${w.doneClaimNoTodosAttempts + 1}/${maxRetries})...`);
|
|
13462
13584
|
bestCandidate = {
|
|
@@ -13599,6 +13721,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13599
13721
|
await new Promise((resolve) => setTimeout(resolve, ABORT_CONTINUE_DELAY_MS));
|
|
13600
13722
|
if (w.status === "busy")
|
|
13601
13723
|
w.status = "idle";
|
|
13724
|
+
if (w.userCancelled || w.completionSignaled) {
|
|
13725
|
+
w.aborting = false;
|
|
13726
|
+
await log("info", `${short(sid)} - abort+resume stood down: session cancelled/completed during abort delay`);
|
|
13727
|
+
return false;
|
|
13728
|
+
}
|
|
13602
13729
|
try {
|
|
13603
13730
|
await sendContinuePrompt(sid, continuePrompt, w);
|
|
13604
13731
|
await log("info", `${short(sid)} - abort+continue done`);
|
|
@@ -13851,6 +13978,18 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13851
13978
|
continue;
|
|
13852
13979
|
if (busyCount() !== 0)
|
|
13853
13980
|
continue;
|
|
13981
|
+
try {
|
|
13982
|
+
if (hasPendingUserInput(await getSessionMessages(sid))) {
|
|
13983
|
+
await log("info", `${short(sid)} - awaiting user input (pending tool_use), skipping periodic open-todos nudge`);
|
|
13984
|
+
continue;
|
|
13985
|
+
}
|
|
13986
|
+
} catch (e) {
|
|
13987
|
+
dbg(`periodic recheck sid=${short(sid)}: awaiting-input check error: ${e}`);
|
|
13988
|
+
}
|
|
13989
|
+
if (userRecentlyActive(w)) {
|
|
13990
|
+
await log("info", `${short(sid)} - user recently active, skipping periodic open-todos nudge`);
|
|
13991
|
+
continue;
|
|
13992
|
+
}
|
|
13854
13993
|
if ((w.todos || []).length === 0) {
|
|
13855
13994
|
const fetched = await fetchSessionTodos(sid);
|
|
13856
13995
|
if (fetched.length > 0) {
|
|
@@ -13942,8 +14081,46 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13942
14081
|
}
|
|
13943
14082
|
prevBusyCount = currentBusy;
|
|
13944
14083
|
log("debug", `${short(sid)} -> idle (${currentBusy})`);
|
|
14084
|
+
if (w.isSubagent) {
|
|
14085
|
+
try {
|
|
14086
|
+
let subAwaitingInput = false;
|
|
14087
|
+
try {
|
|
14088
|
+
subAwaitingInput = hasPendingUserInput(await getSessionMessages(sid));
|
|
14089
|
+
} catch (e) {
|
|
14090
|
+
dbg(`session.idle sid=${short(sid)}: awaiting-input check error: ${e}`);
|
|
14091
|
+
}
|
|
14092
|
+
if (subAwaitingInput) {
|
|
14093
|
+
await log("info", `${short(sid)} - awaiting user input (pending tool_use), skipping subagent saturation check`);
|
|
14094
|
+
} else if (userRecentlyActive(w)) {
|
|
14095
|
+
await log("info", `${short(sid)} - user recently active, skipping subagent saturation check`);
|
|
14096
|
+
} else if (w.lastTokenTotal > 0 && w.contextWrapupAttempts < 1 && !w.userCancelled && !w.completionSignaled && !w.aborting) {
|
|
14097
|
+
const usable = await getUsableContextLimit(sid);
|
|
14098
|
+
if (usable && w.lastTokenTotal / usable >= contextSaturationThreshold) {
|
|
14099
|
+
if (subagentNativeCompactionEnabled) {
|
|
14100
|
+
w.contextWrapupAttempts++;
|
|
14101
|
+
await log("warn", `${short(sid)} - context saturation (subagent): ${w.lastTokenTotal}/${usable} tokens (${Math.round(w.lastTokenTotal / usable * 100)}% of usable); triggering native compaction`);
|
|
14102
|
+
if (w.userCancelled || w.completionSignaled)
|
|
14103
|
+
break;
|
|
14104
|
+
await ctx.client.session.summarize({ path: { id: sid } });
|
|
14105
|
+
}
|
|
14106
|
+
}
|
|
14107
|
+
}
|
|
14108
|
+
} catch (e) {
|
|
14109
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
14110
|
+
dbg(`session.idle sid=${short(sid)}: context-saturation check error: ${errMsg}`);
|
|
14111
|
+
}
|
|
14112
|
+
}
|
|
13945
14113
|
if (!w.isSubagent) {
|
|
13946
|
-
|
|
14114
|
+
let awaitingUserInput = false;
|
|
14115
|
+
try {
|
|
14116
|
+
awaitingUserInput = hasPendingUserInput(await getSessionMessages(sid));
|
|
14117
|
+
} catch (e) {
|
|
14118
|
+
dbg(`session.idle sid=${short(sid)}: awaiting-input check error: ${e}`);
|
|
14119
|
+
}
|
|
14120
|
+
if (awaitingUserInput) {
|
|
14121
|
+
await log("info", `${short(sid)} - awaiting user input (pending tool_use), standing down all idle checks/nudges`);
|
|
14122
|
+
}
|
|
14123
|
+
if (!awaitingUserInput && !w.pendingRecovery && !w.completionSignaled && !w.userCancelled && !w.aborting) {
|
|
13947
14124
|
try {
|
|
13948
14125
|
const errInfo = getLastAssistantError(await getSessionMessages(sid));
|
|
13949
14126
|
if (errInfo && isStreamingFailure(errInfo.name, errInfo.message, streamingFailureErrorNames, streamingFailureMessagePatterns)) {
|
|
@@ -13982,6 +14159,27 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13982
14159
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
13983
14160
|
dbg(`session.idle sid=${short(sid)}: silent-dead-stream check error: ${errMsg}`);
|
|
13984
14161
|
}
|
|
14162
|
+
try {
|
|
14163
|
+
if (w.lastTokenTotal > 0 && w.contextWrapupAttempts < 1 && !w.userCancelled && !w.completionSignaled && !userRecentlyActive(w)) {
|
|
14164
|
+
const usable = await getUsableContextLimit(sid);
|
|
14165
|
+
if (usable && w.lastTokenTotal / usable >= contextSaturationThreshold) {
|
|
14166
|
+
const installed = await isMagicContextInstalled();
|
|
14167
|
+
if (!installed)
|
|
14168
|
+
break;
|
|
14169
|
+
w.contextWrapupAttempts++;
|
|
14170
|
+
await log("warn", `${short(sid)} - context saturation: ${w.lastTokenTotal}/${usable} tokens (${Math.round(w.lastTokenTotal / usable * 100)}% of usable); sending magic-context wrapup command`);
|
|
14171
|
+
if (w.userCancelled || w.completionSignaled)
|
|
14172
|
+
break;
|
|
14173
|
+
await ctx.client.session.command({
|
|
14174
|
+
path: { id: sid },
|
|
14175
|
+
body: { command: CTX_WRAPUP_TRIGGER, arguments: "" }
|
|
14176
|
+
});
|
|
14177
|
+
}
|
|
14178
|
+
}
|
|
14179
|
+
} catch (e) {
|
|
14180
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
14181
|
+
dbg(`session.idle sid=${short(sid)}: context-saturation check error: ${errMsg}`);
|
|
14182
|
+
}
|
|
13985
14183
|
}
|
|
13986
14184
|
let todos = w.todos || [];
|
|
13987
14185
|
if (todos.length === 0) {
|
|
@@ -13992,7 +14190,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13992
14190
|
}
|
|
13993
14191
|
}
|
|
13994
14192
|
const open = getOpenTodos(todos);
|
|
13995
|
-
if (open.length > 0 && currentBusy === 0 && !w.completionSignaled && !w.userCancelled && w.todoNudgeAttempts < maxRetries) {
|
|
14193
|
+
if (open.length > 0 && currentBusy === 0 && !awaitingUserInput && !userRecentlyActive(w) && !w.completionSignaled && !w.userCancelled && w.todoNudgeAttempts < maxRetries) {
|
|
13996
14194
|
const isCelebration = await lastAssistantEndsWithCelebration(sid);
|
|
13997
14195
|
await log("info", `${short(sid)} - open todos=${open.length}, isCelebration=${isCelebration}, currentBusy=${currentBusy}`);
|
|
13998
14196
|
if (isCelebration) {
|
|
@@ -14018,6 +14216,14 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14018
14216
|
return;
|
|
14019
14217
|
}
|
|
14020
14218
|
const msgs = await getSessionMessages(idleSid);
|
|
14219
|
+
if (hasPendingUserInput(msgs)) {
|
|
14220
|
+
dbg(`session.idle sid=${short(idleSid)}: awaiting user input, skipping action-intent prompt`);
|
|
14221
|
+
return;
|
|
14222
|
+
}
|
|
14223
|
+
if (userRecentlyActive(idleW)) {
|
|
14224
|
+
dbg(`session.idle sid=${short(idleSid)}: user recently active, skipping action-intent prompt`);
|
|
14225
|
+
return;
|
|
14226
|
+
}
|
|
14021
14227
|
const lastAssistantMsg = msgs.slice().reverse().find((m) => (m.role ?? m.info?.role) === "assistant");
|
|
14022
14228
|
if (lastAssistantMsg) {
|
|
14023
14229
|
let lastText = "";
|
|
@@ -14058,7 +14264,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14058
14264
|
const w = ensureWatch(sid);
|
|
14059
14265
|
w.pendingTools = 0;
|
|
14060
14266
|
w.pendingCommands = 0;
|
|
14061
|
-
|
|
14267
|
+
const createdProps = ev.properties;
|
|
14268
|
+
const parentID = createdProps?.parentID ?? createdProps?.session?.parentID;
|
|
14269
|
+
w.isSubagent = typeof parentID === "string" && parentID.length > 0;
|
|
14270
|
+
log("debug", `New session: ${short(sid)} (${sessions.size})${w.isSubagent ? " [subagent]" : ""}`);
|
|
14062
14271
|
break;
|
|
14063
14272
|
}
|
|
14064
14273
|
case "session.updated": {
|
|
@@ -14082,6 +14291,15 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14082
14291
|
return;
|
|
14083
14292
|
}
|
|
14084
14293
|
const msgs = await getSessionMessages(sid);
|
|
14294
|
+
if (hasPendingUserInput(msgs)) {
|
|
14295
|
+
dbg(`session.idle sid=${short(sid)}: awaiting user input, skipping action-intent prompt`);
|
|
14296
|
+
return;
|
|
14297
|
+
}
|
|
14298
|
+
const w2pre = sessions.get(sid);
|
|
14299
|
+
if (w2pre && userRecentlyActive(w2pre)) {
|
|
14300
|
+
dbg(`session.idle sid=${short(sid)}: user recently active, skipping action-intent prompt`);
|
|
14301
|
+
return;
|
|
14302
|
+
}
|
|
14085
14303
|
const lastAssistantMsg = msgs.slice().reverse().find((m) => (m.role ?? m.info?.role) === "assistant");
|
|
14086
14304
|
if (lastAssistantMsg) {
|
|
14087
14305
|
let lastText = "";
|
|
@@ -14131,6 +14349,34 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14131
14349
|
}
|
|
14132
14350
|
break;
|
|
14133
14351
|
}
|
|
14352
|
+
case "message.updated": {
|
|
14353
|
+
if (!sid)
|
|
14354
|
+
break;
|
|
14355
|
+
const props = ev.properties;
|
|
14356
|
+
const info = props?.info;
|
|
14357
|
+
const role = info?.role ?? props?.role;
|
|
14358
|
+
if (role === "user") {
|
|
14359
|
+
const w = sessions.get(sid);
|
|
14360
|
+
if (w) {
|
|
14361
|
+
w.doneClaimNoTodosAttempts = 0;
|
|
14362
|
+
w.lastUserMessageAt = Date.now();
|
|
14363
|
+
}
|
|
14364
|
+
break;
|
|
14365
|
+
}
|
|
14366
|
+
if (role !== "assistant")
|
|
14367
|
+
break;
|
|
14368
|
+
const tokens = info?.tokens ?? props?.tokens;
|
|
14369
|
+
if (!tokens)
|
|
14370
|
+
break;
|
|
14371
|
+
const cache = tokens.cache;
|
|
14372
|
+
const total = tokens.total ?? (tokens.input ?? 0) + (tokens.output ?? 0) + (cache?.read ?? 0) + (cache?.write ?? 0);
|
|
14373
|
+
if (typeof total === "number" && total > 0) {
|
|
14374
|
+
const w = ensureWatch(sid);
|
|
14375
|
+
w.lastActivityAt = Date.now();
|
|
14376
|
+
w.lastTokenTotal = total;
|
|
14377
|
+
}
|
|
14378
|
+
break;
|
|
14379
|
+
}
|
|
14134
14380
|
case "todo.updated": {
|
|
14135
14381
|
if (!sid)
|
|
14136
14382
|
break;
|
package/package.json
CHANGED