Reconciliation
The stuck-task problem
Section titled “The stuck-task problem”If a worker crashes mid-task, the engine may emit task_started but never task_success or task_failure. Without reconciliation, the dashboard shows "running" forever.
The reconciliation worker
Section titled “The reconciliation worker”Runs in z4j every Z4J_RECONCILIATION_SWEEP_SECONDS (default 5 minutes). For each agent:
- Find all tasks in state
startedolder thanZ4J_RECONCILIATION_STALE_THRESHOLD_SECONDS(default 15 minutes). - Issue a
reconcile_taskcommand to the agent that owns the task. It is an ordinary command row, audited under the actionreconcile_task, carrying an idempotency key so two sweeps inside one window collapse onto a single probe. - The agent asks its engine: Celery
AsyncResult.status, RQjob.get_status(), and so on, and answers with one canonical state:pending,started,success,failure, orunknown. unknownis a no-op. It means the adapter has no result backend to consult, which is not evidence that the task is gone, so the brain changes nothing and the task is probed again on a later sweep.- A terminal answer (
successorfailure) is applied, and the brain records the outcome the missing event would have carried. Applying it fires thetask.orphanedautomation trigger once for that correction. - A non-terminal answer is applied only when the row has not been written since the probe was issued. If it has, the brain has fresher information than the probe saw and the response is dropped. A task already in a terminal state is never moved back out of it.
Why not rely on the engine alone
Section titled “Why not rely on the engine alone”Each engine reports differently:
- Celery -
AsyncResultis Redis-TTL-bound; after expiry it returnsPENDINGregardless. - RQ - cleanly reports lost, but requires periodic cleanup for stale jobs.
- arq - no introspection API for historical jobs; relies on application-level result store.
The reconciliation worker owns the "was it really lost?" question so no adapter has to.
Tunables
Section titled “Tunables”| Setting | Default | Meaning |
|---|---|---|
Z4J_RECONCILIATION_SWEEP_SECONDS |
300 |
Seconds between reconciliation passes |
Z4J_RECONCILIATION_STALE_THRESHOLD_SECONDS |
900 |
How old a "started" task must be before the worker queries the agent |
Audit impact
Section titled “Audit impact”Each probe writes an audit log entry under the action reconcile_task, naming the task it asked about. The correction itself is visible as the task's state change and, where an automation rule is armed, as a task.orphaned firing.
Limits
Section titled “Limits”- Reconciliation does not retry anything. It corrects the brain's snapshot to whatever the engine says is true. Retrying is a separate operator decision, or an automation rule armed on
task.orphaned. - If the agent is offline, reconciliation skips that agent and retries next pass.
- No retroactive reconciliation across deploys - only tasks still present in
tasks+eventsare examined.