Skip to content

Commit 517dcf4

Browse files
authored
Merge pull request #221 from SwordFaith/fix/bg-task-stop-recognition
Recognize stopped bg tasks and resolve cross-session output paths
2 parents 075db6b + 8aa0480 commit 517dcf4

2 files changed

Lines changed: 1021 additions & 681 deletions

File tree

hooks/lib/loop-bg-tasks.sh

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)