Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@

- Added dynamic GitHub Copilot model population from the live CAPI `/models` catalog: picker-enabled, non-disabled plain chat ids are synthesized from catalog metadata (endpoints, capabilities, limits, and display names) while built-in `pi-ai` definitions still win, namespaced enterprise deployments such as `org/deployment/model` are skipped, and cached catalog metadata enables the same models on cold start.
- Added catalog-driven thinking-level gating for GitHub Copilot models so dynamically synthesized entries and bundled `pi-ai` Copilot models only offer the reasoning levels advertised by CAPI's `capabilities.supports.reasoning_effort` arrays, while models without an effort array keep their existing thinking behavior.
- Added a subagent watchdog escape hatch: setting `ATOMIC_SUBAGENT_ATTEMPT_IDLE_TIMEOUT_MS` or `ATOMIC_SUBAGENT_ATTEMPT_TIMEOUT_MS` to `0` (or a negative value) now disables the corresponding per-attempt timeout entirely; non-numeric values are ignored and the defaults apply. The `ATOMIC_SUBAGENT_ATTEMPT_KILL_GRACE_MS` SIGTERM→SIGKILL grace period intentionally cannot be disabled — `0`, negative, or non-numeric values fall back to its default so escalation always stays bounded ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).

### Fixed

- Fixed the `read` tool to parse colon-delimited `file:START:END` (and grep-style `file:LINE:COL`) path selectors as a line range instead of leaving the leading number glued to the path (`file:395`), which produced a bogus `ENOENT` and pushed models (e.g. Opus) to fall back to `sed` ([#1585](https://github.com/bastani-inc/atomic/issues/1585)).
- Fixed GitHub Copilot models to use the live `max_output_tokens` value from the Copilot model catalog, preventing `github-copilot/claude-opus-4.8` from being capped by Atomic's stale built-in output-token limit after compaction ([#1582](https://github.com/bastani-inc/atomic/issues/1582)).
- Fixed active GitHub Copilot sessions to adopt live catalog model metadata as soon as the catalog loads, so fallback models refresh their supported reasoning levels without requiring a restart.
- Fixed workflow and subagent model fallback chains so request/context incompatibility failures (HTTP 400/413/422 bad/unprocessable/payload-too-large request, unsupported tool/parameter, context-length/context-window overflow, `invalid_request`/`bad_request`/`too_large` errors) advance to the next candidate instead of stopping. This ensures that when none of the configured fallback candidates can serve the current request, Atomic falls back to the currently selected user model rather than failing outright. Refusals, content-filter/safety blocks, cancellations, and task failures still stop the chain and are never retried on another model ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
- Fixed foreground and background subagent model attempts that produced no child activity from hanging indefinitely. Atomic now bounds each candidate with an idle watchdog and wall-clock cap, records retryable timeout attempts so fallback continues, and skips known unauthenticated providers before spawning while preserving unknown/custom providers and the current-model last resort ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
- Fixed the subagent per-attempt idle watchdog so an in-flight tool execution counts as activity: a slow, quiet tool call (long build or test run with no interim output) is no longer killed as a stalled attempt; the wall-clock cap still bounds such attempts ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
- Aligned the subagents and workflows model-failure classifiers' direct-message precedence and added a cross-package conformance test suite so the two classifier copies cannot silently drift ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
- Fixed the background subagent runner to spawn one default-model attempt when no model candidates were ever configured (no primary model, no fallbacks, and no current model), mirroring the foreground path instead of silently exiting 1 with no error and no spawn; an explicitly empty candidate list produced by pre-spawn auth filtering is still respected and surfaced as an error ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).

## [0.9.4-alpha.6] - 2026-07-01

Expand Down
6 changes: 6 additions & 0 deletions packages/coding-agent/docs/subagents.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ Dynamic fanout `collect.outputSchema` validates the collected result array after

Agents can define ordered `fallbackModels` for retryable provider or model failures such as rate limits, quota/auth problems, unavailable models, network timeouts, or 5xx errors. Atomic tries the requested primary model first, then configured fallbacks, and finally appends the current user-selected model as the last fallback candidate when available.

A candidate that cannot serve the current request — for example an HTTP 400/413/422 bad/unprocessable/payload-too-large request, an unsupported tool or parameter, a context-length/context-window overflow, or a `too large` / `invalid_request` error — is treated as request/context incompatible and the chain advances to the next candidate rather than stopping. This means that if none of the configured candidates are applicable to the request, Atomic falls back to the currently selected user model instead of failing outright.

Each foreground and background model candidate is bounded by a per-attempt idle watchdog (default 5 minutes without child stdout, stderr, or JSON child events) and an absolute wall-clock cap (default 60 minutes). An in-flight tool execution counts as activity, so a slow, quiet tool call (a long build or test run that streams nothing until it finishes) is not mistaken for a stalled attempt; only the wall-clock cap bounds such attempts. If either watchdog trips, Atomic terminates that child attempt, records a retryable timeout in `modelAttempts`, and continues to the next fallback candidate. The defaults can be overridden with `ATOMIC_SUBAGENT_ATTEMPT_IDLE_TIMEOUT_MS` and `ATOMIC_SUBAGENT_ATTEMPT_TIMEOUT_MS`; `ATOMIC_SUBAGENT_ATTEMPT_KILL_GRACE_MS` controls SIGTERM-to-SIGKILL escalation. Setting the idle or wall-clock variable to `0` (or a negative value) disables that timeout entirely; non-numeric values are ignored and the default applies. The kill-grace period cannot be disabled — `0`, negative, or non-numeric values fall back to its default so escalation always stays bounded.

When registry availability shows that a known candidate provider has no configured auth, Atomic records a skipped model attempt before spawning a child. Unknown/custom providers are still attempted, and the current user-selected model appended as the final fallback is never filtered out by this pre-spawn check.

Fallbacks do not retry ordinary task failures, validation failures, tool failures, cancellations, or workflow-code errors. Because a fallback may send the same prompt and context to a different provider, choose models that match your cost, privacy, and data-handling requirements.

Each candidate can also carry its own reasoning effort — see [Reasoning levels](#reasoning-levels).
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -1816,6 +1816,8 @@ For lower-level integrations, `@bastani/workflows` also exports `setupGitWorktre

`fallbackModels` retries transient provider/model failures with the primary `model` first, then each fallback, then the current Atomic-selected model when available. It is for rate limits, quota/auth/provider outages, unavailable models, network timeouts, generic transport errors such as `Connection error.` / `fetch failed`, and 5xx errors — not workflow-code errors, tool failures, validation failures, or cancellations.

A candidate that is **request/context incompatible** with the current turn — for example an HTTP 400/413/422 bad/unprocessable/payload-too-large request, an unsupported tool or parameter, a context-length/context-window overflow, or a `too large` / `invalid_request` / `bad_request` error — also advances the chain to the next candidate rather than stopping. This ensures that if none of the configured candidates can serve the request, the workflow stage falls back to the currently selected user model instead of hard-failing. Refusals, content-filter/safety blocks, cancellations, and task failures still stop the chain and are never retried on another model.

When a finished stage's session is reattached for a follow-up (for example a post-completion follow-up, or after the CLI is reloaded), the stage resumes on the model the session last settled on — the one that actually worked — instead of replaying the chain from the primary. If that model fails again with a transient/retryable error, the full chain is retried from the primary.

### Reasoning levels
Expand Down
12 changes: 12 additions & 0 deletions packages/subagents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## [Unreleased]

### Fixed

- Fixed subagent model fallback so request/context incompatibility failures (HTTP 400/413/422 bad/unprocessable/payload-too-large request, unsupported tool/parameter, context-length/context-window overflow, `invalid_request`/`bad_request`/`too_large` errors) advance the chain to the next candidate instead of stopping. When none of the configured candidates can serve the request, the subagent now falls back to the current user-selected model. Refusals, content-filter/safety blocks, cancellations, and task failures still stop the chain ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
- Fixed foreground and background subagent model attempts that produced no child activity from hanging indefinitely. Each candidate attempt now has a conservative idle watchdog and absolute wall-clock cap, records a retryable timeout failure, and advances to the next fallback candidate; known providers without configured auth are skipped before spawning while unknown/custom providers and the current-model last resort are still attempted ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
- Fixed the per-attempt idle watchdog so an in-flight tool execution counts as activity: a slow, quiet tool call (long build or test run with no interim output) is no longer killed as a stalled attempt; the wall-clock cap still bounds such attempts ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
- Aligned the subagents model-failure classifier's direct-message precedence with the workflows classifier (direct-message classification now runs after nested cause/diagnostic traversal and before outer status/code classification), and added a cross-package conformance test suite so the two classifier copies cannot silently drift ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).
- Fixed the background subagent runner to spawn one default-model attempt when no model candidates were ever configured (no primary model, no fallbacks, and no current model), mirroring the foreground path instead of silently exiting 1 with no error and no spawn. An explicitly empty candidate list produced by pre-spawn auth filtering is still respected and surfaced as an error ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).

### Added

- Added a watchdog escape hatch: setting `ATOMIC_SUBAGENT_ATTEMPT_IDLE_TIMEOUT_MS` or `ATOMIC_SUBAGENT_ATTEMPT_TIMEOUT_MS` to `0` (or a negative value) now disables the corresponding per-attempt timeout entirely; non-numeric values are ignored and the defaults apply. The `ATOMIC_SUBAGENT_ATTEMPT_KILL_GRACE_MS` SIGTERM→SIGKILL grace period intentionally cannot be disabled — `0`, negative, or non-numeric values fall back to its default so escalation always stays bounded ([#1581](https://github.com/bastani-inc/atomic/pull/1581)).

## [0.9.4-alpha.6] - 2026-07-01

### Changed
Expand Down
12 changes: 11 additions & 1 deletion packages/subagents/src/runs/background/async-execution-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { RunnerStep } from "../shared/parallel-utils.ts";
import { injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
import { buildModelCandidates, resolveModelCandidate } from "../shared/model-fallback.ts";
import { filterSpawnableModelCandidates } from "../shared/model-candidate-filter.ts";
import { NESTED_RUNS_DIR, nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
Expand Down Expand Up @@ -57,6 +58,7 @@ export function executeAsyncChain(
const resultMode = params.resultMode ?? "chain";
const chainSkills = params.chainSkills ?? [];
const availableModels = params.availableModels;
const knownModelProviders = params.knownModelProviders;
const fastModeScope = resolveSubagentCodexFastModeScope(workflowStageSubagentGuard);
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
const firstStep = chain[0];
Expand Down Expand Up @@ -149,9 +151,16 @@ export function executeAsyncChain(

const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
const model = applyThinkingSuffix(primaryModel, a.thinking);
const modelCandidates = buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel, a.fallbackThinkingLevels)
const rawModelCandidates = buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel, a.fallbackThinkingLevels)
.map((candidate) => applyThinkingSuffix(candidate, a.thinking))
.filter((candidate): candidate is string => typeof candidate === "string");
const filteredCandidates = filterSpawnableModelCandidates({
candidates: rawModelCandidates,
availableModels,
knownModelProviders,
currentModel: applyThinkingSuffix(ctx.currentModel, a.thinking),
});
const modelCandidates = filteredCandidates.candidates;
const fastModeSettings = getSubagentCodexFastModeSettings(stepCwd);
return {
agent: s.agent,
Expand All @@ -165,6 +174,7 @@ export function executeAsyncChain(
thinking: resolveEffectiveThinking(model, a.thinking),
...resolveSubagentModelFastModeMetadata({ model, modelCandidates, cwd: stepCwd, settings: fastModeSettings, scope: fastModeScope }),
modelCandidates,
modelAttempts: filteredCandidates.skippedAttempts,
codexFastModeSettings: fastModeSettings,
codexFastModeScope: fastModeScope,
tools: a.tools,
Expand Down
12 changes: 11 additions & 1 deletion packages/subagents/src/runs/background/async-execution-single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { resolveChildCwd } from "../../shared/utils.ts";
import { applyThinkingSuffix, SUBAGENT_INTERCOM_SESSION_NAME_ENV } from "../shared/pi-args.ts";
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
import { buildModelCandidates, resolveModelCandidate } from "../shared/model-fallback.ts";
import { filterSpawnableModelCandidates } from "../shared/model-candidate-filter.ts";
import { NESTED_RUNS_DIR, nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
import {
UNAVAILABLE_SUBAGENT_SKILL_ERROR,
Expand Down Expand Up @@ -50,6 +51,7 @@ export function executeAsyncSingle(
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
const skillNames = params.skills ?? agentConfig.skills ?? [];
const availableModels = params.availableModels;
const knownModelProviders = params.knownModelProviders;
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, runnerCwd, ctx.cwd);
if (missingSkills.includes("subagent")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
Expand Down Expand Up @@ -84,9 +86,16 @@ export function executeAsyncSingle(
resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider),
agentConfig.thinking,
);
const modelCandidates = buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel)
const rawModelCandidates = buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, ctx.currentModel)
.map((candidate) => applyThinkingSuffix(candidate, agentConfig.thinking))
.filter((candidate): candidate is string => typeof candidate === "string");
const filteredCandidates = filterSpawnableModelCandidates({
candidates: rawModelCandidates,
availableModels,
knownModelProviders,
currentModel: applyThinkingSuffix(ctx.currentModel, agentConfig.thinking),
});
const modelCandidates = filteredCandidates.candidates;
const fastModeSettings = getSubagentCodexFastModeSettings(runnerCwd);
const fastModeScope = resolveSubagentCodexFastModeScope(workflowStageSubagentGuard);
let spawnResult: AsyncSpawnResult = {};
Expand All @@ -103,6 +112,7 @@ export function executeAsyncSingle(
thinking: resolveEffectiveThinking(model, agentConfig.thinking),
...resolveSubagentModelFastModeMetadata({ model, modelCandidates, cwd: runnerCwd, settings: fastModeSettings, scope: fastModeScope }),
modelCandidates,
modelAttempts: filteredCandidates.skippedAttempts,
codexFastModeSettings: fastModeSettings,
codexFastModeScope: fastModeScope,
tools: agentConfig.tools,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface AsyncChainParams {
agents: AgentConfig[];
ctx: AsyncExecutionContext;
availableModels?: AvailableModelInfo[];
knownModelProviders?: string[];
cwd?: string;
maxOutput?: MaxOutputConfig;
artifactsDir?: string;
Expand Down Expand Up @@ -62,6 +63,7 @@ export interface AsyncSingleParams {
outputMode?: "inline" | "file-only";
modelOverride?: string;
availableModels?: AvailableModelInfo[];
knownModelProviders?: string[];
maxSubagentDepth: number;
workflowStageSubagentGuard?: boolean;
worktreeSetupHook?: string;
Expand Down
Loading
Loading