@bybrawe/opencode-loop 0.5.33 → 0.5.34

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.
@@ -130,6 +130,24 @@ export function createSessionStatusRuntime(options = {}) {
130
130
  return cached === "idle" && seenAt > (active.startedAt || 0)
131
131
  }
132
132
 
133
+ async function recoverCompletedTailWithoutActiveRun(directory, client, sessionID, liveType, seenAt) {
134
+ if (activeRuns.has(sessionID)) return false
135
+ if (liveType !== "busy" && liveType !== "retry") return false
136
+ // Never override the first fresh busy observation. Waiting at least one cache
137
+ // window gives the host time to expose a new user/assistant turn if it is real.
138
+ if (!seenAt || now() - seenAt < sessionStatusCacheMs) return false
139
+ const completion = await activeRunCompletionFromMessages(directory, client, sessionID, { startedAt: 0 })
140
+ if (completion !== "completed") return false
141
+ markSessionStatus(sessionID, "idle")
142
+ await appendLoopLog(directory, "status-message-idle-recovery", {
143
+ sessionID,
144
+ staleStatus: liveType,
145
+ statusSeenAt: seenAt,
146
+ staleForMs: Math.max(0, now() - seenAt),
147
+ })
148
+ return true
149
+ }
150
+
133
151
  async function sessionStatusType(client, sessionID, directory, options = {}) {
134
152
  // OpenCode can briefly report an idle session while a long-running tool or
135
153
  // subtask is still executing. Tool lifecycle hooks are the more specific
@@ -152,10 +170,16 @@ export function createSessionStatusRuntime(options = {}) {
152
170
 
153
171
  const live = await readLiveSessionStatus(client, sessionID, directory)
154
172
  if (live?.type) {
155
- // Some OpenCode 1.15.x TUI builds can leave session.status at busy after a
156
- // plugin-injected turn until the next user command touches the session.
157
- // When the only reason we still think the session is busy is our own stale
158
- // active-run guard, recover instead of waiting for another manual command.
173
+ // Some OpenCode 1.15.x/1.18.x TUI builds can leave session.status at busy
174
+ // after a plugin command acknowledgement. If no Loop run exists yet, a
175
+ // completed assistant tail plus an already-stale busy observation is a
176
+ // stronger signal than the unchanged live status. A genuinely running turn
177
+ // has an unfinished assistant tail (or latest user message) and is never
178
+ // force-recovered by this path.
179
+ if (await recoverCompletedTailWithoutActiveRun(directory, client, sessionID, live.type, seenAt)) return "idle"
180
+
181
+ // When a Loop-owned turn exists, use its exact start boundary so an older
182
+ // completed assistant message can never finalize a newer active run.
159
183
  if ((live.type === "busy" || live.type === "retry") && options.recoverStaleActive !== false) {
160
184
  const active = activeRuns.get(sessionID)
161
185
  if (active) {