@deksden-com/dd-flow-cli 0.9.0-beta.49 → 0.9.0-beta.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +4 -1
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/code-checks.js +237 -32
- package/dist/services/runs.js +16 -0
- package/dist/services/usage.js +4 -3
- package/dist/services/vnext-code-review.js +45 -21
- package/dist/services/vnext-code.js +62 -122
- package/dist/services/vnext-merge.js +73 -24
- package/dist/services/vnext-plan-review.js +24 -19
- package/dist/services/vnext-plan.js +4 -0
- package/dist/services/work-registry.js +32 -8
- package/dist/services/workspace-bootstrap.js +29 -12
- package/dist/storage/database.js +6 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { withStageSettlement } from "./runs.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { AppError } from "../shared/errors.js";
|
|
@@ -70,13 +71,15 @@ export function startVnextPlanReview(context, input) {
|
|
|
70
71
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
71
72
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
72
73
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
return withStageSettlement(context, () => {
|
|
75
|
+
const registered = registerCode(context, project.id, run.id, batch);
|
|
76
|
+
const report = reportFor({ run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
|
|
77
|
+
writeReport(root, report);
|
|
78
|
+
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
79
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
|
|
80
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
|
|
81
|
+
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
|
|
82
|
+
});
|
|
80
83
|
}
|
|
81
84
|
const rootWork = rootWorkId(context, project.id, run.id);
|
|
82
85
|
const workId = nextWorkId(context, project.id, "plan-review");
|
|
@@ -194,7 +197,7 @@ export function finishVnextPlanReview(context, input) {
|
|
|
194
197
|
}
|
|
195
198
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
196
199
|
preserveDecisionReceipt(root, decisionPath);
|
|
197
|
-
return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision });
|
|
200
|
+
return withStageSettlement(context, () => finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }));
|
|
198
201
|
}
|
|
199
202
|
if (children.some((child) => child.status === "created" || child.status === "running"))
|
|
200
203
|
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
@@ -241,17 +244,19 @@ export function finishVnextPlanReview(context, input) {
|
|
|
241
244
|
preserveDecisionReceipt(root, decisionPath);
|
|
242
245
|
const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
|
|
243
246
|
const finalBatchChecksum = checksum(batch);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
247
|
+
return withStageSettlement(context, () => {
|
|
248
|
+
const registered = registerCode(context, project.id, run.id, batch);
|
|
249
|
+
const now = context.now();
|
|
250
|
+
settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
|
|
251
|
+
const report = reportFor({ run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context) });
|
|
252
|
+
applyReviewResults(home, latestChildren, decision);
|
|
253
|
+
writeReport(root, report);
|
|
254
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
255
|
+
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
256
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
|
|
257
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
|
|
258
|
+
return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
|
|
259
|
+
});
|
|
255
260
|
}
|
|
256
261
|
function preparedExisting(context, input) {
|
|
257
262
|
const promptPath = path.join(input.root, "stage-prompt.md");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { withStageSettlement } from "./runs.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { AppError } from "../shared/errors.js";
|
|
@@ -106,6 +107,9 @@ export function startVnextPlan(context, input) {
|
|
|
106
107
|
return { ok: true, run_id: run.id, work_id: planWorkId, id: workSessionId, artifact_materialization: artifactMaterialization, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), prompt_path: promptPath, next_command: finishCommand, ...(externalContext ? { external_context: externalContext } : {}) };
|
|
107
108
|
}
|
|
108
109
|
export function finishVnextPlan(context, input) {
|
|
110
|
+
return withStageSettlement(context, () => finishVnextPlanOwned(context, input));
|
|
111
|
+
}
|
|
112
|
+
function finishVnextPlanOwned(context, input) {
|
|
109
113
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
110
114
|
const project = requireProjectByRoot(context, projectRoot);
|
|
111
115
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -6,7 +6,7 @@ import { findRecentMatchingHookEvent, claimStageLifecycleHookEvent, claimStageSt
|
|
|
6
6
|
import { validateSchema } from "./schema-validation.js";
|
|
7
7
|
import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
|
|
8
8
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
9
|
-
import { readCodeCheckProfile, runCodeChecks, workspaceFingerprint } from "./code-checks.js";
|
|
9
|
+
import { readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
10
10
|
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
11
11
|
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
12
12
|
import { resolveStageTransition } from "./execution-policy.js";
|
|
@@ -39,7 +39,7 @@ export function assertStageLifecycleOwner(context, input) {
|
|
|
39
39
|
if (!project)
|
|
40
40
|
throw new AppError("not_found", "Project is not registered", 1, { project_root: projectRoot });
|
|
41
41
|
requireRun(context, project.id, input.runId);
|
|
42
|
-
|
|
42
|
+
let owner = context.db.get(`SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id
|
|
43
43
|
FROM work_sessions ws
|
|
44
44
|
JOIN works w ON w.work_id = ws.work_id
|
|
45
45
|
JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
|
|
@@ -47,6 +47,13 @@ export function assertStageLifecycleOwner(context, input) {
|
|
|
47
47
|
AND w.status IN ('running', 'paused') AND (w.parent_work_id IS NULL OR s.current_stage = ?)
|
|
48
48
|
${input.workId ? "AND w.work_id = ?" : ""}
|
|
49
49
|
ORDER BY ws.created_at DESC LIMIT 1`, input.workId ? [project.id, input.runId, input.stage, input.workId] : [project.id, input.runId, input.stage]);
|
|
50
|
+
if (!owner && input.operation === "stage_finish" && ["code", "code-review"].includes(input.stage)) {
|
|
51
|
+
const row = context.db.get("SELECT run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
52
|
+
const index = row ? JSON.parse(row.index_json) : null;
|
|
53
|
+
const completed = index?.stage_runs.find(entry => entry.stage === input.stage && entry.status === "done");
|
|
54
|
+
if (row && completed && (index?.current_stage ?? index?.stage_runs.at(-1)?.stage) === input.stage)
|
|
55
|
+
owner = context.db.get("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND w.parent_work_id IS NULL AND w.status = 'completed' AND ws.status = 'completed' AND ws.result_path = ? ORDER BY ws.created_at DESC LIMIT 1", [project.id, input.runId, path.join(row.run_root, completed.dir, "stage-report.json")]);
|
|
56
|
+
}
|
|
50
57
|
if (!owner)
|
|
51
58
|
throw new AppError("trusted_session_binding_required", "Stage lifecycle command has no running coordinator Work/Session binding", 1, { run_id: input.runId, stage: input.stage, ...(input.workId ? { work_id: input.workId } : {}) });
|
|
52
59
|
const identity = claimStageLifecycleHookEvent(context, {
|
|
@@ -135,7 +142,7 @@ export function addWorkBatch(context, input) {
|
|
|
135
142
|
if (item.parent && !keys.has(item.parent) && !existingIds.has(item.parent))
|
|
136
143
|
throw new AppError("validation", "work parent is unknown", 2, { key: item.key, parent: item.parent });
|
|
137
144
|
const now = context.now();
|
|
138
|
-
context.db.exec("
|
|
145
|
+
context.db.exec("SAVEPOINT work_batch");
|
|
139
146
|
try {
|
|
140
147
|
const allocated = nextWorkIds(context, parent.project_id, items.map((item) => item.key));
|
|
141
148
|
const ids = new Map(items.map((item, index) => [item.key, allocated[index]]));
|
|
@@ -146,14 +153,15 @@ export function addWorkBatch(context, input) {
|
|
|
146
153
|
for (const item of proposed)
|
|
147
154
|
context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, started_at, created_at, updated_at, completed_at)
|
|
148
155
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', NULL, NULL, ?, ?, NULL)`, [item.id, parent.project_id, parent.run_id, item.parentId, item.task, item.launch_policy ?? "reuse_allowed", item.result_schema ?? null, item.payload ? JSON.stringify(item.payload) : null, JSON.stringify(item.dependencies), now, now]);
|
|
149
|
-
context.db.exec("COMMIT");
|
|
150
156
|
for (const item of proposed)
|
|
151
157
|
appendFlowRunTimelineEvent(context, parent.project_id, parent.run_id, { type: "work_materialized", work_id: item.id, parent_work_id: item.parentId, depends_on: item.dependencies, launch_policy: item.launch_policy ?? "reuse_allowed" });
|
|
152
158
|
refreshRunWorkProjection(context, parent.project_id, parent.run_id);
|
|
159
|
+
context.db.exec("RELEASE work_batch");
|
|
153
160
|
return { ok: true, work_ids: Object.fromEntries(ids) };
|
|
154
161
|
}
|
|
155
162
|
catch (error) {
|
|
156
|
-
context.db.exec("ROLLBACK");
|
|
163
|
+
context.db.exec("ROLLBACK TO work_batch");
|
|
164
|
+
context.db.exec("RELEASE work_batch");
|
|
157
165
|
throw error;
|
|
158
166
|
}
|
|
159
167
|
}
|
|
@@ -496,7 +504,21 @@ export function startBoundWork(context, work, run, identity, hookEventId) {
|
|
|
496
504
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
497
505
|
return { ok: true, work_id: work.work_id, work_session_id: linkId, worker_prompt_markdown: prompt, prompt_path: promptPath, ...(recovery ? { recovery } : {}), session_binding: { source: "PreToolUse", session: { harness_id: identity.harness, session_id: identity.nativeSessionId } } };
|
|
498
506
|
}
|
|
499
|
-
|
|
507
|
+
/** Root coordinators have no worker checks: settle them inside the stage transaction. */
|
|
508
|
+
export function finishStageCoordinatorWork(context, id, result) {
|
|
509
|
+
const work = requireWork(context, id);
|
|
510
|
+
if (work.parent_work_id !== null || codePacket(work) || parsePayload(work)?.read_only)
|
|
511
|
+
throw new AppError("invalid_work_state", "Stage settlement requires a root coordinator", 2);
|
|
512
|
+
const run = requireRun(context, work.project_id, work.run_id);
|
|
513
|
+
const link = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [id]);
|
|
514
|
+
if (!link)
|
|
515
|
+
throw new AppError("runtime_missing", "Coordinator has no open Work/Session link", 1);
|
|
516
|
+
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link.result_path);
|
|
517
|
+
finishFanInWork(context, id, result);
|
|
518
|
+
context.db.run("UPDATE sessions SET status = 'idle', updated_at = ? WHERE project_id = ? AND session_id = ? AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')", [context.now(), work.project_id, link.session_id, link.session_id]);
|
|
519
|
+
settleRecoveryWork(context, id, "completed");
|
|
520
|
+
}
|
|
521
|
+
export function finishWork(context, id, result, progress, retry) { return settle(context, id, "completed", result, progress, retry); }
|
|
500
522
|
/** Structured fan-in closes a parent after its Session was handed to a child. */
|
|
501
523
|
export function finishFanInWork(context, id, result) { const work = requireWork(context, id); assertRunMutationAllowed(context, work.project_id, work.run_id); if (work.status !== "running")
|
|
502
524
|
throw new AppError("invalid_work_state", "Fan-in Work is not running", 2, { work_id: work.work_id, status: work.status }); if (context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created','running','paused') LIMIT 1", [work.work_id]))
|
|
@@ -571,7 +593,7 @@ export function retryWork(context, id, reason) {
|
|
|
571
593
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
572
594
|
return { ok: true, work_id: work.work_id, archived_attempt: path.relative(requireRunHome(run), archive).split(path.sep).join("/"), reason };
|
|
573
595
|
}
|
|
574
|
-
async function settle(context, id, status, result, progress) {
|
|
596
|
+
async function settle(context, id, status, result, progress, retry) {
|
|
575
597
|
const work = requireWork(context, id);
|
|
576
598
|
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
577
599
|
id = work.work_id;
|
|
@@ -605,7 +627,9 @@ async function settle(context, id, status, result, progress) {
|
|
|
605
627
|
const requiredChecks = packet.repair?.verification_check_refs?.length
|
|
606
628
|
? uniqueChecks([...packet.checks.filter((check) => check.run_at === "work"), ...packet.repair.verification_check_refs])
|
|
607
629
|
: packet.checks.filter((check) => check.run_at === "work");
|
|
608
|
-
|
|
630
|
+
if (retry)
|
|
631
|
+
validateFailedCheckRetry(context, { projectId: work.project_id, runId: work.run_id, receiptId: retry.receiptId, declarations: requiredChecks, reason: retry.reason });
|
|
632
|
+
receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: requiredChecks, ...(retry ? { retryReceiptId: retry.receiptId, retryReason: retry.reason } : {}), ...(progress ? { progress } : {}) });
|
|
609
633
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
610
634
|
if (failed.length)
|
|
611
635
|
throw new AppError("work_checks_failed", "Work remains running because required checks failed", 2, { work_id: id, failures: failed, all_receipts: receipts });
|
|
@@ -3,33 +3,49 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
6
|
-
import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
|
|
6
|
+
import { codeExecutionEnvironment, reconcileWorkspaceOwnership, runCheck, workspaceOperationId } from "./code-checks.js";
|
|
7
7
|
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedOwnerIsAlive, managedProcessStatus, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
|
|
8
8
|
/** CODE and MERGE prepare their own checkout through the same managed executor.
|
|
9
9
|
* Bootstrap is a project operation, not an alias classified by the engine as a test. */
|
|
10
10
|
export async function runWorkspaceBootstrap(context, input) {
|
|
11
|
-
|
|
11
|
+
reconcileWorkspaceOwnership(context, input.workspaceRoot);
|
|
12
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
13
|
+
const operationId = workspaceOperationId(input.workspaceRoot);
|
|
14
|
+
const identity = crypto.createHash("sha256").update(JSON.stringify([input.projectId, input.runId, generation, path.resolve(input.workspaceRoot), input.command])).digest("hex");
|
|
12
15
|
const pending = managedProcessStatus(context).find(item => item.operation_id === operationId && ["starting", "running", "stopping"].includes(item.state));
|
|
13
16
|
if (pending) {
|
|
17
|
+
if (pending.kind !== "workspace-bootstrap")
|
|
18
|
+
throw new AppError("bootstrap_in_progress", "A check currently owns this workspace; wait for it before bootstrapping", 2, { process_id: pending.id, stdout_path: pending.stdout_path, stderr_path: pending.stderr_path });
|
|
14
19
|
if (processTreeIsAlive(pending) || (pending.state === "starting" && managedOwnerIsAlive(pending) !== false)) {
|
|
15
20
|
throw new AppError("bootstrap_in_progress", "Workspace preparation is still running; inspect its logs and wait for the existing process, do not launch another copy", 2, { process_id: pending.id, stdout_path: pending.stdout_path, stderr_path: pending.stderr_path });
|
|
16
21
|
}
|
|
17
|
-
|
|
18
|
-
let completion = null;
|
|
22
|
+
let pendingIdentity = null;
|
|
19
23
|
try {
|
|
20
|
-
|
|
24
|
+
const metadata = JSON.parse(pending.metadata_json);
|
|
25
|
+
pendingIdentity = typeof metadata.bootstrap_identity === "string" ? metadata.bootstrap_identity : null;
|
|
26
|
+
}
|
|
27
|
+
catch { /* Legacy records have no usable identity. */ }
|
|
28
|
+
if (pendingIdentity !== identity) {
|
|
29
|
+
finishManagedProcess(context, { id: pending.id, leaseToken: pending.lease_token, state: "failed", reason: "bootstrap_identity_superseded" });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const completionPath = path.join(path.dirname(pending.stdout_path), "completion.json");
|
|
33
|
+
let completion = null;
|
|
34
|
+
try {
|
|
35
|
+
completion = JSON.parse(fs.readFileSync(completionPath, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch { /* Interrupted shell has no terminal marker. */ }
|
|
38
|
+
const exitCode = typeof completion?.exit_code === "number" ? completion.exit_code : null;
|
|
39
|
+
const error = exitCode === null ? "bootstrap ended without a completion marker; inspect retained logs before retrying" : null;
|
|
40
|
+
finishManagedProcess(context, { id: pending.id, leaseToken: pending.lease_token, state: exitCode === 0 ? "stopped" : "failed", reason: error ?? "bootstrap_completion_reconciled" });
|
|
41
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
42
|
+
return result(pending.id, pending.stdout_path, pending.stderr_path, completionPath, exitCode, error);
|
|
21
43
|
}
|
|
22
|
-
catch { /* Interrupted shell has no terminal marker. */ }
|
|
23
|
-
const exitCode = typeof completion?.exit_code === "number" ? completion.exit_code : null;
|
|
24
|
-
const error = exitCode === null ? "bootstrap ended without a completion marker; inspect retained logs before retrying" : null;
|
|
25
|
-
finishManagedProcess(context, { id: pending.id, leaseToken: pending.lease_token, state: exitCode === 0 ? "stopped" : "failed", reason: error ?? "bootstrap_completion_reconciled" });
|
|
26
|
-
return result(pending.id, pending.stdout_path, pending.stderr_path, completionPath, exitCode, error);
|
|
27
44
|
}
|
|
28
|
-
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
29
45
|
const directory = path.join(input.runHome, input.artifactDir, `bootstrap-${crypto.randomUUID()}`);
|
|
30
46
|
fs.mkdirSync(directory, { recursive: true });
|
|
31
47
|
const stdoutPath = path.join(directory, "stdout.log"), stderrPath = path.join(directory, "stderr.log"), completionPath = path.join(directory, "completion.json");
|
|
32
|
-
const managed = registerManagedProcess(context, { kind: "workspace-bootstrap", ownerId: operationId
|
|
48
|
+
const managed = registerManagedProcess(context, { kind: "workspace-bootstrap", ownerId: `${operationId}:${identity}`, ownerPid: process.pid, operationId, uniqueActiveOperation: true, projectId: input.projectId, runId: input.runId, stdoutPath, stderrPath, metadata: { command: input.command, workspace_root: input.workspaceRoot, bootstrap_identity: identity } });
|
|
33
49
|
const stdout = fs.openSync(stdoutPath, "a"), stderr = fs.openSync(stderrPath, "a");
|
|
34
50
|
try {
|
|
35
51
|
try {
|
|
@@ -44,6 +60,7 @@ export async function runWorkspaceBootstrap(context, input) {
|
|
|
44
60
|
const current = managedProcessStatus(context).find(item => item.id === managed.id);
|
|
45
61
|
if (processTreeIsAlive(current))
|
|
46
62
|
throw new AppError("bootstrap_tree_not_settled", "Bootstrap shell exited but still owns live processes; resources were retained for inspection", 2, { process_id: managed.id, stdout_path: stdoutPath, stderr_path: stderrPath });
|
|
63
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
47
64
|
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: completed.exitCode === 0 && !completed.error ? "stopped" : "failed", reason: completed.error });
|
|
48
65
|
return result(managed.id, stdoutPath, stderrPath, completionPath, completed.exitCode, completed.error);
|
|
49
66
|
}
|
package/dist/storage/database.js
CHANGED
|
@@ -176,6 +176,9 @@ export function ensureVnextWorkStorage(db) {
|
|
|
176
176
|
command TEXT NOT NULL,
|
|
177
177
|
input_hash TEXT NOT NULL DEFAULT '',
|
|
178
178
|
verification_epoch TEXT NOT NULL DEFAULT '',
|
|
179
|
+
reservation_sequence INTEGER NOT NULL DEFAULT 0,
|
|
180
|
+
retry_of_receipt_id TEXT,
|
|
181
|
+
retry_reason TEXT,
|
|
179
182
|
status TEXT NOT NULL,
|
|
180
183
|
exit_code INTEGER,
|
|
181
184
|
stdout_path TEXT NOT NULL,
|
|
@@ -1030,6 +1033,9 @@ function migrate(db) {
|
|
|
1030
1033
|
ensureColumn(db, "check_receipts", "gate", "ALTER TABLE check_receipts ADD COLUMN gate TEXT NOT NULL DEFAULT 'work'");
|
|
1031
1034
|
ensureColumn(db, "check_receipts", "input_hash", "ALTER TABLE check_receipts ADD COLUMN input_hash TEXT NOT NULL DEFAULT ''");
|
|
1032
1035
|
ensureColumn(db, "check_receipts", "verification_epoch", "ALTER TABLE check_receipts ADD COLUMN verification_epoch TEXT NOT NULL DEFAULT ''");
|
|
1036
|
+
ensureColumn(db, "check_receipts", "reservation_sequence", "ALTER TABLE check_receipts ADD COLUMN reservation_sequence INTEGER NOT NULL DEFAULT 0");
|
|
1037
|
+
ensureColumn(db, "check_receipts", "retry_of_receipt_id", "ALTER TABLE check_receipts ADD COLUMN retry_of_receipt_id TEXT");
|
|
1038
|
+
ensureColumn(db, "check_receipts", "retry_reason", "ALTER TABLE check_receipts ADD COLUMN retry_reason TEXT");
|
|
1033
1039
|
ensureColumn(db, "check_receipts", "before_fingerprint", "ALTER TABLE check_receipts ADD COLUMN before_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
1034
1040
|
ensureColumn(db, "check_receipts", "after_fingerprint", "ALTER TABLE check_receipts ADD COLUMN after_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
1035
1041
|
ensureColumn(db, "check_receipts", "profile_hash", "ALTER TABLE check_receipts ADD COLUMN profile_hash TEXT");
|