Repository navigation
[Loops] Reconciliation cycle blocks SQLite writes for ~20 s; tool results that land inside it are lost #689
Description
Version: compozy 0.3.0-beta.27, macOS, one daemon. ~/.compozy/compozy.db is 337 MB, plus a 17 MB WAL.
Symptom
A loop action tool runs to the end, and its side effects are on disk, but the daemon fails to persist the result:
{"level":"ERROR","msg":"daemon: execute loop action run",
"task_id":"loop.looprun-18818d74440c30de.g1.node.watch.0",
"error":"call action tool \"ext__work__deliver_watch\": tool \"ext__work__deliver_watch\" observability emit failed\nstore: fail task run lease transaction: store: begin immediate sqlite write: context deadline exceeded"}
Afterwards the node never settles correctly. We saw three outcomes:
- The output goes to
enqueuedand stays there. The node is never dispatched again, even withretry.max_attempts: 3and attempt 1 (loop.looprun-42b94147209f7373.g1.node.prepare.0). - The output stays
runninguntil the node timeout. In our case the timeout was 1440m30s, so the run, its parentrun-loop, and the grandparent all hang (loop.looprun-18818d74440c30de.g1.node.watch.0). - The node fails with
failure_class: transportand is retried. The retry runs a tool that has already run once.
Correlation with the reconciliation cycle
The log has one line per reconciliation cycle:
{"level":"INFO","msg":"daemon: Loop reconciliation cycle complete","runs_examined":0,"records_settled":0,"orphans_repaired":0,"provenance_backfilled":0,"duration_ms":20426}
All 7 lost results on 2026-10-01 fell inside a cycle window (from complete − duration_ms to complete), each 10–13 s after the cycle started:
| Lost (local) | Node | Cycle window |
|---|---|---|
| 13:06:36 | settle |
13:06:26–13:06:43 |
| 14:00:16 | review_gate |
14:00:03–14:00:20 |
| 14:32:56 | release |
14:32:44–14:33:02 |
| 17:51:39 | collect |
17:51:29–17:51:47 |
| 18:15:25 | prepare |
18:15:12–18:15:33 |
| 18:28:37 | wave_ok |
18:28:27–18:28:46 |
| 19:25:40 | watch |
19:25:23–19:25:44 |
The same windows also show session supervision failed: store: sweep heartbeat wake events: database is locked (5) (SQLITE_BUSY). CLI calls made during them fail with SQLITE_BUSY (exit 69) or context deadline exceeded.
The cycle keeps getting longer
The cycle runs every ~72 s (median), and every cycle reports runs_examined: 0.
| Day | Median duration | Max | Lost results |
|---|---|---|---|
| 09-27 | 6.7 s | 21.2 s | 0 |
| 09-29 | 8.1 s | 13.6 s | 5 |
| 09-30 | 11.8 s | 20.9 s | 3 |
| 10-01 | 16.0 s (≈20 s by evening) | 23.9 s | 8 |
The losses began on 09-29 between 18:00 and 20:00 local time. That is when the median duration passed ~10 s, which suggests the result write waits a bounded time (~15 s in the reproduction below) that the longer cycles outlast. On 10-01 the cycles filled ~23% of wall-clock time. On that day, from 15:00Z on, about 2% of node attempts lost their result (9 of 475).
Row counts at the time: loop_runs 689, loop_generations 1466, loop_node_attempts 3728, task_runs 9421, task_events 55882, event_summaries 120266, sessions 787. We found no CLI command that prunes history. We have not changed reconcile_interval.
Reproduction (no fault injection)
Use a test tool that returns at a chosen offset from the start of the next reconciliation cycle. The start is predicted from the complete − duration_ms lines.
| Return offset | Outcome |
|---|---|
| ~0.7 s | 15 s later, fail task run lease transaction … context deadline exceeded. The node output stayed enqueued (attempt 1) for at least 22 min, the tool was never invoked again, and scheduler.lease_sweep reported recovered_leases: 0 (3 nodes). |
| 12.7–14.7 s | observability emit failed. The node went to retrying and the tool ran again (the transport form). |
| 15.7 s | Fine. |
There is also no way to recover such a node from the CLI:
loop node resume --mode plain|immediateon theenqueuednode is refused with exit 78,node_not_paused,allowed=pause,cancel.loop node pause --mode cancelfollowed byresume --mode immediateis accepted, but the tool is never invoked again (13 and 26 min observed).loop node cancelfails the run.
Related: the automation scheduler spins on SQLITE_BUSY
When the database is busy, the automation scheduler retries the job claim in a tight loop: automation.scheduler.dispatch_failed … claim scheduled job … database is locked (5), about 18k lines in one minute. The log rotated 5 times in 15 s, and since only 5 backups are kept, the earlier history was overwritten.
Expected
- A reconciliation cycle that examines 0 runs should not hold the write lock for seconds, and its cost should not grow with history (an indexed or incremental scan, or short transactions).
- When the daemon cannot persist a tool result, it should retry the write instead of dropping it. A node whose tool already returned should never stay
runningorenqueued, and should not be retried as atransportfailure.
Workaround on our side
An external supervisor polls the run tree and cancels or recovers stuck nodes. It treats the symptom only.
Related
#678 (sustained SQLite contention degrading session tools) looks like the same lock pressure seen from the session side.
Metadata
Metadata
Assignees
Labels
Type
Fields
Projects
- StatusShow more project fieldsDone
Activity
AlexandreAkiraAkao commentedon Oct 4, 2026
Update 2026-10-03: the cycle keeps growing, and a restart does not help
Same setup: compozy 0.3.0-beta.27, macOS arm64, one daemon,
reconcile_intervalunchanged.The cycle is now ~26–30 s
From the
daemon: Loop reconciliation cycle completelines (runs_examinedis 0 in every one):The cycles now fill about 30–35% of wall-clock time.
The history grew much less than the cycle did over the same period:
loop_runsloop_generationsloop_node_attemptstask_runstask_eventsevent_summariessessionscompozy.dbis still 337 MB.More results are lost
About 11 tool results were lost in ~3 h of one loop tree today, against ~2% of node attempts on 10-01. In each case the tool finished, its side effects were on disk, and the node stayed
running/enqueued/queued. The affected nodes:prepare(3),wave_start(3),review_gate(2),publish,fix_push,verify. One tool that takes 100–130 s, because it makes CLI calls itself, lost its result three times in a row.Two more shapes of the same loss:
enqueuedinloop status --run-id <id>, whileloop nodes --run <id> --allshows itnot_takenwith no timestamps. Its successors staypendingand the run staysrunning.run-agentnode, the agent finished and wrote its final structured output, and then the session recorded{"code":-32603,"message":"Internal error: Claude ended the turn without returning results for tool calls: …","data":{"errorKind":"incomplete_tool_call"}}and afile_mutation_unverifiedmarker. The attempt was classifiedpayload_declaredand was not retried, despiteretry.max_attempts: 2. We cannot tell whether this shares the cause.CLI calls took from 20 s to over 2 min each, and many needed retries after
SQLITE_BUSY(exit 69) orcontext deadline exceeded.A restart does not help
Before the restart, the daemon (33 h uptime) used 100–200% CPU with no live loop run. The log showed:
scheduler.lease_sweepandscheduler.task_block_sweepevery ~0.5 s;dead):session supervision failed: store: sweep heartbeat wake events: database is locked (5) (SQLITE_BUSY).After
compozy daemon stopand a freshdaemon start --foreground:SQLITE_BUSYcame back within ~3 min (the spawn reaper on the same session and the heartbeat sweep);Is there a supported way to prune loop and task history, or to make the reconciliation cycle skip what it already settled? Today the only workaround we have is to detect the lost result from outside and cancel the run.