Found by the #18 end-to-end review. Pre-existing design — the engine's original orchestrator (vendor/tinycortex/src/memory/sync/composio/orchestrator.rs) has the same shape and #48 ported it faithfully; filed separately.
run_incremental_sync loads SyncState from KV, mutates it in memory for the whole run, and saves at the end (plus mid-run checkpoints). There is no per-connection lock and no version check.
Failure scenario: the periodic Gmail sync is mid-run with 200 ids marked synced and 180 billable requests recorded. A trigger starts a second run for the same connection from the pre-run state, syncs 2 messages, saves. The periodic run then saves over it — or vice versa — losing either the dedup set (→ re-fetch, re-spend) or the daily budget count (→ overspend past the cap).
Candidate fixes: (a) a per-(toolkit, connection_id) async mutex held for the run — simplest, single-process; (b) optimistic version stamp on the KV record with reload-and-merge on conflict — survives multi-process. Note the KV seam is now SyncStateStore { get, set } in core; a compare_and_set would need to reach the contract's KV family.
Found by the #18 end-to-end review. Pre-existing design — the engine's original orchestrator (
vendor/tinycortex/src/memory/sync/composio/orchestrator.rs) has the same shape and #48 ported it faithfully; filed separately.run_incremental_syncloadsSyncStatefrom KV, mutates it in memory for the whole run, and saves at the end (plus mid-run checkpoints). There is no per-connection lock and no version check.Failure scenario: the periodic Gmail sync is mid-run with 200 ids marked synced and 180 billable requests recorded. A trigger starts a second run for the same connection from the pre-run state, syncs 2 messages, saves. The periodic run then saves over it — or vice versa — losing either the dedup set (→ re-fetch, re-spend) or the daily budget count (→ overspend past the cap).
Candidate fixes: (a) a per-
(toolkit, connection_id)async mutex held for the run — simplest, single-process; (b) optimistic version stamp on the KV record with reload-and-merge on conflict — survives multi-process. Note the KV seam is nowSyncStateStore { get, set }in core; acompare_and_setwould need to reach the contract's KV family.