@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.138
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 +4 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +2 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +101 -40
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +70 -9
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -292,6 +292,34 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
292
292
|
const nativeVersions = new Map();
|
|
293
293
|
for (const [directory, { state, records }] of daemonInventories) {
|
|
294
294
|
let failedStart = null;
|
|
295
|
+
// Explicit stop can reconcile a retired owner whose last bookkeeping ACK
|
|
296
|
+
// failed. This is physical retirement, not a fabricated clean shutdown.
|
|
297
|
+
if (control?.action === "stop" && state.shutdown_state === "cleanup_failed")
|
|
298
|
+
try {
|
|
299
|
+
const daemonId = state.daemon_id ?? state.config?.daemonId;
|
|
300
|
+
const daemon = records.filter(record => record.kind.endsWith("-daemon"));
|
|
301
|
+
const provider = records.filter(record => record.kind.endsWith("-provider"));
|
|
302
|
+
const operations = retainedDaemonOperations(directory);
|
|
303
|
+
const owner = state.config?.runtime_owner;
|
|
304
|
+
if (daemonId && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))
|
|
305
|
+
&& state.active_tree === false && !state.active_operation && state.shutdown?.daemon_id === daemonId
|
|
306
|
+
&& state.shutdown.phases?.tree === true && state.shutdown.phases.provider === true
|
|
307
|
+
&& daemon.length === 1 && provider.length === 1 && records.length === 2
|
|
308
|
+
&& daemon[0].id === state.resource_process?.id && daemon[0].lease_token === state.resource_process.lease_token && daemon[0].pid === state.pid
|
|
309
|
+
&& provider[0].id === state.provider_process?.id && provider[0].lease_token === state.provider_process.lease_token && provider[0].pid === state.provider_pid
|
|
310
|
+
&& JSON.parse(daemon[0].metadata_json).dispatch_barrier === "run-generation@1"
|
|
311
|
+
&& records.every(record => record.pid && record.pid_started_at && ["stopped", "failed"].includes(record.state) && record.finished_at && !processTreeIsAlive(record))
|
|
312
|
+
&& operations.length > 0 && operations.every(operation => ["completed", "failed", "interrupted"].includes(String(operation.state))
|
|
313
|
+
&& (operation.operation === "daemon.start" ? operation.daemon_id === null && operation.launch?.runtime_owner === JSON.stringify(owner)
|
|
314
|
+
: operation.daemon_id === daemonId && ["daemon.stop", "daemon.status", "session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.cancel", "session.inspect"].includes(String(operation.operation))
|
|
315
|
+
&& (operation.session_id == null || state.sessions.some(item => (typeof item === "string" ? item : adapterSessionId(item)) === operation.session_id))))) {
|
|
316
|
+
nativeVersions.set(directory, nativeInventoryVersion(directory, operations));
|
|
317
|
+
closedDaemons.set(directory, { source: "observed_owned_retirement", daemon_id: daemonId, stopped: true, settled: true,
|
|
318
|
+
session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)),
|
|
319
|
+
processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })) });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch { /* Invalid or unfinished evidence remains on the existing recovery path. */ }
|
|
295
323
|
if (!interrupt) {
|
|
296
324
|
try {
|
|
297
325
|
const nativeOperations = retainedDaemonOperations(directory);
|
|
@@ -309,16 +337,48 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
309
337
|
const saved = JSON.parse(operation.input_json);
|
|
310
338
|
return saved.owner === owner?.owner_id && saved.executable === directoryAdapters.get(directory) && saved.args?.[0] === "daemon" && saved.args?.[1] === "start" && saved.args.includes("--state-dir") && saved.args[saved.args.indexOf("--state-dir") + 1] === directory;
|
|
311
339
|
});
|
|
340
|
+
const cleanup = start?.error?.details;
|
|
341
|
+
const proof = cleanup?.startup_child_cleanup;
|
|
342
|
+
let ownedChildGone = false;
|
|
343
|
+
if (proof && !cleanup?.provider_cleanup_failed && !cleanup?.cleanup_failed && provider.length === 0 && !state.provider_process && state.provider_pid == null
|
|
344
|
+
&& daemon.length === 1 && daemon[0].pid === null && daemon[0].pid_started_at === null
|
|
345
|
+
&& proof.settled === true && proof.physical_exit === "ESRCH" && proof.daemon_id === state.daemon_id
|
|
346
|
+
&& proof.process_id === daemon[0].id
|
|
347
|
+
&& proof.pid === state.pid && Number.isSafeInteger(proof.pid) && Number(proof.pid) > 0
|
|
348
|
+
&& proof.pgid === (process.platform === "win32" ? null : proof.pid)
|
|
349
|
+
&& typeof proof.observed_at === "string" && Number.isFinite(Date.parse(proof.observed_at))
|
|
350
|
+
&& proof.owner_id === owner?.owner_id && proof.project_id === project.id && proof.run_id === input.runId
|
|
351
|
+
&& proof.operation_id === start?.operation_id && proof.operation_id === owner?.operation_id && proof.state_dir === directory) {
|
|
352
|
+
let leaderGone = false;
|
|
353
|
+
try {
|
|
354
|
+
process.kill(Number(proof.pid), 0);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
leaderGone = error.code === "ESRCH";
|
|
358
|
+
}
|
|
359
|
+
if (leaderGone) {
|
|
360
|
+
if (process.platform === "win32")
|
|
361
|
+
ownedChildGone = true;
|
|
362
|
+
else {
|
|
363
|
+
try {
|
|
364
|
+
process.kill(-Number(proof.pgid), 0);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
ownedChildGone = error.code === "ESRCH";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
312
372
|
if (state.shutdown_state === "starting" && state.sessions?.length === 0 && state.active_tree !== true && !state.active_operation
|
|
313
373
|
&& start?.operation === "daemon.start" && start.state === "failed" && start.predecessor_daemon_id === null && start.daemon_id === null
|
|
314
374
|
&& typeof state.daemon_id === "string" && controllerStarts.length === 1 && starts.length === 1 && starts[0].operation_id === start.operation_id
|
|
315
375
|
&& launch?.runtime_owner === JSON.stringify(owner)
|
|
316
376
|
&& daemon.length === 1 && provider.length <= 1 && records.length === daemon.length + provider.length
|
|
317
377
|
&& daemon[0].id === state.resource_process?.id && daemon[0].lease_token === state.resource_process.lease_token
|
|
318
|
-
&& (state.pid == null || state.pid === daemon[0].pid)
|
|
378
|
+
&& (state.pid == null || state.pid === daemon[0].pid || ownedChildGone)
|
|
319
379
|
&& (!state.provider_process && state.provider_pid == null && provider.length === 0 || provider.length === 1 && provider[0].id === state.provider_process?.id && provider[0].lease_token === state.provider_process.lease_token && state.provider_pid === provider[0].pid)
|
|
320
|
-
&& records.every(record => record.pid && record.pid_started_at && !processTreeIsAlive(record) && ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at))) {
|
|
321
|
-
failedStart = { source: "failed_start_without_create", daemon_id: state.daemon_id, operation_id: start.operation_id, session_ids: [], processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })) };
|
|
380
|
+
&& records.every(record => (record.pid && record.pid_started_at && !processTreeIsAlive(record) || ownedChildGone && record === daemon[0]) && ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at))) {
|
|
381
|
+
failedStart = { source: "failed_start_without_create", daemon_id: state.daemon_id, operation_id: start.operation_id, session_ids: [], processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })), ...(ownedChildGone ? { startup_child_cleanup: proof } : {}) };
|
|
322
382
|
closedDaemons.set(directory, failedStart);
|
|
323
383
|
}
|
|
324
384
|
for (const operation of nativeOperations)
|
|
@@ -331,6 +391,8 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
331
391
|
unresolved.push({ state_dir: directory, status: "pending", reason: "daemon_operation_inventory_invalid" });
|
|
332
392
|
}
|
|
333
393
|
}
|
|
394
|
+
if (closedDaemons.get(directory)?.source === "observed_owned_retirement")
|
|
395
|
+
continue;
|
|
334
396
|
if (state.shutdown_state !== "clean") {
|
|
335
397
|
if (state.shutdown_state === "unclean")
|
|
336
398
|
unresolved.push({ state_dir: directory, status: "pending", reason: "native_shutdown_unclean" });
|
|
@@ -634,11 +696,17 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
634
696
|
const nativeOperations = retainedDaemonOperations(directory);
|
|
635
697
|
const targetList = [...targets.values()];
|
|
636
698
|
const sessionsSettled = ids.every(id => nodes.some((node, index) => targetList[index]?.session.state_dir === directory && targetList[index]?.session.id === id && node.status === "completed"));
|
|
637
|
-
// A confirmed
|
|
638
|
-
//
|
|
699
|
+
// A confirmed failure may retry the same incarnation's settled tree or
|
|
700
|
+
// remaining idempotent resource finish. Unknown outcomes keep their ID.
|
|
639
701
|
if (sessionsSettled) {
|
|
702
|
+
const remainingFinish = state.shutdown_state === "cleanup_failed" && state.active_tree === false
|
|
703
|
+
&& state.shutdown?.daemon_id === daemonId && state.shutdown.phases?.tree === true
|
|
704
|
+
&& state.shutdown.required_phases?.includes("daemon_resource")
|
|
705
|
+
&& state.shutdown.phases.daemon_resource !== true
|
|
706
|
+
&& state.shutdown.required_phases.every(phase => phase === "daemon_resource" || state.shutdown.phases[phase] === true);
|
|
640
707
|
while (nativeOperations.some(operation => operation.operation_id === operationId && operation.daemon_id === daemonId && operation.operation === "daemon.stop"
|
|
641
|
-
&& operation.state === "failed" && operation.error?.code === "tree_not_settled"
|
|
708
|
+
&& operation.state === "failed" && (operation.error?.code === "tree_not_settled"
|
|
709
|
+
|| remainingFinish && operation.error?.code === "process_maintenance_timeout")))
|
|
642
710
|
operationId += ":finalize";
|
|
643
711
|
}
|
|
644
712
|
assertCurrent();
|
|
@@ -697,11 +765,13 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
697
765
|
const executable = directoryAdapters.get(directory);
|
|
698
766
|
if (!inventory)
|
|
699
767
|
continue;
|
|
768
|
+
const pending = retainedDaemonOperations(directory).filter(operation => operation.daemon_id === daemonId && ["running", "awaiting_provider"].includes(String(operation.state)) && ["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(String(operation.operation)) && typeof operation.operation_id === "string");
|
|
769
|
+
if (!pending.length)
|
|
770
|
+
continue;
|
|
700
771
|
if (!daemonId || !executable || !node.operation_id) {
|
|
701
772
|
terminalizedOperations.push({ state_dir: directory, status: "pending", reason: "operation_terminalization_binding_unknown" });
|
|
702
773
|
continue;
|
|
703
774
|
}
|
|
704
|
-
const pending = retainedDaemonOperations(directory).filter(operation => operation.daemon_id === daemonId && ["running", "awaiting_provider"].includes(String(operation.state)) && ["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(String(operation.operation)) && typeof operation.operation_id === "string");
|
|
705
775
|
terminalizedOperations.push(...await Promise.all(pending.map(async (operation) => {
|
|
706
776
|
try {
|
|
707
777
|
const operationId = operation.operation_id, sessionId = typeof operation.session_id === "string" ? operation.session_id : null;
|
|
@@ -795,7 +865,8 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
795
865
|
verifyInventory();
|
|
796
866
|
}
|
|
797
867
|
catch (error) {
|
|
798
|
-
unresolved.push({ status: "pending", reason: error instanceof AppError && error.code === "owned_inventory_changed" ? error.code : "owned_inventory_recheck_failed"
|
|
868
|
+
unresolved.push({ status: "pending", reason: error instanceof AppError && error.code === "owned_inventory_changed" ? error.code : "owned_inventory_recheck_failed",
|
|
869
|
+
...(error instanceof AppError && error.code === "owned_inventory_changed" ? { components: error.details.components } : {}) });
|
|
799
870
|
}
|
|
800
871
|
}
|
|
801
872
|
assertCurrent();
|
|
@@ -818,7 +889,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
818
889
|
}
|
|
819
890
|
export function nativeInventoryVersion(directory, operations = retainedDaemonOperations(directory)) {
|
|
820
891
|
const state = JSON.parse(fs.readFileSync(path.join(directory, "daemon.json"), "utf8"));
|
|
821
|
-
const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes"];
|
|
892
|
+
const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "shutdown", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes"];
|
|
822
893
|
// Only these native verbs are read-only. Unknown verbs remain guarded.
|
|
823
894
|
const productive = operations.filter(operation => !isObservationOperation(operation.operation));
|
|
824
895
|
return crypto.createHash("sha256").update(JSON.stringify([Object.fromEntries(keys.map(key => [key, state[key]])), productive])).digest("hex");
|
|
@@ -6,7 +6,7 @@ import { createEvalRunSnapshot, verifyControllerBoundarySnapshot } from "./eval-
|
|
|
6
6
|
import { observeControllerBoundary } from "./run-control.js";
|
|
7
7
|
import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
|
|
8
8
|
import { appendEvent, digest, errorRecord, requireController, requireRun } from "./run-controller-state.js";
|
|
9
|
-
import { getFlowRunStatus } from "./runs.js";
|
|
9
|
+
import { getFlowRunStatus, readFlowRunExecutionBundle } from "./runs.js";
|
|
10
10
|
import { recoveryObservationBudget, withRecoveryObservation } from "./recovery-observation-budget.js";
|
|
11
11
|
import { getResourceDatabase } from "../storage/database.js";
|
|
12
12
|
import { managedProcessBinding, resourceHome } from "./managed-processes.js";
|
|
@@ -60,6 +60,7 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
|
|
|
60
60
|
throw error;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
const preparedBundle = readFlowRunExecutionBundle(context, scope);
|
|
63
64
|
const assertStartOwner = prepareControllerPublication(context, row, assertPhysicalOwner);
|
|
64
65
|
context.db.writeTransaction(() => {
|
|
65
66
|
assertStartOwner();
|
|
@@ -82,7 +83,7 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
|
|
|
82
83
|
if (!current || !["done", "skipped"].includes(current.status) || digest(identity(current)) !== boundaryKey)
|
|
83
84
|
throw new AppError("controller_boundary_changed", "Stage identity changed after its capture fence", 1);
|
|
84
85
|
if (physical) {
|
|
85
|
-
const continuation = getFlowRunStatus(context, scope).continuation;
|
|
86
|
+
const continuation = getFlowRunStatus(context, scope, preparedBundle ?? undefined).continuation;
|
|
86
87
|
const stoppedAtTarget = nextStage === null && manifest.request.stop_after === stage.stage && continuation.kind === "start_stage";
|
|
87
88
|
if (nextStage ? continuation.kind !== "start_stage" || continuation.stage !== nextStage : continuation.kind !== "terminal" && !stoppedAtTarget)
|
|
88
89
|
throw new AppError("controller_boundary_changed", "Stage successor changed after its capture fence", 1);
|
|
@@ -123,7 +124,13 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
|
|
|
123
124
|
assertCurrent();
|
|
124
125
|
context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(COALESCE(receipt_json, '{}'), '$.recovery_observation', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(observationBudget.state()), context.now(), operationId]);
|
|
125
126
|
let verify = () => { throw new AppError("controller_boundary_unsettled", "Boundary has no stable writer observation", 1); };
|
|
126
|
-
const observation = await withRecoveryObservation(observationBudget, () => observeControllerBoundary(context, { ...scope, controllerId: row.controller_id, operationId, assertCurrent, retainVerification: value => {
|
|
127
|
+
const observation = await withRecoveryObservation(observationBudget, () => observeControllerBoundary(context, { ...scope, controllerId: row.controller_id, operationId, assertCurrent, retainVerification: value => {
|
|
128
|
+
verify = () => {
|
|
129
|
+
value();
|
|
130
|
+
if (readFlowRunExecutionBundle(context, scope)?.sha256 !== preparedBundle?.sha256)
|
|
131
|
+
throw new AppError("controller_boundary_changed", "Frozen bundle changed before snapshot publication", 1);
|
|
132
|
+
};
|
|
133
|
+
} }));
|
|
127
134
|
if (observation.settled === true) {
|
|
128
135
|
try {
|
|
129
136
|
createEvalRunSnapshot(context, { ...scope, output, ...(nextStage ? { stageEntry: nextStage } : { candidate: true }), boundary: { controllerId: row.controller_id, operationId, boundaryKey, observation, verify } });
|
|
@@ -48,6 +48,22 @@ export function assertControllerRecoveryAllowed(context, row) {
|
|
|
48
48
|
|| !context.db.get("SELECT 1 FROM run_controls WHERE control_id = ? AND recovery_id = ? AND status = 'resuming'", [state.recovery.control_id, state.recovery.recovery_id]))
|
|
49
49
|
throw new AppError("run_control_superseded", "Controller launch no longer owns the accepted recovery", 1);
|
|
50
50
|
}
|
|
51
|
+
/** An admitted physical owner may renew while closing a terminal RUN. This
|
|
52
|
+
* does not admit a new owner or release a recovery/generation fence. */
|
|
53
|
+
export function assertControllerLeaseAllowed(context, row) {
|
|
54
|
+
const current = requireController(context, row.controller_id);
|
|
55
|
+
if (current.owner_token !== row.owner_token || current.generation !== row.generation
|
|
56
|
+
|| current.owner_pid !== process.pid || current.process_id !== row.process_id
|
|
57
|
+
|| current.process_lease_token !== row.process_lease_token || !current.process_id || !current.process_lease_token
|
|
58
|
+
|| !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
|
|
59
|
+
throw new AppError("controller_owner_mismatch", "Lease renewal no longer owns this physical controller", 1);
|
|
60
|
+
const run = requireRun(context, current.project_id, current.run_id);
|
|
61
|
+
if ((["done", "failed", "cancelled"].includes(run.status) || current.status === "capturing") && launchRecord(context, current)?.value.dispatch_ready === true) {
|
|
62
|
+
assertControllerRecoveryAllowed(context, current);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
assertLaunchAllowed(context, current);
|
|
66
|
+
}
|
|
51
67
|
function retire(context, row, identity) {
|
|
52
68
|
const record = managedProcessStatus(context).find(item => item.id === identity.id);
|
|
53
69
|
if (!record || record.kind !== "run-controller" || record.owner_id !== row.controller_id || record.project_id !== row.project_id || record.run_id !== row.run_id
|
|
@@ -123,6 +139,10 @@ export function claimControllerProcess(context, row) {
|
|
|
123
139
|
throw new AppError("controller_owner_mismatch", "Managed registration is not authorized", 1);
|
|
124
140
|
const owner = birth();
|
|
125
141
|
context.db.writeTransaction(() => {
|
|
142
|
+
// Recovery fences are reconciled by serve's retained failure boundary;
|
|
143
|
+
// only terminal admission must reject before claiming a physical owner.
|
|
144
|
+
if (["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
|
|
145
|
+
throw new AppError("invalid_run_state", "A terminal RUN cannot start a physical controller", 1);
|
|
126
146
|
const claimed = context.db.run("UPDATE run_controllers SET owner_pid = ?, status = 'running', updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), row.controller_id, row.owner_token]);
|
|
127
147
|
if (claimed.changes !== 1)
|
|
128
148
|
throw new AppError("controller_already_owned", "Managed owner has already claimed this launch", 1);
|
|
@@ -142,6 +162,7 @@ export function claimControllerProcess(context, row) {
|
|
|
142
162
|
}
|
|
143
163
|
}
|
|
144
164
|
export function markControllerDispatchReady(context, row) {
|
|
165
|
+
assertLaunchAllowed(context, row);
|
|
145
166
|
context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(receipt_json, '$.dispatch_ready', json('true')), updated_at = ? WHERE operation_id = ?", [context.now(), launchId(row)]);
|
|
146
167
|
}
|
|
147
168
|
export async function spawnController(context, row) {
|
|
@@ -10,6 +10,7 @@ import { verifyRecoverySnapshot } from "./eval-snapshots.js";
|
|
|
10
10
|
import { recoveryGuard, resumeRunRecovery } from "./run-recovery.js";
|
|
11
11
|
import { prepareRunRecoveryDispatch, retainRecoveryCapacityIntent } from "./run-recovery-runtime.js";
|
|
12
12
|
import { runtimeHarness } from "./execution-policy.js";
|
|
13
|
+
import { selectControllerExecution } from "./controller-execution.js";
|
|
13
14
|
import { managedProcessBinding, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processTreeIsAlive, processStartedAt } from "./managed-processes.js";
|
|
14
15
|
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, digest, errorRecord } from "./run-controller-state.js";
|
|
15
16
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
@@ -74,7 +75,8 @@ export function prepareControllerResume(context, input) {
|
|
|
74
75
|
if (!guard || guard.recovery_id !== control?.recovery_id || guard.status !== "sealed" || control.status !== "settled" || !guard.capture_path || JSON.parse(guard.settlement_json ?? "{}").settled !== true)
|
|
75
76
|
throw new AppError("run_control_not_resumable", "Resume requires the current settled control and its immutable recovery capture", 1);
|
|
76
77
|
const manifest = JSON.parse(row.manifest_json), state = JSON.parse(row.state_json);
|
|
77
|
-
|
|
78
|
+
selectControllerExecution(context, run, manifest);
|
|
79
|
+
if (!path.isAbsolute(manifest.resource_home) || !path.isAbsolute(manifest.flow_executable))
|
|
78
80
|
throw new AppError("controller_manifest_invalid", "Resume requires the original frozen controller manifest", 1);
|
|
79
81
|
if (state.current_session === null || !state.sessions[state.current_session]?.id)
|
|
80
82
|
throw new AppError("recovery_owner_missing", "Resume requires the retained coordinator Session", 1);
|
|
@@ -3,7 +3,7 @@ import { AppError, errorRecord as structuredErrorRecord } from "../shared/errors
|
|
|
3
3
|
import { runtimeHarness } from "./execution-policy.js";
|
|
4
4
|
import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
|
|
5
5
|
export function requireRun(context, projectId, runId) {
|
|
6
|
-
const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root, status, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
6
|
+
const run = context.db.get("SELECT id, project_id, flow_kind, project_root, workspace_root, run_root, status, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
7
7
|
if (!run?.run_root)
|
|
8
8
|
throw new AppError("not_found", "Managed RUN has no materialized root", 1, { run_id: runId });
|
|
9
9
|
return run;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
export function recoveryPacketMatchesStage(packet, scope) {
|
|
3
|
+
return packet.schema_id === 'dd-flow/root-recovery-packet@1' && packet.continuation === 'unfinished_work'
|
|
4
|
+
&& packet.run_id === scope.runId && packet.generation === scope.generation && packet.stage === scope.stage
|
|
5
|
+
&& packet.work_id === scope.workId && packet.paused === false;
|
|
6
|
+
}
|
|
2
7
|
import crypto from "node:crypto";
|
|
3
8
|
import fs from "node:fs";
|
|
4
9
|
import path from "node:path";
|
|
@@ -13,10 +18,11 @@ import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
|
|
|
13
18
|
import { parseRuntimeBudget } from "./runtime-budget.js";
|
|
14
19
|
import { requireProjectByRoot } from "./projects.js";
|
|
15
20
|
import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
|
|
16
|
-
import { getFlowRunStatus, setFlowRunRecoveryPaused } from "./runs.js";
|
|
21
|
+
import { getFlowRunStatus, setFlowRunRecoveryPaused, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
17
22
|
import { loadExternalStageContext } from "./stage-context.js";
|
|
18
23
|
import { flowCommand } from "./stage-pause.js";
|
|
19
|
-
import {
|
|
24
|
+
import { sameSessionProfile, runtimeHarness } from "./execution-policy.js";
|
|
25
|
+
import { selectControllerExecution } from "./controller-execution.js";
|
|
20
26
|
import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
|
|
21
27
|
import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
|
|
22
28
|
import { assertManagedProcessLease, confirmManagedProcess, renewManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
@@ -29,7 +35,7 @@ import { claimQueuedMergeRequest } from "./merge-server.js";
|
|
|
29
35
|
import { prepareVnextCodeWorkspace } from "./vnext-code.js";
|
|
30
36
|
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
31
37
|
import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
32
|
-
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady,
|
|
38
|
+
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
33
39
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
34
40
|
import { retainedChildObserver } from "./controller-child-observer.js";
|
|
35
41
|
import { capacityDelay, continueCodexCapacity, eligibleCodexChildOverload } from "./codex-capacity.js";
|
|
@@ -40,6 +46,11 @@ import { storageSessionId } from "./session-identity.js";
|
|
|
40
46
|
import { CAPACITY_POLICY, nativeItemsSettled, overloadBurst, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
41
47
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
42
48
|
export { resumeRunController } from "./run-controller-recovery.js";
|
|
49
|
+
const preparedLaunchInputs = new WeakMap();
|
|
50
|
+
function launchInputDigest(input) {
|
|
51
|
+
return digest([input.projectRoot, input.runId, input.requestId, input.contextFile, input.contextSha256,
|
|
52
|
+
input.stopAfter ?? null, input.captureRoot ?? null]);
|
|
53
|
+
}
|
|
43
54
|
export function prepareRunControllerLaunch(context, input) {
|
|
44
55
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
|
|
45
56
|
throw new AppError("usage", "Managed launch needs a request id and context SHA-256", 2);
|
|
@@ -60,58 +71,79 @@ export function prepareRunControllerLaunch(context, input) {
|
|
|
60
71
|
if (existing) {
|
|
61
72
|
if (existing.request_hash !== requestHash)
|
|
62
73
|
throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
|
|
63
|
-
|
|
74
|
+
selectControllerExecution(context, run, JSON.parse(existing.manifest_json));
|
|
75
|
+
const prepared = { kind: "replay", existing };
|
|
76
|
+
preparedLaunchInputs.set(prepared, { home: context.ddFlowHome, sha256: launchInputDigest(input) });
|
|
77
|
+
return prepared;
|
|
64
78
|
}
|
|
65
79
|
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
66
80
|
if (run.status !== "running")
|
|
67
81
|
throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1, { status: run.status });
|
|
68
|
-
const
|
|
69
|
-
const execution = index.execution_profile;
|
|
70
|
-
if (!execution?.settings.execution || !execution.agent_profiles)
|
|
71
|
-
throw new AppError("execution_profile_not_frozen", "Managed launch requires the RUN's frozen execution profiles", 1);
|
|
82
|
+
const routing = selectControllerExecution(context, run);
|
|
72
83
|
const contexts = readContexts(input.contextFile, input.contextSha256);
|
|
84
|
+
if (routing.bundle) {
|
|
85
|
+
for (const stage of [...Object.keys(contexts), ...(input.stopAfter ? [input.stopAfter] : [])])
|
|
86
|
+
routing.bundle.descriptor.stage(stage);
|
|
87
|
+
}
|
|
73
88
|
const next = getFlowRunStatus(context, { projectRoot: project.root, runId: run.id }).continuation;
|
|
74
89
|
if (next.kind !== "start_stage" || !next.stage)
|
|
75
90
|
throw new AppError("controller_attach_required", "Managed launch cannot replace an attached or unfinished coordinator; reconcile its existing owner", 1, { continuation: next });
|
|
76
91
|
if (!contexts[next.stage])
|
|
77
92
|
throw new AppError("controller_context_missing", "Managed context omits the first stage", 2, { stage: next.stage });
|
|
78
93
|
const adapters = {};
|
|
79
|
-
for (const profile of Object.values(
|
|
94
|
+
for (const profile of Object.values(routing.profiles)) {
|
|
80
95
|
const harness = runtimeHarness(profile);
|
|
81
96
|
adapters[harness] = { executable: resolveHarnessCommand(context.ddFlowHome, harness, "adapter"), runtime_args: harnessRuntimeArguments(context.ddFlowHome, harness) };
|
|
82
97
|
}
|
|
83
98
|
const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
84
99
|
if (!fs.existsSync(entry))
|
|
85
100
|
throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached controller", 1);
|
|
86
|
-
|
|
101
|
+
const prepared = { kind: "create", project, run, request, requestHash, generation, budget, routing, contexts, adapters };
|
|
102
|
+
preparedLaunchInputs.set(prepared, { home: context.ddFlowHome, sha256: launchInputDigest(input) });
|
|
103
|
+
return prepared;
|
|
87
104
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
105
|
+
/** Retain the existing launch intent and frozen inputs without starting a
|
|
106
|
+
* process. Dispatch remains exclusively owned by launchRunController. */
|
|
107
|
+
export function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
108
|
+
const selection = preparedLaunchInputs.get(prepared);
|
|
109
|
+
if (!selection || selection.home !== context.ddFlowHome || selection.sha256 !== launchInputDigest(input)) {
|
|
110
|
+
throw new AppError("runtime_request_conflict", "Controller launch differs from its prepared request", 1, { effect: "no_effect" });
|
|
111
|
+
}
|
|
112
|
+
if (prepared.kind === "replay") {
|
|
113
|
+
const current = requireController(context, prepared.existing.controller_id);
|
|
114
|
+
if (current.request_hash !== prepared.existing.request_hash)
|
|
115
|
+
throw new AppError("runtime_request_conflict", "Controller launch authority changed after replay preparation", 1);
|
|
116
|
+
selectControllerExecution(context, requireRun(context, current.project_id, current.run_id), JSON.parse(current.manifest_json));
|
|
117
|
+
return { controller: current, reused: true };
|
|
118
|
+
}
|
|
119
|
+
const { project, run, request, requestHash, generation, budget, routing, contexts, adapters } = prepared;
|
|
92
120
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
93
121
|
ensureRunControllerStorage(context.db);
|
|
94
122
|
const controllerId = `DRV-${crypto.randomUUID()}`;
|
|
95
123
|
const root = path.join(run.run_root, "controllers", controllerId);
|
|
96
124
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
97
|
-
const flowExecutable = prepareHarnessFlowExecutable(context, root);
|
|
125
|
+
const flowExecutable = prepareHarnessFlowExecutable(context, root, routing.bundle?.engine());
|
|
98
126
|
const frozenContexts = {};
|
|
99
127
|
for (const [stage, value] of Object.entries(contexts)) {
|
|
100
128
|
const file = path.join(root, `${stage}.context.json`);
|
|
101
129
|
fs.writeFileSync(file, value.bytes, { flag: "wx", mode: 0o600 });
|
|
102
130
|
frozenContexts[stage] = { file, sha256: value.sha256 };
|
|
103
131
|
}
|
|
104
|
-
const manifest = { schema_id: "dd-flow/run-controller-manifest@
|
|
132
|
+
const manifest = { schema_id: routing.kind === "descriptor" ? "dd-flow/run-controller-manifest@2" : "dd-flow/run-controller-manifest@1", request,
|
|
133
|
+
...(routing.kind === "descriptor" ? { execution_binding: routing.binding } : { execution_profile: routing.legacy }),
|
|
134
|
+
contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget };
|
|
105
135
|
const token = crypto.randomUUID();
|
|
106
136
|
const now = context.now();
|
|
107
137
|
const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
|
|
108
138
|
const launcher = { pid: process.pid, pid_started_at: processStartedAt(process.pid) };
|
|
109
139
|
const duplicate = context.db.writeTransaction(() => {
|
|
140
|
+
if (routing.bundle)
|
|
141
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, routing.bundle);
|
|
110
142
|
const duplicate = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]);
|
|
111
143
|
if (duplicate) {
|
|
112
144
|
if (duplicate.request_hash !== requestHash)
|
|
113
145
|
throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
|
|
114
|
-
return
|
|
146
|
+
return duplicate;
|
|
115
147
|
}
|
|
116
148
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
117
149
|
if (requireRun(context, project.id, run.id).status !== "running")
|
|
@@ -126,9 +158,16 @@ export async function launchRunController(context, input, prepared = prepareRunC
|
|
|
126
158
|
appendEvent(context, controllerId, "launch_requested", { request_id: input.requestId, generation, request_hash: requestHash });
|
|
127
159
|
return null;
|
|
128
160
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
return { controller: duplicate ?? requireController(context, controllerId), reused: duplicate !== null };
|
|
162
|
+
}
|
|
163
|
+
export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
164
|
+
const retained = retainRunControllerLaunch(context, input, prepared);
|
|
165
|
+
// A simultaneous publisher already owns dispatch. Preserve the original
|
|
166
|
+
// race behavior: only an explicit replay may reconcile that existing owner.
|
|
167
|
+
if (prepared.kind === "create" && retained.reused)
|
|
168
|
+
return { ...controllerReceipt(retained.controller), reused: true };
|
|
169
|
+
const receipt = await spawnController(context, retained.controller);
|
|
170
|
+
return retained.reused ? { ...receipt, reused: true } : receipt;
|
|
132
171
|
}
|
|
133
172
|
/**
|
|
134
173
|
* A registry write can be temporarily unavailable while another runtime client
|
|
@@ -179,6 +218,7 @@ export async function serveRunController(context, input) {
|
|
|
179
218
|
throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
|
|
180
219
|
const confirmed = confirmManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
|
|
181
220
|
const lease = controllerLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: row.process_id, leaseToken: row.process_lease_token }), confirmed.lease_expires_at);
|
|
221
|
+
const state = JSON.parse(row.state_json);
|
|
182
222
|
// Resource and RUN commits are distinct effects. A RUN keepalive failure must
|
|
183
223
|
// neither erase a resource receipt nor grant RUN dispatch authority.
|
|
184
224
|
const mergeLease = controllerLeaseMonitor(attemptMs => {
|
|
@@ -194,21 +234,19 @@ export async function serveRunController(context, input) {
|
|
|
194
234
|
return { owned: true, lease_expires_at: expiry };
|
|
195
235
|
});
|
|
196
236
|
const assertLeaseFence = () => {
|
|
197
|
-
|
|
198
|
-
if (current.owner_token !== row.owner_token || current.generation !== row.generation)
|
|
199
|
-
throw new AppError("controller_owner_mismatch", "Lease renewal no longer owns this controller generation", 1);
|
|
200
|
-
assertControllerRecoveryAllowed(context, current);
|
|
237
|
+
assertControllerLeaseAllowed(context, row);
|
|
201
238
|
};
|
|
202
239
|
const heartbeat = setInterval(() => {
|
|
203
240
|
lease.background(assertLeaseFence, () => { });
|
|
204
|
-
|
|
241
|
+
if (state.merge_claim)
|
|
242
|
+
mergeLease.background(assertLeaseFence, () => { });
|
|
205
243
|
}, 30_000);
|
|
206
244
|
heartbeat.unref();
|
|
207
245
|
const manifest = JSON.parse(row.manifest_json);
|
|
208
|
-
const state = JSON.parse(row.state_json);
|
|
209
246
|
const assertPhysicalOwner = () => {
|
|
210
247
|
lease.assertNoRenewal();
|
|
211
|
-
|
|
248
|
+
if (state.merge_claim)
|
|
249
|
+
mergeLease.assertNoRenewal();
|
|
212
250
|
assertManagedProcessLease({ ...context, databaseDeadlineAt: Date.now() + 5_000 }, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
|
|
213
251
|
const current = requireController(context, row.controller_id);
|
|
214
252
|
if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
|
|
@@ -221,7 +259,8 @@ export async function serveRunController(context, input) {
|
|
|
221
259
|
};
|
|
222
260
|
try {
|
|
223
261
|
await lease.ensureConfirmed(assertLeaseFence);
|
|
224
|
-
|
|
262
|
+
if (state.merge_claim)
|
|
263
|
+
await mergeLease.ensureConfirmed(assertLeaseFence);
|
|
225
264
|
context.db.writeTransaction(() => {
|
|
226
265
|
markControllerDispatchReady(context, row);
|
|
227
266
|
if (!state.recovery && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation && requireRun(context, row.project_id, row.run_id).status === "paused")
|
|
@@ -230,7 +269,8 @@ export async function serveRunController(context, input) {
|
|
|
230
269
|
appendEvent(context, row.controller_id, "owner_started", { pid: process.pid, generation: row.generation });
|
|
231
270
|
if (state.recovery && !["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
|
|
232
271
|
await resumeControllerSession(context, row, manifest, state, assertPhysicalOwner);
|
|
233
|
-
await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence);
|
|
272
|
+
await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (state.merge_claim)
|
|
273
|
+
await mergeLease.ensureConfirmed(assertLeaseFence); });
|
|
234
274
|
return controllerReceipt(requireController(context, row.controller_id));
|
|
235
275
|
}
|
|
236
276
|
catch (error) {
|
|
@@ -449,8 +489,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
449
489
|
}
|
|
450
490
|
}
|
|
451
491
|
};
|
|
452
|
-
const
|
|
453
|
-
const
|
|
492
|
+
const routing = selectControllerExecution(context, requireRun(context, row.project_id, row.run_id), manifest);
|
|
493
|
+
const policy = routing.policy;
|
|
494
|
+
const profiles = routing.profiles;
|
|
454
495
|
let executionContext = { ...context, env: { ...context.env, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined, DD_FLOW_CODE_READINESS_OWNER: row.controller_id } };
|
|
455
496
|
const bindInvocationScope = (session) => {
|
|
456
497
|
executionContext = managedInvocationContext(executionContext, { harness: runtimeHarness(session.profile), stateDir: session.state_dir, sessionId: session.id,
|
|
@@ -745,7 +786,10 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
745
786
|
if (state.current_session !== null && !state.sessions[state.current_session].stopped)
|
|
746
787
|
bindInvocationScope(state.sessions[state.current_session]);
|
|
747
788
|
const run = requireRun(context, row.project_id, row.run_id);
|
|
748
|
-
|
|
789
|
+
const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
|
|
790
|
+
const next = view.continuation;
|
|
791
|
+
terminalFinalization = next.kind === "terminal";
|
|
792
|
+
if (state.pending_capacity && next.kind !== "terminal" && !["done", "failed", "cancelled"].includes(run.status)) {
|
|
749
793
|
const pending = state.pending_capacity;
|
|
750
794
|
const session = state.sessions.find(item => item.id === pending.session_id && !item.stopped);
|
|
751
795
|
const operation = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [pending.root_operation_id, row.controller_id, row.generation]);
|
|
@@ -757,9 +801,6 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
757
801
|
await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
|
|
758
802
|
continue;
|
|
759
803
|
}
|
|
760
|
-
const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
|
|
761
|
-
const next = view.continuation;
|
|
762
|
-
terminalFinalization = next.kind === "terminal";
|
|
763
804
|
const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
|
|
764
805
|
if (state.current_session !== null && (completed || next.stage !== state.stage) && childObserver(state.sessions[state.current_session])?.failed().length)
|
|
765
806
|
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids completing or advancing its Stage", 1);
|
|
@@ -833,8 +874,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
833
874
|
loadExternalStageContext({ file: stageContext.file, expectedSha256: stageContext.sha256, stage: next.stage });
|
|
834
875
|
const continuation = JSON.stringify(["start_stage", next.stage, next.attempt ?? null]);
|
|
835
876
|
retainControllerContinuation(state, continuation, { code: "stage_entry_nonprogressing", message: "The coordinator returned without starting the assigned Stage", details: { stage: next.stage, attempt: next.attempt ?? null, last_receipt: state.last_receipt ?? null } });
|
|
836
|
-
const transition =
|
|
837
|
-
if (next.stage === "merge" &&
|
|
877
|
+
const transition = routing.transition(next.stage, state.stage ?? undefined);
|
|
878
|
+
if (routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "server" && !state.merge_claim) {
|
|
838
879
|
const request = context.db.get("SELECT merge_request_id, status FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, merge_request_id DESC LIMIT 1", [row.project_id, row.run_id]);
|
|
839
880
|
if (!request || request.status !== "queued")
|
|
840
881
|
throw new AppError("controller_attach_required", "Managed MERGE needs an unclaimed queued request; reconcile an existing owner", 1, { request });
|
|
@@ -847,10 +888,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
847
888
|
state.merge_claim = { request_id: claim.request.merge_request_id, token: claim.token, target_workspace: claim.request.target_workspace, work_id: claim.request.executor_work_id };
|
|
848
889
|
saveState(context, row, state);
|
|
849
890
|
appendEvent(context, row.controller_id, "merge_claimed", { request_id: claim.request.merge_request_id });
|
|
891
|
+
await renewLease();
|
|
850
892
|
}
|
|
851
|
-
const stageCwd =
|
|
893
|
+
const stageCwd = routing.cwd(next.stage, state.merge_claim?.target_workspace);
|
|
852
894
|
let session = state.current_session === null ? null : state.sessions[state.current_session];
|
|
853
|
-
if (session && next.stage === "merge" &&
|
|
895
|
+
if (session && routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "same_session" && path.resolve(session.cwd) !== stageCwd) {
|
|
854
896
|
throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native Session working directory", 1, { session_cwd: session.cwd, target_cwd: stageCwd });
|
|
855
897
|
}
|
|
856
898
|
if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
|
|
@@ -890,7 +932,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
890
932
|
state.child_capacity = {};
|
|
891
933
|
saveState(context, row, state);
|
|
892
934
|
setStatus("running");
|
|
893
|
-
if (next.stage === "code" || next.stage === "code-review") {
|
|
935
|
+
if (routing.kind === "sdlc" && (next.stage === "code" || next.stage === "code-review")) {
|
|
894
936
|
// A CODE_REVIEW fork has source files but intentionally no installed
|
|
895
937
|
// dependencies. Its repair Work executes checks just like CODE Work.
|
|
896
938
|
const readiness = await prepareVnextCodeWorkspace(executionContext, { projectRoot: run.project_root, runId: run.id, ownerId: row.controller_id, stage: next.stage });
|
|
@@ -1016,6 +1058,25 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1016
1058
|
const fanout = await nextControllerFanout(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0 });
|
|
1017
1059
|
assertOwner();
|
|
1018
1060
|
if (fanout.kind === "none") {
|
|
1061
|
+
// An ACK-only recovery Turn may leave the original root Work unfinished.
|
|
1062
|
+
// Continue that retained packet once; never restart its Stage or repeat
|
|
1063
|
+
// an unknown productive dispatch merely because the observer reattached.
|
|
1064
|
+
const recovery = context.db.get(`SELECT o.operation_id,b.packet_json,b.packet_sha256,b.work_id
|
|
1065
|
+
FROM run_controller_operations o JOIN run_recovery_bindings b ON b.recovery_id=json_extract(o.receipt_json,'$.recovery_id')
|
|
1066
|
+
JOIN run_recovery_guards g ON g.recovery_id=b.recovery_id
|
|
1067
|
+
WHERE o.controller_id=? AND o.generation=? AND o.status='completed' AND json_extract(o.input_json,'$.action')='control_resume'
|
|
1068
|
+
AND b.status='accepted' AND g.status='recovered' AND g.project_id=? AND g.run_id=?
|
|
1069
|
+
AND b.provider_session_id=? AND b.daemon_id=json_extract(o.receipt_json,'$.daemon_id') ORDER BY o.created_at DESC LIMIT 1`, [row.controller_id, row.generation, row.project_id, row.run_id, session.id]);
|
|
1070
|
+
if (recovery && digestBytes(recovery.packet_json) === recovery.packet_sha256) {
|
|
1071
|
+
const packet = JSON.parse(recovery.packet_json);
|
|
1072
|
+
const operationId = `${recovery.operation_id}:unfinished-root`;
|
|
1073
|
+
const work = context.db.get('SELECT status FROM works WHERE work_id=? AND project_id=? AND run_id=? AND parent_work_id IS NULL', [recovery.work_id, row.project_id, row.run_id]);
|
|
1074
|
+
if (recoveryPacketMatchesStage(packet, { runId: row.run_id, generation: row.generation, stage: next.stage, workId: recovery.work_id }) && work?.status === 'running'
|
|
1075
|
+
&& !context.db.get('SELECT 1 FROM run_controller_operations WHERE operation_id=?', [operationId])) {
|
|
1076
|
+
await prompt(session, `Continue the original unfinished root Work from this accepted recovery packet. Inspect retained receipts and partial effects first. Do not restart the Stage, repeat observed lifecycle operations, create another Work or start a successor Stage. Preserve the packet write scope and result schema; stop at its original boundary.\n\nAccepted recovery packet (JSON data):\n${recovery.packet_json}`, operationId);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1019
1080
|
assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
|
|
1020
1081
|
const cause = currentLifecycleFailure(executionContext, { runId: run.id, generation: row.generation, stage: next.stage, turn: state.turns, projectId: row.project_id, projectRoot: run.project_root });
|
|
1021
1082
|
if (cause)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppError } from "../shared/errors.js";
|
|
2
2
|
import { CAPACITY_POLICY, overloadBurst } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
3
|
+
import { assertRunStartupSettled } from "./run-start-custody.js";
|
|
3
4
|
export function recoveryGuard(context, projectId, runId) { return context.db.get("SELECT recovery_id, project_id, run_id, generation, status, interruption_json, settlement_json, capture_path, created_at, updated_at, sealed_at, resumed_at FROM run_recovery_guards WHERE project_id = ? AND run_id = ? ORDER BY generation DESC LIMIT 1", [projectId, runId]) ?? null; }
|
|
4
5
|
export function assertRunRecoveryAllowed(context, projectId, runId, expectedGeneration) {
|
|
5
6
|
const guard = recoveryGuard(context, projectId, runId);
|
|
@@ -11,6 +12,7 @@ export function assertRunRecoveryAllowed(context, projectId, runId, expectedGene
|
|
|
11
12
|
return generation;
|
|
12
13
|
}
|
|
13
14
|
export function assertRunMutationAllowed(context, projectId, runId, expectedGeneration) {
|
|
15
|
+
assertRunStartupSettled(context, projectId, runId);
|
|
14
16
|
const generation = assertRunRecoveryAllowed(context, projectId, runId, expectedGeneration);
|
|
15
17
|
if (context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'")
|
|
16
18
|
&& context.db.get("SELECT 1 FROM run_controllers WHERE project_id = ? AND run_id = ? AND status IN ('capturing','capture_failed') LIMIT 1", [projectId, runId]))
|