MCP token keeps the old provider after a thread switches back to a reused provider session; delegate_task fails with parent_not_active#16565

opened23d agobySsnnbotchway

What happened

Version: 0.0.46-nightly.20261005.2702 (Linux, systemd user service).

  1. Thread runs on Claude (claude_proxy). Its Claude provider session stays alive.

  2. The user switches the thread to Codex (codex_proxy, shared session). Codex attaches to the thread. prepareMcpSession(thread, codex_proxy) revokes the thread's credential and issues a Codex-scoped one into the per-thread slot.

  3. The user switches back to Claude. The old Claude provider session is still in sessions and still attached to the thread, so ensureThreadAttached does not call prepareMcpSession. The Claude process starts (--resume) with the token from the slot. That token is still scoped to codex_proxy.

  4. Every orchestration call that checks the live caller fails:

    • delegate_task: parent_not_active: Delegated tasks require an active run owned by this MCP provider session.
    • t3_queue_cancel: parent_not_active: The calling provider no longer owns an active thread run.

    The active run is correct (providerInstanceId: claude_proxy, rootNodeId set). The mismatch is scope.thread.providerInstanceId (codex_proxy).

Evidence: orchestration_v2_projection_provider_session_bindings lists both provider-instance:claude_proxy:thread:<id>:… and provider-instance:codex_proxy:shared for the thread. The trace shows no McpSessionRegistry.issue when the thread went back to Claude.

Expected

When a run starts on a provider, the thread's MCP credential is scoped to that provider. If the slot holds a credential for another provider, it is rotated, even when the provider session is reused.

Workaround

Restart the T3 service. The next run opens a fresh Claude session, which issues a correct token.

Possible fix

On each run start (or provider process spawn), call prepareMcpSession(threadId, run.providerInstanceId), not only when the thread is newly attached. Or keep one slot per thread and provider, not one per thread.

Related: #15173 (also about session reuse and delegated runs).

Note

Grok responding on behalf of Julius.#18852 is the 401 form of this bug. After Claude → Codex → Claude → Codex inside Codex's 30-minute idle window, Codex keeps a revoked MCP credential and every T3 tool returns "Authentication required". PR #17096 lists this as a known limit. A complete fix needs per-(thread, provider instance) credentials, or releasing the previous provider's session on switch.

Synced from GitHub

Prepared the focused sequential switch-back repair in #18884. It refreshes the reused native MCP client as well as the server credential, preserves peer conversations, and covers retained starts and interrupted issuance. Focused tests pass; the linked fork CI is running. Live provider-switch verification remains untested.

Synced from GitHub

Additional historical incident with two instances of the same provider, offered as supporting symptom evidence rather than a minimized reproduction or confirmation of the credential root cause.

In retained local T3 tool results from 13 October UTC, two long-lived threads, including a Codex coordinator, received OrchestratorMcpFailure, code parent_not_active, message The calling provider no longer owns an active thread run. The failures affected actual orchestration calls, including task status and delegation, rather than only an agent's description of a failure.

One coordinator's sanitized recovery sequence was:

  • The active turn on Codex instance A completed at 21:39:58 UTC.
  • A continuation requested on Codex instance B at 21:39:47 failed at 21:39:59.
  • A user-requested /compact on B completed at 21:42:07.
  • Task-status calls succeeded at 21:42:13 and 21:42:27, followed by delegation at 21:43:42.

The account switch, old-run completion, compaction and a new continuation overlap. This does not establish that context length caused the failure or that compaction alone fixed it. Issued credential scope was not inspected. The precise incident build is not established by this supplement, and no new provider-switch run was made.

The relevant proposed solution appears to be #19413's per-thread/per-provider-instance credentials, with same-driver A/B instances included in regression coverage. I also read the later correction on #19308: that incident did involve a scheduled task and provider switch, so it should not be cited as independent proof of a compaction-only bug.

For verification, the useful matrix seems to be A -> B -> A on both different drivers and two accounts of one driver, retaining native sessions, plus a query reopen/compaction and independent revocation. No request to weaken active-run ownership checks. Historical evidence cannot certify that the open fix works on a current build.

Prepared with GPT-6.1 Sol through the Codex harness in T3 Code at the user's request. Private thread IDs, paths, credentials and transcripts are omitted.

Synced from GitHub

Confirming this on Linux desktop nightly 0.1.1-nightly.20261016.3218. The trigger here was a thread-bound scheduled task pinned to Codex. Each of its wakes switched a Claude thread to Codex and back, and the reused Claude session's token stayed scoped to codex (details in https://github.com/pingdotgg/t3code/issues/17932#issuecomment-6231857637).

The active run was claudeAgent, running, and McpSessionRegistry.resolve succeeded. t3_thread_send to another thread still failed in mcp.loadCaller with parent_not_active. Restarting T3 Code was the only fix. A new turn in the same thread did not help, which is consistent with no new credential being issued for a reused session. +1 for rotating the credential on each run start, or keeping one slot per (thread, provider instance), as proposed in https://github.com/pingdotgg/t3code/pull/17096 and https://github.com/pingdotgg/t3code/pull/19413.

Fresh triage against main b5a226e49c: this is a functional bug report, not a reported exploitable vulnerability. Read the report and all four comments, and scanned open issues through the 90-day cutoff; no earlier matching report found. #15173 has a different idle-release trigger; #18852 is already consolidated here.

Source inspection confirms the relevant repair concerns MCP credential issuance, revocation and provider scoping in ProviderSessionManager. The automated triage hard stop explicitly reserves credential and MCP changes for Julius. No runtime reproduction was attempted and no fix was written; leaving this issue open for maintainer handling.

Reproduced on current main 3b95e7831b on Linux in the web dev app with isolated state. Ran Claude Opus 5.5 → Codex GPT-6.1-Sol → Claude Opus 5.5 in one thread, retaining both provider sessions. The returning Claude turn’s delegate_task failed with parent_not_active (The calling provider no longer owns an active thread run); no child started. Both session bindings were still ready and attached. The earlier credential-surface stop was incorrect: this report describes a functional bug, not an exploitable vulnerability.

Synced from GitHub

Confirming on stable 0.1.1 (remote agent host on Linux x64, run as a systemd user service with t3 serve; client is the 0.1.1 Linux desktop app). Same sequence as reported, Claude → Codex → Claude in one thread:

  • 20:04Z: runs 1–2 on claude. Claude provider session opened.
  • 20:09Z: thread.provider-switched → codex (shared session). provider-session.attached for Codex. Run 3 completed.
  • 20:16Z: thread.provider-switched → claude. Only provider-session.updated was logged, with no .attached event and no new credential. The Claude process restarted with --resume.
  • 20:25–20:28Z, during runs 5 and 7 (status running, providerInstanceId: claude, rootNodeId set): delegate_task ×3 and t3_thread_launch ×1 all returned parent_not_active / "The calling provider no longer owns an active thread run." In the trace, McpHttpServer.authenticateRequest → McpSessionRegistry.resolve → mcp.loadCaller all succeeded, then the tool call failed, so the token was valid but scoped to the other provider.
  • orchestration_v2_projection_provider_session_bindings lists both provider-instance:claude:thread:<id>:… and provider-instance:codex:shared for the thread.

Workaround not applied yet: other threads on the host had active runs, so a service restart wasn't safe at that point.

Triaged with t3 triage by Claude Code (Claude Opus 5.5).

Synced from GitHub

Another trigger, with no user action: a usage-limit failover between two instances of the same provider.

Nightly 0.1.2-nightly.20261022.3541, macOS desktop app, two Claude provider instances signed in to different accounts (call them A and B). From the thread's provider event log:

  • Runs on instance A until 03:19:16Z.
  • 03:19:35Z: the next run goes to instance B, which is already at its weekly limit, so the turn ends immediately with the limit message.
  • 03:19:55Z: the next turn goes back to instance A and reuses A's existing provider session. A new Claude process starts with --resume.
  • 03:20:07Z onward: every t3_thread_send / steer to another thread fails with parent_not_active ("The calling provider no longer owns an active thread run."). It still fails on every later turn, 5+ runs over about 45 minutes.

autoResumeLimitedThreads is on, so the bounce to the limited instance and back can happen without anyone choosing a provider. Any setup with more than one account for a provider can end up in this state just by hitting a limit.

Same mechanism as the original report: the reused session never re-runs prepareMcpSession, so the slot keeps the credential scoped to the other instance. #19413 (one credential per thread and provider instance) looks like it covers this case too.

Synced from GitHub

Confirmed another live occurrence on 0.1.3-nightly.20261023.3553 (release commit 51273020db3245f4ec29514a419d29b5dcb012dc), Ubuntu 24.04 ARM64 on an EC2 host running T3 as a systemd user service, with a remote T3 client.

In an existing Codex conversation, a fresh diagnostic turn called orchestrator_capabilities, then delegate_task exactly once with:

json
1{
2 "target": {
3 "providerInstanceId": "codex",
4 "model": "gpt-6-luna",
5 "options": { "reasoningEffort": "low", "serviceTier": "priority" }
6 },
7 "mode": "wait",
8 "timeoutMs": 20000
9}

The task was a no-tools, no-files response probe. Capabilities returned the correct parent thread, inherited provider codex, and model gpt-6-astra. Delegation returned:

1OrchestratorMcpFailure
2code: parent_not_active
3message: The calling provider no longer owns an active thread run.

No child task was created. This reproduced an earlier failure with medium reasoning and priority. As a control, the equivalent Codex/Luna/priority delegation from a separate macOS T3 conversation completed successfully, with the returned selection confirming priority. This is an orchestration refusal before model execution, rather than evidence that Luna Priority is unsupported.

The EC2 conversation has previously used different providers, which makes this issue a plausible match. The actual credential scope at the failure has not been inspected, so stale provider scoping remains a hypothesis for this occurrence. Reading the matching release source places the exact refusal in the live-caller checks; no access checks have been disabled and no fix has been deployed. A new turn alone did not recover delegation.

Reporting at the user's request. Private conversation IDs, project contents, credentials, and host addresses omitted. Prepared with Codex in T3 Code.

Synced from GitHub

What happened

Additional trace-backed occurrence of the HTTP 401 variant of this issue, using Codex → OpenCode → Codex in one existing app thread. Reporting here because #18852, which describes the same Authentication required symptom, was consolidated into this issue.

The user's report was that T3 orchestration tools worked before a provider transition, then all returned Authentication required, while GitHub CLI still worked. The affected operations included task_status, orchestrator_capabilities, PR linking, and thread listing/launch. The agent could continue ordinary shell work, but could no longer manage delegated work through T3. Further ordinary turns did not recover authentication.

Diagnosis

The server rejects an actual presented token before tool dispatch. This is the revoked-token variant, rather than an authenticated request reaching the parent_not_active guard.

Evidence from the retained provider protocol log and server trace:

  1. The original Codex native conversation received T3 MCP configuration on thread/start, and again on an earlier thread/resume. T3 calls succeeded.
  2. When the parent switched to OpenCode, McpSessionRegistry.revokeThread immediately preceded McpSessionRegistry.issue. Those spans share the parent span for orchestrationV2.providerTurnStart.start.
  3. OpenCode successfully called capabilities, read existing tasks, and delegated work after the switch.
  4. When the parent switched back about 19 minutes later, the Codex protocol log shows thread/inject_items and turn/start against the original native conversation. There is no corresponding thread/resume or replacement thread/start to supply current MCP configuration.
  5. Its next T3 MCP request returned HTTP 401. In that same request trace, the auth middleware logged unknown_or_expired_token, not missing_bearer_token.
  6. Eleven Codex MCP POST requests returned HTTP 401 over the next roughly 33 minutes. Repeated ordinary continuation turns did not recover the client.
  7. A separate, fresh Codex conversation initialized with MCP configuration and successfully called T3 capabilities and read the affected app thread. The provider-session identifier remained the shared Codex runtime identifier.

The v0.1.0 source agrees with the installed bundle inspected on this machine:

  • prepareMcpSession reads one per-thread credential slot, requires provider-instance compatibility for reuse, and otherwise revokes the thread's credentials before issuing a replacement.
  • ensureThreadAttached prepares credentials only on a new attachment. An already attached thread bypasses this preparation.
  • providerThreadLoadKey does not include credential identity. The resume path skips the adapter's native resume when the conversation remains loaded.
  • McpHttpServer rejects the unusable provider credential before dispatch.
  • The default credential liveness window is 24 hours. This incident began less than an hour after initialization, so it does not require the idle-expiry trigger in #19489.

High-confidence explanation: provider switching revoked the credential captured by the retained Codex MCP client; switching back reused that client without a configuration refresh. Exact token comparison is unavailable because the provider logs redact Authorization values. The HTTP rejection, native conversation reuse, and missing resume are directly observed; connecting them to the old token is an inference supported by the source.

The same failed request also contains MalformedSessionTokenError through SessionStore.verify and EnvironmentAuth.authenticateMcpClient. That is the secondary external-client authentication attempt after provider-token lookup fails, not evidence of expired GitHub or provider-account login.

Steps to reproduce

Observed production sequence, not a new minimized deterministic reproduction:

  1. Start a Codex app thread and successfully call a read-only T3 MCP tool.
  2. Complete the turn and switch that same app thread to OpenCode.
  3. Complete an OpenCode turn with successful T3 MCP calls.
  4. Switch back to Codex while its native conversation is still loaded in the shared app-server, here about 19 minutes after leaving it.
  5. Call orchestrator_capabilities or task_status from the returning Codex turn.
  6. Observe Authentication required, backed by HTTP 401 and unknown_or_expired_token on the server. Further ordinary turns reuse the failure.

Expected: the returning native client has a valid T3 credential for its provider instance and existing task/PR state remains accessible.

Version

The incident server reports 0.1.0, confirmed through the live environment endpoint and installed server package. The source comparison uses release tag v0.1.0, commit 81493efb71e8e8244f95d3e9f71d8369654ab151. The packaged build's precise provenance beyond its reported version is not independently established. No separate t3 triage CLI was launched.

Environment

  • Linux x64, Nix-packaged T3 server running as a systemd user service.
  • Node.js 24.21.0.
  • Codex app-server; HTTP client identifies itself as codex-mcp-client/0.163.0.
  • Parent models: gpt-6.1-sol and opencode/deepseek-v4.1-flash-free.
  • Full-access, default interaction mode. Exact frontend client build was not recorded. The failure is visible in server/provider traces.

Evidence

Selected events below are reduced to relevant fields, with relative times and placeholder identifiers. C1 is the same native Codex conversation throughout.

text
1T+00:00.000 parent switches to OpenCode
2T+00:00.102 McpSessionRegistry.revokeThread
3T+00:00.104 McpSessionRegistry.issue
4T+06:11 OpenCode orchestrator_capabilities -> success
5T+06:43 OpenCode delegate_task -> success
6
7T+19:00 parent switches back to Codex
8 outgoing thread/inject_items { threadId: C1 }
9 outgoing turn/start { threadId: C1, model: gpt-6.1-sol }
10 no native thread/resume or replacement thread/start
11
12T+19:26 same request trace:
13 McpSessionRegistry.resolve
14 SessionStore.verify -> MalformedSessionTokenError
15 EnvironmentAuth.authenticateMcpClient -> invalid credential
16 McpHttpServer.authenticateRequest:
17 rejected MCP request with an unusable credential
18 reason: unknown_or_expired_token
19 POST /mcp -> 401
20 user agent: codex-mcp-client/0.163.0
21 task_status -> Authentication required
22
23T+19:26 other task_status, link_pull_request,
24 orchestrator_capabilities -> Authentication required
25T+52:41 affected Codex MCP request still returns HTTP 401
26T+53:01 fresh Codex conversation gets MCP config;
27 capabilities and affected-thread read succeed

A local read-only evidence checker verified the recorded tool failures, HTTP 401 responses, presented-token rejection, switch-time revoke/issue sequence, old native conversation reuse, and absence of native resume during the failed continuation period. All six historical checks passed. This checker examines real retained logs; it is not a regression test or proof of a fix.

Related issues

  • #18852 was already consolidated here as this same 401 variant.
  • #19489 has a separate 24-hour idle-expiry trigger, which was absent here.
  • At reporting time, #19413 and #17096 remain open and unmerged. #18884 is closed without merge. No shipped remediation for this exact path was verified. The latest stable release is v0.1.2, but upgrading alone is not claimed to fix this incident.

Useful regression coverage would retain the actual native Codex conversation across Codex → OpenCode → Codex, then assert successful capabilities and task-status requests with the credential that the client actually consumed. Testing only fresh native conversations would miss this case.

Fix applied or workaround

No source patch, database write, credential change, or service restart was applied during diagnosis. Fresh-thread T3 access works, but that alone does not restore the original session. After posting this report, a bounded probe in the original app thread successfully called capabilities, retrieved an existing delegated result, and listed its linked PRs. No configuration changes, source patches, database writes, credential changes, or service restart were needed between the report and this probe. The recovery trigger was not established, so this is not proof that the underlying switch-back defect is fixed. The original thread was then instructed to resume its previously authorized work. A global restart would affect other live work and was avoided.

Filed by

GPT-6.1-Sol through the Codex harness in T3 Code, at the user's request, following the upstream triage playbook. Project names, repository and PR details, private thread/task/native IDs, host identity, home paths, exact incident timestamps, and credentials are omitted. Full logs and transcripts are intentionally not attached.

Synced from GitHub

Follow-up: recovered the affected environment on nightly 0.1.3-nightly.20261023.3566 with a controlled T3 service restart after all active threads had saved checkpoints and reached a terminal state.

In the previously failing conversation (now using the Claude provider), a real native T3 delegate_task call to codex / gpt-6-luna with {reasoningEffort:"low",serviceTier:"priority"} completed after restart. The actual tool result confirmed status: completed, provider codex, model gpt-6-luna, options low/priority, and the requested no-tool response marker. Paused project work was then resumed.

This verifies the restart workaround and cross-provider delegation on this environment. It does not establish a permanent fix, inspect the old credential scope, or prove retained provider switching is repaired. No ownership checks were changed and no source patch was deployed.