What happen
syncLocalSavings (packages/cli/src/index.ts) reads the sync watermark, queries the local spend DB for rows past it, POSTs them to /api/v1/imports, and only then writes the new watermark. There's no lock around that read-query-post-write sequence, so two processes that both fire it around the same time (two terminals finishing a cave wrap session close together, or two cave sync runs) can both read the same watermark before either writes it. Both then fetch the same/overlapping row set and both POST successfully, so the same spend/savings rows get uploaded twice.
It's the same shape as the telemetry watermark race fixed in #1116, just with no guard at all, not even the optimistic check telemetryTokenDelta had before that fix.
Ran it: built the CLI in a container, wrote a regression test using the same technique #1116's own test uses (a --require preload that delays fs.writeFileSync on the watermark file, to widen the exit race), pointed it at sync.json instead of config.json, seeded 2 rows in the local spend DB, and ran two caveman sync processes at once against a stub import endpoint. Both exited 0, both printed "synced 2 spans", the stub got 2 separate POSTs, 4 rows uploaded for 2 real spans.
No idea if the control-api backend dedupes imports server-side by span/trace id since that's not in this repo, so I can't say whether this actually double-counts on a real dashboard, only that the client sends the duplicate POST and the stub accepts both.
Expected
Each local spend row makes it to the dashboard exactly once, whichever process happens to sync it.
Before/after example
Input: two `caveman sync` processes racing, 2 unsynced rows
Got: both POST the same 2 rows, watermark still ends up correct, but 4 rows landed server-side for 2 real spans
Want: one process wins the race, the other sees the advanced watermark and sends nothing (or the same lock #1116 already uses)
Platform
Not tied to a particular wrapped agent, any two concurrent cave wrap/cave sync invocations trigger it.
Version / install method
HEAD 2fd153c6, built from source.
Happy to send a PR using the same lock as #1116 if that's useful, just say the word.
What happen
syncLocalSavings(packages/cli/src/index.ts) reads the sync watermark, queries the local spend DB for rows past it, POSTs them to/api/v1/imports, and only then writes the new watermark. There's no lock around that read-query-post-write sequence, so two processes that both fire it around the same time (two terminals finishing acave wrapsession close together, or twocave syncruns) can both read the same watermark before either writes it. Both then fetch the same/overlapping row set and both POST successfully, so the same spend/savings rows get uploaded twice.It's the same shape as the telemetry watermark race fixed in #1116, just with no guard at all, not even the optimistic check
telemetryTokenDeltahad before that fix.Ran it: built the CLI in a container, wrote a regression test using the same technique #1116's own test uses (a
--requirepreload that delaysfs.writeFileSyncon the watermark file, to widen the exit race), pointed it atsync.jsoninstead ofconfig.json, seeded 2 rows in the local spend DB, and ran twocaveman syncprocesses at once against a stub import endpoint. Both exited 0, both printed "synced 2 spans", the stub got 2 separate POSTs, 4 rows uploaded for 2 real spans.No idea if the control-api backend dedupes imports server-side by span/trace id since that's not in this repo, so I can't say whether this actually double-counts on a real dashboard, only that the client sends the duplicate POST and the stub accepts both.
Expected
Each local spend row makes it to the dashboard exactly once, whichever process happens to sync it.
Before/after example
Platform
Not tied to a particular wrapped agent, any two concurrent
cave wrap/cave syncinvocations trigger it.Version / install method
HEAD
2fd153c6, built from source.Happy to send a PR using the same lock as #1116 if that's useful, just say the word.