opencode-auto-resume 1.1.14 → 1.1.15
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 +7 -1
- package/dist/index.js +127 -1
- 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
|
|
@@ -261,7 +265,7 @@ All recovery paths fall into three families. Which family fires determines what
|
|
|
261
265
|
|
|
262
266
|
Fire only after OpenCode reports the session **idle** — the runner has exited. The prompt starts a new run.
|
|
263
267
|
|
|
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.
|
|
268
|
+
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
269
|
|
|
266
270
|
### 2. Busy-silence continue (stream stall)
|
|
267
271
|
|
|
@@ -384,6 +388,8 @@ With options:
|
|
|
384
388
|
| `doneWithoutDetailsPrompt` | `DONE_WITHOUT_DETAILS_PROMPT` | Override the done-claim-with-no-todos report prompt |
|
|
385
389
|
| `silentDeadStreamMinTokens` | `200` | Min output tokens to treat a textless `finish:"unknown"` message as a dead stream |
|
|
386
390
|
| `busyStallStrategy` | `"continue"` | Busy-stall response: `"continue"`, `"abort"` (abort-first), or `"off"` (disabled) |
|
|
391
|
+
| `contextSaturationThreshold` | `0.85` | Ratio of used/usable context that routes a saturated parent to magic-context `ctx-wrapup` (only when magic-context is installed) |
|
|
392
|
+
| `subagentNativeCompactionEnabled` | `false` | Opt-in native `session.summarize()` for saturated subagent sessions (no magic-context detection required) |
|
|
387
393
|
|
|
388
394
|
Message patterns are matched case-insensitively. Error names use exact match.
|
|
389
395
|
|
package/dist/index.js
CHANGED
|
@@ -12370,6 +12370,7 @@ var SESSION_DISCOVERY_INTERVAL_MS = 60000;
|
|
|
12370
12370
|
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
12371
|
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
12372
|
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.";
|
|
12373
|
+
var CTX_WRAPUP_TRIGGER = "ctx-wrapup";
|
|
12373
12374
|
var TOOL_TEXT_PATTERNS = [
|
|
12374
12375
|
/<function\s*=/i,
|
|
12375
12376
|
/<function>/i,
|
|
@@ -12619,6 +12620,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12619
12620
|
const silentDeadStreamMinTokens = options?.silentDeadStreamMinTokens ?? DEFAULT_SILENT_DEAD_STREAM_MIN_TOKENS;
|
|
12620
12621
|
const rawBusyStallStrategy = options?.busyStallStrategy ?? "continue";
|
|
12621
12622
|
const busyStallStrategy = rawBusyStallStrategy === "abort" || rawBusyStallStrategy === "off" ? rawBusyStallStrategy : "continue";
|
|
12623
|
+
const contextSaturationThreshold = options?.contextSaturationThreshold ?? 0.85;
|
|
12624
|
+
const subagentNativeCompactionEnabled = options?.subagentNativeCompactionEnabled ?? false;
|
|
12622
12625
|
const dbg = (...args) => {
|
|
12623
12626
|
if (debug)
|
|
12624
12627
|
console.log("[debug]", ...args);
|
|
@@ -12706,6 +12709,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12706
12709
|
interruptedContinueCount: 0,
|
|
12707
12710
|
recentToolCalls: [],
|
|
12708
12711
|
liveToolSigs: [],
|
|
12712
|
+
lastTokenTotal: 0,
|
|
12713
|
+
contextWrapupAttempts: 0,
|
|
12709
12714
|
toolLoopAttempts: 0,
|
|
12710
12715
|
isSubagent: false,
|
|
12711
12716
|
completionSignaled: false,
|
|
@@ -13124,6 +13129,67 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13124
13129
|
return false;
|
|
13125
13130
|
}
|
|
13126
13131
|
}
|
|
13132
|
+
let magicContextDetected = null;
|
|
13133
|
+
async function isMagicContextInstalled() {
|
|
13134
|
+
if (magicContextDetected !== null)
|
|
13135
|
+
return magicContextDetected;
|
|
13136
|
+
try {
|
|
13137
|
+
const res = await ctx.client.config?.get?.();
|
|
13138
|
+
const cfg = res?.data;
|
|
13139
|
+
const plugins = cfg?.plugin;
|
|
13140
|
+
if (!Array.isArray(plugins)) {
|
|
13141
|
+
dbg("magic-context detection: plugin list unavailable, treating as not installed");
|
|
13142
|
+
return false;
|
|
13143
|
+
}
|
|
13144
|
+
magicContextDetected = plugins.some((p) => {
|
|
13145
|
+
const spec = typeof p === "string" ? p : Array.isArray(p) ? String(p[0]) : "";
|
|
13146
|
+
return spec.toLowerCase().includes("magic-context");
|
|
13147
|
+
});
|
|
13148
|
+
return magicContextDetected;
|
|
13149
|
+
} catch (e) {
|
|
13150
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
13151
|
+
dbg(`magic-context detection failed, treating as not installed: ${errMsg}`);
|
|
13152
|
+
return false;
|
|
13153
|
+
}
|
|
13154
|
+
}
|
|
13155
|
+
const usableLimitCache = new Map;
|
|
13156
|
+
async function getUsableContextLimit(sid) {
|
|
13157
|
+
try {
|
|
13158
|
+
const msgs = await getSessionMessages(sid);
|
|
13159
|
+
let model;
|
|
13160
|
+
for (let i = msgs.length - 1;i >= 0; i--) {
|
|
13161
|
+
const m = msgs[i];
|
|
13162
|
+
if (m.role === "user") {
|
|
13163
|
+
model = m.model ?? m.info?.model;
|
|
13164
|
+
break;
|
|
13165
|
+
}
|
|
13166
|
+
}
|
|
13167
|
+
if (!model || typeof model.providerID !== "string" || typeof model.modelID !== "string") {
|
|
13168
|
+
return null;
|
|
13169
|
+
}
|
|
13170
|
+
const key = `${model.providerID}/${model.modelID}`;
|
|
13171
|
+
const cached = usableLimitCache.get(key);
|
|
13172
|
+
if (cached !== undefined)
|
|
13173
|
+
return cached;
|
|
13174
|
+
const res = await ctx.client.provider?.get?.();
|
|
13175
|
+
const providers = res?.data ?? res;
|
|
13176
|
+
if (!Array.isArray(providers))
|
|
13177
|
+
return null;
|
|
13178
|
+
const prov = providers.find((p) => p.id === model.providerID);
|
|
13179
|
+
const models = prov?.models;
|
|
13180
|
+
const entry = models?.find((x) => x.id === model.modelID);
|
|
13181
|
+
const limit = entry?.limit;
|
|
13182
|
+
if (!limit || typeof limit.context !== "number" || limit.context === 0)
|
|
13183
|
+
return null;
|
|
13184
|
+
const usable = limit.context - Math.min(20000, limit.output ?? 0);
|
|
13185
|
+
usableLimitCache.set(key, usable);
|
|
13186
|
+
return usable;
|
|
13187
|
+
} catch (e) {
|
|
13188
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
13189
|
+
dbg(`usable-context-limit lookup failed for ${short(sid)}: ${errMsg}`);
|
|
13190
|
+
return null;
|
|
13191
|
+
}
|
|
13192
|
+
}
|
|
13127
13193
|
async function checkSessionHasActiveTool(sid) {
|
|
13128
13194
|
try {
|
|
13129
13195
|
const statusMap = await getSessionStatusMap();
|
|
@@ -13234,6 +13300,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13234
13300
|
w.recentToolCalls = [];
|
|
13235
13301
|
w.liveToolSigs = [];
|
|
13236
13302
|
w.toolLoopAttempts = 0;
|
|
13303
|
+
w.contextWrapupAttempts = 0;
|
|
13237
13304
|
w.pendingRecovery = false;
|
|
13238
13305
|
w.pendingRecoveryReason = null;
|
|
13239
13306
|
w.pendingRecoveryAt = 0;
|
|
@@ -13942,6 +14009,23 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13942
14009
|
}
|
|
13943
14010
|
prevBusyCount = currentBusy;
|
|
13944
14011
|
log("debug", `${short(sid)} -> idle (${currentBusy})`);
|
|
14012
|
+
if (w.isSubagent) {
|
|
14013
|
+
try {
|
|
14014
|
+
if (w.lastTokenTotal > 0 && w.contextWrapupAttempts < 1 && !w.userCancelled && !w.completionSignaled && !w.aborting) {
|
|
14015
|
+
const usable = await getUsableContextLimit(sid);
|
|
14016
|
+
if (usable && w.lastTokenTotal / usable >= contextSaturationThreshold) {
|
|
14017
|
+
if (subagentNativeCompactionEnabled) {
|
|
14018
|
+
w.contextWrapupAttempts++;
|
|
14019
|
+
await log("warn", `${short(sid)} - context saturation (subagent): ${w.lastTokenTotal}/${usable} tokens (${Math.round(w.lastTokenTotal / usable * 100)}% of usable); triggering native compaction`);
|
|
14020
|
+
await ctx.client.session.summarize({ path: { id: sid } });
|
|
14021
|
+
}
|
|
14022
|
+
}
|
|
14023
|
+
}
|
|
14024
|
+
} catch (e) {
|
|
14025
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
14026
|
+
dbg(`session.idle sid=${short(sid)}: context-saturation check error: ${errMsg}`);
|
|
14027
|
+
}
|
|
14028
|
+
}
|
|
13945
14029
|
if (!w.isSubagent) {
|
|
13946
14030
|
if (!w.pendingRecovery && !w.completionSignaled && !w.userCancelled && !w.aborting) {
|
|
13947
14031
|
try {
|
|
@@ -13982,6 +14066,25 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13982
14066
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
13983
14067
|
dbg(`session.idle sid=${short(sid)}: silent-dead-stream check error: ${errMsg}`);
|
|
13984
14068
|
}
|
|
14069
|
+
try {
|
|
14070
|
+
if (w.lastTokenTotal > 0 && w.contextWrapupAttempts < 1 && !w.userCancelled && !w.completionSignaled) {
|
|
14071
|
+
const usable = await getUsableContextLimit(sid);
|
|
14072
|
+
if (usable && w.lastTokenTotal / usable >= contextSaturationThreshold) {
|
|
14073
|
+
const installed = await isMagicContextInstalled();
|
|
14074
|
+
if (!installed)
|
|
14075
|
+
break;
|
|
14076
|
+
w.contextWrapupAttempts++;
|
|
14077
|
+
await log("warn", `${short(sid)} - context saturation: ${w.lastTokenTotal}/${usable} tokens (${Math.round(w.lastTokenTotal / usable * 100)}% of usable); sending magic-context wrapup command`);
|
|
14078
|
+
await ctx.client.session.command({
|
|
14079
|
+
path: { id: sid },
|
|
14080
|
+
body: { command: CTX_WRAPUP_TRIGGER, arguments: "" }
|
|
14081
|
+
});
|
|
14082
|
+
}
|
|
14083
|
+
}
|
|
14084
|
+
} catch (e) {
|
|
14085
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
14086
|
+
dbg(`session.idle sid=${short(sid)}: context-saturation check error: ${errMsg}`);
|
|
14087
|
+
}
|
|
13985
14088
|
}
|
|
13986
14089
|
let todos = w.todos || [];
|
|
13987
14090
|
if (todos.length === 0) {
|
|
@@ -14058,7 +14161,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14058
14161
|
const w = ensureWatch(sid);
|
|
14059
14162
|
w.pendingTools = 0;
|
|
14060
14163
|
w.pendingCommands = 0;
|
|
14061
|
-
|
|
14164
|
+
const createdProps = ev.properties;
|
|
14165
|
+
const parentID = createdProps?.parentID ?? createdProps?.session?.parentID;
|
|
14166
|
+
w.isSubagent = typeof parentID === "string" && parentID.length > 0;
|
|
14167
|
+
log("debug", `New session: ${short(sid)} (${sessions.size})${w.isSubagent ? " [subagent]" : ""}`);
|
|
14062
14168
|
break;
|
|
14063
14169
|
}
|
|
14064
14170
|
case "session.updated": {
|
|
@@ -14131,6 +14237,26 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14131
14237
|
}
|
|
14132
14238
|
break;
|
|
14133
14239
|
}
|
|
14240
|
+
case "message.updated": {
|
|
14241
|
+
if (!sid)
|
|
14242
|
+
break;
|
|
14243
|
+
const props = ev.properties;
|
|
14244
|
+
const info = props?.info;
|
|
14245
|
+
const role = info?.role ?? props?.role;
|
|
14246
|
+
if (role !== "assistant")
|
|
14247
|
+
break;
|
|
14248
|
+
const tokens = info?.tokens ?? props?.tokens;
|
|
14249
|
+
if (!tokens)
|
|
14250
|
+
break;
|
|
14251
|
+
const cache = tokens.cache;
|
|
14252
|
+
const total = tokens.total ?? (tokens.input ?? 0) + (tokens.output ?? 0) + (cache?.read ?? 0) + (cache?.write ?? 0);
|
|
14253
|
+
if (typeof total === "number" && total > 0) {
|
|
14254
|
+
const w = ensureWatch(sid);
|
|
14255
|
+
w.lastActivityAt = Date.now();
|
|
14256
|
+
w.lastTokenTotal = total;
|
|
14257
|
+
}
|
|
14258
|
+
break;
|
|
14259
|
+
}
|
|
14134
14260
|
case "todo.updated": {
|
|
14135
14261
|
if (!sid)
|
|
14136
14262
|
break;
|
package/package.json
CHANGED