@@ -211,6 +211,27 @@ is_bg_task_alive() {
211211 fi
212212 [[ -n " $output_file " ]] || output_file=" $tasks_dir /$task_id .output"
213213
214+ # Session-resume fallback: when the transcript has no extractable
215+ # launch path AND the session-derived path does not exist, the real
216+ # .output file likely lives under a DIFFERENT session id (the one that
217+ # launched the task). Search sibling session dirs under the same
218+ # project slug before giving up. This only EXPANDS where we look, so a
219+ # genuinely alive task is never wrongly pruned (its file exists and
220+ # lsof still sees the open fd); a file missing everywhere remains
221+ # genuinely unknown and fails open below as before.
222+ if [[ ! -e " $output_file " ]] && [[ -n " $tasks_dir " ]]; then
223+ local slug_dir _candidate
224+ slug_dir=$( dirname " $( dirname " $tasks_dir " ) " ) # /tmp/claude-<uid>/<slug>
225+ if [[ -d " $slug_dir " ]]; then
226+ for _candidate in " $slug_dir " /* /tasks/" $task_id " .output; do
227+ if [[ -f " $_candidate " ]]; then
228+ output_file=" $_candidate "
229+ break
230+ fi
231+ done
232+ fi
233+ fi
234+
214235 # Output file absent -> fail open (treat as still running).
215236 [[ -f " $output_file " ]] || return 0
216237 # lsof unavailable -> fail open.
@@ -241,7 +262,7 @@ prune_dead_bg_task_ids() {
241262# - Background shell: toolUseResult.backgroundTaskId non-empty
242263# -> id is toolUseResult.backgroundTaskId
243264#
244- # Completion events are recognised from two Claude Code transcript forms:
265+ # Completion events are recognised from three Claude Code transcript forms:
245266#
246267# 1. Structured SDK record
247268# (see SDKTaskNotificationMessage in docs/typescript.md):
@@ -253,6 +274,15 @@ prune_dead_bg_task_ids() {
253274# `<task-notification>` XML block with `<task-id>...</task-id>`;
254275# kept for transcripts produced by older Claude Code versions.
255276#
277+ # 3. TaskStop tool_result: when the model or user stops a background
278+ # task via the TaskStop tool, Claude Code records a top-level
279+ # `.toolUseResult` whose `.message` is "Successfully stopped task:
280+ # <id>" and (usually) whose `.task_id` is the stopped id. The id is
281+ # read from `.task_id` when present and otherwise parsed out of the
282+ # message text as a fallback. Many builds do NOT also emit a
283+ # `task_notification` system event for a TaskStop, so this source is
284+ # required or stopped tasks stay pending forever.
285+ #
256286# pending := launched \ completed
257287#
258288# Optional second argument `since_ts` (ISO-8601 string, e.g. the value
@@ -300,7 +330,7 @@ list_pending_background_task_ids() {
300330 | (.toolUseResult.agentId // .toolUseResult.backgroundTaskId)
301331 ' " $transcript_path " 2> /dev/null | sort -u) || return 1
302332
303- # Union of both completion formats. Either source alone is enough to
333+ # Union of all completion formats. Any one source alone is enough to
304334 # mark a launched id terminal.
305335 #
306336 # The `grep -oE || true` guard on the legacy branch keeps `set -o
@@ -321,6 +351,28 @@ list_pending_background_task_ids() {
321351 ' " $transcript_path " 2> /dev/null \
322352 | { grep -oE ' <task-id>[^<]+</task-id>' || true ; } \
323353 | sed -E ' s|</?task-id>||g'
354+ # TaskStop tool_result (source 3 above): the model/user stopped
355+ # a background task. Match the top-level .toolUseResult whose
356+ # .message is "Successfully stopped task: <id>" and emit its id.
357+ # Required because many builds do not also emit a task_notification
358+ # system event for a TaskStop. The id is read from .task_id when
359+ # present; otherwise it is parsed out of the message text as a
360+ # fallback, because some builds record the id only there. The
361+ # message is coerced with `tostring` before `contains()` because
362+ # unrelated toolUseResult records may carry a structured (object,
363+ # array, number) `.message`, and `contains()` errors on non-strings;
364+ # without the coercion one bad record would wipe the whole `completed`
365+ # set under pipefail. The `try ... catch empty` keeps a degenerate
366+ # message (prefix with no id, so the regex cannot match) from raising.
367+ jq -r '
368+ select(.toolUseResult != null)
369+ | ((.toolUseResult.message // "") | tostring) as $msg
370+ | select($msg | contains("Successfully stopped task:"))
371+ | (.toolUseResult.task_id
372+ // try ($msg
373+ | capture("Successfully stopped task: (?<i>[^ (]+)")
374+ | .i) catch empty)
375+ ' " $transcript_path " 2> /dev/null
324376 } | sort -u | sed ' /^$/d'
325377 ) || completed=" "
326378
0 commit comments