CedarCopilot wants to merge 0 commits into staging from fix/reaper-redrive-once-and-notify
Not deployed
Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.
A user's post-meeting follow-up (Kendall @ Warp) silently never appeared. Root cause: the on-event run hung partway through (a subagent stalled on an in-flight call ~1 min in), sat executing for 30 min, and the stale-execution reaper tombstoned it with no retry , so the recap was silently lost. This is systemic: every reaper sweep tombstones 5,18 hung runs across users, and llm_request_timeout has fired 0 times in 3 days (107 llm_request_failed in the same window prove the log pipeline works), i.e. the stalls are in an unbounded await the per-request LLM timeout can't see , most likely a tool call (external-CRM push / enrichment / MCP).
Full write-up: docs/design/execution-hang-guard-and-self-recovery.md.
Layer A , bound the hang (activity watchdog). execution-watchdog.ts: a per-execution idle deadline, not a total wall-clock cap , so a legitimately long, progressing run (20+ min, many subagents) is never killed. Every completed step kicks it (wired to onStepFinish); only a step that never finishes (a hung call) lets it fire. watchdog.race() then rejects the awaited generate() the instant it does , regardless of which await is stuck and without depending on the framework threading the signal into tool execution. The signal also rides the existing LlmCallContext ALS, and prompt-cache-fetch composes it into every LLM request so an in-flight socket aborts promptly (subagents inherit the ALS). EXECUTION_IDLE_TIMEOUT_MS (default 10 min of no progress, 0 disables); the loop is separately bounded by maxSteps.
Layer B , recover (self-healing tail). On a stall the orchestrator step marks the run failed and immediately runs attemptEventRunRecovery (services/agent-action-queue/reaper-recovery.ts): re-drive the event once via dispatchReDerivation(scope:'full') , the same proven path a manual refresh_conversation uses , and if the re-driven run also fails, Slack-DM the user ("Processing your recent meeting failed ⦠if this is happening often or it's mildly annoying you, please report it to the Cedar team"). Retry count is derived from data (failed root execs for the same eventId in a 6h window) , no schema change, no marker threaded through the re-exec stack. The reaper stays the backstop for process-restart orphans (where the in-process watchdog dies with the process) and calls the same recovery.
execution_stalled logged, run marked failed, re-drive runs, recap appears; a second stall ā user Slack-DM; retry_count derivation caps at one retry.execution-watchdog (incl. the 20-min-progressing-run case + the stop-progressing case), reaper-recovery, existing reaper, existing prompt-cache-fetch (34, regression). Server types clean on touched files; deps:check clean.tool_request_timeout log to pinpoint which await hangs.generate() unwind instead of lingering; now an optimization, not load-bearing).on-event-agent-execution-workflow.ts (now 1041 lines , extract orchestratorAgentStep).š¤ Generated with Claude Code
The PR adds an activity-based execution watchdog and a shared recovery tail that re-drives stalled event runs once before notifying the user.
Libra has not measured any production surfaces for this change yet.