@deksden-com/dd-flow-cli 0.9.0-beta.161 → 0.9.0-beta.162
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 +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +7 -4
- package/dist/services/hooks.js +54 -16
- package/dist/services/lifecycle-invocations.js +40 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.162
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Bind check-recovery help to its single committed evidence-file successor and canonical RUN path. Strip stale executable retry promises in common and hook-only fatal, repeated and persistence outcomes, including response overlays. Retain proven native assignment and directory refusal diagnostics without granting authority or masking their primary typed error (plan116).
|
|
8
|
+
|
|
3
9
|
## 0.9.0-beta.161
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-10-
|
|
3
|
+
"cli_version": "0.9.0-beta.162",
|
|
4
|
+
"cli_commit": "d78bcd1bb6f510fee45a3d77463b172cc92420dd",
|
|
5
|
+
"built_at": "2026-10-29T23:21:33.965Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.4",
|
|
8
8
|
"commit": "7cc415b3ef3ce37c9624c7cee51023e66f67cd57",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { assertAuxiliaryCommandScope, cliManagedInvocationScope, manualRecoveryI
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import { quote } from "shell-quote";
|
|
11
|
-
import { observeLifecycleCommand, observedLifecycleNativeEvent } from "../services/lifecycle-invocations.js";
|
|
11
|
+
import { observeLifecycleCommand, observedLifecycleNativeEvent, removeRetryPromises } from "../services/lifecycle-invocations.js";
|
|
12
12
|
import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
|
|
13
13
|
import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, bindLifecyclePublishedPacket, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleDiagnostic, observedLifecycleInvocation, resolveObservedLifecycleArgs, retainObservedLifecycleSemantics, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
|
|
14
14
|
import { readAcceptedAssignedWork } from "../services/assigned-lifecycle.js";
|
|
@@ -316,6 +316,8 @@ async function runCliRequest(args, io, env) {
|
|
|
316
316
|
const diagnostic = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
|
|
317
317
|
? observedLifecycleDiagnostic(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID, observedInvocation?.id) : null;
|
|
318
318
|
if (diagnostic && (!nativeEvent || Number(diagnostic.hook_event_id) > nativeEvent.id)) {
|
|
319
|
+
if (["invocation_assignment_missing", "invocation_directory_mismatch", "opencode_directory_mismatch", "agy_directory_mismatch", "droid_directory_mismatch"].includes(String(diagnostic.code)))
|
|
320
|
+
throw new AppError(String(diagnostic.code), String(diagnostic.reason), 1, { ...diagnostic, phase: "prepare", effect: "no_effect", recoverable: false });
|
|
319
321
|
throw new AppError("lifecycle_shell_syntax_invalid", `Native observer rejected the shell form: ${String(diagnostic.reason ?? "unsupported shell composition")}. Write structured input to a file and run the returned dd-flow command separately.`, 2, { ...diagnostic, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
320
322
|
}
|
|
321
323
|
let manualRecoveryHookEventKey;
|
|
@@ -550,8 +552,7 @@ async function runCliRequest(args, io, env) {
|
|
|
550
552
|
if (invocationSettlement && error.details.effect !== "committed") {
|
|
551
553
|
try {
|
|
552
554
|
if (isAppError(error)) {
|
|
553
|
-
|
|
554
|
-
delete error.details.retry_instruction;
|
|
555
|
+
removeRetryPromises(error.details);
|
|
555
556
|
// The common settlement classifies original validated proof before
|
|
556
557
|
// errorRecord clones diagnostic objects and loses capability identity.
|
|
557
558
|
settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, error);
|
|
@@ -560,8 +561,10 @@ async function runCliRequest(args, io, env) {
|
|
|
560
561
|
settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
|
|
561
562
|
}
|
|
562
563
|
catch (persistenceError) {
|
|
563
|
-
if (isAppError(error))
|
|
564
|
+
if (isAppError(error)) {
|
|
564
565
|
error.details.recoverable = false;
|
|
566
|
+
removeRetryPromises(error.details);
|
|
567
|
+
}
|
|
565
568
|
io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
|
|
566
569
|
}
|
|
567
570
|
}
|
package/dist/services/hooks.js
CHANGED
|
@@ -14,7 +14,7 @@ import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-pre
|
|
|
14
14
|
import { nativeHookNonParticipation } from "../cli/native-hook-preparation.js";
|
|
15
15
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
16
16
|
import { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
|
|
17
|
-
import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope } from "./lifecycle-invocations.js";
|
|
17
|
+
import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope, lifecycleDiagnosticFingerprint } from "./lifecycle-invocations.js";
|
|
18
18
|
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
|
|
19
19
|
import { registerProject, requireProjectByRoot } from "./project-registry.js";
|
|
20
20
|
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./hook-session-observation.js";
|
|
@@ -421,7 +421,7 @@ export function handleCodexHook(context, input) {
|
|
|
421
421
|
throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
|
|
422
422
|
let executionCwd;
|
|
423
423
|
try {
|
|
424
|
-
executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
|
|
424
|
+
executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop", () => { persist(); return eventKey; });
|
|
425
425
|
}
|
|
426
426
|
catch (error) {
|
|
427
427
|
// A proven external Work is not its Stage coordinator. Deny the tool
|
|
@@ -614,7 +614,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
|
|
|
614
614
|
return context.db.writeTransaction(() => persist());
|
|
615
615
|
}
|
|
616
616
|
let persistedResult;
|
|
617
|
-
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
|
|
617
|
+
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp", () => { persist(); return eventKey; });
|
|
618
618
|
const receipt = observeLifecycleInvocation(context, {
|
|
619
619
|
currentEventKey: eventKey,
|
|
620
620
|
...(invocationId ? { id: invocationId } : {}), command,
|
|
@@ -715,7 +715,7 @@ export function handleGrokEvent(context, input) {
|
|
|
715
715
|
return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
716
716
|
}
|
|
717
717
|
let inserted = false;
|
|
718
|
-
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
|
|
718
|
+
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp", () => { persist(); return eventKey; });
|
|
719
719
|
const receipt = observeLifecycleInvocation(context, {
|
|
720
720
|
currentEventKey: eventKey,
|
|
721
721
|
command, harness: "grok-acp", projectId: project.id,
|
|
@@ -751,7 +751,7 @@ function requiresAssignedLifecycleCwd(facts) {
|
|
|
751
751
|
// their tools/checks receive owned paths, not the shell's working directory.
|
|
752
752
|
return !facts || !["stage_finish", "stage_pause", "stage_resume", "work_finish", "work_fail"].includes(facts.invocation.operation);
|
|
753
753
|
}
|
|
754
|
-
function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness) {
|
|
754
|
+
function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness, recordRefusal) {
|
|
755
755
|
const auxiliary = continuationCommand(command);
|
|
756
756
|
if ((!facts && !auxiliary) || facts?.analysis.supported === false)
|
|
757
757
|
return undefined;
|
|
@@ -764,8 +764,12 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
|
|
|
764
764
|
}
|
|
765
765
|
const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
|
|
766
766
|
const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
|
|
767
|
-
if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
|
|
768
|
-
|
|
767
|
+
if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId) {
|
|
768
|
+
const error = assignmentMissing();
|
|
769
|
+
if (proven && recordRefusal)
|
|
770
|
+
retainAssignmentRefusal(context, projectRoot, command, recordRefusal, error);
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
769
773
|
if (!requiresAssignedLifecycleCwd(facts))
|
|
770
774
|
return undefined;
|
|
771
775
|
const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd);
|
|
@@ -774,14 +778,38 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
|
|
|
774
778
|
if (!expected)
|
|
775
779
|
return undefined;
|
|
776
780
|
const route = facts?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
+
const directoryError = route && resolveProjectRoot(route) !== expected
|
|
782
|
+
? new AppError("invocation_directory_mismatch", "Literal lifecycle route differs from the assigned workspace", 1, { expected, actual: route, actual_provenance: "shell_literal_route", phase: "prepare", effect: "no_effect", recoverable: false })
|
|
783
|
+
: !route && observedCwd && resolveProjectRoot(observedCwd) !== expected
|
|
784
|
+
? new AppError("invocation_directory_mismatch", "Native lifecycle directory differs from the assigned workspace", 1, { expected, actual: observedCwd, actual_provenance: "native_tool_cwd", phase: "prepare", effect: "no_effect", recoverable: false }) : null;
|
|
785
|
+
if (directoryError) {
|
|
786
|
+
if (proven && recordRefusal)
|
|
787
|
+
retainAssignmentRefusal(context, projectRoot, command, recordRefusal, directoryError);
|
|
788
|
+
throw directoryError;
|
|
789
|
+
}
|
|
781
790
|
// ACP without native cwd still binds assigned authority; CLI confirms the
|
|
782
791
|
// physical process directory, rather than inventing native observation.
|
|
783
792
|
return expected;
|
|
784
793
|
}
|
|
794
|
+
function assignmentMissing() {
|
|
795
|
+
return new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
796
|
+
}
|
|
797
|
+
/** A proven native refusal is diagnosis, never an executable assignment. */
|
|
798
|
+
function retainAssignmentRefusal(context, projectRoot, command, record, error) {
|
|
799
|
+
const commandFingerprint = lifecycleDiagnosticFingerprint(command);
|
|
800
|
+
if (!commandFingerprint)
|
|
801
|
+
return;
|
|
802
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
803
|
+
context.db.writeTransaction(() => {
|
|
804
|
+
const eventKey = record();
|
|
805
|
+
const row = context.db.get("SELECT outcome_json FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, eventKey]);
|
|
806
|
+
if (row?.outcome_json)
|
|
807
|
+
return;
|
|
808
|
+
context.db.run("UPDATE hook_events SET outcome_json = ? WHERE project_id = ? AND event_key = ? AND outcome_json IS NULL", [JSON.stringify({ lifecycle_observation: {
|
|
809
|
+
...error.details, code: error.code, reason: error.message, command_fingerprint: commandFingerprint, phase: "prepare", effect: "no_effect", recoverable: false
|
|
810
|
+
} }), project.id, eventKey]);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
785
813
|
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd) {
|
|
786
814
|
if (facts?.bootstrapStageStart)
|
|
787
815
|
return projectRoot;
|
|
@@ -913,19 +941,18 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
913
941
|
const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
|
|
914
942
|
const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
|
|
915
943
|
? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
|
|
916
|
-
|
|
917
|
-
throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { effect: "no_effect", recoverable: false });
|
|
918
|
-
}
|
|
944
|
+
const missingAssignment = !shellRefusal && lifecycle && lifecycle.invocation.operation.startsWith("stage_") && !lifecycle.bootstrapStageStart && !lifecycle.runId && !issuedRunId;
|
|
919
945
|
// These controlled platforms additionally qualify their native launch
|
|
920
946
|
// route against the stable project root when no CLI target is assigned.
|
|
921
947
|
const workspaceBound = requiresAssignedLifecycleCwd(lifecycle);
|
|
922
948
|
const expectedCwd = shellRefusal || !workspaceBound ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId, observedCwd) ?? expectedRoot;
|
|
923
949
|
const executionRoute = shellRefusal || !workspaceBound ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
|
|
950
|
+
let directoryError;
|
|
924
951
|
if (executionRoute && resolveProjectRoot(executionRoute) !== expectedCwd) {
|
|
925
|
-
|
|
952
|
+
directoryError = new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
|
|
926
953
|
}
|
|
927
954
|
if (workspaceBound && !executionRoute && observedCwd !== expectedCwd) {
|
|
928
|
-
|
|
955
|
+
directoryError = new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, effect: "no_effect", recoverable: false, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
|
|
929
956
|
}
|
|
930
957
|
const bootstrapStageStart = !shellRefusal && lifecycle?.bootstrapStageStart;
|
|
931
958
|
let matchKey = lifecycle && !shellRefusal ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
@@ -959,6 +986,17 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
959
986
|
return inserted;
|
|
960
987
|
};
|
|
961
988
|
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId });
|
|
989
|
+
if (directoryError) {
|
|
990
|
+
if (provenScope)
|
|
991
|
+
retainAssignmentRefusal(context, expectedRoot, command, () => { persist(); return baseKey; }, directoryError);
|
|
992
|
+
throw directoryError;
|
|
993
|
+
}
|
|
994
|
+
if (missingAssignment) {
|
|
995
|
+
const error = assignmentMissing();
|
|
996
|
+
if (provenScope)
|
|
997
|
+
retainAssignmentRefusal(context, expectedRoot, command, () => { persist(); return baseKey; }, error);
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
962
1000
|
if (lifecycle) {
|
|
963
1001
|
let inserted = false;
|
|
964
1002
|
const receipt = observeLifecycleInvocation(context, {
|
|
@@ -20,7 +20,7 @@ import { isRegisteredRepair } from "./repair-receipt.js";
|
|
|
20
20
|
import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
|
|
21
21
|
import { contextualPathOptionsForLifecycle } from "../cli/command-inputs.js";
|
|
22
22
|
import { canonicalPath } from "../storage/paths.js";
|
|
23
|
-
import { runtimeCommand, shellArgument } from "./runtime-command.js";
|
|
23
|
+
import { runtimeCommand, shellArgument, checkRetryArgs, recoveryEvidenceInput } from "./runtime-command.js";
|
|
24
24
|
import { assignedLifecycleBinding, claimAssignedLifecycle, currentAssignedLifecycle, bindAssignedCoordinatorPacket, inheritAssignedPackets, retainControllerStageAssignment, admitAssignedAuxiliary, bindAssignedStageResume, readAcceptedAssignedWork, assertTerminalSettlementUnreserved } from "./assigned-lifecycle.js";
|
|
25
25
|
import { appendAudit } from "./audit.js";
|
|
26
26
|
import { recordTechnicalNote } from "./technical-notes.js";
|
|
@@ -628,8 +628,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
628
628
|
}
|
|
629
629
|
else {
|
|
630
630
|
error.details.recoverable = false;
|
|
631
|
-
|
|
632
|
-
delete error.details.retry_instruction;
|
|
631
|
+
removeRetryPromises(error.details);
|
|
633
632
|
if (inputKey)
|
|
634
633
|
error.details.repeated_without_progress = true;
|
|
635
634
|
else
|
|
@@ -641,8 +640,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
641
640
|
}
|
|
642
641
|
if (error && !auxiliary && record.disposition === "fatal") {
|
|
643
642
|
error.details.recoverable = false;
|
|
644
|
-
|
|
645
|
-
delete error.details.retry_instruction;
|
|
643
|
+
removeRetryPromises(error.details);
|
|
646
644
|
}
|
|
647
645
|
if (previous?.storage_retry === 1)
|
|
648
646
|
Object.assign(record, { storage_retry: 1, work_id: previous.work_id });
|
|
@@ -663,9 +661,10 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
663
661
|
throw persistence;
|
|
664
662
|
if (error) {
|
|
665
663
|
error.details.recoverable = false;
|
|
666
|
-
|
|
667
|
-
delete error.details.retry_instruction;
|
|
664
|
+
removeRetryPromises(error.details);
|
|
668
665
|
}
|
|
666
|
+
if (outcome.error)
|
|
667
|
+
removeRetryPromises(outcome.error.details);
|
|
669
668
|
// Diagnostic evidence is not replay authority. If SQL is unavailable,
|
|
670
669
|
// the controller still sees this failure and fences instead of waiting
|
|
671
670
|
// for a model to report it or invent a new invocation.
|
|
@@ -686,10 +685,10 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
|
|
|
686
685
|
if (outcome.error && error) {
|
|
687
686
|
outcome.error.code = error.code;
|
|
688
687
|
outcome.error.message = error.message;
|
|
688
|
+
removeRetryPromises(outcome.error.details);
|
|
689
689
|
Object.assign(outcome.error.details, error.details);
|
|
690
690
|
if (record.disposition === "fatal") {
|
|
691
|
-
|
|
692
|
-
delete outcome.error.details.retry_instruction;
|
|
691
|
+
removeRetryPromises(outcome.error.details);
|
|
693
692
|
}
|
|
694
693
|
}
|
|
695
694
|
};
|
|
@@ -892,16 +891,16 @@ export function managedLifecycleCommand(context, command, options = {}) {
|
|
|
892
891
|
return publicInvocationCommand(context, load(context, issued.id), command);
|
|
893
892
|
});
|
|
894
893
|
}
|
|
895
|
-
export function retryLifecycleInvocationCommand(context, id, checkReceiptId) {
|
|
894
|
+
export function retryLifecycleInvocationCommand(context, id, checkReceiptId, needsEvidenceInput = false) {
|
|
896
895
|
const prior = load(context, id);
|
|
897
896
|
if (prior.status !== "executing")
|
|
898
897
|
throw new AppError("invocation_retry_not_authorized", "Only the executing lifecycle operation can publish its next attempt", 1);
|
|
899
|
-
return successorLifecycleInvocationCommand(context, prior, checkReceiptId);
|
|
898
|
+
return successorLifecycleInvocationCommand(context, prior, checkReceiptId, needsEvidenceInput);
|
|
900
899
|
}
|
|
901
900
|
/** A predecessor may publish exactly one successor only while the caller has
|
|
902
901
|
* proved that no lifecycle effect was admitted. Shared by CLI argument
|
|
903
902
|
* validation and conclusive lifecycle/result rejection, never by hooks. */
|
|
904
|
-
function successorLifecycleInvocationCommand(context, prior, checkReceiptId) {
|
|
903
|
+
function successorLifecycleInvocationCommand(context, prior, checkReceiptId, needsEvidenceInput = false) {
|
|
905
904
|
const parsed = invocation(prior.command);
|
|
906
905
|
const argv = [...parsed.argv];
|
|
907
906
|
const at = argv.indexOf("--invocation-id");
|
|
@@ -925,7 +924,12 @@ function successorLifecycleInvocationCommand(context, prior, checkReceiptId) {
|
|
|
925
924
|
throw new AppError("invocation_command_invalid", "Retained retry option cannot be rewritten unambiguously", 1);
|
|
926
925
|
retained = retained.replace(fragments[0], "");
|
|
927
926
|
}
|
|
928
|
-
|
|
927
|
+
const scope = JSON.parse(prior.scope_json);
|
|
928
|
+
if (needsEvidenceInput && !scope.runId)
|
|
929
|
+
throw new AppError("runtime_missing", "Check recovery requires a bound RUN", 1);
|
|
930
|
+
const retryArgs = needsEvidenceInput ? checkRetryArgs(context, scope.projectRoot, scope.runId, checkReceiptId)
|
|
931
|
+
: ["--retry-check", checkReceiptId, "--reason", "Recheck after diagnosis and correction of the failed check"];
|
|
932
|
+
command = retained.replace(` --invocation-id ${prior.id}`, ` ${retryArgs.map(shellArgument).join(" ")}`);
|
|
929
933
|
}
|
|
930
934
|
const successor = managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
|
|
931
935
|
const next = stagedContinuations.get(context.db)?.get(prior.id) ?? currentIssuedCommand(context, prior.scope_json, successor, prior.id);
|
|
@@ -946,6 +950,15 @@ function settleUnadmittedInvocation(context, prior, error) {
|
|
|
946
950
|
return details;
|
|
947
951
|
}
|
|
948
952
|
const receiptTimeout = (id) => ({ code: "invocation_receipt_timeout", message: "No native confirmation before the receipt deadline; lifecycle state was not changed", exitCode: 1, details: { invocation_id: id } });
|
|
953
|
+
export function removeRetryPromises(details) {
|
|
954
|
+
delete details.retry_command;
|
|
955
|
+
delete details.retry_instruction;
|
|
956
|
+
if (details.continuation && typeof details.continuation === "object" && !Array.isArray(details.continuation)) {
|
|
957
|
+
const continuation = { ...details.continuation };
|
|
958
|
+
delete continuation.command_after_input;
|
|
959
|
+
details.continuation = continuation;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
949
962
|
/** Settle a conclusive rejection and publish its only legal successor atomically.
|
|
950
963
|
* A crash must leave either the old attempt executing, or both the settled
|
|
951
964
|
* outcome and its successor; never a usable successor beside an unknown effect. */
|
|
@@ -962,8 +975,7 @@ export function settleLifecycleRejection(context, id, error) {
|
|
|
962
975
|
}
|
|
963
976
|
storage(context);
|
|
964
977
|
// Only this settlement may publish a successor; inherited help is not authority.
|
|
965
|
-
|
|
966
|
-
delete error.details.retry_instruction;
|
|
978
|
+
removeRetryPromises(error.details);
|
|
967
979
|
// A producer may have rendered help before throwing. Only the decision below
|
|
968
980
|
// may publish replacement authority for a rejected operation.
|
|
969
981
|
stagedContinuations.get(context.db)?.delete(id);
|
|
@@ -992,8 +1004,15 @@ export function settleLifecycleRejection(context, id, error) {
|
|
|
992
1004
|
&& !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]);
|
|
993
1005
|
if (prepared.retryable || storageRetry) {
|
|
994
1006
|
const failures = prepared.receipts.map(value => value.receipt);
|
|
995
|
-
const
|
|
1007
|
+
const continuation = details.continuation;
|
|
1008
|
+
const needsInput = failures.length > 0 && continuation?.kind === "needs_input";
|
|
1009
|
+
const retry = retryLifecycleInvocationCommand(context, id, failures[0]?.id, needsInput);
|
|
996
1010
|
details.retry_command = retry;
|
|
1011
|
+
if (needsInput) {
|
|
1012
|
+
const scope = JSON.parse(prior.scope_json);
|
|
1013
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_root = ? AND id = ?", [scope.projectRoot, scope.runId]);
|
|
1014
|
+
details.continuation = { ...continuation, ...recoveryEvidenceInput(run.run_root, failures[0].id), command_after_input: retry };
|
|
1015
|
+
}
|
|
997
1016
|
details.effect = "no_effect";
|
|
998
1017
|
details.recoverable = true;
|
|
999
1018
|
if (storageRetry)
|
|
@@ -1866,6 +1885,10 @@ export function observedLifecycleNativeEvent(context, command, daemonId) {
|
|
|
1866
1885
|
// never choose the latest of several identical native calls.
|
|
1867
1886
|
return matches.length === 1 ? { project_id: matches[0].project_id, id: matches[0].id } : null;
|
|
1868
1887
|
}
|
|
1888
|
+
export function lifecycleDiagnosticFingerprint(command) {
|
|
1889
|
+
const analysis = parseLifecycleCommand(command);
|
|
1890
|
+
return analysis.kind === "standalone" ? fingerprint(analysis.invocation) : null;
|
|
1891
|
+
}
|
|
1869
1892
|
/** Keep shell diagnostics distinct from genuinely absent native delivery. */
|
|
1870
1893
|
export function observedLifecycleDiagnostic(context, command, daemonId, invocationId) {
|
|
1871
1894
|
const analysis = parseLifecycleCommand(command);
|
|
@@ -2086,8 +2109,7 @@ function commandWithObservedWorkSemantics(row, argv) {
|
|
|
2086
2109
|
return command;
|
|
2087
2110
|
}
|
|
2088
2111
|
export function settleLifecyclePreparationRejection(context, id, error, preparedCommand) {
|
|
2089
|
-
|
|
2090
|
-
delete error.details.retry_instruction;
|
|
2112
|
+
removeRetryPromises(error.details);
|
|
2091
2113
|
try {
|
|
2092
2114
|
return context.db.writeTransaction(() => {
|
|
2093
2115
|
let row = load(context, id);
|