@deksden-com/dd-flow-cli 0.9.0-beta.63 → 0.9.0-beta.64
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 +12 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/run-cli.js +45 -4
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -0
- package/dist/harness-runtime/lib/process-json.mjs +15 -6
- package/dist/services/cleanup.js +1 -1
- package/dist/services/code-checks.js +1 -1
- package/dist/services/eval-snapshots.js +2 -2
- package/dist/services/harness-adapter.js +61 -9
- package/dist/services/hooks.js +26 -0
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-invocations.js +194 -22
- package/dist/services/managed-processes.js +2 -2
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +1 -1
- package/dist/services/migrations.js +1 -1
- package/dist/services/projects.js +1 -1
- package/dist/services/protocols.js +2 -2
- package/dist/services/run-control-worker.js +3 -3
- package/dist/services/run-control.js +1 -1
- package/dist/services/run-controller-adapter.js +8 -3
- package/dist/services/run-controller-capture.js +2 -2
- package/dist/services/run-controller-process.js +2 -2
- package/dist/services/run-controller-recovery.js +3 -3
- package/dist/services/run-controller.js +297 -180
- package/dist/services/run-recovery-runtime.js +1 -1
- package/dist/services/run-recovery.js +3 -3
- package/dist/services/runs.js +13 -14
- package/dist/services/runtime-budget.js +4 -4
- package/dist/services/runtime-scope-control.js +5 -5
- package/dist/services/runtime-scope-resume.js +7 -7
- package/dist/services/runtime-scope-worker.js +2 -2
- package/dist/services/stage-pause.js +2 -2
- package/dist/services/vnext-code-review.js +9 -4
- package/dist/services/vnext-code.js +10 -3
- package/dist/services/vnext-merge.js +16 -21
- package/dist/services/vnext-plan-review.js +4 -2
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/work-registry.js +198 -121
- package/dist/storage/database.js +112 -20
- package/dist/storage/writer-contract.js +2 -1
- package/dist/storage/writer-migration.js +2 -2
- package/package.json +1 -1
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import { quote } from "shell-quote";
|
|
4
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
6
7
|
import { commandOption, parseLifecycleCommand, shellSuffixOffset } from "./lifecycle-command.js";
|
|
7
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
9
|
import { assertRunMutationAllowed, assertStageSettlementAllowed, assertWorkSettlementAllowed, recoveryGuard } from "./run-recovery.js";
|
|
9
|
-
import { requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
10
|
+
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
11
|
+
import { managedProcessStatus } from "./managed-processes.js";
|
|
12
|
+
import { appendEvent } from "./run-controller-state.js";
|
|
13
|
+
import { recoverCommittedWorkStart } from "./work-registry.js";
|
|
14
|
+
import { observeCommandHook } from "./hooks.js";
|
|
15
|
+
import { runtimeHarness } from "./execution-policy.js";
|
|
10
16
|
// Conclusive result/check rejections, never transport, ownership, registry or
|
|
11
17
|
// unknown-effect failures. These validators reject before accepting the result.
|
|
12
18
|
export function isRetryableLifecycleRejection(code, details = {}) {
|
|
@@ -52,6 +58,164 @@ function load(context, id) {
|
|
|
52
58
|
export function lifecycleInvocationScope(context, id) {
|
|
53
59
|
return JSON.parse(load(context, id).scope_json);
|
|
54
60
|
}
|
|
61
|
+
/** Read-only controller observation of a retained lifecycle outcome. */
|
|
62
|
+
export function lifecycleInvocationOutcome(context, id) {
|
|
63
|
+
const row = load(context, id);
|
|
64
|
+
return { status: row.status, outcome: row.outcome_json ? JSON.parse(row.outcome_json) : null };
|
|
65
|
+
}
|
|
66
|
+
/** Current failures are read from runtime authority, never from model text. */
|
|
67
|
+
export function assertLifecycleOutcomes(context, scope) {
|
|
68
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
69
|
+
const diagnostics = lifecycleDiagnostics(context, project.id, scope.runId, scope.generation);
|
|
70
|
+
if (fs.existsSync(diagnostics)) {
|
|
71
|
+
for (const name of fs.readdirSync(diagnostics).filter(name => name.endsWith(".json"))) {
|
|
72
|
+
const record = JSON.parse(fs.readFileSync(path.join(diagnostics, name), "utf8"));
|
|
73
|
+
if (record.project_id !== project.id || record.run_id !== scope.runId || record.generation !== scope.generation)
|
|
74
|
+
throw new AppError("lifecycle_outcome_conflict", "Lifecycle diagnostic scope differs", 1);
|
|
75
|
+
throw new AppError(record.error?.code ?? "lifecycle_outcome_unknown", record.error?.message ?? "Lifecycle settlement could not be persisted", 1, { ...record.error?.details, lifecycle_outcome: record, diagnostic_artifact: path.join(diagnostics, name) });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const columns = context.db.all("PRAGMA table_info(hook_events)");
|
|
79
|
+
if (columns.some(column => column.name === "outcome_json")) {
|
|
80
|
+
for (const row of context.db.all("SELECT outcome_json FROM hook_events WHERE project_id = ? AND json_extract(outcome_json, '$.run_id') = ? AND json_extract(outcome_json, '$.generation') = ? ORDER BY id", [project.id, scope.runId, scope.generation])) {
|
|
81
|
+
const outcome = JSON.parse(row.outcome_json);
|
|
82
|
+
if (outcome.disposition === "retrying" && outcome.deadline <= Date.now())
|
|
83
|
+
throw new AppError("lifecycle_outcome_unknown", "Storage retry has no terminal receipt; reconcile its committed evidence", 1, { lifecycle_outcome: outcome, effect: "unknown" });
|
|
84
|
+
if (outcome.disposition === "fatal")
|
|
85
|
+
throw new AppError(outcome.error.code, outcome.error.message, 1, { ...outcome.error.details, lifecycle_outcome: outcome });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'lifecycle_invocations'"))
|
|
89
|
+
return;
|
|
90
|
+
const rows = context.db.all(`SELECT a.* FROM lifecycle_invocations a
|
|
91
|
+
WHERE json_extract(a.scope_json, '$.projectRoot') = ? AND json_extract(a.scope_json, '$.runId') = ?
|
|
92
|
+
AND json_extract(a.scope_json, '$.generation') = ? AND a.status = 'settled'
|
|
93
|
+
AND NOT EXISTS (SELECT 1 FROM lifecycle_invocations newer WHERE newer.scope_json = a.scope_json AND newer.fingerprint = a.fingerprint AND newer.rowid > a.rowid)
|
|
94
|
+
ORDER BY a.rowid`, [scope.projectRoot, scope.runId, scope.generation]);
|
|
95
|
+
for (const row of rows) {
|
|
96
|
+
const error = (row.outcome_json ? JSON.parse(row.outcome_json) : null)?.error;
|
|
97
|
+
if (typeof error?.code === "string" && typeof error.details?.retry_command !== "string")
|
|
98
|
+
throw new AppError(error.code, typeof error.message === "string" ? error.message : "Mandatory lifecycle failed", 1, { ...(error.details ?? {}), invocation_id: row.id });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Hooks already own native identity. Retain command outcomes there, with an
|
|
102
|
+
* atomic controller notification; missing notifications reconcile from the row. */
|
|
103
|
+
export function observeLifecycleCommand(context, command) {
|
|
104
|
+
const parsed = parseLifecycleCommand(command);
|
|
105
|
+
if (parsed.kind !== "standalone")
|
|
106
|
+
return () => { };
|
|
107
|
+
let anchor;
|
|
108
|
+
context.observeLifecycleHook = (projectId, eventId) => {
|
|
109
|
+
if (anchor)
|
|
110
|
+
return;
|
|
111
|
+
const event = context.db.get("SELECT session_id, provider_session_id, parent_session_id, daemon_id, harness FROM hook_events WHERE id = ? AND project_id = ?", [eventId, projectId]);
|
|
112
|
+
if (!event)
|
|
113
|
+
return;
|
|
114
|
+
let runId = context.db.get("SELECT run_id FROM sessions WHERE project_id = ? AND harness = ? AND (session_id = ? OR session_id = ? OR provider_session_id = ?) AND run_id IS NOT NULL ORDER BY session_id = ? DESC LIMIT 1", [projectId, event.harness, event.session_id, event.parent_session_id, event.parent_session_id, event.session_id])?.run_id;
|
|
115
|
+
let generation;
|
|
116
|
+
// A first Stage command precedes logical Session binding. Its native root
|
|
117
|
+
// is already owned by the controller's retained create receipt.
|
|
118
|
+
if (context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'run_controllers'")) {
|
|
119
|
+
const owners = context.db.all("SELECT run_id, generation, state_json FROM run_controllers WHERE project_id = ? AND status NOT IN ('completed','cancelled','superseded')", [projectId]).filter(owner => {
|
|
120
|
+
const state = JSON.parse(owner.state_json);
|
|
121
|
+
return state.sessions?.some(session => [event.provider_session_id ?? event.session_id, event.parent_session_id].includes(session.id) && runtimeHarness(session.profile) === event.harness);
|
|
122
|
+
});
|
|
123
|
+
if (owners.length > 1)
|
|
124
|
+
return;
|
|
125
|
+
if (owners.length === 1) {
|
|
126
|
+
if (runId && runId !== owners[0].run_id)
|
|
127
|
+
return;
|
|
128
|
+
runId = owners[0].run_id;
|
|
129
|
+
generation = owners[0].generation;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (event.daemon_id) {
|
|
133
|
+
for (const record of managedProcessStatus(context)) {
|
|
134
|
+
if (record.project_id !== projectId || !record.run_id || !record.kind.endsWith("-daemon"))
|
|
135
|
+
continue;
|
|
136
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
137
|
+
if (typeof metadata.state_dir !== "string")
|
|
138
|
+
continue;
|
|
139
|
+
let daemonId;
|
|
140
|
+
try {
|
|
141
|
+
daemonId = readNativeDaemonState(metadata.state_dir).daemon_id;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue;
|
|
145
|
+
} // Discovery of unrelated historical daemons is not authority.
|
|
146
|
+
if (daemonId !== event.daemon_id)
|
|
147
|
+
continue;
|
|
148
|
+
const binding = requireManagedDaemonBinding(context, { stateDir: metadata.state_dir, projectId, runId: record.run_id });
|
|
149
|
+
if (binding.state.daemon_id !== event.daemon_id)
|
|
150
|
+
continue;
|
|
151
|
+
runId = record.run_id;
|
|
152
|
+
generation = metadata.run_generation;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!runId)
|
|
157
|
+
return;
|
|
158
|
+
anchor = { project_id: projectId, run_id: runId, generation: generation ?? recoveryGuard(context, projectId, runId)?.generation ?? 0, hook_event_id: eventId, session_id: event.session_id, harness: event.harness, operation: parsed.invocation.operation };
|
|
159
|
+
};
|
|
160
|
+
observeCommandHook(context, command);
|
|
161
|
+
const invocationId = commandOption(parsed.invocation, "invocation-id");
|
|
162
|
+
if (!anchor && invocationId) {
|
|
163
|
+
const attempt = load(context, invocationId);
|
|
164
|
+
checkCommand(attempt, command);
|
|
165
|
+
if (attempt.event_key && ["observed", "executing"].includes(attempt.status)) {
|
|
166
|
+
const scope = JSON.parse(attempt.scope_json);
|
|
167
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
168
|
+
const event = context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, attempt.event_key]);
|
|
169
|
+
if (event)
|
|
170
|
+
context.observeLifecycleHook(project.id, event.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return outcome => {
|
|
174
|
+
delete context.observeLifecycleHook;
|
|
175
|
+
if (!anchor)
|
|
176
|
+
return;
|
|
177
|
+
const retryable = outcome.error && (isRetryableLifecycleRejection(outcome.error.code, outcome.error.details) || typeof outcome.error.details.retry_command === "string");
|
|
178
|
+
const record = { ...anchor, ...outcome, effect: outcome.error ? outcome.error.details.effect ?? (retryable ? "no_effect" : "unknown") : "committed", disposition: outcome.error ? retryable ? "correctable" : "fatal" : "completed" };
|
|
179
|
+
try {
|
|
180
|
+
context.db.writeTransaction(() => {
|
|
181
|
+
const retained = context.db.get("SELECT outcome_json FROM hook_events WHERE id = ?", [anchor.hook_event_id]);
|
|
182
|
+
const previous = retained?.outcome_json ? JSON.parse(retained.outcome_json) : null;
|
|
183
|
+
if (previous?.storage_retry === 1)
|
|
184
|
+
Object.assign(record, { storage_retry: 1, work_id: previous.work_id });
|
|
185
|
+
const encoded = JSON.stringify(record);
|
|
186
|
+
if (retained?.outcome_json && previous?.disposition !== "retrying") {
|
|
187
|
+
if (retained.outcome_json !== encoded)
|
|
188
|
+
throw new AppError("lifecycle_outcome_conflict", "Hook outcome cannot change", 1, { hook_event_id: anchor.hook_event_id });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
context.db.run("UPDATE hook_events SET outcome_json = ? WHERE id = ?", [encoded, anchor.hook_event_id]);
|
|
192
|
+
if (context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'run_controllers'"))
|
|
193
|
+
for (const controller of context.db.all("SELECT controller_id FROM run_controllers WHERE project_id = ? AND run_id = ? AND generation = ?", [anchor.project_id, anchor.run_id, anchor.generation]))
|
|
194
|
+
appendEvent(context, controller.controller_id, "lifecycle_outcome", record);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (persistence) {
|
|
198
|
+
if (persistence instanceof AppError && persistence.code === "lifecycle_outcome_conflict")
|
|
199
|
+
throw persistence;
|
|
200
|
+
// Diagnostic evidence is not replay authority. If SQL is unavailable,
|
|
201
|
+
// the controller still sees this failure and fences instead of waiting
|
|
202
|
+
// for a model to report it or invent a new invocation.
|
|
203
|
+
const directory = lifecycleDiagnostics(context, anchor.project_id, anchor.run_id, anchor.generation);
|
|
204
|
+
try {
|
|
205
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
206
|
+
fs.writeFileSync(path.join(directory, `${anchor.hook_event_id}.json`), JSON.stringify({ ...record, disposition: "fatal", persistence_error: String(persistence) }), { flag: "wx", mode: 0o600, flush: true });
|
|
207
|
+
}
|
|
208
|
+
catch (diagnostic) {
|
|
209
|
+
if (diagnostic.code !== "EEXIST")
|
|
210
|
+
process.stderr.write(`${JSON.stringify({ primary: record.error ?? { effect: "committed" }, persistence: String(persistence), diagnostic: String(diagnostic) })}\n`);
|
|
211
|
+
}
|
|
212
|
+
throw persistence;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function lifecycleDiagnostics(context, projectId, runId, generation) {
|
|
217
|
+
return path.join(context.ddFlowHome, "diagnostics", "lifecycle", encodeURIComponent(projectId), encodeURIComponent(runId), String(generation));
|
|
218
|
+
}
|
|
55
219
|
/** Current, explicitly-issued replacements for commands from the resumed stage.
|
|
56
220
|
* Original prompts remain immutable evidence; callers may present this small
|
|
57
221
|
* overlay after a state transition made an earlier no-effect rejection retryable. */
|
|
@@ -120,25 +284,15 @@ export function managedLifecycleCommand(context, command) {
|
|
|
120
284
|
return renderInvocationCommand(retained, command);
|
|
121
285
|
}
|
|
122
286
|
storage(context);
|
|
123
|
-
context.db.
|
|
124
|
-
try {
|
|
287
|
+
return context.db.writeTransaction(() => {
|
|
125
288
|
const prior = context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
|
|
126
289
|
if (prior && prior.id !== context.env.DD_FLOW_CURRENT_INVOCATION) {
|
|
127
|
-
|
|
128
|
-
context.db.exec("RELEASE invocation_command");
|
|
129
|
-
return rendered;
|
|
290
|
+
return renderInvocationCommand(prior, command);
|
|
130
291
|
}
|
|
131
292
|
if (context.env.DD_FLOW_INVOCATION_READONLY === "1")
|
|
132
293
|
throw new AppError("invocation_command_unprepared", "No prepared lifecycle command is available; read-only inspection cannot issue one", 1);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return prepared.command;
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
context.db.exec("ROLLBACK TO invocation_command");
|
|
139
|
-
context.db.exec("RELEASE invocation_command");
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
294
|
+
return issueLifecycleInvocation(context, command, scope).command;
|
|
295
|
+
});
|
|
142
296
|
}
|
|
143
297
|
export function retryLifecycleInvocationCommand(context, id) {
|
|
144
298
|
const prior = load(context, id);
|
|
@@ -170,7 +324,7 @@ function successorLifecycleInvocationCommand(context, prior) {
|
|
|
170
324
|
* callers must never time out merely because the observer rejected syntax. */
|
|
171
325
|
export function settleLifecycleNoEffectRejection(context, id, command, error, identity) {
|
|
172
326
|
storage(context);
|
|
173
|
-
context.db.
|
|
327
|
+
context.db.beginWriteTransaction();
|
|
174
328
|
try {
|
|
175
329
|
const prior = load(context, id);
|
|
176
330
|
checkCommand(prior, command);
|
|
@@ -211,14 +365,18 @@ const receiptTimeout = (id) => ({ code: "invocation_receipt_timeout", message: "
|
|
|
211
365
|
export function settleLifecycleRejection(context, id, error) {
|
|
212
366
|
storage(context);
|
|
213
367
|
const details = { ...error.details };
|
|
214
|
-
context.db.
|
|
368
|
+
context.db.beginWriteTransaction();
|
|
215
369
|
try {
|
|
216
370
|
const prior = load(context, id);
|
|
217
371
|
if (prior.status !== "executing")
|
|
218
372
|
throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
|
|
219
|
-
|
|
373
|
+
const storageRetry = error.code === "storage_write_failed" && error.details.effect === "no_effect"
|
|
374
|
+
&& !context.db.get("SELECT 1 FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND json_extract(outcome_json, '$.error.details.storage_retry') = 1 LIMIT 1", [prior.scope_json, prior.fingerprint]);
|
|
375
|
+
if (isRetryableLifecycleRejection(error.code, error.details) || storageRetry) {
|
|
220
376
|
const retry = retryLifecycleInvocationCommand(context, id);
|
|
221
377
|
details.retry_command = retry;
|
|
378
|
+
if (storageRetry)
|
|
379
|
+
details.storage_retry = 1;
|
|
222
380
|
details.retry_instruction = "Correct only the evidenced defect, then use retry_command with the required result input. Reissuing the old command only reads its retained outcome.";
|
|
223
381
|
}
|
|
224
382
|
settleLifecycleInvocation(context, id, { error: { ...error, details } });
|
|
@@ -339,6 +497,9 @@ function commandWithInvocationId(command, id) {
|
|
|
339
497
|
* Preserve current stdin/heredoc/response-file text while validating that it
|
|
340
498
|
* still names the exact persisted operation and project. */
|
|
341
499
|
function assertRenderableInvocation(row) {
|
|
500
|
+
const outcome = row.outcome_json ? JSON.parse(row.outcome_json) : null;
|
|
501
|
+
if (row.status === "settled" && outcome?.error)
|
|
502
|
+
throw new AppError("invocation_command_blocked", "Failed lifecycle authority cannot be advertised as an executable command", 1, { invocation_id: row.id, outcome });
|
|
342
503
|
if (row.status === "expired" || row.status === "issued" && row.deadline !== null && row.deadline <= Date.now()) {
|
|
343
504
|
throw new AppError("invocation_expired", "Retained lifecycle attempt has expired; inspection cannot grant a new attempt", 1, { invocation_id: row.id, status: row.status, outcome: row.outcome_json ? JSON.parse(row.outcome_json) : null });
|
|
344
505
|
}
|
|
@@ -373,7 +534,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
373
534
|
const identity = input.identity;
|
|
374
535
|
checkNativeIdentity(scope, identity, input.id);
|
|
375
536
|
const identityJson = JSON.stringify([identity.daemonId, identity.rootSessionId, identity.sessionId, identity.parentSessionId, identity.toolCallId]);
|
|
376
|
-
context.db.
|
|
537
|
+
context.db.beginWriteTransaction();
|
|
377
538
|
try {
|
|
378
539
|
const row = context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ?", [input.id]);
|
|
379
540
|
if (row.status === "settled") {
|
|
@@ -420,8 +581,19 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
420
581
|
const row = load(context, input.id);
|
|
421
582
|
if (row.status === "settled")
|
|
422
583
|
return { outcome: JSON.parse(row.outcome_json), replay: true };
|
|
423
|
-
if (row.status === "executing")
|
|
424
|
-
|
|
584
|
+
if (row.status === "executing") {
|
|
585
|
+
const scope = JSON.parse(row.scope_json);
|
|
586
|
+
if (invocation(row.command).operation === "work_start" && row.event_key) {
|
|
587
|
+
input.assertCurrent(scope);
|
|
588
|
+
const result = recoverCommittedWorkStart(context, row.id, row.event_key, scope.generation);
|
|
589
|
+
if (result) {
|
|
590
|
+
const outcome = { result, effect: "committed" };
|
|
591
|
+
settleLifecycleInvocation(context, row.id, outcome);
|
|
592
|
+
return { outcome, replay: true };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
throw new AppError("invocation_outcome_pending", "This attempt already started; inspect its retained operation outcome before retrying", 1, { invocation_id: input.id, effect: "unknown" });
|
|
596
|
+
}
|
|
425
597
|
input.assertCurrent(JSON.parse(row.scope_json));
|
|
426
598
|
if (row.status === "observed") {
|
|
427
599
|
const claimed = context.db.run("UPDATE lifecycle_invocations SET status = 'executing', updated_at = ? WHERE id = ? AND status = 'observed'", [context.now(), input.id]);
|
|
@@ -430,7 +602,7 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
430
602
|
continue;
|
|
431
603
|
}
|
|
432
604
|
if (row.status === "expired" || (row.deadline !== null && row.deadline <= Date.now())) {
|
|
433
|
-
context.db.
|
|
605
|
+
context.db.beginWriteTransaction();
|
|
434
606
|
try {
|
|
435
607
|
const current = load(context, input.id);
|
|
436
608
|
if (["issued", "expired"].includes(current.status)) {
|
|
@@ -47,7 +47,7 @@ export function registerManagedProcess(context, input) {
|
|
|
47
47
|
if (!input.operationId)
|
|
48
48
|
throw new Error("Unique managed operation requires operationId");
|
|
49
49
|
const db = registry(context);
|
|
50
|
-
db.
|
|
50
|
+
db.beginWriteTransaction();
|
|
51
51
|
try {
|
|
52
52
|
const existing = db.get("SELECT * FROM managed_processes WHERE operation_id = ? AND state IN ('starting','running','stopping','orphaned')", [input.operationId]);
|
|
53
53
|
if (existing)
|
|
@@ -154,7 +154,7 @@ export function managedOwnerIsAlive(record) {
|
|
|
154
154
|
export function claimExpiredManagedProcesses(context, ownerId) {
|
|
155
155
|
const db = registry(context);
|
|
156
156
|
const now = context.now();
|
|
157
|
-
db.
|
|
157
|
+
db.beginWriteTransaction();
|
|
158
158
|
try {
|
|
159
159
|
const candidates = db.all("SELECT * FROM managed_processes WHERE state IN ('starting','running','stopping','orphaned') AND lease_expires_at < ? ORDER BY lease_expires_at, id", [now]);
|
|
160
160
|
const claimed = [];
|
|
@@ -36,7 +36,7 @@ export function claimNextMergeJob(context, input) {
|
|
|
36
36
|
});
|
|
37
37
|
const now = context.now();
|
|
38
38
|
let claimedProtocolId = null;
|
|
39
|
-
context.db.
|
|
39
|
+
context.db.beginWriteTransaction();
|
|
40
40
|
try {
|
|
41
41
|
const job = context.db.get(`SELECT * FROM merge_queue
|
|
42
42
|
WHERE project_id = ? AND status IN ('ready', 'requeued')
|
|
@@ -90,7 +90,7 @@ export function claimMergeJob(context, input) {
|
|
|
90
90
|
workspacePath: input.workspacePath
|
|
91
91
|
});
|
|
92
92
|
const now = context.now();
|
|
93
|
-
context.db.
|
|
93
|
+
context.db.beginWriteTransaction();
|
|
94
94
|
try {
|
|
95
95
|
const job = queueJobByProtocol(context, project.id, input.protocolId);
|
|
96
96
|
if (!job || !["ready", "requeued"].includes(job.status)) {
|
|
@@ -141,7 +141,7 @@ export function claimMergeBundle(context, input) {
|
|
|
141
141
|
workspacePath: input.workspacePath
|
|
142
142
|
});
|
|
143
143
|
const now = context.now();
|
|
144
|
-
context.db.
|
|
144
|
+
context.db.beginWriteTransaction();
|
|
145
145
|
try {
|
|
146
146
|
for (const protocolId of protocolIds) {
|
|
147
147
|
const update = context.db.run(`UPDATE merge_queue
|
|
@@ -381,7 +381,7 @@ export function completeMergeBundle(context, input) {
|
|
|
381
381
|
});
|
|
382
382
|
}
|
|
383
383
|
const now = context.now();
|
|
384
|
-
context.db.
|
|
384
|
+
context.db.beginWriteTransaction();
|
|
385
385
|
try {
|
|
386
386
|
for (const protocolId of protocolIds) {
|
|
387
387
|
const protocol = requireProtocol(context, protocolId, project.id);
|
|
@@ -517,7 +517,7 @@ export function failMergeBundle(context, input) {
|
|
|
517
517
|
}
|
|
518
518
|
const now = context.now();
|
|
519
519
|
const status = input.requeue ? "requeued" : "failed";
|
|
520
|
-
context.db.
|
|
520
|
+
context.db.beginWriteTransaction();
|
|
521
521
|
try {
|
|
522
522
|
for (const protocolId of protocolIds) {
|
|
523
523
|
const protocol = requireProtocol(context, protocolId, project.id);
|
|
@@ -574,7 +574,7 @@ export function cancelMergeQueueJob(context, input) {
|
|
|
574
574
|
throw new AppError("validation", "merge-queue cancel requires --reason", 2);
|
|
575
575
|
}
|
|
576
576
|
const now = context.now();
|
|
577
|
-
context.db.
|
|
577
|
+
context.db.beginWriteTransaction();
|
|
578
578
|
try {
|
|
579
579
|
const job = queueJobByProtocol(context, protocol.project_id, protocol.id);
|
|
580
580
|
if (!job) {
|
|
@@ -63,7 +63,7 @@ export function stopMergeServer(context, input) { const changed = context.db.run
|
|
|
63
63
|
throw new AppError("not_found", "Running merge-server was not found", 1, { server_id: input.serverId }); return { ok: true, server_id: input.serverId, status: "stopping" }; }
|
|
64
64
|
/** Both dispatchers enter the same project FIFO before creating any provider Session. */
|
|
65
65
|
export function claimQueuedMergeRequest(context, input) {
|
|
66
|
-
context.db.
|
|
66
|
+
context.db.beginWriteTransaction();
|
|
67
67
|
try {
|
|
68
68
|
const request = context.db.get("SELECT merge_request_id, project_id, run_id, executor_work_id, target_workspace, execution_route, status, created_at FROM merge_requests WHERE merge_request_id = ?", [input.requestId]);
|
|
69
69
|
if (!request)
|
|
@@ -217,7 +217,7 @@ export function applyMigration(context, input) {
|
|
|
217
217
|
chain: chain.steps.map((step) => ({ ...step, status: "applied" }))
|
|
218
218
|
};
|
|
219
219
|
try {
|
|
220
|
-
writeDb.
|
|
220
|
+
writeDb.beginWriteTransaction();
|
|
221
221
|
writeDb.run(`INSERT INTO runtime_config (key, value_json, created_at, updated_at) VALUES (?, ?, ?, ?)`, [applyKey(project.id, sourceVersion, targetVersion), JSON.stringify(result), context.now(), context.now()]);
|
|
222
222
|
writeDb.exec("COMMIT");
|
|
223
223
|
}
|
|
@@ -140,7 +140,7 @@ export function migrateProjectIds(context, input) {
|
|
|
140
140
|
throw new AppError("conflict", `Target project id already exists: ${target.id}`, 1, plan);
|
|
141
141
|
}
|
|
142
142
|
context.db.exec("PRAGMA foreign_keys = OFF");
|
|
143
|
-
context.db.
|
|
143
|
+
context.db.beginWriteTransaction();
|
|
144
144
|
try {
|
|
145
145
|
context.db.run("UPDATE projects SET id = ?, short_id = ?, slug = ?, state_root = ?, updated_at = ? WHERE id = ?", [target.id, target.short_id, target.slug, target.state_root, now, oldId]);
|
|
146
146
|
for (const table of projectReferenceTables()) {
|
|
@@ -416,7 +416,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
416
416
|
next_action: nextActionForSyncedTarget(targetStage),
|
|
417
417
|
updated_at: now
|
|
418
418
|
};
|
|
419
|
-
context.db.
|
|
419
|
+
context.db.beginWriteTransaction();
|
|
420
420
|
try {
|
|
421
421
|
persistProtocolState(context, protocol, nextState);
|
|
422
422
|
if (targetStage === "ready_for_merge") {
|
|
@@ -486,7 +486,7 @@ export function cancelProtocol(context, input) {
|
|
|
486
486
|
const now = context.now();
|
|
487
487
|
const worktree = context.db.get("SELECT worktree_path, status FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
488
488
|
const worktreeRemoval = input.worktree === "remove" ? removeProtocolWorktreeBeforeCancel(protocol, state, worktree, input.force) : undefined;
|
|
489
|
-
context.db.
|
|
489
|
+
context.db.beginWriteTransaction();
|
|
490
490
|
try {
|
|
491
491
|
const nextState = {
|
|
492
492
|
...state,
|
|
@@ -57,7 +57,7 @@ async function launchControlWorker(context, input) {
|
|
|
57
57
|
// Reclaim only a proven dead launcher or admitted worker. Lease expiry
|
|
58
58
|
// never proves death or authorizes a second physical owner.
|
|
59
59
|
let claimed = false;
|
|
60
|
-
context.db.
|
|
60
|
+
context.db.beginWriteTransaction();
|
|
61
61
|
try {
|
|
62
62
|
claimed = context.db.run("INSERT OR IGNORE INTO run_control_workers (control_id, owner_token, status, resource_home, state_dir, created_at, updated_at) VALUES (?, ?, 'starting', ?, ?, ?, ?)", [input.controlId, token, home, directory, now, now]).changes === 1;
|
|
63
63
|
if (claimed) {
|
|
@@ -163,7 +163,7 @@ function reclaimControlWorker(context, input, projectId, token, home) {
|
|
|
163
163
|
// and a crash before the RUN CAS leaves the same dead owner reconcilable.
|
|
164
164
|
if (record && !finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: "stopped", reason: "control_owner_crash_reconciled" }))
|
|
165
165
|
return false;
|
|
166
|
-
context.db.
|
|
166
|
+
context.db.beginWriteTransaction();
|
|
167
167
|
try {
|
|
168
168
|
const changed = context.db.run(`UPDATE run_control_workers SET owner_token = ?, owner_pid = NULL, status = 'starting', process_id = NULL, process_lease_token = NULL, updated_at = ?
|
|
169
169
|
WHERE control_id = ? AND owner_token = ? AND process_id IS ? AND owner_pid IS ? AND status = ?
|
|
@@ -202,7 +202,7 @@ export async function serveRunControl(context, input) {
|
|
|
202
202
|
throw new AppError("run_control_owner_mismatch", "Control registration is not authorized", 1);
|
|
203
203
|
// Physical identity is committed with the sole RUN claim, before the worker
|
|
204
204
|
// registers itself in the separate resource store or performs any dispatch.
|
|
205
|
-
context.db.
|
|
205
|
+
context.db.beginWriteTransaction();
|
|
206
206
|
try {
|
|
207
207
|
const claimed = context.db.run("UPDATE run_control_workers SET status = 'running', owner_pid = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), input.controlId, worker.owner_token]);
|
|
208
208
|
if (claimed.changes !== 1)
|
|
@@ -748,7 +748,7 @@ export function recordRunControlIntent(context, input) {
|
|
|
748
748
|
const request = { project_root: project.root, run_id: input.runId, action: input.action, ...(input.action === "pause" ? { grace_ms: input.graceMs ?? 30_000, escalate: input.escalate ?? null } : { force: input.force ?? false }) };
|
|
749
749
|
const requestHash = crypto.createHash("sha256").update(JSON.stringify(request)).digest("hex");
|
|
750
750
|
ensureRunControlStorage(context.db);
|
|
751
|
-
context.db.
|
|
751
|
+
context.db.beginWriteTransaction();
|
|
752
752
|
try {
|
|
753
753
|
const existing = context.db.get("SELECT * FROM run_controls WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, input.runId, input.requestId]);
|
|
754
754
|
if (existing) {
|
|
@@ -50,7 +50,7 @@ export function controllerEnvironment(context, manifest, session) {
|
|
|
50
50
|
env.CODEX_HOME = session.codex_home;
|
|
51
51
|
return env;
|
|
52
52
|
}
|
|
53
|
-
export async function controllerAdapter(context, row, manifest, session, args, assertOwner, retainedOperationId) {
|
|
53
|
+
export async function controllerAdapter(context, row, manifest, session, args, assertOwner, retainedOperationId, signal) {
|
|
54
54
|
assertOwner?.();
|
|
55
55
|
const operationId = retainedOperationId ?? `${row.controller_id}:${crypto.randomUUID()}`;
|
|
56
56
|
const executable = manifest.adapters[runtimeHarness(session.profile)].executable;
|
|
@@ -60,7 +60,7 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
60
60
|
appendEvent(context, row.controller_id, "operation_requested", { operation_id: operationId, operation: args.slice(0, 2), session_id: session.id || null });
|
|
61
61
|
try {
|
|
62
62
|
assertOwner?.();
|
|
63
|
-
const receipt = await runHarnessAdapter({ executable, args, env: controllerEnvironment(context, manifest, session), operationId, runtimeOwner: session.owner, timeoutMs: args[0] === "session" && args[1] === "prompt" ? 2 * 60 * 60_000 : 120_000 });
|
|
63
|
+
const receipt = await runHarnessAdapter({ executable, args, env: controllerEnvironment(context, manifest, session), operationId, runtimeOwner: session.owner, timeoutMs: args[0] === "session" && args[1] === "prompt" ? 2 * 60 * 60_000 : 120_000, ...(signal ? { signal } : {}) });
|
|
64
64
|
context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(receipt), context.now(), operationId]);
|
|
65
65
|
const target = args[args.indexOf("--session-id") + 1];
|
|
66
66
|
if (args[0] === "session" && args.includes("--session-id") && adapterSessionId(receipt) !== target)
|
|
@@ -73,7 +73,12 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
73
73
|
return receipt;
|
|
74
74
|
}
|
|
75
75
|
catch (error) {
|
|
76
|
-
|
|
76
|
+
try {
|
|
77
|
+
context.db.run("UPDATE run_controller_operations SET status = CASE WHEN status = 'dispatching' THEN 'outcome_unknown' ELSE status END, error_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(errorRecord(error)), context.now(), operationId]);
|
|
78
|
+
}
|
|
79
|
+
catch (persistence) {
|
|
80
|
+
process.stderr.write(`${JSON.stringify({ operation_id: operationId, primary: errorRecord(error), persistence_error: errorRecord(persistence) })}\n`);
|
|
81
|
+
}
|
|
77
82
|
throw error;
|
|
78
83
|
}
|
|
79
84
|
}
|
|
@@ -35,7 +35,7 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
|
|
|
35
35
|
throw error;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
context.db.
|
|
38
|
+
context.db.beginWriteTransaction();
|
|
39
39
|
try {
|
|
40
40
|
assertPhysicalOwner();
|
|
41
41
|
assertRunMutationAllowed(context, row.project_id, row.run_id, row.generation);
|
|
@@ -112,7 +112,7 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
|
|
|
112
112
|
await delay(observationBudget.nextDelay());
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
context.db.
|
|
115
|
+
context.db.beginWriteTransaction();
|
|
116
116
|
try {
|
|
117
117
|
assertCurrent();
|
|
118
118
|
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = ?, error_json = NULL, updated_at = ? WHERE operation_id = ?", [JSON.stringify(receipt), context.now(), operationId]);
|
|
@@ -92,7 +92,7 @@ function prepareSpawn(context, row) {
|
|
|
92
92
|
return null;
|
|
93
93
|
if (launch.value.retired_process)
|
|
94
94
|
retire(resourceContext, row, launch.value.retired_process);
|
|
95
|
-
context.db.
|
|
95
|
+
context.db.beginWriteTransaction();
|
|
96
96
|
try {
|
|
97
97
|
assertLaunchAllowed(context, row);
|
|
98
98
|
const changed = context.db.run(`UPDATE run_controllers SET owner_token = ?, owner_pid = NULL, process_id = NULL, process_lease_token = NULL, status = 'starting', last_error_json = NULL, updated_at = ?
|
|
@@ -121,7 +121,7 @@ export function claimControllerProcess(context, row) {
|
|
|
121
121
|
const launch = launchRecord(context, row);
|
|
122
122
|
if (!launch && (!row.process_id || !row.process_lease_token))
|
|
123
123
|
throw new AppError("controller_owner_mismatch", "Managed registration is not authorized", 1);
|
|
124
|
-
context.db.
|
|
124
|
+
context.db.beginWriteTransaction();
|
|
125
125
|
try {
|
|
126
126
|
const claimed = context.db.run("UPDATE run_controllers SET owner_pid = ?, status = 'running', updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), row.controller_id, row.owner_token]);
|
|
127
127
|
if (claimed.changes !== 1)
|
|
@@ -78,7 +78,7 @@ export function recordControllerResume(context, input) {
|
|
|
78
78
|
if (!previousOwner || previousOwner.lease_token !== row.process_lease_token || previousOwner.kind !== "run-controller" || previousOwner.owner_id !== row.controller_id || previousOwner.project_id !== project.id || previousOwner.run_id !== run.id || managedProcessRuntimeHome(previousOwner, context) !== context.ddFlowHome || !previousOwner.pid || !previousOwner.pid_started_at || processTreeIsAlive(previousOwner))
|
|
79
79
|
throw new AppError("controller_owner_unsettled", "Resume requires proof that the previous physical controller has exited", 1);
|
|
80
80
|
const token = crypto.randomUUID(), now = context.now();
|
|
81
|
-
context.db.
|
|
81
|
+
context.db.beginWriteTransaction();
|
|
82
82
|
try {
|
|
83
83
|
const duplicate = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
|
|
84
84
|
if (duplicate) {
|
|
@@ -168,7 +168,7 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
168
168
|
throw new AppError("recovery_session_unsettled", "Retained native Session is not ready for same-Session recovery", 1);
|
|
169
169
|
assertRecoveryOwner();
|
|
170
170
|
const prepared = { prepared: true, recovery_id: recovery.recovery_id, session_id: session.id, daemon_id: daemonId, process_id: dispatch.process_id, resume_operation_id: dispatch.resume_operation_id };
|
|
171
|
-
context.db.
|
|
171
|
+
context.db.beginWriteTransaction();
|
|
172
172
|
try {
|
|
173
173
|
assertRecoveryOwner();
|
|
174
174
|
const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id, ownerId: session.owner.owner_id });
|
|
@@ -195,7 +195,7 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
195
195
|
assertRecoveryOwner();
|
|
196
196
|
if (recoveryGuard(context, row.project_id, row.run_id)?.status !== "recovered")
|
|
197
197
|
throw new AppError("trusted_recovery_acceptance_required", "Recovery Turn ended without its native acknowledgement", 1);
|
|
198
|
-
context.db.
|
|
198
|
+
context.db.beginWriteTransaction();
|
|
199
199
|
try {
|
|
200
200
|
assertRecoveryOwner();
|
|
201
201
|
context.db.run("UPDATE run_controls SET status = 'resumed', updated_at = ? WHERE control_id = ? AND status = 'resuming'", [context.now(), recovery.control_id]);
|