@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156
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 +31 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +274 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +250 -33
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +274 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +25 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +257 -44
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +255 -69
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -13,6 +13,8 @@ import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.m
|
|
|
13
13
|
import { readOperationRecord } from "../harness-runtime/lib/operation-progress.mjs";
|
|
14
14
|
import { readNativeDaemonState } from "./managed-daemon-binding.js";
|
|
15
15
|
import { validateCheckEnvironment } from "./check-environment.js";
|
|
16
|
+
import { controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
|
|
17
|
+
import { recordTechnicalNote } from "./technical-notes.js";
|
|
16
18
|
/** A paused RUN admits only its controller's exact accepted answer Turn. */
|
|
17
19
|
export function controllerHitlAdmission(context, record, input) {
|
|
18
20
|
if (input.operation !== "session.prompt" || !input.operationId || !input.promptSha256)
|
|
@@ -286,11 +288,16 @@ async function awaitSettlement(context, row, manifest, session, executable, args
|
|
|
286
288
|
}
|
|
287
289
|
return adapterSessionId(current) ? { ...initial, ...current, ...(initial.assistant_text !== undefined ? { assistant_text: initial.assistant_text } : {}) } : { ...initial, settlement: settlement(current) };
|
|
288
290
|
}
|
|
289
|
-
export async function controllerAdapter(context, row, manifest, session, args, assertOwner, retainedOperationId, signal, reconfirmOwner) {
|
|
291
|
+
export async function controllerAdapter(context, row, manifest, session, args, assertOwner, retainedOperationId, signal, reconfirmOwner, obligation) {
|
|
290
292
|
const executable = manifest.adapters[runtimeHarness(session.profile)].executable;
|
|
291
293
|
const promptFile = args.indexOf("--prompt-file"), inlinePrompt = args.indexOf("--prompt");
|
|
292
294
|
const promptSha256 = promptFile >= 0 ? digestBytes(fs.readFileSync(args[promptFile + 1])) : inlinePrompt >= 0 ? digestBytes(args[inlinePrompt + 1] ?? "") : undefined;
|
|
293
|
-
const
|
|
295
|
+
const priorInput = retainedOperationId ? context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id=? AND controller_id=? AND generation=?", [retainedOperationId, row.controller_id, row.generation]) : undefined;
|
|
296
|
+
const effectiveObligation = priorInput ? JSON.parse(priorInput.input_json).obligation : obligation;
|
|
297
|
+
// Committing an answer resumes a Stage; it does not finish the semantic
|
|
298
|
+
// work owed by that prompt. Preserve its provider error for same-Session recovery.
|
|
299
|
+
const acceptedPromptBoundary = (expected) => controllerObligationAccepted(context, row, expected.action === "resume" ? { ...expected, action: "finish" } : expected);
|
|
300
|
+
const input = { executable, args, owner: session.owner.owner_id, state_dir: session.state_dir, ...(args[0] === "session" && args[1] === "prompt" && promptSha256 ? { prompt_sha256: promptSha256 } : {}), ...(effectiveObligation ? { obligation: effectiveObligation } : {}) };
|
|
294
301
|
const prepared = retainedOperationId ? [] : context.db.all("SELECT operation_id FROM run_controller_operations WHERE controller_id = ? AND generation = ? AND input_hash = ? AND status = 'dispatching' AND json_extract(input_json, '$.dispatch_phase') = 'prepared'", [row.controller_id, row.generation, digest(input)]);
|
|
295
302
|
if (prepared.length > 1)
|
|
296
303
|
throw new AppError("controller_operation_conflict", "Multiple prepared intents require explicit reconciliation", 1);
|
|
@@ -369,6 +376,14 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
369
376
|
recoveredOperation = true;
|
|
370
377
|
}
|
|
371
378
|
validateReceipt(receipt, session, args);
|
|
379
|
+
const hookDiagnostics = Array.isArray(receipt.native_hook_diagnostics) ? receipt.native_hook_diagnostics : [];
|
|
380
|
+
if (hookDiagnostics.some(item => item && typeof item === "object" && ["native_hook_timeout", "native_hook_failed"].includes(String(item.code)))) {
|
|
381
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "native_hook_observation_unavailable",
|
|
382
|
+
message: "Native hook observation failed; the actual lifecycle command remains authoritative.", phase: "native_observation",
|
|
383
|
+
generation: row.generation, ...(effectiveObligation?.stage ? { stage: effectiveObligation.stage } : {}),
|
|
384
|
+
harness: runtimeHarness(session.profile), source_operation_id: operationId, resource_identity: session.state_dir,
|
|
385
|
+
diagnostic_reference: session.journal, consequence: "none" });
|
|
386
|
+
}
|
|
372
387
|
retainPhase("receipt_observed");
|
|
373
388
|
const persistReceipt = () => {
|
|
374
389
|
const changed = context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND EXISTS (SELECT 1 FROM run_controllers WHERE controller_id = ? AND owner_token = ? AND generation = ?)", [JSON.stringify(receipt), context.now(), operationId, row.controller_id, row.generation, row.controller_id, row.owner_token, row.generation]);
|
|
@@ -376,7 +391,13 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
376
391
|
throw new AppError("controller_owner_mismatch", "Controller lost ownership before receipt retention", 1);
|
|
377
392
|
};
|
|
378
393
|
persistReceipt();
|
|
379
|
-
if (["pending", "blocked"].includes(String(settlement(receipt)?.state))) {
|
|
394
|
+
if (["pending", "blocked"].includes(String(settlement(receipt)?.state)) && effectiveObligation && acceptedPromptBoundary(effectiveObligation)) {
|
|
395
|
+
// Required semantic work is already committed. Keep the honest pending
|
|
396
|
+
// native proof for resource cleanup without holding its accepted result.
|
|
397
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "accepted_boundary_native_settlement_pending", message: "The assigned result is committed; native settlement remains pending.", phase: "native_observation", generation: row.generation, stage: effectiveObligation.stage, harness: runtimeHarness(session.profile), source_operation_id: operationId, diagnostic_reference: session.state_dir, consequence: "cleanup_pending" });
|
|
398
|
+
receipt = { ...receipt, accepted_boundary: effectiveObligation };
|
|
399
|
+
}
|
|
400
|
+
else if (["pending", "blocked"].includes(String(settlement(receipt)?.state))) {
|
|
380
401
|
const recovered = await awaitSettlement(context, row, manifest, session, executable, args, operationId, receipt, assertOwner, signal);
|
|
381
402
|
receipt = recovered;
|
|
382
403
|
}
|
|
@@ -389,6 +410,19 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
389
410
|
return receipt;
|
|
390
411
|
}
|
|
391
412
|
catch (error) {
|
|
413
|
+
// A lost provider/transport acknowledgement cannot revoke the exact
|
|
414
|
+
// boundary already accepted by the engine. Identity/integrity failures
|
|
415
|
+
// and unfinished siblings remain on the existing recovery path.
|
|
416
|
+
const expected = effectiveObligation;
|
|
417
|
+
if (expected && lateNativeFailure(error, session.id) && acceptedPromptBoundary(expected)) {
|
|
418
|
+
assertOwner?.();
|
|
419
|
+
const receipt = { adapter_session_id: session.id, accepted_boundary: expected, native_observation_unavailable: true, settlement: { state: "pending", reason: "native_observation_unavailable" } };
|
|
420
|
+
const completed = context.db.run("UPDATE run_controller_operations SET status='completed',receipt_json=?,error_json=NULL,updated_at=? WHERE operation_id=? AND controller_id=? AND generation=? AND EXISTS (SELECT 1 FROM run_controllers WHERE controller_id=? AND owner_token=? AND generation=?)", [JSON.stringify(receipt), context.now(), operationId, row.controller_id, row.generation, row.controller_id, row.owner_token, row.generation]);
|
|
421
|
+
if (completed.changes !== 1)
|
|
422
|
+
throw new AppError("controller_owner_mismatch", "Controller lost ownership before accepting the committed boundary", 1);
|
|
423
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "accepted_boundary_native_observation_unavailable", message: "The assigned result is committed; the native acknowledgement is unavailable.", phase: "native_observation", generation: row.generation, stage: expected.stage, harness: runtimeHarness(session.profile), source_operation_id: operationId, diagnostic_reference: session.state_dir, consequence: "cleanup_pending" });
|
|
424
|
+
return receipt;
|
|
425
|
+
}
|
|
392
426
|
try {
|
|
393
427
|
context.db.run("UPDATE run_controller_operations SET status = CASE WHEN status = 'dispatching' AND ? <> 'prepared' THEN 'outcome_unknown' ELSE status END, error_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND EXISTS (SELECT 1 FROM run_controllers WHERE controller_id = ? AND owner_token = ? AND generation = ?)", [phase ?? "legacy_unknown", JSON.stringify(errorRecord(error)), context.now(), operationId, row.controller_id, row.generation, row.controller_id, row.owner_token, row.generation]);
|
|
394
428
|
}
|
|
@@ -7,6 +7,7 @@ import { validateCheckEnvironment } from "./check-environment.js";
|
|
|
7
7
|
import { getResourceDatabase } from "../storage/database.js";
|
|
8
8
|
import { finishManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processStartedAt, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
|
|
9
9
|
import { setFlowRunRecoveryPaused } from "./runs.js";
|
|
10
|
+
import { physicalStartup } from "./physical-startup.js";
|
|
10
11
|
import { assertRunMutationAllowed, assertRunRecoveryAllowed, recoveryGuard } from "./run-recovery.js";
|
|
11
12
|
import { requireRun, requireController, controllerReceipt, appendEvent, digest, errorRecord } from "./run-controller-state.js";
|
|
12
13
|
const launchId = (row) => `${row.controller_id}:owner-launch:${digest(row.owner_token)}`;
|
|
@@ -15,6 +16,8 @@ const birth = () => ({ pid: process.pid, pid_started_at: processStartedAt(proces
|
|
|
15
16
|
const dead = (value) => Boolean(value && Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.pid_started_at === "string" && value.pid_started_at && !processIsAlive(value));
|
|
16
17
|
/** Called inside the RUN intent transaction, before the client can disappear. */
|
|
17
18
|
export function recordControllerLauncher(context, row, retired, identity = birth()) {
|
|
19
|
+
if (!identity.pid_started_at)
|
|
20
|
+
throw new AppError("process_ownership_unknown", "Controller launcher requires an observed physical identity", 1, { phase: "process_confirmation", effect: "no_effect", native_dispatch_started: false, reason: "physical_identity_unavailable" });
|
|
18
21
|
const request = { action: "controller_owner_launch", owner_id: launchId(row) };
|
|
19
22
|
const receipt = { ...identity, registration: "controller_self@1", ...(retired ? { retired_process: { id: retired.id, lease_token: retired.lease_token, pid: retired.pid, pid_started_at: retired.pid_started_at } } : {}) };
|
|
20
23
|
const now = context.now();
|
|
@@ -72,12 +75,31 @@ function retire(context, row, identity) {
|
|
|
72
75
|
throw new AppError("controller_owner_unsettled", "Prior controller identity is not proven dead and retired", 1);
|
|
73
76
|
}
|
|
74
77
|
/** Retry only before productive admission, with exact prior birth and journal CAS. */
|
|
75
|
-
function prepareSpawn(context, row) {
|
|
78
|
+
async function prepareSpawn(context, row) {
|
|
76
79
|
const launch = launchRecord(context, row);
|
|
77
80
|
if (!launch || launch.value.dispatch_ready || !["starting", "running", "recovery_required"].includes(row.status))
|
|
78
81
|
return null;
|
|
82
|
+
const superseded = new AppError("controller_owner_mismatch", "Controller launch owner changed during physical observation", 1);
|
|
83
|
+
let launcher;
|
|
84
|
+
try {
|
|
85
|
+
launcher = await physicalStartup(context.databaseDeadlineAt).birth(() => {
|
|
86
|
+
const current = requireController(context, row.controller_id);
|
|
87
|
+
if (current.generation !== row.generation)
|
|
88
|
+
throw new AppError("controller_owner_mismatch", "Controller launch generation changed during physical observation", 1);
|
|
89
|
+
if (current.owner_token !== row.owner_token)
|
|
90
|
+
throw superseded;
|
|
91
|
+
assertLaunchAllowed(context, current);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Like a lost launch CAS below, a concurrent winner is observed, not
|
|
96
|
+
// retried or replaced. Other ownership/admission errors remain refusals.
|
|
97
|
+
if (error === superseded)
|
|
98
|
+
return null;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
79
101
|
const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: JSON.parse(row.manifest_json).resource_home } };
|
|
80
|
-
const ours = launch.value.pid ===
|
|
102
|
+
const ours = launch.value.pid === launcher.pid && launch.value.pid_started_at === launcher.pid_started_at;
|
|
81
103
|
let token = row.owner_token;
|
|
82
104
|
if (!ours) {
|
|
83
105
|
if (row.owner_pid) {
|
|
@@ -115,7 +137,6 @@ function prepareSpawn(context, row) {
|
|
|
115
137
|
return null;
|
|
116
138
|
if (launch.value.retired_process)
|
|
117
139
|
retire(resourceContext, row, launch.value.retired_process);
|
|
118
|
-
const launcher = birth();
|
|
119
140
|
return context.db.writeTransaction(() => {
|
|
120
141
|
assertLaunchAllowed(context, row);
|
|
121
142
|
const changed = context.db.run(`UPDATE run_controllers SET owner_token = ?, owner_pid = NULL, process_id = NULL, process_lease_token = NULL, status = 'starting', last_error_json = NULL, updated_at = ?
|
|
@@ -165,7 +186,7 @@ export function markControllerDispatchReady(context, row) {
|
|
|
165
186
|
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)]);
|
|
166
187
|
}
|
|
167
188
|
export async function spawnController(context, row) {
|
|
168
|
-
const admitted = prepareSpawn(context, row);
|
|
189
|
+
const admitted = await prepareSpawn(context, row);
|
|
169
190
|
if (!admitted)
|
|
170
191
|
return controllerReceipt(requireController(context, row.controller_id));
|
|
171
192
|
row = admitted;
|
|
@@ -18,16 +18,19 @@ import { controllerAdapter, controllerEnvironment, sessionArguments } from "./ru
|
|
|
18
18
|
import { runHarnessAdapter } from "./harness-adapter.js";
|
|
19
19
|
import { continueProviderCapacity } from "./provider-capacity.js";
|
|
20
20
|
import { spawnController, recordControllerLauncher } from "./run-controller-process.js";
|
|
21
|
-
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
21
|
+
import { readNativeDaemonState, requireManagedDaemonBinding, managedDaemonExecutionAbsent } from "./managed-daemon-binding.js";
|
|
22
22
|
import { managedInvocationContext } from "./lifecycle-invocations.js";
|
|
23
23
|
import { runControlStatus } from "./run-control.js";
|
|
24
24
|
import { runtimeBudgetStatus, waitForRuntimeScopeRecovery } from "./runtime-budget.js";
|
|
25
25
|
import { prepareControllerPublication } from "./run-controller-capture.js";
|
|
26
|
+
import { technicalDependencyControl } from "./controller-result-authority.js";
|
|
27
|
+
import { recordTechnicalNote } from "./technical-notes.js";
|
|
28
|
+
import { setFlowRunRecoveryPaused } from "./runs.js";
|
|
26
29
|
/** CLI observation is bounded; the physical controller owns the recovery work. */
|
|
27
|
-
export async function resumeRunController(context, input) {
|
|
30
|
+
export async function resumeRunController(context, input, validate) {
|
|
28
31
|
if (input.waitMs !== undefined && (!Number.isSafeInteger(input.waitMs) || input.waitMs < 0 || input.waitMs > 60_000))
|
|
29
32
|
throw new AppError("usage", "--wait-ms must be between 0 and 60000", 2);
|
|
30
|
-
const intent =
|
|
33
|
+
const intent = prepareControllerResume(context, input, validate)();
|
|
31
34
|
const manifest = JSON.parse(intent.controller.manifest_json);
|
|
32
35
|
const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: manifest.resource_home } };
|
|
33
36
|
await spawnController(resourceContext, intent.controller);
|
|
@@ -44,7 +47,7 @@ export function recordControllerResume(context, input) {
|
|
|
44
47
|
}
|
|
45
48
|
/** Verify immutable recovery evidence before a caller reserves the registry
|
|
46
49
|
* writer. The returned operation rechecks SQL ownership under the RUN CAS. */
|
|
47
|
-
export function prepareControllerResume(context, input) {
|
|
50
|
+
export function prepareControllerResume(context, input, validate) {
|
|
48
51
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !input.controlId.trim())
|
|
49
52
|
throw new AppError("usage", "Resume requires request-id and the exact source control", 2);
|
|
50
53
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -97,6 +100,8 @@ export function prepareControllerResume(context, input) {
|
|
|
97
100
|
throw new AppError("runtime_request_conflict", "Resume request id has different inputs", 1);
|
|
98
101
|
return { controller: requireController(context, row.controller_id), operationId };
|
|
99
102
|
}
|
|
103
|
+
if (validate && !validate())
|
|
104
|
+
throw new AppError("run_control_superseded", "Technical recovery evidence changed before resume admission", 1);
|
|
100
105
|
const current = recoveryGuard(context, project.id, run.id);
|
|
101
106
|
if (current?.recovery_id !== guard.recovery_id || current.generation !== guard.generation || current.status !== "sealed" || current.capture_path !== guard.capture_path)
|
|
102
107
|
throw new AppError("run_control_superseded", "Control changed before resume could be accepted", 1);
|
|
@@ -146,6 +151,39 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
146
151
|
assertRecoveryOwner();
|
|
147
152
|
const readDaemon = () => readNativeDaemonState(session.state_dir);
|
|
148
153
|
const { state: retired, process: record } = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id, ownerId: session.owner.owner_id });
|
|
154
|
+
const run = requireRun(context, row.project_id, row.run_id);
|
|
155
|
+
const boundary = JSON.parse(run.index_json).stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
|
|
156
|
+
if (boundary && technicalDependencyControl(context, row.project_id, row.run_id, recovery.control_id) && !scopeControl) {
|
|
157
|
+
if (!managedDaemonExecutionAbsent(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id, ownerId: session.owner.owner_id }))
|
|
158
|
+
throw new AppError("recovery_native_shutdown_unproven", "Technical boundary recovery still has an unknown or live owned writer", 1);
|
|
159
|
+
const assertPublication = prepareControllerPublication(executionContext, row, assertPhysicalOwner);
|
|
160
|
+
context.db.writeTransaction(() => {
|
|
161
|
+
assertPublication();
|
|
162
|
+
assertRecoveryOwner(false);
|
|
163
|
+
if (requireRun(context, row.project_id, row.run_id).index_json !== run.index_json)
|
|
164
|
+
throw new AppError("controller_boundary_changed", "Accepted boundary changed before technical continuation", 1);
|
|
165
|
+
const now = context.now();
|
|
166
|
+
if (context.db.run("UPDATE run_recovery_guards SET status='recovered',resumed_at=?,updated_at=? WHERE recovery_id=? AND generation=? AND status='sealed'", [now, now, recovery.recovery_id, row.generation]).changes !== 1)
|
|
167
|
+
throw new AppError("run_recovery_generation_stale", "Technical boundary no longer owns its sealed recovery", 1);
|
|
168
|
+
if (context.db.run("UPDATE run_controls SET status='resumed',updated_at=? WHERE control_id=? AND status='resuming'", [now, recovery.control_id]).changes !== 1)
|
|
169
|
+
throw new AppError("run_control_superseded", "Technical recovery control changed before continuation", 1);
|
|
170
|
+
const receipt = { schema_id: "dd-flow/technical-boundary-resume@1", resumed: true, recovery_id: recovery.recovery_id, stage: boundary.stage, attempt: boundary.attempt, native_cleanup_confirmed: retired.shutdown_state === "clean", native_model_call: false };
|
|
171
|
+
if (context.db.run("UPDATE run_controller_operations SET status='completed',receipt_json=?,updated_at=? WHERE operation_id=? AND controller_id=? AND generation=? AND status='accepted'", [JSON.stringify(receipt), now, recovery.operation_id, row.controller_id, row.generation]).changes !== 1)
|
|
172
|
+
throw new AppError("run_control_superseded", "Technical recovery operation changed before continuation", 1);
|
|
173
|
+
session.retired = true;
|
|
174
|
+
session.cleanup_pending = retired.shutdown_state !== "clean";
|
|
175
|
+
state.current_session = null;
|
|
176
|
+
state.last_continuation = null;
|
|
177
|
+
delete state.last_receipt;
|
|
178
|
+
delete state.recovery;
|
|
179
|
+
setFlowRunRecoveryPaused(context, row.project_id, row.run_id, false);
|
|
180
|
+
saveState(context, row, state);
|
|
181
|
+
appendEvent(context, row.controller_id, "technical_boundary_resumed", { ...receipt, control_id: recovery.control_id, state_dir: session.state_dir });
|
|
182
|
+
});
|
|
183
|
+
if (retired.shutdown_state !== "clean")
|
|
184
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "native_cleanup_pending_after_execution_exit", message: "The accepted Stage continues in a fresh Session; old native cleanup remains unconfirmed.", phase: "recovery", generation: row.generation, resource_identity: session.state_dir, diagnostic_reference: session.state_dir, consequence: "cleanup_pending" });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
149
187
|
const call = (args, operationId) => controllerAdapter(executionContext, row, manifest, session, args, assertRecoveryOwner, operationId);
|
|
150
188
|
if (processTreeIsAlive(record)) {
|
|
151
189
|
const stopped = await call(["daemon", "stop", "--state-dir", session.state_dir, "--json"]);
|
|
@@ -157,8 +195,10 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
157
195
|
assertRecoveryOwner();
|
|
158
196
|
}
|
|
159
197
|
}
|
|
160
|
-
if (processTreeIsAlive(record)
|
|
161
|
-
throw new AppError("recovery_native_shutdown_unproven", "The previous native daemon
|
|
198
|
+
if (processTreeIsAlive(record))
|
|
199
|
+
throw new AppError("recovery_native_shutdown_unproven", "The previous native daemon still owns execution", 1);
|
|
200
|
+
if (readDaemon().shutdown_state !== "clean")
|
|
201
|
+
throw new AppError("recovery_native_shutdown_unproven", "An unfinished coordinator requires its retained native Session to be cleanly resumable", 1);
|
|
162
202
|
assertRecoveryOwner();
|
|
163
203
|
session.stopped = false;
|
|
164
204
|
saveState(context, row, state);
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { AppError, errorRecord as structuredErrorRecord } from "../shared/errors.js";
|
|
3
3
|
import { runtimeHarness } from "./execution-policy.js";
|
|
4
4
|
import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
|
|
5
|
+
import { readTechnicalNotes } from "./technical-notes.js";
|
|
5
6
|
export function requireRun(context, projectId, runId) {
|
|
6
7
|
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
8
|
if (!run?.run_root)
|
|
@@ -14,10 +15,10 @@ export function requireController(context, id) {
|
|
|
14
15
|
throw new AppError("not_found", "Managed controller was not found", 1, { controller_id: id });
|
|
15
16
|
return row;
|
|
16
17
|
}
|
|
17
|
-
export function controllerReceipt(row) {
|
|
18
|
+
export function controllerReceipt(row, context) {
|
|
18
19
|
const state = JSON.parse(row.state_json);
|
|
19
20
|
const manifest = JSON.parse(row.manifest_json);
|
|
20
|
-
return { ok: true, schema_id: "dd-flow/run-controller-receipt@1", controller: { controller_id: row.controller_id, request_id: row.request_id, project_id: row.project_id, run_id: row.run_id, generation: row.generation, runtime_budget: manifest.budget ?? null, status: row.status, process_id: row.process_id, owner_pid: row.owner_pid, state_dir: row.state_dir, stage: state.stage, sessions: state.sessions.map(session => ({ session_id: session.id, harness: runtimeHarness(session.profile), profile_id: session.profile.id, state_dir: session.state_dir, journal: session.journal, stopped: session.stopped === true })), error: row.last_error_json ? JSON.parse(row.last_error_json) : null } };
|
|
21
|
+
return { ok: true, schema_id: "dd-flow/run-controller-receipt@1", ...(context ? { technical_notes: readTechnicalNotes(context, row.project_id, row.run_id) } : {}), controller: { controller_id: row.controller_id, request_id: row.request_id, project_id: row.project_id, run_id: row.run_id, generation: row.generation, runtime_budget: manifest.budget ?? null, status: row.status, process_id: row.process_id, owner_pid: row.owner_pid, state_dir: row.state_dir, stage: state.stage, sessions: state.sessions.map(session => ({ session_id: session.id, harness: runtimeHarness(session.profile), profile_id: session.profile.id, state_dir: session.state_dir, journal: session.journal, stopped: session.stopped === true, ...(session.cleanup_pending ? { cleanup_pending: true } : {}) })), error: row.last_error_json ? JSON.parse(row.last_error_json) : null } };
|
|
21
22
|
}
|
|
22
23
|
/** Read-only causal diagnostics, not a settlement or capture authorization. */
|
|
23
24
|
export function terminalControllerFinalization(context, project, runId, runStatus) {
|