@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.75
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 +404 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +121 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +211 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +224 -216
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +605 -157
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
6
|
+
import { codeExecutionEnvironment, reconcileWorkspaceOwnership, runCheck, workspaceOperationId } from "./code-checks.js";
|
|
7
|
+
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedOwnerIsAlive, managedProcessStatus, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
|
|
8
|
+
/** CODE and MERGE prepare their own checkout through the same managed executor.
|
|
9
|
+
* Bootstrap is a project operation, not an alias classified by the engine as a test. */
|
|
10
|
+
export async function runWorkspaceBootstrap(context, input) {
|
|
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");
|
|
15
|
+
const pending = managedProcessStatus(context).find(item => item.operation_id === operationId && ["starting", "running", "stopping"].includes(item.state));
|
|
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 });
|
|
19
|
+
if (processTreeIsAlive(pending) || (pending.state === "starting" && managedOwnerIsAlive(pending) !== false)) {
|
|
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 });
|
|
21
|
+
}
|
|
22
|
+
let pendingIdentity = null;
|
|
23
|
+
try {
|
|
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);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const directory = path.join(input.runHome, input.artifactDir, `bootstrap-${crypto.randomUUID()}`);
|
|
46
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
47
|
+
const stdoutPath = path.join(directory, "stdout.log"), stderrPath = path.join(directory, "stderr.log"), completionPath = path.join(directory, "completion.json");
|
|
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 } });
|
|
49
|
+
const stdout = fs.openSync(stdoutPath, "a"), stderr = fs.openSync(stderrPath, "a");
|
|
50
|
+
try {
|
|
51
|
+
try {
|
|
52
|
+
input.progress?.(`workspace bootstrap preparing: ${input.command}; logs: ${directory}`);
|
|
53
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: "failed", reason: "dispatch_fenced_before_spawn" });
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const completed = await runCheck(input.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_CHECK_COMPLETION_FILE: completionPath }, stdout, stderr, elapsed => input.progress?.(`workspace bootstrap still running (${elapsed}s); logs: ${directory}; next report in 15s`), pid => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), () => heartbeatManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token }));
|
|
60
|
+
const current = managedProcessStatus(context).find(item => item.id === managed.id);
|
|
61
|
+
if (processTreeIsAlive(current))
|
|
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);
|
|
64
|
+
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: completed.exitCode === 0 && !completed.error ? "stopped" : "failed", reason: completed.error });
|
|
65
|
+
return result(managed.id, stdoutPath, stderrPath, completionPath, completed.exitCode, completed.error);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
fs.closeSync(stdout);
|
|
69
|
+
fs.closeSync(stderr);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function result(id, stdout, stderr, completion, exitCode, error) {
|
|
73
|
+
// Full logs remain on disk; keep the returned prompt bounded.
|
|
74
|
+
const excerpt = (file) => fs.existsSync(file) ? fs.readFileSync(file, "utf8").slice(-64_000) : "";
|
|
75
|
+
return { exit_code: exitCode, stdout: excerpt(stdout), stderr: excerpt(stderr), error, process_id: id, stdout_path: stdout, stderr_path: stderr, completion_path: completion };
|
|
76
|
+
}
|
package/dist/shared/errors.js
CHANGED
|
@@ -9,6 +9,18 @@ export class AppError extends Error {
|
|
|
9
9
|
this.details = details;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
/** Error fields must cross JSON boundaries as data: Error.message is not enumerable. */
|
|
13
|
+
export function errorRecord(error, depth = 0) {
|
|
14
|
+
const value = error;
|
|
15
|
+
const details = value?.details && typeof value.details === "object" && !Array.isArray(value.details) ? value.details : {};
|
|
16
|
+
return {
|
|
17
|
+
code: typeof value?.code === "string" ? value.code : "unexpected",
|
|
18
|
+
message: typeof value?.message === "string" ? value.message : String(error),
|
|
19
|
+
exitCode: typeof value?.exitCode === "number" ? value.exitCode : 1,
|
|
20
|
+
details,
|
|
21
|
+
...(value?.cause && depth < 3 ? { cause: errorRecord(value.cause, depth + 1) } : {})
|
|
22
|
+
};
|
|
23
|
+
}
|
|
12
24
|
export function isAppError(error) {
|
|
13
25
|
return error instanceof AppError;
|
|
14
26
|
}
|
package/dist/storage/database.js
CHANGED
|
@@ -4,9 +4,110 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { ensureDir } from "./paths.js";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter } from "./writer-contract.js";
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
const { DatabaseSync } = require("node:sqlite");
|
|
9
10
|
const resourceDatabases = new Map();
|
|
11
|
+
/** RUN authority stays in its existing store; process leases stay in runtime.sqlite. */
|
|
12
|
+
export function ensureRunControlStorage(db) {
|
|
13
|
+
db.execSchema(`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS run_controls (
|
|
15
|
+
control_id TEXT PRIMARY KEY,
|
|
16
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
17
|
+
request_id TEXT NOT NULL, request_hash TEXT NOT NULL, request_json TEXT NOT NULL,
|
|
18
|
+
recovery_id TEXT NOT NULL REFERENCES run_recovery_guards(recovery_id),
|
|
19
|
+
action TEXT NOT NULL, status TEXT NOT NULL,
|
|
20
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
21
|
+
UNIQUE(project_id, run_id, request_id),
|
|
22
|
+
FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
|
|
23
|
+
);
|
|
24
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_control_active
|
|
25
|
+
ON run_controls(project_id, run_id) WHERE status IN ('requested','settled','resuming');
|
|
26
|
+
CREATE TABLE IF NOT EXISTS run_control_admissions (
|
|
27
|
+
control_id TEXT NOT NULL REFERENCES run_controls(control_id),
|
|
28
|
+
work_session_id TEXT NOT NULL REFERENCES work_sessions(id),
|
|
29
|
+
work_id TEXT NOT NULL REFERENCES works(work_id),
|
|
30
|
+
session_id TEXT NOT NULL, admitted_at TEXT NOT NULL,
|
|
31
|
+
PRIMARY KEY(control_id, work_session_id)
|
|
32
|
+
);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS run_control_stage_admissions (
|
|
34
|
+
control_id TEXT NOT NULL REFERENCES run_controls(control_id),
|
|
35
|
+
stage TEXT NOT NULL, attempt TEXT NOT NULL,
|
|
36
|
+
work_session_id TEXT NOT NULL REFERENCES work_sessions(id),
|
|
37
|
+
PRIMARY KEY(control_id, stage)
|
|
38
|
+
);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS run_control_operations (
|
|
40
|
+
operation_id TEXT PRIMARY KEY, control_id TEXT NOT NULL REFERENCES run_controls(control_id),
|
|
41
|
+
target_json TEXT NOT NULL, status TEXT NOT NULL, receipt_json TEXT, error_json TEXT,
|
|
42
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
CREATE TABLE IF NOT EXISTS run_control_workers (
|
|
45
|
+
control_id TEXT PRIMARY KEY REFERENCES run_controls(control_id),
|
|
46
|
+
owner_token TEXT NOT NULL, owner_pid INTEGER,
|
|
47
|
+
status TEXT NOT NULL, process_id TEXT, process_lease_token TEXT,
|
|
48
|
+
resource_home TEXT NOT NULL, state_dir TEXT NOT NULL,
|
|
49
|
+
snapshot_json TEXT, error_json TEXT,
|
|
50
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
export function ensureRunControllerStorage(db) {
|
|
55
|
+
db.execSchema(`
|
|
56
|
+
CREATE TABLE IF NOT EXISTS run_controllers (
|
|
57
|
+
controller_id TEXT PRIMARY KEY,
|
|
58
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
59
|
+
request_id TEXT NOT NULL, request_hash TEXT NOT NULL,
|
|
60
|
+
generation INTEGER NOT NULL, status TEXT NOT NULL,
|
|
61
|
+
owner_token TEXT NOT NULL, process_id TEXT, process_lease_token TEXT,
|
|
62
|
+
owner_pid INTEGER, manifest_json TEXT NOT NULL, state_json TEXT NOT NULL,
|
|
63
|
+
state_dir TEXT NOT NULL, last_error_json TEXT,
|
|
64
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
65
|
+
FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id),
|
|
66
|
+
UNIQUE(project_id, run_id, request_id)
|
|
67
|
+
);
|
|
68
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_controller_owner
|
|
69
|
+
ON run_controllers(project_id, run_id)
|
|
70
|
+
WHERE status NOT IN ('completed', 'cancelled', 'superseded');
|
|
71
|
+
CREATE TABLE IF NOT EXISTS run_controller_events (
|
|
72
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
+
controller_id TEXT NOT NULL REFERENCES run_controllers(controller_id),
|
|
74
|
+
event_type TEXT NOT NULL, data_json TEXT NOT NULL, created_at TEXT NOT NULL
|
|
75
|
+
);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_run_controller_events
|
|
77
|
+
ON run_controller_events(controller_id, sequence);
|
|
78
|
+
CREATE TABLE IF NOT EXISTS run_controller_operations (
|
|
79
|
+
operation_id TEXT PRIMARY KEY,
|
|
80
|
+
controller_id TEXT NOT NULL REFERENCES run_controllers(controller_id),
|
|
81
|
+
generation INTEGER NOT NULL, input_hash TEXT NOT NULL,
|
|
82
|
+
input_json TEXT NOT NULL, status TEXT NOT NULL,
|
|
83
|
+
receipt_json TEXT, error_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
/** Recovery guards apply to every RUN kind, including custom eval fixtures. */
|
|
88
|
+
export function ensureRecoveryStorage(db) {
|
|
89
|
+
db.exec(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS run_recovery_guards (
|
|
91
|
+
recovery_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, run_id TEXT NOT NULL, generation INTEGER NOT NULL, status TEXT NOT NULL, interruption_json TEXT NOT NULL, settlement_json TEXT, capture_path TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, sealed_at TEXT, resumed_at TEXT, UNIQUE(project_id, run_id, generation), FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
|
|
92
|
+
);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_run_recovery_guards_run ON run_recovery_guards(project_id, run_id, generation DESC);
|
|
94
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_recovery_guards_active ON run_recovery_guards(project_id, run_id) WHERE status IN ('draining', 'sealed', 'resuming');
|
|
95
|
+
CREATE TABLE IF NOT EXISTS work_recovery_segments (
|
|
96
|
+
recovery_id TEXT NOT NULL, work_id TEXT NOT NULL, generation INTEGER NOT NULL, prior_started_at TEXT, evidence_path TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(recovery_id, work_id), FOREIGN KEY(recovery_id) REFERENCES run_recovery_guards(recovery_id), FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
97
|
+
);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_work_recovery_segments_work ON work_recovery_segments(work_id, generation DESC);
|
|
99
|
+
CREATE TABLE IF NOT EXISTS run_recovery_bindings (
|
|
100
|
+
recovery_id TEXT PRIMARY KEY REFERENCES run_recovery_guards(recovery_id),
|
|
101
|
+
work_id TEXT NOT NULL REFERENCES works(work_id), prior_work_session_id TEXT NOT NULL,
|
|
102
|
+
session_id TEXT NOT NULL, harness TEXT NOT NULL, provider_session_id TEXT NOT NULL,
|
|
103
|
+
daemon_id TEXT NOT NULL, status TEXT NOT NULL, hook_event_key TEXT,
|
|
104
|
+
created_at TEXT NOT NULL, accepted_at TEXT
|
|
105
|
+
);
|
|
106
|
+
`);
|
|
107
|
+
ensureColumn(db, "run_recovery_bindings", "packet_json", "ALTER TABLE run_recovery_bindings ADD COLUMN packet_json TEXT");
|
|
108
|
+
ensureColumn(db, "run_recovery_bindings", "packet_sha256", "ALTER TABLE run_recovery_bindings ADD COLUMN packet_sha256 TEXT");
|
|
109
|
+
ensureColumn(db, "run_recovery_bindings", "runtime_dispatch_json", "ALTER TABLE run_recovery_bindings ADD COLUMN runtime_dispatch_json TEXT");
|
|
110
|
+
}
|
|
10
111
|
/** Creates the single Work/Session authority used by a fresh vNext beta runtime. */
|
|
11
112
|
export function ensureVnextWorkStorage(db) {
|
|
12
113
|
const legacy = db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('vnext_works', 'vnext_agent_turns', 'flow_agent_turns') LIMIT 1");
|
|
@@ -16,7 +117,7 @@ export function ensureVnextWorkStorage(db) {
|
|
|
16
117
|
if (existing && (!/task TEXT NOT NULL/.test(existing.sql) || /launch_token|adapter_session_id/.test(existing.sql))) {
|
|
17
118
|
throw new AppError("beta_storage_recreate_required", "This beta requires a fresh DD_FLOW_HOME; the existing works schema is not compatible", 1);
|
|
18
119
|
}
|
|
19
|
-
db.
|
|
120
|
+
db.execSchema(`
|
|
20
121
|
CREATE TABLE IF NOT EXISTS works (
|
|
21
122
|
work_id TEXT PRIMARY KEY,
|
|
22
123
|
project_id TEXT NOT NULL,
|
|
@@ -46,6 +147,7 @@ export function ensureVnextWorkStorage(db) {
|
|
|
46
147
|
status TEXT NOT NULL,
|
|
47
148
|
prompt_path TEXT NOT NULL,
|
|
48
149
|
result_path TEXT,
|
|
150
|
+
start_receipt_json TEXT,
|
|
49
151
|
created_at TEXT NOT NULL,
|
|
50
152
|
updated_at TEXT NOT NULL,
|
|
51
153
|
completed_at TEXT,
|
|
@@ -53,6 +155,16 @@ export function ensureVnextWorkStorage(db) {
|
|
|
53
155
|
);
|
|
54
156
|
CREATE INDEX IF NOT EXISTS idx_work_sessions_work ON work_sessions(work_id, created_at DESC);
|
|
55
157
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_sessions_open ON work_sessions(work_id) WHERE status = 'running';
|
|
158
|
+
|
|
159
|
+
CREATE TABLE IF NOT EXISTS run_recovery_guards (
|
|
160
|
+
recovery_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, run_id TEXT NOT NULL, generation INTEGER NOT NULL, status TEXT NOT NULL, interruption_json TEXT NOT NULL, settlement_json TEXT, capture_path TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, sealed_at TEXT, resumed_at TEXT, UNIQUE(project_id, run_id, generation), FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
|
|
161
|
+
);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_run_recovery_guards_run ON run_recovery_guards(project_id, run_id, generation DESC);
|
|
163
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_recovery_guards_active ON run_recovery_guards(project_id, run_id) WHERE status IN ('draining', 'sealed', 'resuming');
|
|
164
|
+
CREATE TABLE IF NOT EXISTS work_recovery_segments (
|
|
165
|
+
recovery_id TEXT NOT NULL, work_id TEXT NOT NULL, generation INTEGER NOT NULL, prior_started_at TEXT, evidence_path TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(recovery_id, work_id), FOREIGN KEY(recovery_id) REFERENCES run_recovery_guards(recovery_id), FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
166
|
+
);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_work_recovery_segments_work ON work_recovery_segments(work_id, generation DESC);
|
|
56
168
|
CREATE TABLE IF NOT EXISTS check_receipts (
|
|
57
169
|
id TEXT PRIMARY KEY,
|
|
58
170
|
project_id TEXT NOT NULL,
|
|
@@ -65,6 +177,9 @@ export function ensureVnextWorkStorage(db) {
|
|
|
65
177
|
command TEXT NOT NULL,
|
|
66
178
|
input_hash TEXT NOT NULL DEFAULT '',
|
|
67
179
|
verification_epoch TEXT NOT NULL DEFAULT '',
|
|
180
|
+
reservation_sequence INTEGER NOT NULL DEFAULT 0,
|
|
181
|
+
retry_of_receipt_id TEXT,
|
|
182
|
+
retry_reason TEXT,
|
|
68
183
|
status TEXT NOT NULL,
|
|
69
184
|
exit_code INTEGER,
|
|
70
185
|
stdout_path TEXT NOT NULL,
|
|
@@ -82,6 +197,34 @@ export function ensureVnextWorkStorage(db) {
|
|
|
82
197
|
);
|
|
83
198
|
CREATE INDEX IF NOT EXISTS idx_check_receipts_run ON check_receipts(project_id, run_id, started_at);
|
|
84
199
|
`);
|
|
200
|
+
// A committed Work start is authoritative even if publishing its prompt
|
|
201
|
+
// files is interrupted after commit. Keep the exact bytes for repair.
|
|
202
|
+
db.writeTransaction(() => {
|
|
203
|
+
const columns = db.all("PRAGMA table_info(work_sessions)");
|
|
204
|
+
if (!columns.some((column) => column.name === "start_receipt_json"))
|
|
205
|
+
db.execSchema("ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** Durable intent for a provider-root external Work. It is deliberately
|
|
209
|
+
* separate from `work_sessions`: only a trusted hook may bind that Session. */
|
|
210
|
+
export function ensureWorkLaunchStorage(db) {
|
|
211
|
+
db.execSchema(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS work_launches (
|
|
213
|
+
operation_id TEXT PRIMARY KEY,
|
|
214
|
+
work_id TEXT NOT NULL UNIQUE,
|
|
215
|
+
profile_json TEXT NOT NULL,
|
|
216
|
+
profile_hash TEXT NOT NULL,
|
|
217
|
+
status TEXT NOT NULL,
|
|
218
|
+
state_dir TEXT NOT NULL,
|
|
219
|
+
journal_path TEXT NOT NULL,
|
|
220
|
+
provider_session_id TEXT,
|
|
221
|
+
adapter_receipt_json TEXT,
|
|
222
|
+
created_at TEXT NOT NULL,
|
|
223
|
+
updated_at TEXT NOT NULL,
|
|
224
|
+
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
225
|
+
);
|
|
226
|
+
CREATE INDEX IF NOT EXISTS idx_work_launches_status ON work_launches(status, updated_at DESC);
|
|
227
|
+
`);
|
|
85
228
|
}
|
|
86
229
|
export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
87
230
|
const dbPath = path.join(ddFlowHome, "db.sqlite");
|
|
@@ -92,23 +235,45 @@ export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
|
92
235
|
if (!exists || mode === "migrate")
|
|
93
236
|
ensureDir(ddFlowHome);
|
|
94
237
|
const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
95
|
-
|
|
238
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
239
|
+
if (mode !== "read_existing") {
|
|
240
|
+
try {
|
|
241
|
+
registerStoreWriter(db);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
db.close();
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
configureDatabase(db, mode);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
db.close();
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
96
255
|
// Internal additive schema changes must be available to every write command.
|
|
97
256
|
// Higher-level Memory Bank migrations remain explicit in services/migrations.
|
|
98
|
-
if (mode !== "read_existing")
|
|
99
|
-
|
|
257
|
+
if (mode !== "read_existing") {
|
|
258
|
+
try {
|
|
259
|
+
const initialize = () => { migrate(db); ensureRecoveryStorage(db); };
|
|
260
|
+
const fresh = !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
|
|
261
|
+
if (fresh || readStoreWriterContract(db) !== null)
|
|
262
|
+
executeStoreSchema(db, initialize, fresh);
|
|
263
|
+
else
|
|
264
|
+
initialize();
|
|
265
|
+
// This migration owns a VACUUM backup and a separate data transaction.
|
|
266
|
+
migrateSessionStorageKeys(db, dbPath);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
db.close();
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
100
273
|
if (mode === "read_existing")
|
|
101
274
|
db.exec("PRAGMA query_only = ON");
|
|
102
275
|
db.exec("PRAGMA foreign_keys = ON");
|
|
103
|
-
return
|
|
104
|
-
path: dbPath,
|
|
105
|
-
writable: mode !== "read_existing",
|
|
106
|
-
close: () => db.close?.(),
|
|
107
|
-
exec: (sql) => db.exec(sql),
|
|
108
|
-
run: (sql, params = []) => db.prepare(sql).run(...params),
|
|
109
|
-
get: (sql, params = []) => db.prepare(sql).get(...params),
|
|
110
|
-
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
111
|
-
};
|
|
276
|
+
return databaseFacade(dbPath, mode !== "read_existing", db);
|
|
112
277
|
}
|
|
113
278
|
/**
|
|
114
279
|
* Opens the small host-wide resource registry. It is intentionally separate
|
|
@@ -122,9 +287,25 @@ export function getResourceDatabase(resourceHome) {
|
|
|
122
287
|
if (cached)
|
|
123
288
|
return cached;
|
|
124
289
|
const db = new DatabaseSync(dbPath);
|
|
125
|
-
|
|
290
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
291
|
+
try {
|
|
292
|
+
registerStoreWriter(db);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
db.close();
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
configureDatabase(db, "initialize");
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
db.close();
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
126
305
|
db.exec("PRAGMA foreign_keys = ON");
|
|
127
|
-
db.
|
|
306
|
+
const fresh = !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
|
|
307
|
+
try {
|
|
308
|
+
executeStoreSchema(db, `
|
|
128
309
|
CREATE TABLE IF NOT EXISTS managed_processes (
|
|
129
310
|
id TEXT PRIMARY KEY,
|
|
130
311
|
kind TEXT NOT NULL,
|
|
@@ -163,16 +344,16 @@ export function getResourceDatabase(resourceHome) {
|
|
|
163
344
|
);
|
|
164
345
|
CREATE INDEX IF NOT EXISTS idx_managed_resources_lease
|
|
165
346
|
ON managed_resources(lease_expires_at, updated_at);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
};
|
|
347
|
+
`, fresh);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
db.close();
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
const database = databaseFacade(dbPath, true, db, () => {
|
|
354
|
+
resourceDatabases.delete(dbPath);
|
|
355
|
+
db.close?.();
|
|
356
|
+
});
|
|
176
357
|
resourceDatabases.set(dbPath, database);
|
|
177
358
|
return database;
|
|
178
359
|
}
|
|
@@ -181,19 +362,131 @@ function emptyReadOnlyDatabase(dbPath) {
|
|
|
181
362
|
path: dbPath,
|
|
182
363
|
writable: false,
|
|
183
364
|
exec: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
365
|
+
execSchema: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
366
|
+
beginWriteTransaction: () => { throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath }); },
|
|
367
|
+
writeTransaction: () => { throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath }); },
|
|
184
368
|
run: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
185
369
|
get: () => undefined,
|
|
186
370
|
all: () => []
|
|
187
371
|
};
|
|
188
372
|
}
|
|
373
|
+
function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
|
|
374
|
+
let reserved = false;
|
|
375
|
+
let poisoned = false;
|
|
376
|
+
let savepoint = 0;
|
|
377
|
+
const assertUsable = () => { if (poisoned)
|
|
378
|
+
throw new AppError("write_transaction_unresolved", "Transaction cleanup failed; reopen the connection before continuing", 1); };
|
|
379
|
+
const beginWriteTransaction = () => {
|
|
380
|
+
assertUsable();
|
|
381
|
+
if (!writable)
|
|
382
|
+
throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath });
|
|
383
|
+
db.exec("BEGIN IMMEDIATE");
|
|
384
|
+
reserved = true;
|
|
385
|
+
};
|
|
386
|
+
const rollback = (sql, original) => {
|
|
387
|
+
try {
|
|
388
|
+
db.exec(sql);
|
|
389
|
+
}
|
|
390
|
+
catch (cleanup) {
|
|
391
|
+
poisoned = true;
|
|
392
|
+
if (original instanceof AppError)
|
|
393
|
+
throw new AppError(original.code, original.message, original.exitCode, { ...original.details, transaction_cleanup_error: errorDetails(cleanup) });
|
|
394
|
+
throw new AppError("write_transaction_failed", "Write transaction failed and SQLite cleanup also failed", 1, { cause: errorDetails(original), transaction_cleanup_error: errorDetails(cleanup) });
|
|
395
|
+
}
|
|
396
|
+
throw original;
|
|
397
|
+
};
|
|
398
|
+
const writeTransaction = (operation) => {
|
|
399
|
+
assertUsable();
|
|
400
|
+
if (!writable)
|
|
401
|
+
throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath });
|
|
402
|
+
if (db.isTransaction && !reserved)
|
|
403
|
+
throw new AppError("write_transaction_unowned", "Cannot write inside an unowned transaction or read snapshot", 1);
|
|
404
|
+
const nested = db.isTransaction;
|
|
405
|
+
const marker = `dd_flow_write_${++savepoint}`;
|
|
406
|
+
try {
|
|
407
|
+
if (nested)
|
|
408
|
+
db.exec(`SAVEPOINT ${marker}`);
|
|
409
|
+
else
|
|
410
|
+
beginWriteTransaction();
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
throw new AppError("storage_write_failed", "Cannot reserve SQLite writer", 1, { cause: errorDetails(error), transaction_phase: "begin", transaction_rolled_back: true });
|
|
414
|
+
}
|
|
415
|
+
let phase = "body";
|
|
416
|
+
try {
|
|
417
|
+
const value = operation();
|
|
418
|
+
if (value && typeof value.then === "function") {
|
|
419
|
+
void Promise.resolve(value).catch(() => { });
|
|
420
|
+
throw new AppError("write_transaction_async", "Write transaction callback must be synchronous", 1);
|
|
421
|
+
}
|
|
422
|
+
phase = "commit";
|
|
423
|
+
if (nested)
|
|
424
|
+
db.exec(`RELEASE ${marker}`);
|
|
425
|
+
else
|
|
426
|
+
db.exec("COMMIT");
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
try {
|
|
431
|
+
return rollback(nested ? `ROLLBACK TO ${marker}; RELEASE ${marker}` : "ROLLBACK", error);
|
|
432
|
+
}
|
|
433
|
+
catch (failure) {
|
|
434
|
+
if (failure === error && !(error instanceof AppError) && typeof error === "object" && error !== null && "code" in error && String(error.code).startsWith("ERR_SQLITE"))
|
|
435
|
+
throw new AppError("storage_write_failed", "SQLite write failed", 1, { cause: errorDetails(error), transaction_phase: phase, transaction_rolled_back: true });
|
|
436
|
+
throw failure;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
if (!db.isTransaction)
|
|
441
|
+
reserved = false;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
path: dbPath,
|
|
446
|
+
writable,
|
|
447
|
+
close,
|
|
448
|
+
exec: (sql) => { assertUsable(); try {
|
|
449
|
+
db.exec(sql);
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
if (!db.isTransaction)
|
|
453
|
+
reserved = false;
|
|
454
|
+
} },
|
|
455
|
+
execSchema: (sql) => { assertUsable(); if (db.isTransaction && !reserved)
|
|
456
|
+
throw new AppError("write_transaction_unowned", "Cannot change schema inside an unowned transaction", 1); executeStoreSchema(db, sql); },
|
|
457
|
+
beginWriteTransaction,
|
|
458
|
+
writeTransaction,
|
|
459
|
+
run: (sql, params = []) => { assertUsable(); return db.prepare(sql).run(...params); },
|
|
460
|
+
get: (sql, params = []) => db.prepare(sql).get(...params),
|
|
461
|
+
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function errorDetails(error) {
|
|
465
|
+
const value = error;
|
|
466
|
+
return { message: String(value?.message ?? error), ...(typeof value?.code === "string" ? { code: value.code } : {}), ...(typeof value?.errcode === "number" ? { sqlite_extended_code: value.errcode } : {}) };
|
|
467
|
+
}
|
|
189
468
|
function configureDatabase(db, mode) {
|
|
190
|
-
db.exec("PRAGMA busy_timeout = 4000");
|
|
191
469
|
if (mode !== "read_existing") {
|
|
192
|
-
|
|
470
|
+
// Concurrent first openers can collide while changing journal mode, even
|
|
471
|
+
// with busy_timeout set. Retrying this configuration does not replay writes.
|
|
472
|
+
const deadline = performance.now() + 4000, pause = new Int32Array(new SharedArrayBuffer(4));
|
|
473
|
+
for (;;) {
|
|
474
|
+
try {
|
|
475
|
+
const current = db.prepare("PRAGMA journal_mode").get();
|
|
476
|
+
if (current.journal_mode !== "wal")
|
|
477
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
if ((error.errcode ?? 0) % 256 !== 5 || performance.now() >= deadline)
|
|
482
|
+
throw error;
|
|
483
|
+
Atomics.wait(pause, 0, 0, Math.min(25, Math.max(1, deadline - performance.now())));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
193
486
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
194
487
|
}
|
|
195
488
|
}
|
|
196
|
-
function migrate(db
|
|
489
|
+
function migrate(db) {
|
|
197
490
|
db.exec(`
|
|
198
491
|
CREATE TABLE IF NOT EXISTS projects (
|
|
199
492
|
id TEXT PRIMARY KEY,
|
|
@@ -564,6 +857,7 @@ function migrate(db, dbPath) {
|
|
|
564
857
|
target_branch TEXT NOT NULL,
|
|
565
858
|
enqueue_target_head TEXT,
|
|
566
859
|
execution_target_head TEXT,
|
|
860
|
+
accepted_tree TEXT,
|
|
567
861
|
integration_commit TEXT,
|
|
568
862
|
execution_route TEXT NOT NULL,
|
|
569
863
|
status TEXT NOT NULL,
|
|
@@ -573,6 +867,7 @@ function migrate(db, dbPath) {
|
|
|
573
867
|
lock_acquired_at TEXT,
|
|
574
868
|
checkpoint TEXT NOT NULL DEFAULT 'queued',
|
|
575
869
|
profile_hash TEXT,
|
|
870
|
+
replacement_of_merge_request_id TEXT,
|
|
576
871
|
adapter_receipt_json TEXT,
|
|
577
872
|
result_json TEXT,
|
|
578
873
|
last_error_json TEXT,
|
|
@@ -584,8 +879,8 @@ function migrate(db, dbPath) {
|
|
|
584
879
|
);
|
|
585
880
|
CREATE INDEX IF NOT EXISTS idx_merge_requests_fifo
|
|
586
881
|
ON merge_requests(project_id, status, created_at, merge_request_id);
|
|
587
|
-
CREATE
|
|
588
|
-
ON merge_requests(project_id, run_id);
|
|
882
|
+
CREATE INDEX IF NOT EXISTS idx_merge_requests_run
|
|
883
|
+
ON merge_requests(project_id, run_id, created_at);
|
|
589
884
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_merge_requests_active_project
|
|
590
885
|
ON merge_requests(project_id)
|
|
591
886
|
WHERE status IN ('active', 'waiting_user', 'action_required', 'recovery_required');
|
|
@@ -797,7 +1092,6 @@ function migrate(db, dbPath) {
|
|
|
797
1092
|
ensureColumn(db, "sessions", "plan_item_id", "ALTER TABLE sessions ADD COLUMN plan_item_id TEXT");
|
|
798
1093
|
ensureColumn(db, "sessions", "session_kind", "ALTER TABLE sessions ADD COLUMN session_kind TEXT");
|
|
799
1094
|
ensureColumn(db, "sessions", "coverage_units_json", "ALTER TABLE sessions ADD COLUMN coverage_units_json TEXT NOT NULL DEFAULT '[]'");
|
|
800
|
-
migrateSessionStorageKeys(db, dbPath);
|
|
801
1095
|
ensureColumn(db, "usage", "cache_read_input_tokens", "ALTER TABLE usage ADD COLUMN cache_read_input_tokens INTEGER");
|
|
802
1096
|
ensureColumn(db, "usage", "cache_write_input_tokens", "ALTER TABLE usage ADD COLUMN cache_write_input_tokens INTEGER");
|
|
803
1097
|
ensureColumn(db, "usage", "uncached_input_tokens", "ALTER TABLE usage ADD COLUMN uncached_input_tokens INTEGER");
|
|
@@ -808,9 +1102,21 @@ function migrate(db, dbPath) {
|
|
|
808
1102
|
db.prepare("UPDATE usage SET cache_read_input_tokens = cached_input_tokens WHERE cache_read_input_tokens IS NULL AND cached_input_tokens IS NOT NULL").run();
|
|
809
1103
|
ensureColumn(db, "runs", "run_home_path", "ALTER TABLE runs ADD COLUMN run_home_path TEXT");
|
|
810
1104
|
ensureColumn(db, "runs", "run_root", "ALTER TABLE runs ADD COLUMN run_root TEXT");
|
|
1105
|
+
db.prepare("UPDATE runs SET run_root = COALESCE(run_root, run_home_path, run_dir) WHERE run_root IS NULL OR run_root = ''").run();
|
|
1106
|
+
ensureColumn(db, "merge_requests", "accepted_tree", "ALTER TABLE merge_requests ADD COLUMN accepted_tree TEXT");
|
|
1107
|
+
ensureColumn(db, "merge_requests", "replacement_of_merge_request_id", "ALTER TABLE merge_requests ADD COLUMN replacement_of_merge_request_id TEXT");
|
|
1108
|
+
// A failed integration gate may create a new immutable source candidate in
|
|
1109
|
+
// the same RUN. The historical unique index made that repair path impossible.
|
|
1110
|
+
// Rebuilding an already-current index changes schema/data_version on every
|
|
1111
|
+
// CLI observation and prevents a controller's writer barrier from settling.
|
|
1112
|
+
const mergeRunIndex = db.prepare("PRAGMA index_list(merge_requests)").all().find(index => index.name === "idx_merge_requests_run");
|
|
1113
|
+
if (mergeRunIndex?.unique)
|
|
1114
|
+
db.exec("DROP INDEX idx_merge_requests_run");
|
|
1115
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_merge_requests_run ON merge_requests(project_id, run_id, created_at)");
|
|
811
1116
|
ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
|
|
812
1117
|
ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
|
|
813
1118
|
ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
|
|
1119
|
+
ensureColumn(db, "work_sessions", "start_receipt_json", "ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
|
|
814
1120
|
ensureColumn(db, "check_receipts", "declaration_id", "ALTER TABLE check_receipts ADD COLUMN declaration_id TEXT NOT NULL DEFAULT 'CHK-LEGACY'");
|
|
815
1121
|
ensureColumn(db, "check_receipts", "workspace_fingerprint", "ALTER TABLE check_receipts ADD COLUMN workspace_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
816
1122
|
ensureColumn(db, "check_receipts", "artifacts_json", "ALTER TABLE check_receipts ADD COLUMN artifacts_json TEXT NOT NULL DEFAULT '[]'");
|
|
@@ -818,6 +1124,9 @@ function migrate(db, dbPath) {
|
|
|
818
1124
|
ensureColumn(db, "check_receipts", "gate", "ALTER TABLE check_receipts ADD COLUMN gate TEXT NOT NULL DEFAULT 'work'");
|
|
819
1125
|
ensureColumn(db, "check_receipts", "input_hash", "ALTER TABLE check_receipts ADD COLUMN input_hash TEXT NOT NULL DEFAULT ''");
|
|
820
1126
|
ensureColumn(db, "check_receipts", "verification_epoch", "ALTER TABLE check_receipts ADD COLUMN verification_epoch TEXT NOT NULL DEFAULT ''");
|
|
1127
|
+
ensureColumn(db, "check_receipts", "reservation_sequence", "ALTER TABLE check_receipts ADD COLUMN reservation_sequence INTEGER NOT NULL DEFAULT 0");
|
|
1128
|
+
ensureColumn(db, "check_receipts", "retry_of_receipt_id", "ALTER TABLE check_receipts ADD COLUMN retry_of_receipt_id TEXT");
|
|
1129
|
+
ensureColumn(db, "check_receipts", "retry_reason", "ALTER TABLE check_receipts ADD COLUMN retry_reason TEXT");
|
|
821
1130
|
ensureColumn(db, "check_receipts", "before_fingerprint", "ALTER TABLE check_receipts ADD COLUMN before_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
822
1131
|
ensureColumn(db, "check_receipts", "after_fingerprint", "ALTER TABLE check_receipts ADD COLUMN after_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
823
1132
|
ensureColumn(db, "check_receipts", "profile_hash", "ALTER TABLE check_receipts ADD COLUMN profile_hash TEXT");
|
|
@@ -837,6 +1146,7 @@ function migrate(db, dbPath) {
|
|
|
837
1146
|
ensureColumn(db, "hook_events", "parent_session_id", "ALTER TABLE hook_events ADD COLUMN parent_session_id TEXT");
|
|
838
1147
|
ensureColumn(db, "hook_events", "daemon_id", "ALTER TABLE hook_events ADD COLUMN daemon_id TEXT");
|
|
839
1148
|
ensureColumn(db, "hook_events", "cwd", "ALTER TABLE hook_events ADD COLUMN cwd TEXT");
|
|
1149
|
+
ensureColumn(db, "hook_events", "outcome_json", "ALTER TABLE hook_events ADD COLUMN outcome_json TEXT");
|
|
840
1150
|
ensureColumn(db, "harness_usage_snapshots", "tool_calls", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_calls INTEGER");
|
|
841
1151
|
ensureColumn(db, "harness_usage_snapshots", "tool_failures", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_failures INTEGER");
|
|
842
1152
|
ensureColumn(db, "harness_usage_snapshots", "tool_by_name_json", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_by_name_json TEXT");
|