Skip to content

feat(sutando-app): add --emit-task to health-check invocation - #640

Merged
sonichi merged 3 commits into
mainfrom
feat/sutando-app-emit-task
May 8, 2026
Merged

sonichi merged 3 commits into
mainfrom
feat/sutando-app-emit-task

Conversation

@sonichi

@sonichi sonichi commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Sutando.app menu-bar Timer was firing health-check.py with --fix only.
  • Failures health-check can't auto-fix (e.g. stale-service detection where binary is older than source) went silent — no DM, no task file, no surface.
  • Adds --emit-task to args so failures write tasks/task-health-{ts}.txt (with access_tier: owner, 1h cooldown per failure-set hash, dedup history).

Why

3 stale services (voice-agent, web-client, sutando-app) suppressed in trio cooldown all day 2026-05-07 PT. The trio's scan_health_anomalies was firing but its surface_owner proposals kept hitting cooldown / ACT's "owner-aware" archival. The --emit-task path is independent of the trio queue — gives a redundant peer in the file-bridge layer.

Test plan

  • Wait for next 30min Timer fire OR open src/Sutando/Sutando to trigger a fresh check
  • Verify a tasks/task-health-{ts}.txt lands when health-check exits non-zero
  • Verify the dedup state file (state/health-emit-dedup.json or similar — check health-check.py source) prevents flooding within 1h

🤖 Generated with Claude Code

Sutando.app's menu-bar Timer was firing health-check.py with --fix only,
so failures it can't auto-fix went silent. The --emit-task flag (already
implemented in health-check.py with 1h cooldown per failure-set hash)
writes tasks/task-health-{ts}.txt for the bridge to pick up as owner-tier
tasks.

Gives the trio's `scan_health_anomalies` surface_owner path a redundant
peer in the file-bridge layer, so failures the trio suppresses by
cooldown (or ACT's LLM step archives by judgment) still reach the agent.

Per Chi 2026-05-07 PT after observing 3 stale services (voice-agent,
web-client, sutando-app) silently suppressed in trio cooldown all day.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cold-review: looks clean. Verified --emit-task exists at src/health-check.py:1021 with 1h cooldown + dedup via failure-set hash (line 863), so adding it to both python arg arrays is safe. Motivation checks out — health-check just now showed the same 3 stale services (voice-agent / web-client / sutando-app) suppressed in trio cooldown. Independent file-bridge surface is the right redundancy. Minimal 9-LOC, no behavior change for the success path. Mini-side review-clean — Mini's gh shares auth so can't formally approve, but no blockers spotted.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my earlier comment-review — codex (running independently on MacBook's #bot2bot review-request task) caught a real semantic bug I missed.

The bug: health-check.py:1028-1031 runs emit_task_for_failures(checks) BEFORE the fix loop at health-check.py:1076+. So --emit-task --fix together emits on the pre-fix failure set, not the post-fix residual. If --fix resolves a transient (e.g. relaunches a crashed launchd service that gets fixed on retry), the emit-task already wrote a task for the pre-fix state — even though the issue is gone.

Why it matters for this PR: the PR description's intent is "failures health-check can't auto-fix went silent" → surface only residuals. The current implementation surfaces ALL pre-fix failures. The 1h dedup hash + the fact that genuinely-stuck issues persist across runs means the practical noise is small, but the semantics don't match the intent.

Two ways to address:

(a) Move emit inside health-check.py post-fix: run checks → fix → re-run checks → emit on residual. Adds a re-check pass to the 30min Timer cycle but the emit decision matches intent. ~10-15 lines of py shuffling + the residual check.

(b) Ship as-is + acknowledge the always-on contract: rename arg/comment to --emit-pre-fix-failures and treat the dedup window as the de-facto noise control. Works fine in practice for genuinely-stuck issues (they keep failing across re-checks).

Both are defensible. (a) is the cleaner shape for the documented intent; (b) is the lower-LOC route that matches what the PR actually does today.

Either way, the trio-cooldown problem this PR addresses is real and the file-bridge redundancy is the right architecture. Just want the contract crisp.

…x set

Per Mini's PR #640 v1 review (verified): the original code emitted the
task BEFORE the --fix loop, so `--fix --emit-task` together meant
"emit pre-fix failures, then fix some". Created owner tasks for
failures resolved in the same invocation.

This commit moves emit_task_for_failures to AFTER the fix loop, and
when --fix ran, re-runs checks to capture the residual state. The 2s
delay between fix loop and re-check matches the existing per-service
sleep(1) restart budget.

Behavior matrix:
  --emit-task only         → emit on initial checks (= residual, no fix)
  --emit-task + --fix      → re-run checks after fix loop, emit on residual
  --notify-on-fail         → unchanged: notifies on INITIAL set (launchd
                              fallback wants the alert immediately)

Achieves PR #640's stated contract ("redundant peer for failures the trio
suppresses") rather than the noisy pre-fix variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mini cold-review on v2 (commit f9a88e4): the residual-vs-pre-fix fix is correct in shape, but introduces a regression.

Verified what's right:

  • Emit moved AFTER fix loop ✓
  • Re-run run_all_checks() only when do_fix and issues (otherwise initial checks IS residual) ✓
  • 2s delay reasonable — bridges' time.sleep(1) post-kill restart budget fits ✓
  • --notify-on-fail unchanged on initial set, with correct comment update explaining the intent split ✓

Real regression — launchd-fallback plist breaks:

The com.sutando.health-check-fallback plist runs:

python3 src/health-check.py --emit-task --notify-on-fail --quiet

Note: NO --fix. Walking the v2 control flow:

  1. Line 1025: checks = run_all_checks()
  2. Line 1037: do_notify → notify_for_failures(checks) ✓
  3. Line ~1041: as_json=False, skip
  4. Line ~1045: quiet=True, issues non-empty → print + if do_fix: pass else: sys.exit(1)
  5. sys.exit(1) here BYPASSES the new emit block at line ~1182

Result: launchd's hourly fallback no longer writes tasks/task-health-*.txt even on failures. The whole point of --emit-task for that path is now dead.

In v1 this worked because emit was BEFORE if quiet branch. v2 moved it past the early-exit.

Fix: the emit block needs to run before any sys.exit path that has issues. Two ways:

(a) Inline the emit in the quiet else branch (before sys.exit(1)) — but then it'd duplicate the post-fix path's emit. Need to gate.

(b) Cleaner: restructure to a single emit point that always runs before exit. Replace the 4 sys.exit calls with setting an exit-code variable, then handle emit + exit at the end:

exit_code = 1 if issues else 0

if quiet:
    if issues:
        for c in issues: print(f"{icon} ...")
    # don't exit yet
elif not as_json:
    # human-readable print loop
    ...

if do_fix and issues:
    # fix loop
    ...

if do_emit:
    if do_fix and issues:
        time.sleep(2); residual_checks = run_all_checks()
    else:
        residual_checks = checks
    emit_task_for_failures(residual_checks)

sys.exit(exit_code)

Tightens the contract: emit ALWAYS runs at the end if do_emit is set, regardless of quiet/as_json/exit-path. as_json early-return is the one tricky case — that path probably shouldn't emit anyway (programmatic callers like agent-api.py and dashboard.py use --json only, no --emit-task). So leave the as_json early-return as-is.

Other minor: the inline import time as _t at line ~1186 — time is already imported at module top (used at line ~977 for cooldown_ms = 3600 * 1000 math + likely elsewhere). The aliased import is unnecessary; just time.sleep(2) works.

Re: your two questions:

  • (a) 2s delay choice: reasonable, matches the per-service sleep(1) restart budget. No concern.
  • (b) do_fix and issues gate: correct. If do_fix is unset, initial = residual; if issues empty, no fix attempts, no need to re-check. Tight.

Want me to push a v3 with the single-exit-point restructure, or do you want to take it?

…quiet coverage

PR #640 v2 (commit f9a88e4) moved the emit-task block to end-of-main so
it'd surface the post-fix residual set. Mini caught a regression: the
launchd fallback's `--quiet --emit-task --notify-on-fail` invocation
bypassed the moved emit via the quiet-path `sys.exit(1)` before reaching
end-of-main. So no-fix callers stopped emitting tasks at all.

Fix: split by --fix state.
- `--emit-task` without `--fix` → emit immediately (initial check IS the
  residual; placed BEFORE --json / --quiet early exits)
- `--emit-task` with `--fix` → emit at end-of-main on residual after fix
  loop (re-runs checks)

Both paths reach emit. No-fix path no longer bypassed by --quiet exit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v3 LGTM. Verified the split-by-do_fix approach restores coverage for the launchd-fallback path I flagged in v2.

Walked the four caller cases:

Caller do_fix do_emit Path Result
launchd plist (--emit-task --notify-on-fail --quiet) F T pre-fix emit (line ~1043) → quiet print → sys.exit(1) ✓ Emits
Sutando.app Timer (--fix --emit-task) T T post-fix re-check emit (line ~1182) ✓ Emits residual
Direct --emit-task only F T pre-fix emit → human-readable ✓ Emits
--emit-task --fix direct T T post-fix emit ✓ Emits residual

The mutual-exclusion between if do_emit and not do_fix and if do_emit and do_fix and issues cleanly avoids double-emit. Comments correctly explain WHY the split exists (referencing the v2 regression I caught).

One residual theoretical hole: --json --emit-task --fix would skip both emit branches because as_json: return early-exits between them. No actual caller today (agent-api.py and dashboard.py use --json alone; launchd uses --quiet not --json). Not blocking, but worth a comment if you want to be defensive — or just leave it as documented behavior.

Minor unchanged: the inline import time as _t; _t.sleep(2) is still funky (time is already imported at module top) but it's harmless. Push or fold into a follow-up nit, your call.

Mini-side LGTM. Ready to merge whenever Chi greenlights.

@sonichi
sonichi merged commit 0666758 into main May 8, 2026
1 check passed
@sonichi
sonichi deleted the feat/sutando-app-emit-task branch May 8, 2026 05:59
sonichi added a commit that referenced this pull request May 14, 2026
…/archive (#653)

* feat(sutando-app): add --emit-task flag to runHealthCheck

Sutando.app's menu-bar Timer was firing health-check.py with --fix only,
so failures it can't auto-fix went silent. The --emit-task flag (already
implemented in health-check.py with 1h cooldown per failure-set hash)
writes tasks/task-health-{ts}.txt for the bridge to pick up as owner-tier
tasks.

Gives the trio's `scan_health_anomalies` surface_owner path a redundant
peer in the file-bridge layer, so failures the trio suppresses by
cooldown (or ACT's LLM step archives by judgment) still reach the agent.

Per Chi 2026-05-07 PT after observing 3 stale services (voice-agent,
web-client, sutando-app) silently suppressed in trio cooldown all day.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(health-check): --emit-task surfaces residual failures, not pre-fix set

Per Mini's PR #640 v1 review (verified): the original code emitted the
task BEFORE the --fix loop, so `--fix --emit-task` together meant
"emit pre-fix failures, then fix some". Created owner tasks for
failures resolved in the same invocation.

This commit moves emit_task_for_failures to AFTER the fix loop, and
when --fix ran, re-runs checks to capture the residual state. The 2s
delay between fix loop and re-check matches the existing per-service
sleep(1) restart budget.

Behavior matrix:
  --emit-task only         → emit on initial checks (= residual, no fix)
  --emit-task + --fix      → re-run checks after fix loop, emit on residual
  --notify-on-fail         → unchanged: notifies on INITIAL set (launchd
                              fallback wants the alert immediately)

Achieves PR #640's stated contract ("redundant peer for failures the trio
suppresses") rather than the noisy pre-fix variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(health-check): split emit-task logic by --fix state to restore --quiet coverage

PR #640 v2 (commit f9a88e4) moved the emit-task block to end-of-main so
it'd surface the post-fix residual set. Mini caught a regression: the
launchd fallback's `--quiet --emit-task --notify-on-fail` invocation
bypassed the moved emit via the quiet-path `sys.exit(1)` before reaching
end-of-main. So no-fix callers stopped emitting tasks at all.

Fix: split by --fix state.
- `--emit-task` without `--fix` → emit immediately (initial check IS the
  residual; placed BEFORE --json / --quiet early exits)
- `--emit-task` with `--fix` → emit at end-of-main on residual after fix
  loop (re-runs checks)

Both paths reach emit. No-fix path no longer bypassed by --quiet exit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(discord-bridge): notify agent-api on dm-fallback delivery + archive task/result files

Two related fixes for the dm-fallback path (`poll_dm_fallback`):

1. POST to localhost:7843/task-done after dm-result.py succeeds, mirroring
   voice-agent's task-bridge.ts:533 path. Without this, web UI has a ~5s
   lag flipping the task status to "done" (waiting for agent-api's next
   /tasks/active poll). Best-effort; silent on failure.

2. Archive task + result files in all three dm-fallback exit paths
   (drop-empty / drop-stale / sent-via-dm). Previously, only the result
   file was unlinked, leaving orphan task files in tasks/ that never
   got archived. Mirrors the parity of the main reply path. This
   addresses the systemic missed-replies pattern surfaced by
   today's user-behavior probe (187 historical owner-tier orphans
   in 7d archive without matching results).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…i#640)

* feat(sutando-app): add --emit-task flag to runHealthCheck

Sutando.app's menu-bar Timer was firing health-check.py with --fix only,
so failures it can't auto-fix went silent. The --emit-task flag (already
implemented in health-check.py with 1h cooldown per failure-set hash)
writes tasks/task-health-{ts}.txt for the bridge to pick up as owner-tier
tasks.

Gives the trio's `scan_health_anomalies` surface_owner path a redundant
peer in the file-bridge layer, so failures the trio suppresses by
cooldown (or ACT's LLM step archives by judgment) still reach the agent.

Per Chi 2026-05-07 PT after observing 3 stale services (voice-agent,
web-client, sutando-app) silently suppressed in trio cooldown all day.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(health-check): --emit-task surfaces residual failures, not pre-fix set

Per Mini's PR sonichi#640 v1 review (verified): the original code emitted the
task BEFORE the --fix loop, so `--fix --emit-task` together meant
"emit pre-fix failures, then fix some". Created owner tasks for
failures resolved in the same invocation.

This commit moves emit_task_for_failures to AFTER the fix loop, and
when --fix ran, re-runs checks to capture the residual state. The 2s
delay between fix loop and re-check matches the existing per-service
sleep(1) restart budget.

Behavior matrix:
  --emit-task only         → emit on initial checks (= residual, no fix)
  --emit-task + --fix      → re-run checks after fix loop, emit on residual
  --notify-on-fail         → unchanged: notifies on INITIAL set (launchd
                              fallback wants the alert immediately)

Achieves PR sonichi#640's stated contract ("redundant peer for failures the trio
suppresses") rather than the noisy pre-fix variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(health-check): split emit-task logic by --fix state to restore --quiet coverage

PR sonichi#640 v2 (commit f9a88e4) moved the emit-task block to end-of-main so
it'd surface the post-fix residual set. Mini caught a regression: the
launchd fallback's `--quiet --emit-task --notify-on-fail` invocation
bypassed the moved emit via the quiet-path `sys.exit(1)` before reaching
end-of-main. So no-fix callers stopped emitting tasks at all.

Fix: split by --fix state.
- `--emit-task` without `--fix` → emit immediately (initial check IS the
  residual; placed BEFORE --json / --quiet early exits)
- `--emit-task` with `--fix` → emit at end-of-main on residual after fix
  loop (re-runs checks)

Both paths reach emit. No-fix path no longer bypassed by --quiet exit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…/archive (sonichi#653)

* feat(sutando-app): add --emit-task flag to runHealthCheck

Sutando.app's menu-bar Timer was firing health-check.py with --fix only,
so failures it can't auto-fix went silent. The --emit-task flag (already
implemented in health-check.py with 1h cooldown per failure-set hash)
writes tasks/task-health-{ts}.txt for the bridge to pick up as owner-tier
tasks.

Gives the trio's `scan_health_anomalies` surface_owner path a redundant
peer in the file-bridge layer, so failures the trio suppresses by
cooldown (or ACT's LLM step archives by judgment) still reach the agent.

Per Chi 2026-05-07 PT after observing 3 stale services (voice-agent,
web-client, sutando-app) silently suppressed in trio cooldown all day.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(health-check): --emit-task surfaces residual failures, not pre-fix set

Per Mini's PR sonichi#640 v1 review (verified): the original code emitted the
task BEFORE the --fix loop, so `--fix --emit-task` together meant
"emit pre-fix failures, then fix some". Created owner tasks for
failures resolved in the same invocation.

This commit moves emit_task_for_failures to AFTER the fix loop, and
when --fix ran, re-runs checks to capture the residual state. The 2s
delay between fix loop and re-check matches the existing per-service
sleep(1) restart budget.

Behavior matrix:
  --emit-task only         → emit on initial checks (= residual, no fix)
  --emit-task + --fix      → re-run checks after fix loop, emit on residual
  --notify-on-fail         → unchanged: notifies on INITIAL set (launchd
                              fallback wants the alert immediately)

Achieves PR sonichi#640's stated contract ("redundant peer for failures the trio
suppresses") rather than the noisy pre-fix variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(health-check): split emit-task logic by --fix state to restore --quiet coverage

PR sonichi#640 v2 (commit f9a88e4) moved the emit-task block to end-of-main so
it'd surface the post-fix residual set. Mini caught a regression: the
launchd fallback's `--quiet --emit-task --notify-on-fail` invocation
bypassed the moved emit via the quiet-path `sys.exit(1)` before reaching
end-of-main. So no-fix callers stopped emitting tasks at all.

Fix: split by --fix state.
- `--emit-task` without `--fix` → emit immediately (initial check IS the
  residual; placed BEFORE --json / --quiet early exits)
- `--emit-task` with `--fix` → emit at end-of-main on residual after fix
  loop (re-runs checks)

Both paths reach emit. No-fix path no longer bypassed by --quiet exit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(discord-bridge): notify agent-api on dm-fallback delivery + archive task/result files

Two related fixes for the dm-fallback path (`poll_dm_fallback`):

1. POST to localhost:7843/task-done after dm-result.py succeeds, mirroring
   voice-agent's task-bridge.ts:533 path. Without this, web UI has a ~5s
   lag flipping the task status to "done" (waiting for agent-api's next
   /tasks/active poll). Best-effort; silent on failure.

2. Archive task + result files in all three dm-fallback exit paths
   (drop-empty / drop-stale / sent-via-dm). Previously, only the result
   file was unlinked, leaving orphan task files in tasks/ that never
   got archived. Mirrors the parity of the main reply path. This
   addresses the systemic missed-replies pattern surfaced by
   today's user-behavior probe (187 historical owner-tier orphans
   in 7d archive without matching results).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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