@deksden-com/dd-flow-cli 0.9.0-beta.145 → 0.9.0-beta.149
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 +71 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +6 -2
- package/dist/cli/command-metadata.js +18 -2
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/memory-flow-admission.js +4 -2
- package/dist/cli/memory-init.js +5 -2
- package/dist/cli/run-cli.js +16 -15
- package/dist/cli.js +6 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +306 -34
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +70 -28
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/code-checks.js +23 -15
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +13 -6
- package/dist/services/controller-memory-worker.js +3 -2
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +22 -3
- package/dist/services/descriptor-stage-coordinator.js +37 -4
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/eval-snapshots.js +85 -29
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/flow-catalog.js +6 -1
- package/dist/services/frozen-engine.js +37 -2
- package/dist/services/hybrid-contracts.js +2 -0
- package/dist/services/installed-flow-source.js +25 -3
- package/dist/services/lifecycle-invocations.js +147 -34
- package/dist/services/memory-init-contexts.js +16 -0
- package/dist/services/memory-init-controller-start.js +12 -4
- package/dist/services/memory-init-lint-capability.js +5 -5
- package/dist/services/memory-init-native-capacity.js +4 -4
- package/dist/services/memory-init-preflight.js +10 -16
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +4 -3
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-verification-checks.js +15 -4
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/provider-capacity.js +43 -38
- package/dist/services/release-cache.js +8 -0
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +2 -1
- package/dist/services/run-controller-recovery.js +2 -1
- package/dist/services/run-controller.js +85 -15
- package/dist/services/run-fork.js +1 -1
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/stage-handlers.js +16 -3
- package/dist/services/stage-pause.js +4 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +31 -1
- package/dist/services/work-registry.js +37 -6
- package/dist/services/workspace-bootstrap.js +2 -1
- package/package.json +6 -3
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
|
+
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent } from "./lifecycle-invocations.js";
|
|
2
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
2
3
|
export function recoveryPacketMatchesStage(packet, scope) {
|
|
3
4
|
return packet.schema_id === 'dd-flow/root-recovery-packet@1' && packet.continuation === 'unfinished_work'
|
|
4
5
|
&& packet.run_id === scope.runId && packet.generation === scope.generation && packet.stage === scope.stage
|
|
5
6
|
&& packet.work_id === scope.workId && packet.paused === false;
|
|
6
7
|
}
|
|
7
8
|
import crypto from "node:crypto";
|
|
9
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
8
10
|
import fs from "node:fs";
|
|
9
11
|
import path from "node:path";
|
|
10
12
|
import { setTimeout as delay } from "node:timers/promises";
|
|
11
13
|
import { fileURLToPath } from "node:url";
|
|
12
14
|
import { quote } from "shell-quote";
|
|
15
|
+
import { retainedBrowserEnvironment, validateCheckEnvironment } from "./check-environment.js";
|
|
13
16
|
import { ensureRunControllerStorage, getDatabase } from "../storage/database.js";
|
|
14
17
|
import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
|
|
15
18
|
import { publishImmutableInput } from "../storage/immutable-input.js";
|
|
@@ -20,7 +23,7 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
20
23
|
import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
|
|
21
24
|
import { getFlowRunStatus, setFlowRunRecoveryPaused, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
22
25
|
import { loadExternalStageContext } from "./stage-context.js";
|
|
23
|
-
import { flowCommand } from "./stage-pause.js";
|
|
26
|
+
import { flowCommand, stageResumeSourcePath } from "./stage-pause.js";
|
|
24
27
|
import { sameSessionProfile, runtimeHarness } from "./execution-policy.js";
|
|
25
28
|
import { selectControllerExecution } from "./controller-execution.js";
|
|
26
29
|
import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
|
|
@@ -33,6 +36,8 @@ import { initCodexHome } from "./hooks.js";
|
|
|
33
36
|
import { launchExternalWork } from "./external-work-launch.js";
|
|
34
37
|
import { runControllerMemoryTask } from "./controller-memory-task.js";
|
|
35
38
|
import { controllerNativeChildren, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
|
|
39
|
+
import { publishPromptDelivery } from "./prompt-delivery.js";
|
|
40
|
+
import { publishStagePacket } from "./stage-report-renderer.js";
|
|
36
41
|
import { claimQueuedMergeRequest } from "./merge-server.js";
|
|
37
42
|
import { prepareVnextCodeWorkspace } from "./vnext-code.js";
|
|
38
43
|
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
@@ -135,7 +140,8 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
135
140
|
}
|
|
136
141
|
const manifest = { schema_id: routing.kind === "descriptor" ? "dd-flow/run-controller-manifest@2" : "dd-flow/run-controller-manifest@1", request,
|
|
137
142
|
...(routing.kind === "descriptor" ? { execution_binding: routing.binding } : { execution_profile: routing.legacy }),
|
|
138
|
-
contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget
|
|
143
|
+
contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget,
|
|
144
|
+
...(retainedBrowserEnvironment(context.env) ? { check_environment: retainedBrowserEnvironment(context.env) } : {}) };
|
|
139
145
|
const token = crypto.randomUUID();
|
|
140
146
|
const now = context.now();
|
|
141
147
|
const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
|
|
@@ -533,7 +539,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
533
539
|
const routing = selectControllerExecution(context, requireRun(context, row.project_id, row.run_id), manifest);
|
|
534
540
|
const policy = routing.policy;
|
|
535
541
|
const profiles = routing.profiles;
|
|
536
|
-
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 } };
|
|
542
|
+
let executionContext = { ...context, env: { ...context.env, ...validateCheckEnvironment(manifest.check_environment), 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 } };
|
|
537
543
|
const bindInvocationScope = (session) => {
|
|
538
544
|
executionContext = managedInvocationContext(executionContext, { harness: runtimeHarness(session.profile), stateDir: session.state_dir, sessionId: session.id,
|
|
539
545
|
projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
|
|
@@ -634,9 +640,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
634
640
|
const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
|
|
635
641
|
const confirmStopped = async (stateDir, receipt) => {
|
|
636
642
|
const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
|
|
637
|
-
if (receipt.stopped !== true || receipt.clean !== true)
|
|
638
|
-
throw fail();
|
|
639
643
|
const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
|
|
644
|
+
if (!binding.state.daemon_id || receipt.stopped !== true || !nativeShutdownConfirmed(receipt, { daemonId: binding.state.daemon_id }))
|
|
645
|
+
throw fail();
|
|
640
646
|
const deadline = performance.now() + 30_000;
|
|
641
647
|
while (processTreeIsAlive(binding.process) && performance.now() < deadline)
|
|
642
648
|
await delay(100);
|
|
@@ -681,6 +687,45 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
681
687
|
if (!pending)
|
|
682
688
|
await reconcilePromptPredecessor(session);
|
|
683
689
|
const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
690
|
+
const hitlText = async () => {
|
|
691
|
+
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
692
|
+
const answer = context.db.get("SELECT input_json,receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status IN ('accepted','completed')", [answerId, row.controller_id]);
|
|
693
|
+
const accepted = answer && JSON.parse(answer.input_json);
|
|
694
|
+
const packet = answer?.receipt_json && JSON.parse(answer.receipt_json).hitl_packet;
|
|
695
|
+
if (!packet || !accepted || packet.generation !== row.generation || packet.pause_id !== accepted.pause_id || packet.stage !== accepted.stage || packet.work_id !== accepted.work_id || packet.answer_file !== accepted.file || packet.answer_sha256 !== accepted.sha256)
|
|
696
|
+
throw new AppError("controller_hitl_packet_invalid", "Accepted HITL continuation lost its exact binding", 1);
|
|
697
|
+
const issued = context.db.get("SELECT id,status,scope_json,command FROM lifecycle_invocations WHERE id = ?", [packet.invocation_id ?? null]);
|
|
698
|
+
if (!issued || issued.scope_json !== executionContext.env.DD_FLOW_INVOCATION_SCOPE)
|
|
699
|
+
throw new AppError("controller_hitl_packet_invalid", "Accepted HITL command belongs to another scope", 1);
|
|
700
|
+
if (issued.status === "executing" || issued.status === "settled") {
|
|
701
|
+
const recovered = await awaitLifecycleInvocation(executionContext, { id: issued.id, command: issued.command, assertCurrent: scope => assertLifecycleInvocationCurrent(executionContext, scope, issued.command) });
|
|
702
|
+
if (!recovered.replay)
|
|
703
|
+
throw new AppError("controller_hitl_reconciliation_required", "Accepted resume is not committed", 1);
|
|
704
|
+
const outcome = recovered.outcome;
|
|
705
|
+
if (!outcome.result)
|
|
706
|
+
throw new AppError("controller_hitl_reconciliation_required", "Accepted resume has no committed continuation", 1);
|
|
707
|
+
if (outcome.result.resume_source_path !== packet.resume_source_path || typeof outcome.result.answer_path !== "string")
|
|
708
|
+
throw new AppError("controller_hitl_packet_invalid", "Committed resume differs from the accepted packet locator", 1);
|
|
709
|
+
publishStagePacket(executionContext, row.project_id, row.run_id, packet.stage, outcome.result.answer_path);
|
|
710
|
+
publishStagePacket(executionContext, row.project_id, row.run_id, packet.stage, packet.resume_source_path);
|
|
711
|
+
const bytes = fs.readFileSync(outcome.result.answer_path);
|
|
712
|
+
if (digestBytes(bytes) !== packet.answer_sha256)
|
|
713
|
+
throw new AppError("context_checksum_mismatch", "Committed answer differs from its accepted bytes", 1);
|
|
714
|
+
fs.mkdirSync(path.dirname(packet.answer_file), { recursive: true, mode: 0o700 });
|
|
715
|
+
publishImmutableInput(packet.answer_file, bytes, "hitl_answer_conflict");
|
|
716
|
+
publishPromptDelivery(outcome.result);
|
|
717
|
+
const delivery = outcome.result.prompt_delivery;
|
|
718
|
+
const promptPath = delivery?.path ?? outcome.result.prompt_path;
|
|
719
|
+
if (typeof promptPath !== "string" || !path.isAbsolute(promptPath))
|
|
720
|
+
throw new AppError("controller_hitl_packet_invalid", "Committed resume has no absolute prompt delivery", 1);
|
|
721
|
+
// Keep continuation bytes stable across a retained overload episode.
|
|
722
|
+
// Its conditional instructions point at this restored exact custody.
|
|
723
|
+
return controllerStageResumePrompt(packet);
|
|
724
|
+
}
|
|
725
|
+
if (issued.status !== "issued" || digestBytes(fs.readFileSync(packet.answer_file)) !== packet.answer_sha256)
|
|
726
|
+
throw new AppError("context_checksum_mismatch", "Accepted HITL answer or command changed before resume", 1);
|
|
727
|
+
return controllerStageResumePrompt(packet);
|
|
728
|
+
};
|
|
684
729
|
const send = (value, id) => {
|
|
685
730
|
const retained = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [id, row.controller_id, row.generation]);
|
|
686
731
|
if (retained) {
|
|
@@ -714,7 +759,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
714
759
|
promptPending = true;
|
|
715
760
|
observedPromptOperation = firstOperationId;
|
|
716
761
|
try {
|
|
717
|
-
receipt = await send(text, firstOperationId);
|
|
762
|
+
receipt = await send(hitl ? await hitlText() : text, firstOperationId);
|
|
718
763
|
}
|
|
719
764
|
finally {
|
|
720
765
|
promptPending = false;
|
|
@@ -722,9 +767,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
722
767
|
}
|
|
723
768
|
}
|
|
724
769
|
catch (error) {
|
|
725
|
-
const
|
|
726
|
-
if (hitl && !resumeCommand?.includes(" stage resume "))
|
|
727
|
-
throw error;
|
|
770
|
+
const hitlContinuationText = hitl && terminalProviderOverload(error, session.id) ? await hitlText() : undefined;
|
|
728
771
|
const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters[runtimeHarness(session.profile)].executable,
|
|
729
772
|
args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
|
|
730
773
|
const resumed = await continueProviderCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
|
|
@@ -781,7 +824,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
781
824
|
},
|
|
782
825
|
retainIntent: intent => retainControllerCapacityIntent(context, row, state, intent, session.id, kind ?? "stage", assertOwner),
|
|
783
826
|
prompt: (id, value) => send(value, id),
|
|
784
|
-
...(
|
|
827
|
+
...(hitlContinuationText ? { continuationText: hitlContinuationText } : {}) });
|
|
785
828
|
if (!resumed)
|
|
786
829
|
throw error;
|
|
787
830
|
receipt = resumed;
|
|
@@ -845,6 +888,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
845
888
|
await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
|
|
846
889
|
continue;
|
|
847
890
|
}
|
|
891
|
+
if (next.kind !== "paused" && state.current_session !== null) {
|
|
892
|
+
const committed = context.db.get("SELECT operation_id,receipt_json FROM run_controller_operations WHERE controller_id = ? AND status = 'accepted' AND json_extract(input_json,'$.action') = 'user_answer' AND json_extract(receipt_json,'$.hitl_packet.generation') = ? AND json_extract(receipt_json,'$.hitl_packet.stage') = ? ORDER BY created_at LIMIT 1", [row.controller_id, row.generation, state.stage]);
|
|
893
|
+
if (committed) {
|
|
894
|
+
const packet = JSON.parse(committed.receipt_json).hitl_packet;
|
|
895
|
+
const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(packet), `${committed.operation_id}:prompt:${row.generation}`, "hitl");
|
|
896
|
+
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = json_set(receipt_json,'$.delivered',json('true'),'$.session_id',?), updated_at = ? WHERE operation_id = ? AND status = 'accepted'", [adapterSessionId(receipt), context.now(), committed.operation_id]);
|
|
897
|
+
state.last_continuation = null;
|
|
898
|
+
saveState(context, row, state);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
848
902
|
const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
|
|
849
903
|
if (state.current_session !== null && (completed || next.stage !== state.stage) && childObserver(state.sessions[state.current_session])?.failed().length)
|
|
850
904
|
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids completing or advancing its Stage", 1);
|
|
@@ -890,13 +944,29 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
890
944
|
assertOwner();
|
|
891
945
|
// The accepted answer is logical intent; only its generation-bound
|
|
892
946
|
// adapter operation represents a physical dispatch that stop must drain.
|
|
893
|
-
|
|
894
|
-
const
|
|
947
|
+
let invocationId;
|
|
948
|
+
const capture = executionContext.captureLifecycleAssignment;
|
|
949
|
+
executionContext.captureLifecycleAssignment = id => { invocationId = id; capture?.(id); };
|
|
950
|
+
let command;
|
|
951
|
+
try {
|
|
952
|
+
command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage resume ${run.id} --stage ${next.stage} --work ${input.work_id} --project-root ${quote([run.project_root])} --answer-file ${quote([input.file])} --json`, { replaceUnobservedAnswer: true });
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
if (capture)
|
|
956
|
+
executionContext.captureLifecycleAssignment = capture;
|
|
957
|
+
else
|
|
958
|
+
delete executionContext.captureLifecycleAssignment;
|
|
959
|
+
}
|
|
960
|
+
if (!invocationId)
|
|
961
|
+
throw new AppError("controller_hitl_packet_invalid", "Accepted resume did not capture its issued identity", 1);
|
|
962
|
+
const packet = { pause_id: pause.id, stage: next.stage, work_id: input.work_id, answer_file: input.file, answer_sha256: input.sha256, command, generation: row.generation, invocation_id: invocationId, resume_source_path: stageResumeSourcePath(pause, fs.readFileSync(input.file, "utf8")) };
|
|
963
|
+
context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'accepted'", [JSON.stringify({ hitl_packet: packet }), context.now(), answer.operation_id]);
|
|
964
|
+
const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(packet), `${answer.operation_id}:prompt:${row.generation}`, "hitl");
|
|
895
965
|
const afterAnswer = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
|
|
896
966
|
if (afterAnswer.index.stage_runs.some(stage => stage.status === "paused" && stage.pause?.id === pause.id)) {
|
|
897
967
|
throw new AppError("controller_answer_not_applied", "The answer Turn completed without resuming the assigned HITL pause", 1, { pause_id: pause.id, operation_id: answer.operation_id, stage: next.stage, effect: "no_progress" });
|
|
898
968
|
}
|
|
899
|
-
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json =
|
|
969
|
+
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = json_set(receipt_json,'$.delivered',json('true'),'$.session_id',?), updated_at = ? WHERE operation_id = ?", [adapterSessionId(receipt), context.now(), answer.operation_id]);
|
|
900
970
|
state.last_continuation = null;
|
|
901
971
|
saveState(context, row, state);
|
|
902
972
|
setStatus("running");
|
|
@@ -1129,7 +1199,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1129
1199
|
continue;
|
|
1130
1200
|
}
|
|
1131
1201
|
}
|
|
1132
|
-
const memoryFlow = ["mb-init", "mb-upgrade"].includes(run.flow_kind);
|
|
1202
|
+
const memoryFlow = ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind);
|
|
1133
1203
|
const fanoutInput = { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0 };
|
|
1134
1204
|
const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
|
|
1135
1205
|
: await nextControllerFanout(executionContext, fanoutInput);
|
|
@@ -28,7 +28,7 @@ export function prepareRunFork(input) {
|
|
|
28
28
|
}
|
|
29
29
|
prepareForkEngine(engineVersion, input.integrityChecksum);
|
|
30
30
|
const manifest = prepareEvalRunSnapshot(snapshot);
|
|
31
|
-
if (
|
|
31
|
+
if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@7"].includes(manifest.schema_id) || manifest.purpose !== "stage_entry" || typeof manifest.stage_entry !== "string" || typeof manifest.run_id !== "string") {
|
|
32
32
|
throw new AppError("fork_checkpoint_invalid", "Fork requires a sealed stage-entry snapshot", 2, { snapshot });
|
|
33
33
|
}
|
|
34
34
|
const operational = prepareSnapshotOperationalDecision({ snapshot, runId: manifest.run_id, projectRoot: path.join(output, "project"), now: new Date().toISOString(), ...(input.operationalDecisionFile ? { file: input.operationalDecisionFile } : {}) });
|
package/dist/services/runs.js
CHANGED
|
@@ -27,6 +27,7 @@ import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
|
27
27
|
import { admitBundledFlow, bundledHandlerContext } from "./stage-handlers.js";
|
|
28
28
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
29
29
|
import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
|
|
30
|
+
import { readRunEngineBinding } from "./run-engine-bindings.js";
|
|
30
31
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
31
32
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
32
33
|
const allowedRunFlowKinds = [
|
|
@@ -36,6 +37,7 @@ const allowedRunFlowKinds = [
|
|
|
36
37
|
"mb-init",
|
|
37
38
|
"mb-upgrade",
|
|
38
39
|
"mb-audit",
|
|
40
|
+
"project-review",
|
|
39
41
|
"mb-distill",
|
|
40
42
|
"mb-upgrade-review",
|
|
41
43
|
"mb-sdlc-review",
|
|
@@ -1843,7 +1845,10 @@ export function readFlowRunExecutionBundle(context, input) {
|
|
|
1843
1845
|
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
|
|
1844
1846
|
}
|
|
1845
1847
|
const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
|
|
1846
|
-
const
|
|
1848
|
+
const relocations = runContextRelocations(context, run.project_id, run.id);
|
|
1849
|
+
const binding = relocations.length ? readRunEngineBinding(path.join(runArtifactRoot(run), "engine-binding.json")) : null;
|
|
1850
|
+
const relocation = binding?.source === "fork_upgrade" ? { binding, relocations } : undefined;
|
|
1851
|
+
const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission, relocation);
|
|
1847
1852
|
assertBundleRunIdentity(context, run, bundle);
|
|
1848
1853
|
if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
|
|
1849
1854
|
throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -16,15 +17,19 @@ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
|
16
17
|
import { harnessKey } from "./harness-config.js";
|
|
17
18
|
import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeouts.mjs";
|
|
18
19
|
import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
20
|
+
import { validateNativeLaunchEnvironment } from "../harness-runtime/lib/daemon-operations.mjs";
|
|
19
21
|
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
20
|
-
const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
|
|
21
22
|
function nativeLaunchEnvironment(context, saved) {
|
|
22
23
|
const env = { ...context.env };
|
|
23
24
|
for (const name of ["DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN", "DD_FLOW_DAEMON_ID", "DD_FLOW_RUNTIME_OWNER"])
|
|
24
25
|
delete env[name];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
try {
|
|
27
|
+
validateNativeLaunchEnvironment(saved);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new AppError("runtime_scope_native_launch_required", "Native launch has an unsupported or invalid configuration environment", 1);
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(saved)) {
|
|
28
33
|
if (saved[name] === null)
|
|
29
34
|
delete env[name];
|
|
30
35
|
else if (typeof saved[name] === "string")
|
|
@@ -460,8 +465,9 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
460
465
|
}
|
|
461
466
|
if (!launch || launch.schema_id !== "dd-flow/native-launch@1" || launch.executable !== source.runtime_owner?.adapter_executable || !path.isAbsolute(launch.executable) || !path.isAbsolute(launch.cwd) || !Array.isArray(launch.args) || launch.args[0] !== "daemon" || launch.args[1] !== "start" || launch.args[launch.args.indexOf("--state-dir") + 1] !== source.state_dir || !isDeepStrictEqual(launchOwner, source.runtime_owner))
|
|
462
467
|
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires the original owned launch contract", 1);
|
|
463
|
-
if (
|
|
468
|
+
if (launch.args.some(arg => typeof arg !== "string"))
|
|
464
469
|
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
|
|
470
|
+
nativeLaunchEnvironment(context, launch.env);
|
|
465
471
|
let dispatch = false;
|
|
466
472
|
const preparedInventory = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]) ? null : nativeInventoryVersion(source.state_dir);
|
|
467
473
|
const operation = db.writeTransaction(() => {
|
|
@@ -494,7 +500,7 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
494
500
|
assertOwner();
|
|
495
501
|
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
496
502
|
}
|
|
497
|
-
if (operation.receipt.stopped !== true || operation.receipt.
|
|
503
|
+
if (operation.receipt.stopped !== true || !nativeShutdownConfirmed(operation.receipt, { daemonId: source.daemon_id }) || state().shutdown_state !== "clean" || processTreeIsAlive(record))
|
|
498
504
|
return { ...source, ...operation, pending: true, reason: "native_retirement_settlement_required" };
|
|
499
505
|
assertOwner();
|
|
500
506
|
operation.status = "retired";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import os from "node:os";
|
|
@@ -30,7 +31,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
30
31
|
if (!state.daemon_id || !owner || owner.budget?.scope_id !== scopeId || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.state_dir !== metadata.state_dir || owner.adapter_executable !== metadata.adapter_executable || !retainedResourceHomeMatches(context, owner.resource_home) || (owner.project_id ?? null) !== record.project_id || (owner.run_id ?? null) !== record.run_id || (owner.work_id ?? null) !== record.work_id || !record.operation_id?.startsWith(`${owner.operation_id}:`) || state.resource_process?.id !== record.id || state.resource_process.lease_token !== record.lease_token || state.pid !== record.pid)
|
|
31
32
|
throw new AppError("runtime_scope_owner_unknown", "Native owner does not match its registration", 1);
|
|
32
33
|
const closed = state.shutdown_state === "clean" && state.active_tree === false && !processTreeIsAlive(record)
|
|
33
|
-
? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && operation.result
|
|
34
|
+
? retainedDaemonOperations(metadata.state_dir).find(operation => operation.daemon_id === state.daemon_id && operation.operation === "daemon.stop" && operation.state === "completed" && operation.result?.stopped === true && nativeShutdownConfirmed(operation.result, { daemonId: state.daemon_id ?? "" })) : undefined;
|
|
34
35
|
const operationId = closed ? String(closed.operation_id) : `scope-stop:${crypto.createHash("sha256").update(JSON.stringify([scopeId, record.id, state.daemon_id])).digest("hex")}`;
|
|
35
36
|
const requested = path.join(metadata.state_dir, "operations", crypto.createHash("sha256").update(operationId).digest("hex"), "requested.json");
|
|
36
37
|
const prior = fs.existsSync(requested);
|
|
@@ -56,7 +57,7 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
56
57
|
const finalOwner = managedProcessStatus(context).find(item => item.id === record.id);
|
|
57
58
|
if (!finalOwner || finalOwner.lease_token !== record.lease_token || finalOwner.pid !== record.pid || finalOwner.pid_started_at !== record.pid_started_at || after.resource_process?.id !== record.id || after.resource_process.lease_token !== record.lease_token || JSON.stringify(after.config?.runtime_owner) !== JSON.stringify(owner))
|
|
58
59
|
throw new AppError("runtime_scope_owner_changed", "Native ownership changed during control", 1);
|
|
59
|
-
if (result?.stopped !== true || result.
|
|
60
|
+
if (result?.stopped !== true || !nativeShutdownConfirmed(result, { daemonId: state.daemon_id }) || after.daemon_id !== state.daemon_id || after.shutdown_state !== "clean" || after.active_tree !== false || processTreeIsAlive(finalOwner))
|
|
60
61
|
throw new AppError("runtime_scope_not_settled", "Native close has not proved whole-tree settlement", 1);
|
|
61
62
|
const processes = managedProcessStatus(context).filter(item => managedProviderBelongsToDaemon(item, finalOwner, after) && item.operation_id?.startsWith(`${owner.operation_id}:`) && !processTreeIsAlive(item)).map(item => ({ process_id: item.id, native_settled: true, binding: managedProcessBinding(item, resourceHome(context)) }));
|
|
62
63
|
return { process_id: record.id, state_dir: metadata.state_dir, settled: true, native_settled: true, physical_stopped: true, operation_id: operationId, owner_operation_id: owner.operation_id, processes };
|
|
@@ -7,6 +7,7 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
|
7
7
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
8
|
import { resolveRun } from "./runs.js";
|
|
9
9
|
import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
|
|
10
|
+
import { retainedBrowserEnvironment } from "./check-environment.js";
|
|
10
11
|
import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, processTreeIsAlive, registerManagedProcess, reservePorts, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
11
12
|
export function prepareRuntimeService(context, input) {
|
|
12
13
|
validateObservationDuration(input.timeoutMs);
|
|
@@ -33,7 +34,7 @@ export async function startRuntimeService(context, input) {
|
|
|
33
34
|
const save = () => { const temporary = `${receiptPath}.${process.pid}.tmp`; fs.writeFileSync(temporary, JSON.stringify(receipt, null, 2)); fs.renameSync(temporary, receiptPath); };
|
|
34
35
|
try {
|
|
35
36
|
const allocation = await reservePorts(context, { ownerId: record.owner_id, processId: record.id, names: input.ports });
|
|
36
|
-
const environment = { ...codeExecutionEnvironment(run.workspace_root), ...Object.fromEntries(Object.entries(allocation.ports).map(([name, port]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(port)])), DD_FLOW_EVIDENCE_DIR: directory, DD_FLOW_CHECK_COMPLETION_FILE: path.join(directory, "completion.json") };
|
|
37
|
+
const environment = { ...codeExecutionEnvironment(run.workspace_root, retainedBrowserEnvironment(context.env)), ...Object.fromEntries(Object.entries(allocation.ports).map(([name, port]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(port)])), DD_FLOW_EVIDENCE_DIR: directory, DD_FLOW_CHECK_COMPLETION_FILE: path.join(directory, "completion.json") };
|
|
37
38
|
const quoted = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
38
39
|
Object.assign(receipt, { ports: allocation.ports, environment: Object.fromEntries(Object.entries(environment).filter(([key]) => key.startsWith("DD_FLOW_PORT_"))), stop_command: `DD_FLOW_HOME=${quoted(context.ddFlowHome)} DD_FLOW_RESOURCE_HOME=${quoted(resourceHome(context))} dd-flow runtime process stop --id ${record.id} --lease-token ${record.lease_token} --json` });
|
|
39
40
|
save();
|
|
@@ -10,6 +10,8 @@ import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
|
|
|
10
10
|
import { vnextStages } from "../domain/stage-catalog.js";
|
|
11
11
|
import { sdlcHandlerIdentities as ids } from "../domain/stage-handler-identities.js";
|
|
12
12
|
import { memoryInitStageHandlers, memoryUpgradeStageHandlers } from "./memory-stage-dispatch.js";
|
|
13
|
+
import { diagnosticStageHandlers } from "./diagnostic-stage-handlers.js";
|
|
14
|
+
import { isDiagnosticFlow, diagnosticFlowStages } from "../domain/diagnostic-flows.js";
|
|
13
15
|
/** Engine-owned code, not a plugin registration surface or executable descriptor expression. */
|
|
14
16
|
export const sdlcStageHandlers = Object.freeze({
|
|
15
17
|
specify: Object.freeze({ ...ids.specify, entry: launchVnextSpecify, finish: finishVnextSpecifyStage }),
|
|
@@ -23,8 +25,8 @@ export const sdlcStageHandlers = Object.freeze({
|
|
|
23
25
|
/** A fresh inventory cannot mutate the registered handler functions. */
|
|
24
26
|
export function bundledHandlerContext(blobs) {
|
|
25
27
|
return {
|
|
26
|
-
entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers)].map(handler => handler.entryId)),
|
|
27
|
-
finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers)].map(handler => handler.finishId)), blobs
|
|
28
|
+
entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers), ...diagnosticStageHandlers].map(handler => handler.entryId)),
|
|
29
|
+
finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers), ...diagnosticStageHandlers].map(handler => handler.finishId)), blobs
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
/** Handler identity is also stage/flow scoped: a known CODE handler is not a memory handler. */
|
|
@@ -64,9 +66,20 @@ export function admitBundledFlow(input, sha256, blobs) {
|
|
|
64
66
|
throw new AppError("flow_handler_scope_invalid", "Memory init stage requires its own registered result schema", 2, { phase: "prepare", effect: "no_effect", stage: stage.id });
|
|
65
67
|
}
|
|
66
68
|
}
|
|
69
|
+
if (isDiagnosticFlow(flow.descriptor.id)) {
|
|
70
|
+
const stages = diagnosticFlowStages[flow.descriptor.id];
|
|
71
|
+
if (flow.descriptor.entry_stage !== "preflight" || flow.descriptor.terminal_stages.length !== 1 || flow.descriptor.terminal_stages[0] !== "report"
|
|
72
|
+
|| flow.descriptor.stages.length !== stages.length || flow.descriptor.edges.length !== stages.length - 1
|
|
73
|
+
|| flow.descriptor.stages.some((stage, index) => stage.id !== stages[index] || stage.directory !== `${String(index + 1).padStart(2, "0")}-${stage.id}`)
|
|
74
|
+
|| stages.slice(0, -1).some((stage, index) => !flow.descriptor.edges.some(edge => edge.from === stage && edge.to === stages[index + 1] && edge.on === "accepted"))
|
|
75
|
+
|| flow.descriptor.stages.some(stage => JSON.parse(flow.stageBlob(stage.id, "result_schema").toString("utf8")).$id !== "dd-flow/diagnostic-stage-result@1")) {
|
|
76
|
+
throw new AppError("flow_handler_scope_invalid", "Diagnostic handlers require their registered four-stage layout and result schema", 2, { phase: "prepare", effect: "no_effect" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
67
79
|
for (const stage of flow.descriptor.stages) {
|
|
68
80
|
const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers)
|
|
69
|
-
: flow.descriptor.id === "mb-upgrade" ? Object.values(memoryUpgradeStageHandlers)
|
|
81
|
+
: flow.descriptor.id === "mb-upgrade" ? Object.values(memoryUpgradeStageHandlers)
|
|
82
|
+
: diagnosticStageHandlers.filter(handler => handler.flowId === flow.descriptor.id);
|
|
70
83
|
const handler = handlers.find(candidate => candidate.stage === stage.id);
|
|
71
84
|
if (!handler || stage.entry_handler !== handler.entryId || stage.finish_handler !== handler.finishId) {
|
|
72
85
|
throw new AppError("flow_handler_scope_invalid", "Registered handlers do not belong to this flow stage", 2, { phase: "prepare", effect: "no_effect", flow_id: flow.descriptor.id, stage: stage.id });
|
|
@@ -19,6 +19,9 @@ import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
|
19
19
|
import { latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
20
20
|
import { acceptedIntentReviewFingerprint, prepareAcceptedIntentSources, validatePreparedAcceptedIntentReview } from "./accepted-intent.js";
|
|
21
21
|
export { flowCommand } from "./runtime-command.js";
|
|
22
|
+
export function stageResumeSourcePath(pause, answer) {
|
|
23
|
+
return path.join(path.dirname(pause.question_path), `resume-${crypto.createHash("sha256").update(`${pause.id}\0${answer}`).digest("hex")}.json`);
|
|
24
|
+
}
|
|
22
25
|
export function prepareStagePause(context, input) {
|
|
23
26
|
if (typeof input.question !== "string" || !input.question.trim())
|
|
24
27
|
throw new AppError("validation", "stage pause requires a non-empty user question", 2);
|
|
@@ -119,7 +122,7 @@ export function resumeStageAfterUser(context, input) {
|
|
|
119
122
|
// Capture the exact continuation inputs and already-issued commands before
|
|
120
123
|
// accepting the answer. A publication retry must not issue new authority.
|
|
121
124
|
const retryCommands = lifecycleRetryCommands(context, stage.stage);
|
|
122
|
-
const resumeSourcePath =
|
|
125
|
+
const resumeSourcePath = stageResumeSourcePath(pause, input.answer);
|
|
123
126
|
let review;
|
|
124
127
|
let reviewPreparationError;
|
|
125
128
|
const previousAnswer = context.db.get("SELECT MAX(id) AS id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ?", [project.id, run.id])?.id ?? 0;
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
export const canonicalOnlyFlowNames = Object.freeze(["mb-init.md", "mb-upgrade.md", "mb-distill.md"]);
|
|
5
|
+
export const canonicalOnlyFlowNames = Object.freeze(["mb-init.md", "mb-upgrade.md", "mb-distill.md", "mb-audit.md", "review.md"]);
|
|
6
6
|
export function resolveStatusProjectRoot(input) {
|
|
7
7
|
if (input.requestedRoot) {
|
|
8
8
|
const absolute = path.resolve(input.requestedRoot);
|
|
@@ -194,7 +194,7 @@ export async function finishVnextCode(context, input) {
|
|
|
194
194
|
const retainedMatrix = latestRunVerificationMatrix(context, project.id, run.id, stage);
|
|
195
195
|
const retainedMatrixInstruction = verificationMatrixInstruction(retainedMatrix?.metadata ?? null, home, "the current semantic verification and repair");
|
|
196
196
|
if (verification.verdict === "blocked") {
|
|
197
|
-
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke
|
|
197
|
+
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke next.finish_command. Do not repeat while the blocker remains unresolved.\n${retainedMatrixInstruction}`, next: { finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) } };
|
|
198
198
|
}
|
|
199
199
|
if (verification.verdict === "passed" && verification.unresolved.length > 0) {
|
|
200
200
|
throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
|
|
@@ -209,7 +209,7 @@ export async function finishVnextCode(context, input) {
|
|
|
209
209
|
verification,
|
|
210
210
|
objective: verification.summary
|
|
211
211
|
});
|
|
212
|
-
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke
|
|
212
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke next.finish_command.\n${retainedMatrixInstruction}`, next: { finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) } };
|
|
213
213
|
}
|
|
214
214
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
215
215
|
if (input.retryCheckId) {
|
|
@@ -15,6 +15,8 @@ import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/del
|
|
|
15
15
|
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
16
16
|
import { selectMemoryCompositionRegistration } from "./memory-composition-registration.js";
|
|
17
17
|
import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
18
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
19
|
+
import { readRegisteredDiagnosticGraph } from "./diagnostic-work-registration.js";
|
|
18
20
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
19
21
|
/** External concurrency is policy, never evidence of native child qualification. */
|
|
20
22
|
export function getStageFanoutCapacity(context, input) {
|
|
@@ -176,6 +178,30 @@ export function memoryStageFanoutDescriptor(context, input) {
|
|
|
176
178
|
}
|
|
177
179
|
return { schema_id: "dd-flow/work-fanout@2", kind: "work_fanout", scope, stage: input.stage, parent_work_id: root.workId, dispatch: "none", capacity_required: true };
|
|
178
180
|
}
|
|
181
|
+
/** Diagnostic graphs are registered by the Stage handler, never fanout.json. */
|
|
182
|
+
export function diagnosticStageFanoutDescriptor(context, input) {
|
|
183
|
+
if (!["audit", "analyze", "review"].includes(input.stage))
|
|
184
|
+
return null;
|
|
185
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
186
|
+
const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
187
|
+
if (!run || !isDiagnosticFlow(run.flow_kind))
|
|
188
|
+
return null;
|
|
189
|
+
const index = JSON.parse(run.index_json);
|
|
190
|
+
if (index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running"))
|
|
191
|
+
return null;
|
|
192
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
193
|
+
if (!bundle || bundle.manifest.flow_id !== run.flow_kind)
|
|
194
|
+
throw new AppError("stage_graph_conflict", "Diagnostic graph requires its frozen bundle", 1);
|
|
195
|
+
const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
|
|
196
|
+
const root = readDescriptorRootWork(context, scope, bundle), graph = readRegisteredDiagnosticGraph(context, scope);
|
|
197
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
198
|
+
if (!graph || graph.parent_work_id !== root.workId || graph.bundle_sha256 !== bundle.sha256 || root.status !== "running"
|
|
199
|
+
|| !members.has(root.workId) || members.size !== Object.keys(graph.work_ids).length + 1
|
|
200
|
+
|| Object.values(graph.work_ids).some(id => !members.has(id)))
|
|
201
|
+
throw new AppError("stage_graph_conflict", "Diagnostic graph requires its exact registered Work; replay the issued Stage entry if registration was interrupted", 1);
|
|
202
|
+
return { schema_id: "dd-flow/work-fanout@2", kind: "work_fanout", scope: stageGraphScope(context, project.id, input.runId, input.stage),
|
|
203
|
+
stage: input.stage, parent_work_id: root.workId, dispatch: "none", capacity_required: true };
|
|
204
|
+
}
|
|
179
205
|
/** Read-only projection for an external orchestrator. dd-flow keeps the graph authority. */
|
|
180
206
|
export function getVnextFanoutStatus(context, input) {
|
|
181
207
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -189,7 +215,8 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
189
215
|
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot, runId: input.runId }) : null;
|
|
190
216
|
const stageRoot = path.join(run.run_root, bundle ? bundle.descriptor.stage(input.stage).directory : vnextStageDirectory(input.stage));
|
|
191
217
|
const memoryDescriptor = memoryStageFanoutDescriptor(context, input);
|
|
192
|
-
|
|
218
|
+
const diagnosticDescriptor = diagnosticStageFanoutDescriptor(context, input);
|
|
219
|
+
let descriptor = memoryDescriptor ?? diagnosticDescriptor ?? (bundle && isDiagnosticFlow(bundle.manifest.flow_id) ? null : readFanoutDescriptor(stageRoot));
|
|
193
220
|
// Registration is authoritative even if projection publication was interrupted,
|
|
194
221
|
// or an otherwise review-off stage acquired an aggregate repair graph.
|
|
195
222
|
if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
|
|
@@ -218,6 +245,9 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
218
245
|
// Only SQL-owned memory registration can attest a deliberately empty
|
|
219
246
|
// graph. A mutable fanout.json must never turn missing Work into success.
|
|
220
247
|
let registeredEmpty = false;
|
|
248
|
+
if (diagnosticDescriptor && !hasWork) {
|
|
249
|
+
registeredEmpty = Object.keys(readRegisteredDiagnosticGraph(context, { projectId: project.id, runId: input.runId, stage: input.stage }).work_ids).length === 0;
|
|
250
|
+
}
|
|
221
251
|
if (memoryDescriptor && input.stage === "upgrade" && !hasWork
|
|
222
252
|
&& context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')='upgrade' AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 1", [project.id, input.runId, currentScope.attempt, currentScope.cycle, descriptor.parent_work_id])) {
|
|
223
253
|
const registered = selectMemoryCompositionRegistration(context, { ...currentScope, stage: "upgrade", parent_work_id: descriptor.parent_work_id });
|