Skip to content

fix(daemon): stop away-mode busy-guard false positives and refuse silent wedge-alarm entry - #3476

Open
ktapa wants to merge 6 commits into
kunchenguid:mainfrom
ktapa:fm/fm-afk-inject-wedge-rebased
Open

fix(daemon): stop away-mode busy-guard false positives and refuse silent wedge-alarm entry#3476
ktapa wants to merge 6 commits into
kunchenguid:mainfrom
ktapa:fm/fm-afk-inject-wedge-rebased

Conversation

@ktapa

@ktapa ktapa commented Sep 1, 2026

Copy link
Copy Markdown

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's pane_is_busy now short-circuits to "not busy" whenever fm_backend_composer_state reads an affirmatively empty composer, 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 same pane_is_busy call.
  • Extracted wedge-alarm channel resolution (wedge_alarm_configured_channels, wedge_alarm_platform_default) out of fm-supervise-daemon.sh into a new shared bin/fm-wedge-alarm-lib.sh, and added wedge_alarm_reliable_channel_configured, which rejects unrecognized/malformed directives and a payload-less bare command: 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's start and start-native now call a new fm_afk_launch_wedge_alarm_preflight that refuses a fresh away-mode entry when no channel resolves to something that can actually fire (an explicit off still 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.
  • Added regression coverage for the settled-scrollback busy-guard false positive (tests/fm-daemon.test.sh) and for the wedge-alarm entry preflight, including the bare-command: rejection and accepted command:<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 with command:?*), 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
ok - wedge-alarm preflight: refuses a fresh entry with no configured channel and no platform default
ok - wedge-alarm preflight: an explicit 'off' acknowledgment passes entry
ok - wedge-alarm preflight: refuses entry on an unrecognized/malformed channel directive
ok - wedge-alarm preflight: refuses entry on a bare 'command:' directive with no payload
ok - wedge-alarm preflight: a 'command:<cmd>' directive with a real payload passes entry
Evidence: reverting the fix reproduces the bug (pre-fix failure)
not ok - wedge-alarm preflight: a bare 'command:' directive was accepted as reliable (rc=0):

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.sh
  • Regression proof: reverted bin/fm-wedge-alarm-lib.sh's command:?* back to command:*, 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 pass
  • bin/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.

ktapa and others added 4 commits September 1, 2026 18:14
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
@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "no-mistakes(ci): Fixed the Greptile-flag..." | Re-trigger Greptile

Comment thread bin/fm-wedge-alarm-lib.sh Outdated
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
@ktapa ktapa changed the title fix(bin): stop away-mode busy-guard false positives and add wedge-alarm entry refusal fix(daemon): stop away-mode busy-guard false positives and refuse silent wedge-alarm entry Sep 1, 2026
Comment thread bin/fm-wedge-alarm-lib.sh Outdated
…-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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant