fix(tasks): background reconcile loop so completion hooks fire without a reader (closes #96)#113
Merged
Merged
Conversation
…96) Until now task status was reconciled only lazily — on list/get/stream. A finished task whose only purpose is a response_url completion hook would therefore not flip running->completed (and not fire its hook) until something happened to read it, so headless webhook/cron callbacks could be arbitrarily delayed, or never fire if nothing polled. Add a single-process background daemon (TaskReconciler, modeled on memory.sync.ClaudeMemorySyncer) that periodically calls a new ClaudeTaskManager.reconcile_running(): it iterates non-terminal tasks and runs the existing _reconcile_status (which already flips status and fires the completion hook at-most-once under the meta lock). Terminal tasks are skipped cheaply (no tmux subprocess); a bad task dir is skipped, never raised; the loop never dies on one bad task. Interval is configurable via KC_RECONCILE_INTERVAL (default 10s) and the daemon starts in the server bootstrap. This is the prerequisite for the SSE /api/events work in #93. Tests: tests/task_reconciler_test.py (10) — dead-task completion + hook fire, no-hook-without-response_url, terminal cheap-skip, live stays running, corrupt/missing meta skipped, max_tasks bound, failure isolation, start idempotency, status shape. Full suite 458, OK. Closes #96 Co-Authored-By: Claude Opus 4.8 (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.
What
Adds a background reconcile loop so a finished task's status flips and its completion hook fires even when no client is reading the task. Closes #96 and is the prerequisite for the SSE
/api/eventswork in #93.The bug
_reconcile_statusonly ran lazily — onlist_tasks/get_task/ stream (server.py). A task whose sole purpose is aresponse_urlcallback (webhook/cron-spawned, headless) would not transitionrunning → completedor fire its hook until something happened to read it. So those callbacks could be arbitrarily delayed — or never fire if nothing polled.The fix
ClaudeTaskManager.reconcile_running()— iterates non-terminal tasks and runs the existing_reconcile_status(which already flips status and fires the hook at-most-once under the meta lock). Terminal tasks are skipped cheaply (notmuxsubprocess); a corrupt/missing task dir is skipped; one bad task never kills the pass.TaskReconciler— a single-process daemon (modeled onmemory.sync.ClaudeMemorySyncer), idempotentstart(),status()for introspection.KC_RECONCILE_INTERVAL(default 10s).No behavior change for the lazy read paths — this just also runs reconciliation in the background.
Tests (
tests/task_reconciler_test.py, 10)dead-task → completed + hook fired · no hook when no
response_url· terminal tasks skipped with no subprocess call · live session stays running · corrupt/missing meta skipped ·max_tasksbound · reconcile failure isolated (no raise) ·start()idempotent ·status()shape.Full Python suite: 458 tests, OK.
Next
With this merged, #93 can add the
/api/eventsSSE emitter on top of the reconcile loop, and the SPA can drop its polling (part 2 of #101).Closes #96
🤖 Generated with Claude Code