@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 +16 -0
- package/README.md +10 -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/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/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- 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 +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- 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/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- 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 +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- 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 +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- 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/vnext-code-review.js +10 -9
- 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 +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- 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 +34 -4
- 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
|
+
}
|
|
@@ -8,6 +8,13 @@ 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
|
}
|
|
@@ -433,7 +489,7 @@ export async function reservePorts(context, input) {
|
|
|
433
489
|
}
|
|
434
490
|
return { ports, release: () => releasePortClaims(db, claims) };
|
|
435
491
|
}
|
|
436
|
-
function registry(context) { return getResourceDatabase(resourceHome(context)); }
|
|
492
|
+
function registry(context) { return getResourceDatabase(resourceHome(context), context.resourceDatabaseMode, context.databaseDeadlineAt); }
|
|
437
493
|
function requireProcess(db, id) { const row = db.get("SELECT * FROM managed_processes WHERE id = ?", [id]); if (!row)
|
|
438
494
|
throw new AppError("not_found", `Managed process is missing: ${id}`, 1); return row; }
|
|
439
495
|
export function prepareManagedProcessLease(context, leaseMs) { return leaseExpiry(context.now(), leaseMs); }
|
|
@@ -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";
|
|
@@ -109,27 +110,58 @@ async function dispatch(context, input) {
|
|
|
109
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)
|
|
110
111
|
throw new AppError("capacity_continuation_blocked", "MERGE dispatch owner changed before durable receipt retention", 1); };
|
|
111
112
|
retain();
|
|
112
|
-
const
|
|
113
|
-
|
|
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);
|
|
114
132
|
leaseHeartbeat.unref();
|
|
115
133
|
try {
|
|
116
|
-
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
|
+
};
|
|
117
150
|
await adapterCall(["daemon", "start", ...common]);
|
|
118
|
-
const created = await adapterCall(["session", "create", ...common]
|
|
151
|
+
const created = await adapterCall(["session", "create", ...common]);
|
|
119
152
|
const sessionId = adapterSessionId(created);
|
|
120
153
|
if (!sessionId)
|
|
121
154
|
throw new AppError("merge_adapter_invalid", "Harness adapter did not return a Session id", 1, { harness: input.profile.harness, response: created });
|
|
122
|
-
Object.assign(retained, { session_id: sessionId, create: created });
|
|
155
|
+
Object.assign(retained, { session_id: sessionId, create: created, settlement_pending: false });
|
|
123
156
|
retain();
|
|
124
157
|
runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(input.profile), stateDir, sessionId, projectRoot: run.project_root, runId: input.request.run_id, generation });
|
|
125
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`);
|
|
126
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`);
|
|
127
160
|
let prompted;
|
|
128
|
-
|
|
129
|
-
retained.prompt_deadline = promptDeadline;
|
|
161
|
+
retained.progress_policy = "native-inactivity@1";
|
|
130
162
|
retain();
|
|
131
163
|
try {
|
|
132
|
-
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`);
|
|
133
165
|
}
|
|
134
166
|
catch (error) {
|
|
135
167
|
if (runtimeHarness(input.profile) !== "codex-desktop")
|
|
@@ -137,7 +169,7 @@ async function dispatch(context, input) {
|
|
|
137
169
|
const capacityIntents = (retained.capacity_intents ?? []);
|
|
138
170
|
const capacityFailures = (retained.capacity_failures ?? []);
|
|
139
171
|
const resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id), allowSettledDescendants: true,
|
|
140
|
-
|
|
172
|
+
signal: ownership.signal,
|
|
141
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.`,
|
|
142
174
|
retainedIntents: () => capacityIntents,
|
|
143
175
|
retainedFailures: () => capacityFailures,
|
|
@@ -169,7 +201,7 @@ async function dispatch(context, input) {
|
|
|
169
201
|
}
|
|
170
202
|
else
|
|
171
203
|
fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
|
|
172
|
-
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);
|
|
173
205
|
} });
|
|
174
206
|
if (!resumed)
|
|
175
207
|
throw error;
|
|
@@ -181,7 +213,7 @@ async function dispatch(context, input) {
|
|
|
181
213
|
const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
|
|
182
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() });
|
|
183
215
|
if (request?.status === "dispatching")
|
|
184
|
-
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]);
|
|
185
217
|
}
|
|
186
218
|
catch (error) {
|
|
187
219
|
if (error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code)) {
|
|
@@ -189,24 +221,29 @@ async function dispatch(context, input) {
|
|
|
189
221
|
throw error;
|
|
190
222
|
}
|
|
191
223
|
const request = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
|
|
192
|
-
if (request?.status === "dispatching")
|
|
193
|
-
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]);
|
|
194
|
-
else if (request?.status === "active")
|
|
195
|
-
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]);
|
|
196
226
|
throw error;
|
|
197
227
|
}
|
|
198
228
|
finally {
|
|
229
|
+
lease.close();
|
|
199
230
|
clearInterval(leaseHeartbeat);
|
|
200
231
|
}
|
|
201
232
|
}
|
|
202
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]))
|
|
203
234
|
return false; selected.add(request.project_id); return true; }).slice(0, limit); }
|
|
204
|
-
function reconcileExpiredDispatches(context) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
247
|
function loadProfile(context, id) { return loadAgentProfile(context.ddFlowHome, id); }
|
|
211
248
|
function event(file, value) { fs.appendFileSync(file, `${JSON.stringify(value)}\n`); }
|
|
212
249
|
function guarded(context, request) { const guard = recoveryGuard(context, request.project_id, request.run_id); return Boolean(guard && ["draining", "sealed", "resuming"].includes(guard.status)); }
|