Skip to content

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

Merged
ktapa merged 6 commits into
mainfrom
fm/fm-afk-inject-wedge-rebased
Sep 2, 2026
Merged

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

Conversation

@ktapa

@ktapa ktapa commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Lands the away-mode fix into this fork so it takes effect locally without waiting on upstream.

Same commits as the upstream contribution at kunchenguid#3476, which remains open and unaffected.

What it fixes:

  • The busy guard falsely read an idle pane as mid-turn by matching Claude's thinking-indicator shape against settled scrollback text. Because an idle pane prints nothing new, the false match never cleared - two sessions delivered zero escalations for over two hours each. Now short-circuits on an affirmatively empty composer before that scan.
  • Away-mode entry now refuses when no alert channel can actually reach the captain, rather than discovering it at failure time. A bare 'command:' directive with no payload is rejected as unusable instead of being classified reliable.

Evidence: cause established by live measurement, not inference - the suspected daemon-hosting deadlock was disproven end to end, and the real false match was reproduced deliberately. Regression coverage added for both halves. Pipeline review, tests, docs and lint returned zero findings; the PR's automated reviewer passes at 5/5 after catching and confirming the fix of an earlier defect.

Not run: the parent project's own CI matrix, which is held at action_required pending maintainer approval of fork workflows. Reviewed for platform-sensitive constructs before landing; none introduced.

Merges cleanly into this fork's main - verified with a test merge, no conflicts.

Authorized by the captain 2026-09-01.

ktapa and others added 6 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
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
@ktapa
ktapa merged commit b70f235 into main Sep 2, 2026
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