Fix Code Queue runner skills delivery

This commit is contained in:
Codex
2026-05-23 19:56:01 +00:00
parent fccd25b31e
commit 9b164c1781
9 changed files with 402 additions and 8 deletions
@@ -2481,6 +2481,43 @@ function resolvedRunnerSkillsPath(): string {
return currentSkillAvailability().resolution.runnerEnvValue;
}
function runnerSkillsBlocker(): Record<string, JsonValue> | null {
const skills = currentSkillAvailability();
if (skills.runnerUsable) return null;
const pathSpelling = {
expectedTarget: skills.pathSpelling.expectedTarget,
forbiddenPathChecked: skills.pathSpelling.forbiddenPathChecked,
forbiddenPathExists: skills.pathSpelling.forbiddenPathExists,
forbiddenPathConfigured: skills.pathSpelling.forbiddenPathConfigured,
forbiddenPathRoles: skills.pathSpelling.forbiddenPathRoles,
forbiddenPathMustNotBeUsed: skills.pathSpelling.forbiddenPathMustNotBeUsed,
};
return {
ok: false,
runnerDisposition: "infra-blocked",
failureKind: skills.blocker ?? "runner-skills-unavailable",
degradedReason: skills.degradedReason ?? skills.blocker ?? "runner-skills-unavailable",
message: "Code Queue runner skills are unavailable; refusing to start a code agent without the controlled skills projection.",
checkedAt: skills.checkedAt,
source: skills.source,
target: skills.target,
resolvedPath: skills.resolvedPath,
resolvedPathSource: skills.resolvedPathSource,
requiredSkills: skills.requiredSkills,
missingSkills: skills.missingSkills,
counts: {
sourceSkills: skills.sourceSkillCount,
targetSkills: skills.targetSkillCount,
missingSourceSkills: skills.sourceMissingSkills.length,
missingTargetSkills: skills.targetMissingSkills.length,
},
version: skills.version,
pathSpelling,
repairHint: skills.repairHint,
valuesPrinted: false,
};
}
function collectDevReady(): JsonValue {
const now = Date.now();
if (devReadyCache !== null && now - devReadyCache.checkedAtMs < 30_000) return devReadyCache.value;
@@ -4046,6 +4083,44 @@ function failTaskForFallbackRetryLimit(task: QueueTask, judge: JudgeResult | nul
async function runTask(task: QueueTask): Promise<void> {
const claimQueueId = queueIdOf(task);
logger("info", "task_processor_start", { taskId: task.id, queueId: claimQueueId, providerId: task.providerId, executionMode: task.executionMode, cwd: task.cwd, maxAttempts: task.maxAttempts, model: task.model, agentPort: codeAgentPortForModel(task.model), promptPreview: safePreview(task.prompt, 240) });
const skillsBlocker = runnerSkillsBlocker();
if (skillsBlocker !== null) {
const finishedAt = nowIso();
const message = String(skillsBlocker.message);
task.status = "failed";
task.startedAt ??= finishedAt;
task.finishedAt = finishedAt;
task.updatedAt = finishedAt;
task.activeTurnId = null;
task.lastError = message;
appendOutput(task, "error", `${message}\n${JSON.stringify(skillsBlocker)}\n`, "runner-skills/blocker");
task.attempts.push({
index: task.attempts.length + 1,
mode: task.nextMode ?? "initial",
startedAt: finishedAt,
finishedAt,
providerId: task.providerId,
executionMode: task.executionMode,
terminalStatus: "failed",
transportClosedBeforeTerminal: false,
appServerExitCode: null,
appServerSignal: null,
error: message,
events: [],
finalResponse: "",
finalResponsePreview: "",
finalResponseChars: 0,
judge: null,
judgeAt: null,
judgeSeq: null,
stderrTail: "",
runnerErrorClassification: skillsBlocker as JsonValue,
});
persistTaskState(task);
logger("error", "task_blocked_by_runner_skills", { taskId: task.id, queueId: claimQueueId, skills: skillsBlocker as JsonValue });
void notifyTaskTerminal(task);
return;
}
if (
task.status === "retry_wait"
&& task.lastJudge?.source === "fallback"
@@ -101,6 +101,10 @@ export function classifyRunnerError(message: string, providerId?: string | null)
const schedulerEvidence = collectEvidence(normalized, [
/code[- ]queue scheduler/gu,
/\bscheduler\b/gu,
/runner skills?/gu,
/skills projection/gu,
/required[- ]target[- ]skills[- ]missing/gu,
/forbidden[- ]skills[- ]path[- ]configured/gu,
/active run/gu,
/runtime-preflight/gu,
/database claim/gu,
@@ -1,4 +1,5 @@
import { accessSync, constants, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
import { resolve } from "node:path";
import { spawnSync } from "node:child_process";
import type { JsonValue } from "./types";
@@ -70,6 +71,16 @@ export interface SkillAvailabilityReport {
forbiddenPathMustNotBeUsed: true;
};
expectedMount: string;
version: {
selectedFingerprint: string | null;
selectedLatestMtime: string | null;
sourceFingerprint: string | null;
targetFingerprint: string | null;
sourceLatestMtime: string | null;
targetLatestMtime: string | null;
sourceSampledSkillNames: string[];
targetSampledSkillNames: string[];
};
repairHint: string | null;
error: string | null;
valuesPrinted: false;
@@ -87,6 +98,12 @@ export interface SkillSyncPathReport {
symlink: boolean;
realPath: string | null;
skillCount: number;
version: {
fingerprint: string | null;
latestMtime: string | null;
latestMtimeMs: number | null;
sampledSkillNames: string[];
};
requiredSkills: string[];
missingSkills: string[];
skills: Array<{ name: string; present: boolean; skillMdPresent: boolean; path: string }>;
@@ -139,6 +156,14 @@ export interface SkillSyncPreflightReport {
missingSourceSkills: number;
missingTargetSkills: number;
};
version: {
sourceFingerprint: string | null;
targetFingerprint: string | null;
sourceLatestMtime: string | null;
targetLatestMtime: string | null;
sourceSampledSkillNames: string[];
targetSampledSkillNames: string[];
};
missing: {
sourceSkills: string[];
targetSkills: string[];
@@ -246,6 +271,38 @@ function skillStatus(target: string, requiredSkills: string[]): SkillAvailabilit
});
}
function skillSetVersion(path: string, readable: boolean): SkillSyncPathReport["version"] {
if (!readable) return { fingerprint: null, latestMtime: null, latestMtimeMs: null, sampledSkillNames: [] };
try {
const entries = readdirSync(path, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => {
const skillPath = resolve(path, entry.name);
const skillMdPath = resolve(skillPath, "SKILL.md");
const skillStat = existsSync(skillMdPath) ? statSync(skillMdPath) : statSync(skillPath);
return {
name: entry.name,
mtimeMs: Math.floor(skillStat.mtimeMs),
skillMd: existsSync(skillMdPath),
};
})
.sort((left, right) => left.name.localeCompare(right.name));
const latestMtimeMs = entries.reduce<number | null>((latest, entry) => latest === null || entry.mtimeMs > latest ? entry.mtimeMs : latest, null);
const fingerprint = createHash("sha256")
.update(JSON.stringify(entries))
.digest("hex")
.slice(0, 16);
return {
fingerprint: entries.length === 0 ? null : fingerprint,
latestMtime: latestMtimeMs === null ? null : new Date(latestMtimeMs).toISOString(),
latestMtimeMs,
sampledSkillNames: entries.map((entry) => entry.name).slice(0, 12),
};
} catch {
return { fingerprint: null, latestMtime: null, latestMtimeMs: null, sampledSkillNames: [] };
}
}
function accessProbe(path: string, mode: number, operation: string): { ok: boolean; failure: { path: string; operation: string; error: string } | null } {
try {
accessSync(path, mode);
@@ -332,6 +389,7 @@ function collectSyncPathReport(path: string, approved: boolean, requiredSkills:
symlink,
realPath,
skillCount,
version: skillSetVersion(path, readable),
requiredSkills,
missingSkills,
skills,
@@ -479,6 +537,16 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
forbiddenPathMustNotBeUsed: true,
},
expectedMount: `${defaultSource} mounted read-only to ${expectedTarget}`,
version: {
selectedFingerprint: selectedReport.version.fingerprint,
selectedLatestMtime: selectedReport.version.latestMtime,
sourceFingerprint: sourceProbe.version.fingerprint,
targetFingerprint: targetProbe.version.fingerprint,
sourceLatestMtime: sourceProbe.version.latestMtime,
targetLatestMtime: targetProbe.version.latestMtime,
sourceSampledSkillNames: sourceProbe.version.sampledSkillNames,
targetSampledSkillNames: targetProbe.version.sampledSkillNames,
},
repairHint: contractOk
? null
: runnerUsable
@@ -555,6 +623,14 @@ export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {
missingSourceSkills: source.report.missingSkills.length,
missingTargetSkills: target.report.missingSkills.length,
},
version: {
sourceFingerprint: source.report.version.fingerprint,
targetFingerprint: target.report.version.fingerprint,
sourceLatestMtime: source.report.version.latestMtime,
targetLatestMtime: target.report.version.latestMtime,
sourceSampledSkillNames: source.report.version.sampledSkillNames,
targetSampledSkillNames: target.report.version.sampledSkillNames,
},
missing: {
sourceSkills: source.report.missingSkills,
targetSkills: target.report.missingSkills,