fix(daemon): stop away-mode busy-guard false positives and refuse silent wedge-alarm entry - #3476
Open
ktapa wants to merge 6 commits into
Open
fix(daemon): stop away-mode busy-guard false positives and refuse silent wedge-alarm entry#3476ktapa wants to merge 6 commits into
ktapa wants to merge 6 commits into
Conversation
Two real away-mode sessions delivered zero escalations for 2+ hours each
because pane_is_busy() misread the captain's idle pane as busy on every
15s cycle. Live measurement in an isolated Herdr lab disproved the
suspected cause (the daemon's own tracked background job wedging its
hosting pane): a real fm-supervise-daemon.sh, launched exactly as
production does and targeting its own hosting pane, delivered a seeded
escalation successfully with a background shell alive the whole time.
The actual cause: pane_is_busy's rendered-tail fallback has no positional
anchor. Claude's busy-shape signature ("…" + a parenthesized elapsed
duration) is meant to catch a live spinner footer, but it matches equally
well against ordinary SETTLED reply text sitting in scrollback (e.g. "...
holding steady… (2h into the soak test)"). With no new output landing
while escalations keep failing, nothing scrolls the line out of view, so
the false match persists for the rest of the session - reproduced live
and confirmed as the trigger.
Fix: pane_is_busy now treats an affirmatively empty composer as positive
proof the turn has ended and short-circuits the rendered scan before it
runs. A genuinely busy pane never reads an empty composer, so this can't
mask a real busy turn. Both the terminal-backed and native-hosted launch
paths share this same check, so neither needed separate handling.
Separately, the wedge alarm's own escape hatch was inert on this Linux
box: no osascript, no org.freedesktop.Notifications D-Bus service, and
even herdr's own notification channel reported {"reason":"disabled"}
rather than posting anything. Extracted the channel-resolution logic into
bin/fm-wedge-alarm-lib.sh (shared by the daemon and the launcher) and
added an entry-time refusal: fm-afk-launch.sh now refuses a fresh away-mode
entry when no channel can plausibly reach the captain, unless the captain
has explicitly configured one (including an explicit "off" acknowledging
marker-only).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019924TgGwpoBc2rKMbFgi67
…es, add preflight tests
…dge-alarm-lib.sh in toolbelt
…ge-alarm preflight test"}
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Reviews (3): Last reviewed commit: "no-mistakes(ci): Fixed the Greptile-flag..." | Re-trigger Greptile |
wedge_alarm_reliable_channel_configured matched command:* as a reliable directive even with an empty payload, so a config/wedge-alarm containing only "command:" passed away-mode entry as covered while wedge_alarm_via_command's own non-empty-cmd guard silently no-ops it at alarm time - the exact reassurance-that-doesn't-hold gap this predicate exists to catch. Require at least one character after the colon (command:?*) so an empty payload falls into the same immediate rejection as any other unrecognized directive. osascript and herdr are left unchanged: a missing binary is a per-host environmental fact, the same class of runtime risk this predicate already leaves to wedge_alarm_notify's best-effort logged dispatch for a broken command:<cmd> payload, not a config-only defect like an empty command:. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LnXRSBGuVx1oZEnPdKxm8F
…-lib.sh: `wedge_alarm_reliable_channel_configured` used the case pattern `command:?*`, which requires only one character after the colon — so `command: ` (or any all-whitespace payload) matched and was accepted as a "reliable" channel at away-mode entry. At runtime, `wedge_alarm_via_command`'s `[ -n "$cmd" ]` guard treats whitespace as non-empty too, so `sh -c ' '` would run, do nothing, and exit 0 — silently no-op'ing the alarm with no signal to the captain, reproducing the exact fail-silent gap this PR's user intent targets. Fix: changed the pattern to `command:*[![:space:]]*`, requiring at least one non-whitespace character after the colon, so both a bare `command:` and a whitespace-only `command: ` are rejected at entry (falling into the existing "no active wedge-alarm channel is configured" refusal), while `command:<real cmd>` still passes. Updated the adjacent comment to document the whitespace case. Added a regression test (`unit_wedge_alarm_preflight_refuses_whitespace_command_directive` in tests/fm-afk-launch.test.sh) that pins FM_WEDGE_ALARM_CHANNEL='command: ' being rejected; verified it fails on the pre-fix code and passes after the fix. Confirmed the CI/CD runs for this PR are conclusion=action_required (workflow-approval gating, not a real test failure), so the only concrete code defect to address was the Greptile finding. Full fm-afk-launch.test.sh and fm-daemon.test.sh suites pass (all wedge-alarm preflight + dispatch tests green), and shellcheck is clean on both changed files
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 free
to 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.
Intent
Away-mode supervision on the claude harness with native-hosted injection was delivering zero escalations: the busy guard falsely reported the supervisor pane busy continuously (not transiently) even while idle, for over 2 hours across two separate sessions, leaving a task parked awaiting a decision with firstmate unaware. Establish by direct measurement (not by reasoning from source alone) whether the daemon's own tracked background-job hosting under start-native is what trips the pane busy-guard on claude, or whether the true cause lies elsewhere (a claude busy-signature matching an idle rendered pane, or a mis-resolved supervisor target), and state the finding plainly either way. Fix the root cause so an escalation raised while away mode is active reliably reaches its target pane on claude with herdr, demonstrated end to end in an isolated Herdr lab session (not just a passing unit test). Add regression coverage for the busy-guard false positive. Separately, fix the wedge alarm's fail-silent failure mode: on Linux there is no OS-level alert channel, so a busy-guard false positive can wedge silently with only an inert durable marker as the signal; either provide a working default alert channel on Linux or make the absence of one loud at away-mode entry (before the captain walks away) rather than only at failure time - prefer entry-time refusal when no reliable Linux channel is available. Check whether the wedge-alarm entry preflight (wedge_alarm_reliable_channel_configured) can itself be tricked into reporting a channel reliable when it cannot actually fire, and fix any such gap: specifically, a bare 'command:' directive with no payload was matching the command:* pattern and passing as reliable while the runtime command dispatch's own non-empty-payload guard would silently no-op it - fixed so an empty payload is rejected at entry like any other malformed directive, with regression coverage pinning both the bare-command rejection and the still-accepted command: case. Also determine whether the terminal-backed away-mode launch path (not just the native-hosted one) is affected by the busy-guard defect, and if only native hosting is broken, fix the afk skill's harness routing accordingly as part of this task.
What Changed
bin/fm-supervise-daemon.sh'spane_is_busynow short-circuits to "not busy" wheneverfm_backend_composer_statereads an affirmativelyemptycomposer, before falling back to the unanchored rendered-tail scan — fixing a false-positive where settled scrollback text matching Claude's busy-shape signature (e.g. "…(2h into the soak test)") kept the pane marked busy indefinitely on both the terminal-backed (start) and native-hosted (start-native) launch paths, since both share the samepane_is_busycall.wedge_alarm_configured_channels,wedge_alarm_platform_default) out offm-supervise-daemon.shinto a new sharedbin/fm-wedge-alarm-lib.sh, and addedwedge_alarm_reliable_channel_configured, which rejects unrecognized/malformed directives and a payload-less barecommand:directive that would otherwise pass as "reliable" while the runtime dispatch's non-empty-payload guard silently no-ops it.bin/fm-afk-launch.sh'sstartandstart-nativenow call a newfm_afk_launch_wedge_alarm_preflightthat refuses a fresh away-mode entry when no channel resolves to something that can actually fire (an explicitoffstill passes as a deliberate acknowledgment), so the missing-alert-channel gap is surfaced loudly before the captain walks away instead of only at failure time.tests/fm-daemon.test.sh) and for the wedge-alarm entry preflight, including the bare-command:rejection and acceptedcommand:<cmd>cases (tests/fm-afk-launch.test.sh), plus supporting docs updates (docs/wedge-alarm.md,docs/verification/supervision.md,docs/scripts.md,.agents/skills/afk/SKILL.md).Risk Assessment
✅ Low: The branch cleanly implements the fix (composer-state short-circuit in pane_is_busy backed by pre-existing, well-tested composer classification, verified live per the docs), correctly resolves all three prior-round findings (vacuous rollback test now reaches the real failure path, the unvalidated-directive gap is closed via an explicit recognized-directive allowlist, and the bare
command:payload is now rejected withcommand:?*), and adds direct unit coverage for the new preflight function; source, tests, and docs are consistent with each other and with the required intent (busy-guard root cause investigated and fixed, wedge-alarm entry-time refusal added, both launch paths confirmed to share the fix).Testing
Targeted tests for this final commit's fix (bare
command:wedge-alarm directive rejection) and the related daemon busy-guard suite both pass; a before/after revert check proved the new unit test is a genuine, non-vacuous regression test that fails without the fix and passes with it. No findings.Evidence: wedge-alarm preflight regression test results
Evidence: reverting the fix reproduces the bug (pre-fix failure)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh tests/fm-afk-launch.test.sh tests/fm-daemon.test.shRegression proof: reverted bin/fm-wedge-alarm-lib.sh'scommand:?*back tocommand:*, reran tests/fm-afk-launch.test.sh, confirmed unit_wedge_alarm_preflight_refuses_empty_command_directive failed (rc=0, bare command: wrongly accepted) — then restored the fix and reran to confirm all passbin/fm-test-run.sh tests/fm-afk-launch.test.sh (post-restore full rerun, exit=0)git status --porcelain / git diff --stat (confirmed clean working tree, no transient artifacts left behind)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.