Skip to content

OpenCode executes a child-spawning tool after cancellation #33364

Closed as not planned

Description

@N0zoM1z0

Description

OpenCode can continue into a child-spawning tool call after a user interrupt has already been delivered, then fail to return control within the bounded run.

The standalone reproducer installs public opencode-ai@1.17.9, starts a local mock provider, schedules SIGINT 800 ms after the CLI starts, and delays the provider tool call for 2000 ms. The reproduction succeeds only when the delayed child-spawning tool call still records its PID side effect and the process exits by timeout/signal.

Plugins

None.

OpenCode version

1.17.9

Steps to reproduce

Prerequisites: Docker, Python 3, and the GitHub CLI (gh) for the clone command below. The linked reproducer is self-contained and uses only Python standard-library modules plus Docker. It installs the affected OpenCode version from the public npm package, starts a local mock provider, and runs the CLI in an isolated workspace. The Docker run is limited to 2 CPUs and 4 GiB RAM by default.

Complete self-contained reproducer: https://gist.github.com/N0zoM1z0/b6b7daaf046c5d51345a54f886b11b61

The Gist contains opencode-interrupt-child-late-effect.reproduce.py. Download and run:

gh gist clone b6b7daaf046c5d51345a54f886b11b61 opencode-interrupt-child-late-effect-reproducer
cd opencode-interrupt-child-late-effect-reproducer
python3 opencode-interrupt-child-late-effect.reproduce.py

To reuse an already-built local image:

python3 opencode-interrupt-child-late-effect.reproduce.py --skip-build

Expected successful reproduction output includes:

provider_requests_at_least: observed=... expected>=1
side_effect_file_exists interrupt-child.pid: bytes=... expected>=1
timeout_or_signal: returncode=137 timed_out=True
REPRODUCED

Actual behavior: OpenCode receives the user interrupt before the delayed provider tool call is sent, but the child-spawning tool still records its side effect and the CLI only exits by timeout/signal.

Expected behavior: A user interrupt should prevent later provider-stream tool calls from causing local side effects, and the CLI should return control boundedly.

Screenshot and/or share link

Not applicable. The linked reproducer prints the provider transcript checks and REPRODUCED.

Operating System

Linux x86_64 Docker, using a node:24-bookworm-based target image.

Terminal

Non-interactive CLI run from Python subprocess.

Activity

  1. JihoonJeong commented on Jul 21, 2026

    @JihoonJeong

    I can corroborate this at the provider-transport boundary on OpenCode 1.18.3 and 1.18.4.

    Using an isolated opencode serve plus a loopback OpenAI-compatible SSE provider, I start an actual configured-model turn, wait until the provider emits a valid partial chat-completion SSE frame, then call POST /api/session/:id/interrupt.

    Observed on both versions:

    • interrupt returns 204
    • /api/session/active removes the session
    • the loopback provider request receives neither request aborted nor response close within 5 seconds
    • this reproduces for both a queued and a steered delivery (2 provider requests total)
    • no external provider is contacted

    So the application state is cleaned up, but the in-flight provider HTTP/SSE stream remains alive. That leaves a path for a late provider tool call like the one in this issue to arrive after cancellation.

    The relevant cancellation files are unchanged from v1.18.3 through v1.18.4 and current dev at 4cc022481c184cb9d5e14f7e882b6749d2414553:

    • packages/core/src/session/run-coordinator.ts
    • packages/core/src/session/runner/llm.ts
    • packages/llm/src/route/transport/http.ts

    Expected invariant: once interrupt completes, the provider response body/socket is cancelled promptly and no later provider event can be admitted or execute a tool. I can provide the self-contained TypeScript probe and secret-free JSON reports if useful.

  2. github-actions commented on Sep 19, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

  3. N0zoM1z0 commented on Sep 19, 2026

    @N0zoM1z0
    Author

    Hi, adding a current-version recheck since this was auto-closed today.

    I revalidated the original standalone Docker reproducer against the current npm latest, opencode-ai@1.18.31 (checked 2026-09-19 UTC). The only change to the repro case was updating the package/image version from 1.17.9 to 1.18.31.

    Result: this still reproduces consistently for me. I ran one build run and then three --skip-build reruns against the same 1.18.31 image; all reproduced. The reruns all ended with:

    • process_exit=137
    • process_timed_out=True
    • provider_requests=2 to /v1/messages
    • side_effect_file_exists interrupt-child.pid
    • timeout_or_signal: returncode=137 timed_out=True

    So the child-spawning tool side effect is still observed after the scheduled interrupt, and the CLI still only returns via the reproducer timeout/signal.

    I also did not find a linked merged PR on this issue's timeline that directly covers #33364. Related issue #40829 still references this as the separate “tool starting after cancellation” case.

    Could we reopen this as still current? If you would prefer a fresh issue because of the bot auto-close message, I can also file a new one with the 1.18.31 retest 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