Skip to content

eca__git tool calls in one assistant message run concurrently and race on index.lock #611

Description

@stig

Describe the bug

Multiple eca__git calls in a single assistant message execute concurrently, so they race on .git/index.lock and the ref store. The failure is usually silent: the second command operates on the tree as it was before the first, and both report success.

on-tools-called! in src/eca/features/chat/tool_calls.clj reduces over the turn's tool calls and starts each approved one as a future — the :add-future action forces the delay — then moves to the next without waiting. The futures are joined in a doseq only after the reduce has started them all. So a turn containing two git calls runs two git processes against the same repo simultaneously.

That's correct and desirable for most tools. Git is the exception: its writes take exclusive locks, so concurrent invocations in one repo are never safe, even when they look independent.

To Reproduce

Get the model to emit two eca__git calls in one message, e.g.

  1. git add -A
  2. git commit --amend -F - <<'EOF' ... EOF

Observed outcomes:

  • fatal: Unable to create '.../index.lock': File exists — visible and recoverable
  • the amend silently doesn't happen, the previous commit stays in place, and both calls report success

git commit + git push has the same shape, and there the silent case pushes the pre-commit tree while reporting a normal ref update.

Expected behavior

Git writes against the same repo are serialised, or the second fails loudly instead of racing.

Doctor

ECA server: 0.160.2
Client: emacs GNU Emacs 30.2 (aarch64-apple-darwin25.6.0)
OS: Mac OS X 26.7
Java: 25.0.4

Additional context

Prompt-level rules are unreliable here. The system prompt encourages batching independent tool calls, and add + commit read as independent verbs while being one logical operation, so the judgement goes wrong exactly where it matters.

Possible approaches, increasing in precision:

  1. Serialise all eca__git calls behind a per-repo lock. Git commands are short and there's little to gain from running them concurrently.
  2. Serialise writes only. The tool's operation enum nearly classifies this already (status, diff, log, pr_view are reads), but it's LLM-supplied, so parsing the command is safer — shell-parser/approval-keys already does command parsing for approvals.
  3. Reject a second concurrent git write with an explicit error. Noisier, but it teaches the model to split the calls rather than hiding the mistake.

eca__shell_command can also run git, so it has the same exposure, though detecting it there is harder.

One caveat: the dispatch behaviour above is from reading tool_calls.clj, not from instrumenting a build. The delay/future/:add-future chain and the post-reduce join look unambiguous, but worth confirming before acting on it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions