Fix silent task.completed / task.blocked hooks (#2449)#536
Merged
Conversation
Investigation summary --------------------- `/home/exedev/notifications.jsonl` on the agent server had not received a write since 2026-03-11 despite dozens of task completions since. The file is populated by `~/.config/task/hooks/task.completed` and `~/.config/task/hooks/task.blocked` scripts. The `task.blocked` script had only fired twice (both on the day hooks were installed) and then again on 2026-04-20. The `task.completed` script had never fired. Root cause turned out to be three separate gaps: 1. **db.UpdateTaskStatus only emitted `task.updated`**, never the lifecycle events `task.blocked` / `task.completed`. Every caller (CLI, TUI, MCP server, the Claude `Notification` / `Stop` hook subprocesses) routes through this function, so none of them fired the specific hooks. 2. **The executor's success path transitions to StatusBacklog**, not StatusDone, because "only humans should mark tasks as done." That leaves no code path that fires `task.completed` on agent success. 3. **Event hook goroutines were racing CLI exits**. `events.Emitter.Emit` spawned `go e.runHook(event)` but never awaited it. Short-lived commands like `ty close` returned before the hook subprocess even forked, so the append to notifications.jsonl silently dropped. Fix --- - `internal/db/events.go` + `internal/db/tasks.go`: extend `EventEmitter` interface with `EmitTaskBlocked` and `EmitTaskCompleted`, and fire them from `UpdateTaskStatus` on the relevant status transitions. Every caller now emits lifecycle events as long as an emitter is registered. - `internal/executor/executor.go`: fire `EmitTaskCompleted` on agent success (which transitions to backlog, not done, so the db emit doesn't cover it) and `EmitTaskFailed` on agent failure so watchers can distinguish "needs input" from "agent died." - `cmd/task/main.go`: add `openTaskDB` helper that registers a process-wide events emitter on every db open — claude-hook, MCP server, and every CLI/TUI command share the same emitter — plus a `PersistentPostRun` on the root Cobra command that waits for pending hook goroutines to finish before the process exits. - `internal/events/events.go`: track in-flight emits with a WaitGroup and expose `Emitter.Wait()` so CLI commands can flush before exit without blocking the daemon's hot path. Verification ------------ Upgraded server to this build and exercised three real scenarios on the agent server; each produced the expected line in `notifications.jsonl`: - agent execution hitting Claude's `Stop` hook → `task.blocked` fires - `ty close <id>` on a blocked task → `task.completed` fires - `ty status <id> blocked` on a done task → `task.blocked` fires Extras ------ - `examples/hooks/notifications-health-check.sh`: cron-friendly script that alerts if notifications.jsonl goes silent longer than a threshold (default 24 hours) so this class of outage is caught earlier next time. - README/examples docs updated to reflect when each event actually fires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
The
notifications.jsonlfile on the agent server had been silent for ~40 days despite many task completions. Three separate bugs in the hook pipeline compounded to suppresstask.completedcompletely and lettask.blockedfire only by accident.db.UpdateTaskStatusonly emitted the generictask.updatedevent, so every caller (CLI, TUI, MCP, Claude hook subprocesses) silently skipped lifecycle hooksbacklognotdone, so no code path firedtask.completedfor agent-finished tasksevents.Emitter.Emitused a fire-and-forget goroutine, so short-lived CLI commands likety closeexited before the hook subprocess even forkedFix
EventEmitterwithEmitTaskBlocked/EmitTaskCompleted, fire them fromUpdateTaskStatuson the matching transitions — every caller benefits as long as an emitter is registeredtask.completedwhen the agent succeeds (since the status goes to backlog) andtask.failedwhen it diesopenTaskDBhelper registers a process-wide emitter on every DB open;PersistentPostRunon the root Cobra command flushes pending hook goroutines before exitEmitter.Wait()viasync.WaitGroupso CLI commands can flush without blocking the daemonVerification
Built the fix, deployed to the agent server, and verified three real scenarios each produced the expected
notifications.jsonlline:Stophook →task.blockedfires via the Claude hook subprocess emitting through the db layerty close <id>→task.completedfires, and the PersistentPostRun wait prevents the process from exiting before the append completesty status <id> blocked→task.blockedfires the same wayExtras
examples/hooks/notifications-health-check.sh: cron-friendly script that alerts when notifications.jsonl stays silent longer than a threshold (default 24 hours) so the next outage is caught earlyTest plan
go test ./...greenhowdyrunner-agents.exe.xyz, restartedty daemontask.blockedline appearsty close→ verifiedtask.completedline appearsty status ... blocked→ verifiedtask.blockedline appearsnotifications-health-check.shas a cron on the agent server🤖 Generated with Claude Code