Summary
The reported-*.json snapshots (interventions / prompt-tokens / daily-sessions) are updated with a lock-free read-merge-write: read the snapshot, spread-merge this session's ack, write the whole object back with a non-atomic writeJson. When two auto-reports from two agent sessions interleave read-read-write-write, the first writer's ack is clobbered by the second (which merged onto a stale read). The dropped session then double-counts on the next report cycle, because it is no longer recorded as already-reported.
Same root cause as the usage.jsonl lost-update defect (sibling issue): a lock-free RMW of a globally shared ~/.teamai file.
Where
src/team-push.ts @ 48b3dcb
writeReportedInterventions() — line 149, writeJson(getReportedInterventionsPath(), data) (line 151)
- interventions ack merge — line 587,
writeReportedInterventions({ ...existingIv, ...nextReported })
- prompt-tokens twin —
writeFile(p, JSON.stringify(data)) (line 230)
- daily-sessions twin —
writeJson(getReportedDailySessionsPath(), data) (line 296)
All three files live under ~/.teamai/dashboard/ — global, one file each, scope-independent. writeJson is a plain (non-atomic) writeFile. None of these take a lock.
Why it is reachable in practice
- Within a single
pull(), the auto-report loop is serial (for … await), so it does not self-collide.
- The collision is cross-process: two SessionStart auto-pulls from two agent sessions on one machine, each carrying an ack for a different session id.
- The per-scope
.sync-lock does not help — it guards the team clone, not ~/.teamai/dashboard/. Auto-report can even outlive the lock: it runs under a 5s withTimeout, so pull() may return and release the lock while auto-report keeps running.
- The read→write gap spans a
git push, which is long, widening the window for a read-read-write-write interleaving.
Counterexample (TLA+ / TLC 2.19)
Modelled the snapshot as a set of acknowledged session ids. Two writers P and Q each add their own id; the correct final state is {p, q}. BothAcked states that once both writers finish, both acks are present. TLC finds a violation:
State 1: Init — snap = {}
State 2: P.read — writer P reads the snapshot (pView = {})
State 3: Q.read — writer Q reads the same snapshot (qView = {})
State 4: P.write — P writes its view + its own ack "p" (snap = {p})
State 5: Q.write — Q writes its STALE view + "q" (snap = {q})
-> "p" is gone. BothAcked VIOLATED.
Consequence: session p's ack is lost, so on the next report cycle its interventions/prompts/tokens are counted again and pushed to the team — a double-report.
Suggested fix (smallest correct)
Same remedy as the usage.jsonl issue: serialize each snapshot's read-merge-write under a per-file lock (reuse acquireLock/releaseLock from src/update.ts), and use writeFileAtomic (or a writeJsonAtomic) for the write so a crash mid-write cannot truncate the snapshot either. Structurally identical to the usage.jsonl defect — a shared "locked JSON read-modify-write" helper covers both, plus the events.jsonl compaction.
Severity
High — corrupts the team's aggregated stats (double-counted sessions), and the corruption is pushed to the shared repo. Less easily triggered than the usage.jsonl race (needs two near-simultaneous auto-pulls), but the damage is durable and team-visible.
Found via TLA+/TLC concurrency audit of the sync/state layer. Sibling defects: usage.jsonl lost update (Critical), events.jsonl compaction race (Medium).
Summary
The
reported-*.jsonsnapshots (interventions / prompt-tokens / daily-sessions) are updated with a lock-free read-merge-write:readthe snapshot, spread-merge this session's ack,writethe whole object back with a non-atomicwriteJson. When two auto-reports from two agent sessions interleave read-read-write-write, the first writer's ack is clobbered by the second (which merged onto a stale read). The dropped session then double-counts on the next report cycle, because it is no longer recorded as already-reported.Same root cause as the
usage.jsonllost-update defect (sibling issue): a lock-free RMW of a globally shared~/.teamaifile.Where
src/team-push.ts@48b3dcbwriteReportedInterventions()— line 149,writeJson(getReportedInterventionsPath(), data)(line 151)writeReportedInterventions({ ...existingIv, ...nextReported })writeFile(p, JSON.stringify(data))(line 230)writeJson(getReportedDailySessionsPath(), data)(line 296)All three files live under
~/.teamai/dashboard/— global, one file each, scope-independent.writeJsonis a plain (non-atomic)writeFile. None of these take a lock.Why it is reachable in practice
pull(), the auto-report loop is serial (for … await), so it does not self-collide..sync-lockdoes not help — it guards the team clone, not~/.teamai/dashboard/. Auto-report can even outlive the lock: it runs under a 5swithTimeout, sopull()may return and release the lock while auto-report keeps running.git push, which is long, widening the window for a read-read-write-write interleaving.Counterexample (TLA+ / TLC 2.19)
Modelled the snapshot as a set of acknowledged session ids. Two writers P and Q each add their own id; the correct final state is
{p, q}.BothAckedstates that once both writers finish, both acks are present. TLC finds a violation:Consequence: session
p's ack is lost, so on the next report cycle its interventions/prompts/tokens are counted again and pushed to the team — a double-report.Suggested fix (smallest correct)
Same remedy as the
usage.jsonlissue: serialize each snapshot's read-merge-write under a per-file lock (reuseacquireLock/releaseLockfromsrc/update.ts), and usewriteFileAtomic(or awriteJsonAtomic) for the write so a crash mid-write cannot truncate the snapshot either. Structurally identical to theusage.jsonldefect — a shared "locked JSON read-modify-write" helper covers both, plus theevents.jsonlcompaction.Severity
High — corrupts the team's aggregated stats (double-counted sessions), and the corruption is pushed to the shared repo. Less easily triggered than the
usage.jsonlrace (needs two near-simultaneous auto-pulls), but the damage is durable and team-visible.Found via TLA+/TLC concurrency audit of the sync/state layer. Sibling defects:
usage.jsonllost update (Critical),events.jsonlcompaction race (Medium).