Sync tools in one assistant batch now run via Promise.all instead of
sequentially — a tool pending on I/O no longer blocks its siblings.
Three coordinated changes keep the event-sourced runtime sound:
- executeAllowedTools is two-phase: invocation events are appended
serially in source order (durable before any side effect, deterministic
log prefix), then all sync executions run concurrently, each appending
progress/results as they land. Per-call error and cancel semantics are
unchanged (moved to executeSyncTool).
- append() commits through an internal queue: persist → reduce → stream
runs to completion per batch, so file order, in-memory order, and
stream order stay identical even while executions overlap. A failed
commit rejects only its caller; the chain survives for siblings.
- Abort-registry state is scoped per tool call (turnId:toolCallId) via a
wrapper, fixing two latent races: createForRun destroying a running
sibling's tracked processes, and cleanup tearing down the turn-wide
force-kill scope when the first tool finished.
Wire ordering is untouched: model requests already reference tool
results by the assistant message's source order, pinned by a new test.
No concurrency cap and no per-tool serialization by design; tools that
share state must tolerate racing (file edits already reject stale
writes via their search/replace precondition).
Spec §4.5/§10.5 updated. New runtime tests cover overlap (deadlock
unless concurrent), progress interleaving, sibling failure isolation,
mid-batch cancellation, and crash recovery with multiple open
invocations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>