rowboat/apps/x/packages
Ramnique Singh 1dc042adc8 fix(x): hold the per-turn lock until straggler sync-tool appends land
A sync-tool batch runs under Promise.all, which rejects the moment one
tool's result append fails (a transient fs fault, or errorMessage()
throwing on a nullish thrown value). That rejection propagated out
through withLock and freed the per-turn lock while sibling tools were
still executing. A straggler's later append then ran with no lock held
and validated against a stale in-memory history rather than the file —
so if the caller re-advanced in between (writing the recovery
indeterminate result for the not-yet-settled tool), the straggler
appended a second tool_result for the same call. Duplicate results make
reduceTurn throw on every future read, permanently corrupting the turn
file and, through context references, every session behind it.

- executeAllowedTools now awaits Promise.allSettled and rethrows the
  first fault only after every tool has finished appending, so a fault
  never orphans a sibling.
- advance() drains the append queue (settleAppends) before withLock
  releases the turn, closing the fire-and-forget reportProgress path and
  any future append-path rejection.
- errorMessage() uses optional chaining so a `throw null`/`throw
  undefined` yields a normal isError tool result instead of a TypeError
  that escapes as an infrastructure rejection (the same fault that
  triggers the lock-escape mid-batch).

Adds two regression tests (both fail without the fix): a nullish throw
records an isError result and completes the turn; a faulted batch does
not settle until the slow sibling's append is durable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:17:42 +05:30
..
core fix(x): hold the per-turn lock until straggler sync-tool appends land 2026-07-13 11:17:42 +05:30
shared fix(x): fail-closed tool permissions with per-tool catalog declarations 2026-07-10 23:07:11 +05:30