@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.139
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 +19 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/runtime-process.js +1 -1
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +56 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +53 -22
- package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
- package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
- package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
- package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-children.d.mts +2 -0
- package/dist/harness-runtime/lib/native-children.mjs +20 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +32 -9
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +26 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-capacity.js +13 -0
- package/dist/services/controller-child-observer.js +18 -2
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +93 -15
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +250 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/managed-processes.js +31 -10
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +296 -0
- package/dist/services/memory-init-preflight.js +330 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +181 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +178 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/merge-server.js +2 -4
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
- package/dist/services/recovery-snapshot-database.js +417 -2
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-adapter.js +1 -1
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +5 -5
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +158 -64
- package/dist/services/run-recovery-guard.js +5 -3
- package/dist/services/run-recovery-runtime.js +3 -3
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +413 -30
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code-review.js +1 -1
- package/dist/services/vnext-code.js +6 -6
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +73 -11
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +7 -5
- package/dist/services/vnext-plan.js +3 -3
- package/dist/services/vnext-protocolize.js +20 -6
- package/dist/services/vnext-workspace-policy.js +7 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
|
@@ -11,7 +11,7 @@ import { commandOption, commandPosition, continuationCommand, lifecycleCommandFr
|
|
|
11
11
|
import { requireProjectByRoot } from "./projects.js";
|
|
12
12
|
import { assertRunMutationAllowed, assertStageSettlementAllowed, assertWorkSettlementAllowed, recoveryGuard, recoveryPacketReceipt } from "./run-recovery.js";
|
|
13
13
|
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
14
|
-
import { managedProcessStatus, processIsAlive } from "./managed-processes.js";
|
|
14
|
+
import { managedProcessStatus, processIsAlive, processStartedAt } from "./managed-processes.js";
|
|
15
15
|
import { appendEvent } from "./run-controller-state.js";
|
|
16
16
|
import { recoverCommittedWorkStart } from "./work-registry.js";
|
|
17
17
|
import { observeCommandHook } from "./hooks.js";
|
|
@@ -21,7 +21,24 @@ import { isRegisteredRepair } from "./repair-intents.js";
|
|
|
21
21
|
import { stageAttempt } from "./stage-work-graph.js";
|
|
22
22
|
import { contextualPathOptionsForLifecycle } from "../cli/command-inputs.js";
|
|
23
23
|
import { canonicalPath } from "../storage/paths.js";
|
|
24
|
-
import { runtimeCommand } from "./runtime-command.js";
|
|
24
|
+
import { runtimeCommand, shellArgument } from "./runtime-command.js";
|
|
25
|
+
// Process-local custody: an executing SQL row or ambient variable alone cannot
|
|
26
|
+
// recreate admission after a CLI restart, or in another resource database.
|
|
27
|
+
const currentAdmissions = new WeakMap();
|
|
28
|
+
export function currentLifecycleAdmission(context, id) {
|
|
29
|
+
const owned = currentAdmissions.get(context.db)?.get(id);
|
|
30
|
+
if (!owned || context.env.DD_FLOW_CURRENT_INVOCATION !== id || owned.pid !== process.pid
|
|
31
|
+
|| process.cwd() !== owned.cwd)
|
|
32
|
+
return null;
|
|
33
|
+
// This private module cannot survive owner death/restart/PID reuse. Its
|
|
34
|
+
// captured physical birth + exact PID/CWD/store remain a lifetime proof;
|
|
35
|
+
// revalidation reads SQL only, including inside the atomic claim writer.
|
|
36
|
+
const row = context.db.get("SELECT * FROM lifecycle_invocations WHERE id=?", [id]);
|
|
37
|
+
if (!row || row.status !== "executing" || row.event_key !== owned.eventKey || row.identity_json !== owned.identity
|
|
38
|
+
|| row.scope_json !== owned.scope || row.command !== owned.command || row.updated_at !== owned.admittedAt)
|
|
39
|
+
return null;
|
|
40
|
+
return { admittedAt: owned.admittedAt };
|
|
41
|
+
}
|
|
25
42
|
/** Auxiliary commands have no invocation capability, but may not escape their
|
|
26
43
|
* native daemon's project/RUN. Context aliases never grant mutation authority. */
|
|
27
44
|
export function assertAuxiliaryCommandScope(context, command, projectRoot) {
|
|
@@ -94,6 +111,12 @@ export function isCorrectableLifecycleRejection(code, details = {}) {
|
|
|
94
111
|
|| (code === "fresh_session_required" && details.handoff_required === true
|
|
95
112
|
&& details.recoverable === true);
|
|
96
113
|
}
|
|
114
|
+
function registeredLifecycleContinuation(context, error, projectId, runId) {
|
|
115
|
+
if (runId && isRegisteredRepair(context, error.details.continuation, { projectId, runId }))
|
|
116
|
+
return true;
|
|
117
|
+
return error.code === "fresh_session_required" && error.details.handoff_required === true && runId !== null
|
|
118
|
+
&& typeof error.details.work_id === "string" && Boolean(context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND work_id = ? AND launch_policy = 'fresh_agent_required' AND status IN ('created','running','paused')", [projectId, runId, error.details.work_id]));
|
|
119
|
+
}
|
|
97
120
|
function storage(context) {
|
|
98
121
|
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'lifecycle_invocations'")) {
|
|
99
122
|
throw new AppError("invocation_storage_unprepared", "Lifecycle invocation storage is not prepared; start dd-flow normally before issuing native work", 1);
|
|
@@ -447,24 +470,106 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
447
470
|
delete context.observeLifecycleHook;
|
|
448
471
|
if (!anchor)
|
|
449
472
|
return;
|
|
450
|
-
|
|
451
|
-
|
|
473
|
+
let error = outcome.error ? errorRecord(outcome.error) : undefined;
|
|
474
|
+
// The exact committed event can diagnose a broken transport, but cannot
|
|
475
|
+
// grant execution authority or select an assignment by command similarity.
|
|
476
|
+
if (error && parsed.kind === "standalone" && (nativeEvent || commandOption(parsed.invocation, "hook-event-id"))) {
|
|
477
|
+
const event = context.db.get("SELECT event_key, daemon_id, provider_session_id FROM hook_events WHERE id = ? AND project_id = ?", [anchor.hook_event_id, anchor.project_id]);
|
|
478
|
+
const assigned = event && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'lifecycle_invocations'")
|
|
479
|
+
? context.db.all("SELECT * FROM lifecycle_invocations WHERE event_key = ? AND status = 'observed'", [event.event_key]) : [];
|
|
480
|
+
if (assigned.length === 1 && event?.daemon_id && context.env.DD_FLOW_DAEMON_ID !== event.daemon_id) {
|
|
481
|
+
const scope = JSON.parse(assigned[0].scope_json);
|
|
482
|
+
const identity = assigned[0].identity_json ? JSON.parse(assigned[0].identity_json) : null;
|
|
483
|
+
const project = context.db.get("SELECT root FROM projects WHERE id = ?", [anchor.project_id]);
|
|
484
|
+
if (assigned[0].fingerprint === fingerprint(parsed.invocation) && scope.projectRoot === project?.root && scope.daemonId === event.daemon_id && scope.runId === anchor.run_id && scope.generation === anchor.generation
|
|
485
|
+
&& Array.isArray(identity) && identity[0] === event.daemon_id && identity[1] === scope.rootSessionId && identity[2] === (event.provider_session_id ?? anchor.session_id))
|
|
486
|
+
error = errorRecord(new AppError("invocation_scope_unproven", "Managed shell did not receive its native daemon binding", 1, {
|
|
487
|
+
reason: "managed_shell_context_missing", phase: "prepare", effect: "no_effect", recoverable: false,
|
|
488
|
+
cause: { code: error.code, parameter: error.details.parameter, reason: error.details.reason }
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const registeredContinuation = error && registeredLifecycleContinuation(context, error, anchor.project_id, anchor.run_id);
|
|
493
|
+
const retainedSuccessor = error && typeof error.details.retry_command === "string"
|
|
494
|
+
&& typeof error.details.rejection_signature === "string"
|
|
495
|
+
&& context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'lifecycle_invocations'")
|
|
496
|
+
&& context.db.get(`SELECT 1 FROM lifecycle_invocations a JOIN hook_events h ON h.event_key = a.event_key
|
|
497
|
+
JOIN projects p ON p.id = h.project_id WHERE h.id = ? AND h.project_id = ? AND a.status = 'settled'
|
|
498
|
+
AND json_extract(a.scope_json,'$.projectRoot') = p.root AND json_extract(a.scope_json,'$.runId') = ?
|
|
499
|
+
AND json_extract(a.scope_json,'$.generation') = ? AND json_extract(a.outcome_json,'$.error.details.retry_command') = ?
|
|
500
|
+
AND json_extract(a.outcome_json,'$.error.details.rejection_signature') = ?`, [anchor.hook_event_id, anchor.project_id, anchor.run_id, anchor.generation, error.details.retry_command, error.details.rejection_signature]);
|
|
501
|
+
const hookOnlyArgumentDefect = error && !invocationId && !context.env.DD_FLOW_INVOCATION_SCOPE
|
|
502
|
+
&& !retainedSuccessor
|
|
503
|
+
&& ["missing_required_option", "missing_option_value", "empty_option_value", "invalid_number", "invalid_choice", "duplicate_option"].includes(String(error.details.reason));
|
|
504
|
+
const hookOnlyCorrection = error && parsed.kind === "standalone" && !auxiliary && !invocationId && !context.env.DD_FLOW_INVOCATION_SCOPE
|
|
505
|
+
&& !retainedSuccessor && !hookOnlyArgumentDefect
|
|
506
|
+
&& error.details.effect === "no_effect" && isRetryableLifecycleRejection(error.code, error.details)
|
|
507
|
+
&& error.code !== "usage" && !["invocation_command_mismatch", "invocation_argument_mismatch"].includes(error.code)
|
|
508
|
+
&& parsed.invocation.argv.some(value => /^(?:--(?:(?:result|semantic|decision|verification|context|answer|intake|question|reason)-file|data))(?:=|$)/.test(value))
|
|
509
|
+
&& !isRegisteredRepair(context, error.details.continuation, { projectId: anchor.project_id, runId: anchor.run_id });
|
|
510
|
+
let correctionCwd;
|
|
511
|
+
if (error && hookOnlyCorrection) {
|
|
512
|
+
const receipt = context.db.get("SELECT sanitized_summary FROM hook_events WHERE id = ? AND project_id = ?", [anchor.hook_event_id, anchor.project_id]);
|
|
513
|
+
let payload = {};
|
|
514
|
+
try {
|
|
515
|
+
const decoded = JSON.parse(receipt?.sanitized_summary ?? "{}");
|
|
516
|
+
if (decoded && typeof decoded === "object" && !Array.isArray(decoded))
|
|
517
|
+
payload = decoded;
|
|
518
|
+
}
|
|
519
|
+
catch { /* Truncated summaries cannot supply a route. */ }
|
|
520
|
+
if (payload.lifecycle_execution_cwd !== undefined) {
|
|
521
|
+
let confirmed = false;
|
|
522
|
+
const cwd = payload.lifecycle_execution_cwd;
|
|
523
|
+
if (typeof cwd === "string" && path.isAbsolute(cwd) && payload.lifecycle_execution_cwd_provenance === "literal_cd_intent_requires_cli_confirmation") {
|
|
524
|
+
try {
|
|
525
|
+
confirmed = fs.realpathSync(cwd) === fs.realpathSync(process.cwd());
|
|
526
|
+
}
|
|
527
|
+
catch { /* No physical confirmation, no executable correction. */ }
|
|
528
|
+
}
|
|
529
|
+
if (confirmed)
|
|
530
|
+
correctionCwd = cwd;
|
|
531
|
+
else
|
|
532
|
+
error = errorRecord(new AppError("invocation_directory_mismatch", "Hook-only correction has no confirmed native execution directory", 1, {
|
|
533
|
+
reason: "literal_execution_cwd_unconfirmed", phase: "prepare", effect: "no_effect", recoverable: false,
|
|
534
|
+
cause: { code: error.code, parameter: error.details.parameter, reason: error.details.reason }
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const correctable = error && !hookOnlyArgumentDefect && error.details.recoverable !== false && error.details.effect !== "unknown"
|
|
539
|
+
&& (registeredContinuation || error.details.effect === "no_effect" && isCorrectableLifecycleRejection(error.code, error.details)
|
|
540
|
+
&& typeof error.details.retry_command === "string");
|
|
452
541
|
const correlationKey = parsed.kind === "standalone" ? fingerprint(parsed.invocation)
|
|
453
542
|
: JSON.stringify([auxiliary.operation, auxiliary.operation === "stage_block" ? auxiliary.args.positional[2] ?? null : auxiliary.args.options.get("run")?.[0] ?? null, auxiliary.args.options.get("stage")?.[0] ?? null]);
|
|
454
|
-
const record = { ...anchor, correlation_key: correlationKey, ...(parsed.kind === "standalone" ? { fingerprint: correlationKey } : {}), ...(error ? { error } : outcome), effect: error ? error.details.effect ?? (correctable ? "no_effect" : "unknown") : "committed", disposition: error ? auxiliary ? "pending_continuation" : correctable ? "correctable" : "fatal" : "completed" };
|
|
543
|
+
const record = { ...anchor, stage: anchor.stage ?? null, attempt: anchor.attempt ?? null, correlation_key: correlationKey, ...(parsed.kind === "standalone" ? { fingerprint: correlationKey } : {}), ...(error ? { error } : outcome), effect: error ? error.details.effect ?? (correctable ? "no_effect" : "unknown") : "committed", disposition: error ? auxiliary ? "pending_continuation" : correctable ? "correctable" : "fatal" : "completed" };
|
|
455
544
|
try {
|
|
456
545
|
context.db.writeTransaction(() => {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
546
|
+
const retained = context.db.get("SELECT outcome_json FROM hook_events WHERE id = ?", [anchor.hook_event_id]);
|
|
547
|
+
const previous = retained?.outcome_json ? JSON.parse(retained.outcome_json) : null;
|
|
548
|
+
if (previous?.disposition !== "retrying" && previous?.error && error && previous.correlation_key === correlationKey
|
|
549
|
+
&& preparationRejectionSignature(new AppError(previous.error.code, previous.error.message, 1, previous.error.details))
|
|
550
|
+
=== preparationRejectionSignature(new AppError(error.code, error.message, 1, error.details))) {
|
|
551
|
+
error = previous.error;
|
|
552
|
+
Object.assign(record, previous);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (error && parsed.kind === "standalone" && hookOnlyCorrection && error.details.recoverable !== false) {
|
|
460
556
|
const signature = preparationRejectionSignature(new AppError(error.code, error.message, 1, error.details));
|
|
461
557
|
error.details.hook_retry_signature = signature;
|
|
462
|
-
const repeated = context.db.get(
|
|
558
|
+
const repeated = context.db.get(`SELECT 1 FROM hook_events WHERE project_id = ? AND id <> ?
|
|
559
|
+
AND json_extract(outcome_json, '$.run_id') = ? AND json_extract(outcome_json, '$.generation') = ?
|
|
560
|
+
AND json_extract(outcome_json, '$.operation') = ? AND json_extract(outcome_json, '$.fingerprint') = ?
|
|
561
|
+
AND json_extract(outcome_json, '$.harness') = ? AND json_extract(outcome_json, '$.session_id') = ?
|
|
562
|
+
AND json_type(outcome_json, '$.stage') IS NOT NULL AND json_type(outcome_json, '$.attempt') IS NOT NULL
|
|
563
|
+
AND json_extract(outcome_json, '$.stage') IS ? AND json_extract(outcome_json, '$.attempt') IS ?
|
|
564
|
+
AND json_extract(outcome_json, '$.error.details.hook_retry_signature') = ? LIMIT 1`, [anchor.project_id, anchor.hook_event_id, anchor.run_id, anchor.generation, anchor.operation, correlationKey,
|
|
565
|
+
anchor.harness, anchor.session_id, anchor.stage ?? null, anchor.attempt ?? null, signature]);
|
|
463
566
|
if (!repeated) {
|
|
464
|
-
const argv = parsed.invocation.argv.filter((value, index, all) =>
|
|
465
|
-
|
|
466
|
-
error.details.
|
|
567
|
+
const argv = parsed.invocation.argv.filter((value, index, all) => !/^--(?:hook-event-id|invocation-id)(?:=|$)/.test(value)
|
|
568
|
+
&& !/^--(?:hook-event-id|invocation-id)$/.test(all[index - 1] ?? ""));
|
|
569
|
+
error.details.retry_command = `${correctionCwd ? `cd ${shellArgument(correctionCwd)} && ` : ""}${runtimeCommand(context, argv)}`;
|
|
570
|
+
error.details.retry_instruction = "Correct the evidenced input, then execute the exact supplied retry_command once in this Session without changing its launch route. The rejected attempt had no lifecycle effect.";
|
|
467
571
|
error.details.recoverable = true;
|
|
572
|
+
record.disposition = "correctable";
|
|
468
573
|
}
|
|
469
574
|
else {
|
|
470
575
|
error.details.recoverable = false;
|
|
@@ -472,8 +577,11 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
472
577
|
record.disposition = "fatal";
|
|
473
578
|
}
|
|
474
579
|
}
|
|
475
|
-
|
|
476
|
-
|
|
580
|
+
if (error && !auxiliary && record.disposition === "fatal") {
|
|
581
|
+
error.details.recoverable = false;
|
|
582
|
+
delete error.details.retry_command;
|
|
583
|
+
delete error.details.retry_instruction;
|
|
584
|
+
}
|
|
477
585
|
if (previous?.storage_retry === 1)
|
|
478
586
|
Object.assign(record, { storage_retry: 1, work_id: previous.work_id });
|
|
479
587
|
const encoded = JSON.stringify(record);
|
|
@@ -491,6 +599,11 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
491
599
|
catch (persistence) {
|
|
492
600
|
if (persistence instanceof AppError && persistence.code === "lifecycle_outcome_conflict")
|
|
493
601
|
throw persistence;
|
|
602
|
+
if (error) {
|
|
603
|
+
error.details.recoverable = false;
|
|
604
|
+
delete error.details.retry_command;
|
|
605
|
+
delete error.details.retry_instruction;
|
|
606
|
+
}
|
|
494
607
|
// Diagnostic evidence is not replay authority. If SQL is unavailable,
|
|
495
608
|
// the controller still sees this failure and fences instead of waiting
|
|
496
609
|
// for a model to report it or invent a new invocation.
|
|
@@ -508,11 +621,15 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
508
621
|
// Serializers intentionally clone JSON-safe diagnostics. These response
|
|
509
622
|
// fields are the deliberate API overlay on the original rejection, only
|
|
510
623
|
// after its exact durable hook outcome has committed.
|
|
511
|
-
if (outcome.error && error)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
624
|
+
if (outcome.error && error) {
|
|
625
|
+
outcome.error.code = error.code;
|
|
626
|
+
outcome.error.message = error.message;
|
|
627
|
+
Object.assign(outcome.error.details, error.details);
|
|
628
|
+
if (record.disposition === "fatal") {
|
|
629
|
+
delete outcome.error.details.retry_command;
|
|
630
|
+
delete outcome.error.details.retry_instruction;
|
|
515
631
|
}
|
|
632
|
+
}
|
|
516
633
|
};
|
|
517
634
|
}
|
|
518
635
|
function lifecycleDiagnostics(context, projectId, runId, generation) {
|
|
@@ -618,16 +735,16 @@ export function managedLifecycleCommand(context, command, options = {}) {
|
|
|
618
735
|
return publicInvocationCommand(context, load(context, issued.id), command);
|
|
619
736
|
});
|
|
620
737
|
}
|
|
621
|
-
export function retryLifecycleInvocationCommand(context, id) {
|
|
738
|
+
export function retryLifecycleInvocationCommand(context, id, checkReceiptId) {
|
|
622
739
|
const prior = load(context, id);
|
|
623
740
|
if (prior.status !== "executing")
|
|
624
741
|
throw new AppError("invocation_retry_not_authorized", "Only the executing lifecycle operation can publish its next attempt", 1);
|
|
625
|
-
return successorLifecycleInvocationCommand(context, prior);
|
|
742
|
+
return successorLifecycleInvocationCommand(context, prior, checkReceiptId);
|
|
626
743
|
}
|
|
627
744
|
/** A predecessor may publish exactly one successor only while the caller has
|
|
628
745
|
* proved that no lifecycle effect was admitted. Shared by CLI argument
|
|
629
746
|
* validation and conclusive lifecycle/result rejection, never by hooks. */
|
|
630
|
-
function successorLifecycleInvocationCommand(context, prior) {
|
|
747
|
+
function successorLifecycleInvocationCommand(context, prior, checkReceiptId) {
|
|
631
748
|
const parsed = invocation(prior.command);
|
|
632
749
|
const argv = [...parsed.argv];
|
|
633
750
|
const at = argv.indexOf("--invocation-id");
|
|
@@ -636,7 +753,23 @@ function successorLifecycleInvocationCommand(context, prior) {
|
|
|
636
753
|
// The parser proves the marker belongs to argv. Remove that one rendered
|
|
637
754
|
// token from the retained text so response-file/stdin/heredoc presentation
|
|
638
755
|
// remains byte-for-byte stable across a retry.
|
|
639
|
-
|
|
756
|
+
let command = retainedCommandWithoutInvocationId(prior);
|
|
757
|
+
if (checkReceiptId) {
|
|
758
|
+
// Keep the trusted route and all stdin/redirection bytes. Replace only
|
|
759
|
+
// already-parsed literal retry options; ambiguous rendering fails closed.
|
|
760
|
+
let retained = prior.command;
|
|
761
|
+
for (const key of ["retry-check", "reason", "reason-file"]) {
|
|
762
|
+
const value = commandOption(parsed, key);
|
|
763
|
+
if (value === undefined)
|
|
764
|
+
continue;
|
|
765
|
+
const spellings = [...new Set([shellArgument(value), quote([value]), JSON.stringify(value)])];
|
|
766
|
+
const fragments = spellings.flatMap(rendered => [` --${key} ${rendered}`, ` --${key}=${rendered}`]).filter(fragment => retained.includes(fragment));
|
|
767
|
+
if (fragments.length !== 1 || retained.indexOf(fragments[0]) !== retained.lastIndexOf(fragments[0]))
|
|
768
|
+
throw new AppError("invocation_command_invalid", "Retained retry option cannot be rewritten unambiguously", 1);
|
|
769
|
+
retained = retained.replace(fragments[0], "");
|
|
770
|
+
}
|
|
771
|
+
command = retained.replace(` --invocation-id ${prior.id}`, ` --retry-check ${shellArgument(checkReceiptId)} --reason ${shellArgument("Recheck after candidate-source correction")}`);
|
|
772
|
+
}
|
|
640
773
|
return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
|
|
641
774
|
}
|
|
642
775
|
// Caller holds the write transaction and has checked authority. Issued (or
|
|
@@ -657,31 +790,48 @@ const receiptTimeout = (id) => ({ code: "invocation_receipt_timeout", message: "
|
|
|
657
790
|
* outcome and its successor; never a usable successor beside an unknown effect. */
|
|
658
791
|
export function settleLifecycleRejection(context, id, error) {
|
|
659
792
|
storage(context);
|
|
793
|
+
// Only this settlement may publish a successor; inherited help is not authority.
|
|
794
|
+
delete error.details.retry_command;
|
|
795
|
+
delete error.details.retry_instruction;
|
|
660
796
|
const details = { ...error.details };
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
details.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
797
|
+
try {
|
|
798
|
+
context.db.writeTransaction(() => {
|
|
799
|
+
const prior = load(context, id);
|
|
800
|
+
if (prior.status !== "executing")
|
|
801
|
+
throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
|
|
802
|
+
const storageRetry = error.code === "storage_write_failed" && error.details.effect === "no_effect"
|
|
803
|
+
&& !context.db.get("SELECT 1 FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND json_extract(outcome_json, '$.error.details.storage_retry') = 1 LIMIT 1", [prior.scope_json, prior.fingerprint]);
|
|
804
|
+
if (isRetryableLifecycleRejection(error.code, error.details) || storageRetry) {
|
|
805
|
+
const failures = ["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(error.code) ? error.details.failures : undefined;
|
|
806
|
+
const retry = retryLifecycleInvocationCommand(context, id, failures?.[0]?.id);
|
|
807
|
+
details.retry_command = retry;
|
|
808
|
+
details.effect = "no_effect";
|
|
809
|
+
details.recoverable = true;
|
|
810
|
+
if (storageRetry)
|
|
811
|
+
details.storage_retry = 1;
|
|
812
|
+
details.retry_instruction = isRegisteredRepair(context, details.continuation)
|
|
813
|
+
? "The repair is already registered. End this coordinator turn so the controller can launch its fresh child. Use retry_command only after that repair completes, with fresh semantic verification."
|
|
814
|
+
: "Correct only the evidenced defect, then use retry_command with the required result input. Reissuing the old command only reads its retained outcome.";
|
|
815
|
+
if (["memory_review_corrections_required", "memory_correction_checks_invalid"].includes(error.code) && typeof details.correction_instruction === "string")
|
|
816
|
+
details.retry_instruction = details.correction_instruction;
|
|
817
|
+
}
|
|
818
|
+
else if (isCorrectableLifecycleRejection(error.code, error.details)
|
|
819
|
+
&& registeredLifecycleContinuation(context, error, requireProjectByRoot(context, JSON.parse(prior.scope_json).projectRoot).id, JSON.parse(prior.scope_json).runId)) {
|
|
820
|
+
details.effect = "no_effect";
|
|
821
|
+
details.recoverable = true;
|
|
822
|
+
details.handoff_required = true;
|
|
823
|
+
details.recovery_instruction = "Do not retry this command in the current Session. End the coordinator Turn; the controller will launch the already-registered Work in a fresh child Session.";
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
details.recoverable = false;
|
|
827
|
+
}
|
|
828
|
+
settleLifecycleInvocation(context, id, { error: { ...error, details } });
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
catch (persistence) {
|
|
832
|
+
error.details.recoverable = false;
|
|
833
|
+
throw persistence;
|
|
834
|
+
}
|
|
685
835
|
Object.assign(error.details, details);
|
|
686
836
|
}
|
|
687
837
|
/** Recovery preserves the original packet as evidence. Only its retained,
|
|
@@ -1593,38 +1743,46 @@ export function retainObservedLifecycleSemantics(context, id, argv) {
|
|
|
1593
1743
|
}
|
|
1594
1744
|
}
|
|
1595
1745
|
export function settleLifecyclePreparationRejection(context, id, error) {
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1746
|
+
delete error.details.retry_command;
|
|
1747
|
+
delete error.details.retry_instruction;
|
|
1748
|
+
try {
|
|
1749
|
+
return context.db.writeTransaction(() => {
|
|
1750
|
+
const row = load(context, id);
|
|
1751
|
+
if (row.status === "settled") {
|
|
1752
|
+
const details = row.outcome_json ? JSON.parse(row.outcome_json)?.error?.details : undefined;
|
|
1753
|
+
return details?.effect === "no_effect" && details.retry_command ? details : { effect: "unknown", recoverable: false };
|
|
1754
|
+
}
|
|
1755
|
+
if (!["issued", "observed"].includes(row.status))
|
|
1756
|
+
return { effect: "unknown", recoverable: false };
|
|
1757
|
+
const scope = JSON.parse(row.scope_json);
|
|
1758
|
+
if (scope.runId)
|
|
1759
|
+
assertLifecycleInvocationCurrent(context, scope, row.command);
|
|
1760
|
+
const signature = preparationRejectionSignature(error);
|
|
1761
|
+
const repeated = context.db.all("SELECT outcome_json FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND id <> ? AND status = 'settled'", [row.scope_json, row.fingerprint, row.id])
|
|
1762
|
+
.some(prior => { try {
|
|
1763
|
+
return JSON.parse(prior.outcome_json ?? "null")?.error?.details?.rejection_signature === signature;
|
|
1764
|
+
}
|
|
1765
|
+
catch {
|
|
1766
|
+
return false;
|
|
1767
|
+
} });
|
|
1768
|
+
const retryable = !repeated && isRetryableLifecycleRejection(error.code, error.details);
|
|
1769
|
+
const details = { ...error.details, effect: "no_effect", recoverable: retryable, rejection_signature: signature,
|
|
1770
|
+
...(repeated ? { repeated_without_progress: true } : {}),
|
|
1771
|
+
...(retryable ? {
|
|
1772
|
+
retry_command: successorLifecycleInvocationCommand(context, row),
|
|
1773
|
+
retry_instruction: "Correct the reported input, then execute retry_command verbatim in this same Session. The rejected call changed no lifecycle state."
|
|
1774
|
+
} : {}) };
|
|
1775
|
+
context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ?", [JSON.stringify({ error: { ...errorRecord(error), details } }), context.now(), row.id]);
|
|
1776
|
+
return details;
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
catch (persistence) {
|
|
1780
|
+
error.details.recoverable = false;
|
|
1781
|
+
throw persistence;
|
|
1782
|
+
}
|
|
1625
1783
|
}
|
|
1626
1784
|
function preparationRejectionSignature(error) {
|
|
1627
|
-
const omitted = new Set(["retry_command", "retry_instruction", "invocation_id", "hook_event_id", "rejection_signature", "hook_retry_signature"]);
|
|
1785
|
+
const omitted = new Set(["retry_command", "retry_instruction", "invocation_id", "hook_event_id", "rejection_signature", "hook_retry_signature", "event_key", "tool_call_id", "controller_turn", "timestamp", "created_at", "updated_at", "repeated_without_progress", "recoverable"]);
|
|
1628
1786
|
const normalize = (value) => {
|
|
1629
1787
|
if (Array.isArray(value))
|
|
1630
1788
|
return value.map(normalize);
|
|
@@ -1797,9 +1955,29 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
1797
1955
|
}
|
|
1798
1956
|
// Another native observation can replace an unclaimed receipt. Claim
|
|
1799
1957
|
// exactly the identity/route checked above, or reload and validate it.
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1958
|
+
const workStart = invocation(row.command).operation === "work_start";
|
|
1959
|
+
const birth = workStart ? processStartedAt(process.pid) : null, cwd = workStart ? process.cwd() : "";
|
|
1960
|
+
input.assertCurrent(JSON.parse(row.scope_json));
|
|
1961
|
+
let admittedAt = "";
|
|
1962
|
+
const claimed = context.db.writeTransaction(() => {
|
|
1963
|
+
// Acquire the writer before taking the anchor; waiting for SQLite or
|
|
1964
|
+
// physical birth observation must not make an old Hook appear fresh.
|
|
1965
|
+
admittedAt = context.now();
|
|
1966
|
+
return context.db.run("UPDATE lifecycle_invocations SET status = 'executing', updated_at = ? WHERE id = ? AND status = 'observed' AND event_key IS ? AND identity_json IS ? AND scope_json=? AND command=?", [admittedAt, input.id, row.event_key, row.identity_json, row.scope_json, row.command]);
|
|
1967
|
+
});
|
|
1968
|
+
if (claimed.changes === 1) {
|
|
1969
|
+
if (birth) {
|
|
1970
|
+
let owned = currentAdmissions.get(context.db);
|
|
1971
|
+
if (!owned) {
|
|
1972
|
+
owned = new Map();
|
|
1973
|
+
currentAdmissions.set(context.db, owned);
|
|
1974
|
+
}
|
|
1975
|
+
if (owned.size >= 256)
|
|
1976
|
+
owned.delete(owned.keys().next().value);
|
|
1977
|
+
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 });
|
|
1978
|
+
}
|
|
1802
1979
|
return { eventKey: row.event_key, replay: false };
|
|
1980
|
+
}
|
|
1803
1981
|
continue;
|
|
1804
1982
|
}
|
|
1805
1983
|
if (row.status === "expired" || (row.deadline !== null && row.deadline <= Date.now())) {
|
|
@@ -1816,6 +1994,7 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
1816
1994
|
}
|
|
1817
1995
|
}
|
|
1818
1996
|
export function settleLifecycleInvocation(context, id, outcome) {
|
|
1997
|
+
currentAdmissions.get(context.db)?.delete(id);
|
|
1819
1998
|
const encoded = JSON.stringify(outcome);
|
|
1820
1999
|
if (encoded === undefined)
|
|
1821
2000
|
throw new AppError("invocation_outcome_invalid", "Invocation outcome must be JSON", 1);
|
|
@@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import net from "node:net";
|
|
6
6
|
import { getResourceDatabase } from "../storage/database.js";
|
|
7
7
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
8
|
+
import { maintenanceRetryable } from "../harness-runtime/lib/lease-renewal.mjs";
|
|
8
9
|
export function managedProcessBinding(record, home) {
|
|
9
10
|
return { resource_home: home, process_id: record.id, registration_sha256: crypto.createHash("sha256").update(JSON.stringify([home, record.id, record.kind, record.owner_id, record.lease_token, record.pid, record.pid_started_at, record.project_id, record.run_id, record.work_id, record.check_id, record.operation_id, record.metadata_json])).digest("hex") };
|
|
10
11
|
}
|
|
@@ -203,17 +204,28 @@ export function heartbeatManagedProcess(context, input) {
|
|
|
203
204
|
}
|
|
204
205
|
export function renewManagedProcess(context, input) {
|
|
205
206
|
prepareManagedProcessHeartbeat(context, input);
|
|
206
|
-
|
|
207
|
-
registry(context);
|
|
208
|
-
const reader = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
|
|
207
|
+
let db;
|
|
209
208
|
try {
|
|
210
|
-
if (
|
|
211
|
-
|
|
209
|
+
if (context.resourceDatabaseMode === "prepared_existing")
|
|
210
|
+
registry(context);
|
|
211
|
+
const reader = getResourceDatabase(resourceHome(context), "read_existing", context.databaseDeadlineAt);
|
|
212
|
+
try {
|
|
213
|
+
if (!reader.get("SELECT 1 FROM managed_processes WHERE id=? AND lease_token=? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
|
|
214
|
+
return { owned: false, process_id: input.id };
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
reader.close?.();
|
|
218
|
+
}
|
|
219
|
+
db = registry(context);
|
|
212
220
|
}
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
catch (error) {
|
|
222
|
+
// No lease mutation has started. The storage helper cannot know that
|
|
223
|
+
// operation boundary and deliberately defaults to an unknown effect.
|
|
224
|
+
if (error instanceof AppError && error.code === "process_heartbeat_timeout"
|
|
225
|
+
&& maintenanceRetryable({ ...error, details: { ...error.details, effect: "no_effect" } }))
|
|
226
|
+
throw new AppError(error.code, error.message, error.exitCode, { ...error.details, effect: "no_effect", maintenance_phase: "renewal_prepare" });
|
|
227
|
+
throw error;
|
|
215
228
|
}
|
|
216
|
-
const db = registry(context);
|
|
217
229
|
return db.writeTransaction(() => {
|
|
218
230
|
const now = context.now();
|
|
219
231
|
if (!db.get("SELECT 1 FROM managed_processes WHERE id=? AND lease_token=? AND state IN ('starting','running','stopping')", [input.id, input.leaseToken]))
|
|
@@ -228,8 +240,9 @@ export function renewManagedProcess(context, input) {
|
|
|
228
240
|
/** Read-only lease check for a caller already holding a RUN writer. Renewal
|
|
229
241
|
* must happen before entering that writer, never from this assertion. */
|
|
230
242
|
export function assertManagedProcessLease(context, input) {
|
|
231
|
-
|
|
243
|
+
let reader;
|
|
232
244
|
try {
|
|
245
|
+
reader = getResourceDatabase(resourceHome(context), "read_isolated", context.databaseDeadlineAt);
|
|
233
246
|
const current = reader.get("SELECT pid, lease_expires_at, state FROM managed_processes WHERE id = ? AND lease_token = ?", [input.id, input.leaseToken]);
|
|
234
247
|
if (!current || current.pid !== input.pid || !["starting", "running"].includes(current.state)) {
|
|
235
248
|
throw new AppError("controller_lease_lost", "Managed owner lease changed before productive dispatch", 1, { process_id: input.id });
|
|
@@ -237,8 +250,16 @@ export function assertManagedProcessLease(context, input) {
|
|
|
237
250
|
if (!Number.isFinite(Date.parse(current.lease_expires_at)) || Date.parse(current.lease_expires_at) <= Date.now())
|
|
238
251
|
throw new AppError("controller_lease_unconfirmed", "Managed owner requires renewal before productive dispatch", 1, { process_id: input.id, reason: "renewal_required" });
|
|
239
252
|
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const failure = errorRecord(error);
|
|
255
|
+
// This assertion only reads. Unavailable observation is not evidence of
|
|
256
|
+
// owner loss; the caller must reconfirm outside its RUN writer.
|
|
257
|
+
if (maintenanceRetryable({ ...failure, details: { ...failure.details, effect: "no_effect" } }))
|
|
258
|
+
throw new AppError("controller_lease_unconfirmed", "Managed owner observation requires confirmation before productive dispatch", 1, { process_id: input.id, reason: "observation_unavailable", cause: failure });
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
240
261
|
finally {
|
|
241
|
-
reader
|
|
262
|
+
reader?.close?.();
|
|
242
263
|
}
|
|
243
264
|
}
|
|
244
265
|
export function prepareManagedProcessFinish(context, input) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
4
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
5
|
+
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
6
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
7
|
+
import { selectMemoryCompositionRegistration } from "./memory-composition-registration.js";
|
|
8
|
+
import { PreparedMemoryResultBinding } from "./memory-result-binding.js";
|
|
9
|
+
import { PreparedMemoryStageResult, memorySourceKey } from "./memory-stage-results.js";
|
|
10
|
+
import { PreparedMemoryCompositionAcceptance } from "./memory-composition-acceptance.js";
|
|
11
|
+
import { captureMemoryCompositionSources } from "./memory-compose-handler.js";
|
|
12
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
13
|
+
function refuse(reason) {
|
|
14
|
+
throw new AppError("memory_accepted_composition_invalid", "Accepted composition lacks original plan and current settled output custody", 2, { reason, effect: "no_effect" });
|
|
15
|
+
}
|
|
16
|
+
/** Shared current-document authority for VERIFY and initial REVIEW. Historical
|
|
17
|
+
* replay uses separate retained receipts; repaired outputs cannot masquerade
|
|
18
|
+
* as the original composition. This helper writes nothing and dispatches no Work.
|
|
19
|
+
*/
|
|
20
|
+
export class PreparedAcceptedMemoryComposition {
|
|
21
|
+
#bundle;
|
|
22
|
+
#runRoot;
|
|
23
|
+
#blueprint;
|
|
24
|
+
#binding;
|
|
25
|
+
#outputs;
|
|
26
|
+
#result;
|
|
27
|
+
constructor(bundle, runRoot, blueprint, binding, outputs, result) {
|
|
28
|
+
this.#bundle = bundle;
|
|
29
|
+
this.#runRoot = runRoot;
|
|
30
|
+
this.#blueprint = blueprint;
|
|
31
|
+
this.#binding = binding;
|
|
32
|
+
this.#outputs = outputs;
|
|
33
|
+
this.#result = result;
|
|
34
|
+
Object.freeze(this);
|
|
35
|
+
}
|
|
36
|
+
static prepare(context, input) {
|
|
37
|
+
const { scope, bundle } = input;
|
|
38
|
+
if (!(bundle instanceof RetainedExecutionBundle) || bundle.descriptor.descriptor.id !== "mb-init")
|
|
39
|
+
refuse("registered_memory_bundle_required");
|
|
40
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), root = readDescriptorRootWork(context, scope, bundle);
|
|
41
|
+
const registration = selectMemoryCompositionRegistration(context, { ...stageGraphScope(context, scope.projectId, scope.runId, "compose"), stage: "compose", parent_work_id: root.workId });
|
|
42
|
+
const blueprint = PreparedMemoryResultBinding.prepareRegisteredComposition(context, { ...scope, flow: bundle.descriptor, execution: bundle.execution, bundle, acceptedReplay: true,
|
|
43
|
+
registrationScope: registration.scope });
|
|
44
|
+
const outputs = PreparedMemoryCompositionAcceptance.prepare(context, { scope: registration.scope, flow: bundle.descriptor, engine: bundle.engine(), binding: blueprint.binding() });
|
|
45
|
+
const composed = readAcceptedStageData(context, { ...scope, stage: "compose" }), runRoot = run.run_root ?? run.run_dir;
|
|
46
|
+
const binding = PreparedMemoryResultBinding.prepare(context, { ...scope, stage: "compose", flow: bundle.descriptor, execution: bundle.execution, bundle, acceptedReplay: true,
|
|
47
|
+
sources: captureMemoryCompositionSources(context, bundle, runRoot, composed.bytes, outputs), historicalEvidence: outputs.historicalEvidence() });
|
|
48
|
+
if (!binding.acceptedBytes()?.equals(composed.bytes))
|
|
49
|
+
refuse("accepted_composition_changed");
|
|
50
|
+
const result = PreparedMemoryStageResult.prepare(composed.bytes, bundle.descriptor, binding.binding());
|
|
51
|
+
outputs.assertResult(result);
|
|
52
|
+
const prepared = new PreparedAcceptedMemoryComposition(bundle, runRoot, blueprint, binding, outputs, result);
|
|
53
|
+
prepared.assertCustody(context);
|
|
54
|
+
return prepared;
|
|
55
|
+
}
|
|
56
|
+
result() { return this.#result; }
|
|
57
|
+
outputReferences() {
|
|
58
|
+
const hashes = this.#outputs.outputHashes();
|
|
59
|
+
return this.#outputs.outputLocations().map(location => ({ ...location, sha256: hashes.get(memorySourceKey(location.root, location.path)) }));
|
|
60
|
+
}
|
|
61
|
+
historicalEvidence() { return this.#outputs.historicalEvidence(); }
|
|
62
|
+
captureSources(context, bytes, checks) {
|
|
63
|
+
return captureMemoryCompositionSources(context, this.#bundle, this.#runRoot, bytes, this.#outputs, checks, createHash("sha256").update(bytes).digest("hex"));
|
|
64
|
+
}
|
|
65
|
+
assertCustody(context) {
|
|
66
|
+
this.#blueprint.assertCustody(context);
|
|
67
|
+
this.#binding.assertCustody(context);
|
|
68
|
+
this.#outputs.assertCustody(context);
|
|
69
|
+
this.#outputs.assertResult(this.#result);
|
|
70
|
+
}
|
|
71
|
+
revalidate(context) {
|
|
72
|
+
this.#blueprint.revalidate(context);
|
|
73
|
+
this.#binding.revalidate(context);
|
|
74
|
+
this.#outputs.revalidate(context);
|
|
75
|
+
this.assertCustody(context);
|
|
76
|
+
}
|
|
77
|
+
}
|