Skip to content

fix(core): runtime block without timeouts silently discards legacy codex timeout declarations #888

Description

@moncher-dev

Problem

resolveWorkflowRuntimeTimeouts (packages/core/src/workflow/config.ts:285) is written as a fallback:

export function resolveWorkflowRuntimeTimeouts(
  workflow: Pick<WorkflowDefinition, "runtime" | "codex">
): WorkflowRuntimeTimeoutsConfig {
  return workflow.runtime?.timeouts ?? workflow.codex;
}

The ?? workflow.codex arm is unreachable for any workflow that declares a runtime: block, because the parser materializes runtime.timeouts with hardcoded defaults whenever a runtime: block is present. A legacy top-level codex: timeout declaration is then silently discarded.

The orchestrator forwards this resolver to the runtime, so this is a behavior bug, not only a reporting one.

Reproduction

Front-matter containing both a runtime: block with no timeouts: sub-block and a legacy top-level codex: block:

runtime:
  kind: codex-app-server
  command: codex
  args:
    - app-server
codex:
  read_timeout_ms: 7777
  stall_timeout_ms: 88888
  turn_timeout_ms: 999999
parseWorkflowMarkdown(...)
  workflow.codex          -> {readTimeoutMs: 7777,  stallTimeoutMs: 88888,  turnTimeoutMs: 999999}
  workflow.runtime.timeouts -> {readTimeoutMs: 5000, stallTimeoutMs: 300000, turnTimeoutMs: 3600000}  # defaults, never declared
  resolveWorkflowRuntimeTimeouts(workflow) -> 5000 / 300000 / 3600000

The declared 7777 / 88888 is dropped without a warning. Removing the runtime: block resolves correctly to 7777, which confirms the trigger is the presence of the runtime: block rather than the codex: block being malformed.

Why it matters

This is the mirror of #882. There the CLI reported a value the runtime did not use; here the runtime uses a value the operator did not declare. A repository migrating from the legacy codex: timeouts to runtime.timeouts — the exact state where both blocks coexist — silently reverts to a 5s read timeout and a 300s stall timeout. On this repository those defaults are 6x and 3x tighter than the declared values, which is the failure mode #882 was chasing.

Found while verifying #884 (PR for #882). Not caused by that PR: main and the PR branch resolve identically here. #884 is correct to report the effective value; this issue is about the effective value itself being wrong.

Suggested fix

Distinguish "declared" from "defaulted" for runtime.timeouts so the fallback can actually fire — e.g. keep runtime.timeouts optional through parsing and apply defaults only in resolveWorkflowRuntimeTimeouts after the ?? workflow.codex arm, or track which timeout fields were explicitly present.

Per-field merge is worth considering: today a runtime.timeouts block that sets only read_timeout_ms also silently defaults the other two rather than inheriting them from codex:.

Either way, a workflow declaring both blocks should not lose the declared values in silence — a validation warning naming the shadowed keys would help operators mid-migration.

Acceptance

  • A workflow with a runtime: block, no runtime.timeouts:, and a legacy codex: timeout block resolves to the codex: values.
  • A workflow with an explicit runtime.timeouts: continues to resolve to those values (fix(cli): workflow validate reports codex timeouts the runtime does not use #882 behavior preserved).
  • A workflow with neither still resolves to the documented defaults.
  • Unit tests in packages/core cover all three, plus the partial-runtime.timeouts case.

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions