[Bug]: Claude subagents show the thread's effort instead of the one their agent definition sets#24144

opened20d agobySsmolpaw

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

An agent with shell access can run all of this.

  1. In a project, add an agent definition that sets its own effort:
    sh
    1mkdir -p .claude/agents && cat > .claude/agents/effort-probe.md <<'EOF'
    2---
    3name: effort-probe
    4description: Replies OK.
    5effort: low
    6---
    7Reply with exactly the word OK.
    8EOF
  2. Start a new Claude thread in that project at xhigh effort. Claude Code loads agent definitions when the session starts.
  3. Send: Call the Agent tool with subagent_type "effort-probe" and the prompt "Reply OK". Do not pass effort.
  4. Read the effort T3 stored for the subagent. ~/.t3/userdata is the state directory of an installed build:
    sh
    1sqlite3 -readonly ~/.t3/userdata/statev2.sqlite "select json_extract(payload_json,'$.title'), json_extract(payload_json,'$.modelSelection') from orchestration_v2_projection_subagents where origin = 'provider_native' order by started_at desc limit 1"
  5. Read the effort the subagent ran at, from its Claude Code transcript:
    sh
    1jq -r 'select(.type=="assistant") | .effort' "$(ls -t ~/.claude/projects/*/*/subagents/agent-*.jsonl | head -1)" | sort | uniq -c

Expected behavior

The subagent shows low, the effort it runs at, or no effort when T3 can't tell.

Actual behavior

T3 stores and shows xhigh, the thread's effort. Claude Code ran the subagent at low:

1step 4: Effort probe reply OK|{"instanceId":"claudeAgent","model":"claude-opus-5-5","options":[{"id":"effort","value":"xhigh"}]}
2step 5: 2 low

updateClaudeSubagentNode in apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts (around line 5669 on main @ 66ee628e) takes the effort from the Agent call's effort argument, else from the owner subagent, else from the thread. Claude Code applies a definition's effort after the call is written, so T3 never sees it.

A PreToolUse hook that sets effort through updatedInput hits the same path. With a hook that sets high, every request goes out at high and T3 shows xhigh.

Related: #14842 reported this on 0.0.40 and was closed when V2 stopped showing effort. #17496 brought the label back with the thread fallback. Open PR #17042 removes the fallback, but its "Not verified" list includes a live custom-agent run. The steps above are that run.

Impact

Minor bug or occasional failure

Version or commit

0.1.2. The code is unchanged on main @ 66ee628e.

Environment

T3 Code 0.1.2 on Arch Linux x86_64 (desktop) and Ubuntu 24.04 arm64 (service). Claude Code 2.1.309, Claude Opus 5.5.

Workaround

Read the effort from the subagent's transcript (step 5).

Synced from GitHub

Confirmed on macOS: T3 Code Nightly 0.1.2-nightly.20261022.3526 (desktop), macOS 26.6.2, Claude Code 2.1.309. The definition case reproduces as described with a user-level definition in ~/.claude/agents/ (Haiku, effort: medium): from a max thread, T3 stores max and the subagent runs at medium. On earlier nightlies with the same fallback (Claude Code 2.1.306โ€“2.1.308), it also went the other way: with the thread at medium, a definition with effort: high showed medium and ran at high.

A third path: Claude Code's maxEffortLevel clamp. Claude Code can also lower the effort after the Agent call is written, and T3 doesn't see that either. This path also applies when the Agent call passes effort explicitly, and #17042 keeps explicit values, so that case would still show the wrong effort after it merges.

Steps, using the same checks as steps 4 and 5 above:

  1. In ~/.claude/settings.json, cap Sonnet at high (these are my exact settings):
    json
    1"modelSettings": { "claude-sonnet-5-5": { "effortLevel": "high", "maxEffortLevel": "high" } }
  2. Start a Claude Opus 5.5 thread at max.
  3. Send: Call the Agent tool three times with subagent_type "general-purpose" and the prompt "Reply OK": model "sonnet" without passing effort, model "sonnet" with effort "xhigh", and model "haiku" without passing effort.
  4. Run steps 4 and 5 above for each subagent.
Agent callT3 storesRan at (transcript .effort)
model: "sonnet", effort not passedmaxhigh
model: "sonnet", effort: "xhigh"xhighhigh
model: "haiku", effort not passed (control, no cap)maxmax

$CLAUDE_EFFORT inside each subagent matched its transcript (high, high, max). The effortLevel default isn't what lowers it: from a medium thread, the same Sonnet subagent ran at medium (earlier nightly, Claude Code 2.1.306).

The first row is the common one in practice. In my state database, 34 Sonnet general-purpose subagents launched without an effort argument from a max thread over Oct 20โ€“23 show max and ran at high on every turn. #17042 would blank that label; the second row would keep showing xhigh. This is the subagent side of #17110, where T3 doesn't read the maxEffortLevel cap. #17496 notes that Claude Code only publishes the applied effort on Remote Control init frames and in hook inputs, not on the stream T3 reads.


Investigated by Claude Opus 5.5 in T3 Code.

Synced from GitHub

Still happens on main @ 6575ce3, with a user-level agent definition and a model override.

  • ~/.claude/agents/explorer.md sets model: sonnet and effort: medium.
  • The parent thread runs Opus at high and calls Agent with subagent_type: "explorer" and no effort argument.
  • The subagent's transcript has claude-sonnet-5-5 with effort: "medium" on every request (perTurnEffort: "medium" too).
  • T3 shows Claude Sonnet 5.5 ยท high in the task row and the Lineage hover card.

The model is right and the effort is wrong. The fallback is now at:

https://github.com/pingdotgg/t3code/blob/6575ce32508bd7908444241f3719aa33aaf77f90/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts#L6272-L6277

The model starts from the same launch data. It ends up right only because it is later corrected from the subagent's own assistant messages (L8616โ€“L8651). Effort gets no such correction.

Claude Code 2.1.312.

Synced from GitHub

Still happens on T3 Code 0.1.7 with Claude Code 2.1.318. Claude Code's new CLAUDE_CODE_SUBAGENT_EFFORT (added in 2.1.316) makes it hit every subagent launched without an effort argument, not only agents with an effort: definition.

CLAUDE_CODE_SUBAGENT_EFFORT sets the effort a subagent runs at when neither the Agent call's effort nor the agent definition's effort sets one. Without it, a subagent inherits the session's effort. Claude Code applies it when it starts the subagent, after the Agent call is written, just as it applies a definition's effort. So T3 sees no effort on the call and falls back to the thread's.

This replaces the PreToolUse hook from the report. That hook did the same thing through updatedInput and ran into the same path.

Steps:

  1. In ~/.claude/settings.json, set "env": { "CLAUDE_CODE_SUBAGENT_EFFORT": "high" }.
  2. Start a Claude Opus 5.5 thread at xhigh.
  3. Send: Call the Agent tool twice with subagent_type "general-purpose" and the prompt "Reply OK": once without passing effort, once with effort "medium".
  4. Run steps 4 and 5 from the report for each subagent.
Agent callT3 storesRan at (transcript .effort)
effort not passedxhighhigh
effort: "medium" (control)mediummedium

The parent ran at xhigh on every turn. With the parent thread at high, the same subagent shows high and runs at high, so this only shows up when the thread and the env var differ.

The fallback is unchanged on main @ 67eebac:

https://github.com/pingdotgg/t3code/blob/67eebaca23f8f7bc46e8817434087fc9c5acf8e3/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts#L6788-L6793

T3 can't take the effort from its own launch settings: Claude Code reads the env var from settings.json, and the agent definition can override it. Dropping the thread fallback, as #17042 does, would show no effort instead of a wrong one for these subagents too. Showing the real level would need the effort Claude Code applied. Claude Code writes it into the subagent's transcript (.effort and perTurnEffort on each assistant entry), but per #17496 not on the stream T3 reads.


Investigated by Claude Opus 5.5 in T3 Code.

Synced from GitHub

Still happens on T3 Code 0.1.8-nightly.20261109.4399 (desktop app on macOS connected to a Linux server, Claude Code 2.1.322).

  • User-level definition ~/.claude/agents/opus-medium.md: model: opus, effort: medium.
  • Parent thread: Claude Fable 5.2 at Extra High, calling Agent with subagent_type: "opus-medium" and no effort.
  • Every subagent transcript (subagents/agent-<id>.jsonl) has claude-opus-5-5 with "effort":"medium" on all requests. The .meta.json files have "agentType":"opus-medium".
  • T3 stores modelSelection: { model: "claude-opus-5-5", options: [{ id: "effort", value: "xhigh" }] } and the Lineage hover card shows Opus 5.5 ยท Claude ยท xhigh.

The wrong effort also passes to nested forks. Two of the opus-medium subagents started subagent_type: "fork" children. All six forks ran at medium according to their transcripts, but T3 stored xhigh for them too. They get it through input.owner?.effort, so the fallback's wrong value is passed down a level.

Counts from one thread: 27 Claude subagents, all of which ran at medium. T3 stored xhigh for 23 of them (17 opus-medium and 6 nested forks) and no effort for the other 4.

Same fallback at ClaudeAdapterV2.ts:6867 on the v0.1.8-nightly.20261109.4399 tag, unchanged on main.

Comment drafted by Claude Opus 5.5 in Claude Code, from a T3 Code triage session.

Synced from GitHub

Still happens on T3 Code 0.1.9-nightly.20261110.4478 (desktop app on Windows 11, Claude Code 2.1.322).

  • User-level definition ~/.claude/agents/explorer.md: model: haiku, effort: medium.
  • Parent threads: Claude Fable 5.2 and Claude Opus 5.5, calling Agent with subagent_type: "explorer" and no effort.
  • 29 explorer subagents from one day, across 7 threads. Every subagent transcript (subagents/agent-<id>.jsonl) has claude-haiku-5-5 with "effort":"medium" on all requests.
  • T3 stored the parent thread's effort in modelSelection for each of them:
Parent effortT3 storedTranscriptSubagents
xhighxhighmedium19
lowlowmedium6
highhighmedium1
mediummediummedium3

The label is wrong in both directions. It shows a higher effort than the subagent used when the thread is at xhigh, and a lower one when the thread is at low.

Reported by an AI agent: Claude Fable 5.2 at xhigh effort, running in T3 Code, posted at the account owner's request.

Synced from GitHub

A working fix for the "show the real level" part, in case it helps: https://github.com/syz51/t3code/pull/12 (a fork of 0.1.9).

It keeps the current guess as the first label, then replaces it with the effort from the subagent's transcript (<config>/projects/<project>/<session>/subagents/agent-<task_id>.jsonl). The adapter reads it on the subagent's first assistant frame and again when the task settles. The read runs outside the frame pump, with at most 8 tries per subagent. The child thread's model selection gets the same correction.

Because it reads what Claude Code applied, it should cover the definition, CLAUDE_CODE_SUBAGENT_EFFORT, hook and maxEffortLevel paths alike. Checked with an adapter test for both moments (label goes medium โ†’ xhigh). I have not yet run it against a live custom agent.

This could sit on top of #17042: no label until the transcript gives one.

This is the same problem as #17042, so I'm closing it in favour of that one. If it is different, say how and it will be reopened.