Yeachan-Heo/oh-my-codex · error · Error

canonical_scale_down_config_missing

Error message

canonical_scale_down_config_missing

What it means

During scale-down, after acquiring the membership barrier and recovering any pending transaction, the canonical team config could not be read (readTeamConfig returned null). Scale-down requires an authoritative config to identify removable workers.

Source

Thrown at src/team/scaling.ts:2067

                   );
          }),
        );
        if (allDrained.every(Boolean)) break;
        await new Promise(r => setTimeout(r, 2_000));
      }
    }

    // Phase 3: acquire the membership barrier and task claim locks before any
    // pane effect. The barrier and locks remain held through the canonical
    // snapshot, exact pane teardown, and forward-recoverable commit.
    let removableWorkers = targetWorkers;
    let removableWorkerNames = new Set(removableWorkers.map((worker) => worker.name));
    let teardownFailure: ScaleError | null = null;
    try {
      await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
        await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
        const authoritativeConfig = await readTeamConfig(sanitized, leaderCwd);
        if (!authoritativeConfig) throw new Error('canonical_scale_down_config_missing');
        const authoritativeWorkers = removableWorkers.map((worker) => authoritativeConfig.workers.find((candidate) => candidate.name === worker.name));
        if (authoritativeWorkers.some((worker): worker is undefined => !worker)) {
          throw new Error('canonical_scale_down_membership_changed');
        }
        // Capture targets from the canonical generation while the membership
        // authority is held; no caller-owned worker record authorizes a pane effect.
        removableWorkers = authoritativeWorkers as WorkerInfo[];
        removableWorkerNames = new Set(removableWorkers.map((worker) => worker.name));
        teamStateRoot = authoritativeConfig.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
        Object.assign(config, authoritativeConfig);
        const candidateTaskIds = (await listTasks(sanitized, leaderCwd))
          .filter((task) => task.status !== 'completed' && task.status !== 'failed')
          .map((task) => task.id);
        await withTaskClaimLocks(sanitized, candidateTaskIds, leaderCwd, async () => {
          const lockedTasks = await listTasks(sanitized, leaderCwd);
          const configPath = join(teamStateRoot, 'team', sanitized, 'config.json');
          const configSnapshot = await readFile(configPath);
          const manifestPath = join(teamStateRoot, 'team', sanitized, 'manifest.v2.json');

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Verify the team exists and the config file is present under the team state root (run team status/list)
  2. Ensure scaleDown is called with the same cwd/team name used at team creation
  3. Restore or regenerate the team config if it was corrupted or deleted
Defensive patterns

Strategy: try-catch

Validate before calling

const config = await readTeamConfig(team, cwd);
if (!config) throw new Error(`team '${team}' has no config; create it before scaling down`);

Try / catch

try {
  await scaleDown(team, workers, opts);
} catch (e) {
  if ((e as Error).message === 'canonical_scale_down_config_missing') {
    // team may already be gone; treat as no-op success or recreate
    return refreshTeamState(team);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling scaleDown when the team config file does not exist or is unreadable/corrupt under the team state root.

Common situations: Team state directory deleted or moved; config deleted by another process mid-operation; wrong cwd/team name so a different (empty) state root is consulted; corrupt JSON.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/a16550bd2a0bd5eb. Report an issue: GitHub.