@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
package/dist/cli/run-cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { requiredOption, optionalOption, optionalNumber, optionalPositiveNumber } from "./argument-values.js";
|
|
2
3
|
import { resolveCommandPaths, resolveCommandProjectRoot } from "../services/command-context.js";
|
|
3
|
-
import { assertAuxiliaryCommandScope } from "../services/lifecycle-invocations.js";
|
|
4
|
+
import { assertAuxiliaryCommandScope, cliManagedInvocationScope, manualRecoveryInvocationScope, assertManualRecoveryInvocationTarget } from "../services/lifecycle-invocations.js";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { quote } from "shell-quote";
|
|
@@ -9,11 +10,12 @@ import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
|
|
|
9
10
|
import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, bindLifecyclePublishedPacket, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleDiagnostic, observedLifecycleInvocation, resolveObservedLifecycleArgs, retainObservedLifecycleSemantics, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
|
|
10
11
|
import { createContext, createRouterContext } from "../runtime/context.js";
|
|
11
12
|
import { dispatchNativeHook, isNativeHookIngress } from "./native-hook-ingress.js";
|
|
13
|
+
import { isRuntimeMaintenance, runtimeMaintenanceDiagnostics, withMaintenanceDiagnosticScope } from "./runtime-maintenance-diagnostics.js";
|
|
12
14
|
import { migrateStoreWriter } from "../storage/writer-migration.js";
|
|
13
15
|
import { helpForArgs } from "./help.js";
|
|
14
|
-
import { AppError, isAppError } from "../shared/errors.js";
|
|
16
|
+
import { AppError, errorRecord, isAppError } from "../shared/errors.js";
|
|
15
17
|
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
16
|
-
import { writeJson } from "../shared/json.js";
|
|
18
|
+
import { writeJson, parseJsonObject } from "../shared/json.js";
|
|
17
19
|
import { archiveProject, prepareProjectArchive, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
|
|
18
20
|
import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, prepareProtocolRegistration, requireProtocol, syncProtocolFromRun, prepareProtocolTransition, transitionProtocol } from "../services/protocols.js";
|
|
19
21
|
import { getPlanStatus, preparePlanItemMutation, updatePlanItem } from "../services/plans.js";
|
|
@@ -41,7 +43,7 @@ import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage,
|
|
|
41
43
|
import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, prepareMigrationApply, verifyMigrationReport } from "../services/migrations.js";
|
|
42
44
|
import { publishProjectSummary } from "../services/project-summary.js";
|
|
43
45
|
import { requireProjectByRoot } from "../services/projects.js";
|
|
44
|
-
import { resolveProjectRoot } from "../storage/paths.js";
|
|
46
|
+
import { resolveProjectRoot, resolveDdFlowHome } from "../storage/paths.js";
|
|
45
47
|
import { doctorEngines, bindRunEngine, prepareRunEngineBind, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
|
|
46
48
|
import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
|
|
47
49
|
import { finishVnextSpecifyStage, getVnextWorkStatus, isVnextSpecifyFlow, isVnextSpecifyRun, launchVnextSpecify, prepareVnextSpecifyStart } from "../services/vnext-specify.js";
|
|
@@ -62,20 +64,17 @@ import { addWorkBatch, prepareWorkBatch, prepareWorkDeps, prepareWorkDelete, pre
|
|
|
62
64
|
import { launchExternalWork } from "../services/external-work-launch.js";
|
|
63
65
|
import { createEvalBootstrapSnapshot, prepareEvalBootstrapSnapshot, createEvalRunSnapshot, prepareEvalRunSnapshotCreation, prepareVnextSpecifyRun, prepareEvalRunRestore, prepareEvalBootstrapRestore, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
|
|
64
66
|
import { forkRunFromSnapshot, prepareRunFork } from "../services/run-fork.js";
|
|
65
|
-
import { acceptRunRecovery,
|
|
66
|
-
import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
|
|
67
|
+
import { acceptRunRecovery, beginRunRecovery, inspectRunRecovery, prepareRecoveryCapture, prepareRecoveryPayload, prepareRunRecoveryResume, recordRecoveryCapture, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
|
|
67
68
|
import { loadExternalStageContext, prepareExternalStageContext } from "../services/stage-context.js";
|
|
68
69
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
69
70
|
import { prepareCommandRoute } from "./command-inputs.js";
|
|
70
71
|
import { parseLifecycleCommand } from "../services/lifecycle-command.js";
|
|
71
|
-
import {
|
|
72
|
-
import { fenceRuntimeBudget, runtimeBudgetStatus,
|
|
72
|
+
import { dispatchRuntimeProcess, prepareRuntimeProcess } from "./runtime-process.js";
|
|
73
|
+
import { fenceRuntimeBudget, runtimeBudgetStatus, prepareRuntimeScopeControl, prepareRuntimeScopeFence } from "../services/runtime-budget.js";
|
|
73
74
|
import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
|
|
74
75
|
import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScopeReconciliation, prepareRuntimeScopeServing, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
|
|
75
76
|
import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
|
|
76
77
|
import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
|
|
77
|
-
import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
|
|
78
|
-
import { assertCapacityOwnerAdmission } from "../services/codex-capacity.js";
|
|
79
78
|
import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
|
|
80
79
|
import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
|
|
81
80
|
import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
|
|
@@ -86,6 +85,9 @@ const defaultIo = {
|
|
|
86
85
|
};
|
|
87
86
|
const flagValue = "__dd_flow_flag__";
|
|
88
87
|
export async function runCli(args, io = defaultIo, env = process.env) {
|
|
88
|
+
return withMaintenanceDiagnosticScope(() => runCliRequest(args, io, env));
|
|
89
|
+
}
|
|
90
|
+
async function runCliRequest(args, io, env) {
|
|
89
91
|
let output;
|
|
90
92
|
try {
|
|
91
93
|
output = parseOutputOptions(args);
|
|
@@ -94,7 +96,10 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
94
96
|
const rejection = isAppError(error) ? error : new AppError("usage", error instanceof Error ? error.message : String(error), 2);
|
|
95
97
|
Object.assign(rejection.details, { phase: "prepare", effect: "no_effect", recoverable: true, ...rejection.details });
|
|
96
98
|
try {
|
|
97
|
-
const command = quote(["dd-flow", ...args])
|
|
99
|
+
const command = quote(["dd-flow", ...args]);
|
|
100
|
+
const analysis = parseLifecycleCommand(command);
|
|
101
|
+
const event = analysis.kind !== "none" || analysis.auxiliary
|
|
102
|
+
? observedLifecycleNativeEvent(createRouterContext(env), command, env.DD_FLOW_DAEMON_ID) : null;
|
|
98
103
|
if (event) {
|
|
99
104
|
const context = createContext({ ...env }, "hook");
|
|
100
105
|
try {
|
|
@@ -108,7 +113,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
108
113
|
catch (persistence) {
|
|
109
114
|
rejection.details.lifecycle_diagnostic_error = String(persistence);
|
|
110
115
|
}
|
|
111
|
-
writeJson(io.stderr, { ok: false, error:
|
|
116
|
+
writeJson(io.stderr, { ok: false, error: errorRecord(rejection) });
|
|
112
117
|
return rejection.exitCode;
|
|
113
118
|
}
|
|
114
119
|
const progress = progressForCommand(output.args);
|
|
@@ -116,6 +121,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
116
121
|
let invocationSettlement;
|
|
117
122
|
let settleHook;
|
|
118
123
|
let diagnosticHome;
|
|
124
|
+
let maintenanceDiagnostics;
|
|
119
125
|
try {
|
|
120
126
|
if (output.args.length === 1 && output.args[0] === "--version") {
|
|
121
127
|
const payload = getCliVersionReport();
|
|
@@ -135,6 +141,36 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
135
141
|
writeJson(io.stdout, await dispatchNativeHook(output.args, await readStdin(io.stdin), env));
|
|
136
142
|
return 0;
|
|
137
143
|
}
|
|
144
|
+
if (output.args.slice(0, 3).join(" ") === "codex hook handle") {
|
|
145
|
+
const parsed = prepareCommandRoute(output.args).parsed;
|
|
146
|
+
const stdin = await readStdin(io.stdin);
|
|
147
|
+
const payload = parseJsonObject(stdin || "{}", "codex hook stdin");
|
|
148
|
+
const event = requiredOption(parsed, "event");
|
|
149
|
+
const isolatedHome = event === "PreToolUse" ? isolatedDdFlowHomeFromHookStdin(stdin) : null;
|
|
150
|
+
const hookEnv = isolatedHome ? { ...env, DD_FLOW_HOME: isolatedHome } : env;
|
|
151
|
+
const proof = await verifyCodexHookDelivery(hookEnv, payload);
|
|
152
|
+
let opened;
|
|
153
|
+
const mode = databaseModeForCommand(output.args, classifyCliOperation(output.args, hookEnv));
|
|
154
|
+
const context = {
|
|
155
|
+
env: hookEnv, ddFlowHome: resolveDdFlowHome(hookEnv), now: () => new Date().toISOString(),
|
|
156
|
+
get db() { return (opened ??= createContext(hookEnv, mode)).db; }
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
// Retired events retain the explicit startup/migration path. PreToolUse
|
|
160
|
+
// only opens its selected prepared store when the handler needs it.
|
|
161
|
+
if (mode !== "hook")
|
|
162
|
+
opened = createContext(hookEnv, mode);
|
|
163
|
+
if (proof)
|
|
164
|
+
context.codexHookProof = proof;
|
|
165
|
+
const result = handleCodexHook(context, { event, projectRoot: optionalOption(parsed, "project-root"), stdin });
|
|
166
|
+
writeJson(io.stdout, result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result
|
|
167
|
+
? { hookSpecificOutput: result.hookSpecificOutput } : {});
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
opened?.db.close?.();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
138
174
|
if (output.args[0] === "run" && ["drive", "control"].includes(output.args[1] ?? "")) {
|
|
139
175
|
const controlArgs = parseArgs(output.args.slice(2));
|
|
140
176
|
requiredOption(controlArgs, "run");
|
|
@@ -151,22 +187,45 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
151
187
|
required_marker: "DD_FLOW_COMPATIBILITY_MODE=mb-upgrade"
|
|
152
188
|
});
|
|
153
189
|
}
|
|
154
|
-
|
|
190
|
+
// Registration may bootstrap the resource registry, but never the unrelated
|
|
191
|
+
// RUN store. Its caller's absolute deadline also bounds that first open.
|
|
192
|
+
const maintenanceOptions = isRuntimeMaintenance(output.args) ? { resourceDatabaseMode: output.args[2] === "register" ? "initialize" : output.args[2] === "status" ? "read_existing" : "prepared_existing", ...(env.DD_FLOW_HOOK_DEADLINE_AT ? { databaseDeadlineAt: Number(env.DD_FLOW_HOOK_DEADLINE_AT) } : {}) } : {};
|
|
193
|
+
// Keep ordinary CLI JSON/stderr compatibility; bounded maintenance callers
|
|
194
|
+
// provide this request's deadline and consume its structured phase evidence.
|
|
195
|
+
if (maintenanceOptions.databaseDeadlineAt !== undefined)
|
|
196
|
+
maintenanceDiagnostics = runtimeMaintenanceDiagnostics(output.args, maintenanceOptions.databaseDeadlineAt, line => io.stderr.write(line));
|
|
197
|
+
const routerContext = createRouterContext(env, maintenanceOptions.databaseDeadlineAt);
|
|
155
198
|
const publicCommand = quote(["dd-flow", ...output.args]);
|
|
156
199
|
const nativeCommand = quote(["dd-flow", ...args]);
|
|
157
200
|
const lifecycleCommand = parseLifecycleCommand(publicCommand);
|
|
158
201
|
if (lifecycleCommand.kind === "standalone")
|
|
159
202
|
validateLifecyclePathAliases(output.args, lifecycleCommand.invocation.operation);
|
|
160
|
-
const
|
|
203
|
+
const managedScope = lifecycleCommand.kind === "standalone" && env.DD_FLOW_DAEMON_ID ? cliManagedInvocationScope({ ...routerContext, env }) : null;
|
|
161
204
|
const nativeEvent = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
|
|
162
205
|
? observedLifecycleNativeEvent(routerContext, nativeCommand, env.DD_FLOW_DAEMON_ID) : null;
|
|
206
|
+
const nativeEventKey = nativeEvent && routerContext.db.get("SELECT event_key FROM hook_events WHERE project_id = ? AND id = ?", [nativeEvent.project_id, nativeEvent.id])?.event_key;
|
|
207
|
+
const observedInvocation = managedScope ? observedLifecycleInvocation(routerContext, publicCommand, managedScope, nativeEventKey ? { eventKey: nativeEventKey } : undefined) : null;
|
|
208
|
+
if (managedScope && suppliedInvocationId
|
|
209
|
+
&& routerContext.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'lifecycle_invocations'")
|
|
210
|
+
&& routerContext.db.get("SELECT id FROM lifecycle_invocations WHERE id = ?", [suppliedInvocationId])) {
|
|
211
|
+
const suppliedScope = lifecycleInvocationScope({ ...routerContext, env }, suppliedInvocationId);
|
|
212
|
+
if (["projectRoot", "daemonId", "rootSessionId", "runId", "generation"].some(key => suppliedScope[key] !== managedScope[key])) {
|
|
213
|
+
throw new AppError("invocation_scope_mismatch", "Explicit lifecycle assignment differs from the current physical native owner", 1, { effect: "no_effect", recoverable: false });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
163
216
|
const diagnostic = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
|
|
164
217
|
? observedLifecycleDiagnostic(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID, observedInvocation?.id) : null;
|
|
165
218
|
if (diagnostic && (!nativeEvent || Number(diagnostic.hook_event_id) > nativeEvent.id)) {
|
|
166
219
|
throw new AppError("lifecycle_shell_syntax_invalid", `Native observer rejected the shell form: ${String(diagnostic.reason ?? "unsupported shell composition")}. Write structured input to a file and run the returned dd-flow command separately.`, 2, { ...diagnostic, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
167
220
|
}
|
|
221
|
+
let manualRecoveryHookEventKey;
|
|
168
222
|
if (!observedInvocation && !suppliedInvocationId && env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone") {
|
|
169
|
-
|
|
223
|
+
const receipt = nativeEvent && routerContext.db.get("SELECT event_key,harness,provider_session_id,parent_session_id,daemon_id,status FROM hook_events WHERE id = ? AND project_id = ?", [nativeEvent.id, nativeEvent.project_id]);
|
|
224
|
+
const manual = managedScope && receipt?.status === "observed" ? manualRecoveryInvocationScope({ ...routerContext, env }, managedScope.projectRoot, { daemonId: receipt.daemon_id, rootSessionId: receipt.provider_session_id, sessionId: receipt.provider_session_id, parentSessionId: receipt.parent_session_id, toolCallId: String(nativeEvent.id) }, receipt.harness) : null;
|
|
225
|
+
if (!manual || JSON.stringify(manual) !== JSON.stringify(managedScope))
|
|
226
|
+
throw new AppError(nativeEvent ? "invocation_assignment_missing" : "invocation_receipt_missing", nativeEvent ? "Native call was observed, but its arguments do not match an issued lifecycle assignment" : "Managed lifecycle command has no committed native receipt", 1, { effect: "no_effect", recoverable: false });
|
|
227
|
+
assertManualRecoveryInvocationTarget({ ...routerContext, env }, manual, publicCommand);
|
|
228
|
+
manualRecoveryHookEventKey = receipt.event_key;
|
|
170
229
|
}
|
|
171
230
|
const invocationId = observedInvocation?.id ?? suppliedInvocationId;
|
|
172
231
|
let boundInvocationArgs;
|
|
@@ -207,7 +266,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
207
266
|
}
|
|
208
267
|
// Route by retained authority, so a mistyped project path reaches the CLI
|
|
209
268
|
// validator instead of selecting a different engine or failing preflight.
|
|
210
|
-
const scopeProjectRoot = invocationScope?.projectRoot ?? resolveCommandProjectRoot(routerContext, output.args, resolveProjectRootBeforeRouting(output.args, env));
|
|
269
|
+
const scopeProjectRoot = invocationScope?.projectRoot ?? resolveCommandProjectRoot(routerContext, output.args, resolveProjectRootBeforeRouting(output.args, env, maintenanceOptions.databaseDeadlineAt));
|
|
211
270
|
assertAuxiliaryCommandScope({ ...routerContext, env }, publicCommand, scopeProjectRoot);
|
|
212
271
|
invocationArgs = resolveCommandPaths(routerContext, invocationArgs, scopeProjectRoot);
|
|
213
272
|
const routedArgs = output.responseFile ? [...invocationArgs, "--response-file", output.responseFile] : invocationArgs;
|
|
@@ -223,7 +282,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
223
282
|
let preparedInput;
|
|
224
283
|
let preparedRoute;
|
|
225
284
|
if (!invocationId) {
|
|
226
|
-
const preparationContext = createContext({ ...env }, "read_existing");
|
|
285
|
+
const preparationContext = createContext({ ...env }, "read_existing", undefined, maintenanceOptions);
|
|
227
286
|
try {
|
|
228
287
|
preparedInput = await prepareCliInput(invocationArgs, preparationContext, io, scopeProjectRoot, output.responseFile);
|
|
229
288
|
}
|
|
@@ -232,8 +291,8 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
232
291
|
}
|
|
233
292
|
}
|
|
234
293
|
const dispatchContext = routed?.inProcess
|
|
235
|
-
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(invocationArgs, classification))
|
|
236
|
-
: createContext({ ...env }, databaseModeForCommand(invocationArgs, classification));
|
|
294
|
+
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(invocationArgs, classification), undefined, maintenanceOptions)
|
|
295
|
+
: createContext({ ...env }, databaseModeForCommand(invocationArgs, classification), undefined, maintenanceOptions);
|
|
237
296
|
diagnosticHome = dispatchContext.ddFlowHome;
|
|
238
297
|
dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
|
|
239
298
|
let dispatchArgs = invocationArgs;
|
|
@@ -271,6 +330,13 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
271
330
|
dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
|
|
272
331
|
}
|
|
273
332
|
}
|
|
333
|
+
if (!invocationId && manualRecoveryHookEventKey) {
|
|
334
|
+
// Keep the physically scoped receipt selected by admission; a service must
|
|
335
|
+
// not rediscover a different daemon's matching command event.
|
|
336
|
+
if (!invocationArgs.includes("--hook-event-id"))
|
|
337
|
+
dispatchArgs = [...invocationArgs, "--hook-event-id", manualRecoveryHookEventKey];
|
|
338
|
+
preparedInput?.route?.parsed.options.set("hook-event-id", [manualRecoveryHookEventKey]);
|
|
339
|
+
}
|
|
274
340
|
if (retainedOutcome?.error) {
|
|
275
341
|
const error = retainedOutcome.error;
|
|
276
342
|
throw new AppError(error.code, error.message, error.exitCode, error.details);
|
|
@@ -313,6 +379,9 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
313
379
|
emitHumanProgress(io, output, progress, "done");
|
|
314
380
|
const payload = attachProgress(resultWithWarnings, progress, output);
|
|
315
381
|
const visible = writeResponseFile(payload, output);
|
|
382
|
+
const maintenanceReceipt = payload;
|
|
383
|
+
const maintenanceExpiry = maintenanceReceipt?.lease_expires_at ?? maintenanceReceipt?.process?.lease_expires_at;
|
|
384
|
+
maintenanceDiagnostics?.phase("ack", { ...(typeof maintenanceExpiry === "string" && Number.isFinite(Date.parse(maintenanceExpiry)) ? { lease_expires_at: maintenanceExpiry } : {}) });
|
|
316
385
|
if (output.json)
|
|
317
386
|
writeJson(io.stdout, visible);
|
|
318
387
|
else
|
|
@@ -320,19 +389,24 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
320
389
|
return completedCommandExitCode(payload);
|
|
321
390
|
}
|
|
322
391
|
catch (caught) {
|
|
323
|
-
const
|
|
392
|
+
const record = errorRecord(caught);
|
|
393
|
+
maintenanceDiagnostics?.failure(caught);
|
|
394
|
+
const error = isAppError(caught) ? caught : Object.assign(new AppError(record.code, record.message, record.exitCode, { effect: "unknown", ...record.details, ...(caught?.errcode !== undefined ? { sqlite_extended_code: caught.errcode } : {}) }), { ...(record.cause ? { cause: record.cause } : {}), ...(record.cleanup_error ? { cleanup_error: record.cleanup_error } : {}), ...(record.retryable !== undefined ? { retryable: record.retryable } : {}) });
|
|
324
395
|
if (!settleHook && !invocationSettlement) {
|
|
325
396
|
let earlyContext;
|
|
326
397
|
try {
|
|
327
398
|
const command = quote(["dd-flow", ...args]);
|
|
328
|
-
const
|
|
329
|
-
|
|
399
|
+
const analysis = parseLifecycleCommand(command);
|
|
400
|
+
// Maintenance failures must not start an unrelated, unbounded hook
|
|
401
|
+
// lookup after their own request deadline has already expired.
|
|
402
|
+
const router = analysis.kind !== "none" || analysis.auxiliary ? createRouterContext(env) : null;
|
|
403
|
+
const event = router ? observedLifecycleNativeEvent(router, command, env.DD_FLOW_DAEMON_ID) : null;
|
|
330
404
|
// A correctable prepare rejection precedes the terminal command's
|
|
331
405
|
// single-use claim. Correlate its unique hook by the same lifecycle
|
|
332
406
|
// match key used at dispatch, including compound provider tool shells;
|
|
333
407
|
// otherwise a repaired retry sees two fresh, unclaimed events.
|
|
334
|
-
if (event || (error.details.phase === "prepare" && error.details.effect === "no_effect"
|
|
335
|
-
&& fs.existsSync(path.join(router.ddFlowHome, "db.sqlite")))) {
|
|
408
|
+
if (router && (event || (error.details.phase === "prepare" && error.details.effect === "no_effect"
|
|
409
|
+
&& fs.existsSync(path.join(router.ddFlowHome, "db.sqlite"))))) {
|
|
336
410
|
earlyContext = createContext({ ...env }, "hook");
|
|
337
411
|
observeLifecycleCommand(earlyContext, command, event ?? undefined)({ error });
|
|
338
412
|
}
|
|
@@ -360,8 +434,13 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
360
434
|
clearInterval(heartbeat);
|
|
361
435
|
if (invocationSettlement && error.details.effect !== "committed") {
|
|
362
436
|
try {
|
|
363
|
-
if (isAppError(error))
|
|
364
|
-
|
|
437
|
+
if (isAppError(error)) {
|
|
438
|
+
const settled = errorRecord(error);
|
|
439
|
+
settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, settled);
|
|
440
|
+
// Serialization owns a copy. Publish the committed settlement, not
|
|
441
|
+
// the original rejection which has no successor authority.
|
|
442
|
+
Object.assign(error.details, settled.details);
|
|
443
|
+
}
|
|
365
444
|
else
|
|
366
445
|
settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
|
|
367
446
|
}
|
|
@@ -377,9 +456,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
377
456
|
io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
|
|
378
457
|
}
|
|
379
458
|
}
|
|
380
|
-
const payload =
|
|
381
|
-
? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
|
|
382
|
-
: { ok: false, error: { code: "unexpected", message: String(error) } };
|
|
459
|
+
const payload = { ok: false, error: errorRecord(error) };
|
|
383
460
|
progress.splice(0, progress.length, ...progress.filter(event => event.phase !== "done"));
|
|
384
461
|
if (progress.length)
|
|
385
462
|
progress.push({ phase: "failed", message: payload.error.message, at: new Date().toISOString() });
|
|
@@ -403,14 +480,23 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
403
480
|
io.stderr.write(renderHumanError(payload));
|
|
404
481
|
return isAppError(error) ? error.exitCode : 1;
|
|
405
482
|
}
|
|
483
|
+
finally {
|
|
484
|
+
maintenanceDiagnostics?.close();
|
|
485
|
+
}
|
|
406
486
|
}
|
|
407
|
-
function resolveProjectRootBeforeRouting(args, env) {
|
|
487
|
+
function resolveProjectRootBeforeRouting(args, env, deadlineAt) {
|
|
408
488
|
const explicit = resolveExplicitProjectRoot(args);
|
|
409
489
|
if (explicit)
|
|
410
490
|
return explicit;
|
|
411
491
|
try {
|
|
412
492
|
// Router identity lookup is read-only; it must not initialize or migrate runtime storage.
|
|
413
|
-
|
|
493
|
+
const context = createContext(env, "read_existing", undefined, deadlineAt === undefined ? {} : { databaseDeadlineAt: deadlineAt });
|
|
494
|
+
try {
|
|
495
|
+
return resolveOperationProjectRoot(context, args);
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
context.db.close?.();
|
|
499
|
+
}
|
|
414
500
|
}
|
|
415
501
|
catch {
|
|
416
502
|
return null;
|
|
@@ -419,6 +505,8 @@ function resolveProjectRootBeforeRouting(args, env) {
|
|
|
419
505
|
function databaseModeForCommand(args, classification) {
|
|
420
506
|
if (isNativeHookIngress(args))
|
|
421
507
|
return "hook";
|
|
508
|
+
if (isRuntimeMaintenance(args))
|
|
509
|
+
return "read_existing";
|
|
422
510
|
const [family, command] = args;
|
|
423
511
|
// Managed attempts are issued only from a prepared store. Opening that
|
|
424
512
|
// existing writer must not run migrations before their input is admitted.
|
|
@@ -1005,22 +1093,8 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
|
|
|
1005
1093
|
return prepareError(error, { parameter: "scope-id/request-id/manifest-json" });
|
|
1006
1094
|
}
|
|
1007
1095
|
}
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
}
|
|
1011
|
-
if (route.key === "runtime process heartbeat")
|
|
1012
|
-
prepareManagedProcessHeartbeat(context, runtimeProcessHeartbeatInput(parsed));
|
|
1013
|
-
if (route.key === "runtime process finish")
|
|
1014
|
-
prepareManagedProcessFinish(context, runtimeProcessFinishInput(parsed));
|
|
1015
|
-
if (route.key === "runtime process confirm")
|
|
1016
|
-
prepareManagedProcessConfirmation(context, runtimeProcessConfirmationInput(parsed));
|
|
1017
|
-
if (route.key === "runtime process stop")
|
|
1018
|
-
prepareManagedProcessStopOperation(context, runtimeProcessStopInput(parsed));
|
|
1019
|
-
if (route.key === "runtime process reconcile")
|
|
1020
|
-
prepareManagedProcessReconcile(requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms"));
|
|
1021
|
-
if (route.key === "runtime process release-turn") {
|
|
1022
|
-
prepared.providerTurnRelease = prepareProviderTurnRelease({ daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
|
|
1023
|
-
}
|
|
1096
|
+
if (family === "runtime" && command === "process")
|
|
1097
|
+
Object.assign(prepared, prepareRuntimeProcess(context, route.key, parsed));
|
|
1024
1098
|
if (route.key === "runtime process start") {
|
|
1025
1099
|
try {
|
|
1026
1100
|
prepareRuntimeService(context, runtimeServiceInput(parsed));
|
|
@@ -1990,7 +2064,8 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1990
2064
|
return dispatchCodexHome(context, parsed, prepared);
|
|
1991
2065
|
}
|
|
1992
2066
|
if (family === "codex" && command === "hook") {
|
|
1993
|
-
|
|
2067
|
+
// `handle` is owned by the pre-context ingress above, never generic dispatch.
|
|
2068
|
+
throw new AppError("usage", "Use: dd-flow codex hook handle --event EVENT", 2);
|
|
1994
2069
|
}
|
|
1995
2070
|
if (family === "zcode" && command === "hook") {
|
|
1996
2071
|
return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
|
|
@@ -2383,14 +2458,14 @@ function snapshotCreateInput(parsed) {
|
|
|
2383
2458
|
return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), output: requiredOption(parsed, "output"), candidate: parsed.options.has("candidate"), incomplete: parsed.options.has("incomplete"), ...(recoveryId !== undefined ? { recoveryId } : {}), ...(stageEntry !== undefined ? { stageEntry } : {}) };
|
|
2384
2459
|
}
|
|
2385
2460
|
function snapshotRestoreInput(parsed) {
|
|
2386
|
-
return { snapshot: requiredOption(parsed, "snapshot"), projectRoot: requiredOption(parsed, "project-root"), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}) };
|
|
2461
|
+
return { snapshot: requiredOption(parsed, "snapshot"), projectRoot: requiredOption(parsed, "project-root"), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), operationalDecisionFile: optionalOption(parsed, "operational-decision-file"), ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}) };
|
|
2387
2462
|
}
|
|
2388
2463
|
function runStartInput(parsed) {
|
|
2389
2464
|
const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
|
|
2390
2465
|
return { projectRoot: requiredOption(parsed, "project-root"), workspaceRoot: optionalOption(parsed, "workspace-root"), flowKind: requiredOption(parsed, "flow-kind"), subjectType: requiredOption(parsed, "subject-type"), subjectId: requiredOption(parsed, "subject-id"), slug: requiredOption(parsed, "slug"), nextAction: optionalOption(parsed, "next-action"), preset: optionalOption(parsed, "preset"), taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), ...(protocolOverrides !== undefined ? { protocolOverrides } : {}), runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag")) };
|
|
2391
2466
|
}
|
|
2392
2467
|
function runForkInput(parsed) {
|
|
2393
|
-
return { snapshot: requiredOption(parsed, "from"), output: requiredOption(parsed, "output"), engineVersion: requiredOption(parsed, "engine-version"), requestId: requiredOption(parsed, "request-id"), ...(optionalOption(parsed, "integrity-checksum") ? { integrityChecksum: optionalOption(parsed, "integrity-checksum") } : {}) };
|
|
2468
|
+
return { snapshot: requiredOption(parsed, "from"), output: requiredOption(parsed, "output"), engineVersion: requiredOption(parsed, "engine-version"), requestId: requiredOption(parsed, "request-id"), ...(optionalOption(parsed, "integrity-checksum") ? { integrityChecksum: optionalOption(parsed, "integrity-checksum") } : {}), ...(optionalOption(parsed, "operational-decision-file") ? { operationalDecisionFile: optionalOption(parsed, "operational-decision-file") } : {}) };
|
|
2394
2469
|
}
|
|
2395
2470
|
function recoveryCaptureInput(parsed) {
|
|
2396
2471
|
return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") };
|
|
@@ -2542,44 +2617,9 @@ function stageCompletionInput(parsed) {
|
|
|
2542
2617
|
function runtimeServiceInput(parsed) {
|
|
2543
2618
|
return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), command: requiredOption(parsed, "command"), ports: requiredOption(parsed, "ports").split(","), readyPort: requiredOption(parsed, "ready-port"), readyPath: optionalOption(parsed, "ready-path") ?? "/", timeoutMs: optionalPositiveNumber(parsed, "ready-timeout-ms", true) ?? 30_000 };
|
|
2544
2619
|
}
|
|
2545
|
-
function runtimeProcessStopInput(parsed) {
|
|
2546
|
-
const graceMs = optionalNumber(parsed, "grace-ms");
|
|
2547
|
-
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
|
|
2548
|
-
}
|
|
2549
2620
|
function runtimeScopeResumeInput(parsed) {
|
|
2550
2621
|
return { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") };
|
|
2551
2622
|
}
|
|
2552
|
-
function runtimeProcessRegistrationInput(context, parsed) {
|
|
2553
|
-
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
2554
|
-
const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
|
|
2555
|
-
const kind = requiredOption(parsed, "kind");
|
|
2556
|
-
const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
|
|
2557
|
-
const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
|
|
2558
|
-
const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
|
|
2559
|
-
return {
|
|
2560
|
-
kind, ownerId: requiredOption(parsed, "owner"), uniqueActiveOperation: kind === "eval-observer",
|
|
2561
|
-
...(kind === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
|
|
2562
|
-
ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), projectId, runId,
|
|
2563
|
-
workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
|
|
2564
|
-
operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
|
|
2565
|
-
stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs !== undefined ? { leaseMs } : {}),
|
|
2566
|
-
metadata: { ...(budget ? { budget, harness: optionalOption(parsed, "harness") ?? null } : {}), ...(projectId && runId ? { run_generation: guard?.generation ?? 0, run_admitted: Boolean(run) && !["draining", "sealed", "resuming"].includes(guard?.status ?? "") } : {}), ...(optionalOption(parsed, "dispatch-barrier") ? { dispatch_barrier: optionalOption(parsed, "dispatch-barrier") } : {}), ...(optionalOption(parsed, "role") ? { role: optionalOption(parsed, "role") } : {}), ...(optionalOption(parsed, "state-dir") ? { state_dir: optionalOption(parsed, "state-dir") } : {}), ...(optionalOption(parsed, "adapter-executable") ? { adapter_executable: optionalOption(parsed, "adapter-executable") } : {}) }
|
|
2567
|
-
};
|
|
2568
|
-
}
|
|
2569
|
-
function runtimeProcessConfirmationInput(parsed) {
|
|
2570
|
-
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true), processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
|
|
2571
|
-
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId !== undefined ? { processGroupId } : {}), ...(leaseMs !== undefined ? { leaseMs } : {}) };
|
|
2572
|
-
}
|
|
2573
|
-
function runtimeProcessHeartbeatInput(parsed) {
|
|
2574
|
-
const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
|
|
2575
|
-
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs !== undefined ? { leaseMs } : {}) };
|
|
2576
|
-
}
|
|
2577
|
-
function runtimeProcessFinishInput(parsed) {
|
|
2578
|
-
const state = requiredOption(parsed, "state");
|
|
2579
|
-
if (state !== "stopped" && state !== "failed")
|
|
2580
|
-
throw new AppError("validation", "--state must be stopped or failed", 2);
|
|
2581
|
-
return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state, reason: optionalOption(parsed, "reason") ?? null };
|
|
2582
|
-
}
|
|
2583
2623
|
async function dispatchRuntime(context, command, parsed, progress, prepared = {}) {
|
|
2584
2624
|
if (command === "scope") {
|
|
2585
2625
|
if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
|
|
@@ -2622,60 +2662,9 @@ async function dispatchRuntime(context, command, parsed, progress, prepared = {}
|
|
|
2622
2662
|
if (command !== "process")
|
|
2623
2663
|
throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
|
|
2624
2664
|
const action = requiredPosition(parsed, 0, "runtime process action");
|
|
2625
|
-
if (action === "check-admission")
|
|
2626
|
-
return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
|
|
2627
2665
|
if (action === "start")
|
|
2628
2666
|
return await startRuntimeService(context, { ...runtimeServiceInput(parsed), progress });
|
|
2629
|
-
|
|
2630
|
-
return { ok: true, process: await stopManagedProcess(context, runtimeProcessStopInput(parsed)) };
|
|
2631
|
-
if (action === "register") {
|
|
2632
|
-
const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
|
|
2633
|
-
return { ok: true, process };
|
|
2634
|
-
}
|
|
2635
|
-
if (action === "confirm")
|
|
2636
|
-
return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
|
|
2637
|
-
if (action === "heartbeat")
|
|
2638
|
-
return { ok: heartbeatManagedProcess(context, runtimeProcessHeartbeatInput(parsed)) };
|
|
2639
|
-
if (action === "admission") {
|
|
2640
|
-
const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
|
|
2641
|
-
if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || record.state !== "running" || !record.pid || !record.pid_started_at || !processIsAlive(record))
|
|
2642
|
-
throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
|
|
2643
|
-
const metadata = JSON.parse(record.metadata_json);
|
|
2644
|
-
if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
|
|
2645
|
-
throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
|
|
2646
|
-
if (record.project_id && record.run_id) {
|
|
2647
|
-
if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
2648
|
-
throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
|
|
2649
|
-
const recovering = recoveryRuntimeAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), daemonId: optionalOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
|
|
2650
|
-
if (!recovering) {
|
|
2651
|
-
if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
|
|
2652
|
-
throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
|
|
2653
|
-
assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
|
|
2654
|
-
}
|
|
2655
|
-
const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
|
|
2656
|
-
const retainedHitlPrompt = recovering && ["paused", "waiting_for_user"].includes(run?.status ?? "") && ["session.prompt", "session.resume"].includes(optionalOption(parsed, "operation") ?? "") && context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND b.daemon_id = ? AND b.provider_session_id = ?", [record.project_id, record.run_id, metadata.run_generation, optionalOption(parsed, "daemon-id"), optionalOption(parsed, "native-session-id")]);
|
|
2657
|
-
const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
|
|
2658
|
-
if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
|
|
2659
|
-
throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
|
|
2660
|
-
if (!recovering)
|
|
2661
|
-
assertCapacityOwnerAdmission(context, record, optionalOption(parsed, "operation-id"));
|
|
2662
|
-
}
|
|
2663
|
-
const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
|
|
2664
|
-
return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
|
|
2665
|
-
}
|
|
2666
|
-
if (action === "release-turn") {
|
|
2667
|
-
const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
|
|
2668
|
-
if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
2669
|
-
throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
|
|
2670
|
-
return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") }, prepared.providerTurnRelease);
|
|
2671
|
-
}
|
|
2672
|
-
if (action === "finish")
|
|
2673
|
-
return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
|
|
2674
|
-
if (action === "status")
|
|
2675
|
-
return { ok: true, processes: managedProcessStatus(context) };
|
|
2676
|
-
if (action === "reconcile")
|
|
2677
|
-
return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
|
|
2678
|
-
throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
|
|
2667
|
+
return await dispatchRuntimeProcess(context, action, parsed, prepared);
|
|
2679
2668
|
}
|
|
2680
2669
|
function dispatchStat(context, command, parsed) {
|
|
2681
2670
|
if (command === "usage") {
|
|
@@ -3048,26 +3037,6 @@ function dispatchCodexHome(context, parsed, prepared) {
|
|
|
3048
3037
|
}
|
|
3049
3038
|
throw new AppError("usage", `Unknown codex home action: ${action}`, 2);
|
|
3050
3039
|
}
|
|
3051
|
-
async function dispatchCodexHook(context, parsed, stdin) {
|
|
3052
|
-
const action = requiredPosition(parsed, 0, "codex hook action");
|
|
3053
|
-
if (action === "handle") {
|
|
3054
|
-
const isolatedHome = isolatedDdFlowHomeFromHookStdin(stdin);
|
|
3055
|
-
const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }, "hook") : context;
|
|
3056
|
-
const proof = await verifyCodexHookDelivery(hookContext.env, JSON.parse(stdin || "{}"));
|
|
3057
|
-
if (proof)
|
|
3058
|
-
hookContext.codexHookProof = proof;
|
|
3059
|
-
const result = handleCodexHook(hookContext, {
|
|
3060
|
-
event: requiredOption(parsed, "event"),
|
|
3061
|
-
projectRoot: optionalOption(parsed, "project-root"),
|
|
3062
|
-
stdin
|
|
3063
|
-
});
|
|
3064
|
-
if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
|
|
3065
|
-
return { hookSpecificOutput: result.hookSpecificOutput };
|
|
3066
|
-
}
|
|
3067
|
-
return {};
|
|
3068
|
-
}
|
|
3069
|
-
throw new AppError("usage", `Unknown codex hook action: ${action}`, 2);
|
|
3070
|
-
}
|
|
3071
3040
|
function dispatchZcodeEvent(context, parsed, stdin) {
|
|
3072
3041
|
const action = requiredPosition(parsed, 0, "zcode event action");
|
|
3073
3042
|
if (action === "handle") {
|
|
@@ -3214,25 +3183,9 @@ function requiredPosition(parsed, index, label) {
|
|
|
3214
3183
|
}
|
|
3215
3184
|
return value;
|
|
3216
3185
|
}
|
|
3217
|
-
function optionalOption(parsed, key) {
|
|
3218
|
-
const values = parsed.options.get(key);
|
|
3219
|
-
const value = values?.[values.length - 1];
|
|
3220
|
-
if (value === flagValue)
|
|
3221
|
-
throw new AppError("usage", `--${key} requires a value`, 2);
|
|
3222
|
-
if (value !== undefined && !value.trim())
|
|
3223
|
-
throw new AppError("usage", `--${key} requires a non-empty value`, 2);
|
|
3224
|
-
return value;
|
|
3225
|
-
}
|
|
3226
3186
|
function allOptions(parsed, key) {
|
|
3227
3187
|
return (parsed.options.get(key) ?? []).filter((value) => value !== flagValue);
|
|
3228
3188
|
}
|
|
3229
|
-
function requiredOption(parsed, key) {
|
|
3230
|
-
const value = optionalOption(parsed, key);
|
|
3231
|
-
if (!value) {
|
|
3232
|
-
throw new AppError("usage", `Missing required option: --${key}`, 2);
|
|
3233
|
-
}
|
|
3234
|
-
return value;
|
|
3235
|
-
}
|
|
3236
3189
|
function requiredScopeProjectRoot(root, operation) {
|
|
3237
3190
|
if (root)
|
|
3238
3191
|
return root;
|
|
@@ -3272,26 +3225,6 @@ function parseBoolean(value, key) {
|
|
|
3272
3225
|
function parseOptionalBoolean(value, key) {
|
|
3273
3226
|
return typeof value === "string" ? parseBoolean(value, key) : undefined;
|
|
3274
3227
|
}
|
|
3275
|
-
function optionalNumber(parsed, key) {
|
|
3276
|
-
const value = optionalOption(parsed, key);
|
|
3277
|
-
if (typeof value === "undefined") {
|
|
3278
|
-
return undefined;
|
|
3279
|
-
}
|
|
3280
|
-
const parsedNumber = Number(value);
|
|
3281
|
-
if (!Number.isFinite(parsedNumber)) {
|
|
3282
|
-
throw new AppError("validation", `--${key} must be a number`, 2);
|
|
3283
|
-
}
|
|
3284
|
-
return parsedNumber;
|
|
3285
|
-
}
|
|
3286
|
-
function optionalPositiveNumber(parsed, key, integer = false) {
|
|
3287
|
-
const value = optionalNumber(parsed, key);
|
|
3288
|
-
if (typeof value === "undefined")
|
|
3289
|
-
return undefined;
|
|
3290
|
-
if (value <= 0 || (integer && !Number.isInteger(value))) {
|
|
3291
|
-
throw new AppError("validation", `--${key} must be a positive${integer ? " integer" : ""} number`, 2);
|
|
3292
|
-
}
|
|
3293
|
-
return value;
|
|
3294
|
-
}
|
|
3295
3228
|
function requiredWorkerId(parsed) {
|
|
3296
3229
|
return requiredOption(parsed, "worker-id");
|
|
3297
3230
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { channel } from "node:diagnostics_channel";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
const requests = new AsyncLocalStorage();
|
|
4
|
+
export function isRuntimeMaintenance(args) {
|
|
5
|
+
return args[0] === "runtime" && args[1] === "process" && ["register", "heartbeat", "confirm", "admission", "finish", "check-admission", "release-turn", "status"].includes(args[2] ?? "");
|
|
6
|
+
}
|
|
7
|
+
export const withMaintenanceDiagnosticScope = (action) => requests.run(Symbol(), action);
|
|
8
|
+
/** Request-local, bounded evidence; never command bytes, paths or lease tokens. */
|
|
9
|
+
export function runtimeMaintenanceDiagnostics(args, deadline, write) {
|
|
10
|
+
const started = performance.now();
|
|
11
|
+
const request = requests.getStore();
|
|
12
|
+
const option = (name) => { const at = args.indexOf(`--${name}`); return at < 0 ? undefined : args[at + 1]; };
|
|
13
|
+
const safe = (value) => typeof value === "string" && /^[A-Za-z0-9:._-]{1,128}$/.test(value) ? value : undefined;
|
|
14
|
+
const identity = { action: safe(args[2]), process_id: safe(option("id")), operation_id: safe(option("operation-id")), native_dispatch_started: false };
|
|
15
|
+
let previous = "", count = 0, committed = false;
|
|
16
|
+
const phase = (name, details = {}) => {
|
|
17
|
+
const key = `${details.store_kind ?? ""}:${name}`;
|
|
18
|
+
if (key === previous || count >= 64)
|
|
19
|
+
return;
|
|
20
|
+
previous = key;
|
|
21
|
+
count++;
|
|
22
|
+
write(`${JSON.stringify({ kind: "process_maintenance_phase", ...identity, phase: name, elapsed_ms: Math.round(performance.now() - started), ...(deadline !== undefined ? { remaining_ms: Math.max(0, deadline - Date.now()) } : {}), ...details, ...(["ack", "failed"].includes(name) ? { maintenance_effect: committed ? "committed" : name === "ack" ? "no_mutation_observed" : "unknown" } : {}) })}\n`);
|
|
23
|
+
};
|
|
24
|
+
const timing = channel("dd-flow.sqlite.ingress");
|
|
25
|
+
const log = (message) => {
|
|
26
|
+
if (requests.getStore() !== request)
|
|
27
|
+
return;
|
|
28
|
+
const record = message;
|
|
29
|
+
if (record.phase === "committed" && record.store_kind === "resource")
|
|
30
|
+
committed = true;
|
|
31
|
+
phase(record.phase, { ...(record.store_kind ? { store_kind: record.store_kind } : {}) });
|
|
32
|
+
};
|
|
33
|
+
timing.subscribe(log);
|
|
34
|
+
phase("request", { maintenance_effect: "unknown" });
|
|
35
|
+
return { phase, failure: (error) => {
|
|
36
|
+
let current = error;
|
|
37
|
+
let sqlite;
|
|
38
|
+
for (let depth = 0; current && depth < 8; depth++) {
|
|
39
|
+
sqlite = current.errcode ?? current.sqlite_extended_code ?? current.details?.sqlite_extended_code;
|
|
40
|
+
if (Number.isSafeInteger(sqlite))
|
|
41
|
+
break;
|
|
42
|
+
current = (current.cause ?? current.details?.cause);
|
|
43
|
+
}
|
|
44
|
+
phase("failed", { ...(Number.isSafeInteger(sqlite) ? { sqlite_extended_code: sqlite } : {}) });
|
|
45
|
+
}, close: () => timing.unsubscribe(log) };
|
|
46
|
+
}
|