feat(web): add source-first Python autopilot and optimize provider waits
This commit is contained in:
@@ -81,6 +81,8 @@ const pushVerification = (task) => {
|
||||
return { path: file, localHead: match?.[1] ?? null, remoteHead: match?.[2] ?? null, equal: Boolean(match && match[1] === match[2]) };
|
||||
};
|
||||
const collect = () => {
|
||||
const autopilot = readJson(`${controlRoot}/autopilot/state.json`);
|
||||
const autopilotLock = lockInfo(`${controlRoot}/autopilot/owner.lock`);
|
||||
const queue = currentPointer();
|
||||
const scheduler = readJson(schedulerCheckpointPath);
|
||||
const guardian = readJson(guardianCheckpointPath);
|
||||
@@ -88,21 +90,23 @@ const collect = () => {
|
||||
const schedulerLock = lockInfo(schedulerLockPath);
|
||||
const guardianLock = lockInfo(guardianLockPath);
|
||||
const supervisorLock = lockInfo(supervisorLockPath);
|
||||
const pythonOwnsScheduler = autopilotLock.alive && schedulerLock.pid === autopilotLock.pid;
|
||||
const handoff = latestHandoff();
|
||||
const active = schedulerLock.alive || (guardianLock.alive && supervisorLock.alive);
|
||||
const active = autopilotLock.alive || schedulerLock.alive || (guardianLock.alive && supervisorLock.alive);
|
||||
let terminalQueue = false;
|
||||
try {
|
||||
const manifest = queue.task ? readJson(`tests/golden/${queue.task}/manifest.json`) : null;
|
||||
const index = readJson("tests/golden/WBV2/task-index.json");
|
||||
terminalQueue = !active && index?.activeTask === null && manifest?.status === "done" && manifest?.nextTask === null;
|
||||
terminalQueue = !active && index?.activeTask === null && manifest?.status === "done" && manifest?.nextTask === null && manifest?.acceptanceComplete === true && manifest?.verificationDeferred !== true;
|
||||
} catch { /* A malformed pointer remains visible as a normal checkpoint failure. */ }
|
||||
const checkpointBlocked = scheduler?.state === "blocked" || guardian?.state === "blocked" || supervisor?.state === "blocked";
|
||||
// A terminal task can leave a historical blocked checkpoint behind (for
|
||||
// example, after a push retry). The watchdog owns the next planner attempt;
|
||||
// do not report that stale checkpoint as a live execution block.
|
||||
const blocked = checkpointBlocked && !terminalQueue;
|
||||
const autopilotBlocked = ["blocked", "provider_blocked", "needs_reconcile", "planning_required", "budget_exhausted", "ready_to_publish"].includes(autopilot?.state);
|
||||
const blocked = autopilotBlocked || (!autopilotLock.alive && checkpointBlocked && !terminalQueue);
|
||||
const state = blocked ? "BLOCKED" : active ? "RUNNING" : "IDLE";
|
||||
const phase = blocked ? "blocked" : scheduler?.state === "running" ? "scheduler-task" : supervisor?.state === "running" ? "task-or-gate" : scheduler?.state ?? supervisor?.state ?? "unknown";
|
||||
const phase = autopilotLock.alive || autopilotBlocked ? `python-${autopilot?.state ?? "starting"}` : blocked ? "blocked" : scheduler?.state === "running" ? "scheduler-task" : supervisor?.state === "running" ? "task-or-gate" : scheduler?.state ?? supervisor?.state ?? "unknown";
|
||||
const observed = new Date().toISOString();
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
@@ -110,10 +114,11 @@ const collect = () => {
|
||||
state,
|
||||
phase,
|
||||
message: state === "RUNNING"
|
||||
? `RUNNING: ${queue.task ?? "unknown task"} is owned by the guardian/supervisor process; wait for its handoff.`
|
||||
? `RUNNING: ${queue.task ?? "unknown task"} is owned by ${autopilotLock.alive ? "Python autopilot" : "guardian/supervisor"}; wait for its handoff.`
|
||||
: state === "BLOCKED" ? "BLOCKED: inspect the scheduler/guardian/supervisor checkpoint reason before any write or retry." : "IDLE: no live scheduler, guardian or supervisor lock was found.",
|
||||
queue,
|
||||
scheduler: { pid: scheduler?.pid ?? schedulerLock.pid, alive: schedulerLock.alive, state: scheduler?.state ?? null, updatedAt: scheduler?.updatedAt ?? null, iteration: scheduler?.iteration ?? null, before: scheduler?.before ?? null, after: scheduler?.after ?? null, latestBatch: scheduler?.latestBatch ?? null },
|
||||
autopilot: { pid: autopilot?.pid ?? autopilotLock.pid, alive: autopilotLock.alive, state: autopilot?.state ?? null, phase: autopilot?.currentProcess?.phase ?? null, activeJob: autopilot?.activeJob ?? null, reason: autopilot?.reason ?? null, tokensUsed: autopilot?.tokensUsed ?? 0, providerFailures: autopilot?.providerFailures ?? 0, nextRetryAt: autopilot?.nextRetryAt ?? null },
|
||||
scheduler: { pid: schedulerLock.alive ? schedulerLock.pid : scheduler?.pid ?? schedulerLock.pid, alive: schedulerLock.alive, state: pythonOwnsScheduler ? "python-owned" : scheduler?.state ?? null, updatedAt: pythonOwnsScheduler ? autopilot?.updatedAt : scheduler?.updatedAt ?? null, iteration: pythonOwnsScheduler ? null : scheduler?.iteration ?? null, before: pythonOwnsScheduler ? null : scheduler?.before ?? null, after: pythonOwnsScheduler ? null : scheduler?.after ?? null, latestBatch: pythonOwnsScheduler ? null : scheduler?.latestBatch ?? null },
|
||||
guardian: { pid: guardian?.pid ?? guardianLock.pid, alive: guardianLock.alive, state: guardian?.state ?? null, updatedAt: guardian?.updatedAt ?? null, heartbeatAt: guardian?.heartbeatAt ?? null, restartCount: guardian?.restartCount ?? null },
|
||||
supervisor: { pid: supervisor?.pid ?? supervisorLock.pid, alive: supervisorLock.alive, state: supervisor?.state ?? null, updatedAt: supervisor?.updatedAt ?? null, iteration: supervisor?.iteration ?? null, before: supervisor?.before ?? null, after: supervisor?.after ?? null },
|
||||
latestHandoff: handoff,
|
||||
@@ -127,6 +132,7 @@ const format = (status) => [
|
||||
`阶段: ${status.phase}`,
|
||||
`当前任务: ${status.queue.task ?? "unknown"}`,
|
||||
`parent manifest: ${status.queue.parentManifest ?? "unknown"}`,
|
||||
`Python autopilot: pid=${status.autopilot.pid ?? "none"} alive=${status.autopilot.alive} state=${status.autopilot.state ?? "unknown"} job=${status.autopilot.activeJob ?? "none"} reason=${status.autopilot.reason ?? "none"}`,
|
||||
`scheduler: pid=${status.scheduler.pid ?? "none"} alive=${status.scheduler.alive} state=${status.scheduler.state ?? "unknown"} iteration=${status.scheduler.iteration ?? "none"} batch=${status.scheduler.latestBatch?.batchId ?? "none"} completed=${status.scheduler.latestBatch?.completed ?? false}`,
|
||||
`guardian: pid=${status.guardian.pid ?? "none"} alive=${status.guardian.alive} state=${status.guardian.state ?? "unknown"} heartbeat=${status.guardian.heartbeatAt ?? "none"}`,
|
||||
`supervisor: pid=${status.supervisor.pid ?? "none"} alive=${status.supervisor.alive} state=${status.supervisor.state ?? "unknown"} iteration=${status.supervisor.iteration ?? "none"}`,
|
||||
|
||||
@@ -168,7 +168,7 @@ try {
|
||||
guardianExit = stopSignal === "SIGINT" ? 130 : 143;
|
||||
break;
|
||||
}
|
||||
if (checkpoint?.state === "complete") {
|
||||
if (checkpoint?.state === "complete" && result.code === 0) {
|
||||
writeCheckpoint({ state: "complete", restartCount, current: after, supervisorExit: result.code });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -31,9 +31,39 @@ if (!Number.isSafeInteger(pollMs) || pollMs < 0) throw new Error("--poll-ms must
|
||||
if (!Number.isSafeInteger(pushRetries) || pushRetries < 0) throw new Error("--push-retries must be a non-negative integer");
|
||||
if (pushEnabled && !commitEnabled) throw new Error("--push requires --commit");
|
||||
|
||||
// A preflight must neither claim ownership nor rotate queue/checkpoint files.
|
||||
if (preflightOnly) {
|
||||
try {
|
||||
const pointer = currentTask();
|
||||
if (!pointer) throw new Error("queue pointer is missing; completion is unproven");
|
||||
parentManifest(pointer);
|
||||
const check = spawnSync(process.execPath, [abs("tools/web/check-task-context.mjs")], { cwd: root, encoding: "utf8" });
|
||||
if (check.status !== 0) throw new Error("task context validation failed");
|
||||
const readiness = terminalTask(pointer.task)
|
||||
? { ok: true, terminal: true, reason: "accepted_terminal_manifest" }
|
||||
: sourceMappingReadiness(pointer);
|
||||
process.stdout.write(`${JSON.stringify({ task: pointer.task, ...readiness })}\n`);
|
||||
process.exit(readiness.ok ? 0 : 2);
|
||||
}
|
||||
catch (error) {
|
||||
process.stderr.write(`scheduler-preflight-blocked: ${error.message}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(abs(controlRoot), { recursive: true });
|
||||
let lockHandle;
|
||||
const acquireLock = () => {
|
||||
const pythonOwnerPath = abs(`${controlRoot}/autopilot/owner.lock`);
|
||||
if (fs.existsSync(pythonOwnerPath)) {
|
||||
const owner = JSON.parse(fs.readFileSync(pythonOwnerPath, "utf8"));
|
||||
let alive = false;
|
||||
if (Number.isSafeInteger(owner.pid) && owner.pid > 0) {
|
||||
try { process.kill(owner.pid, 0); alive = true; }
|
||||
catch (error) { if (error.code !== "ESRCH") alive = true; }
|
||||
}
|
||||
if (alive) throw new Error("the Python autopilot already owns the project");
|
||||
}
|
||||
try {
|
||||
return fs.openSync(abs(lockPath), "wx");
|
||||
}
|
||||
@@ -186,7 +216,13 @@ function terminalTask(task) {
|
||||
const index = JSON.parse(read("tests/golden/WBV2/task-index.json"));
|
||||
const manifestPath = `tests/golden/${task}/manifest.json`;
|
||||
const manifest = fs.existsSync(abs(manifestPath)) ? JSON.parse(read(manifestPath)) : null;
|
||||
return index.activeTask === null && manifest?.task === task && manifest.status === "done" && manifest.nextTask === null;
|
||||
return index.activeTask === null && manifest?.task === task && manifest.status === "done" && manifest.nextTask === null
|
||||
&& manifest.acceptanceComplete === true && manifest.verificationDeferred !== true
|
||||
&& Object.keys(manifest.artifacts ?? {}).length > 0
|
||||
&& Object.values(manifest.artifacts).every((artifact) => typeof artifact.path === "string"
|
||||
&& typeof artifact.sha256 === "string" && !path.isAbsolute(artifact.path)
|
||||
&& !artifact.path.split("/").includes("..")
|
||||
&& crypto.createHash("sha256").update(fs.readFileSync(abs(artifact.path))).digest("hex") === artifact.sha256);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
@@ -319,7 +355,7 @@ try {
|
||||
// guardian/supervisor chain.
|
||||
try {
|
||||
const index = JSON.parse(read("tests/golden/WBV2/task-index.json"));
|
||||
if (index.activeTask === null && index.entries?.[before.task]) {
|
||||
if (index.activeTask === null && index.entries?.[before.task] && terminalTask(before.task)) {
|
||||
const completedBatchInfo = { ...batchInfo(before), completed: true };
|
||||
latestBatch = completedBatchInfo;
|
||||
const planned = preflightOnly ? { terminal: true } : planNextBatch(before);
|
||||
|
||||
@@ -12,7 +12,7 @@ const arg = (name, fallback = null) => {
|
||||
return index >= 0 ? process.argv[index + 1] : fallback;
|
||||
};
|
||||
if (process.argv.includes("--help")) {
|
||||
process.stdout.write("usage: node tools/web/search-wbv2-batch-source.mjs [--task <task-id>] --pattern <text> [--runtime <path> ...] or --list\n");
|
||||
process.stdout.write("usage: node tools/web/search-wbv2-batch-source.mjs [--task <task-id>] --pattern <text> [--unit <source-unit-id> ...] [--runtime <path> ...] [--max-hits <count>] or --list\n");
|
||||
process.exit(0);
|
||||
}
|
||||
const task = arg("--task") ?? read("docs/EXECUTION_QUEUE.md").match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
@@ -32,6 +32,21 @@ for (const artifact of Object.values(manifest?.artifacts ?? {})) {
|
||||
for (const source of taskRecord?.module?.sourcePaths ?? []) {
|
||||
if (source.startsWith("blender-5.2.0/source/blender/")) sourcePaths.add(source);
|
||||
}
|
||||
const selectedUnits = [];
|
||||
for (let i = 0; i < process.argv.length; i += 1) {
|
||||
if (process.argv[i] === "--unit") selectedUnits.push(process.argv[++i]);
|
||||
}
|
||||
if (selectedUnits.length > 16) throw new Error("source context is limited to 16 explicit unit IDs");
|
||||
if (selectedUnits.length) {
|
||||
const inventory = json("tests/golden/WBV2/dynamic-inventory.json");
|
||||
for (const id of selectedUnits) {
|
||||
const unit = inventory.units.find((item) => item.id === id);
|
||||
if (!unit) throw new Error(`unknown source unit: ${id}`);
|
||||
for (const location of unit.locations ?? []) {
|
||||
if (location.path.startsWith("blender-5.2.0/source/blender/")) sourcePaths.add(location.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
const runtimeAllowlist = new Set([
|
||||
"tools/web/run-wbv2-module-task.mjs",
|
||||
"tools/web/run-wbv2-continuous.mjs",
|
||||
@@ -78,6 +93,17 @@ const runtimeAllowlist = new Set([
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
]);
|
||||
const runtimePaths = [];
|
||||
// Product workflows are bounded subtasks of the current authoritative task.
|
||||
const backlogPath = abs("tests/golden/WBV2/web-product-backlog.json");
|
||||
if (fs.existsSync(backlogPath)) {
|
||||
const backlog = JSON.parse(fs.readFileSync(backlogPath, "utf8"));
|
||||
if (backlog.queueTask === task) {
|
||||
for (const job of backlog.tasks ?? []) {
|
||||
for (const file of job.ownedPaths ?? []) runtimeAllowlist.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const file of ["tools/web/deployment-http-server.mjs", "my-tools/wbv2_autopilot.py", "my-tools/run_web_product_migration.py", "tools/web/run-wbv2-scheduler.mjs", "tools/web/run-wbv2-guardian.mjs", "tools/web/check-wbv2-module-execution.mjs"]) runtimeAllowlist.add(file);
|
||||
for (let index = 0; index < process.argv.length; index += 1) {
|
||||
if (process.argv[index] === "--runtime") {
|
||||
const file = process.argv[index + 1];
|
||||
@@ -101,4 +127,6 @@ if (result.error) throw result.error;
|
||||
if (result.status !== 0 && result.status !== 1) throw new Error(`batch search failed with exit ${result.status}`);
|
||||
const output = result.stdout ?? "";
|
||||
const hits = output ? output.trimEnd().split("\n").filter(Boolean) : [];
|
||||
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, task, scope: "batch", pattern, sourcePathCount: sourcePaths.size, runtimePathCount: runtimePaths.length, hitCount: hits.length, hits }, null, 2)}\n`);
|
||||
const maxHits = Number(arg("--max-hits", "60"));
|
||||
if (!Number.isSafeInteger(maxHits) || maxHits < 1 || maxHits > 200) throw new Error("--max-hits must be between 1 and 200");
|
||||
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, task, scope: "batch", pattern, selectedUnits, sourcePathCount: sourcePaths.size, runtimePathCount: runtimePaths.length, hitCount: hits.length, truncated: hits.length > maxHits, hits: hits.slice(0, maxHits) }, null, 2)}\n`);
|
||||
|
||||
Reference in New Issue
Block a user