Skip to content

🚕 feat: Add Per-Call Subagent Machine Selection - #16757

Merged
danny-avila merged 19 commits into
devfrom
danny-avila/subagent-workspace-choice
Oct 5, 2026
Merged

danny-avila merged 19 commits into
devfrom
danny-avila/subagent-workspace-choice

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A parent agent working in an attached machine's workspace could delegate to a subagent that then ran somewhere else. In the incident behind this change, Lia worked in a BYOM workspace for danny-avila/agents and delegated a review to the PR Reviewer subagent, which started in the Code API checkout and could not see the PR. #16756 makes children inherit their parent's machine by default when allowed. This PR lets the parent choose explicitly, per subagent call.

Each lazy subagent now advertises optional machine and workspace arguments on the subagent tool, as enums of exactly the choices that child can reach right now: machines admitted in the conversation's sealed codeWorkspaces, on the child's default or allowlist, present in the requesting principal's tenant-scoped machine list, and served by a ready worker. A child the conversation pinned to a machine offers only that machine; a child with no reachable machine declares nothing and its schema is unchanged. workspace is declared only when each reachable machine's workspace ID is distinct (each machine has exactly one workspace in a conversation, so it is an alternative way to name the same choice).

When the parent passes a choice, the child's resolver re-validates it against the current request (allowlist, principal list, sealed decision, live worker and workspace) before initializing the child, and refuses anything no longer reachable with a model-visible error naming the argument, for example Subagent error: The requested "machine" is unavailable right now. Omit "machine" to let the host choose, or pass a different value. There is no silent fallback. Omitting both arguments keeps #16756's inherit-by-default routing, extended so a child also inherits the machine a per-call-routed parent runs on (passing through agents that run no code).

A child whose default workspace is unavailable keeps #16756's guard (listed as unavailable, never runs) unless it has per-call choices; then it stays callable, its description asks the parent to pass a listed machine, and an omitted call is still refused with the same workspace error.

A subagent keeps one machine for the whole request, matching #16756's "one route per subagent" rule: a later call that omits the machine follows that subagent's earlier per-call route, and a call that would move it elsewhere is refused. The route is claimed when the call is placed (before the asynchronous initialization), including for a default route and for graph members. A call's claims, the per-agent tool context it sets, and its config's entry in the shared graph-member cache stay pending until the run hands the child's inputs to the SDK (a settle hook on the lazy descriptor wraps the whole selection); a failed or canceled selection gives them back unless another execution still holds them. Run-wide state keyed by agent (the attached-machine permission policy, tool contexts) therefore always matches where the child runs; graph members a routed child initializes are registered with the policy of the machine they actually run on, and a routable child that resolves off attached machines drops its provisional attached-machine policy.

When run file sharing is requested for the root, no attached machine is offered, probed or accepted per call, since shared children must run on managed code. A call whose parent run was never placed (an allowSelf spawn) follows its parent agent's routed or passed-on machine.

Builds on #16756 (merged into dev). Depends on LibreChat-AI/agents#596 (the SDK host-argument extension point).

How it works

initialize.js  toLazySubagentMetadata(child)
  subagentCodeRouting.describe(child)          # packages/api/src/code/targets.ts
    resolveSubagentCodeTargets                 # sealed decision ∩ default/allowlist ∩ principal list ∩ live worker
    -> descriptor.subagentHostArgs             # run.ts puts it on SubagentConfig.hostArgs
    -> descriptor.codeExecutionChoices         # approval bindings, attached-policy install, skill priming, tool-output refs

SDK subagent call { subagent_type, machine?, workspace? }
  SDK checks the value against the child's enum and binds a digest to the execution
  child.resolve(context)
  descriptor.settle(context, resolveInputs)    # subagentCodeRouting.settleExecution: commit on handoff, give back on failure
    child.resolve(context)
      subagentCodeRouting.place(...)           # explicit choice > this agent's earlier route > routed parent's machine > default
        placeSubagentOnCodeTarget              # code_environment_id = M, code_environment_ids = [M]
      subagentCodeRouting.settle(placement, initializeAgent(routed agent))
      subagentCodeRouting.attach(...)          # asserts the route; records per-execution tool context and child route
      resolveGraphSubagentsFor                 # routed members via createRoutedGraphMemberLoader (packages/api)

createSubagentCodeRouting is a request-scoped session in packages/api that owns host-argument interpretation, inheritance, the unavailable-default decision, placement validation, route claims and per-execution state; initialize.js only constructs it and passes values through. Live target probes go through a process-wide queue of eight, below the shared status poller's ceiling, and honor the request or call signal.

The routed child is pinned by narrowing its agent document to the chosen machine (code_environment_id and a one-entry allowlist). Every existing routing site (initializeAgent, ToolService tool loading, provisioning) reads that document, so neither #16756's inheritance nor another admitted selection can move it, and no routing function needed a new parameter. A routed child's tool context is also recorded per execution and consulted first by tool loading, toolEndCallback artifact handling and file provisioning (the run-file provisioning wrapper now forwards the execution context).

captureCodeExecutionApprovalBinding folds an agent's default route and its codeExecutionChoices into one target, so a paused approval fails closed if any of those targets changed while it waited, and each agent contributes at most one binding entry.

Preflight decisions consider every machine a child may be routed to: the requested approval mode is validated against each attached default and choice (collectAttachedCodeApprovalPolicies; full access still requires all of them to permit it), and scheduled-run pause admission counts an agent as able to pause when any of its routes asks (collectAttachedCodeRoutePolicies). Historical skill files are primed for every route, including those of graph members only a lazy child initializes.

Type of change

  • Feature

Testing

Tested environments/configuration: Windows, Node 24.16.0, mongodb-memory-server; attached machines simulated with a control plane and worker status responses.

Automated tests:

  • packages/api/src/code/targets.spec.ts: enum construction per child (admitted + allowlisted + principal + ready only; allowlist exclusion; selection disabled keeps only the default; machine missing from the principal list; allowed-but-unadmitted machine never added; offline worker and missing workspace reported as unavailable; conversation-pinned child; sealed decision wins over a conflicting request); same inputs give the same route and session key; declaration shape and description length; selection and every refusal kind; re-validation on use (offline worker after describe, revoked allowlist entry, principal that lost the machine); omitted args leave the agent on its default; a routed child stays on its machine when its parent's machine would otherwise be inherited; inheritance from a routed parent, including through an agent without code; one machine per subagent per request; unavailable default refused only when no per-call route applies; approval binding covers and detects changed choices and folds 32 choices into one target; attached-policy install and skill priming for choice-only children; per-execution tool contexts for concurrent siblings; a route that initialization did not land on fails closed.
  • packages/api/src/code/targets.integration.spec.ts (mongodb-memory-server): a machine registered through the real code-environment registry and merged into the principal's config is offered to its owner, refused to another principal, and refused across tenants.
  • packages/api/src/code/targets.spec.ts (concurrency and lifecycle): concurrent omitted-machine calls from differently routed parents settle on one machine; workspace-only conflicts name workspace; a call canceled during target resolution or before attaching claims nothing; a failed initialization gives its machine back unless another call holds it; an attached claim is kept; shared-file runs offer and accept no attached machine.
  • api/server/services/Endpoints/agents/initialize.spec.js: a descriptor resolved with machine initializes the child pinned to that machine, a later call without arguments stays there, and an unadmitted machine is refused before initialization; a routed reviewer's graph members land on its machine; a lazily loaded graph member's default is held while it initializes; a failed default initialization does not block a retry on another machine.
  • packages/api/src/agents/__tests__/run-codeTools.test.ts: hostArgs reaches only the lazy configs that declare choices; through createRun's real PreToolUse hooks, a routed graph member takes its machine's policy and a routable child that resolved on managed code drops the attached policy.
  • packages/api/src/agents/hitl/byom.spec.ts: approval-mode validation and pause admission over per-call machines.
  • packages/api/src/agents/codeFilesSession.spec.ts: lazy graph-member choices are primed.
  • packages/api/src/files/provision/callback.spec.ts: a routed child provisions into its own execution's context.
  • Mutation check: removing the allowlist filter and the allowlist narrowing makes five target tests fail.
  • npx tsc --noEmit in packages/api, the --isolatedDeclarations declaration check on touched files, ESLint, Prettier and import sorting on touched files; the api initialize and resume specs pass.

The SDK-side behavior (schema, validation, execution binding, replay digest) is covered in LibreChat-AI/agents#596.

Screenshots / recordings

No user-facing change.

Risk / compatibility

SDK dependency and merge order. The feature needs @librechat/agents with SubagentConfig.hostArgs, SubagentResolveContext.hostArgs and SubagentHostArgumentError (LibreChat-AI/agents#596, the first release after 4.0.2). This PR does not bump the dependency: the declarations are gated on the installed SDK exporting SubagentHostArgumentError, and the host-argument types are declared locally, so on 4.0.x nothing is advertised and behavior is unchanged. Order: (1) merge and release agents#596, (2) bump @librechat/agents in this PR to that release, and drop the local types and the test stand-in for the SDK error class. #16756 is already merged into dev.

Approval bindings. Bindings now also cover per-call choices, so a pause whose child had alternative machines fails closed if one of those machines changes (or goes offline) while the approval waits. A pause captured by a replica without this change and resumed on one with it also fails closed when the child has choices; the user retries. Agents without choices keep their existing binding entry.

Approval modes. A requested full-access mode now also requires every attached machine a child may be routed to per call to permit it.

One machine per subagent per request. A parent cannot fan the same subagent out to two machines in one turn; the second call is refused with a machine error. Different subagents can run on different machines.

Not included. A worktree argument (.worktrees/<name>) is left out: there is no host-side default working directory to route a child into a linked worktree, and workspace tools already accept a per-command cwd, so the parent can name the worktree in the task description. A child that is also a root (handoff/parallel) agent is initialized eagerly for that role and keeps its existing route, since the SDK only accepts host arguments on lazy configs.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published
  • Required documentation PR: N/A

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 1 of 4 on head 0b92692.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-05T07:22:25.910822Z f274737 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b926923e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +26 to +28
* Host argument declaration accepted by `@librechat/agents` subagent configs
* (`SubagentConfig.hostArgs`, added after 4.0.2). Declared locally so this
* package typechecks against older SDKs, which ignore the field.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Upgrade the SDK that implements host arguments

The checked-in lockfile still installs @librechat/agents 4.0.1 (package-lock.json lines 9219-9222), while this code states that hostArgs was added after 4.0.2. Consequently an npm ci deployment has no SubagentHostArgumentError, isSubagentHostArgsSupported() is always false, and describe() never advertises the machine or workspace arguments, leaving the feature entirely inactive.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional for now, and the PR stays a draft until it is resolved. The SDK side is LibreChat-AI/agents#596, which is not released yet, so there is no version to bump to. Until then the declarations are gated on the installed SDK exporting SubagentHostArgumentError, which keeps this change inert (no schema change, unchanged behavior) on 4.0.x rather than breaking it. The merge order in the description is: release agents#596, merge #16756, then bump @librechat/agents here, drop the local types and the test stand-in, and mark ready.

Comment on lines 139 to +142
toolNames: string[],
agentId?: string,
signal?: AbortSignal,
executionContext?: SubagentExecutionContext,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward execution context through the provisioning wrapper

When run-file sharing is inactive, createChatRunFileBindings.wrapProvision calls its fallback with only (names, agentId, signal) and drops the fourth executionContext argument. For repeated or concurrent calls of the same subagent routed to different machines, this makes resolveExecutionContext miss and provisioning falls back to the single per-agent context, so attachments are uploaded to one sibling's machine while the selected sibling executes on another machine.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 034ee0f: wrapProvision now forwards the execution context to the fallback when run-file sharing is inactive, so resolveExecutionContext sees the routed child's execution.

Comment thread packages/api/src/agents/execution.ts Outdated
Comment on lines +154 to +156
for (const agent of agents) {
const context = agent?.codeExecutionContext;
if (context?.statefulSessions !== true) {
continue;
const agentId = agent?.id ?? null;
const contexts = [agent?.codeExecutionContext, ...(agent?.codeExecutionChoices ?? [])];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep generated approval bindings within the accepted limit

Adding every codeExecutionChoices entry can produce more than 128 targets—for example, five reachable subagents each allowed on the valid maximum of 32 conversation machines. captureCodeExecutionApprovalBinding() persists all of them, but isCodeExecutionApprovalBinding() rejects any binding over MAX_CODE_EXECUTION_APPROVAL_TARGETS, so a stateful-code approval created for such a valid graph can never resume and always reports that the execution target changed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 034ee0f: each agent's per-call choices fold into a single approval target (a hash over the sorted choice hashes), so an agent contributes at most two targets regardless of how many machines it may use. A test binds five agents with 32 choices each into five targets and re-asserts it.

Comment thread packages/api/src/code/targets.ts Outdated
if (!isSubagentHostArgsSupported() || flags.statefulCodeSessions !== true) {
return {};
}
const { targets } = await resolveSubagentCodeTargets(paramsFor(agent, flags));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse worker capability checks across subagent descriptors

Each unique lazy subagent calls resolveSubagentCodeTargets() independently during startup, and that function performs a live worker-status request for every admitted machine without request-scoped reuse. A valid graph of 50 subagents and 32 selections therefore performs up to 1,600 largely duplicate bridge requests before initialization completes, adding substantial latency and load; cache each worker capability result for the request while still deriving agent-specific contexts separately.

AGENTS.md reference: AGENTS.md:L186-L188

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. The live checks go through the module-level createCodeBridgeStatusPoller, which coalesces in-flight requests per worker and caches each result for 2 s (and remembers a ready lease). Descriptors are built concurrently (Promise.all over children), so N children over the same M machines issue about M bridge requests, not N x M; agent-specific contexts are still derived per child. A second request-scoped cache would duplicate that layer.

Comment on lines +1666 to +1669
/** Assigned rather than spread so the field typechecks against SDKs that predate it. */
return child.subagentHostArgs == null
? config
: Object.assign(config, { hostArgs: child.subagentHostArgs });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add host arguments to eagerly initialized subagents

hostArgs is attached only by createLazySubagentConfig, but buildLazySubagentDescriptors returns an already initialized config whenever the same child is also a handoff/parallel agent. That config takes the eager subagentAgentConfigs branch in buildSubagentConfigs, which never adds host arguments or a per-call resolver, so this valid overlap configuration silently loses the machine and workspace choices even after the SDK dependency is upgraded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as a documented limitation. A child that is also a root (handoff/parallel) agent is initialized eagerly for that role before any subagent call exists, and the SDK only accepts host arguments on lazy configs (it rejects them on eager ones), because only a resolver can act on a per-call value. Routing such an overlap per call would mean re-initializing an agent the run already owns, which is out of scope here; that child keeps its existing route.

context.signal,
);
throwIfAborted(context.signal);
const routedAgent = placement.agent;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate routed placement to nested subagent inheritance

When a parent routes a child to a non-default machine, only the child's copied agent document and per-execution tool context are updated. The request-wide req.codeWorkspaceInheritance map was computed earlier from saved/default routes and is not updated per execution, so if this routed child delegates to a grandchild without an explicit machine, the grandchild inherits the original machine rather than the machine its actual parent is running on, potentially reading or modifying the wrong workspace.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 034ee0f: a routed child's execution records the machine it was routed to, and its own children (whose resolver context carries the parent execution as parentRunId) inherit that machine when they may reach it, ahead of the request-wide map. An explicit choice on the grandchild still wins, and a grandchild that cannot reach the parent's machine keeps its own authorized route. Covered by a test.

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 2 of 4 on head 034ee0f.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 034ee0f77a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +413 to +419
const resolution = await resolveSubagentCodeTargets(paramsFor(agent, flags));
const target = requested
? selectSubagentCodeTarget(hostArgs, resolution)
: resolution.targets.find((candidate) => candidate.environmentId === parentEnvironmentId);
return target == null
? { agent }
: { agent: placeSubagentOnCodeTarget(agent, target), target };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve dynamic routes across non-code intermediaries

When a dynamically routed parent spawns an agent without stateful code, resolveSubagentCodeTargets() returns no targets, so this returns the child unchanged and never records parentEnvironmentId. If that child then spawns a code-enabled grandchild, environmentFor(context.parentRunId) cannot recover the dynamic route and the grandchild falls back to the statically computed/default machine; this breaks the existing resolveCodeWorkspaceInheritance() behavior that non-code agents pass their parent's machine through and can send the grandchild to the wrong workspace.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 092a13c: an agent that runs no stateful code now records its routed parent's machine for its own execution, so a code-enabled grandchild reached through it inherits that machine (when it may reach it), matching #16756's pass-through rule. Covered by a parent -> non-code writer -> tester test.

Comment on lines 1592 to 1593
if (metadata.codeWorkspaceUnavailable && hostArgs == null) {
throw new CodeWorkspaceSelectionError(metadata.codeWorkspaceUnavailable);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let inherited routes recover an unavailable default

When the child's saved/default worker is unavailable but a dynamically routed parent is running on another allowed, reachable machine, metadata records codeWorkspaceUnavailable before the per-call parent route is known. An omitted machine is precisely how the child requests inheritance, but this guard throws before subagentCodeRouting.place() can select the parent's healthy target, so nested inheritance fails unless the model redundantly supplies an explicit host argument. Keep this availability decision in the TypeScript routing boundary and evaluate the inherited route before rejecting the call.

AGENTS.md reference: AGENTS.md:L77-L81

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 092a13c: the unavailable-default decision moved into the routing session. place first tries an explicit call choice, then the machine a routed parent runs on, and only refuses with the default route's CodeWorkspaceSelectionError when neither applies; the descriptor no longer throws before routing. Covered by a test.

Comment on lines +1377 to +1382
const placement = await waitForAbort(
subagentCodeRouting.place(
agent,
codeFlags ?? {},
getSubagentHostArgValues(context),
subagentPlacements.environmentFor(context.parentRunId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move routing behavior out of the legacy CJS initializer

This change adds route selection, host-argument interpretation, placement validation, and execution-scoped routing state directly to the legacy /api initializer rather than leaving it as wiring. Extract the orchestration into a dependency-injected TypeScript service under packages/api and have this file call that boundary; otherwise this substantial backend behavior remains outside the repository's required module boundary.

AGENTS.md reference: AGENTS.md:L73-L81

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 092a13c: createSubagentCodeRouting in packages/api/src/code/targets.ts is now a request-scoped session that owns host-argument interpretation, parent-route inheritance, the unavailable-default decision, placement validation, per-execution tool contexts and the routed-config check. initialize.js only constructs it and passes values through (describe, place, attach, getToolContext, isRouted).

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 3 of 4 on head 092a13c.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 092a13c176

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +476 to +480
if (requested) {
const resolution = await resolveSubagentCodeTargets(paramsFor(agent, flags));
const target = selectSubagentCodeTarget(hostArgs, resolution);
if (target != null) {
return routeTo(agent, target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install the attached-code policy before routing a fallback

When a lazy child's default workspace is unavailable but another advertised machine is healthy, this branch allows the child to initialize on that attached machine even though its descriptor has no codeExecutionContext. If no root or other descriptor initially targets an attached environment, createRun sees an empty attachedCodeEnvironmentAgentIds set, skips both assertAttachedCodeEnvironmentApprovalSupported and creation of createAttachedCodeEnvironmentPolicyHook (packages/api/src/agents/run.ts), and the later resolved-agent callback can only mutate sets that no installed hook reads. Thus this fallback path can execute against an attached machine without the required machine permission/approval policy; alternate attached targets must cause that policy hook and HITL capability check to be installed before the run starts.

AGENTS.md reference: AGENTS.md:L186-L189

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd35232: collectAttachedCodeEnvironmentAgentIds now counts a descriptor whose per-call choices include an attached machine, so the attached-machine policy hook and the HITL capability check are installed before the run starts even when no default route is attached. The resolved agent's machine settings are registered on resolution as before. Covered by a test.

Comment on lines +504 to +511
if (executionId && placement.childEnvironmentId != null) {
childRoutes.set(executionId, placement.childEnvironmentId);
}
if (placement.target == null || !executionId) {
contexts.set(agentId, toolContext);
return;
}
routedContexts.set(executionId, { agentId, toolContext });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Key attached-machine policy by execution

When concurrent calls invoke the same child on machines with different permission settings, tool contexts are separated by execution here, but createRun still stores attachedCodeEnvironmentSettings only by agent ID. Each lazy resolution overwrites that single entry in registerResolvedMCPToolAliases, while createAttachedCodeEnvironmentPolicyHook looks it up using only executingAgentId, so the last-resolved sibling's policy governs both executions; for example, machine B's allow can replace machine A's deny or ask. Preserve the selected policy per execution and consult the trusted execution context during PreToolUse.

AGENTS.md reference: AGENTS.md:L186-L189

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd35232 by making the per-agent policy correct rather than re-keying it: the routing session now keeps one machine per subagent for the whole request (the same "one route per subagent" rule #16756's inheritance uses). A later call that omits the machine follows the subagent's earlier per-call route, and a call that would move it to another machine is refused with a model-visible machine error, so two executions of one agent can never run on machines with different permission settings and the agent-keyed policy entry always matches. Covered by a test.

Comment on lines +1250 to +1251
subagentHostArgs,
codeExecutionChoices,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prime historical skill files for every routable choice

When a lazy child is routed to a non-default machine and the conversation history references a file-backed skill, this new choice is not included by collectCodeExecutionProfileRoutes, which traverses codeExecutionContext but ignores codeExecutionChoices. Consequently primeInvokedSkillsForProfiles uploads and seeds the skill files only for the descriptor's default route (or for no route when that default is unavailable), and the selected alternate route's codeSessionKey starts without the historical skill files. Include every choice in profile collection or prime the selected profile during lazy resolution.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd35232: collectCodeExecutionProfileRoutes now adds every codeExecutionChoices route (with that route's session key), so historical skill files are primed for each machine a child may be routed to. Covered by a test.

Comment on lines +1250 to +1252
subagentHostArgs,
codeExecutionChoices,
subagentCodeFlags: { statefulCodeSessions, statefulCodeEnvironment },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enable output references for fallback-routed code children

When the only code-capable agent is a lazy child whose default workspace is unavailable but an alternate machine is healthy, the descriptor keeps codeEnvAvailable false while these raw flags still let place() initialize it on the alternate route. anyAgentHasCodeEnv therefore returns false before the SDK run is compiled and leaves toolOutputReferences disabled; once resolved, the child can register the bash tool, but its {{tool<idx>turn<turn>}} references pass to the shell unsubstituted, exactly the failure the gate in packages/api/src/agents/run.ts is intended to prevent. Treat a reachable code choice as code availability for this run-wide gate.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd35232: the run-wide tool-output-reference gate treats a descriptor with per-call choices as code-capable, so a child that can only run code through a choice still gets substitution.

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

This is the final review for this PR (pass 4 of 4) of head bd35232. Please flag only blocking issues: correctness, security, or data loss.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: bd35232d1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila marked this pull request as ready for review October 5, 2026 01:33
@danny-avila
danny-avila added this pull request to stack #16758 October 5, 2026 01:33
Base automatically changed from danny-avila/subagent-inherit-workspace to dev October 5, 2026 02:19
A parent agent can now pass optional `machine` / `workspace` arguments on a
subagent call to pick where the child runs, using the host-argument
extension point added to @librechat/agents.

- Each lazy child declares enums built only from machines it may reach:
  admitted in the conversation's sealed codeWorkspaces, on its default or
  allowlist, in the requesting principal's tenant-scoped list, and served by
  a ready worker. A child the conversation pinned to a machine offers only
  that machine; a child with no reachable machine declares nothing.
- On use, a request-scoped routing session in packages/api re-validates the
  choice against the current request and live worker status and refuses an
  invalid one with a typed, model-visible error instead of falling back. A
  routed child is pinned to the chosen machine at every routing site,
  overriding parent inheritance.
- Omitted arguments keep inherit-by-default routing, extended so a child
  follows the machine a per-call-routed parent runs on (passing through
  agents without code). A subagent keeps one machine for the whole request.
- A child whose default workspace is unavailable stays guarded unless it has
  per-call choices; then it stays callable and its description asks the
  parent to pick a listed machine.
- Routed children keep their tool context per execution for tool loading,
  provisioning and artifact handling.
- Paused-approval bindings, the attached-machine policy hook, skill-file
  priming and tool-output references account for per-call choices.

The declarations are gated on the installed SDK exporting
SubagentHostArgumentError, so this change is inert until LibreChat depends on
an @librechat/agents release that includes it.
@danny-avila
danny-avila force-pushed the danny-avila/subagent-workspace-choice branch from bd35232 to 45ad7a6 Compare October 5, 2026 02:34
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 5 of 5 on head 45ad7a6.

@lia-by-librechat lia-by-librechat Bot changed the title 🧭 feat: Let a Parent Choose Its Subagent's Code Machine Per Call 🚕 feat: Add Per-Call Subagent Machine Selection Oct 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45ad7a6ebb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
if (unavailableReason != null) {
throw new CodeWorkspaceSelectionError(unavailableReason);
}
return { agent };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reserve default routes before asynchronous initialization

When two parallel calls target the same lazy child and one omits machine while the other requests an alternate, this default branch returns without reserving the child's default in routeByAgent; that reservation occurs only in attach() after the asynchronous initializeAgent call. The alternate call can therefore claim another machine while the default call is initializing, allowing both executions to resolve on different machines, after which registerResolvedMCPToolAliases overwrites the attached-machine permission settings keyed only by agent ID and one execution may receive the other's allow/deny/ask policy. Fresh evidence on this head is that the new one-machine enforcement still leaves the default route unclaimed across that initialization window; reserve it synchronously in place() or key the policy by execution.

AGENTS.md reference: AGENTS.md:L186-L189

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9d2a990: place now reserves the default route synchronously, computing it with the same resolveCodeExecutionContext inputs initializeAgent uses (including #16756's request-scoped inheritance via getInheritedEnvironments), so a concurrent call naming another machine is refused while the default call initializes; naming the same machine still works. Covered by tests for a plain default and an inherited default.

Comment on lines 1600 to 1603
if (!subagentCodeRouting.isRouted(context.executionId)) {
graphMemberConfigsById.set(config.id, config);
}
await resolveGraphSubagentsFor(config, context.signal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass routed placement into graph-member initialization

When a lazy child is explicitly placed on a non-default machine and then invokes one of its subagents.graphs, the per-call placement stops at the child: resolveGraphSubagentsFor initializes each member through loadGraphMember with only { signal }, so no parentRunId, codeFlags, or routed environment reaches subagentCodeRouting.place. The member consequently uses req.codeWorkspaceInheritance, which was computed from the parent's saved/default route, even though static inheritance explicitly treats graph members as children; it can therefore read or modify a different workspace from the routed parent. Fresh evidence on this head is that routed configs avoid the global cache here, but their graph-member initialization still never consumes placement.childEnvironmentId; propagate the routed execution context when resolving the graph.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9d2a990: when a per-call-routed subagent resolves its subagents.graphs, members are initialized with the routed execution as their parent (loadRoutedGraphMember), so each follows the routed machine when it may reach it, with its own code flags; such routed members are not put in the shared graph-member cache. A member that already initialized on its own route this request keeps it (one machine per agent per request). Covered by an initialize test where a routed reviewer's graph member lands on the reviewer's machine.

…ph Members

- Reserve a subagent's default machine when it is placed, as
  initializeAgent will resolve it (including parent inheritance), so a
  concurrent call cannot claim another machine during initialization.
- Graph members spawned by a per-call-routed subagent follow its machine
  when they may, unless they already initialized on their own route this
  request.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 6 of 6 on head 9d2a990.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d2a990190

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
const { targets } = await resolveSubagentCodeTargets(paramsFor(agent, flags));
const target = targets.find((candidate) => candidate.environmentId === inherited);
if (target != null) {
return routeTo(agent, target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck inherited-route conflicts after resolution

The new default-route reservation does not cover two concurrent omitted-machine calls that reach the same child from parents routed to different machines: both can compute inherited before either route is claimed, await target resolution, and then this branch commits its target without another conflict check. Both executions can consequently initialize on different machines, violating the one-machine-per-agent invariant; because registerResolvedMCPToolAliases stores attached-machine permission settings by agent ID, the later resolution can make one execution use the other machine's allow/deny/ask policy. Recheck or atomically reserve the inherited target after the await.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aaeef87. place now re-reads the inherited route after the resolveSubagentCodeTargets await and only then commits it, so if a concurrent call settled this subagent meanwhile, the later call follows that machine (or falls back to the reserved default) instead of claiming its own parent's. Covered by a test where two omitted-machine calls from parents routed to different machines settle on one machine.

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +562 to +563
if (conflicts(agent.id, target.environmentId)) {
throw createSubagentHostArgumentError(SUBAGENT_MACHINE_ARG, 'unavailable');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Attribute workspace-only conflicts to the workspace argument

When an earlier call has claimed one machine and a later call supplies only the workspace for another machine, target selection succeeds and this conflict branch always rejects machine. Because workspace is explicitly advertised as an alternative argument and the SDK exposes argument-specific recovery guidance, the parent is told to change or omit an argument it never supplied and may repeatedly retry the same invalid workspace choice. Reject workspace when machine is absent, while retaining machine for machine-based or combined selections.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aaeef87: a conflict is attributed to workspace when the call supplied only workspace, and to machine for machine-based or combined selections. Covered by a test.

Comment on lines +1754 to +1757
if (!subagentCodeRouting.isRouted(executionId)) {
graphMemberConfigsById.set(memberId, config);
}
return config;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register the selected policy for routed graph members

When a routed lazy child sends a graph member to an alternate machine, this newly initialized member is returned without being passed through the resolved-agent registration callback; createLazySubagentConfig only registers the outer child. The run-wide attachedCodeEnvironmentSettings entry for the member therefore remains the policy captured from its lightweight metadata and default machine, so PreToolUse can apply that machine's allow decision while the member is actually executing on an alternate machine configured as deny or ask. Register each dynamically initialized routed graph member's selected configuration before exposing its graph inputs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b55612a / c78629f: the lazy resolver now passes each graph member the resolved child initialized through the resolved-agent callback before building the child's inputs, so the run-wide attached-machine policy entry for a routed member is the one of the machine it actually runs on. The test drives createRun's real PreToolUse hook: the member flips from its metadata machine's allow to its routed machine's deny once the parent resolves.

…embers

- Re-read a subagent's inherited route after resolving targets, so two
  concurrent calls from differently routed parents settle on one machine.
- Attribute a workspace-only conflict to the workspace argument.
- Register graph members a routed subagent initialized, so each gets the
  attached-machine policy of the machine it actually runs on.
- Re-read a subagent's inherited route after the target resolution await,
  so two concurrent omitted-machine calls from differently routed parents
  settle on one machine.
- Attribute a workspace-only conflict to the workspace argument.
…h createRun

- Register a resolved child's graph members inline in the lazy resolver
  instead of through an exported generic helper whose test did not
  typecheck.
- Drive the check through createRun's real PreToolUse hook: a member
  routed to a deny machine flips from its metadata machine's allow to deny
  once its parent resolves.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 7 of 7+ on head c78629f.

…nitializes

- Pass a graph member's code flags when it initializes on its default
  route, so the routing session reserves that machine before the
  asynchronous initialization instead of only after it, and a concurrent
  call cannot place the same agent on another machine meanwhile.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c78629f5e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +194 to +196
const routable =
agent.codeExecutionChoices?.some((choice) => choice.environmentType === 'attached') === true;
if (agent.id && (agent.codeExecutionContext?.environmentType === 'attached' || routable)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate approval modes against routable machine policies

When a lazy child has no usable default codeExecutionContext but has an attached entry in codeExecutionChoices, this adds the child to the attached-agent set while collectAttachedCodeEnvironmentPolicySettings() still has no settings for it. createRun() therefore validates a requested acceptEdits or fullAccess mode against the config-less fallback before the child is routed, which permits only ask, so a fallback-routable child fails run creation even when its alternate machine explicitly permits the requested mode. Include the choice policies in initial approval-mode validation, especially for descriptors whose default route is unavailable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6ce0fa: the requested mode is now validated against collectAttachedCodeApprovalPolicies, which includes each agent's default attached route and every attached machine in its codeExecutionChoices, in createRun and in both client checks. A child whose default is unavailable but whose per-call machine permits accept edits no longer fails run creation; full access still requires every machine it may run on to permit it, as for default routes. The agent-keyed policy map used by the PreToolUse hook is unchanged. Covered by tests.

…y Use

- Collect each agent's default attached route and each attached machine a
  parent may route it to per call, and validate the requested approval
  mode against that set in createRun and both client checks.
- A child whose default is unavailable but whose per-call machine permits
  accept edits no longer fails run creation; full access still requires
  every such machine to permit it.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 8 of 8+ on head a6ce0fa.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6ce0faf0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 4565 to +4567
const codeApprovalMode = resolveAttachedCodeApprovalMode(
this.options.req.body.codeApprovalMode,
attachedCodeEnvironmentSettings,
collectAttachedCodeApprovalPolicies(topLevelAgents),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include alternate policies in scheduled pause admission

When a lazy child has a permissive attached default but a routable alternate machine whose policy can return ask, approval-mode validation now examines both routes, but buildAttachedCodeEnvironmentAdmissionHooks still receives the default-only attachedCodeEnvironmentSettings. On a scheduled run, runCanPause can therefore remain false and skip the Redis/durable-checkpointer requirements; if the model selects the alternate, lazy registration changes the runtime hook to ask only after that preflight, leaving the resulting pause on an unsupported topology. Build admission metadata conservatively from every codeExecutionChoice, not only the default context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ab9e7c7: buildAttachedCodeEnvironmentAdmissionHooks now also takes each agent's per-call route policies (collectAttachedCodeRoutePolicies: default route plus every attached codeExecutionChoice), and an agent counts as able to pause for a category when any of those machines asks. The client's scheduled-run admission passes them, so a permissive default no longer hides an asking alternate and the Redis/durable-checkpointer requirements apply. Covered by a test.

Comment on lines +100 to +101
for (const choice of agent.codeExecutionChoices ?? []) {
addRoute(choice, choice.codeSessionKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Traverse lazy graph-member metadata when priming choices

When a graph member is reachable only through a lazy child's subagents.graphs, it exists at startup only in subagentGraphMemberMetadata, which enqueueCodeFilesChildren does not traverse. Consequently this new choice loop never sees that member's codeExecutionChoices, so a historical file-backed skill is not primed for any machine the member may run on and its first code call lacks the historical skill files. Fresh evidence beyond the earlier priming fix is that other run-wide walkers now explicitly traverse subagentGraphMemberMetadata, while this queue still omits it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ab9e7c7: enqueueCodeFilesChildren now walks subagentGraphMemberMetadata, so collectCodeExecutionProfileRoutes primes historical skill files for every route (default and per-call choices) of a graph member that only a lazy child initializes. Covered by a test.

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +559 to +560
const resolution = await resolveSubagentCodeTargets(paramsFor(agent, flags));
const target = selectSubagentCodeTarget(hostArgs, resolution);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop committing routes after canceled resolution

When a subagent call is canceled while resolveSubagentCodeTargets() is awaiting live worker checks, the outer waitForAbort rejects but does not cancel this promise; once the checks finish, routeTo() still mutates routeByAgent and claims the machine for an execution that never initialized. If the parent run remains active and retries that child on another machine, the stale claim rejects the retry as a route conflict. Pass the call signal into routing or recheck it immediately after each target-resolution await before mutating session state.

AGENTS.md reference: AGENTS.md:L45-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ab9e7c7: place rechecks the call's signal after each target-resolution await and throws before touching session state, so a call canceled during the live worker checks never claims a machine and a retry on another machine is not refused. Covered by a test.

- Build scheduled pause admission from every attached machine a parent
  may route an agent to, so a permissive default does not hide a
  per-call machine that asks.
- Walk lazy graph-member metadata when collecting execution routes, so
  historical skill files are primed for every machine such a member may
  run on.
- Stop placing a canceled call after its target resolution resolves, so
  it never claims a machine for an execution that will not initialize.
…ed Member Configs

- A call whose parent run was never placed (an allowSelf spawn reuses its
  routed inputs) inherits the routed claim of its parent agent, which keeps
  one machine per request.
- Never share a graph member initialized for a routed parent's execution
  through the graph-member cache, so a failed resolution leaves no config
  behind for a route it gave back.
- Race lazy target discovery with the request signal.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 15 of 15+ on head dcc783b.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcc783b7bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +673 to +674
const claimed = routeByAgent.get(context.parentAgentId);
return claimed?.routed === true ? (claimed.environmentId ?? undefined) : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve pass-through routes for non-code self-spawns

When a routed code agent delegates through a non-stateful child that then uses allowSelf, the self run has no childRoutes entry, so this fallback must recover the route through parentAgentId. However, attach() records that non-code child in routeByAgent as routed: false with no code environment, causing this line to return undefined; a code-enabled grandchild invoked by the self-spawn then falls back to its saved/default machine instead of the routed ancestor's workspace. Fresh evidence beyond the separate non-code-intermediary and self-spawn fixes is their composition: the pass-through route exists only for the original child execution and is lost at the self boundary. Preserve the inherited childEnvironmentId per agent for this fallback.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f63b34e: attach now records, per agent, the machines an agent without stateful code passed on, and the parentAgentId fallback uses that when the agent has no routed claim. It follows the pass-through only when the agent passed on exactly one machine this request (a non-code agent may run under differently routed parents); otherwise the call keeps #16756's static inheritance. Covered by a routing test for a self-spawn of a non-code writer, including the ambiguous case.

…Code

- Record the machine each agent without stateful code passes on, and let a
  call from that agent's unplaced run (an allowSelf spawn) follow it when
  the agent passed on exactly one machine this request.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 16 of 16+ on head f63b34e.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f63b34eca5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +776 to +779
if (placement.target == null && placement.childEnvironmentId != null) {
const passedOn = passedOnByAgent.get(agentId) ?? new Set<string>();
passedOn.add(placement.childEnvironmentId);
passedOnByAgent.set(agentId, passedOn);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll back pass-through routes after failed resolution

When a non-code child is initialized under a routed parent, this records the inherited machine before the lazy child's graph resolution finishes, but settleExecution() only rolls back route holds on cancellation or failure and never removes this passedOnByAgent entry. If that child is later retried under an unrouted parent and self-spawns, parentRouteOf() can recover the stale machine and send a code-enabled grandchild to the workspace from the failed attempt. Track this pass-through per pending execution and commit or discard it with the rest of that execution's routing state.

AGENTS.md reference: AGENTS.md:L45-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f3f8b82: a pass-through recorded during a call is now kept with that call's other routing state (grouped by its execution and parent run): settleExecution records it only when the resolution deciding it succeeds and drops it on failure or cancellation; a pass-through outside any call is recorded directly. Covered by the self-spawn routing test, which now fails a first writer resolution and checks that a self-spawn does not follow it.

Comment on lines +697 to +699
if (!isSubagentHostArgsSupported() || flags.statefulCodeSessions !== true) {
return {};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Upgrade the agents SDK before gating the new host arguments

With the committed dependency set, this condition always returns early: package-lock.json pins @librechat/agents 4.0.1, while this file says hostArgs was added after 4.0.2, and the new test explicitly mocks SubagentHostArgumentError because the installed SDK does not export it. Consequently descriptors never receive machine or workspace, codeExecutionChoices remains empty, and the per-call selection feature is disabled in every npm ci deployment. Update the package and lockfile to a release that supplies the host-argument API instead of testing only against the mock.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the earlier thread on this point: intentional until LibreChat-AI/agents#596 is released. There is no published @librechat/agents version with the host-argument API to bump to yet, so the declarations are gated on the installed SDK exporting SubagentHostArgumentError and the feature is inert on 4.0.x (no schema change, unchanged behavior). The merge order in the description is: release agents#596, then bump @librechat/agents in this PR and drop the local types and the test stand-in.

- Record the machine an agent without code passes on with the rest of its
  call's routing state: kept when the resolution deciding it succeeds,
  dropped when it fails, so a self-spawn never follows a failed attempt.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 17 of 17+ on head f3f8b82.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3f8b82086

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +826 to +827
if (!contexts.has(agentId)) {
contexts.set(agentId, toolContext);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace graph-member contexts after a failed route

When a routed lazy child initializes one graph member and then its overall resolution fails or is canceled while loading another member, settleExecution() releases the route claims but leaves the completed member's tool context in contexts. A retry may therefore select another machine, yet this guard preserves the first machine's context; graph members are registered under a synthetic initialization ID, so event-driven tool loading can fall back to this per-agent entry and execute or provision files against the abandoned machine while approval policy reflects the retry's machine. Roll back that entry with the failed resolution or replace it when the prior route was released.

AGENTS.md reference: AGENTS.md:L45-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 331c2e7: when a still-pending placement seeds the per-agent tool context, the routing session records it as the entry's owner together with the value it replaced; when that placement's resolution fails or is canceled the entry is restored (or removed), so a retry on another machine seeds its own context instead of inheriting the abandoned one. Covered by a routing test where a routed reviewer and its routed graph member attach and the resolution then fails.

Comment on lines +859 to +860
settleHoldsOf(context?.executionId, true);
return resolved;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Commit routes only after the SDK resolver finishes

When cancellation arrives after child.resolve(context) finishes but before createLazySubagentConfig.resolveAgentInputs performs its post-await abort check, this commits the child and graph-member claims and removes their abort listeners, then the outer resolver throws without exposing the child. A retry in the still-active parent run cannot select another machine because the canceled call's route is now permanent. Fresh evidence after the graph-resolution fix is that settleExecution() still ends before the SDK resolver's cancellation check and input-building steps; keep the claims pending until that outer resolver succeeds.

AGENTS.md reference: AGENTS.md:L45-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 331c2e7: lazy descriptors now carry a settle hook, and createLazySubagentConfig runs its whole resolveAgentInputs body (the host resolver, the post-await abort check, resolved-agent registration and input building) inside it. The routing state (routes, per-agent contexts, the shared graph-cache entry) is therefore committed only when the inputs are handed to the SDK and given back if anything before that throws. Covered by a createRun test where the call is canceled right after the host resolver returns, and by the initialize tests, which now select through the hook.

- Wrap the run's whole lazy resolution, through the inputs handed to the
  SDK, in the descriptor's settle hook, so a cancel after the host
  resolver returns still gives the call's routes back.
- Roll back the per-agent tool context a pending placement set (restoring
  the entry it replaced) when its resolution fails, so a retry on another
  machine seeds its own context.
- Drop a failed call's shared config from the graph cache through the
  same settle hook.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 18 of 18+ on head 331c2e7.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 331c2e77dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +852 to +854
routedContexts.set(executionId, { agentId, toolContext });
if (!contexts.has(agentId)) {
seed();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve context for surviving parallel placements

When two parallel executions of the same routable child settle on the same machine, the first attachment seeds agentToolContexts, while the second skips this block because the agent key already exists and therefore never becomes a context owner. If the second resolution succeeds and the first later fails or is canceled, cleanup for the first placement deletes the shared entry even though the successful execution remains; callbacks without execution ancestry then fall back through agentToolContexts and can load or provision tools with no context or the primary agent's context. Track every live placement using the entry, or reseed it from a surviving placement before deleting it.

AGENTS.md reference: AGENTS.md:L45-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faded6d: every pending placement that uses an agent's per-agent tool context is now tracked with its own context (whether it set the entry or found it present). A committed placement sets the entry to its own context; a failed one hands the entry to a surviving placement; only when the last user fails is the earlier entry restored (or removed). Covered by a routing test with parallel placements in both orders.

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +866 to +868
}
const routed = routedContexts.get(entry.subagentRunId);
return routed?.agentId === agentId ? routed.toolContext : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Continue past unplaced self-spawn ancestry entries

When a per-call-routed child uses allowSelf, the self execution appears as the innermost ancestry entry with the same agent ID but is never added to routedContexts; this immediate return therefore yields undefined instead of continuing to the outer routed execution. With parallel invocations of the same saved agent, the per-agent fallback may belong to a sibling invocation, so the self-spawn can load tools, provision files, or attribute artifacts using that sibling's context rather than its own routed parent's context. Continue scanning matching ancestry entries until a routed record is found.

AGENTS.md reference: AGENTS.md:L45-L47

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faded6d: getToolContext keeps scanning outward past an ancestry entry of the same agent that has no routed record (an unplaced allowSelf run) until it finds the routed run that spawned it. By the one-machine-per-agent rule the enclosing routed run's context is the right one for the self run. Covered by a test.

Comment on lines +236 to +238
const results = await Promise.all(
authorized.map((selection) => resolveCandidate(params, selections, selection)),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound target probes below the shared poller limit

On a cold cache, resolving choices for multiple lazy descriptors concurrently can start more than 32 distinct worker probes here. The shared status poller rejects every request above its global maxConcurrent = 32 ceiling with CodeBridgeStatusError('busy'), and resolveCandidate() catches that rejection and records the healthy machine as unavailable; because descriptor construction also runs in parallel, even agents within the normal per-agent choice limit can nondeterministically lose advertised routes. Queue or concurrency-limit these probes instead of launching all authorized selections at once.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faded6d: resolveSubagentCodeTargets now runs candidate probes through a process-wide limiter of eight in flight, well below the shared status poller's ceiling of 32, so concurrent descriptor construction queues instead of drawing busy rejections that would be recorded as unavailable machines. Covered by a test that resolves twelve machines and asserts the peak in-flight probes.

…et Probes

- Track every pending placement that uses an agent's per-agent tool
  context: a committed one sets it, a failed one hands it to a surviving
  placement, and only the last failure restores the earlier entry.
- Let an unplaced run of an agent (an allowSelf spawn) defer to the routed
  run of the same agent that encloses it when looking up tool contexts.
- Queue live target probes at eight in flight, below the shared status
  poller's ceiling, so concurrent descriptors never misreport a healthy
  machine as unavailable.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 19 of 19+ on head faded6d.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: faded6dbec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1764 to +1768
const agent = await waitForAbort(db.getAgentWithVersionCount({ id: memberId }), graphSignal);
if (!agent || !(await hasSubagentViewAccess(agent, memberId, graphSignal))) {
skippedAgentIds.add(memberId);
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move routed graph-member behavior into packages/api

This new path performs database loading, view authorization, routing initialization, and failure policy directly inside legacy /api, rather than leaving this file as wiring. Move the routed graph-member workflow into a TypeScript service under packages/api and inject its dependencies here so the new backend behavior respects the repository's module boundary.

AGENTS.md reference: AGENTS.md:L73-L81

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a25692: the routed graph-member workflow (shared-member reuse, skip list, view check, execution identity, failure policy) is now createRoutedGraphMemberLoader in packages/api/src/agents/lazySubagents.ts, with unit tests; initialize.js only injects the database read, view check, initializer and fatal-error predicate.

Comment on lines +132 to +133
const SUBAGENT_TARGET_PROBE_CONCURRENCY = 8;
const probeTarget = createConcurrencyLimiter(SUBAGENT_TARGET_PROBE_CONCURRENCY);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Queue probes at the shared poller capacity boundary

Under process load where 25 or more non-target worker-status polls are already active, this independent eight-slot limiter can still push createCodeBridgeStatusPoller past its shared maxConcurrent = 32 ceiling. The excess polls are rejected as busy, and resolveCandidate() catches that as an unavailable target, so healthy machines can still disappear nondeterministically; queue at the shared poller or retry busy rather than assuming eight slots reserve global capacity.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this one. The 32-request ceiling is the process-wide back-pressure valve of the shared status poller, and every status consumer (default-route resolution, #16756 inheritance, workspace validation) already treats busy as unavailable rather than queueing or retrying; making per-call targets the one caller that retries would let it keep pressure on a saturated poller. The limiter keeps this path's own contribution well below the ceiling, and a machine dropped under saturation is reported as unavailable with a model-visible, retryable error. It is never misrouted.

Comment on lines +245 to +248
const results = await Promise.all(
authorized.map((selection) =>
probeTarget(() => resolveCandidate(params, selections, selection)),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove canceled target probes from the global queue

When a request is canceled while one of these probes is queued, waitForAbort() in initialize.js only rejects the caller; no AbortSignal reaches resolveSubagentCodeTargets() or the limiter. Fresh evidence on this head is the new process-wide queue: its stale thunks still execute, so canceled large graphs can occupy all eight slots and delay unrelated requests through multiple worker timeouts. Thread the signal through and discard queued work before it starts.

AGENTS.md reference: AGENTS.md:L45-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a25692: target resolution now takes the request or call signal (describe gets the request signal, place the call's). A probe that reaches the front of the queue after cancellation is skipped without touching the poller, and the resolution rejects with the abort reason. Covered by a test.

Comment on lines +885 to +886
if (replace || !contexts.has(agentId)) {
contexts.set(agentId, toolContext);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve contexts installed by committed placements

When a pending lazy placement has registered an owner and the same agent is then initialized as an unrouted graph member, that graph-member placement has no execution IDs and is committed before register(true) writes its context here. Fresh evidence beyond the earlier parallel-placement fix is that this committed writer is not added to owner.users; if the older pending placement later fails, settleContext() restores its saved previous value or deletes the entry, clobbering the live graph member's context and leaving its tool loading or provisioning with stale/no state. Update or retire the pending ownership record whenever a committed placement replaces the entry.

AGENTS.md reference: AGENTS.md:L45-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a25692: when a committed placement (one outside any call) installs the per-agent entry, any pending ownership record for that agent is retired, so a later failure of the older pending placement cannot restore or delete over it. Covered by a test.

Comment thread packages/api/src/code/targets.ts Outdated
Comment on lines +683 to +684
const resolveTargets = async (params: SubagentCodeTargetParams): Promise<SubagentCodeTargets> => {
const resolution = await resolveSubagentCodeTargets(params);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip worker probes when shared files disable attached routes

In a shared-file run every target returned by resolveSubagentCodeTargets() is attached and is discarded immediately afterward, but the code still performs and awaits all live worker probes first. Fresh evidence beyond the earlier shared-file routing fix is this post-resolution filtering order: an offline attached machine can impose two 10-second status attempts, repeated in eight-wide batches across lazy descriptors, delaying a run that cannot use any of those routes. Return no targets before probing when sharedRunFiles is true.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a25692: with run files shared, the routing session returns no targets before probing (every per-call target is an attached machine), so neither descriptors nor calls wait on worker checks. The shared-file test now also asserts no status request is made.

- Move the routed graph-member load (shared reuse, skip list, view check,
  execution identity, failure policy) into createRoutedGraphMemberLoader;
  the initializer only injects its dependencies.
- Thread the request and call signals into target resolution so queued
  probes are skipped once canceled.
- Probe nothing in shared-file runs, where every per-call target is
  attached and unusable.
- Retire pending tool-context ownership when a committed placement
  installs the per-agent entry.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 20 of 20+ on head 1a25692.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 21 of 21+ on head 1a25692.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a25692818

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +251 to +256
} catch (error) {
if (isFatal(error, signal)) {
throw error;
}
logger.error(`[lazySubagents] Error initializing routed graph member ${memberId}:`, error);
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate routed graph-member initialization failures

When initialize rejects with an ordinary operational error, such as a provider, tool-loading, or database failure, isFatalAgentInitializationError returns false here and the loader converts the failure to null. resolveGraphSubagentsFor then merely skips the incomplete graph, allowing the routed parent to run successfully without its configured graph instead of surfacing the outage. Propagate unexpected initialization failures; reserve null for documented absence or access denial.

AGENTS.md reference: AGENTS.md:L116-L121

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping this for parity with the non-routed path. A graph member loaded for an unrouted parent (loadGraphMemberOnce, unchanged from dev) already logs a non-fatal initialization failure and returns null, and resolveGraphSubagentsFor then skips that incomplete graph with a warning while the parent still runs. The routed loader follows the same policy, so whether a graph is skipped or fails the call does not depend on whether its parent was routed per call. Changing that policy for graph members belongs in its own change covering both paths.

Comment on lines +1637 to +1639
if (!subagentCodeRouting.isRouted(context.executionId)) {
graphMemberConfigsById.set(config.id, config);
sharedConfigByCall.set(context, config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish shared configs only after their route commits

When one default-placed lazy resolution reaches this insertion while another routed parent concurrently loads the same agent as a graph member, the second resolution can reuse this config before the first call settles. If the first call then fails or is canceled, forgetSharedConfig removes the entry and settleExecution releases its route and tool context, but the second parent has already retained the borrowed config without acquiring its own hold; a later retry can therefore move that agent to another machine while the surviving graph still executes with the abandoned workspace and policy. Keep pending configs private until settlement succeeds, or make every concurrent borrower retain the route independently.

AGENTS.md reference: AGENTS.md:L44-L47

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f274737: a default-placed lazy child's config is no longer written to the shared graph cache during its resolution. It is published from the settle hook's onCommit, after the call's routes commit, so no concurrent graph can borrow a config whose route may still be given back (a child that appears in its own graph resolves to its own config directly). Covered by an initialize test where an unsettled resolution is not reused by a later routed graph and a settled one is.

Comment on lines +159 to +161
const routeHashes = [agent?.codeExecutionContext, ...(agent?.codeExecutionChoices ?? [])]
.filter((context): context is CodeExecutionContext => context?.statefulSessions === true)
.map((context) => hashCodeExecutionTarget(agentId, context));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind approvals to the call-time route snapshot

When a worker reconnects or a workspace instance changes after lazy descriptors are built but before a subagent call, place() correctly re-probes and executes against the new context, but these hashes still come from the descriptor's original codeExecutionChoices; resolved children are registered in policy/tool registries without replacing the objects later passed to captureCodeExecutionApprovalBinding. A code tool can therefore pause after executing against the new route while persisting a binding for the old route, and resume initialization then reconstructs the new route and rejects the user's approval as changed even though no change occurred after the pause. Update the binding metadata with the revalidated selection before the pause, or capture the resolved run contexts directly.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. The choices are bound exactly like the default route: both come from the descriptor built at request start, and the resume check rebuilds descriptors and compares. A worker whose routing identity changes between request start and the pause therefore fails the resume closed for defaults and choices alike; the user re-runs. Binding the call-time route instead would need a per-execution binding (the SDK pause carries no execution-scoped target), and a spurious fail-closed on a mid-turn reconnect is the conservative outcome for an approval gate.

…mmits

- Publish a default-placed lazy child's config to the shared graph cache
  from the settle hook's commit, so no concurrent graph can borrow a config
  whose route may still be given back.
- Resolve a child's own appearance in its graph to the child config itself.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 22 of 22+ on head f274737.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 23 of 23+ on head f274737.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: f274737868

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila merged commit 301f9ac into dev Oct 5, 2026
37 checks passed
@danny-avila
danny-avila deleted the danny-avila/subagent-workspace-choice branch October 5, 2026 10:21
danny-avila added a commit that referenced this pull request Oct 5, 2026
Dev's per-call subagent machine selection (#16757) changed the approval-mode
resolver to validate against every attached policy a run may execute under,
including machines a parent may route a subagent to per call. The no-target
branch, persisted mode, and preserved-field rule now read that policy list
(collectAttachedCodeApprovalPolicies), so a turn is treated as having no
attached target only when no agent runs, or may be routed, on one.
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.

1 participant