@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
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 +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDelay, renewalFailure, renewalRemaining, RENEWAL_POLICY } from "../harness-runtime/lib/lease-renewal.mjs";
|
|
4
|
+
/** Registry renewals happen outside RUN writers. Assertions never renew. */
|
|
5
|
+
export function managedLeaseMonitor(renew) {
|
|
6
|
+
const episode = createRenewalState();
|
|
7
|
+
let expiry = 0;
|
|
8
|
+
let failure = null;
|
|
9
|
+
let lost = false;
|
|
10
|
+
let fatal = null;
|
|
11
|
+
let closing = false;
|
|
12
|
+
let pending;
|
|
13
|
+
const shutdown = new AbortController();
|
|
14
|
+
const uncertain = () => new AppError("controller_lease_unconfirmed", "Managed lease requires confirmation outside the writer", 1, { cause: failure, exhausted: episode.exhausted, closing });
|
|
15
|
+
const assertNoRenewal = () => {
|
|
16
|
+
if (closing)
|
|
17
|
+
throw uncertain();
|
|
18
|
+
if (fatal)
|
|
19
|
+
throw fatal;
|
|
20
|
+
if (lost)
|
|
21
|
+
throw new AppError("controller_lease_lost", "Managed owner was replaced or retired", 1);
|
|
22
|
+
if (failure || Date.now() >= expiry)
|
|
23
|
+
throw uncertain();
|
|
24
|
+
};
|
|
25
|
+
const tick = () => {
|
|
26
|
+
if (closing)
|
|
27
|
+
throw uncertain();
|
|
28
|
+
if (fatal)
|
|
29
|
+
throw fatal;
|
|
30
|
+
if (lost || failure && !renewalRemaining(episode) || episode.exhausted)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
episode.uncertainSince ??= performance.now();
|
|
34
|
+
const remaining = renewalRemaining(episode);
|
|
35
|
+
if (!remaining) {
|
|
36
|
+
failure = uncertain();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const receipt = renew(Math.min(RENEWAL_POLICY.attemptMs, remaining));
|
|
40
|
+
if (!receipt.owned) {
|
|
41
|
+
lost = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const value = Date.parse(receipt.lease_expires_at);
|
|
45
|
+
if (!Number.isFinite(value))
|
|
46
|
+
throw new AppError("managed_lease_receipt_invalid", "Renewal has no valid persisted expiry", 1);
|
|
47
|
+
if (value <= Date.now())
|
|
48
|
+
throw new AppError("controller_lease_unconfirmed", "Renewal receipt has already expired", 1);
|
|
49
|
+
if ((!failure || renewalRemaining(episode)) && renewalConfirmed(episode)) {
|
|
50
|
+
expiry = value;
|
|
51
|
+
failure = null;
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
failure = uncertain();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
failure = error;
|
|
58
|
+
if (maintenanceRetryable(error) || error instanceof AppError && error.code === "controller_lease_unconfirmed")
|
|
59
|
+
renewalFailure(episode);
|
|
60
|
+
else {
|
|
61
|
+
fatal = error;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const assertConfirmed = () => { tick(); assertNoRenewal(); };
|
|
67
|
+
const runConfirmation = async (assertCurrent) => {
|
|
68
|
+
for (;;) {
|
|
69
|
+
if (closing)
|
|
70
|
+
throw uncertain();
|
|
71
|
+
assertCurrent();
|
|
72
|
+
tick();
|
|
73
|
+
try {
|
|
74
|
+
assertNoRenewal();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (lost || episode.exhausted || !renewalRemaining(episode))
|
|
79
|
+
throw error;
|
|
80
|
+
try {
|
|
81
|
+
await delay(Math.min(renewalDelay(episode), renewalRemaining(episode)), undefined, { signal: shutdown.signal });
|
|
82
|
+
}
|
|
83
|
+
catch (cause) {
|
|
84
|
+
if (closing)
|
|
85
|
+
throw uncertain();
|
|
86
|
+
throw cause;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const ensureConfirmed = async (assertCurrent = () => { }) => {
|
|
92
|
+
if (closing)
|
|
93
|
+
throw uncertain();
|
|
94
|
+
if (pending)
|
|
95
|
+
assertCurrent();
|
|
96
|
+
pending ??= runConfirmation(assertCurrent).finally(() => { pending = undefined; });
|
|
97
|
+
await pending;
|
|
98
|
+
if (closing)
|
|
99
|
+
throw uncertain();
|
|
100
|
+
assertCurrent();
|
|
101
|
+
assertNoRenewal();
|
|
102
|
+
};
|
|
103
|
+
const background = (assertCurrent, onFailure, onConfirmed = () => { }) => {
|
|
104
|
+
void ensureConfirmed(assertCurrent).then(() => {
|
|
105
|
+
if (closing)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
onConfirmed();
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
fatal = failure = error;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}).catch(error => {
|
|
115
|
+
if (closing)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
onFailure(error);
|
|
119
|
+
}
|
|
120
|
+
catch (reporting) {
|
|
121
|
+
fatal = failure = reporting;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
return { tick, assertNoRenewal, assertConfirmed, ensureConfirmed, background,
|
|
126
|
+
close: () => { closing = true; shutdown.abort(); },
|
|
127
|
+
isConfirmed: () => !closing && !lost && !failure && Date.now() < expiry };
|
|
128
|
+
}
|
|
@@ -4,10 +4,17 @@ import path from "node:path";
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import net from "node:net";
|
|
6
6
|
import { getResourceDatabase } from "../storage/database.js";
|
|
7
|
-
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
8
8
|
export function managedProcessBinding(record, home) {
|
|
9
9
|
return { resource_home: home, process_id: record.id, registration_sha256: crypto.createHash("sha256").update(JSON.stringify([home, record.id, record.kind, record.owner_id, record.lease_token, record.pid, record.pid_started_at, record.project_id, record.run_id, record.work_id, record.check_id, record.operation_id, record.metadata_json])).digest("hex") };
|
|
10
10
|
}
|
|
11
|
+
export function assertManagedProcessAdmission(context, record) {
|
|
12
|
+
const expiry = Date.parse(record.lease_expires_at);
|
|
13
|
+
if (!Number.isFinite(expiry))
|
|
14
|
+
throw new AppError("runtime_artifact_invalid", "Managed lease expiry is malformed", 1, { process_id: record.id });
|
|
15
|
+
if (expiry <= Date.parse(context.now()))
|
|
16
|
+
throw new AppError("process_ownership_unknown", "Managed owner needs fresh renewal before admission", 1, { process_id: record.id, reason: "renewal_required", lease_expires_at: record.lease_expires_at });
|
|
17
|
+
}
|
|
11
18
|
const defaultLeaseMs = 15 * 60_000;
|
|
12
19
|
export function resourceHome(context) {
|
|
13
20
|
return context.env?.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome ?? "/tmp/dd-flow-runtime";
|
|
@@ -53,8 +60,10 @@ export function prepareManagedProcessRegistration(context, input) {
|
|
|
53
60
|
const observer = input.kind === "eval-observer";
|
|
54
61
|
const observerBirth = observer && input.pid ? processStartedAt(input.pid) : null;
|
|
55
62
|
if (observer) {
|
|
56
|
-
if (!Number.isSafeInteger(input.pid) || !input.pid || input.pid < 1 ||
|
|
63
|
+
if (!Number.isSafeInteger(input.pid) || !input.pid || input.pid < 1 || input.ownerPid !== undefined && input.ownerPid !== input.pid || input.processGroupId !== undefined && input.processGroupId !== input.pid)
|
|
57
64
|
throw new AppError("process_ownership_unknown", "EVAL observer registration requires its live self-owned process identity", 1);
|
|
65
|
+
if (!observerBirth)
|
|
66
|
+
throw new AppError("process_ownership_unknown", "EVAL observer physical identity observation is unavailable", 1, { process_id: input.id, reason: "physical_identity_unavailable", phase: "observer_registration", effect: "no_effect", native_dispatch_started: false });
|
|
58
67
|
input = { ...input, ownerPid: input.pid, uniqueActiveOperation: true };
|
|
59
68
|
}
|
|
60
69
|
if (input.uniqueActiveOperation && !input.operationId)
|
|
@@ -71,7 +80,7 @@ export function prepareManagedProcessRegistration(context, input) {
|
|
|
71
80
|
catch {
|
|
72
81
|
throw new AppError("validation", "metadata must be JSON-serializable", 2);
|
|
73
82
|
}
|
|
74
|
-
const reader = getResourceDatabase(resourceHome(context), "read_existing");
|
|
83
|
+
const reader = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
|
|
75
84
|
try {
|
|
76
85
|
if (input.id && reader.get("SELECT 1 FROM managed_processes WHERE id = ?", [input.id]))
|
|
77
86
|
throw new AppError("validation", "Managed process ID already exists; use its retained registration", 2, { process_id: input.id });
|
|
@@ -89,11 +98,14 @@ export function prepareManagedProcessRegistration(context, input) {
|
|
|
89
98
|
export function registerManagedProcess(context, rawInput) {
|
|
90
99
|
const { input, observer, observerBirth, metadataJson } = prepareManagedProcessRegistration(context, rawInput);
|
|
91
100
|
if (!input.uniqueActiveOperation)
|
|
92
|
-
return insertManagedProcess(context, input, metadataJson);
|
|
93
|
-
const db = registry(context);
|
|
101
|
+
return registry(context).writeTransaction(() => insertManagedProcess(context, input, metadataJson));
|
|
94
102
|
const observerProcessBirth = observer ? stableProcessBirth(input.pid) : null;
|
|
103
|
+
const observerOwner = observer ? parseMetadata(metadataJson) : null;
|
|
104
|
+
if (observer && (!observerProcessBirth || !observerOwner?.owner_pid_started_at || !parseBirth(observerOwner.owner_birth)))
|
|
105
|
+
throw new AppError("process_ownership_unknown", "EVAL observer physical identity observation is unavailable", 1, { process_id: input.id, reason: "physical_identity_unavailable", phase: "observer_registration", effect: "no_effect", native_dispatch_started: false });
|
|
95
106
|
if (observer && !processIsAlive({ pid: input.pid, pid_started_at: observerBirth, metadata_json: JSON.stringify({ process_birth: observerProcessBirth }) }))
|
|
96
107
|
throw new AppError("process_ownership_unknown", "EVAL observer changed before registration", 1);
|
|
108
|
+
const db = registry(context);
|
|
97
109
|
return db.writeTransaction(() => {
|
|
98
110
|
const existing = db.get("SELECT * FROM managed_processes WHERE operation_id = ? AND state IN ('starting','running','stopping','orphaned')", [input.operationId]);
|
|
99
111
|
if (existing)
|
|
@@ -133,29 +145,53 @@ export function prepareManagedProcessConfirmation(context, input) {
|
|
|
133
145
|
const current = readManagedProcess(context, input.id);
|
|
134
146
|
if (current.lease_token !== input.leaseToken)
|
|
135
147
|
throw new AppError("process_lease_lost", "Managed process lease does not match", 1);
|
|
136
|
-
if (current.state !== "starting")
|
|
137
|
-
throw new AppError("process_confirmation_rejected", "Only
|
|
148
|
+
if (current.state !== "starting" && !(current.state === "running" && current.pid === input.pid && processIsAlive(current) && (!input.processGroupId || parseMetadata(current.metadata_json).process_group_id === input.processGroupId) && (!input.ownerPid || parseMetadata(current.metadata_json).owner_pid === input.ownerPid)))
|
|
149
|
+
throw new AppError("process_confirmation_rejected", "Only the retained physical process may be confirmed", 1, { process_id: current.id, state: current.state });
|
|
138
150
|
return current;
|
|
139
151
|
}
|
|
140
152
|
export function confirmManagedProcess(context, input) {
|
|
141
|
-
const
|
|
153
|
+
const prepared = prepareManagedProcessConfirmation(context, input);
|
|
154
|
+
const retainedIdentity = parseMetadata(prepared.metadata_json);
|
|
155
|
+
const retainedOwner = retainedIdentity.owner_pid;
|
|
156
|
+
const validRetainedOwner = typeof retainedOwner === "number" && Number.isSafeInteger(retainedOwner) && retainedOwner > 0;
|
|
157
|
+
const ownerUnknown = Object.hasOwn(retainedIdentity, "owner_pid") && (!validRetainedOwner || typeof retainedIdentity.owner_pid_started_at !== "string" || !retainedIdentity.owner_pid_started_at.trim() || Object.hasOwn(retainedIdentity, "owner_birth") && !parseBirth(retainedIdentity.owner_birth));
|
|
158
|
+
if (prepared.state === "running" && (!prepared.pid_started_at || Object.hasOwn(retainedIdentity, "process_birth") && !parseBirth(retainedIdentity.process_birth) || ownerUnknown) || input.ownerPid === undefined && Object.hasOwn(retainedIdentity, "owner_pid") && !validRetainedOwner)
|
|
159
|
+
throw new AppError("process_ownership_unknown", "Retained managed physical identity is unconfirmed", 1, { process_id: input.id, reason: "stored_identity_unconfirmed", phase: "process_confirmation", effect: "no_effect", native_dispatch_started: false });
|
|
160
|
+
// Initial confirmation must observe the registered owner too, even when a
|
|
161
|
+
// bootstrap caller omits its optional override. Running bindings never repair.
|
|
162
|
+
const ownerPid = input.ownerPid ?? (validRetainedOwner ? retainedOwner : undefined);
|
|
163
|
+
const pidStartedAt = processStartedAt(input.pid), processBirth = stableProcessBirth(input.pid);
|
|
164
|
+
const ownerStartedAt = ownerPid ? processStartedAt(ownerPid) : null, ownerBirth = ownerPid ? stableProcessBirth(ownerPid) : null;
|
|
165
|
+
if (!pidStartedAt || !processBirth || ownerPid && (!ownerStartedAt || !ownerBirth))
|
|
166
|
+
throw new AppError("process_ownership_unknown", "Managed physical identity observation is unavailable", 1, { process_id: input.id, reason: "physical_identity_unavailable", phase: "process_confirmation", effect: "no_effect", native_dispatch_started: false });
|
|
142
167
|
const db = registry(context);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
return db.writeTransaction(() => {
|
|
169
|
+
const current = requireProcess(db, input.id);
|
|
170
|
+
if (current.lease_token !== input.leaseToken || !["starting", "running"].includes(current.state))
|
|
171
|
+
throw new AppError("process_confirmation_rejected", "Managed process changed before confirmation", 1);
|
|
172
|
+
if (current.state === "running") {
|
|
173
|
+
if (prepared.state !== "running" || managedProcessBinding(current, resourceHome(context)).registration_sha256 !== managedProcessBinding(prepared, resourceHome(context)).registration_sha256 || current.pid !== input.pid || input.processGroupId && parseMetadata(current.metadata_json).process_group_id !== input.processGroupId || input.ownerPid && parseMetadata(current.metadata_json).owner_pid !== input.ownerPid)
|
|
174
|
+
throw new AppError("process_confirmation_rejected", "Confirmed process binding changed", 1);
|
|
175
|
+
return { ...current, registration_sha256: managedProcessBinding(current, resourceHome(context)).registration_sha256 };
|
|
176
|
+
}
|
|
177
|
+
const now = context.now();
|
|
178
|
+
const metadata = parseMetadata(current.metadata_json);
|
|
179
|
+
if (input.processGroupId)
|
|
180
|
+
metadata.process_group_id = input.processGroupId;
|
|
181
|
+
metadata.process_birth = processBirth;
|
|
182
|
+
if (ownerPid) {
|
|
183
|
+
metadata.owner_pid = ownerPid;
|
|
184
|
+
metadata.owner_pid_started_at = ownerStartedAt;
|
|
185
|
+
metadata.owner_birth = ownerBirth;
|
|
186
|
+
}
|
|
187
|
+
const result = db.run(`UPDATE managed_processes
|
|
154
188
|
SET pid = ?, pid_started_at = ?, state = 'running', lease_expires_at = ?, updated_at = ?, metadata_json = ?
|
|
155
|
-
WHERE id = ? AND lease_token = ? AND state = 'starting'`, [input.pid,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
189
|
+
WHERE id = ? AND lease_token = ? AND state = 'starting'`, [input.pid, pidStartedAt, leaseExpiry(now, input.leaseMs), now, JSON.stringify(metadata), input.id, input.leaseToken]);
|
|
190
|
+
if (result.changes !== 1)
|
|
191
|
+
throw new Error(`Managed process cannot be confirmed: ${input.id}`);
|
|
192
|
+
const confirmed = requireProcess(db, input.id);
|
|
193
|
+
return { ...confirmed, registration_sha256: managedProcessBinding(confirmed, resourceHome(context)).registration_sha256 };
|
|
194
|
+
});
|
|
159
195
|
}
|
|
160
196
|
export function prepareManagedProcessHeartbeat(context, input) {
|
|
161
197
|
requireProcessText(input.id, "id");
|
|
@@ -163,28 +199,43 @@ export function prepareManagedProcessHeartbeat(context, input) {
|
|
|
163
199
|
prepareManagedProcessLease(context, input.leaseMs);
|
|
164
200
|
}
|
|
165
201
|
export function heartbeatManagedProcess(context, input) {
|
|
202
|
+
return renewManagedProcess(context, input).owned;
|
|
203
|
+
}
|
|
204
|
+
export function renewManagedProcess(context, input) {
|
|
166
205
|
prepareManagedProcessHeartbeat(context, input);
|
|
167
|
-
|
|
206
|
+
if (context.resourceDatabaseMode === "prepared_existing")
|
|
207
|
+
registry(context);
|
|
208
|
+
const reader = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
|
|
168
209
|
try {
|
|
169
|
-
if (!reader.get("SELECT 1 FROM managed_processes WHERE id
|
|
170
|
-
return false;
|
|
210
|
+
if (!reader.get("SELECT 1 FROM managed_processes WHERE id=? AND lease_token=? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
|
|
211
|
+
return { owned: false, process_id: input.id };
|
|
171
212
|
}
|
|
172
213
|
finally {
|
|
173
214
|
reader.close?.();
|
|
174
215
|
}
|
|
175
216
|
const db = registry(context);
|
|
176
|
-
return db.
|
|
217
|
+
return db.writeTransaction(() => {
|
|
218
|
+
const now = context.now();
|
|
219
|
+
if (!db.get("SELECT 1 FROM managed_processes WHERE id=? AND lease_token=? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
|
|
220
|
+
return { owned: false, process_id: input.id };
|
|
221
|
+
const changed = db.run("UPDATE managed_processes SET lease_expires_at = ?, updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping')", [leaseExpiry(now, input.leaseMs), now, input.id, input.leaseToken]).changes === 1;
|
|
222
|
+
if (!changed)
|
|
223
|
+
return { owned: false, process_id: input.id };
|
|
224
|
+
const record = requireProcess(db, input.id);
|
|
225
|
+
return { owned: true, process_id: record.id, lease_expires_at: record.lease_expires_at, registration_sha256: managedProcessBinding(record, resourceHome(context)).registration_sha256 };
|
|
226
|
+
});
|
|
177
227
|
}
|
|
178
228
|
/** Read-only lease check for a caller already holding a RUN writer. Renewal
|
|
179
229
|
* must happen before entering that writer, never from this assertion. */
|
|
180
230
|
export function assertManagedProcessLease(context, input) {
|
|
181
|
-
const reader = getResourceDatabase(resourceHome(context), "read_isolated");
|
|
231
|
+
const reader = getResourceDatabase(resourceHome(context), "read_isolated", context.databaseDeadlineAt);
|
|
182
232
|
try {
|
|
183
233
|
const current = reader.get("SELECT pid, lease_expires_at, state FROM managed_processes WHERE id = ? AND lease_token = ?", [input.id, input.leaseToken]);
|
|
184
|
-
if (!current || current.pid !== input.pid || !["starting", "running"].includes(current.state)
|
|
185
|
-
|
|
186
|
-
throw new AppError("controller_lease_lost", "Managed owner lease changed or expired before productive dispatch", 1, { process_id: input.id });
|
|
234
|
+
if (!current || current.pid !== input.pid || !["starting", "running"].includes(current.state)) {
|
|
235
|
+
throw new AppError("controller_lease_lost", "Managed owner lease changed before productive dispatch", 1, { process_id: input.id });
|
|
187
236
|
}
|
|
237
|
+
if (!Number.isFinite(Date.parse(current.lease_expires_at)) || Date.parse(current.lease_expires_at) <= Date.now())
|
|
238
|
+
throw new AppError("controller_lease_unconfirmed", "Managed owner requires renewal before productive dispatch", 1, { process_id: input.id, reason: "renewal_required" });
|
|
188
239
|
}
|
|
189
240
|
finally {
|
|
190
241
|
reader.close?.();
|
|
@@ -200,21 +251,26 @@ export function prepareManagedProcessFinish(context, input) {
|
|
|
200
251
|
return readManagedProcess(context, input.id);
|
|
201
252
|
}
|
|
202
253
|
export function finishManagedProcess(context, input) {
|
|
203
|
-
|
|
204
|
-
if (existing.lease_token === input.leaseToken && existing.state === input.state)
|
|
205
|
-
return true;
|
|
206
|
-
if (existing.lease_token !== input.leaseToken || !["starting", "running", "stopping", "orphaned"].includes(existing.state))
|
|
207
|
-
return false;
|
|
254
|
+
prepareManagedProcessFinish(context, input);
|
|
208
255
|
const db = registry(context);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
256
|
+
return db.writeTransaction(() => {
|
|
257
|
+
const existing = requireProcess(db, input.id);
|
|
258
|
+
if (existing.lease_token !== input.leaseToken || existing.state !== input.state && !["starting", "running", "stopping", "orphaned"].includes(existing.state))
|
|
259
|
+
return false;
|
|
260
|
+
if (existing.state === input.state) {
|
|
261
|
+
db.run("DELETE FROM managed_resources WHERE process_id = ?", [input.id]);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
const now = context.now();
|
|
265
|
+
const updated = db.run("UPDATE managed_processes SET state = ?, termination_reason = ?, finished_at = ?, updated_at = ?, lease_expires_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping','orphaned')", [input.state, input.reason ?? null, now, now, now, input.id, input.leaseToken]).changes === 1;
|
|
266
|
+
if (updated)
|
|
267
|
+
db.run("DELETE FROM managed_resources WHERE process_id = ?", [input.id]);
|
|
268
|
+
if (updated)
|
|
269
|
+
return true;
|
|
270
|
+
// Another client may have completed the same lease after our initial read.
|
|
271
|
+
const current = requireProcess(db, input.id);
|
|
272
|
+
return current.lease_token === input.leaseToken && current.state === input.state;
|
|
273
|
+
});
|
|
218
274
|
}
|
|
219
275
|
export function processIsAlive(record) {
|
|
220
276
|
if (!record.pid)
|
|
@@ -300,7 +356,7 @@ export function managedLeaseReleased(record) {
|
|
|
300
356
|
&& ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at);
|
|
301
357
|
}
|
|
302
358
|
export function managedProcessStatus(context) {
|
|
303
|
-
const db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
359
|
+
const db = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
|
|
304
360
|
try {
|
|
305
361
|
return db.all("SELECT * FROM managed_processes ORDER BY updated_at DESC, id");
|
|
306
362
|
}
|
|
@@ -315,7 +371,7 @@ export function prepareManagedProcessStopOperation(context, input) {
|
|
|
315
371
|
return { record, graceMs };
|
|
316
372
|
}
|
|
317
373
|
function readManagedProcess(context, id) {
|
|
318
|
-
const db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
374
|
+
const db = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
|
|
319
375
|
try {
|
|
320
376
|
return requireProcess(db, id);
|
|
321
377
|
}
|
|
@@ -339,14 +395,19 @@ export async function stopManagedProcess(context, input) {
|
|
|
339
395
|
return record;
|
|
340
396
|
db.run("UPDATE managed_processes SET state = 'stopping', updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','orphaned')", [context.now(), record.id, record.lease_token]);
|
|
341
397
|
if (processTreeIsAlive(record)) {
|
|
342
|
-
|
|
398
|
+
const signalErrors = [];
|
|
399
|
+
const termError = terminateOwnedProcess(record, "SIGTERM");
|
|
400
|
+
if (termError)
|
|
401
|
+
signalErrors.push(errorRecord(termError));
|
|
343
402
|
await delay(graceMs);
|
|
344
|
-
if (input.force !== false && processTreeIsAlive(record)) {
|
|
345
|
-
terminateOwnedProcess(record, "SIGKILL");
|
|
403
|
+
if (!termError && input.force !== false && processTreeIsAlive(record)) {
|
|
404
|
+
const killError = terminateOwnedProcess(record, "SIGKILL");
|
|
405
|
+
if (killError)
|
|
406
|
+
signalErrors.push(errorRecord(killError));
|
|
346
407
|
await delay(250);
|
|
347
408
|
}
|
|
348
409
|
if (processTreeIsAlive(record))
|
|
349
|
-
throw Object.assign(new Error("Owned process tree could not be safely stopped; resources retained"), { code: "process_tree_not_settled", details: { process_id: record.id } });
|
|
410
|
+
throw Object.assign(new Error("Owned process tree could not be safely stopped; resources retained"), { code: "process_tree_not_settled", details: { process_id: record.id, signal_errors: signalErrors } });
|
|
350
411
|
}
|
|
351
412
|
if (!finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: "stopped", reason: input.reason ?? "requested_stop" }))
|
|
352
413
|
throw Object.assign(new Error("Managed process ownership changed before settlement"), { code: "process_lease_lost" });
|
|
@@ -428,7 +489,7 @@ export async function reservePorts(context, input) {
|
|
|
428
489
|
}
|
|
429
490
|
return { ports, release: () => releasePortClaims(db, claims) };
|
|
430
491
|
}
|
|
431
|
-
function registry(context) { return getResourceDatabase(resourceHome(context)); }
|
|
492
|
+
function registry(context) { return getResourceDatabase(resourceHome(context), context.resourceDatabaseMode, context.databaseDeadlineAt); }
|
|
432
493
|
function requireProcess(db, id) { const row = db.get("SELECT * FROM managed_processes WHERE id = ?", [id]); if (!row)
|
|
433
494
|
throw new AppError("not_found", `Managed process is missing: ${id}`, 1); return row; }
|
|
434
495
|
export function prepareManagedProcessLease(context, leaseMs) { return leaseExpiry(context.now(), leaseMs); }
|
|
@@ -460,7 +521,10 @@ function terminateOwnedProcess(record, signal) {
|
|
|
460
521
|
try {
|
|
461
522
|
process.kill(process.platform === "win32" || group !== record.pid ? record.pid : -group, signal);
|
|
462
523
|
}
|
|
463
|
-
catch {
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (error.code !== "ESRCH")
|
|
526
|
+
return Object.assign(error, { details: { process_id: record.id, pid: record.pid, pgid: group ?? null, signal, phase: "owned_process_stop" } });
|
|
527
|
+
}
|
|
464
528
|
}
|
|
465
529
|
function parseMetadata(value) { try {
|
|
466
530
|
const parsed = JSON.parse(value);
|
|
@@ -7,6 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { AppError } from "../shared/errors.js";
|
|
9
9
|
import { runtimeBudgetForRun } from "./runtime-budget.js";
|
|
10
|
+
import { managedLeaseMonitor } from "./managed-lease-monitor.js";
|
|
10
11
|
import { flowCommand } from "./stage-pause.js";
|
|
11
12
|
import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
|
|
12
13
|
import { harnessRuntimeArguments, resolveHarnessCommand } from "./harness-config.js";
|
|
@@ -106,36 +107,73 @@ async function dispatch(context, input) {
|
|
|
106
107
|
const executable = resolveHarnessCommand(context.ddFlowHome, input.profile.harness, "adapter");
|
|
107
108
|
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", stateDir, "--journal", adapterJournal, "--cwd", input.request.target_workspace, "--provider", input.profile.provider, "--model", input.profile.model, "--reasoning", input.profile.reasoning, "--mode", input.profile.mode, "--permission", input.profile.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, input.profile.harness), "--json"];
|
|
108
109
|
const retained = { profile: input.profile, executable, owner: runtimeOwner, session_id: null, state_dir: stateDir, create_operation_id: `${input.request.merge_request_id}:${token}:session.create`, prompt_operation_id: `${input.request.merge_request_id}:${token}:session.prompt` };
|
|
109
|
-
const retain = () => { context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify(retained), context.now(), input.request.merge_request_id, input.serverId, token]);
|
|
110
|
+
const retain = () => { const changed = context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify(retained), context.now(), input.request.merge_request_id, input.serverId, token]); if (changed.changes !== 1)
|
|
111
|
+
throw new AppError("capacity_continuation_blocked", "MERGE dispatch owner changed before durable receipt retention", 1); };
|
|
110
112
|
retain();
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
+
const lease = managedLeaseMonitor(attemptMs => {
|
|
114
|
+
const db = context.db.withDeadline(Date.now() + attemptMs);
|
|
115
|
+
return db.writeTransaction(() => {
|
|
116
|
+
assertRunMutationAllowed({ ...context, db }, input.request.project_id, input.request.run_id, generation);
|
|
117
|
+
const expiry = new Date(Date.now() + 120_000).toISOString();
|
|
118
|
+
const changed = db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE merge_request_id = ? AND status IN ('dispatching','active') AND dispatch_owner = ? AND dispatch_lease_token = ?", [expiry, context.now(), input.request.merge_request_id, input.serverId, token]);
|
|
119
|
+
return changed.changes === 1 ? { owned: true, lease_expires_at: expiry } : { owned: false };
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
const ownership = new AbortController();
|
|
123
|
+
const leaseHeartbeat = setInterval(() => {
|
|
124
|
+
lease.background(() => assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation), error => {
|
|
125
|
+
ownership.abort(error);
|
|
126
|
+
try {
|
|
127
|
+
event(input.journal, { type: "dispatch_lease_unconfirmed", request_id: input.request.merge_request_id, error: errorRecord(error), at: context.now() });
|
|
128
|
+
}
|
|
129
|
+
catch { /* No unhandled timer exception; the next boundary sees retained monitor failure. */ }
|
|
130
|
+
});
|
|
131
|
+
}, 30_000);
|
|
113
132
|
leaseHeartbeat.unref();
|
|
114
133
|
try {
|
|
115
|
-
const adapterCall = (args, timeoutMs, progressMessage, id = `${input.request.merge_request_id}:${token}:${args[0]}.${args[1]}`) => {
|
|
134
|
+
const adapterCall = async (args, timeoutMs, progressMessage, id = `${input.request.merge_request_id}:${token}:${args[0]}.${args[1]}`) => {
|
|
135
|
+
await lease.ensureConfirmed(() => assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation));
|
|
136
|
+
try {
|
|
137
|
+
return await runHarnessAdapter({ executable, args, harness: runtimeHarness(input.profile), env: runtimeContext.env, runtimeOwner, operationId: id, signal: ownership.signal, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), ...(progressMessage ? { progressMessage } : {}), onEvidence: (value) => event(input.journal, { type: "adapter_call", ...value }), onNativeOutcome: value => {
|
|
138
|
+
if (args[0] === "session" && args[1] === "create" && adapterSessionId(value)) {
|
|
139
|
+
Object.assign(retained, { session_id: adapterSessionId(value), create: value, settlement_pending: true });
|
|
140
|
+
retain();
|
|
141
|
+
}
|
|
142
|
+
} });
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (ownership.signal.aborted)
|
|
146
|
+
throw ownership.signal.reason;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
116
150
|
await adapterCall(["daemon", "start", ...common]);
|
|
117
|
-
const created = await adapterCall(["session", "create", ...common]
|
|
151
|
+
const created = await adapterCall(["session", "create", ...common]);
|
|
118
152
|
const sessionId = adapterSessionId(created);
|
|
119
153
|
if (!sessionId)
|
|
120
154
|
throw new AppError("merge_adapter_invalid", "Harness adapter did not return a Session id", 1, { harness: input.profile.harness, response: created });
|
|
121
|
-
Object.assign(retained, { session_id: sessionId, create: created });
|
|
155
|
+
Object.assign(retained, { session_id: sessionId, create: created, settlement_pending: false });
|
|
122
156
|
retain();
|
|
123
157
|
runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(input.profile), stateDir, sessionId, projectRoot: run.project_root, runId: input.request.run_id, generation });
|
|
124
158
|
const stageCommand = managedLifecycleCommand(runtimeContext, `${flowCommand(runtimeContext)} stage start ${input.request.run_id} --stage merge --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`);
|
|
125
159
|
fs.writeFileSync(promptFile, `Start the assigned MERGE stage now. Your first tool call must be this exact standalone command:\n\n${stageCommand}\n\nTrust and follow the complete stage packet it returns. Continue through merge apply and stage finish. Stop only when the stage reaches a terminal result or explicitly requests user/operator action.\n\n${stageLifecycleInstruction}\n`);
|
|
126
160
|
let prompted;
|
|
127
|
-
|
|
161
|
+
retained.progress_policy = "native-inactivity@1";
|
|
162
|
+
retain();
|
|
128
163
|
try {
|
|
129
|
-
prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common],
|
|
164
|
+
prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null, `MERGE ${input.request.merge_request_id} Session ${sessionId} is still running`);
|
|
130
165
|
}
|
|
131
166
|
catch (error) {
|
|
132
167
|
if (runtimeHarness(input.profile) !== "codex-desktop")
|
|
133
168
|
throw error;
|
|
134
169
|
const capacityIntents = (retained.capacity_intents ?? []);
|
|
170
|
+
const capacityFailures = (retained.capacity_failures ?? []);
|
|
135
171
|
const resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id), allowSettledDescendants: true,
|
|
136
|
-
|
|
172
|
+
signal: ownership.signal,
|
|
137
173
|
continuationText: `Continue only MERGE ${input.request.merge_request_id}, Work ${input.request.executor_work_id}, RUN ${input.request.run_id} in this same Session and target ${input.request.target_workspace}. First inspect current durable Stage/Work/MERGE receipts. Never repeat Stage start, accepted answer, completed apply or checks. Follow only the current exact issued successor and stop at the original boundary.`,
|
|
138
174
|
retainedIntents: () => capacityIntents,
|
|
175
|
+
retainedFailures: () => capacityFailures,
|
|
176
|
+
retainFailure: (_id, refusal) => { capacityFailures.push(refusal); retained.capacity_failures = capacityFailures; retain(); },
|
|
139
177
|
inspectOperation: id => adapterCall(["daemon", "operation", "--state-dir", stateDir, "--operation-id", id, "--json"], 30_000, undefined, `${id}:capacity-observe`),
|
|
140
178
|
inspectSession: turnId => adapterCall(["session", "inspect", "--session-id", sessionId, ...(turnId ? ["--turn-id", turnId] : []), ...common], 30_000, undefined, `${input.request.merge_request_id}:${token}:capacity-inspect`),
|
|
141
179
|
assertCurrent: () => {
|
|
@@ -163,7 +201,7 @@ async function dispatch(context, input) {
|
|
|
163
201
|
}
|
|
164
202
|
else
|
|
165
203
|
fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
|
|
166
|
-
return adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common],
|
|
204
|
+
return adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], null, `MERGE ${input.request.merge_request_id} Session ${sessionId} capacity continuation ${ordinal}`, id);
|
|
167
205
|
} });
|
|
168
206
|
if (!resumed)
|
|
169
207
|
throw error;
|
|
@@ -175,7 +213,7 @@ async function dispatch(context, input) {
|
|
|
175
213
|
const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
|
|
176
214
|
event(input.journal, { type: "adapter_settled", request_id: input.request.merge_request_id, session_id: sessionId, request_status: request?.status ?? null, at: context.now() });
|
|
177
215
|
if (request?.status === "dispatching")
|
|
178
|
-
context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "adapter_did_not_start_stage", response: prompted }), context.now(), input.request.merge_request_id]);
|
|
216
|
+
context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'dispatching' AND dispatch_owner = ? AND dispatch_lease_token = ? AND ? = COALESCE((SELECT MAX(generation) FROM run_recovery_guards WHERE project_id = merge_requests.project_id AND run_id = merge_requests.run_id), 0)", [JSON.stringify({ code: "adapter_did_not_start_stage", response: prompted }), context.now(), input.request.merge_request_id, input.serverId, token, generation]);
|
|
179
217
|
}
|
|
180
218
|
catch (error) {
|
|
181
219
|
if (error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code)) {
|
|
@@ -183,24 +221,29 @@ async function dispatch(context, input) {
|
|
|
183
221
|
throw error;
|
|
184
222
|
}
|
|
185
223
|
const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
|
|
186
|
-
if (request?.status === "dispatching")
|
|
187
|
-
context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "adapter_launch_failed", error: errorRecord(error) }), context.now(), input.request.merge_request_id]);
|
|
188
|
-
else if (request?.status === "active")
|
|
189
|
-
context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "adapter_lost_after_stage_start", error: errorRecord(error) }), context.now(), input.request.merge_request_id]);
|
|
224
|
+
if (request?.status === "dispatching" || request?.status === "active")
|
|
225
|
+
context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = ? AND dispatch_owner = ? AND dispatch_lease_token = ? AND ? = COALESCE((SELECT MAX(generation) FROM run_recovery_guards WHERE project_id = merge_requests.project_id AND run_id = merge_requests.run_id), 0)", [JSON.stringify({ code: request.status === "dispatching" ? "adapter_launch_failed" : "adapter_lost_after_stage_start", error: errorRecord(error) }), context.now(), input.request.merge_request_id, request.status, input.serverId, token, generation]);
|
|
190
226
|
throw error;
|
|
191
227
|
}
|
|
192
228
|
finally {
|
|
229
|
+
lease.close();
|
|
193
230
|
clearInterval(leaseHeartbeat);
|
|
194
231
|
}
|
|
195
232
|
}
|
|
196
233
|
function nextServerRequests(context, limit) { const selected = new Set(); return context.db.all("SELECT merge_request_id, project_id, run_id, executor_work_id, target_workspace, execution_route, status, created_at FROM merge_requests WHERE status = 'queued' AND execution_route = 'server' ORDER BY created_at, merge_request_id").filter((request) => { if (guarded(context, request) || selected.has(request.project_id) || context.db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?)) LIMIT 1", [request.project_id, request.created_at, request.created_at, request.merge_request_id]))
|
|
197
234
|
return false; selected.add(request.project_id); return true; }).slice(0, limit); }
|
|
198
|
-
function reconcileExpiredDispatches(context) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
235
|
+
export function reconcileExpiredDispatches(context) {
|
|
236
|
+
context.db.writeTransaction(() => {
|
|
237
|
+
const now = context.now();
|
|
238
|
+
const expired = context.db.all("SELECT merge_request_id, executor_work_id, project_id, run_id, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at FROM merge_requests WHERE status = 'dispatching' AND dispatch_lease_expires_at < ?", [now]);
|
|
239
|
+
for (const request of expired) {
|
|
240
|
+
if (guarded(context, request))
|
|
241
|
+
continue;
|
|
242
|
+
const work = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
243
|
+
context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'dispatching' AND dispatch_owner IS ? AND dispatch_lease_token IS ? AND dispatch_lease_expires_at = ? AND dispatch_lease_expires_at < ?", [JSON.stringify({ code: "expired_dispatch_outcome_unknown", work_status: work?.status ?? null }), now, request.merge_request_id, request.dispatch_owner, request.dispatch_lease_token, request.dispatch_lease_expires_at, now]);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
204
247
|
function loadProfile(context, id) { return loadAgentProfile(context.ddFlowHome, id); }
|
|
205
248
|
function event(file, value) { fs.appendFileSync(file, `${JSON.stringify(value)}\n`); }
|
|
206
249
|
function guarded(context, request) { const guard = recoveryGuard(context, request.project_id, request.run_id); return Boolean(guard && ["draining", "sealed", "resuming"].includes(guard.status)); }
|
|
@@ -210,6 +253,6 @@ function processAlive(pid) { if (!pid)
|
|
|
210
253
|
process.kill(pid, 0);
|
|
211
254
|
return true;
|
|
212
255
|
}
|
|
213
|
-
catch {
|
|
214
|
-
return
|
|
256
|
+
catch (error) {
|
|
257
|
+
return error.code !== "ESRCH";
|
|
215
258
|
} }
|