@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
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 +23 -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 +273 -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 +114 -16
- 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 +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- 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-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- 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 +214 -24
- 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 +245 -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 +12 -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 +209 -43
- 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 +252 -66
- 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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
4
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
5
|
+
import { readRegisteredInteractiveWork } from "./interactive-work-registration.js";
|
|
6
|
+
import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
|
|
7
|
+
import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
|
|
8
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
9
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
10
|
+
import { assertRunMutationAllowed } from "./run-recovery-guard.js";
|
|
11
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
12
|
+
const event = "interactive.work_start";
|
|
13
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
14
|
+
function refuse(reason) {
|
|
15
|
+
throw new AppError("interactive_work_start_invalid", "Interactive Work requires its issued input and actual physical start", 2, { reason, effect: "no_effect" });
|
|
16
|
+
}
|
|
17
|
+
export function readInteractiveWorkGit(bundle) {
|
|
18
|
+
const git = JSON.parse(bundle.blob("runtime/git-policy.json").toString("utf8"));
|
|
19
|
+
if (!git || typeof git.git_executable !== "string" || !/^[a-f0-9]{64}$/.test(git.git_sha256)
|
|
20
|
+
|| hash(readBoundedReleaseFile(git.git_executable, 64 * 1024 * 1024)) !== git.git_sha256)
|
|
21
|
+
refuse("selected_git_changed");
|
|
22
|
+
return { git_executable: git.git_executable, git_sha256: git.git_sha256 };
|
|
23
|
+
}
|
|
24
|
+
/** Historical SQL-only start custody. Never recapture already edited sources. */
|
|
25
|
+
export function readInteractiveWorkStart(context, scope, bundle, workSessionId) {
|
|
26
|
+
const assignment = readRegisteredInteractiveWork(context, scope, bundle, scope.workId);
|
|
27
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.work_session_id')=? LIMIT 2", [scope.projectId, event, workSessionId]);
|
|
28
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 33 * 1024 * 1024)
|
|
29
|
+
refuse("start_custody_missing");
|
|
30
|
+
const receipt = JSON.parse(rows[0].payload_json);
|
|
31
|
+
if (!receipt || Object.keys(receipt).sort().join() !== "actor,attempt,bundle_sha256,hook_event_id,hook_event_key,packet_sha256,run_id,schema_id,snapshot,snapshot_sha256,stage,work_id,work_session_id"
|
|
32
|
+
|| receipt.schema_id !== "dd-flow/interactive-work-start@1" || receipt.run_id !== scope.runId || receipt.work_id !== scope.workId
|
|
33
|
+
|| receipt.work_session_id !== workSessionId || receipt.stage !== assignment.packet.stage || receipt.attempt !== assignment.receipt.attempt
|
|
34
|
+
|| receipt.packet_sha256 !== assignment.packet.packet_sha256 || receipt.bundle_sha256 !== bundle.sha256
|
|
35
|
+
|| !receipt.actor || Object.keys(receipt.actor).sort().join() !== "harness,provider_session_id,session_id")
|
|
36
|
+
refuse("start_binding_changed");
|
|
37
|
+
const actor = receipt.actor;
|
|
38
|
+
if (!context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN hook_events h ON h.project_id=w.project_id AND h.event_key=ws.hook_event_id WHERE w.project_id=? AND w.run_id=? AND w.work_id=? AND ws.id=? AND ws.session_id=? AND ws.hook_event_id=? AND h.id=? AND h.status='claimed' AND h.session_id=ws.session_id AND h.harness=? AND h.provider_session_id=?", [scope.projectId, scope.runId, scope.workId, workSessionId, actor.session_id, receipt.hook_event_key, receipt.hook_event_id, actor.harness, actor.provider_session_id]))
|
|
39
|
+
refuse("physical_start_custody_changed");
|
|
40
|
+
const baseline = PreparedInteractiveGitBaseline.restore(Buffer.from(JSON.stringify(receipt.snapshot)), receipt.snapshot_sha256);
|
|
41
|
+
const start = context.db.get("SELECT start_receipt_json FROM work_sessions WHERE id=? AND work_id=?", [workSessionId, scope.workId]);
|
|
42
|
+
if (!start?.start_receipt_json || Buffer.byteLength(start.start_receipt_json) > 33 * 1024 * 1024)
|
|
43
|
+
refuse("start_packet_custody_missing");
|
|
44
|
+
const packetReceipt = JSON.parse(start.start_receipt_json);
|
|
45
|
+
if (typeof packetReceipt.context !== "string" || hash(Buffer.from(packetReceipt.context)) !== packetReceipt.context_sha256)
|
|
46
|
+
refuse("start_packet_changed");
|
|
47
|
+
const startContext = JSON.parse(packetReceipt.context);
|
|
48
|
+
if (startContext.run_id !== scope.runId || startContext.work_id !== scope.workId
|
|
49
|
+
|| hybridDigest(startContext.interactive_start) !== hybridDigest({ schema_id: "dd-flow/interactive-work-start-binding@1",
|
|
50
|
+
packet_sha256: assignment.packet.packet_sha256, snapshot_sha256: baseline.sha256 }))
|
|
51
|
+
refuse("start_snapshot_binding_changed");
|
|
52
|
+
const frozen = bundle.execution.snapshot().scope;
|
|
53
|
+
if (baseline.snapshot().project_root !== frozen.projectRoot || baseline.snapshot().workspace !== frozen.workspace)
|
|
54
|
+
refuse("physical_start_scope_changed");
|
|
55
|
+
return { receipt, baseline, assignment };
|
|
56
|
+
}
|
|
57
|
+
/** Physical preparation precedes the ordinary hook/Session claim writer.
|
|
58
|
+
* The original Work snapshot is retained with that claim, never after edits. */
|
|
59
|
+
export class PreparedInteractiveWorkStart {
|
|
60
|
+
#scope;
|
|
61
|
+
#bundle;
|
|
62
|
+
#assignment;
|
|
63
|
+
#changes;
|
|
64
|
+
#baseline;
|
|
65
|
+
#git;
|
|
66
|
+
constructor(scope, bundle, assignment, changes, git) {
|
|
67
|
+
this.#scope = { ...scope };
|
|
68
|
+
this.#bundle = bundle;
|
|
69
|
+
this.#assignment = assignment;
|
|
70
|
+
this.#changes = changes;
|
|
71
|
+
const bytes = Buffer.from(JSON.stringify(changes.currentSnapshot()));
|
|
72
|
+
this.#baseline = PreparedInteractiveGitBaseline.restore(bytes, hash(bytes));
|
|
73
|
+
this.#git = { ...git };
|
|
74
|
+
Object.freeze(this);
|
|
75
|
+
}
|
|
76
|
+
static prepare(context, input) {
|
|
77
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
78
|
+
refuse("preparation_outside_writer_required");
|
|
79
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
80
|
+
if (!bundle)
|
|
81
|
+
refuse("bundle_required");
|
|
82
|
+
const assignment = readRegisteredInteractiveWork(context, input, bundle, input.workId), frozen = bundle.execution.snapshot().scope;
|
|
83
|
+
const git = readInteractiveWorkGit(bundle);
|
|
84
|
+
const changes = PreparedInteractiveWorkspaceChanges.prepare(context, input, bundle, { projectRoot: frozen.projectRoot,
|
|
85
|
+
workspace: frozen.workspace, gitExecutable: git.git_executable, env: context.env });
|
|
86
|
+
const prepared = new PreparedInteractiveWorkStart(input, bundle, assignment, changes, git);
|
|
87
|
+
const patch = changes.snapshot(), packet = assignment.packet;
|
|
88
|
+
if (hybridDigest(patch.delta.changes) !== packet.file_changes_sha256 || patch.scope_revision !== packet.scope_revision
|
|
89
|
+
|| patch.user_inputs_sha256 !== packet.user_inputs_sha256)
|
|
90
|
+
refuse("issued_context_changed_before_start");
|
|
91
|
+
// Unchanged files are also selected reviewer/writer inputs, not just patch paths.
|
|
92
|
+
const files = new Map(changes.currentSnapshot().entries.filter(item => item.kind === "file").map(item => [item.path, item.sha256]));
|
|
93
|
+
if (packet.read_inputs.some(ref => ref.root === "workspace" && files.get(ref.path) !== ref.sha256))
|
|
94
|
+
refuse("issued_source_changed_before_start");
|
|
95
|
+
context.db.readSnapshot(() => prepared.assertSelection(context));
|
|
96
|
+
prepared.revalidate();
|
|
97
|
+
return prepared;
|
|
98
|
+
}
|
|
99
|
+
revalidate() {
|
|
100
|
+
if (hash(readBoundedReleaseFile(this.#git.git_executable, 64 * 1024 * 1024)) !== this.#git.git_sha256)
|
|
101
|
+
refuse("selected_git_changed");
|
|
102
|
+
this.#changes.revalidate();
|
|
103
|
+
}
|
|
104
|
+
binding() {
|
|
105
|
+
return { schema_id: "dd-flow/interactive-work-start-binding@1", packet_sha256: this.#assignment.packet.packet_sha256, snapshot_sha256: this.#baseline.sha256 };
|
|
106
|
+
}
|
|
107
|
+
assertSelection(context) {
|
|
108
|
+
const { projectId, runId, workId } = this.#scope, packet = this.#assignment.packet;
|
|
109
|
+
const current = readRegisteredInteractiveWork(context, this.#scope, this.#bundle, workId);
|
|
110
|
+
if (hybridDigest(current.receipt) !== hybridDigest(this.#assignment.receipt)
|
|
111
|
+
|| hybridDigest(current.custody) !== hybridDigest(this.#assignment.custody)
|
|
112
|
+
|| current.packet.packet_sha256 !== this.#assignment.packet.packet_sha256)
|
|
113
|
+
refuse("assignment_changed_before_start");
|
|
114
|
+
const run = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle), index = JSON.parse(run.index_json);
|
|
115
|
+
const graph = stageGraphScope(context, projectId, runId, packet.owner_stage), stage = readDescriptorStageInput(context, { ...this.#scope, stage: packet.owner_stage }, this.#bundle);
|
|
116
|
+
if (run.status !== "running" || index.current_stage !== packet.owner_stage
|
|
117
|
+
|| !index.stage_runs?.some((item) => item.stage === packet.owner_stage && item.status === "running" && item.attempt === graph.attempt)
|
|
118
|
+
|| graph.attempt !== current.custody.scope.attempt || graph.cycle !== current.custody.scope.cycle
|
|
119
|
+
|| stage.attempt !== graph.attempt || stage.inputSha256 !== packet.input_sha256)
|
|
120
|
+
refuse("active_assignment_required");
|
|
121
|
+
assertRunMutationAllowed(context, projectId, runId);
|
|
122
|
+
}
|
|
123
|
+
assertCustody(context) {
|
|
124
|
+
if (!context.db.isOwnedWriteTransaction?.())
|
|
125
|
+
refuse("owned_writer_required");
|
|
126
|
+
this.assertSelection(context);
|
|
127
|
+
this.#changes.assertCustody(context);
|
|
128
|
+
}
|
|
129
|
+
recordStart(context, workSessionId, hookEventKey, identity) {
|
|
130
|
+
this.assertCustody(context);
|
|
131
|
+
const { projectId, runId, workId } = this.#scope;
|
|
132
|
+
const link = context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id JOIN hook_events h ON h.project_id=w.project_id AND h.event_key=ws.hook_event_id WHERE w.project_id=? AND w.run_id=? AND w.work_id=? AND w.status='running' AND ws.id=? AND ws.status='running' AND ws.session_id=? AND ws.hook_event_id=? AND h.status='claimed' AND h.id=? AND h.session_id=ws.session_id AND s.harness=? AND s.provider_session_id=? AND h.harness=s.harness AND h.provider_session_id=s.provider_session_id", [projectId, runId, workId, workSessionId, identity.sessionId, hookEventKey, identity.hookEventId, identity.harness, identity.providerSessionId]);
|
|
133
|
+
if (!link)
|
|
134
|
+
refuse("claimed_work_session_required");
|
|
135
|
+
if (this.#assignment.retained.body.work_history.works.some(prior => prior.actor?.harness === identity.harness
|
|
136
|
+
&& prior.actor.provider_session_id === identity.providerSessionId))
|
|
137
|
+
refuse("independent_physical_session_required");
|
|
138
|
+
if (context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id WHERE w.project_id=? AND w.run_id=? AND ws.id<>? AND s.harness=? AND s.provider_session_id=? LIMIT 1", [projectId, runId, workSessionId, identity.harness, identity.providerSessionId]))
|
|
139
|
+
refuse("independent_physical_session_required");
|
|
140
|
+
if (context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.work_session_id')=?", [projectId, event, workSessionId]))
|
|
141
|
+
refuse("start_already_retained");
|
|
142
|
+
const packet = this.#assignment.packet;
|
|
143
|
+
const receipt = { schema_id: "dd-flow/interactive-work-start@1", run_id: runId, work_id: workId, work_session_id: workSessionId,
|
|
144
|
+
hook_event_key: hookEventKey, hook_event_id: identity.hookEventId, stage: packet.stage, attempt: this.#assignment.receipt.attempt,
|
|
145
|
+
packet_sha256: packet.packet_sha256, bundle_sha256: this.#bundle.sha256,
|
|
146
|
+
actor: { session_id: identity.sessionId, harness: identity.harness, provider_session_id: identity.providerSessionId },
|
|
147
|
+
snapshot_sha256: this.#baseline.sha256, snapshot: this.#baseline.snapshot() };
|
|
148
|
+
// Same bounded snapshot as the RUN baseline; SQL custody is not worker transport.
|
|
149
|
+
const json = JSON.stringify(receipt);
|
|
150
|
+
if (Buffer.byteLength(json) > 33 * 1024 * 1024)
|
|
151
|
+
refuse("unbounded_start_receipt");
|
|
152
|
+
context.db.run("INSERT INTO audit_events(project_id,event_type,payload_json,created_at) VALUES(?,?,?,?)", [projectId, event, json, context.now()]);
|
|
153
|
+
readInteractiveWorkStart(context, this.#scope, this.#bundle, workSessionId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { checkDiagnosisDisposition } from "./check-runner-evidence.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { quote } from "shell-quote";
|
|
@@ -21,6 +21,12 @@ import { stageAttempt } from "./stage-work-graph.js";
|
|
|
21
21
|
import { contextualPathOptionsForLifecycle } from "../cli/command-inputs.js";
|
|
22
22
|
import { canonicalPath } from "../storage/paths.js";
|
|
23
23
|
import { runtimeCommand, shellArgument } from "./runtime-command.js";
|
|
24
|
+
import { assignedLifecycleBinding, claimAssignedLifecycle, currentAssignedLifecycle, bindAssignedCoordinatorPacket, inheritAssignedPackets, retainControllerStageAssignment, admitAssignedAuxiliary, bindAssignedStageResume, readAcceptedAssignedWork, assertTerminalSettlementUnreserved } from "./assigned-lifecycle.js";
|
|
25
|
+
import { appendAudit } from "./audit.js";
|
|
26
|
+
import { recordTechnicalNote } from "./technical-notes.js";
|
|
27
|
+
import { ObservationClock } from "../harness-runtime/lib/observation-clock.mjs";
|
|
28
|
+
export { currentAssignedLifecycle, assignedSessionIdentity, assertAssignedSettlementCurrent } from "./assigned-lifecycle.js";
|
|
29
|
+
export const bindControllerStageAssignment = retainControllerStageAssignment;
|
|
24
30
|
// Process-local custody: an executing SQL row or ambient variable alone cannot
|
|
25
31
|
// recreate admission after a CLI restart, or in another resource database.
|
|
26
32
|
const currentAdmissions = new WeakMap();
|
|
@@ -48,6 +54,11 @@ export function assertAuxiliaryCommandScope(context, command, projectRoot) {
|
|
|
48
54
|
const daemonId = context.env.DD_FLOW_DAEMON_ID;
|
|
49
55
|
if (!auxiliary || !daemonId)
|
|
50
56
|
return;
|
|
57
|
+
const assignedScope = cliManagedInvocationScope(context);
|
|
58
|
+
if (assignedScope && admitAssignedAuxiliary(context, command, assignedScope, projectRoot)) {
|
|
59
|
+
assertRunMutationAllowed(context, requireProjectByRoot(context, assignedScope.projectRoot).id, assignedScope.runId, assignedScope.generation);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
51
62
|
const event = observedLifecycleNativeEvent(context, command, daemonId);
|
|
52
63
|
if (!event)
|
|
53
64
|
throw new AppError("invocation_receipt_missing", "Auxiliary command has no exact native receipt", 1, { phase: "prepare", effect: "no_effect" });
|
|
@@ -96,7 +107,7 @@ export function isRetryableLifecycleRejection(code, details = {}) {
|
|
|
96
107
|
if (["invocation_command_mismatch", "invocation_argument_mismatch"].includes(code))
|
|
97
108
|
return details.recoverable !== false;
|
|
98
109
|
if (["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(code))
|
|
99
|
-
return Array.isArray(details.failures) && details.failures.length > 0 && details.failures.every(receipt =>
|
|
110
|
+
return Array.isArray(details.failures) && details.failures.length > 0 && details.failures.every(receipt => checkDiagnosisDisposition(receipt) === "diagnosis_required");
|
|
100
111
|
return details.recoverable === true || ["review_repair_incomplete", "plan_review_incomplete", "semantic_result_invalid",
|
|
101
112
|
"review_evidence_invalid", "evidence_obligation_unknown", "invalid_evidence_ref",
|
|
102
113
|
"evidence_ref_missing", "evidence_line_out_of_range", "document_update_missing",
|
|
@@ -289,6 +300,7 @@ export function bindLifecyclePublishedPacket(context, prompt) {
|
|
|
289
300
|
const issued = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND status = 'issued' ORDER BY rowid", [scope]);
|
|
290
301
|
const ids = issued.filter(row => prompt.includes(publicInvocationCommand(context, row, retainedCommandWithoutInvocationId(row)))).map(row => row.id);
|
|
291
302
|
bindLifecyclePacketAssignments(context, prompt, ids);
|
|
303
|
+
bindAssignedCoordinatorPacket(context, prompt);
|
|
292
304
|
}
|
|
293
305
|
function retainedCommandWithoutInvocationId(row) {
|
|
294
306
|
const marker = ` --invocation-id ${row.id}`;
|
|
@@ -391,6 +403,24 @@ export function assertLifecycleOutcomes(context, scope) {
|
|
|
391
403
|
/** Hooks already own native identity. Retain command outcomes there, with an
|
|
392
404
|
* atomic controller notification; missing notifications reconcile from the row. */
|
|
393
405
|
export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
406
|
+
const assigned = currentAssignedLifecycle(context);
|
|
407
|
+
if (assigned)
|
|
408
|
+
return outcome => {
|
|
409
|
+
try {
|
|
410
|
+
const project = requireProjectByRoot(context, assigned.scope.projectRoot);
|
|
411
|
+
try {
|
|
412
|
+
appendAudit(context, { projectId: project.id, eventType: "lifecycle.assigned_outcome", payload: { run_id: assigned.scope.runId,
|
|
413
|
+
generation: assigned.scope.generation, invocation_id: assigned.invocation_id, operation: assigned.operation,
|
|
414
|
+
work_id: assigned.work_id, ...(outcome.error ? { error: { code: outcome.error.code, message: outcome.error.message } } : { accepted: true }) } });
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
recordTechnicalNote(context, project.id, assigned.scope.runId, { code: "assigned_outcome_audit_unavailable", message: "Optional assigned-operation audit could not be written; invocation outcome remains authoritative", phase: "settlement", generation: assigned.scope.generation, source_operation_id: assigned.invocation_id, consequence: "none" });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
process.stderr.write(`${JSON.stringify({ event: "assigned_outcome_audit_unavailable", invocation_id: assigned.invocation_id })}\n`);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
394
424
|
const parsed = parseLifecycleCommand(command);
|
|
395
425
|
const auxiliary = continuationCommand(command);
|
|
396
426
|
if (parsed.kind !== "standalone" && !auxiliary)
|
|
@@ -677,9 +707,41 @@ export function lifecycleRetryCommands(context, stage) {
|
|
|
677
707
|
}
|
|
678
708
|
}))];
|
|
679
709
|
}
|
|
680
|
-
|
|
710
|
+
/** A resumed coordinator retains intent, not the old route's mutation authority. */
|
|
711
|
+
export function rebindAssignedCoordinatorCommands(context) {
|
|
712
|
+
const assigned = currentAssignedLifecycle(context);
|
|
713
|
+
if (assigned?.operation !== "stage_resume" || !assigned.work_session_id)
|
|
714
|
+
return [];
|
|
715
|
+
const rows = context.db.all(`SELECT DISTINCT li.* FROM lifecycle_invocations li JOIN lifecycle_packet_assignments pa ON pa.invocation_id=li.id
|
|
716
|
+
WHERE li.status='issued' AND (pa.packet_sha256=(SELECT json_extract(start_receipt_json,'$.prompt_sha256') FROM work_sessions WHERE id=?)
|
|
717
|
+
OR EXISTS (SELECT 1 FROM audit_events a WHERE a.event_type='lifecycle.coordinator_packet'
|
|
718
|
+
AND json_extract(a.payload_json,'$.work_session_id')=? AND json_extract(a.payload_json,'$.packet_sha256')=pa.packet_sha256))`, [assigned.work_session_id, assigned.work_session_id]);
|
|
719
|
+
const commands = [];
|
|
720
|
+
context.db.writeTransaction(() => {
|
|
721
|
+
for (const row of rows) {
|
|
722
|
+
const parsed = invocation(row.command), scope = JSON.parse(row.scope_json);
|
|
723
|
+
if (!["stage_finish", "stage_pause"].includes(parsed.operation) || commandOption(parsed, "stage") !== assigned.stage
|
|
724
|
+
|| scope.projectRoot !== assigned.scope.projectRoot || scope.runId !== assigned.scope.runId || scope.generation > assigned.scope.generation)
|
|
725
|
+
continue;
|
|
726
|
+
if (row.scope_json === JSON.stringify(assigned.scope)) {
|
|
727
|
+
commands.push(publicInvocationCommand(context, row, row.command));
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const command = canonicalManagedCommand(context, retainedCommandWithoutInvocationId(row), assigned.scope);
|
|
731
|
+
const next = managedLifecycleCommand(context, command);
|
|
732
|
+
const successor = currentIssuedCommand(context, JSON.stringify(assigned.scope), next);
|
|
733
|
+
if (!successor)
|
|
734
|
+
throw new AppError("invocation_assignment_unproven", "Resume has no exact current coordinator successor", 1, { effect: "no_effect", reconciliation_required: true });
|
|
735
|
+
inheritAssignedPackets(context, row.id, successor.id);
|
|
736
|
+
context.db.run("UPDATE lifecycle_invocations SET status='expired',updated_at=? WHERE id=? AND status='issued'", [context.now(), row.id]);
|
|
737
|
+
commands.push(next);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
return [...new Set(commands)];
|
|
741
|
+
}
|
|
742
|
+
function currentIssuedCommand(context, scope, command, excludeId) {
|
|
681
743
|
const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND status = 'issued'", [scope, scopedCommandFingerprint(context, scope, command)])
|
|
682
|
-
.filter(row => row.deadline === null || row.deadline > Date.now());
|
|
744
|
+
.filter(row => row.id !== excludeId && (row.deadline === null || row.deadline > Date.now()));
|
|
683
745
|
if (rows.length > 1)
|
|
684
746
|
throw new AppError("invocation_ambiguous", "Multiple current lifecycle successors match this command", 1);
|
|
685
747
|
return rows[0] ?? null;
|
|
@@ -805,6 +867,7 @@ export function managedLifecycleCommand(context, command, options = {}) {
|
|
|
805
867
|
if (context.env.DD_FLOW_INVOCATION_READONLY === "1")
|
|
806
868
|
throw new AppError("invocation_command_unprepared", "No prepared lifecycle command is available; read-only inspection cannot issue one", 1);
|
|
807
869
|
const issued = issueLifecycleInvocation(context, command, scope);
|
|
870
|
+
bindAssignedStageResume(context, load(context, issued.id));
|
|
808
871
|
return publicInvocationCommand(context, load(context, issued.id), command);
|
|
809
872
|
});
|
|
810
873
|
}
|
|
@@ -841,9 +904,13 @@ function successorLifecycleInvocationCommand(context, prior, checkReceiptId) {
|
|
|
841
904
|
throw new AppError("invocation_command_invalid", "Retained retry option cannot be rewritten unambiguously", 1);
|
|
842
905
|
retained = retained.replace(fragments[0], "");
|
|
843
906
|
}
|
|
844
|
-
command = retained.replace(` --invocation-id ${prior.id}`, ` --retry-check ${shellArgument(checkReceiptId)} --reason ${shellArgument("Recheck after
|
|
907
|
+
command = retained.replace(` --invocation-id ${prior.id}`, ` --retry-check ${shellArgument(checkReceiptId)} --reason ${shellArgument("Recheck after diagnosis and correction of the failed check")}`);
|
|
845
908
|
}
|
|
846
|
-
|
|
909
|
+
const successor = managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
|
|
910
|
+
const next = stagedContinuations.get(context.db)?.get(prior.id) ?? currentIssuedCommand(context, prior.scope_json, successor, prior.id);
|
|
911
|
+
if (next && context.db.get("SELECT 1 FROM lifecycle_invocations WHERE id=?", [next.id]))
|
|
912
|
+
inheritAssignedPackets(context, prior.id, next.id);
|
|
913
|
+
return successor;
|
|
847
914
|
}
|
|
848
915
|
// Caller holds the write transaction and has checked authority. Issued (or
|
|
849
916
|
// legacy expired) attempts have never admitted an effect; observed/executing
|
|
@@ -862,6 +929,16 @@ const receiptTimeout = (id) => ({ code: "invocation_receipt_timeout", message: "
|
|
|
862
929
|
* A crash must leave either the old attempt executing, or both the settled
|
|
863
930
|
* outcome and its successor; never a usable successor beside an unknown effect. */
|
|
864
931
|
export function settleLifecycleRejection(context, id, error) {
|
|
932
|
+
const accepted = readAcceptedAssignedWork(context, id);
|
|
933
|
+
if (accepted) {
|
|
934
|
+
const row = load(context, id), scope = JSON.parse(row.scope_json);
|
|
935
|
+
settleLifecycleInvocation(context, id, { result: accepted });
|
|
936
|
+
Object.assign(error.details, { effect: "committed", recoverable: false, work_result_accepted: true, reconciliation_required: true });
|
|
937
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
938
|
+
recordTechnicalNote(context, project.id, scope.runId, { code: "work_post_commit_pending", message: "Work result was accepted; derivative publication or projection needs reconciliation", phase: "publication", generation: scope.generation,
|
|
939
|
+
source_operation_id: id, work_id: String(accepted.work_id), consequence: "dependency_blocked" });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
865
942
|
storage(context);
|
|
866
943
|
// Only this settlement may publish a successor; inherited help is not authority.
|
|
867
944
|
delete error.details.retry_command;
|
|
@@ -935,24 +1012,33 @@ export function hasLifecyclePacketAssignments(context, originalPrompt) {
|
|
|
935
1012
|
}
|
|
936
1013
|
export function recoverLifecycleCommands(context, originalPrompt, scope) {
|
|
937
1014
|
const digest = crypto.createHash("sha256").update(originalPrompt).digest("hex");
|
|
938
|
-
const
|
|
939
|
-
if (!
|
|
1015
|
+
const packet = context.db.all("SELECT li.* FROM lifecycle_packet_assignments pa JOIN lifecycle_invocations li ON li.id=pa.invocation_id WHERE pa.packet_sha256 = ? ORDER BY pa.rowid", [digest]);
|
|
1016
|
+
if (!packet.length)
|
|
940
1017
|
throw new AppError("invocation_recovery_unbound", "Original packet has no verified lifecycle assignment binding; reconcile it before continuing", 1);
|
|
1018
|
+
const origins = new Map();
|
|
1019
|
+
for (const row of packet) {
|
|
1020
|
+
const priorScope = JSON.parse(row.scope_json);
|
|
1021
|
+
if (priorScope.projectRoot !== scope.projectRoot || priorScope.runId !== scope.runId || priorScope.rootSessionId !== scope.rootSessionId
|
|
1022
|
+
|| priorScope.generation > scope.generation || priorScope.generation === scope.generation && row.scope_json !== JSON.stringify(scope))
|
|
1023
|
+
throw new AppError("invocation_recovery_scope_mismatch", "Retained command does not belong to the recovered root and an earlier generation", 1, { invocation_id: row.id });
|
|
1024
|
+
// Recovery inherits the immutable packet binding. Those new rows are
|
|
1025
|
+
// successors of its original commands, not additional commands to recover.
|
|
1026
|
+
if (priorScope.generation < scope.generation && !origins.has(row.fingerprint))
|
|
1027
|
+
origins.set(row.fingerprint, row);
|
|
1028
|
+
}
|
|
1029
|
+
if (!origins.size)
|
|
1030
|
+
throw new AppError("invocation_recovery_scope_mismatch", "Recovery requires an earlier packet generation", 1);
|
|
941
1031
|
const recovered = [];
|
|
942
|
-
for (const
|
|
943
|
-
|
|
944
|
-
const oldScope = JSON.parse(prior.scope_json);
|
|
945
|
-
if (oldScope.projectRoot !== scope.projectRoot || oldScope.runId !== scope.runId
|
|
946
|
-
|| oldScope.rootSessionId !== scope.rootSessionId || oldScope.generation >= scope.generation) {
|
|
947
|
-
throw new AppError("invocation_recovery_scope_mismatch", "Retained command does not belong to the recovered root and an earlier generation", 1, { invocation_id: id });
|
|
948
|
-
}
|
|
1032
|
+
for (const origin of origins.values()) {
|
|
1033
|
+
const id = origin.id;
|
|
949
1034
|
// The immutable prompt can predate several recoveries. Follow the newest
|
|
950
1035
|
// retained attempt, including an unknown successor, rather than reviving
|
|
951
1036
|
// an older unexecuted command after the successor already produced effects.
|
|
952
|
-
prior = context.db.get(`SELECT
|
|
953
|
-
|
|
954
|
-
AND json_extract(scope_json, '$.
|
|
955
|
-
|
|
1037
|
+
const prior = context.db.get(`SELECT li.* FROM lifecycle_invocations li JOIN lifecycle_packet_assignments pa ON pa.invocation_id=li.id
|
|
1038
|
+
WHERE pa.packet_sha256=? AND li.fingerprint = ?
|
|
1039
|
+
AND json_extract(li.scope_json, '$.projectRoot') = ? AND json_extract(li.scope_json, '$.runId') IS ?
|
|
1040
|
+
AND json_extract(li.scope_json, '$.rootSessionId') = ? AND (json_extract(li.scope_json, '$.generation') < ? OR li.scope_json = ?)
|
|
1041
|
+
ORDER BY json_extract(li.scope_json, '$.generation') DESC, li.rowid DESC LIMIT 1`, [digest, origin.fingerprint, scope.projectRoot, scope.runId, scope.rootSessionId, scope.generation, JSON.stringify(scope)]);
|
|
956
1042
|
if (prior.status === "executing" || prior.status === "settled") {
|
|
957
1043
|
const outcome = prior.outcome_json ? JSON.parse(prior.outcome_json) : null;
|
|
958
1044
|
recovered.push({ invocation_id: id, status: prior.status, outcome,
|
|
@@ -968,6 +1054,9 @@ export function recoverLifecycleCommands(context, originalPrompt, scope) {
|
|
|
968
1054
|
const env = { ...parsed.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(scope) };
|
|
969
1055
|
const command = `${Object.entries(env).map(([key, value]) => `${key}=${quote([value])}`).join(" ")} ${launchExpression(parsed)} ${quote(argv)} ${prior.command.slice(shellSuffixOffset(prior.command))}`.trim();
|
|
970
1056
|
const prepared = managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(scope), DD_FLOW_CURRENT_INVOCATION: undefined } }, command);
|
|
1057
|
+
const successor = currentIssuedCommand(context, JSON.stringify(scope), prepared);
|
|
1058
|
+
if (successor)
|
|
1059
|
+
inheritAssignedPackets(context, prior.id, successor.id);
|
|
971
1060
|
recovered.push({ invocation_id: id, status: "replaced_unexecuted", command: prepared });
|
|
972
1061
|
}
|
|
973
1062
|
return recovered;
|
|
@@ -1029,6 +1118,28 @@ function checkCommand(row, command) {
|
|
|
1029
1118
|
throw new AppError("invocation_command_mismatch", "Lifecycle attempt belongs to another command or project", 1, { invocation_id: row.id });
|
|
1030
1119
|
}
|
|
1031
1120
|
}
|
|
1121
|
+
/** Match a daemon-attested native tool against its exact issued command.
|
|
1122
|
+
* This is only a correlation check: claim/owner/generation are verified again
|
|
1123
|
+
* by the normal invocation path before the command can mutate anything. */
|
|
1124
|
+
export function matchesIssuedNativeCommand(context, id, command) {
|
|
1125
|
+
const row = load(context, id);
|
|
1126
|
+
const analysis = parseLifecycleCommand(command);
|
|
1127
|
+
if (analysis.kind === "none" || analysis.supported === false)
|
|
1128
|
+
return false;
|
|
1129
|
+
try {
|
|
1130
|
+
const scope = JSON.parse(row.scope_json);
|
|
1131
|
+
const parsed = invocation(canonicalManagedCommand(context, analysis.invocation.command, scope));
|
|
1132
|
+
assertSuppliedLifecycleTarget(row, parsed);
|
|
1133
|
+
// Public Work packets deliberately omit the internal UUID. The caller
|
|
1134
|
+
// has already selected the unique issued assignment in its managed scope;
|
|
1135
|
+
// this comparison supplies no authority and must still pass native proof/CAS.
|
|
1136
|
+
const supplied = commandOption(parsed, "invocation-id");
|
|
1137
|
+
return (supplied === id || supplied === undefined && parsed.operation === "work_start") && fingerprint(parsed) === row.fingerprint;
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1032
1143
|
/** Public commands may omit the coordinator RUN. A supplied target remains
|
|
1033
1144
|
* input to validate, not text to silently overwrite with retained authority. */
|
|
1034
1145
|
function assertSuppliedLifecycleTarget(row, actual) {
|
|
@@ -1480,6 +1591,32 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
1480
1591
|
}
|
|
1481
1592
|
function observeLifecycleInvocationReceipt(context, input) {
|
|
1482
1593
|
storage(context);
|
|
1594
|
+
const delivered = parseLifecycleCommand(input.command);
|
|
1595
|
+
if (delivered.kind === "standalone") {
|
|
1596
|
+
const id = commandOption(delivered.invocation, "invocation-id");
|
|
1597
|
+
const direct = context.db.all("SELECT * FROM lifecycle_invocations WHERE admission_json IS NOT NULL AND status IN ('executing','settled')")
|
|
1598
|
+
.filter(row => {
|
|
1599
|
+
const scope = JSON.parse(row.scope_json);
|
|
1600
|
+
if (id && row.id !== id || scope.daemonId !== input.identity.daemonId || scope.rootSessionId !== input.identity.rootSessionId)
|
|
1601
|
+
return false;
|
|
1602
|
+
// A fresh native call must bind the explicitly committed successor,
|
|
1603
|
+
// not short-circuit to its predecessor's successful repair response.
|
|
1604
|
+
// Exact delivery/explicit-ID replay remains historical read-only.
|
|
1605
|
+
const successor = row.status === "settled" && row.outcome_json ? committedSuccessor(context, row, JSON.parse(row.outcome_json)) : null;
|
|
1606
|
+
if (!id && row.identity_json && successor && ["issued", "observed"].includes(successor.status)
|
|
1607
|
+
&& !sameNativeCall(row.identity_json, JSON.stringify([input.identity.daemonId, input.identity.rootSessionId, input.identity.sessionId, input.identity.parentSessionId, input.identity.toolCallId])))
|
|
1608
|
+
return false;
|
|
1609
|
+
try {
|
|
1610
|
+
return fingerprint(invocation(canonicalManagedCommand(context, delivered.invocation.command, scope))) === row.fingerprint;
|
|
1611
|
+
}
|
|
1612
|
+
catch {
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
if (direct.length === 1)
|
|
1617
|
+
return { eventKey: direct[0].event_key, duplicate: true,
|
|
1618
|
+
disposition: direct[0].status === "settled" ? "settled_read" : "in_flight", invocationId: direct[0].id };
|
|
1619
|
+
}
|
|
1483
1620
|
const projectRoot = input.projectId ? context.db.get("SELECT root FROM projects WHERE id = ?", [input.projectId])?.root : undefined;
|
|
1484
1621
|
const provenScope = projectRoot && input.harness ? managedNativeInvocationScope(context, projectRoot, input.identity, input.harness) : null;
|
|
1485
1622
|
const owner = provenScope && input.harness && input.projectId ? managedNativeOwner(context, input.identity, input.harness, input.projectId) : null;
|
|
@@ -1587,6 +1724,10 @@ function observeLifecycleInvocationReceipt(context, input) {
|
|
|
1587
1724
|
// public ID is corrected only by an exact operation/target match inside the
|
|
1588
1725
|
// trusted native root. Never fuzzy-match UUID text or select a global latest.
|
|
1589
1726
|
const initial = supplied ?? (candidates.length === 1 ? candidates[0] : undefined);
|
|
1727
|
+
// A live observation arriving after direct admission is optional correlation,
|
|
1728
|
+
// never a second authority or an executing -> observed transition.
|
|
1729
|
+
if (initial?.admission_json && ["executing", "settled"].includes(initial.status))
|
|
1730
|
+
return { eventKey: initial.event_key, duplicate: true, disposition: initial.status === "settled" ? "settled_read" : "in_flight", invocationId: initial.id };
|
|
1590
1731
|
if (!supplied && candidates.length > 1) {
|
|
1591
1732
|
input.recordReceipt();
|
|
1592
1733
|
throw new AppError("invocation_ambiguous", "More than one issued lifecycle attempt matches this native command", 1, { effect: "no_effect", matches: candidates.map(row => row.id), ...(provenScope ? { lifecycle_assignment: { issuer: "dd-flow", scope: provenScope } } : {}) });
|
|
@@ -2024,13 +2165,50 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
2024
2165
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60000)
|
|
2025
2166
|
throw new AppError("invocation_timeout_invalid", "Receipt timeout must be within 1..60000 ms", 1);
|
|
2026
2167
|
context.db.run("UPDATE lifecycle_invocations SET deadline = COALESCE(deadline, ?), updated_at = ? WHERE id = ? AND status = 'issued'", [Date.now() + timeoutMs, context.now(), input.id]);
|
|
2168
|
+
const initialClock = new ObservationClock({ timeoutMs }), seenProgress = new Set();
|
|
2169
|
+
const receiptClock = new ObservationClock({ timeoutMs, snapshotIsLive: true, saved: { ...initialClock.state(),
|
|
2170
|
+
elapsed_ms: Math.max(0, timeoutMs - Math.max(0, (load(context, input.id).deadline ?? Date.now()) - Date.now())) } });
|
|
2027
2171
|
for (;;) {
|
|
2028
|
-
|
|
2172
|
+
let row = load(context, input.id);
|
|
2173
|
+
if (row.status === "issued" && invocation(row.command).operation === "work_start" && input.reconcileNativeObservation) {
|
|
2174
|
+
const scope = JSON.parse(row.scope_json);
|
|
2175
|
+
input.assertCurrent(scope);
|
|
2176
|
+
await input.reconcileNativeObservation(scope, progress => {
|
|
2177
|
+
input.assertCurrent(scope);
|
|
2178
|
+
if (seenProgress.has(progress.cursor))
|
|
2179
|
+
return;
|
|
2180
|
+
const current = load(context, input.id);
|
|
2181
|
+
if (current.status !== 'issued' || current.scope_json !== row.scope_json || current.deadline === null
|
|
2182
|
+
|| !Number.isFinite(progress.observed_at) || progress.observed_at >= current.deadline || progress.observed_at > Date.now())
|
|
2183
|
+
return;
|
|
2184
|
+
const expired = receiptClock.sample(progress);
|
|
2185
|
+
const state = receiptClock.state();
|
|
2186
|
+
if (expired || state.cursor !== progress.cursor)
|
|
2187
|
+
return;
|
|
2188
|
+
seenProgress.add(progress.cursor);
|
|
2189
|
+
context.db.run("UPDATE lifecycle_invocations SET deadline = ?, updated_at = ? WHERE id = ? AND status = 'issued' AND scope_json = ?", [Date.now() + Math.max(1, timeoutMs - state.elapsed_ms), context.now(), input.id, row.scope_json]);
|
|
2190
|
+
input.progress?.("native_verification_progress");
|
|
2191
|
+
});
|
|
2192
|
+
// A native receipt can arrive while it is being read. Reload it before
|
|
2193
|
+
// deciding whether this exact invocation is eligible for admission.
|
|
2194
|
+
row = load(context, input.id);
|
|
2195
|
+
if (row.status !== "issued")
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2029
2198
|
if (row.status === "settled")
|
|
2030
2199
|
return { outcome: JSON.parse(row.outcome_json), replay: true };
|
|
2031
2200
|
if (row.status === "executing") {
|
|
2032
2201
|
const scope = JSON.parse(row.scope_json);
|
|
2033
2202
|
const parsed = invocation(row.command);
|
|
2203
|
+
if (row.admission_json && ["work_finish", "work_fail"].includes(parsed.operation)) {
|
|
2204
|
+
const { recoverCommittedAssignedWork } = await import("./work-registry.js");
|
|
2205
|
+
const result = recoverCommittedAssignedWork(context, row.id);
|
|
2206
|
+
if (result) {
|
|
2207
|
+
const outcome = { result };
|
|
2208
|
+
settleLifecycleInvocation(context, row.id, outcome);
|
|
2209
|
+
return { outcome, replay: true };
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2034
2212
|
if (parsed.operation === "stage_resume" && scope.runId) {
|
|
2035
2213
|
input.assertCurrent(scope);
|
|
2036
2214
|
const { recoverCommittedStageResume } = await import("./stage-pause.js");
|
|
@@ -2073,6 +2251,14 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
2073
2251
|
throw new AppError("invocation_outcome_pending", "This attempt already started; inspect its retained operation outcome before retrying", 1, { invocation_id: input.id, effect: "unknown" });
|
|
2074
2252
|
}
|
|
2075
2253
|
input.assertCurrent(JSON.parse(row.scope_json));
|
|
2254
|
+
if (["issued", "observed"].includes(row.status) && assignedLifecycleBinding(context, row)) {
|
|
2255
|
+
if (claimAssignedLifecycle(context, row))
|
|
2256
|
+
return { kind: "managed_assignment", replay: false };
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
if (row.status === "issued" && context.env.DD_FLOW_DAEMON_ID && ["work_finish", "work_fail", "stage_finish", "stage_pause", "stage_resume", "stage_start"].includes(invocation(row.command).operation)) {
|
|
2260
|
+
throw new AppError("invocation_assignment_unproven", "Current managed assignment is unavailable; reconcile this operation without repeating Work", 1, { invocation_id: row.id, effect: "no_effect", recoverable: true, reconciliation_required: true });
|
|
2261
|
+
}
|
|
2076
2262
|
if (row.status === "observed") {
|
|
2077
2263
|
if (row.identity_json) {
|
|
2078
2264
|
// Receipt display text is bounded/redacted and may not be valid JSON.
|
|
@@ -2086,11 +2272,13 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
2086
2272
|
}
|
|
2087
2273
|
// Another native observation can replace an unclaimed receipt. Claim
|
|
2088
2274
|
// exactly the identity/route checked above, or reload and validate it.
|
|
2089
|
-
const
|
|
2090
|
-
const
|
|
2275
|
+
const operation = invocation(row.command).operation;
|
|
2276
|
+
const physicalAdmission = operation === "work_start" || operation.startsWith("delivery_");
|
|
2277
|
+
const birth = physicalAdmission ? processStartedAt(process.pid) : null, cwd = physicalAdmission ? process.cwd() : "";
|
|
2091
2278
|
input.assertCurrent(JSON.parse(row.scope_json));
|
|
2092
2279
|
let admittedAt = "";
|
|
2093
2280
|
const claimed = context.db.writeTransaction(() => {
|
|
2281
|
+
assertTerminalSettlementUnreserved(context, row);
|
|
2094
2282
|
// Acquire the writer before taking the anchor; waiting for SQLite or
|
|
2095
2283
|
// physical birth observation must not make an old Hook appear fresh.
|
|
2096
2284
|
admittedAt = context.now();
|
|
@@ -2107,7 +2295,7 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
2107
2295
|
owned.delete(owned.keys().next().value);
|
|
2108
2296
|
owned.set(input.id, { eventKey: row.event_key, identity: row.identity_json, scope: row.scope_json, command: row.command, cwd, pid: process.pid, birth, admittedAt });
|
|
2109
2297
|
}
|
|
2110
|
-
return { eventKey: row.event_key, replay: false };
|
|
2298
|
+
return { kind: "native_hook", eventKey: row.event_key, replay: false };
|
|
2111
2299
|
}
|
|
2112
2300
|
continue;
|
|
2113
2301
|
}
|
|
@@ -2139,8 +2327,10 @@ export function settleLifecycleInvocation(context, id, outcome) {
|
|
|
2139
2327
|
}
|
|
2140
2328
|
if (row.status !== "executing")
|
|
2141
2329
|
throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
|
|
2142
|
-
if (successor)
|
|
2330
|
+
if (successor) {
|
|
2143
2331
|
insertIssuedInvocation(context, successor);
|
|
2332
|
+
inheritAssignedPackets(context, id, successor.id);
|
|
2333
|
+
}
|
|
2144
2334
|
if (context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ? AND status = 'executing'", [encoded, context.now(), id]).changes !== 1)
|
|
2145
2335
|
throw new AppError("invocation_settlement_conflict", "Lifecycle outcome changed before commit", 1, { invocation_id: id });
|
|
2146
2336
|
});
|
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
|
-
import { managedProcessRuntimeHome, managedProcessStatus } from "./managed-processes.js";
|
|
4
|
+
import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
|
|
5
|
+
/** Native execution and AGY's potentially scheduled writers are separate facts. */
|
|
6
|
+
export function managedExecutionReusable(state, harness) {
|
|
7
|
+
const shutdown = state.shutdown, result = shutdown?.result;
|
|
8
|
+
if (state.active_tree !== false || state.unclaimed_activity === true || shutdown?.phases?.execution !== true
|
|
9
|
+
|| result?.native_settled !== true || result.settled !== true || result.native_settlement?.confirmed === false
|
|
10
|
+
|| state.native_execution_revision !== undefined && state.native_execution_revision !== result.native_settlement?.revision)
|
|
11
|
+
return false;
|
|
12
|
+
// These adapters do not yet publish a revision for every possible late
|
|
13
|
+
// child/tool event. A completed native observation alone cannot exclude a
|
|
14
|
+
// newly scheduled writer; the caller can still use physical absence.
|
|
15
|
+
if (["zcode-acp", "droid-cli"].includes(harness))
|
|
16
|
+
return false;
|
|
17
|
+
if (harness !== "antigravity-cli")
|
|
18
|
+
return true;
|
|
19
|
+
const daemon = state.daemon_id ?? state.config?.daemonId, proof = shutdown.native_resource_reconciliation, resources = result.native_resources;
|
|
20
|
+
return Boolean(daemon && shutdown.phases.native_resources_reconciled === true && proof?.daemon_id === daemon
|
|
21
|
+
&& proof.dispatch_generation === shutdown.generation && Number.isSafeInteger(proof.dispatch_generation)
|
|
22
|
+
&& proof.native_revision === state.native_execution_revision && Number.isSafeInteger(proof.native_revision)
|
|
23
|
+
&& resources?.schema_id === "native-resources@1" && resources.owner_daemon_id === daemon
|
|
24
|
+
&& resources.revision === proof.native_revision && Number.isSafeInteger(resources.generation)
|
|
25
|
+
&& result.native_settlement?.owner_daemon_id === daemon && result.native_settlement.generation === resources.generation
|
|
26
|
+
&& Array.isArray(resources.entries)
|
|
27
|
+
&& resources.entries.every(item => item.owner_daemon_id === daemon && item.generation === resources.generation && item.state === "stopped"));
|
|
28
|
+
}
|
|
5
29
|
export function readNativeDaemonState(stateDir) {
|
|
6
30
|
if (!path.isAbsolute(stateDir))
|
|
7
31
|
throw new AppError("daemon_owner_mismatch", "Native state directory must be absolute", 1);
|
|
@@ -36,3 +60,30 @@ export function requireManagedDaemonBinding(context, input) {
|
|
|
36
60
|
}
|
|
37
61
|
return { state, process: record };
|
|
38
62
|
}
|
|
63
|
+
/** A dead bridge is not enough: its provider may own a detached process group. */
|
|
64
|
+
export function managedDaemonExecutionAbsent(context, input) {
|
|
65
|
+
const binding = requireManagedDaemonBinding(context, input);
|
|
66
|
+
const records = managedProcessStatus(context).filter(record => {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(record.metadata_json).state_dir === input.stateDir;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (!records.length || records.some(record => {
|
|
75
|
+
const group = JSON.parse(record.metadata_json).process_group_id;
|
|
76
|
+
// A dead leader without its retained tree boundary cannot exclude surviving
|
|
77
|
+
// children. Fall back to strict native reconciliation, not assumed absence.
|
|
78
|
+
return record.project_id !== input.projectId || record.run_id !== input.runId
|
|
79
|
+
|| managedProcessRuntimeHome(record, context) !== context.ddFlowHome || !record.pid || !record.pid_started_at
|
|
80
|
+
|| !Number.isSafeInteger(group) || group <= 0 || processTreeIsAlive(record);
|
|
81
|
+
}))
|
|
82
|
+
return false;
|
|
83
|
+
if (binding.state.provider_process || binding.state.provider_pid) {
|
|
84
|
+
const provider = records.find(record => record.id === binding.state.provider_process?.id);
|
|
85
|
+
if (!provider || !managedProviderBelongsToDaemon(provider, binding.process, binding.state))
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|