@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100
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 +561 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +91 -15
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/run-cli.js +1506 -334
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +39 -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 +449 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -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 +941 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -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 +267 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -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/harness-runtime/lib/zcode-usage.mjs +54 -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-decision.schema.json +1 -1
- 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/canon.js +11 -8
- package/dist/services/cleanup.js +75 -21
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +676 -79
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +134 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +125 -80
- package/dist/services/eval-snapshots.js +838 -74
- package/dist/services/execution-policy.js +170 -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 +609 -310
- package/dist/services/lanes.js +62 -53
- package/dist/services/lifecycle-command.js +103 -17
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1128 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +425 -0
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +78 -27
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +27 -18
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/repair-intents.js +40 -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 +222 -0
- package/dist/services/run-controller-capture.js +136 -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 +36 -0
- package/dist/services/run-controller.js +803 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +437 -81
- package/dist/services/runtime-budget.js +439 -0
- package/dist/services/runtime-command.js +43 -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 +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +21 -19
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +61 -80
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +112 -108
- package/dist/services/stage-pause.js +117 -73
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +108 -132
- package/dist/services/vnext-code-review.js +245 -98
- package/dist/services/vnext-code.js +355 -257
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +166 -26
- package/dist/services/vnext-merge.js +413 -147
- package/dist/services/vnext-plan-review.js +182 -109
- package/dist/services/vnext-plan.js +199 -82
- package/dist/services/vnext-protocolize.js +133 -60
- package/dist/services/vnext-specify.js +94 -71
- package/dist/services/work-registry.js +813 -160
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +569 -16
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +24 -13
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
- package/tools/repair-paused-run-status.mjs +59 -0
package/dist/storage/database.js
CHANGED
|
@@ -1,10 +1,139 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { ensureDir } from "./paths.js";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter } from "./writer-contract.js";
|
|
8
|
+
const workContinuationStorageSql = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS stage_work_memberships (
|
|
10
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL, work_id TEXT NOT NULL REFERENCES works(work_id),
|
|
11
|
+
stage TEXT NOT NULL, attempt TEXT NOT NULL, cycle TEXT NOT NULL DEFAULT '',
|
|
12
|
+
PRIMARY KEY(project_id, run_id, work_id, stage, attempt, cycle)
|
|
13
|
+
);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS repair_intents (
|
|
15
|
+
identity TEXT PRIMARY KEY, project_id TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
16
|
+
stage TEXT NOT NULL, attempt TEXT NOT NULL, payload_hash TEXT NOT NULL,
|
|
17
|
+
payload_json TEXT NOT NULL, work_id TEXT NOT NULL REFERENCES works(work_id), created_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
`;
|
|
6
20
|
const require = createRequire(import.meta.url);
|
|
7
21
|
const { DatabaseSync } = require("node:sqlite");
|
|
22
|
+
const resourceDatabases = new Map();
|
|
23
|
+
/** Invocation authority is prepared once with the runtime schema. Native
|
|
24
|
+
* hooks must only verify this table; they never create it on the hot path. */
|
|
25
|
+
export function ensureLifecycleInvocationStorage(db) {
|
|
26
|
+
db.exec(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS lifecycle_invocations (
|
|
28
|
+
id TEXT PRIMARY KEY, scope_json TEXT NOT NULL, command TEXT NOT NULL,
|
|
29
|
+
fingerprint TEXT NOT NULL, status TEXT NOT NULL,
|
|
30
|
+
event_key TEXT UNIQUE, identity_json TEXT, deadline INTEGER, outcome_json TEXT,
|
|
31
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_lifecycle_invocations_scope
|
|
34
|
+
ON lifecycle_invocations(scope_json, fingerprint, created_at);
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
/** RUN authority stays in its existing store; process leases stay in runtime.sqlite. */
|
|
38
|
+
export function ensureRunControlStorage(db) {
|
|
39
|
+
db.execSchema(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS run_controls (
|
|
41
|
+
control_id TEXT PRIMARY KEY,
|
|
42
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
43
|
+
request_id TEXT NOT NULL, request_hash TEXT NOT NULL, request_json TEXT NOT NULL,
|
|
44
|
+
recovery_id TEXT NOT NULL REFERENCES run_recovery_guards(recovery_id),
|
|
45
|
+
action TEXT NOT NULL, status TEXT NOT NULL,
|
|
46
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
47
|
+
UNIQUE(project_id, run_id, request_id),
|
|
48
|
+
FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
|
|
49
|
+
);
|
|
50
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_control_active
|
|
51
|
+
ON run_controls(project_id, run_id) WHERE status IN ('requested','settled','resuming');
|
|
52
|
+
CREATE TABLE IF NOT EXISTS run_control_admissions (
|
|
53
|
+
control_id TEXT NOT NULL REFERENCES run_controls(control_id),
|
|
54
|
+
work_session_id TEXT NOT NULL REFERENCES work_sessions(id),
|
|
55
|
+
work_id TEXT NOT NULL REFERENCES works(work_id),
|
|
56
|
+
session_id TEXT NOT NULL, admitted_at TEXT NOT NULL,
|
|
57
|
+
PRIMARY KEY(control_id, work_session_id)
|
|
58
|
+
);
|
|
59
|
+
CREATE TABLE IF NOT EXISTS run_control_stage_admissions (
|
|
60
|
+
control_id TEXT NOT NULL REFERENCES run_controls(control_id),
|
|
61
|
+
stage TEXT NOT NULL, attempt TEXT NOT NULL,
|
|
62
|
+
work_session_id TEXT NOT NULL REFERENCES work_sessions(id),
|
|
63
|
+
PRIMARY KEY(control_id, stage)
|
|
64
|
+
);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS run_control_operations (
|
|
66
|
+
operation_id TEXT PRIMARY KEY, control_id TEXT NOT NULL REFERENCES run_controls(control_id),
|
|
67
|
+
target_json TEXT NOT NULL, status TEXT NOT NULL, receipt_json TEXT, error_json TEXT,
|
|
68
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
69
|
+
);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS run_control_workers (
|
|
71
|
+
control_id TEXT PRIMARY KEY REFERENCES run_controls(control_id),
|
|
72
|
+
owner_token TEXT NOT NULL, owner_pid INTEGER,
|
|
73
|
+
status TEXT NOT NULL, process_id TEXT, process_lease_token TEXT,
|
|
74
|
+
resource_home TEXT NOT NULL, state_dir TEXT NOT NULL,
|
|
75
|
+
snapshot_json TEXT, error_json TEXT,
|
|
76
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
export function ensureRunControllerStorage(db) {
|
|
81
|
+
db.execSchema(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS run_controllers (
|
|
83
|
+
controller_id TEXT PRIMARY KEY,
|
|
84
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
85
|
+
request_id TEXT NOT NULL, request_hash TEXT NOT NULL,
|
|
86
|
+
generation INTEGER NOT NULL, status TEXT NOT NULL,
|
|
87
|
+
owner_token TEXT NOT NULL, process_id TEXT, process_lease_token TEXT,
|
|
88
|
+
owner_pid INTEGER, manifest_json TEXT NOT NULL, state_json TEXT NOT NULL,
|
|
89
|
+
state_dir TEXT NOT NULL, last_error_json TEXT,
|
|
90
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
91
|
+
FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id),
|
|
92
|
+
UNIQUE(project_id, run_id, request_id)
|
|
93
|
+
);
|
|
94
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_controller_owner
|
|
95
|
+
ON run_controllers(project_id, run_id)
|
|
96
|
+
WHERE status NOT IN ('completed', 'cancelled', 'superseded');
|
|
97
|
+
CREATE TABLE IF NOT EXISTS run_controller_events (
|
|
98
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
99
|
+
controller_id TEXT NOT NULL REFERENCES run_controllers(controller_id),
|
|
100
|
+
event_type TEXT NOT NULL, data_json TEXT NOT NULL, created_at TEXT NOT NULL
|
|
101
|
+
);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_run_controller_events
|
|
103
|
+
ON run_controller_events(controller_id, sequence);
|
|
104
|
+
CREATE TABLE IF NOT EXISTS run_controller_operations (
|
|
105
|
+
operation_id TEXT PRIMARY KEY,
|
|
106
|
+
controller_id TEXT NOT NULL REFERENCES run_controllers(controller_id),
|
|
107
|
+
generation INTEGER NOT NULL, input_hash TEXT NOT NULL,
|
|
108
|
+
input_json TEXT NOT NULL, status TEXT NOT NULL,
|
|
109
|
+
receipt_json TEXT, error_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
110
|
+
);
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
/** Recovery guards apply to every RUN kind, including custom eval fixtures. */
|
|
114
|
+
export function ensureRecoveryStorage(db) {
|
|
115
|
+
db.exec(`
|
|
116
|
+
CREATE TABLE IF NOT EXISTS run_recovery_guards (
|
|
117
|
+
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)
|
|
118
|
+
);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_run_recovery_guards_run ON run_recovery_guards(project_id, run_id, generation DESC);
|
|
120
|
+
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');
|
|
121
|
+
CREATE TABLE IF NOT EXISTS work_recovery_segments (
|
|
122
|
+
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)
|
|
123
|
+
);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_work_recovery_segments_work ON work_recovery_segments(work_id, generation DESC);
|
|
125
|
+
CREATE TABLE IF NOT EXISTS run_recovery_bindings (
|
|
126
|
+
recovery_id TEXT PRIMARY KEY REFERENCES run_recovery_guards(recovery_id),
|
|
127
|
+
work_id TEXT NOT NULL REFERENCES works(work_id), prior_work_session_id TEXT NOT NULL,
|
|
128
|
+
session_id TEXT NOT NULL, harness TEXT NOT NULL, provider_session_id TEXT NOT NULL,
|
|
129
|
+
daemon_id TEXT NOT NULL, status TEXT NOT NULL, hook_event_key TEXT,
|
|
130
|
+
created_at TEXT NOT NULL, accepted_at TEXT
|
|
131
|
+
);
|
|
132
|
+
`);
|
|
133
|
+
ensureColumn(db, "run_recovery_bindings", "packet_json", "ALTER TABLE run_recovery_bindings ADD COLUMN packet_json TEXT");
|
|
134
|
+
ensureColumn(db, "run_recovery_bindings", "packet_sha256", "ALTER TABLE run_recovery_bindings ADD COLUMN packet_sha256 TEXT");
|
|
135
|
+
ensureColumn(db, "run_recovery_bindings", "runtime_dispatch_json", "ALTER TABLE run_recovery_bindings ADD COLUMN runtime_dispatch_json TEXT");
|
|
136
|
+
}
|
|
8
137
|
/** Creates the single Work/Session authority used by a fresh vNext beta runtime. */
|
|
9
138
|
export function ensureVnextWorkStorage(db) {
|
|
10
139
|
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");
|
|
@@ -14,7 +143,7 @@ export function ensureVnextWorkStorage(db) {
|
|
|
14
143
|
if (existing && (!/task TEXT NOT NULL/.test(existing.sql) || /launch_token|adapter_session_id/.test(existing.sql))) {
|
|
15
144
|
throw new AppError("beta_storage_recreate_required", "This beta requires a fresh DD_FLOW_HOME; the existing works schema is not compatible", 1);
|
|
16
145
|
}
|
|
17
|
-
db.
|
|
146
|
+
db.execSchema(`
|
|
18
147
|
CREATE TABLE IF NOT EXISTS works (
|
|
19
148
|
work_id TEXT PRIMARY KEY,
|
|
20
149
|
project_id TEXT NOT NULL,
|
|
@@ -44,6 +173,7 @@ export function ensureVnextWorkStorage(db) {
|
|
|
44
173
|
status TEXT NOT NULL,
|
|
45
174
|
prompt_path TEXT NOT NULL,
|
|
46
175
|
result_path TEXT,
|
|
176
|
+
start_receipt_json TEXT,
|
|
47
177
|
created_at TEXT NOT NULL,
|
|
48
178
|
updated_at TEXT NOT NULL,
|
|
49
179
|
completed_at TEXT,
|
|
@@ -51,6 +181,16 @@ export function ensureVnextWorkStorage(db) {
|
|
|
51
181
|
);
|
|
52
182
|
CREATE INDEX IF NOT EXISTS idx_work_sessions_work ON work_sessions(work_id, created_at DESC);
|
|
53
183
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_sessions_open ON work_sessions(work_id) WHERE status = 'running';
|
|
184
|
+
|
|
185
|
+
CREATE TABLE IF NOT EXISTS run_recovery_guards (
|
|
186
|
+
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)
|
|
187
|
+
);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_run_recovery_guards_run ON run_recovery_guards(project_id, run_id, generation DESC);
|
|
189
|
+
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');
|
|
190
|
+
CREATE TABLE IF NOT EXISTS work_recovery_segments (
|
|
191
|
+
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)
|
|
192
|
+
);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_work_recovery_segments_work ON work_recovery_segments(work_id, generation DESC);
|
|
54
194
|
CREATE TABLE IF NOT EXISTS check_receipts (
|
|
55
195
|
id TEXT PRIMARY KEY,
|
|
56
196
|
project_id TEXT NOT NULL,
|
|
@@ -63,6 +203,9 @@ export function ensureVnextWorkStorage(db) {
|
|
|
63
203
|
command TEXT NOT NULL,
|
|
64
204
|
input_hash TEXT NOT NULL DEFAULT '',
|
|
65
205
|
verification_epoch TEXT NOT NULL DEFAULT '',
|
|
206
|
+
reservation_sequence INTEGER NOT NULL DEFAULT 0,
|
|
207
|
+
retry_of_receipt_id TEXT,
|
|
208
|
+
retry_reason TEXT,
|
|
66
209
|
status TEXT NOT NULL,
|
|
67
210
|
exit_code INTEGER,
|
|
68
211
|
stdout_path TEXT NOT NULL,
|
|
@@ -79,6 +222,35 @@ export function ensureVnextWorkStorage(db) {
|
|
|
79
222
|
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
80
223
|
);
|
|
81
224
|
CREATE INDEX IF NOT EXISTS idx_check_receipts_run ON check_receipts(project_id, run_id, started_at);
|
|
225
|
+
${workContinuationStorageSql}
|
|
226
|
+
`);
|
|
227
|
+
// A committed Work start is authoritative even if publishing its prompt
|
|
228
|
+
// files is interrupted after commit. Keep the exact bytes for repair.
|
|
229
|
+
db.writeTransaction(() => {
|
|
230
|
+
const columns = db.all("PRAGMA table_info(work_sessions)");
|
|
231
|
+
if (!columns.some((column) => column.name === "start_receipt_json"))
|
|
232
|
+
db.execSchema("ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** Durable intent for a provider-root external Work. It is deliberately
|
|
236
|
+
* separate from `work_sessions`: only a trusted hook may bind that Session. */
|
|
237
|
+
export function ensureWorkLaunchStorage(db) {
|
|
238
|
+
db.execSchema(`
|
|
239
|
+
CREATE TABLE IF NOT EXISTS work_launches (
|
|
240
|
+
operation_id TEXT PRIMARY KEY,
|
|
241
|
+
work_id TEXT NOT NULL UNIQUE,
|
|
242
|
+
profile_json TEXT NOT NULL,
|
|
243
|
+
profile_hash TEXT NOT NULL,
|
|
244
|
+
status TEXT NOT NULL,
|
|
245
|
+
state_dir TEXT NOT NULL,
|
|
246
|
+
journal_path TEXT NOT NULL,
|
|
247
|
+
provider_session_id TEXT,
|
|
248
|
+
adapter_receipt_json TEXT,
|
|
249
|
+
created_at TEXT NOT NULL,
|
|
250
|
+
updated_at TEXT NOT NULL,
|
|
251
|
+
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
252
|
+
);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_work_launches_status ON work_launches(status, updated_at DESC);
|
|
82
254
|
`);
|
|
83
255
|
}
|
|
84
256
|
export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
@@ -87,41 +259,348 @@ export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
|
87
259
|
if (mode === "read_existing" && !exists) {
|
|
88
260
|
return emptyReadOnlyDatabase(dbPath);
|
|
89
261
|
}
|
|
262
|
+
if (mode === "hook" && !exists)
|
|
263
|
+
throw new AppError("hook_storage_unprepared", "Prepare the runtime store before starting native hooks", 1);
|
|
90
264
|
if (!exists || mode === "migrate")
|
|
91
265
|
ensureDir(ddFlowHome);
|
|
92
266
|
const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
93
|
-
|
|
267
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
268
|
+
if (mode !== "read_existing") {
|
|
269
|
+
try {
|
|
270
|
+
registerStoreWriter(db);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
db.close();
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
configureDatabase(db, mode);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
db.close();
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
94
284
|
// Internal additive schema changes must be available to every write command.
|
|
95
285
|
// Higher-level Memory Bank migrations remain explicit in services/migrations.
|
|
96
|
-
if (mode !== "read_existing")
|
|
97
|
-
|
|
286
|
+
if (mode !== "read_existing") {
|
|
287
|
+
try {
|
|
288
|
+
const initialize = () => { migrate(db); ensureRecoveryStorage(db); ensureLifecycleInvocationStorage(db); };
|
|
289
|
+
if (mode !== "hook") {
|
|
290
|
+
const fresh = !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
|
|
291
|
+
if (fresh || readStoreWriterContract(db) !== null)
|
|
292
|
+
executeStoreSchema(db, initialize, fresh);
|
|
293
|
+
else
|
|
294
|
+
initialize();
|
|
295
|
+
// This migration owns a VACUUM backup and a separate data transaction.
|
|
296
|
+
migrateSessionStorageKeys(db, dbPath);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
assertHookStoreReady(db);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
db.close();
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
98
307
|
if (mode === "read_existing")
|
|
99
308
|
db.exec("PRAGMA query_only = ON");
|
|
100
309
|
db.exec("PRAGMA foreign_keys = ON");
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
310
|
+
return databaseFacade(dbPath, mode !== "read_existing", db);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Hook ingress is a hot, synchronous path. It may write receipts, but it
|
|
314
|
+
* must never perform schema migration or VACUUM while the native host waits.
|
|
315
|
+
* Startup/migration owns this contract; a missing table is an explicit
|
|
316
|
+
* preparation error instead of an implicit migration race.
|
|
317
|
+
*/
|
|
318
|
+
function assertHookStoreReady(db) {
|
|
319
|
+
const required = ["projects", "hook_events", "codex_session_bindings", "sessions", "flow_session_segments", "lifecycle_invocations"];
|
|
320
|
+
const missing = required.filter((table) => !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table));
|
|
321
|
+
if (missing.length)
|
|
322
|
+
throw new AppError("hook_storage_unprepared", "Managed store is not prepared for synchronous hook admission", 1, { missing_tables: missing, next_action: "Open the runtime with a normal dd-flow command and complete migration before starting the native Session." });
|
|
323
|
+
// Table presence alone does not establish compatibility with an older home.
|
|
324
|
+
const receiptColumns = {
|
|
325
|
+
hook_events: "id, project_id, harness, provider_session_id, parent_session_id, daemon_id, session_id, agent_id, turn_id, event_name, tool_name, event_key, cwd, match_key, transcript_path, status, sanitized_summary, outcome_json, created_at",
|
|
326
|
+
lifecycle_invocations: "id, scope_json, command, fingerprint, status, event_key, identity_json, deadline, outcome_json, created_at, updated_at",
|
|
327
|
+
codex_session_bindings: "session_id, project_id, protocol_id, handshake_id, cwd, transcript_path, status, created_at, updated_at"
|
|
109
328
|
};
|
|
329
|
+
for (const [table, columns] of Object.entries(receiptColumns)) {
|
|
330
|
+
try {
|
|
331
|
+
db.prepare(`SELECT ${columns} FROM ${table} LIMIT 0`);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
throw new AppError("hook_storage_unprepared", "Managed receipt schema needs preparation before native hooks start", 1, { table });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Opens the small host-wide resource registry. It is intentionally separate
|
|
340
|
+
* from a RUN database: a process or a port can outlive the CLI client that
|
|
341
|
+
* created it, while a RUN database belongs to one flow home.
|
|
342
|
+
*/
|
|
343
|
+
export function withResourceReadSnapshot(resourceHome, read) {
|
|
344
|
+
const dbPath = path.join(resourceHome, "runtime.sqlite");
|
|
345
|
+
const cached = resourceDatabases.get(dbPath);
|
|
346
|
+
if (!cached && !fs.existsSync(dbPath))
|
|
347
|
+
return read(emptyReadOnlyDatabase(dbPath));
|
|
348
|
+
const db = cached ?? new DatabaseSync(dbPath, { readOnly: true });
|
|
349
|
+
try {
|
|
350
|
+
if (!cached)
|
|
351
|
+
db.exec("PRAGMA busy_timeout = 4000; PRAGMA query_only = ON");
|
|
352
|
+
// A second process can observe SQLite's file before the initializing
|
|
353
|
+
// transaction publishes its schema. Treat that pre-commit view exactly
|
|
354
|
+
// like an absent registry; the writer path remains the serialization
|
|
355
|
+
// boundary and will reopen the committed schema.
|
|
356
|
+
if (!cached && !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get()) {
|
|
357
|
+
return read(emptyReadOnlyDatabase(dbPath));
|
|
358
|
+
}
|
|
359
|
+
const reader = cached
|
|
360
|
+
? { ...emptyReadOnlyDatabase(dbPath), get: cached.get, all: cached.all }
|
|
361
|
+
: databaseFacade(dbPath, false, db);
|
|
362
|
+
// SAVEPOINT also composes with an existing owner's transaction. The
|
|
363
|
+
// callback sees its uncommitted state but never receives write capability.
|
|
364
|
+
db.exec("SAVEPOINT resource_read_snapshot");
|
|
365
|
+
try {
|
|
366
|
+
const result = read(reader);
|
|
367
|
+
db.exec("RELEASE resource_read_snapshot");
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
db.exec("ROLLBACK TO resource_read_snapshot");
|
|
372
|
+
db.exec("RELEASE resource_read_snapshot");
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
if (!cached)
|
|
378
|
+
db.close?.();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export function getResourceDatabase(resourceHome, mode = "initialize") {
|
|
382
|
+
const dbPath = path.join(resourceHome, "runtime.sqlite");
|
|
383
|
+
const cached = resourceDatabases.get(dbPath);
|
|
384
|
+
if (mode === "read_existing") {
|
|
385
|
+
// Reuse a current transaction's view without exposing its write capability.
|
|
386
|
+
if (cached)
|
|
387
|
+
return { ...emptyReadOnlyDatabase(dbPath), get: cached.get, all: cached.all };
|
|
388
|
+
if (!fs.existsSync(dbPath))
|
|
389
|
+
return emptyReadOnlyDatabase(dbPath);
|
|
390
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
391
|
+
try {
|
|
392
|
+
db.exec("PRAGMA busy_timeout = 4000; PRAGMA query_only = ON");
|
|
393
|
+
if (!db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get()) {
|
|
394
|
+
db.close();
|
|
395
|
+
return emptyReadOnlyDatabase(dbPath);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
db.close();
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
return databaseFacade(dbPath, false, db);
|
|
403
|
+
}
|
|
404
|
+
ensureDir(resourceHome);
|
|
405
|
+
if (cached)
|
|
406
|
+
return cached;
|
|
407
|
+
const db = new DatabaseSync(dbPath);
|
|
408
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
409
|
+
try {
|
|
410
|
+
registerStoreWriter(db);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
db.close();
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
configureDatabase(db, "initialize");
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
db.close();
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
424
|
+
const fresh = !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
|
|
425
|
+
try {
|
|
426
|
+
executeStoreSchema(db, `
|
|
427
|
+
CREATE TABLE IF NOT EXISTS managed_processes (
|
|
428
|
+
id TEXT PRIMARY KEY,
|
|
429
|
+
kind TEXT NOT NULL,
|
|
430
|
+
pid INTEGER,
|
|
431
|
+
pid_started_at TEXT,
|
|
432
|
+
owner_id TEXT NOT NULL,
|
|
433
|
+
lease_token TEXT NOT NULL,
|
|
434
|
+
lease_expires_at TEXT NOT NULL,
|
|
435
|
+
project_id TEXT,
|
|
436
|
+
run_id TEXT,
|
|
437
|
+
work_id TEXT,
|
|
438
|
+
check_id TEXT,
|
|
439
|
+
operation_id TEXT,
|
|
440
|
+
stdout_path TEXT,
|
|
441
|
+
stderr_path TEXT,
|
|
442
|
+
state TEXT NOT NULL,
|
|
443
|
+
started_at TEXT NOT NULL,
|
|
444
|
+
updated_at TEXT NOT NULL,
|
|
445
|
+
finished_at TEXT,
|
|
446
|
+
termination_reason TEXT,
|
|
447
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
448
|
+
);
|
|
449
|
+
CREATE INDEX IF NOT EXISTS idx_managed_processes_lease
|
|
450
|
+
ON managed_processes(state, lease_expires_at, updated_at);
|
|
451
|
+
CREATE TABLE IF NOT EXISTS managed_resources (
|
|
452
|
+
resource_kind TEXT NOT NULL,
|
|
453
|
+
resource_key TEXT NOT NULL,
|
|
454
|
+
owner_id TEXT NOT NULL,
|
|
455
|
+
lease_token TEXT NOT NULL,
|
|
456
|
+
lease_expires_at TEXT NOT NULL,
|
|
457
|
+
process_id TEXT,
|
|
458
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
459
|
+
created_at TEXT NOT NULL,
|
|
460
|
+
updated_at TEXT NOT NULL,
|
|
461
|
+
PRIMARY KEY(resource_kind, resource_key)
|
|
462
|
+
);
|
|
463
|
+
CREATE INDEX IF NOT EXISTS idx_managed_resources_lease
|
|
464
|
+
ON managed_resources(lease_expires_at, updated_at);
|
|
465
|
+
`, fresh);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
db.close();
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
const database = databaseFacade(dbPath, true, db, () => {
|
|
472
|
+
resourceDatabases.delete(dbPath);
|
|
473
|
+
db.close?.();
|
|
474
|
+
});
|
|
475
|
+
resourceDatabases.set(dbPath, database);
|
|
476
|
+
return database;
|
|
110
477
|
}
|
|
111
478
|
function emptyReadOnlyDatabase(dbPath) {
|
|
112
479
|
return {
|
|
113
480
|
path: dbPath,
|
|
114
481
|
writable: false,
|
|
115
482
|
exec: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
483
|
+
execSchema: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
484
|
+
beginWriteTransaction: () => { throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath }); },
|
|
485
|
+
writeTransaction: () => { throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath }); },
|
|
116
486
|
run: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
117
487
|
get: () => undefined,
|
|
118
488
|
all: () => []
|
|
119
489
|
};
|
|
120
490
|
}
|
|
491
|
+
function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
|
|
492
|
+
let reserved = false;
|
|
493
|
+
let poisoned = false;
|
|
494
|
+
let savepoint = 0;
|
|
495
|
+
const assertUsable = () => { if (poisoned)
|
|
496
|
+
throw new AppError("write_transaction_unresolved", "Transaction cleanup failed; reopen the connection before continuing", 1); };
|
|
497
|
+
const beginWriteTransaction = () => {
|
|
498
|
+
assertUsable();
|
|
499
|
+
if (!writable)
|
|
500
|
+
throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath });
|
|
501
|
+
db.exec("BEGIN IMMEDIATE");
|
|
502
|
+
reserved = true;
|
|
503
|
+
};
|
|
504
|
+
const rollback = (sql, original) => {
|
|
505
|
+
try {
|
|
506
|
+
db.exec(sql);
|
|
507
|
+
}
|
|
508
|
+
catch (cleanup) {
|
|
509
|
+
poisoned = true;
|
|
510
|
+
if (original instanceof AppError)
|
|
511
|
+
throw new AppError(original.code, original.message, original.exitCode, { ...original.details, transaction_cleanup_error: errorDetails(cleanup) });
|
|
512
|
+
throw new AppError("write_transaction_failed", "Write transaction failed and SQLite cleanup also failed", 1, { cause: errorDetails(original), transaction_cleanup_error: errorDetails(cleanup) });
|
|
513
|
+
}
|
|
514
|
+
throw original;
|
|
515
|
+
};
|
|
516
|
+
const writeTransaction = (operation) => {
|
|
517
|
+
assertUsable();
|
|
518
|
+
if (!writable)
|
|
519
|
+
throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath });
|
|
520
|
+
if (db.isTransaction && !reserved)
|
|
521
|
+
throw new AppError("write_transaction_unowned", "Cannot write inside an unowned transaction or read snapshot", 1);
|
|
522
|
+
const nested = db.isTransaction;
|
|
523
|
+
const marker = `dd_flow_write_${++savepoint}`;
|
|
524
|
+
try {
|
|
525
|
+
if (nested)
|
|
526
|
+
db.exec(`SAVEPOINT ${marker}`);
|
|
527
|
+
else
|
|
528
|
+
beginWriteTransaction();
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
throw new AppError("storage_write_failed", "Cannot reserve SQLite writer", 1, { cause: errorDetails(error), transaction_phase: "begin", transaction_rolled_back: true });
|
|
532
|
+
}
|
|
533
|
+
let phase = "body";
|
|
534
|
+
try {
|
|
535
|
+
const value = operation();
|
|
536
|
+
if (value && typeof value.then === "function") {
|
|
537
|
+
void Promise.resolve(value).catch(() => { });
|
|
538
|
+
throw new AppError("write_transaction_async", "Write transaction callback must be synchronous", 1);
|
|
539
|
+
}
|
|
540
|
+
phase = "commit";
|
|
541
|
+
if (nested)
|
|
542
|
+
db.exec(`RELEASE ${marker}`);
|
|
543
|
+
else
|
|
544
|
+
db.exec("COMMIT");
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
try {
|
|
549
|
+
return rollback(nested ? `ROLLBACK TO ${marker}; RELEASE ${marker}` : "ROLLBACK", error);
|
|
550
|
+
}
|
|
551
|
+
catch (failure) {
|
|
552
|
+
if (failure === error && !(error instanceof AppError) && typeof error === "object" && error !== null && "code" in error && String(error.code).startsWith("ERR_SQLITE"))
|
|
553
|
+
throw new AppError("storage_write_failed", "SQLite write failed", 1, { cause: errorDetails(error), transaction_phase: phase, transaction_rolled_back: true });
|
|
554
|
+
throw failure;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
if (!db.isTransaction)
|
|
559
|
+
reserved = false;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
return {
|
|
563
|
+
path: dbPath,
|
|
564
|
+
writable,
|
|
565
|
+
close,
|
|
566
|
+
exec: (sql) => { assertUsable(); try {
|
|
567
|
+
db.exec(sql);
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
if (!db.isTransaction)
|
|
571
|
+
reserved = false;
|
|
572
|
+
} },
|
|
573
|
+
execSchema: (sql) => { assertUsable(); if (db.isTransaction && !reserved)
|
|
574
|
+
throw new AppError("write_transaction_unowned", "Cannot change schema inside an unowned transaction", 1); executeStoreSchema(db, sql); },
|
|
575
|
+
beginWriteTransaction,
|
|
576
|
+
writeTransaction,
|
|
577
|
+
run: (sql, params = []) => { assertUsable(); return db.prepare(sql).run(...params); },
|
|
578
|
+
get: (sql, params = []) => db.prepare(sql).get(...params),
|
|
579
|
+
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function errorDetails(error) {
|
|
583
|
+
const value = error;
|
|
584
|
+
return { message: String(value?.message ?? error), ...(typeof value?.code === "string" ? { code: value.code } : {}), ...(typeof value?.errcode === "number" ? { sqlite_extended_code: value.errcode } : {}) };
|
|
585
|
+
}
|
|
121
586
|
function configureDatabase(db, mode) {
|
|
122
|
-
db.exec("PRAGMA busy_timeout = 4000");
|
|
123
587
|
if (mode !== "read_existing") {
|
|
124
|
-
|
|
588
|
+
// Concurrent first openers can collide while changing journal mode, even
|
|
589
|
+
// with busy_timeout set. Retrying this configuration does not replay writes.
|
|
590
|
+
const deadline = performance.now() + 4000, pause = new Int32Array(new SharedArrayBuffer(4));
|
|
591
|
+
for (;;) {
|
|
592
|
+
try {
|
|
593
|
+
const current = db.prepare("PRAGMA journal_mode").get();
|
|
594
|
+
if (current.journal_mode !== "wal")
|
|
595
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
if ((error.errcode ?? 0) % 256 !== 5 || performance.now() >= deadline)
|
|
600
|
+
throw error;
|
|
601
|
+
Atomics.wait(pause, 0, 0, Math.min(25, Math.max(1, deadline - performance.now())));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
125
604
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
126
605
|
}
|
|
127
606
|
}
|
|
@@ -351,6 +830,7 @@ function migrate(db) {
|
|
|
351
830
|
project_id TEXT NOT NULL,
|
|
352
831
|
harness TEXT NOT NULL DEFAULT 'codex-desktop',
|
|
353
832
|
provider_session_id TEXT,
|
|
833
|
+
provider_parent_session_id TEXT,
|
|
354
834
|
agent_id TEXT,
|
|
355
835
|
parent_id TEXT,
|
|
356
836
|
provider TEXT,
|
|
@@ -482,6 +962,8 @@ function migrate(db) {
|
|
|
482
962
|
CREATE INDEX IF NOT EXISTS idx_check_receipts_run
|
|
483
963
|
ON check_receipts(project_id, run_id, started_at);
|
|
484
964
|
|
|
965
|
+
${workContinuationStorageSql}
|
|
966
|
+
|
|
485
967
|
CREATE TABLE IF NOT EXISTS merge_requests (
|
|
486
968
|
merge_request_id TEXT PRIMARY KEY,
|
|
487
969
|
project_id TEXT NOT NULL,
|
|
@@ -495,6 +977,7 @@ function migrate(db) {
|
|
|
495
977
|
target_branch TEXT NOT NULL,
|
|
496
978
|
enqueue_target_head TEXT,
|
|
497
979
|
execution_target_head TEXT,
|
|
980
|
+
accepted_tree TEXT,
|
|
498
981
|
integration_commit TEXT,
|
|
499
982
|
execution_route TEXT NOT NULL,
|
|
500
983
|
status TEXT NOT NULL,
|
|
@@ -504,6 +987,7 @@ function migrate(db) {
|
|
|
504
987
|
lock_acquired_at TEXT,
|
|
505
988
|
checkpoint TEXT NOT NULL DEFAULT 'queued',
|
|
506
989
|
profile_hash TEXT,
|
|
990
|
+
replacement_of_merge_request_id TEXT,
|
|
507
991
|
adapter_receipt_json TEXT,
|
|
508
992
|
result_json TEXT,
|
|
509
993
|
last_error_json TEXT,
|
|
@@ -515,8 +999,8 @@ function migrate(db) {
|
|
|
515
999
|
);
|
|
516
1000
|
CREATE INDEX IF NOT EXISTS idx_merge_requests_fifo
|
|
517
1001
|
ON merge_requests(project_id, status, created_at, merge_request_id);
|
|
518
|
-
CREATE
|
|
519
|
-
ON merge_requests(project_id, run_id);
|
|
1002
|
+
CREATE INDEX IF NOT EXISTS idx_merge_requests_run
|
|
1003
|
+
ON merge_requests(project_id, run_id, created_at);
|
|
520
1004
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_merge_requests_active_project
|
|
521
1005
|
ON merge_requests(project_id)
|
|
522
1006
|
WHERE status IN ('active', 'waiting_user', 'action_required', 'recovery_required');
|
|
@@ -713,6 +1197,7 @@ function migrate(db) {
|
|
|
713
1197
|
ensureColumn(db, "sessions", "metadata_json", "ALTER TABLE sessions ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'");
|
|
714
1198
|
ensureColumn(db, "sessions", "run_id", "ALTER TABLE sessions ADD COLUMN run_id TEXT");
|
|
715
1199
|
ensureColumn(db, "sessions", "provider_session_id", "ALTER TABLE sessions ADD COLUMN provider_session_id TEXT");
|
|
1200
|
+
ensureColumn(db, "sessions", "provider_parent_session_id", "ALTER TABLE sessions ADD COLUMN provider_parent_session_id TEXT");
|
|
716
1201
|
ensureColumn(db, "sessions", "harness", "ALTER TABLE sessions ADD COLUMN harness TEXT NOT NULL DEFAULT 'codex-desktop'");
|
|
717
1202
|
ensureColumn(db, "sessions", "agent_id", "ALTER TABLE sessions ADD COLUMN agent_id TEXT");
|
|
718
1203
|
ensureColumn(db, "sessions", "provider", "ALTER TABLE sessions ADD COLUMN provider TEXT");
|
|
@@ -737,9 +1222,21 @@ function migrate(db) {
|
|
|
737
1222
|
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();
|
|
738
1223
|
ensureColumn(db, "runs", "run_home_path", "ALTER TABLE runs ADD COLUMN run_home_path TEXT");
|
|
739
1224
|
ensureColumn(db, "runs", "run_root", "ALTER TABLE runs ADD COLUMN run_root TEXT");
|
|
1225
|
+
db.prepare("UPDATE runs SET run_root = COALESCE(run_root, run_home_path, run_dir) WHERE run_root IS NULL OR run_root = ''").run();
|
|
1226
|
+
ensureColumn(db, "merge_requests", "accepted_tree", "ALTER TABLE merge_requests ADD COLUMN accepted_tree TEXT");
|
|
1227
|
+
ensureColumn(db, "merge_requests", "replacement_of_merge_request_id", "ALTER TABLE merge_requests ADD COLUMN replacement_of_merge_request_id TEXT");
|
|
1228
|
+
// A failed integration gate may create a new immutable source candidate in
|
|
1229
|
+
// the same RUN. The historical unique index made that repair path impossible.
|
|
1230
|
+
// Rebuilding an already-current index changes schema/data_version on every
|
|
1231
|
+
// CLI observation and prevents a controller's writer barrier from settling.
|
|
1232
|
+
const mergeRunIndex = db.prepare("PRAGMA index_list(merge_requests)").all().find(index => index.name === "idx_merge_requests_run");
|
|
1233
|
+
if (mergeRunIndex?.unique)
|
|
1234
|
+
db.exec("DROP INDEX idx_merge_requests_run");
|
|
1235
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_merge_requests_run ON merge_requests(project_id, run_id, created_at)");
|
|
740
1236
|
ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
|
|
741
1237
|
ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
|
|
742
1238
|
ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
|
|
1239
|
+
ensureColumn(db, "work_sessions", "start_receipt_json", "ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
|
|
743
1240
|
ensureColumn(db, "check_receipts", "declaration_id", "ALTER TABLE check_receipts ADD COLUMN declaration_id TEXT NOT NULL DEFAULT 'CHK-LEGACY'");
|
|
744
1241
|
ensureColumn(db, "check_receipts", "workspace_fingerprint", "ALTER TABLE check_receipts ADD COLUMN workspace_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
745
1242
|
ensureColumn(db, "check_receipts", "artifacts_json", "ALTER TABLE check_receipts ADD COLUMN artifacts_json TEXT NOT NULL DEFAULT '[]'");
|
|
@@ -747,6 +1244,9 @@ function migrate(db) {
|
|
|
747
1244
|
ensureColumn(db, "check_receipts", "gate", "ALTER TABLE check_receipts ADD COLUMN gate TEXT NOT NULL DEFAULT 'work'");
|
|
748
1245
|
ensureColumn(db, "check_receipts", "input_hash", "ALTER TABLE check_receipts ADD COLUMN input_hash TEXT NOT NULL DEFAULT ''");
|
|
749
1246
|
ensureColumn(db, "check_receipts", "verification_epoch", "ALTER TABLE check_receipts ADD COLUMN verification_epoch TEXT NOT NULL DEFAULT ''");
|
|
1247
|
+
ensureColumn(db, "check_receipts", "reservation_sequence", "ALTER TABLE check_receipts ADD COLUMN reservation_sequence INTEGER NOT NULL DEFAULT 0");
|
|
1248
|
+
ensureColumn(db, "check_receipts", "retry_of_receipt_id", "ALTER TABLE check_receipts ADD COLUMN retry_of_receipt_id TEXT");
|
|
1249
|
+
ensureColumn(db, "check_receipts", "retry_reason", "ALTER TABLE check_receipts ADD COLUMN retry_reason TEXT");
|
|
750
1250
|
ensureColumn(db, "check_receipts", "before_fingerprint", "ALTER TABLE check_receipts ADD COLUMN before_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
751
1251
|
ensureColumn(db, "check_receipts", "after_fingerprint", "ALTER TABLE check_receipts ADD COLUMN after_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
752
1252
|
ensureColumn(db, "check_receipts", "profile_hash", "ALTER TABLE check_receipts ADD COLUMN profile_hash TEXT");
|
|
@@ -766,6 +1266,7 @@ function migrate(db) {
|
|
|
766
1266
|
ensureColumn(db, "hook_events", "parent_session_id", "ALTER TABLE hook_events ADD COLUMN parent_session_id TEXT");
|
|
767
1267
|
ensureColumn(db, "hook_events", "daemon_id", "ALTER TABLE hook_events ADD COLUMN daemon_id TEXT");
|
|
768
1268
|
ensureColumn(db, "hook_events", "cwd", "ALTER TABLE hook_events ADD COLUMN cwd TEXT");
|
|
1269
|
+
ensureColumn(db, "hook_events", "outcome_json", "ALTER TABLE hook_events ADD COLUMN outcome_json TEXT");
|
|
769
1270
|
ensureColumn(db, "harness_usage_snapshots", "tool_calls", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_calls INTEGER");
|
|
770
1271
|
ensureColumn(db, "harness_usage_snapshots", "tool_failures", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_failures INTEGER");
|
|
771
1272
|
ensureColumn(db, "harness_usage_snapshots", "tool_by_name_json", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_by_name_json TEXT");
|
|
@@ -783,6 +1284,58 @@ function migrate(db) {
|
|
|
783
1284
|
ON runs(short_id);
|
|
784
1285
|
`);
|
|
785
1286
|
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Older builds used public or `harness:native` strings as foreign keys. Move
|
|
1289
|
+
* those keys once to an opaque storage key while preserving the original
|
|
1290
|
+
* native value in provider_session_id. We deliberately never split a native
|
|
1291
|
+
* value on `:`: rows without a recorded provider ID remain untouched and are
|
|
1292
|
+
* reported by normal preflight instead of guessed.
|
|
1293
|
+
*/
|
|
1294
|
+
function migrateSessionStorageKeys(db, dbPath) {
|
|
1295
|
+
const rows = db.prepare("SELECT project_id, session_id, harness, provider_session_id, parent_session_id FROM sessions WHERE provider_session_id IS NOT NULL").all();
|
|
1296
|
+
const changed = rows.map((row) => ({ ...row, next: storageSessionKey(row.harness, row.provider_session_id) })).filter((row) => row.session_id !== row.next);
|
|
1297
|
+
if (changed.length === 0)
|
|
1298
|
+
return;
|
|
1299
|
+
// This is the only non-additive storage migration. `VACUUM INTO` captures
|
|
1300
|
+
// a consistent SQLite snapshot (including WAL state) before keys change.
|
|
1301
|
+
if (dbPath && fs.existsSync(dbPath)) {
|
|
1302
|
+
const backup = `${dbPath}.pre-session-key-v1-${Date.now()}.sqlite`;
|
|
1303
|
+
db.exec(`VACUUM INTO '${backup.replace(/'/g, "''")}'`);
|
|
1304
|
+
}
|
|
1305
|
+
const duplicate = new Set();
|
|
1306
|
+
for (const row of changed) {
|
|
1307
|
+
const key = `${row.project_id}\0${row.next}`;
|
|
1308
|
+
if (duplicate.has(key) || db.prepare("SELECT 1 FROM sessions WHERE project_id = ? AND session_id = ? AND session_id <> ?").get(row.project_id, row.next, row.session_id)) {
|
|
1309
|
+
throw new AppError("session_identity_migration_conflict", "Cannot migrate ambiguous harness/native Session identity", 1, { project_id: row.project_id, harness_id: row.harness, session_id: row.provider_session_id });
|
|
1310
|
+
}
|
|
1311
|
+
duplicate.add(key);
|
|
1312
|
+
}
|
|
1313
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1314
|
+
try {
|
|
1315
|
+
// First update references. Session rows themselves are updated afterwards,
|
|
1316
|
+
// so every dependent row remains resolvable throughout the transaction.
|
|
1317
|
+
for (const row of changed) {
|
|
1318
|
+
db.prepare("UPDATE work_sessions SET session_id = ? WHERE session_id = ? AND work_id IN (SELECT work_id FROM works WHERE project_id = ?)").run(row.next, row.session_id, row.project_id);
|
|
1319
|
+
db.prepare("UPDATE flow_session_segments SET session_id = ? WHERE project_id = ? AND session_id = ?").run(row.next, row.project_id, row.session_id);
|
|
1320
|
+
db.prepare("UPDATE usage SET session_id = ? WHERE project_id = ? AND session_id = ?").run(row.next, row.project_id, row.session_id);
|
|
1321
|
+
const parameters = [row.next, row.project_id, row.session_id];
|
|
1322
|
+
db.prepare("UPDATE hook_events SET session_id = ? WHERE project_id = ? AND session_id = ?").run(...parameters);
|
|
1323
|
+
db.prepare("UPDATE flow_jobs SET worker_session_id = ? WHERE project_id = ? AND worker_session_id = ?").run(...parameters);
|
|
1324
|
+
db.prepare("UPDATE merge_queue SET claimed_by_session_id = ? WHERE project_id = ? AND claimed_by_session_id = ?").run(...parameters);
|
|
1325
|
+
db.prepare("UPDATE sessions SET parent_session_id = ? WHERE project_id = ? AND parent_session_id = ?").run(...parameters);
|
|
1326
|
+
db.prepare("UPDATE sessions SET session_id = ? WHERE project_id = ? AND session_id = ?").run(...parameters);
|
|
1327
|
+
}
|
|
1328
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_harness_native ON sessions(project_id, harness, provider_session_id) WHERE provider_session_id IS NOT NULL");
|
|
1329
|
+
db.exec("COMMIT");
|
|
1330
|
+
}
|
|
1331
|
+
catch (error) {
|
|
1332
|
+
db.exec("ROLLBACK");
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function storageSessionKey(harness, native) {
|
|
1337
|
+
return `SES-${crypto.createHash("sha256").update(harness).update("\0").update(native).digest("hex").slice(0, 24)}`;
|
|
1338
|
+
}
|
|
786
1339
|
function migrateProjectScopedIdentity(db) {
|
|
787
1340
|
const protocolColumns = db.prepare("PRAGMA table_info(protocols)").all();
|
|
788
1341
|
const legacyGlobalPrimaryKey = protocolColumns.some((column) => column.name === "id" && column.pk === 1)
|