Skip to content

fix(tasks): background reconcile loop so completion hooks fire without a reader (closes #96)#113

Merged
imran31415 merged 1 commit into
mainfrom
fix/background-task-reconciler
Jun 16, 2026
Merged

fix(tasks): background reconcile loop so completion hooks fire without a reader (closes #96)#113
imran31415 merged 1 commit into
mainfrom
fix/background-task-reconciler

Conversation

@umi-appcoder

@umi-appcoder umi-appcoder Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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/events work in #93.

The bug

_reconcile_status only ran lazily — on list_tasks / get_task / stream (server.py). A task whose sole purpose is a response_url callback (webhook/cron-spawned, headless) would not transition running → completed or 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 (no tmux subprocess); a corrupt/missing task dir is skipped; one bad task never kills the pass.
  • TaskReconciler — a single-process daemon (modeled on memory.sync.ClaudeMemorySyncer), idempotent start(), status() for introspection.
  • Bootstrap — starts the daemon; interval via 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_tasks bound · reconcile failure isolated (no raise) · start() idempotent · status() shape.

Full Python suite: 458 tests, OK.

Next

With this merged, #93 can add the /api/events SSE emitter on top of the reconcile loop, and the SPA can drop its polling (part 2 of #101).

Closes #96

🤖 Generated with Claude Code

…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>
@imran31415 imran31415 merged commit c4b8e78 into main Jun 16, 2026
7 checks passed
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.

fix(tasks): background reconcile loop so completion hooks fire without a reader

1 participant