Skip to content

fix(thread-control): grant thread control without a wording check - #2271

Merged
chuks-qua merged 9 commits into
mainfrom
feat/ghissue-2267
Oct 21, 2026
Merged

chuks-qua merged 9 commits into
mainfrom
feat/ghissue-2267

Conversation

@chuks-qua

@chuks-qua chuks-qua commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

Before this change, the server read the user's message to decide whether a turn got Mcode thread control. isExplicitMcodeThreadRequest matched wording such as "mcode thread" plus an action verb and rejected messages with a nearby negation. Requests like "Farm this out to Codex" or "Call delegate_task once. Do not call any other tool." got no lease, and the tool call failed with "Internal thread-control MCP request denied".

Now no turn depends on its wording. isExplicitMcodeThreadRequest and its regexes are deleted. Every turn on a provider that supports internal thread control (claude, codex, cursor, copilot) gets an active lease, and the lease still revokes at turn end.

The limit is ADR-0032's permission-mode ceiling. A supervised Coordinator cannot request a full child, and a child with no named mode inherits the Coordinator's. delegated-task-service.ts and thread-control-service.ts enforce it.

A denied call now carries a reason, invalid_tool_call or no_active_turn, with a message the model and the user can act on.

Codex no longer restarts its session when threadControlEligible changes between turns. The flag is now constant per provider, so it never changes.

The fix round also:

  • Activates the lease on turns a provider starts by itself, such as Claude continuing after a background task. These turns never got a lease before.
  • Grants the lease the turn's effective permission mode. A "default" turn on a thread saved as Full now gets a Full lease. Before, it got Supervised.
  • Rewrites the model guidance in packages/thread-orchestration/src/mcode-instructions.ts. "Hand work to another provider or model" now uses delegate_task without naming Mcode threads. Native sub-agents stay the default for "sub-agent" requests, and the model never launches a thread just because the user said "sub-agent".

Verification:

  • Targeted vitest runs pass: thread-orchestration (20 tests), and server agent-service-gate, mcode-session-authority, thread-control-toolkit, and agent-service-container (75 tests).
  • Typecheck passes for thread-orchestration, contracts, server, and providers. bun run lint exits 0.
  • Main was merged in, which added thread_launch. Main's new gate tests in src/mcode-instructions.test.ts were removed along with the gate.
  • Not verified on a live provider turn.

Why

The wording check guarded the wrong thing. It read the user's words, not what the delegated work may do, and it refused ordinary requests with an error that gave no cause. ADR-0032 replaces it with the permission-mode ceiling.

Fixes #2267. Part of #2218.

Review Notes

  • Behavior change: a message such as "Do not create an Mcode thread" no longer blocks thread control on the server. The model's guidance now carries that case, and the permission-mode ceiling still bounds what any child may do.
  • Not in this PR: stepping inherited modes down to the target provider. feat: offer Delegation without the wording gate, bounded by a runtime-mode ceiling #2218 stays open for it.
  • Follow-up: consider deleting the now-constant threadControlEligible plumbing.
  • Open questions for later issues, not fixed here:
    • Thread-control reads (thread_search, thread_get, thread_wait, workspace_search) are not bounded by the permission ceiling, and they are now available on every turn.
    • Delegation has no depth or concurrency cap, and a Full child can stop its Full Coordinator.
    • A replayed delegate_task with the same clientRequestId launches its saved plan before the ceiling check. This predates this PR.
  • No UI change, so no screenshots.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…ted negations

Reproduces #2267. Both prompts from the issue are denied today, and a
denied call gives no reason the model or user can act on.
… its sentence

A negation now voids only the thread-control names after it in the same
sentence, so an unrelated "don't" no longer denies an explicit request.
A denied call now says why: no active turn, a message that did not ask
for thread control, or an invalid tool call id.

Fixes #2267
Every turn on a provider that supports thread control now gets an
active lease. The user's message no longer decides whether the
thread-control tools work, so requests such as "Farm this out to
Codex" or prompts with an unrelated "don't" are no longer refused.

The permission ceiling from ADR-0032 is the real limit and is already
enforced: a supervised Coordinator cannot start a full-access delegate,
and an unnamed mode inherits the Coordinator's.

Codex also stops restarting its session whenever a turn's eligibility
flipped, because eligibility is now fixed per provider.

Fixes #2267
Part of #2218
@chuks-qua chuks-qua changed the title fix(thread-orchestration): scope the thread-control negation check to its sentence fix(thread-control): grant thread control without a wording check Oct 21, 2026
@chuks-qua
chuks-qua enabled auto-merge (squash) October 21, 2026 16:03
@chuks-qua
chuks-qua merged commit dad0fd2 into main Oct 21, 2026
11 checks passed
@chuks-qua
chuks-qua deleted the feat/ghissue-2267 branch October 21, 2026 16:08
chuks-qua added a commit that referenced this pull request Oct 23, 2026
…t a per-turn flag (#2280)

* refactor(delegation): drop the per-turn thread-control eligibility flag

TurnRequest.threadControlEligible was the last piece of the wording
gate. Since #2271 the server set it from the provider alone, so it
never varied for a Codex session, but the Codex adapter still used it
to decide whether to attach the Delegation toolkit and kept a session
restart for an eligibility change that could no longer happen.

Codex now attaches the Delegation toolkit to every session, as the
Claude, Cursor and Copilot adapters already do. The steer runtime
reads whether a turn holds the Delegation lease from the lease
directive itself instead of the request flag.

Part of #2218

* docs(providers): describe the Delegation toolkit attach flag without per-turn eligibility

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Thread-control gate denies explicit requests that contain an unrelated negation

1 participant