Uh oh!
There was an error while loading. Please reload this page.
Allow natural stop while background tasks are in flight - #91
Merged
Conversation
Add a background-task short-circuit to the RLCR stop hook so it exits 0 with a user-facing systemMessage whenever the Claude Code transcript shows at least one pending background dispatch (Agent run_in_background, or Bash run_in_background) that has not yet produced a completion queue-operation notification. The short-circuit precedes every other gate (phase detection, state parse, branch consistency, plan integrity, git cleanliness, summary, BitLesson, max iterations, Codex review) and touches no on-disk state; when the background work finishes, the next natural stop re-enters the normal review flow. Supporting helpers in loop-common.sh: - extract_transcript_path mirrors extract_session_id - list_pending_background_task_ids parses the transcript jsonl and diffs launched ids against queue-operation completion task-ids - has_pending_background_tasks + count_pending_background_tasks wrap that list with fail-closed semantics when the transcript path is missing or jq is unavailable The rlcr-stop-gate wrapper had two latent bugs newly exposed by forwarding transcript_path end-to-end: select(length>0) on an object field collapsed the entire JSON when session_id was empty, and a wrapped response with no decision field was treated as Unexpected instead of ALLOW. Both paths are fixed alongside this change. tests/test-stop-hook-bg-allow.sh covers AC-1..AC-9 end-to-end (11 cases including two missing-path variants and one wrapper smoke test) and is registered in run-all-tests.sh. Full suite: 1690 passed, 0 failed. Branch targets dev; version stays at 1.16.0 as requested.
Round 0 shipped the natural-stop short-circuit using a literal `-f` check on the transcript path, which silently failed for the "~/.claude/projects/<encoded-cwd>/<session>.jsonl" form Claude Code actually publishes. A real session landed on the fall-through path and still ran the full Codex review, defeating the feature. Add a small `expand_leading_tilde` helper in loop-common.sh (no eval, handles bare "~" and "~/..." only, leaves everything else verbatim) and apply it in both extract_transcript_path and list_pending_background_task_ids. The stop hook and the rlcr-stop-gate wrapper inherit the fix through the shared helpers without any call-site edits. Regression coverage in tests/test-stop-hook-bg-allow.sh: AC-10 hook input with transcript_path="~/...\" under $HOME still triggers the short-circuit (exit 0 + systemMessage). AC-10b direct list_pending_background_task_ids on the same tilde form returns the pending id, so a future regression in the helper cannot be masked by additional normalization in the hook itself. Under-$HOME fixture dir is cleaned alongside $TEST_DIR via an extended EXIT trap. Full suite: 1692 passed, 0 failed (+2 vs Round 0). Branch still targets dev; version stays at 1.16.0.
Two test-harness gaps flagged in the previous Codex review:
1. The tilde-path fixture was created with mktemp -d "$HOME/...",
which aborts on sandboxed or read-only-HOME environments and
forces callers to override HOME externally.
2. AC-9 with the literal "~/..." --transcript-path form was named
as a target for the round but never made it into automation;
the checked-in wrapper coverage still used an absolute path.
Fix both inside tests/test-stop-hook-bg-allow.sh only -- no
production code changes. The helper, hook, and wrapper already
handle the tilde form correctly; the missing piece was portable,
checked-in proof.
- Introduce FAKE_HOME="$TEST_DIR/fake-home" rooted inside the
existing test temp dir, so cleanup rides on the setup_test_dir
EXIT trap. The previous cleanup_all / HOME_FIXTURE_DIR
extension is removed.
- Extend run_stop_hook_with_input with an optional 3rd argument
that exports HOME inside the hook subshell when non-empty.
Absent, behavior is verbatim.
- Refactor AC-10 and AC-10b to point at
"$FAKE_HOME/session-data/ac10.jsonl", and pass the tilde-form
literal "~/session-data/ac10.jsonl" verbatim. The previous
dynamic derivation "~/${AC10_TRANSCRIPT#$HOME/}" is gone.
- Add AC-10c: exercises scripts/rlcr-stop-gate.sh with
--transcript-path "~/..." under HOME=$FAKE_HOME and asserts
exit 0 + "^ALLOW:" + "background task".
Validation:
- bash tests/test-stop-hook-bg-allow.sh -> 14 passed, 0 failed
(baseline 13; +1 for AC-10c).
- bash tests/run-all-tests.sh -> 1693 passed, 0 failed
(baseline 1692; +1).
- Portability: HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh
-> 14 passed, 0 failed.
Branch still targets dev; version stays at 1.16.0.Codex review P2 on the background-task short-circuit: allowing the natural stop at the early-exit site returned control to the user while state.md was still bound to the current session_id. If the user closed Claude before the background-task completion notification arrived, find_active_loop in any later session would reject the loop (stored session_id did not match), so the loop was stranded and recovery required manual cancellation. Resolve without touching state.md by introducing a narrow cross-session adoption signal: - Early-exit block now also runs `: > "$LOOP_DIR/bg-pending.marker"` before emitting the systemMessage JSON. Failure is tolerated so the short-circuit never blocks on a flaky filesystem. - find_active_loop's session-filter branch gains a second acceptance step: on a stored-vs-filter session_id mismatch, if the dir has `bg-pending.marker` AND an active state file, it is still accepted. Terminal loops with a stale marker are ignored. Three new regression cases in tests/test-stop-hook-bg-allow.sh: AC-11 session_id mismatch + marker + pending bg -> short-circuit fires; state.md stays byte-identical. AC-11b session_id mismatch + no marker -> hook takes the existing "no active loop" exit-0 path; confirms the marker is the only cross-session adoption signal. AC-11c same-session short-circuit really writes the marker, so AC-11 is grounded in real hook behavior rather than a synthetic setup. Validation: - bash tests/test-stop-hook-bg-allow.sh -> 17 passed, 0 failed - bash tests/run-all-tests.sh -> 1696 passed, 0 failed - HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh -> 17 passed, 0 failed No systemMessage wording change. No state.md mutation. Branch still targets dev; version stays at 1.16.0.
Codex review flagged two regressions from the previous round's bg-pending.marker change: [P1] find_active_loop could return the newest loop dir that had a marker before the scan reached an older dir whose stored session_id actually matched the caller. With multiple active RLCR loops in one repo that breaks session isolation and lets a stop hook attach to the wrong conversation. [P2] The short-circuit wrote bg-pending.marker but nothing cleared it when the next stop saw no pending background task. A later stop from a different session_id would keep being adopted through the stale marker long after the bg had resolved. Both fixes are in the files already touched by this feature: * find_active_loop's session-filter branch now makes exact stored-vs-filter match win over marker fallback. Marker candidates are recorded while the scan walks newest-to-oldest and only returned after the whole listing fails to yield an exact match. Zombie-loop protection still wins for the caller's own session. This preserves Round 3 orphan recovery while restoring isolation across concurrent sessions. * After has_pending_background_tasks returns false, the stop hook now handles the marker: if HOOK_SESSION_ID differs from the stored session_id in the active state file, it rewrites that line with portable sed -i.bak; then the marker is removed unconditionally. Rewrite failure is logged but non-fatal. Regressions in tests/test-stop-hook-bg-allow.sh: AC-12 find_active_loop returns the older exact-match dir over a newer foreign-session dir that has a marker. AC-12b find_active_loop does not touch a foreign session's marker. AC-13 Same-session resume with a stale marker + empty bg transcript clears the marker. AC-13b Same-session resume leaves state.md session_id unchanged. AC-14 Cross-session resume clears the marker. AC-14b Cross-session resume rewrites state.md session_id to the caller's session. Validation: - bash tests/test-stop-hook-bg-allow.sh -> 23 passed, 0 failed - bash tests/run-all-tests.sh -> 1702 passed, 0 failed - HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh -> 23 passed, 0 failed systemMessage wording unchanged. Version stays at 1.16.0.
Two Codex review findings on the previous round:
[P1] The completion parser in list_pending_background_task_ids only
understood the legacy queue-operation XML blocks. Current
Claude Code transcripts emit background-task completions as
SDKTaskNotificationMessage records:
type: "system", subtype: "task_notification", task_id: ...
Without SDK-format recognition, launched ids stayed pending
forever and the short-circuit would fire on every stop.
[P2] The non-short-circuit path cleared bg-pending.marker
unconditionally whenever has_pending_background_tasks
returned false. That helper is fail-closed, so it also
returned false when transcript_path was missing or
unreadable (e.g. rlcr-stop-gate.sh without
--transcript-path). In that case the cleanup still deleted
the marker and rewrote the stored session_id, breaking
cross-session recovery exactly where transcript inspection
is unavailable.
Fixes, confined to the files already touched by this feature:
* loop-common.sh: union the completion set from both SDK
(type:system subtype:task_notification -> .task_id) and legacy
(queue-operation XML) sources. Wrap the legacy branch's grep
in `{ grep -oE ... || true; }` so its "no match -> exit 1"
cannot combine with set -o pipefail to invalidate the SDK
side of the union through the `|| completed=""` fallback.
* loop-codex-stop-hook.sh: gate the non-short-circuit marker
cleanup on `HOOK_TRANSCRIPT_PATH` being a readable regular
file. When transcript inspection is unavailable, leave the
marker AND the stored session_id untouched so cross-session
recovery stays reachable.
Regressions added in tests/test-stop-hook-bg-allow.sh:
AC-15 helper treats an SDK task_notification as terminal.
AC-16 helper unions SDK + legacy completion formats.
AC-17 missing transcript_path key -> marker preserved.
AC-17b same scenario -> stored session_id preserved.
AC-17c transcript_path pointing at a non-existent file -> same
guarantees.
A small emit_sdk_task_notification helper was added to keep AC-15
and AC-16 declarative.
Validation:
- bash tests/test-stop-hook-bg-allow.sh -> 28 passed, 0 failed
- bash tests/run-all-tests.sh -> 1707 passed, 0 failed
- HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh
-> 28 passed, 0 failed
systemMessage wording unchanged. Version stays at 1.16.0.Three blocking findings from the previous review: [P1] The Round 5 non-short-circuit cleanup hijacked foreign parked loops: session B could rewrite a parked loop's stored session_id to B and delete the marker using B's own transcript, even though A's background task was still running. A was locked out of its own loop. [P2] Every caller of find_active_loop inherited the marker fallback, including loop-read-validator, loop-write- validator, loop-bash-validator, and loop-plan-file- validator. An unrelated session's validators started enforcing foreign parked-loop gates (notably the methodology-analysis phase) on ordinary writes and bash commands, breaking existing session isolation guarantees. [P2] The Round 4 sed -i.bak session_id rewrite injected HOOK_SESSION_ID into the replacement text unescaped. Session IDs containing `&` are valid per this repo's test-session-id.sh; adopting such an id corrupted state.md. Fixes, confined to files already touched by this feature: * loop-common.sh: find_active_loop gains a third positional parameter allow_bg_marker_fallback (default false). Both the inner marker_candidate record and the post-loop fallback return are gated on it. Only the stop hook opts in; validators keep the pre-Round-3 strict isolation. * loop-codex-stop-hook.sh: - Call find_active_loop with `true` to opt in. - Add a new "Cross-Session Parked-Loop Guard" block before the short-circuit: when bg-pending.marker is present AND the stored session_id differs from HOOK_SESSION_ID, emit a dedicated "parked by another Claude session" systemMessage and exit 0 without touching marker, state.md, or session_id. B's hook can never advance A's parked loop on B's transcript. - Simplify the non-short-circuit cleanup: the guard ensures only same-session cases reach here, so the cleanup is now just `rm -f bg-pending.marker` (still gated on transcript readability). The sed session_id rewrite is removed entirely, which also removes the unescaped-metacharacter issue. Regressions in tests/test-stop-hook-bg-allow.sh: AC-11 (rewritten) cross-session + marker -> "parked" systemMessage + marker preserved + state.md byte-identical. AC-14 (rewritten) anti-hijack: cross-session stop preserves bg-pending.marker. AC-14b (rewritten) cross-session stop leaves stored session_id intact. AC-18 NEW. find_active_loop default (no opt-in) ignores a foreign marker dir -> validators stay isolated. AC-18b NEW. find_active_loop with opt-in does return the marker dir (confirms the flag is wired). All other existing regressions continue to pass. Validation: - bash tests/test-stop-hook-bg-allow.sh -> 30 passed, 0 failed - bash tests/run-all-tests.sh -> 1709 passed, 0 failed - HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh -> 30 passed, 0 failed Original short-circuit systemMessage wording unchanged. Version stays at 1.16.0.
Two blocking findings from the previous review: [P1] The cross-session parked-loop guard required both a non-empty stored session_id AND a non-empty HOOK_SESSION_ID. Callers that reach the hook without a session_id field -- including scripts/rlcr-stop-gate.sh invoked without --session-id -- bypassed the guard, and a foreign parked loop fell through into the normal path. With any readable transcript, the later cleanup deleted bg-pending.marker and the hook started reviewing a loop it did not own. [P2] Non-short-circuit cleanup only verified that the transcript file existed. list_pending_background_task_ids is fail-closed on malformed or truncated transcripts, yet the cleanup still deleted the marker in that case. The parked-state signal was lost even though background completion was never verified. Fixes, limited to loop-codex-stop-hook.sh: * Cross-session guard: drop the `-n "$HOOK_SESSION_ID"` clause. A non-empty stored session_id that differs from the (possibly empty) hook session_id now triggers the "parked by another Claude session" exit path. Backward-compat semantics are preserved: an empty stored session_id still matches any caller, consistent with find_active_loop's existing rule. * Non-short-circuit cleanup: call list_pending_background_task_ids inline and check its exit code along with its output. The marker is removed only when the helper returned exit 0 AND produced an empty id list. Every fail-closed path (missing file, empty path, jq parse failure, truncation) now leaves the marker intact. No changes to hooks/lib/loop-common.sh; the helper already has the exit-code semantics we rely on. Regressions in tests/test-stop-hook-bg-allow.sh: AC-19 Hook input with NO session_id key + state.md session_id populated + bg-pending.marker -> exit 0 with "parked" systemMessage, marker preserved, state.md byte-identical. AC-20 Hook input pointing transcript_path at a deliberately malformed JSONL file + bg-pending.marker -> marker preserved. Validation: - bash tests/test-stop-hook-bg-allow.sh -> 32 passed, 0 failed - bash tests/run-all-tests.sh -> 1711 passed, 0 failed - HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh -> 32 passed, 0 failed systemMessage wording unchanged. Version stays at 1.16.0.
Two blocking findings from the previous review: [P1] Round 7 dropped the `-n "$HOOK_SESSION_ID"` check from the cross-session guard to close its own P1. That inverted the failure mode: any wrapper call (e.g. rlcr-stop-gate.sh without --session-id) matched the guard forever once the marker was written, and the RLCR loop never resumed through the wrapper path. [P2] list_pending_background_task_ids scanned the entire session-wide Claude transcript. A long-running background Agent/Bash that started earlier in the same session -- before the current RLCR loop was created -- counted as "pending" for this loop. The short-circuit fired forever for a loop that had no in-scope pending work of its own. Fixes: * loop-codex-stop-hook.sh: - Add an "Ambiguous-Caller Marker Guard" before the cross- session guard. When bg-pending.marker is present AND HOOK_SESSION_ID is empty, exit 0 silently (no systemMessage, no on-disk mutation). The real Claude stop hook always has session_id populated and remains the only authoritative driver for parking and cleanup. - Restore `[[ -n "$HOOK_SESSION_ID" ]]` inside the cross- session guard. That branch now fires only when both session ids are non-empty and different. - Compute LOOP_START_TS via derive_loop_start_iso_ts once and pass it through every pending-tasks helper call. * loop-common.sh: - New derive_loop_start_iso_ts helper: parses the loop dir basename YYYY-MM-DD_HH-MM-SS and emits YYYY-MM-DDTHH:MM:SS.000Z for lexical comparison against transcript timestamps. - list_pending_background_task_ids gains an optional since_ts argument. Launch events are filtered by ($since_ts == "" or (.timestamp // "") == "" or (.timestamp // "") >= $since_ts). Empty since_ts preserves old scan-everything behavior; events without .timestamp remain included for fixture / older record compatibility. - has_pending_background_tasks and count_pending_background_tasks pass since_ts through unchanged. Regressions in tests/test-stop-hook-bg-allow.sh: AC-10c refixtured to avoid the AC-10 marker leaking into the wrapper ambiguous-caller branch. AC-19 rewritten: empty HOOK_SESSION_ID + marker -> silent ALLOW, marker and state preserved (inverts Round 7's "parked" expectation). AC-21 helper filters a pre-loop launch and keeps an in-loop launch. AC-21b derive_loop_start_iso_ts produces the expected ISO-8601 form. AC-21c end-to-end: pre-loop launch in transcript does not trigger the short-circuit; Codex runs. AC-22 wrapper without --session-id + no prior marker + pending bg -> writes marker, surfaces systemMessage. AC-22b wrapper without --session-id + prior marker -> silent ALLOW, marker and state preserved. Validation: - bash tests/test-stop-hook-bg-allow.sh -> 37 passed, 0 failed - bash tests/run-all-tests.sh -> 1716 passed, 0 failed - HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh -> 37 passed, 0 failed systemMessage wording unchanged on existing paths. Version stays at 1.16.0.
setup-rlcr-loop.sh creates loop dirs named YYYY-MM-DD_HH-MM-SS with `date +%Y-%m-%d_%H-%M-%S` (no -u), so the basename is local wall clock. The previous derive_loop_start_iso_ts pasted `.000Z` on top of that basename, treating the local wall clock as if it were already UTC. Claude transcript events carry real UTC timestamps, so on any non-UTC machine the filter boundary was shifted by the local offset: west-of-UTC users inherited pre-loop background launches and stayed parked forever, east-of-UTC users missed in-loop background work and ran Codex review too early. Fix in hooks/lib/loop-common.sh: * derive_loop_start_iso_ts now does a two-step local -> epoch -> UTC conversion. Local wall clock is parsed into epoch seconds with `date -d` (GNU) or `date -j -f` (BSD/macOS), then the epoch is formatted in UTC as YYYY-MM-DDTHH:MM:SS.000Z with `date -u -d "@<epoch>"` (GNU) or `date -u -r <epoch>` (BSD/macOS). Any failure yields an empty string, which disables the filter in callers -- same backward-compat behaviour as before. Regressions in tests/test-stop-hook-bg-allow.sh: AC-21b pinned to `export TZ=UTC` inside its subshell so the expected 2026-03-01T00:00:00.000Z is TZ-deterministic. AC-21d NEW. TZ=Asia/Tokyo + basename 2026-03-01_09-00-00 -> 2026-03-01T00:00:00.000Z (9am JST = 0am UTC). AC-21e NEW. TZ=America/Los_Angeles + basename 2026-03-01_00-00-00 -> 2026-03-01T08:00:00.000Z (0am PST = 8am UTC; March 1 is before DST starts on March 8, 2026). Validation: - bash tests/test-stop-hook-bg-allow.sh -> 39 passed, 0 failed - bash tests/run-all-tests.sh -> 1718 passed, 0 failed - TZ=America/Los_Angeles bash tests/test-stop-hook-bg-allow.sh -> 39 passed, 0 failed - TZ=Asia/Tokyo bash tests/test-stop-hook-bg-allow.sh -> 39 passed, 0 failed - HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh -> 39 passed, 0 failed systemMessage wording unchanged. Version stays at 1.16.0.
tests/test-stop-hook-bg-allow.sh invokes rlcr-stop-gate.sh at four
spots (AC-9, AC-10c, AC-22, AC-22b). The wrapper resolves its
project root as `${CLAUDE_PROJECT_DIR:-$(pwd)}`, giving the env
var precedence over `cd`. When the outer runner exports
CLAUDE_PROJECT_DIR (the normal case in hosted environments),
those four tests were inspecting the outer repo instead of their
per-test fixtures, falling through with "ALLOW: stop gate
passed." and causing the suite to go red inside
tests/run-all-tests.sh.
Reproduced directly:
CLAUDE_PROJECT_DIR=/tmp/outer-unrelated \
bash tests/test-stop-hook-bg-allow.sh
...
FAIL: AC-10c
FAIL: AC-22
Fix: pass `--project-root "$FIXTURE_REPO"` at every wrapper call
site. The wrapper priority order is explicit flag >
CLAUDE_PROJECT_DIR env > cwd, so the gate now pins deterministically
to each fixture regardless of inherited environment.
No product-code change.
Validation:
- bash tests/test-stop-hook-bg-allow.sh -> 39 passed, 0 failed
- bash tests/run-all-tests.sh -> 1718 passed, 0 failed
- CLAUDE_PROJECT_DIR=/tmp/outer-unrelated bash
tests/test-stop-hook-bg-allow.sh -> 39 passed, 0 failed
- HOME=/nonexistent/readonly bash tests/test-stop-hook-bg-allow.sh
-> 39 passed, 0 failed
- TZ=America/Los_Angeles bash tests/test-stop-hook-bg-allow.sh
-> 39 passed, 0 failed
- TZ=Asia/Tokyo bash tests/test-stop-hook-bg-allow.sh
-> 39 passed, 0 failed
systemMessage wording unchanged. Version stays at 1.16.0.hooks/loop-codex-stop-hook.sh previously called has_pending_background_tasks AND count_pending_background_tasks back-to-back on the short-circuit path. Both wrap list_pending_background_task_ids, so every pending-bg stop was running jq over the transcript twice. Collapse into a single list_pending_background_task_ids call: capture the id list once, use its non-emptiness for the short-circuit decision, and derive the count with `wc -l`. The public helpers has_pending_background_tasks and count_pending_background_tasks stay for other callers. No behaviour change. systemMessage wording and exit codes identical. Validation (all six suites green): - bash tests/run-all-tests.sh -> 1718 passed, 0 failed - bash tests/test-stop-hook-bg-allow.sh -> 39 passed, 0 failed - HOME=/nonexistent/readonly ... -> 39 passed, 0 failed - TZ=America/Los_Angeles ... -> 39 passed, 0 failed - TZ=Asia/Tokyo ... -> 39 passed, 0 failed - CLAUDE_PROJECT_DIR=/tmp/outer-unrelated ... -> 39 passed, 0 failed Version stays at 1.16.0.
…survival Move the six background-task helpers (expand_leading_tilde, extract_transcript_path, derive_loop_start_iso_ts, list/has/count_pending_background_task[_ids]) and the four hook guard blocks (ambiguous caller, cross-session parked, pending-bg short-circuit, stale-marker cleanup) out of hooks/lib/loop-common.sh and hooks/loop-codex-stop-hook.sh into a new hooks/lib/loop-bg-tasks.sh. The stop hook now delegates to a single handle_bg_task_short_circuit entry point; loop-common.sh sources the new lib so every existing consumer continues to see the helpers transparently. Add a regression test to tests/test-stop-gate.sh that asserts transcript_path is still forwarded to the hook when session_id is empty, freezing the jq object-collapse fix that replaced plain select(length > 0) field values with explicit if/then/else nulls.
SihaoLiu
commented
Apr 16, 2026
ContributorAuthor
@codex review this PR |
Codex Review: Didn't find any major issues. Keep it up! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Uh oh!
There was an error while loading. Please reload this page.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Agent run_in_background=true/Bash run_in_background=true), and surfaces a user-visiblesystemMessageexplaining the pause. State on disk is left intact so the next stop after completion re-enters the normal review flow.bg-pending.marker: a second Claude session in the same repo cannot hijack or mutate a loop parked by another session. Ambiguous callers (nosession_idforwarded) exit silently; foreign sessions see a dedicated "parked by another session" notice; the owning session cleans up the marker only after its own transcript authoritatively shows no pending bg events.find_active_loopwith an opt-inallow_bg_marker_fallbackargument so validators keep strict session isolation while only the stop hook participates in marker-based adoption.type:"system", subtype:"task_notification"SDK records and the legacyqueue-operationXML<task-notification>block.~intranscript_pathbefore the file check and scopes the transcript scan with a loop-start boundary derived from the loop dir basename (converted from local wall clock to real UTC), so pre-loop session-wide background work cannot pin a fresh loop.jqobject-collapse bug inscripts/rlcr-stop-gate.shwhere a plainselect(length > 0)field filter wiped the entire hook-input object whenever any forwarded field (e.g.session_id) was empty; replaces with explicitif/then/elsenulls.hooks/lib/loop-bg-tasks.sh(6 helpers + a singlehandle_bg_task_short_circuitentry point) so the stop hook drops ~100 lines and the helpers live in a cohesive module;loop-common.shsources the new file so every existing consumer picks up the helpers without code changes.tests/test-stop-hook-bg-allow.sh, 39 tests covering AC-1 to AC-22b) and a stop-gate regression test that freezes thetranscript_pathsurvival fix.Test plan
bash tests/test-stop-hook-bg-allow.sh-> 39 passedbash tests/test-stop-gate.sh-> 12 passed (2 new regression tests)bash tests/run-all-tests.sh-> 1720 passed, 0 failed