opencode-auto-resume 1.0.23 → 1.0.24

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.
Files changed (3) hide show
  1. package/README.md +163 -30
  2. package/dist/index.js +77 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,42 +1,162 @@
1
1
  # opencode-auto-resume
2
2
 
3
- **Plugin for [OpenCode](https://github.com/anomalyco/opencode) that automatically detects and recovers from LLM session failures — stalls, broken tool calls, hallucination loops, and stuck subagent parents. Fully silent, zero UI pollution.**
3
+ **Plugin for [OpenCode](https://github.com/anomalyco/opencode) that automatically detects and recovers from LLM session failures — stalls, broken tool calls, hallucination loops, stuck subagent parents, and more. Fully silent, zero UI pollution.**
4
4
 
5
5
  ## What it does
6
6
 
7
- LLM sessions fail in predictable ways. This plugin monitors all sessions and automatically recovers without user intervention.
7
+ LLM sessions fail in predictable ways. This plugin monitors all sessions and automatically recovers without user intervention. Each recovery path below references the upstream OpenCode issues that motivated it — these are problems not yet resolved in the official project.
8
8
 
9
- **Stall recovery** — the stream goes silent but the session stays "busy". The UI shows a blinking cursor with no progress. If no events arrive for 48 seconds, the plugin sends `"continue"` with exponential backoff. After 3 failed attempts it gives up.
9
+ ---
10
+
11
+ ### Stall recovery
12
+
13
+ The stream goes silent but the session stays "busy". The UI shows a blinking cursor with no progress. If no events arrive for 48 seconds (`chunkTimeoutMs` + `gracePeriodMs`), the plugin sends `"continue"` with exponential backoff. After 3 failed attempts it gives up.
10
14
 
11
15
  The plugin extracts the **agent, model, and provider** from the last session message, so it resumes with the exact same configuration the user was using (build, sisyphus, prometheus, etc.).
12
- _( [#55](https://github.com/anomalyco/opencode/issues/55), [#199](https://github.com/anomalyco/opencode/issues/199), [#283](https://github.com/anomalyco/opencode/issues/283) )_
13
16
 
14
- **Tool calls as raw text** — the model prints tool invocations as raw XML (`<function=edit>...`) instead of executing them. The session goes idle normally but the tool was never run. On idle, the plugin fetches the last messages and scans for XML tool-call patterns (including truncated and alternative formats). If found, it sends a specific recovery prompt.
15
- _( [#150](https://github.com/anomalyco/opencode/issues/150), [#313](https://github.com/anomalyco/opencode/issues/313), [#353](https://github.com/anomalyco/opencode/issues/353) )_
17
+ _Motivated by:_
18
+ - [#34214](https://github.com/anomalyco/opencode/issues/34214) Opencode freezes / becomes unresponsive mid-session
19
+ - [#35207](https://github.com/anomalyco/opencode/issues/35207) — Session hangs indefinitely after MCP tool-call — no timeout recovery (deadlock)
20
+ - [#31655](https://github.com/anomalyco/opencode/issues/31655) — Window completely frozen/unresponsive after reopening a project with a stalled session
21
+ - [#34460](https://github.com/anomalyco/opencode/issues/34460) — Thread freezes after switching to Go model — requires Escape key to unstick
22
+
23
+ ---
24
+
25
+ ### Tool calls as raw text
26
+
27
+ The model prints tool invocations as raw XML/JSON (`<function=edit>...`, `{"type":"function",...}`) instead of executing them. The session goes idle normally but the tool was never run. On idle, the plugin fetches the last messages and scans for XML tool-call patterns — including truncated and alternative formats. If found, it sends `TOOL_TEXT_RECOVERY_PROMPT` to force a clean tool call.
28
+
29
+ Also detects tool calls trapped inside **thinking/reasoning** parts and emits `THINKING_TOOL_RECOVERY_PROMPT` to extract them.
30
+
31
+ _Motivated by:_
32
+ - [#31247](https://github.com/anomalyco/opencode/issues/31247) — Copilot Claude Opus 4.8 emits pseudo tool-call text instead of structured tool calls
33
+ - [#34126](https://github.com/anomalyco/opencode/issues/34126) — OpenAI Chat parser treats standalone text before tool_calls as assistant text
34
+ - [#33959](https://github.com/anomalyco/opencode/issues/33959) — OpenCode Desktop does not execute valid OpenAI tool calls from qwen3-coder:30b (Ollama)
35
+ - [#35689](https://github.com/anomalyco/opencode/issues/35689) — DeepSeek silently stops executing (interleaved reasoning_content dropped in tool call messages)
36
+
37
+ ---
38
+
39
+ ### Hallucination loop
40
+
41
+ The model generates the same broken output repeatedly. Each `continue` just picks up the broken generation. If a session needs 3+ continues within 10 minutes, the plugin aborts the request and sends `"continue"` fresh, forcing a clean restart.
42
+
43
+ A separate **tool-call loop detector** also catches the model calling the same tool 3+ consecutive times (or repeating patterns of length 2-5 occurring at least three times). When detected, it emits `TOOL_LOOP_RECOVERY_PROMPT` to break the loop instead of blindly continuing.
44
+
45
+ _Motivated by:_
46
+ - [#22142](https://github.com/anomalyco/opencode/issues/22142) — Repetitive tool-call loops with alibaba-coding-plan-cn/qwen3.6-plus
47
+ - [#16218](https://github.com/anomalyco/opencode/issues/16218) — Model repeats the same response in a loop after generating an answer
48
+ - [#33216](https://github.com/anomalyco/opencode/issues/33216) — OpenCode Repeatedly Ignores Instructions and Loops Responses
49
+ - [#35784](https://github.com/anomalyco/opencode/issues/35784) — opencode-go/glm-5.2 + read file loop
50
+ - [#25129](https://github.com/anomalyco/opencode/issues/25129) — Thinking mode gets stuck in infinite repetition loop
51
+
52
+ ---
53
+
54
+ ### Orphan parent
55
+
56
+ A subagent finishes but the parent session stays stuck as "busy" forever. The plugin detects when `busyCount` drops from >1 to 1, waits 15 seconds (`subagentWaitMs`), then aborts and resumes the parent.
57
+
58
+ _Motivated by:_
59
+ - [#35066](https://github.com/anomalyco/opencode/issues/35066) — notify parent when subagent sessions finish
60
+ - [#33050](https://github.com/anomalyco/opencode/issues/33050) — orphaned sessions continue looping after abort, causing sustained high CPU
61
+ - [#32335](https://github.com/anomalyco/opencode/issues/32335) — opencode run processes don't exit after completing scheduled work, causing memory leak
62
+
63
+ ---
64
+
65
+ ### Subagent stuck detection
66
+
67
+ Detects when a subagent hasn't received new text for >1 minute (or >3 minutes if a tool call is in progress). If stuck, sends a recovery prompt to the subagent before triggering abort+resume on the parent. This avoids killing a parent that merely has a slow child.
68
+
69
+ _Motivated by:_
70
+ - [#35073](https://github.com/anomalyco/opencode/issues/35073) — subagent permission asks hang indefinitely (sync subagents treated as interactive)
71
+ - [#35806](https://github.com/anomalyco/opencode/issues/35806) — malformed tool-input stream ordering crashes Session drains
72
+ - [#32580](https://github.com/anomalyco/opencode/issues/32580) — Agent showing repeated thinking
73
+
74
+ ---
75
+
76
+ ### Active-tool safety guard
77
+
78
+ Before **any** abort, the plugin calls `checkSessionHasActiveTool()` to verify the session isn't mid-tool-execution. If a tool is running, the abort is skipped. This prevents the plugin from killing a long-running build, test suite, or command — even when it looks like a stall.
79
+
80
+ _Motivated by:_
81
+ - [#26063](https://github.com/anomalyco/opencode/issues/26063) — Tool execution aborted/terminated
82
+ - [#31459](https://github.com/anomalyco/opencode/issues/31459) — "Tool execution aborted" during Preparing write
83
+ - [#5937](https://github.com/Mte90/opencode-auto-resume) — (internal) must never abort a tool execution
84
+
85
+ ---
86
+
87
+ ### Model, agent & provider preservation
88
+
89
+ When resuming with `"continue"`, the plugin extracts agent, model, and provider from the last session message — falling back to `msg.info.agent` / `msg.info.model` if the top-level field is missing. This preserves the user's UI selection across resumes instead of reverting to the default agent.
16
90
 
17
- **Hallucination loop** — the model generates the same broken output repeatedly. Each continue just picks up the broken generation. If a session needs 3+ continues within 10 minutes, the plugin aborts the request and sends `"continue"` fresh, forcing a clean restart.
18
- _( [#283](https://github.com/anomalyco/opencode/issues/283), [#353](https://github.com/anomalyco/opencode/issues/353) )_
91
+ _Motivated by:_
92
+ - [#35126](https://github.com/anomalyco/opencode/issues/35126) Subagents launched via task tool ignore their model: frontmatter and inherit parent agent's model
93
+ - [#35899](https://github.com/anomalyco/opencode/issues/35899) — Web: switching sessions overwrites user-selected model with agent default
94
+ - [#34562](https://github.com/anomalyco/opencode/issues/34562) — Model provider not restored correctly when switching projects
19
95
 
20
- **Orphan parent** — a subagent finishes but the parent session stays stuck as "busy" forever. The plugin detects when busyCount drops from >1 to 1, waits 15 seconds, then aborts and resumes the parent.
21
- _( [#122](https://github.com/anomalyco/opencode/issues/122), [#199](https://github.com/anomalyco/opencode/issues/199), [#246](https://github.com/anomalyco/opencode/issues/246) )_
96
+ ---
22
97
 
23
- **False positives during subagent work** — long tool execution or active subagents can look like a stall. Only the session emitting events gets its timer reset (not all sessions). When multiple sessions are busy, stall detection is paused entirely.
24
- _( [#55](https://github.com/anomalyco/opencode/issues/55), [#221](https://github.com/anomalyco/opencode/issues/221) )_
98
+ ### ESC cancel respected
25
99
 
26
- **ESC cancel** — user presses ESC to cancel a request. The plugin detects `MessageAbortedError` and marks all busy sessions as cancelled, never resuming them.
100
+ User presses ESC to cancel a request. The plugin detects `MessageAbortedError` and marks all busy sessions as cancelled, never resuming them. The grace period (`gracePeriodMs`) also lets late ESC/status events arrive before any action.
27
101
 
28
- **Explicit completion via `task_complete`** — the agent can call the built-in `task_complete` tool to signal that all work is done. When invoked, the plugin stops sending any further "continue" prompts, clears all pending timers, and marks the session as complete. This replaces fragile text-based heuristics (emoji patterns, language detection) with a deterministic signal.
102
+ _Motivated by:_
103
+ - [#28453](https://github.com/anomalyco/opencode/issues/28453) — ACP session/cancel emits agent_error for MessageAbortedError before cancelled result
104
+ - [#32432](https://github.com/anomalyco/opencode/issues/32432) — Cancelled subagents can't be opened in TUI + Ctrl+X intermittently fails
105
+ - [#30144](https://github.com/anomalyco/opencode/issues/30144) — Early prompt cancel can poison directory instance
29
106
 
30
- **Spurious error messages** — after normal completion, OpenCode sometimes fires a `session.error`. All logging goes through `ctx.client.app.log()` (zero `console.log`), and errors on already-idle sessions are silently ignored.
31
- _( [#128](https://github.com/anomalyco/opencode/pulls/128), [#22](https://github.com/anomalyco/opencode/pulls/22) )_
107
+ ---
32
108
 
33
- **Session discovery** periodically calls `session.list()` to pick up sessions that were missed by event tracking. Idle sessions are cleaned up after 10 minutes to prevent memory leaks.
109
+ ### Explicit completion via `task_complete`
34
110
 
35
- **Model preservation** when resuming with "continue", the plugin extracts agent, model and provider from the last session message (not from `info` field) to preserve the user's UI selection.
36
- _( [#111](https://github.com/anomalyco/opencode/issues/111), [#277](https://github.com/anomalyco/opencode/issues/277) )_
111
+ The agent can call the built-in `task_complete` tool to signal that all work is done. When invoked, the plugin stops sending any further `"continue"` prompts, clears all pending timers, and marks the session as complete. This replaces fragile text-based heuristics (emoji patterns, language detection) with a deterministic signal.
37
112
 
38
- **Subagent stuck detection** — detects when a subagent hasn't received new text for >1 minute (or >3 minutes if a tool call is in progress). If stuck, sends a recovery prompt before triggering abort+resume on the parent.
39
- _( [#55](https://github.com/anomalyco/opencode/issues/55), [#60](https://github.com/anomalyco/opencode/issues/60), [#246](https://github.com/anomalyco/opencode/issues/246) )_
113
+ ---
114
+
115
+ ### 🎉 emoji completion
116
+
117
+ An assistant message ending with 🎉 resets the tool-text timer and prevents a trigger — the emoji signals the agent considers the task complete.
118
+
119
+ ---
120
+
121
+ ### Ready-to-continue auto-resume
122
+
123
+ When the assistant prints phrases like "Ready to continue with task" or "Proceeding with task", the plugin automatically sends `"continue"` without waiting for the user. This catches the common pattern where the model stops to ask for permission it doesn't need.
124
+
125
+ ---
126
+
127
+ ### Done-claim verification
128
+
129
+ 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. This uses the real `todo.updated` event state — not regex on the message text.
130
+
131
+ ---
132
+
133
+ ### False-positive protection during subagent work
134
+
135
+ Long tool execution or active subagents can look like a stall. Only the session emitting events gets its timer reset (not all sessions). When multiple sessions are busy, stall detection is paused entirely. The plugin also clears existing timers before creating a new `setTimeout` in idle handlers to prevent queued continue triggers.
136
+
137
+ ---
138
+
139
+ ### Spurious error suppression
140
+
141
+ After normal completion, OpenCode sometimes fires a `session.error`. All logging goes through `ctx.client.app.log()` (zero `console.log`), and errors on already-idle sessions are silently ignored.
142
+
143
+ _Motivated by:_
144
+ - [#33687](https://github.com/anomalyco/opencode/issues/33687) — Interrupted assistant messages retain non-error finish value (orphan tool-input-start abort)
145
+ - [#28958](https://github.com/anomalyco/opencode/issues/28958) — Plugin hook rejection aborts unrelated parallel sessions
146
+ - [#25899](https://github.com/anomalyco/opencode/issues/25899) — ACP prompt() returns stopReason: end_turn + zero usage on user cancel
147
+
148
+ ---
149
+
150
+ ### Session discovery & cleanup
151
+
152
+ Periodically calls `session.list()` (every 60s) to pick up sessions that were missed by event tracking. Idle sessions are cleaned up after 10 minutes or when the idle map exceeds 50 entries, preventing memory leaks.
153
+
154
+ _Motivated by:_
155
+ - [#35750](https://github.com/anomalyco/opencode/issues/35750) — Upgrade to 1.17.x hides pre-existing sessions — new path column not back-filled during migration
156
+ - [#33102](https://github.com/anomalyco/opencode/issues/33102) — OpenCode Go workspace subscription is orphaned/hidden and cannot be managed from dashboard
157
+ - [#27759](https://github.com/anomalyco/opencode/issues/27759) — Session heartbeat for multi-session liveness detection
158
+
159
+ ---
40
160
 
41
161
  ## Architecture
42
162
 
@@ -47,17 +167,21 @@ Any SSE Event
47
167
 
48
168
  session.status events:
49
169
  ├─ busy → reset timer, clear retry counters
50
- └─ idle → schedule tool-text check (1.5s delay)
51
- └─ fetch messages → scan for XML patterns
170
+ └─ idle → schedule tool-text check (3s delay)
171
+ └─ fetch messages → scan for XML / thinking-tool patterns
52
172
  ├─ found → send recovery prompt (with backoff)
53
- └─ not found → do nothing
173
+ └─ not found → check ready-to-continue / done-claim patterns
54
174
  └─ orphan check: busyCount dropped from >1 to 1?
55
- └─ 15s watch → abort + continue
175
+ └─ subagentWaitMs watch → abort + continue
176
+
177
+ todo.updated events:
178
+ └─ track real todo state (not regex on message text)
56
179
 
57
180
  Timer loop (every 5s):
58
181
  for each busy session:
59
182
  ├─ orphan watch active? → wait or abort+continue
60
183
  ├─ busyCount > 1? → skip (subagent running)
184
+ ├─ active tool running? → skip (never abort tools)
61
185
  └─ idle > 48s? → hallucination loop? abort : continue with backoff
62
186
 
63
187
  Periodic (every 60s): session.list() to discover missed sessions
@@ -119,6 +243,8 @@ bun run build
119
243
  }
120
244
  ```
121
245
 
246
+ ### Configurable options
247
+
122
248
  | Option | Default | Description |
123
249
  |---|---|---|
124
250
  | `chunkTimeoutMs` | `45000` | Inactivity timeout before considering stream stalled |
@@ -131,13 +257,19 @@ bun run build
131
257
  | `loopMaxContinues` | `3` | Continues in window before triggering abort |
132
258
  | `loopWindowMs` | `600000` | Hallucination loop detection window (10 min) |
133
259
 
134
- ## Verification
260
+ ### Internal constants (not configurable)
135
261
 
136
- To verify the plugin is loaded:
262
+ | Constant | Value | Description |
263
+ |---|---|---|
264
+ | `TOOL_TEXT_CHECK_DELAY_MS` | `3000` | Delay before scanning idle session for tool-as-text |
265
+ | `ABORT_CONTINUE_DELAY_MS` | `2000` | Delay between abort and continue |
266
+ | `MAX_IDLE_SESSIONS` | `50` | Idle session map cap before cleanup |
267
+ | `IDLE_CLEANUP_MS` | `600000` | Idle session age before cleanup (10 min) |
268
+ | `SESSION_DISCOVERY_INTERVAL_MS` | `60000` | `session.list()` poll interval (60s) |
269
+
270
+ ## Verification
137
271
 
138
- 1. Check OpenCode logs for: `opencode-auto-resume ready. timeout=45000ms...`
139
- 2. Let a session idle for 48 seconds — it should auto-resume
140
- 3. Check logs for `Stream stall` or `Ready-to-continue pattern detected`
272
+ To verify the plugin is loaded, run `/status` inside OpenCode it lists all loaded plugins and their versions. You should see `opencode-auto-resume` in the list.
141
273
 
142
274
  The plugin handles all recovery automatically — no manual intervention needed.
143
275
 
@@ -151,3 +283,4 @@ The plugin handles all recovery automatically — no manual intervention needed.
151
283
  | Orphan parent not detected | Increase `subagentWaitMs` to `20000` |
152
284
  | Hallucination loop not caught | Decrease `loopMaxContinues` to `2` |
153
285
  | Tool-text not detected | Check server logs — requires SDK message fetching |
286
+ | Long-running tool killed | Should not happen — active-tool guard prevents it. Report a bug. |
package/dist/index.js CHANGED
@@ -12533,9 +12533,17 @@ var AutoResumePlugin = async (ctx, options) => {
12533
12533
  const props = ev.properties;
12534
12534
  return ev.error ?? props?.error;
12535
12535
  }
12536
- function getStatus(ev) {
12536
+ function getStatusType(ev) {
12537
12537
  const props = ev.properties;
12538
- return ev.status ?? props?.status;
12538
+ const rawStatus = ev.status ?? props?.status;
12539
+ if (typeof rawStatus === "string")
12540
+ return rawStatus;
12541
+ if (rawStatus && typeof rawStatus === "object") {
12542
+ const type = rawStatus.type;
12543
+ if (typeof type === "string")
12544
+ return type;
12545
+ }
12546
+ return "unknown";
12539
12547
  }
12540
12548
  function short(sid) {
12541
12549
  return sid.length > 12 ? `...${sid.slice(-8)}` : sid;
@@ -12586,8 +12594,7 @@ var AutoResumePlugin = async (ctx, options) => {
12586
12594
  let agent;
12587
12595
  let model;
12588
12596
  try {
12589
- const msgResp = await ctx.client.session.messages({ path: { id: sid } });
12590
- const msgs = extractMessages(msgResp);
12597
+ const msgs = await getSessionMessages(sid);
12591
12598
  for (let i = msgs.length - 1;i >= 0; i--) {
12592
12599
  const msg = msgs[i];
12593
12600
  const role = msg.role ?? msg.info?.role;
@@ -12663,6 +12670,38 @@ var AutoResumePlugin = async (ctx, options) => {
12663
12670
  return response.messages;
12664
12671
  return [];
12665
12672
  }
12673
+ async function getSessionMessages(sid) {
12674
+ const response = await ctx.client.session.messages({ path: { id: sid } });
12675
+ return extractMessages(response);
12676
+ }
12677
+ function roleOf(msg) {
12678
+ if (!msg)
12679
+ return;
12680
+ return msg.role ?? msg.info?.role;
12681
+ }
12682
+ async function getSessionStatusMap() {
12683
+ try {
12684
+ const response = await ctx.client.session.status();
12685
+ const raw = response.data ?? response;
12686
+ const result = {};
12687
+ if (raw && typeof raw === "object") {
12688
+ for (const [sid, val] of Object.entries(raw)) {
12689
+ if (typeof val === "string") {
12690
+ result[sid] = val;
12691
+ } else if (val && typeof val === "object") {
12692
+ const type = val.type;
12693
+ if (typeof type === "string")
12694
+ result[sid] = type;
12695
+ }
12696
+ }
12697
+ }
12698
+ return result;
12699
+ } catch (err) {
12700
+ const errMsg = err instanceof Error ? err.message : String(err);
12701
+ await log("debug", `session.status() failed: ${errMsg}`);
12702
+ return {};
12703
+ }
12704
+ }
12666
12705
  const SUBAGENT_STUCK_MS = 60000;
12667
12706
  const SUBAGENT_RECOVERY_PROMPT = "It looks like you may have stalled or timed out. Please retry the last operation or continue with the task.";
12668
12707
  async function recoverSubagent(subagentSid) {
@@ -12681,14 +12720,11 @@ var AutoResumePlugin = async (ctx, options) => {
12681
12720
  }
12682
12721
  async function hasBusySubagents(parentSid) {
12683
12722
  try {
12684
- const response = await ctx.client.session.list();
12685
- const allSessions = extractMessages(response);
12686
- for (const s of allSessions) {
12687
- const sId = s.id;
12723
+ const statusMap = await getSessionStatusMap();
12724
+ for (const [sId, statusType] of Object.entries(statusMap)) {
12688
12725
  if (!sId || sId === parentSid)
12689
12726
  continue;
12690
- const status = s.status;
12691
- if (status === "busy")
12727
+ if (statusType === "busy")
12692
12728
  return true;
12693
12729
  }
12694
12730
  return false;
@@ -12700,21 +12736,16 @@ var AutoResumePlugin = async (ctx, options) => {
12700
12736
  }
12701
12737
  async function checkSessionHasActiveTool(sid) {
12702
12738
  try {
12703
- const listResponse = await ctx.client.session.list();
12704
- const sessions2 = extractMessages(listResponse);
12705
- const currentSession = sessions2.find((s) => s.id === sid);
12706
- const status = currentSession?.status;
12707
- if (status === "busy") {
12739
+ const statusMap = await getSessionStatusMap();
12740
+ if (statusMap[sid] === "busy") {
12708
12741
  await log("debug", `Session ${short(sid)} is busy, likely executing a tool`);
12709
12742
  return true;
12710
12743
  }
12711
- const response = await ctx.client.session.messages({ path: { id: sid } });
12712
- const messages = extractMessages(response);
12744
+ const messages = await getSessionMessages(sid);
12713
12745
  const lastMsg = messages[messages.length - 1];
12714
12746
  if (!lastMsg)
12715
12747
  return false;
12716
- const rawRole = lastMsg?.role ?? lastMsg?.info?.role;
12717
- if (rawRole !== "assistant")
12748
+ if (roleOf(lastMsg) !== "assistant")
12718
12749
  return false;
12719
12750
  const toolCall = lastMsg.toolCall;
12720
12751
  const toolCalls = lastMsg.tool_calls;
@@ -12729,22 +12760,17 @@ var AutoResumePlugin = async (ctx, options) => {
12729
12760
  }
12730
12761
  async function checkSubagentStatus(parentSid) {
12731
12762
  try {
12732
- const response = await ctx.client.session.list();
12733
- const allSessions = extractMessages(response);
12763
+ const statusMap = await getSessionStatusMap();
12734
12764
  const now = Date.now();
12735
12765
  let hasBusySubagent = false;
12736
- for (const s of allSessions) {
12737
- const sId = s.id;
12766
+ for (const [sId, statusType] of Object.entries(statusMap)) {
12738
12767
  if (!sId || sId === parentSid)
12739
12768
  continue;
12740
- const status = s.status;
12741
- if (status === "busy") {
12769
+ if (statusType === "busy") {
12742
12770
  hasBusySubagent = true;
12743
- const msgResponse = await ctx.client.session.messages({ path: { id: sId } });
12744
- const messages = extractMessages(msgResponse);
12771
+ const messages = await getSessionMessages(sId);
12745
12772
  const lastMsg = messages[messages.length - 1];
12746
- const rawRole = lastMsg?.role ?? lastMsg?.info?.role;
12747
- if (lastMsg && rawRole === "assistant" && (("error" in lastMsg) || lastMsg.info && ("error" in lastMsg.info))) {
12773
+ if (lastMsg && roleOf(lastMsg) === "assistant" && (("error" in lastMsg) || lastMsg.info && ("error" in lastMsg.info))) {
12748
12774
  await log("debug", `Subagent ${short(sId)} appears crashed`);
12749
12775
  return { status: "crashed" };
12750
12776
  }
@@ -12856,10 +12882,7 @@ var AutoResumePlugin = async (ctx, options) => {
12856
12882
  return;
12857
12883
  await log("debug", `${short(sid)} - checking for tool-call-as-text (attempt ${w.toolTextAttempts + 1})`);
12858
12884
  try {
12859
- const response = await ctx.client.session.messages({
12860
- path: { id: sid }
12861
- });
12862
- const messages = extractMessages(response);
12885
+ const messages = await getSessionMessages(sid);
12863
12886
  const recent = messages.slice(-3);
12864
12887
  let bestCandidate = null;
12865
12888
  let allAssistantText = "";
@@ -13161,18 +13184,14 @@ var AutoResumePlugin = async (ctx, options) => {
13161
13184
  timer = setInterval(async () => {
13162
13185
  const now = Date.now();
13163
13186
  const numBusy = busyCount();
13187
+ const statusMap = await getSessionStatusMap();
13164
13188
  for (const [sid, w] of sessions) {
13165
- try {
13166
- const listResponse = await ctx.client.session.list();
13167
- const allSessions = extractMessages(listResponse);
13168
- const realSession2 = allSessions.find((s) => s.id === sid);
13169
- const realStatus = realSession2?.status;
13170
- if (realStatus && realStatus !== w.status) {
13171
- w.status = realStatus;
13172
- if (realStatus === "busy")
13173
- w.idleSince = null;
13174
- }
13175
- } catch {}
13189
+ const realStatus = statusMap[sid];
13190
+ if (realStatus && realStatus !== w.status) {
13191
+ w.status = realStatus;
13192
+ if (realStatus === "busy")
13193
+ w.idleSince = null;
13194
+ }
13176
13195
  if (w.status !== "busy")
13177
13196
  continue;
13178
13197
  if (w.userCancelled)
@@ -13183,6 +13202,12 @@ var AutoResumePlugin = async (ctx, options) => {
13183
13202
  const orphanIdle = now - w.orphanWatchStartAt;
13184
13203
  if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
13185
13204
  if (w.resumeAttempts < maxRetries) {
13205
+ const hasActiveTool = await checkSessionHasActiveTool(sid);
13206
+ if (hasActiveTool) {
13207
+ await log("debug", `Parent ${short(sid)} has active tool call, skipping orphan-watch abort`);
13208
+ w.orphanWatchStartAt = now;
13209
+ continue;
13210
+ }
13186
13211
  const subStatus = await checkSubagentStatus(sid);
13187
13212
  if (subStatus.status === "crashed" && subStatus.stuckSid) {
13188
13213
  const recovered = await recoverSubagent(subStatus.stuckSid);
@@ -13218,16 +13243,11 @@ var AutoResumePlugin = async (ctx, options) => {
13218
13243
  continue;
13219
13244
  w.lastSubagentCheckAt = now;
13220
13245
  if (w.lastActivityAt > 0 && now - w.lastActivityAt > subagentWaitMs) {
13221
- try {
13222
- const listResponse = await ctx.client.session.list();
13223
- const allSessions = extractMessages(listResponse);
13224
- const realStatus = realSession?.status;
13225
- if (realStatus === "busy") {
13226
- await log("debug", `Session ${short(sid)} is still busy (real status), skipping abort`);
13227
- w.lastSubagentCheckAt = now;
13228
- continue;
13229
- }
13230
- } catch {}
13246
+ if (realStatus === "busy") {
13247
+ await log("debug", `Session ${short(sid)} is still busy (real status), skipping abort`);
13248
+ w.lastSubagentCheckAt = now;
13249
+ continue;
13250
+ }
13231
13251
  const hasActiveTool = await checkSessionHasActiveTool(sid);
13232
13252
  if (hasActiveTool) {
13233
13253
  await log("debug", `Session ${short(sid)} has active tool call, skipping abort check`);
@@ -13286,14 +13306,14 @@ var AutoResumePlugin = async (ctx, options) => {
13286
13306
  case "session.status": {
13287
13307
  if (!sid)
13288
13308
  break;
13289
- const status = getStatus(ev);
13290
- const statusType = status?.type ?? "unknown";
13309
+ const statusType = getStatusType(ev);
13291
13310
  const w = ensureWatch(sid);
13292
13311
  w.status = statusType;
13293
13312
  if (statusType === "busy") {
13294
13313
  w.lastActivityAt = Date.now();
13295
13314
  resetSessionFlags(w);
13296
- log("debug", `${short(sid)} -> busy (${busyCount()})`);
13315
+ prevBusyCount = busyCount();
13316
+ log("debug", `${short(sid)} -> busy (${prevBusyCount})`);
13297
13317
  } else if (statusType === "idle" || statusType === "interrupted") {
13298
13318
  w.status = "idle";
13299
13319
  resetIdleFlags(w);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",