Fix Code Queue runner skills delivery
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user