Skip to content

Gemini reuses tool call IDs (tool_0), colliding in the global job registry and hanging shell tool calls forever #43940

Closed

Description

@major

Description

Foreground shell calls hang forever on Gemini models. The command actually runs and its output file is complete, but the tool part never settles, the timeout argument has no effect, and only an interrupt clears it.

It's an ID collision in the job registry:

  • packages/ai/src/protocols/gemini.ts:609 assigns `tool_${nextToolCallId++}` and the counter resets per request (gemini.ts:661), so the first tool call of every Gemini request is tool_0. Over 6 days of local history: 3032 Gemini tool calls, 434 distinct IDs, tool_0 used 2570 times. Every other provider I've used (OpenAI, Anthropic, DeepSeek, GLM) was 100% unique, so they never hit this.
  • packages/core/src/tool/plugin/shell.ts:295 starts the job as id: context.id, the raw call ID.
  • packages/core/src/job.ts:205-208: if an entry with that ID is still running, start() returns the stale snapshot and never forks the new run. Entries are never evicted.
  • shell.ts:314 then blocks on the dead job's deferreds (job.ts:278-281), which never resolve.

shell.create happens before job.start, which is why the command still executes and the output looks finished. The timeout argument settles the shell session that nothing is awaiting any more, so it does nothing. Interrupting calls job.cancel, which unpoisons that ID until the next leak, hence the stall/escape/stall cycle.

Some real examples that hung: ls -la .git with timeout: 5000 sat for 756s, git log -n 5 --oneline with timeout: 5000 for 182s. Hang rate for shell calls with an explicit timeout was 6% on Gemini vs 0.1% on a GPT model over 10x the volume.

Gemini does hand back a unique ID, it's already stored as providerState.functionCallId and just isn't used for the part/job ID. Using it would fix the immediate problem, though keying the registry on a provider-supplied string seems worth revisiting regardless.

Not #38291 or #42524, the process exits cleanly here.

OpenCode version

0.0.0-dev-17824 (dev)

Steps to reproduce

  1. Use a Gemini model.
  2. Get one shell job to leak, so a tool_0 entry stays running in the registry (tearing down the run scope without reaching job.cancel does it).
  3. Any later Gemini shell call in the same server process is also tool_0, and hangs forever.

At the job layer: start({id: "tool_0", run: <never settles>}), then start({id: "tool_0", run: <settles immediately>}) and block({id: "tool_0"}) blocks forever, second run never executes.

Operating System

Fedora Linux 44

Terminal

Alacritty

Activity

  1. major commented on Aug 21, 2026

    @major
    ContributorAuthor

    I'll work up a PR.

  2. neriousy commented on Oct 20, 2026

    @neriousy
    Member

    This is fixed by #44051 and #45985: V2 preserves Gemini's supplied function-call IDs and generates unique fallback IDs, while shell jobs use their own shell IDs instead of raw tool-call IDs. This removes the job-registry collision described here. If you still see this on v2, please open a new issue with the details.

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

Metadata

Metadata

Assignees

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