Yeachan-Heo/oh-my-codex · warning · Error
canonical_scale_down_membership_changed
Error message
canonical_scale_down_membership_changed
What it means
The canonical team config read during scale-down no longer contains one or more of the workers requested for removal, meaning membership changed between caller capture and the barrier-held read. Scale-down aborts rather than tearing down stale targets.
Source
Thrown at src/team/scaling.ts:2070
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');
const manifestSnapshot = existsSync(manifestPath) ? await readFile(manifestPath) : null;
const reconciledTasks = lockedTasks.filter((task) => task.status !== 'completed' && task.status !== 'failed'
&& (removableWorkerNames.has(task.owner ?? '') || removableWorkerNames.has(task.claim?.owner ?? '')));View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Re-read the current team membership (team status) and retry scaleDown with workers that still exist
- Serialize team operations so only one scaler runs at a time (lock or CLI-level queue)
- Treat this error as success if the goal (worker removal) is already achieved
Defensive patterns
Strategy: try-catch
Validate before calling
const config = await readTeamConfig(team, cwd); const targets = workers.filter((w) => config?.workers.some((c) => c.name === w.name)); if (targets.length === 0) return; // already removed
Try / catch
catch (e) {
if ((e as Error).message === 'canonical_scale_down_membership_changed') {
return; // someone else already removed them; idempotent success
}
throw e;
} Prevention
- Re-fetch membership immediately before each scale-down
- Serialize scale operations per team
When it happens
Trigger: Calling scaleDown with worker records captured earlier while another process already removed some of those workers from the config.
Common situations: Concurrent scale-down operations racing; UI showing stale worker list; retrying a scaleDown whose first attempt actually succeeded.
Related errors
- canonical_scale_up_rollback_resolved_membership_verification
- scale_down_cleanup_debt_path_missing_parent:${path}
- scale_down_cleanup_debt_path_escape:${path}
- scale_down_cleanup_debt_invalid_team_state_root:${worker.nam
- scale_down_cleanup_debt_invalid_worktree_metadata:${worker.n
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/eff8b5d606061aeb.
Report an issue: GitHub.