Skip to content

V2: Unrelated sessions share one outbound trace ID until the server restarts #54555

Open

Description

@mtraynham

Description

In OpenCode v2.0.26, model requests from unrelated sessions connected to the same shared background server send traceparent headers with the same trace ID. Their parent-span IDs differ. An OpenTelemetry-aware LiteLLM proxy consequently records requests from multiple OpenCode sessions in one trace.

Restarting the OpenCode server changes the trace ID—even for a continuing session. Session-ID propagation works correctly. This reproduces with no local user plugins active.

Expected behavior

Unrelated session work should not inherit a server-lifetime trace. Whether trace boundaries are per request, turn, or session, restarting the server should not determine the trace ID shared by subsequent sessions.

Possible cause (not yet confirmed)

withoutParentSpan applies to HTTP serving but not service boot. The session coordinator captures a fork runtime during service construction, potentially retaining the boot tracing context.

Environment

  • OpenCode: v2.0.26
  • OS: macOS 27.0.1
  • Terminal: iTerm.app
  • Plugins: No local user plugins active
  • Observability: LiteLLM proxy exporting OpenTelemetry traces to Langfuse

Plugins

No response

OpenCode version

v2.0.26

Steps to reproduce

  1. Configure an OpenAI-compatible provider through a proxy that can inspect incoming traceparent headers.
  2. Using the shared OpenCode service, send a model request from each of two new sessions.
  3. Compare the headers. In my test, both requests had trace ID b40f5ce0220bde39bc9c6b1677dd9b9b, but different parent-span IDs: ada5fb70cf80e1a0 and 4512ad72886fcf3b.
  4. Run opencode service restart and send another request. Requests from an existing session and a new session now share a different trace ID: f2ce22c33feca38d69acc7b26d540cf5.

Screenshot and/or share link

No response

Operating System

macOS 27.0.1

Terminal

iTerm2

Activity

  1. opencode-agent commented on Oct 11, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report, I was able to reproduce this.

    I set up an OpenAI-compatible provider pointing at a local endpoint that logs the incoming traceparent and x-session-id headers, and used it on v2.0.26:

    • Two new sessions run through the shared background service sent requests with the same trace ID and different parent span IDs.
    • After opencode service restart, both a continued session and a new session switched together to a new shared trace ID.

    It still happens on the latest dev (d21b079), run from source. The script below reproduces it. Session ID headers were correct throughout, as you said.

    Your guess about the cause looks plausible: the session run coordinator creates its fork runtime while services are booting, so session runs may inherit the server startup span. This isn't confirmed yet.

    Related: #49038 (outbound traceparent propagation) and #37395 (an earlier fix isolating per-request server traces).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #54555: unrelated sessions on the shared background service send the
    # same traceparent trace ID until the service restarts.
    # Usage: bash repro.sh            (uses `opencode` from PATH)
    #        OC_DEV=/path/to/checkout bash repro.sh   (runs from source)
    set -u
    DIR=$(mktemp -d); PROJ=$DIR/proj; mkdir -p $PROJ; export HOME=$DIR/home; mkdir -p $HOME
    PORT=4599
    cat > $DIR/fake.ts <<'EOF'
    Bun.serve({ port: Number(process.env.PORT), async fetch(req) {
      console.log(JSON.stringify({ traceparent: req.headers.get("traceparent"), session: req.headers.get("x-session-id") }))
      const body: any = await req.json().catch(() => ({}))
      if (!body.stream) return Response.json({ id: "x", object: "chat.completion", created: 0, model: "m", choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }] })
      const chunks = [
        { id: "x", object: "chat.completion.chunk", created: 0, model: "m", choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }] },
        { id: "x", object: "chat.completion.chunk", created: 0, model: "m", choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } },
      ]
      return new Response(chunks.map(c => `data: ${JSON.stringify(c)}\n\n`).join("") + "data: [DONE]\n\n", { headers: { "content-type": "text/event-stream" } })
    }})
    EOF
    cat > $PROJ/opencode.json <<EOF
    { "provider": { "fake": { "npm": "@ai-sdk/openai-compatible", "name": "Fake",
        "options": { "baseURL": "http://127.0.0.1:$PORT/v1", "apiKey": "x" }, "models": { "m": { "name": "m" } } } },
      "model": "fake/m", "small_model": "fake/m" }
    EOF
    PORT=$PORT bun $DIR/fake.ts > $DIR/requests.log 2>&1 & FAKE=$!
    sleep 1
    # From source the background service cannot be auto-spawned, so run one server
    # process by hand and point every client at it with --server.
    export OPENCODE_SERVER_PASSWORD=repro OPENCODE_PASSWORD=repro
    start_srv() { (cd $OC_DEV/packages/cli && exec bun run src/index.ts serve --hostname 127.0.0.1 --port 4700 >$DIR/srv.log 2>&1) & SRV=$!; sleep 8; }
    oc() {
      if [ -n "${OC_DEV:-}" ]; then
        case "$1" in
          service) kill $SRV 2>/dev/null; wait $SRV 2>/dev/null; [ "$2" = restart ] && start_srv; return;;
          run) shift; set -- run --server http://127.0.0.1:4700 "$@";;
        esac
        bun --conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js $OC_DEV/packages/cli/src/index.ts "$@"
      else opencode "$@"; fi
    }
    [ -n "${OC_DEV:-}" ] && start_srv
    cd $PROJ
    oc run -m fake/m "session one" >/dev/null 2>&1
    oc run -m fake/m "session two" >/dev/null 2>&1
    echo "== before restart (two new sessions) =="; cat $DIR/requests.log; : > $DIR/requests.log
    oc service restart >/dev/null 2>&1
    oc run -m fake/m -c "continue session two" >/dev/null 2>&1
    oc run -m fake/m "session three" >/dev/null 2>&1
    echo "== after restart (continued session + new session) =="; cat $DIR/requests.log
    oc service stop >/dev/null 2>&1
    kill $FAKE

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions