@deksden-com/dd-flow-cli 0.9.0-beta.133 → 0.9.0-beta.134
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 +21 -0
- package/dist/build-info.json +3 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +21 -1
- package/dist/harness-runtime/lib/native-children.d.mts +1 -0
- package/dist/harness-runtime/lib/native-children.mjs +16 -0
- package/dist/services/controller-fanout.js +20 -3
- package/dist/services/hooks.js +16 -10
- package/dist/services/lifecycle-invocations.js +66 -12
- package/dist/services/vnext-fanout.js +6 -1
- package/dist/services/work-registry.js +54 -11
- package/dist/storage/database.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.134
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7dae2f0: Freeze explicitly accepted RUN operational decisions and deliver the same verified policy/context to Stage and Work. Preserve their authority across recovery, require new acceptance for imported experiments, and retain native aborted-check reasons.
|
|
8
|
+
- Deliver plan 082 Work authority, native failure evidence and Judge diagnostic repairs; admit the exact CP226 ZCode bridge after root, concurrent-child, same-child continuation and clean shutdown probes.
|
|
9
|
+
- 46b7dbc: Persist stage usage through the shared completion service, including vNext SPECIFY and PROTOCOLIZE. Completed stage records now retain scoped token measurements for console readers; missing measurements remain unavailable.
|
|
10
|
+
- 430a7df: Prepare writable parent directories before handing agents HITL, Work result and CODE blocker input paths. Preserve existing payloads and reject unavailable destinations without publishing unusable instructions.
|
|
11
|
+
- 539de25: Capture bounded observation journals without requiring global runtime silence.
|
|
12
|
+
Retain RUN-scoped productive authority, immutable payload checksums and recovery
|
|
13
|
+
materials. Finalize stop-after captures as candidates after native settlement.
|
|
14
|
+
- 9ca6bb3: Use scoped native progress and inactivity windows instead of productive-work
|
|
15
|
+
lifetime limits. Preserve observation uncertainty and exact operation fences
|
|
16
|
+
across quiet probes and observer replacement; bound utility output and cleanup.
|
|
17
|
+
Separate native-work and control waits across all six bundled adapters, keep
|
|
18
|
+
serving services exempt from output silence, and stop MERGE on confirmed lease
|
|
19
|
+
loss. New runtime contracts are operation-progress@1, operation-errors@2 and
|
|
20
|
+
native-operation-wait@1.
|
|
21
|
+
- 0a8923a: Separate existing controller lease renewal from new launch admission so terminal RUNs can finish boundary capture and native settlement. Make terminal stop retain durable cleanup evidence without reopening productive recovery, and expose exact-owner finalization diagnostics. Full-cycle regressions now check sealed final capture and physical retirement before fixture cleanup.
|
|
22
|
+
- 6b2c491: Preserve unavailable ZCode cache and reasoning counters as null instead of inventing measured zeros. Mixed histories keep optional totals unknown while retaining confirmed input, output, and total tokens.
|
|
23
|
+
|
|
3
24
|
## 0.9.0-beta.133
|
|
4
25
|
|
|
5
26
|
### 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.134",
|
|
4
|
+
"cli_commit": "b243f4dac38629b7f0398b3e0b1613904f4aebff",
|
|
5
|
+
"built_at": "2026-10-13T11:24:52.803Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.2",
|
|
8
8
|
"commit": "2ba0b9ceedf8e4544ad9566c240ef37ea5e8d099",
|
|
@@ -14,6 +14,7 @@ import { checkObservedProfile, observeModel, observedFields } from "./model-obse
|
|
|
14
14
|
import { commandText } from "./process-json.mjs";
|
|
15
15
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
16
16
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
17
|
+
import { safeNativeFailure } from "./native-children.mjs";
|
|
17
18
|
|
|
18
19
|
import { readZcodeUsage } from "./zcode-usage.mjs";
|
|
19
20
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
@@ -569,7 +570,25 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
569
570
|
cursor = page.ended.nextCursor;
|
|
570
571
|
if (!cursor) {
|
|
571
572
|
if (ended.size !== first.ended.total) fail("ZCode child directory is incomplete", "native_topology_incomplete");
|
|
572
|
-
|
|
573
|
+
const items = [];
|
|
574
|
+
for (const child of ended.values()) {
|
|
575
|
+
if (child.status !== "failed" || !Number.isSafeInteger(child.startedAt) || child.startedAt <= 0
|
|
576
|
+
|| !Number.isSafeInteger(child.endedAt) || child.endedAt < child.startedAt || child.endedAt > 8.64e15) { items.push(child); continue; }
|
|
577
|
+
// Closed children must never be loaded merely to explain their failure.
|
|
578
|
+
let terminal;
|
|
579
|
+
try { terminal = await bridge.request("zcode/session/retainedTerminal", { sessionId: child.childSessionId, startedAt: child.startedAt, endedAt: child.endedAt }, timeoutMs); }
|
|
580
|
+
catch (error) { items.push({ ...child, terminal_metadata_unavailable: error.code ?? "terminal_metadata_unavailable" }); continue; }
|
|
581
|
+
if (terminal?.available !== true) { items.push({ ...child, terminal_metadata_unavailable: "terminal_metadata_unavailable" }); continue; }
|
|
582
|
+
const observedAt = Date.parse(terminal.terminal_observed_at);
|
|
583
|
+
if (terminal.sessionId !== child.childSessionId || typeof terminal.terminal_turn_id !== "string" || !terminal.terminal_turn_id
|
|
584
|
+
|| terminal.startedAt !== child.startedAt || terminal.endedAt !== child.endedAt
|
|
585
|
+
|| !Number.isFinite(observedAt) || observedAt < child.startedAt || observedAt > child.endedAt) fail("ZCode terminal failure metadata has foreign identity", "native_child_turn_conflict");
|
|
586
|
+
const providerError = safeNativeFailure(terminal.provider_error);
|
|
587
|
+
items.push({ ...child, terminal_turn_id: terminal.terminal_turn_id, terminal_observed_at: terminal.terminal_observed_at,
|
|
588
|
+
...(providerError ? { provider_error: providerError } : {}) });
|
|
589
|
+
clock.sample(child.childSessionId);
|
|
590
|
+
}
|
|
591
|
+
return { ...first, ended: { total: first.ended.total, items } };
|
|
573
592
|
}
|
|
574
593
|
if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
|
|
575
594
|
cursors.add(cursor);
|
|
@@ -923,6 +942,7 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
|
|
|
923
942
|
["b9e0f5106dddb439b76186278bd9be5f45dbf3ef", "dd-zcode-harness@2"],
|
|
924
943
|
["636b14181d5c9a95476055079836fd6b58169f58", "dd-zcode-harness@2"],
|
|
925
944
|
["4621fabd3f517ecdd349dfebebae9077c01d2f2b", "dd-zcode-harness@2"],
|
|
945
|
+
["9c5ca6cc81c4eb5065e43f8b7ab6b7cc5ab7ae47", "dd-zcode-harness@2"],
|
|
926
946
|
]);
|
|
927
947
|
const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
|
|
928
948
|
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
@@ -3,6 +3,22 @@ import { errorRecord } from "./operation-errors.mjs";
|
|
|
3
3
|
|
|
4
4
|
function nativeFailure(code, message, exitCode, details) { return Object.assign(new Error(message), { code, exitCode, details }); }
|
|
5
5
|
|
|
6
|
+
/** Retain typed failure facts, never provider headers, request bodies or free-form messages. */
|
|
7
|
+
export function safeNativeFailure(value, depth = 0) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const key of ["type", "code", "source", "reason", "errorPhase", "exceptionKind", "providerId", "modelId", "providerKind", "transport", "retryable", "willRetry", "statusCode", "resets_at", "retryAfter", "retry_after", "retryAfterMs", "resetAt"]) {
|
|
11
|
+
const scalar = value[key];
|
|
12
|
+
if (typeof scalar === "boolean" || typeof scalar === "number" && Number.isFinite(scalar)
|
|
13
|
+
|| typeof scalar === "string" && scalar.length <= 256 && /^[\w.:/ -]+$/.test(scalar)) result[key] = scalar;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value.codexErrorInfo === "string" && /^[\w-]{1,128}$/.test(value.codexErrorInfo)) result.codexErrorInfo = value.codexErrorInfo;
|
|
16
|
+
else if (value.codexErrorInfo && typeof value.codexErrorInfo.code === "string" && /^[\w-]{1,128}$/.test(value.codexErrorInfo.code)) result.codexErrorInfo = { code: value.codexErrorInfo.code };
|
|
17
|
+
const attribution = depth === 0 && value.attribution && safeNativeFailure(value.attribution, 1);
|
|
18
|
+
if (attribution && Object.keys(attribution).length) result.attribution = attribution;
|
|
19
|
+
return Object.keys(result).length ? result : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
|
|
7
23
|
/** Normalize provider-observed topology only; never parse assistant prose. */
|
|
8
24
|
export function normalizeNativeChildren(receipt, rootSessionId) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
|
-
import { normalizeNativeChildren } from "../harness-runtime/lib/native-children.mjs";
|
|
2
|
+
import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
3
3
|
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
|
|
4
4
|
import { getFlowRunVariables } from "./runs.js";
|
|
5
5
|
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
6
6
|
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
7
7
|
import { lifecycleCommandCompletionInstruction, renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
8
8
|
import { assertWorkLaunchReady } from "./work-registry.js";
|
|
9
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
10
|
+
import { stageWorkIds } from "./stage-work-graph.js";
|
|
9
11
|
function semanticCompletionPrompt(stage, completed) {
|
|
10
12
|
const task = stage === "plan-review"
|
|
11
13
|
? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
|
|
@@ -74,8 +76,23 @@ export async function nextControllerFanout(context, input) {
|
|
|
74
76
|
const prompt = (action, text) => ({ kind: "prompt", action, prompt: action === "dispatch_work" ? text : `${text}\n\n${lifecycleCommandCompletionInstruction}`, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, state: fanout.state, graph: fanout.graph_revision }) });
|
|
75
77
|
if (fanout.state === "coordinator_required")
|
|
76
78
|
return prompt("materialize_graph", `Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
|
|
77
|
-
if (fanout.works.failed || fanout.works.cancelled)
|
|
78
|
-
|
|
79
|
+
if (fanout.works.failed || fanout.works.cancelled) {
|
|
80
|
+
const projectId = requireProjectByRoot(context, input.projectRoot).id;
|
|
81
|
+
const members = stageWorkIds(context, projectId, input.runId, input.stage);
|
|
82
|
+
const failures = context.db.all("SELECT work_id,status,result FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND status IN ('failed','cancelled')", [projectId, input.runId, fanout.parent_work_id]).filter(work => members.has(work.work_id)).map(work => {
|
|
83
|
+
let receipt = {};
|
|
84
|
+
try {
|
|
85
|
+
const value = work.result ? JSON.parse(work.result) : null;
|
|
86
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
87
|
+
receipt = value;
|
|
88
|
+
}
|
|
89
|
+
catch { /* Missing legacy metadata is unknown. */ }
|
|
90
|
+
const providerError = safeNativeFailure(receipt.provider_error);
|
|
91
|
+
return { work_id: work.work_id, status: work.status, ...(providerError ? { provider_error: providerError } : {}),
|
|
92
|
+
...Object.fromEntries(["terminal_turn_id", "terminal_observed_at"].filter(key => typeof receipt[key] === "string" && receipt[key].length <= 256).map(key => [key, receipt[key]])) };
|
|
93
|
+
});
|
|
94
|
+
throw new AppError("fanout_child_failed", "A child Work failed or was cancelled; no retry authority has been issued", 1, { stage: input.stage, run_id: input.runId, parent_work_id: fanout.parent_work_id, failed: fanout.works.failed, cancelled: fanout.works.cancelled, failures, effect: "unknown" });
|
|
95
|
+
}
|
|
79
96
|
if (fanout.works.ready.length) {
|
|
80
97
|
const choices = fanout.works.ready.map(work => {
|
|
81
98
|
const payload = context.db.get("SELECT payload_json FROM works WHERE work_id = ?", [work.work_id])?.payload_json;
|
package/dist/services/hooks.js
CHANGED
|
@@ -18,6 +18,11 @@ import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
|
18
18
|
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
|
|
19
19
|
const defaultProfileName = "default";
|
|
20
20
|
const maxSanitizedSummaryLength = 1600;
|
|
21
|
+
function lifecycleObservationResult(receipt) {
|
|
22
|
+
return { observed: receipt.disposition === "admitted", duplicate: receipt.duplicate,
|
|
23
|
+
event_key: receipt.authoritativeEventKey, current_event_key: receipt.currentEventKey,
|
|
24
|
+
authoritative_event_key: receipt.authoritativeEventKey, receipt_disposition: receipt.disposition };
|
|
25
|
+
}
|
|
21
26
|
export const codexHomeSharedEntries = [
|
|
22
27
|
"auth.json",
|
|
23
28
|
"auth.bk",
|
|
@@ -410,6 +415,7 @@ export function handleCodexHook(context, input) {
|
|
|
410
415
|
throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
|
|
411
416
|
const executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
|
|
412
417
|
const receipt = observeLifecycleInvocation(context, {
|
|
418
|
+
currentEventKey: eventKey,
|
|
413
419
|
...(invocationId ? { id: invocationId } : {}),
|
|
414
420
|
command,
|
|
415
421
|
harness: "codex-desktop",
|
|
@@ -421,12 +427,11 @@ export function handleCodexHook(context, input) {
|
|
|
421
427
|
const stored = context.db.get("SELECT protocol_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
|
|
422
428
|
const admittedInvocationId = receipt.invocationId ?? invocationId;
|
|
423
429
|
if (admittedInvocationId)
|
|
424
|
-
return { ...common, protocol_id: stored?.protocol_id ?? null,
|
|
430
|
+
return { ...common, protocol_id: stored?.protocol_id ?? null, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
|
|
425
431
|
return {
|
|
426
432
|
...common,
|
|
427
433
|
protocol_id: stored?.protocol_id ?? null,
|
|
428
|
-
|
|
429
|
-
duplicate: receipt.duplicate,
|
|
434
|
+
...lifecycleObservationResult(receipt),
|
|
430
435
|
hookSpecificOutput: {
|
|
431
436
|
hookEventName: "PreToolUse",
|
|
432
437
|
permissionDecision: "allow",
|
|
@@ -588,6 +593,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
|
|
|
588
593
|
let persistedResult;
|
|
589
594
|
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
|
|
590
595
|
const receipt = observeLifecycleInvocation(context, {
|
|
596
|
+
currentEventKey: eventKey,
|
|
591
597
|
...(invocationId ? { id: invocationId } : {}), command,
|
|
592
598
|
harness: "zcode-acp",
|
|
593
599
|
projectId: project.id,
|
|
@@ -595,7 +601,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
|
|
|
595
601
|
identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId },
|
|
596
602
|
recordReceipt: scope => { persistedResult = persist(scope); return eventKey; }
|
|
597
603
|
});
|
|
598
|
-
if (receipt.
|
|
604
|
+
if (receipt.disposition === "replay") {
|
|
599
605
|
// Keep first admission immutable. A second transport is independently
|
|
600
606
|
// retained corroboration, never another claim or an invented ACP cwd.
|
|
601
607
|
const retained = context.db.get("SELECT event_name, match_key, harness, provider_session_id, parent_session_id, daemon_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
|
|
@@ -615,7 +621,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
|
|
|
615
621
|
}
|
|
616
622
|
const admittedInvocationId = receipt.invocationId ?? invocationId;
|
|
617
623
|
if (admittedInvocationId)
|
|
618
|
-
return { ok: true,
|
|
624
|
+
return { ok: true, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
|
|
619
625
|
// Without an issued managed attempt this is still ordinary hook evidence.
|
|
620
626
|
// Preserve persist()'s replay result instead of overwriting it with the
|
|
621
627
|
// admission layer's non-authoritative duplicate flag.
|
|
@@ -685,14 +691,13 @@ export function handleGrokEvent(context, input) {
|
|
|
685
691
|
let inserted = false;
|
|
686
692
|
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
|
|
687
693
|
const receipt = observeLifecycleInvocation(context, {
|
|
694
|
+
currentEventKey: eventKey,
|
|
688
695
|
command, harness: "grok-acp", projectId: project.id,
|
|
689
696
|
...(executionCwd ? { executionCwd } : {}),
|
|
690
697
|
identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
|
|
691
698
|
recordReceipt: scope => { inserted = persist(scope); return eventKey; }
|
|
692
699
|
});
|
|
693
|
-
|
|
694
|
-
return { ...result(inserted), invocation_id: receipt.invocationId, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
695
|
-
return { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
700
|
+
return { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
|
|
696
701
|
}
|
|
697
702
|
/** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
|
|
698
703
|
export function handleOpenCodeEvent(context, input) {
|
|
@@ -900,11 +905,12 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
900
905
|
if (lifecycle) {
|
|
901
906
|
let inserted = false;
|
|
902
907
|
const receipt = observeLifecycleInvocation(context, {
|
|
908
|
+
currentEventKey: baseKey,
|
|
903
909
|
command, harness, projectId: project.id, ...(!shellRefusal ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
|
|
904
910
|
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
905
911
|
});
|
|
906
|
-
const admitted = { ...result(inserted), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
907
|
-
return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command,
|
|
912
|
+
const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
913
|
+
return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
|
|
908
914
|
}
|
|
909
915
|
const inserted = context.db.writeTransaction(() => persist());
|
|
910
916
|
return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
|
|
@@ -60,6 +60,11 @@ export function assertAuxiliaryCommandScope(context, command, projectRoot) {
|
|
|
60
60
|
}
|
|
61
61
|
throw new AppError("invocation_scope_unavailable", "Auxiliary command has no live managed daemon binding", 1, { phase: "prepare", effect: "no_effect" });
|
|
62
62
|
}
|
|
63
|
+
function sameNativeCall(left, right) {
|
|
64
|
+
const a = JSON.parse(left), b = JSON.parse(right);
|
|
65
|
+
// Parent and execution route are immutable facts, not a different call ID.
|
|
66
|
+
return [0, 1, 2, 4].every(index => a[index] === b[index]);
|
|
67
|
+
}
|
|
63
68
|
// Conclusive result/check rejections, never transport, ownership, registry or
|
|
64
69
|
// unknown-effect failures. These validators reject before accepting the result.
|
|
65
70
|
export function isRetryableLifecycleRejection(code, details = {}) {
|
|
@@ -567,6 +572,8 @@ export function managedLifecycleCommand(context, command, options = {}) {
|
|
|
567
572
|
const scope = JSON.parse(configured);
|
|
568
573
|
command = canonicalManagedCommand(context, command, scope);
|
|
569
574
|
const parsed = invocation(command);
|
|
575
|
+
if (options.freshWorkAttempt && !["work_finish", "work_fail"].includes(parsed.operation))
|
|
576
|
+
throw new AppError("invocation_command_mismatch", "Fresh Work packet may only issue Work settlement commands", 1);
|
|
570
577
|
const suppliedId = commandOption(parsed, "invocation-id");
|
|
571
578
|
// An invocation id is authority, never decorative text. In particular,
|
|
572
579
|
// callers that add arguments after rendering must not be able to reuse an
|
|
@@ -590,7 +597,7 @@ export function managedLifecycleCommand(context, command, options = {}) {
|
|
|
590
597
|
}
|
|
591
598
|
storage(context);
|
|
592
599
|
return context.db.writeTransaction(() => {
|
|
593
|
-
const prior = context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
|
|
600
|
+
const prior = options.freshWorkAttempt ? undefined : context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
|
|
594
601
|
if (prior && prior.id !== context.env.DD_FLOW_CURRENT_INVOCATION) {
|
|
595
602
|
const replacesAnswer = options.replaceUnobservedAnswer && parsed.operation === "stage_resume"
|
|
596
603
|
&& (prior.status === "settled" || commandOption(invocation(prior.command), "answer-file") !== commandOption(parsed, "answer-file"));
|
|
@@ -817,6 +824,39 @@ export function issueLifecycleInvocation(context, command, scope) {
|
|
|
817
824
|
context.db.run("INSERT INTO lifecycle_invocations (id, scope_json, command, fingerprint, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'issued', ?, ?)", [id, row.scope_json, prepared, row.fingerprint, context.now(), context.now()]);
|
|
818
825
|
return { id, command: prepared };
|
|
819
826
|
}
|
|
827
|
+
/** Prepare outside a write transaction; the Work retry CAS owns issuance. */
|
|
828
|
+
export function prepareWorkStartAuthority(context, command, previousId) {
|
|
829
|
+
const prior = previousId ? load(context, previousId) : null;
|
|
830
|
+
const configured = context.env.DD_FLOW_INVOCATION_SCOPE;
|
|
831
|
+
if (!configured && !prior)
|
|
832
|
+
return null;
|
|
833
|
+
const scope = JSON.parse(configured ?? prior.scope_json);
|
|
834
|
+
if (prior && prior.scope_json !== JSON.stringify(scope))
|
|
835
|
+
throw new AppError("invocation_scope_mismatch", "Work start belongs to another controller scope", 1);
|
|
836
|
+
command = canonicalManagedCommand(context, command, scope);
|
|
837
|
+
const { parsed } = prepareLifecycleIssuance(command, scope);
|
|
838
|
+
if (parsed.operation !== "work_start")
|
|
839
|
+
throw new AppError("invocation_command_mismatch", "Work authority must start its Work", 1);
|
|
840
|
+
if (prior && prior.fingerprint !== fingerprint(parsed))
|
|
841
|
+
throw new AppError("invocation_command_mismatch", "Retained Work start authority belongs to a different Work", 1, { invocation_id: prior.id });
|
|
842
|
+
assertLifecycleInvocationCurrent(context, scope, command);
|
|
843
|
+
return { command, scope };
|
|
844
|
+
}
|
|
845
|
+
/** Caller owns the retry CAS. Preserve settled outcomes; never steal an
|
|
846
|
+
* observed/executing terminal operation from the previous Work attempt. */
|
|
847
|
+
export function retireWorkPacketAuthorities(context, workId) {
|
|
848
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'lifecycle_packet_assignments'"))
|
|
849
|
+
return;
|
|
850
|
+
const attempts = context.db.all("SELECT DISTINCT li.* FROM lifecycle_invocations li JOIN lifecycle_packet_assignments pa ON pa.invocation_id=li.id JOIN work_sessions ws ON pa.packet_sha256=json_extract(ws.start_receipt_json,'$.prompt_sha256') WHERE ws.work_id=?", [workId]);
|
|
851
|
+
for (const attempt of attempts) {
|
|
852
|
+
if (!["work_finish", "work_fail"].includes(invocation(attempt.command).operation))
|
|
853
|
+
continue;
|
|
854
|
+
if (["observed", "executing"].includes(attempt.status))
|
|
855
|
+
throw new AppError("work_retry_incomplete", "Previous Work settlement still has native authority in flight", 1, { work_id: workId, invocation_id: attempt.id });
|
|
856
|
+
if (attempt.status === "issued")
|
|
857
|
+
context.db.run("UPDATE lifecycle_invocations SET status='expired',updated_at=? WHERE id=? AND status='issued'", [context.now(), attempt.id]);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
820
860
|
function commandWithInvocationId(command, id) {
|
|
821
861
|
const at = shellSuffixOffset(command);
|
|
822
862
|
const suffix = command.slice(at);
|
|
@@ -836,8 +876,10 @@ function assertRenderableInvocation(row) {
|
|
|
836
876
|
function renderInvocationCommand(row, command) {
|
|
837
877
|
assertRenderableInvocation(row);
|
|
838
878
|
const parsed = invocation(command);
|
|
839
|
-
if (commandOption(parsed, "invocation-id"))
|
|
840
|
-
|
|
879
|
+
if (commandOption(parsed, "invocation-id")) {
|
|
880
|
+
checkCommand(row, command);
|
|
881
|
+
return command;
|
|
882
|
+
}
|
|
841
883
|
const rendered = commandWithInvocationId(command, row.id);
|
|
842
884
|
checkCommand(row, rendered);
|
|
843
885
|
return rendered;
|
|
@@ -1175,6 +1217,18 @@ export function isControllerManagedInvocation(context, projectRoot, runId) {
|
|
|
1175
1217
|
/** recordReceipt is a synchronous SQL-only hook ingress callback. Native
|
|
1176
1218
|
* adapters must prepare any filesystem or process evidence before calling. */
|
|
1177
1219
|
export function observeLifecycleInvocation(context, input) {
|
|
1220
|
+
const priorReceipt = input.currentEventKey && input.projectId
|
|
1221
|
+
&& context.db.get("SELECT 1 FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.currentEventKey]);
|
|
1222
|
+
const result = observeLifecycleInvocationReceipt(context, input);
|
|
1223
|
+
// Hook-only calls have no invocation row, but their immutable receipt still
|
|
1224
|
+
// distinguishes a repeated native delivery from a new observation.
|
|
1225
|
+
if (priorReceipt && result.disposition === "admitted" && result.eventKey === input.currentEventKey) {
|
|
1226
|
+
result.disposition = "replay";
|
|
1227
|
+
result.duplicate = true;
|
|
1228
|
+
}
|
|
1229
|
+
return { ...result, currentEventKey: input.currentEventKey ?? null, authoritativeEventKey: result.eventKey ?? null };
|
|
1230
|
+
}
|
|
1231
|
+
function observeLifecycleInvocationReceipt(context, input) {
|
|
1178
1232
|
storage(context);
|
|
1179
1233
|
const projectRoot = input.projectId ? context.db.get("SELECT root FROM projects WHERE id = ?", [input.projectId])?.root : undefined;
|
|
1180
1234
|
const provenScope = projectRoot && input.harness ? managedNativeInvocationScope(context, projectRoot, input.identity, input.harness) : null;
|
|
@@ -1246,7 +1300,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
1246
1300
|
}
|
|
1247
1301
|
if (analysis.kind !== "none")
|
|
1248
1302
|
throw new AppError("lifecycle_shell_syntax_invalid", "This shell form cannot bind exactly one lifecycle execution; use the exact issued lifecycle command without the unsupported shell context", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.kind === "compound" ? analysis.reason : "unsupported_lifecycle_command", event_key: eventKey, ...issuedBarrier });
|
|
1249
|
-
return { eventKey, duplicate: false, invocationId: null };
|
|
1303
|
+
return { eventKey, duplicate: false, disposition: "admitted", invocationId: null };
|
|
1250
1304
|
}
|
|
1251
1305
|
// Admit the one parsed leaf, never execute or fingerprint the prefix/suffix.
|
|
1252
1306
|
const leafCommand = analysis.invocation.command;
|
|
@@ -1269,7 +1323,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
1269
1323
|
return false;
|
|
1270
1324
|
}
|
|
1271
1325
|
if (row.identity_json && row.identity_json !== deliveredIdentity
|
|
1272
|
-
&&
|
|
1326
|
+
&& sameNativeCall(row.identity_json, deliveredIdentity)) {
|
|
1273
1327
|
throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
1274
1328
|
}
|
|
1275
1329
|
if (row.status === "executing" && row.identity_json !== deliveredIdentity)
|
|
@@ -1302,7 +1356,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
1302
1356
|
if (retainedOwner !== ownerSnapshot())
|
|
1303
1357
|
throw new AppError("invocation_scope_unproven", "Native owner changed before hook-only recovery receipt", 1, { effect: "no_effect" });
|
|
1304
1358
|
const eventKey = input.recordReceipt();
|
|
1305
|
-
return { eventKey, duplicate: false, invocationId: null };
|
|
1359
|
+
return { eventKey, duplicate: false, disposition: "admitted", invocationId: null };
|
|
1306
1360
|
});
|
|
1307
1361
|
}
|
|
1308
1362
|
const resolved = input.id === initial.id ? {} : { invocationId: initial.id };
|
|
@@ -1313,7 +1367,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
1313
1367
|
// reattach. Do not manufacture a new receipt or reject that read as a second
|
|
1314
1368
|
// executor; the CLI returns the immutable stored outcome without dispatch.
|
|
1315
1369
|
if (initial.status === "settled")
|
|
1316
|
-
return { eventKey: initial.event_key, duplicate: true, ...resolved };
|
|
1370
|
+
return { eventKey: initial.event_key, duplicate: true, disposition: initial.identity_json === deliveredIdentity ? "replay" : "settled_read", ...resolved };
|
|
1317
1371
|
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
1318
1372
|
const identity = input.identity;
|
|
1319
1373
|
const identityJson = deliveredIdentity;
|
|
@@ -1324,16 +1378,16 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
1324
1378
|
if (row.scope_json !== initial.scope_json)
|
|
1325
1379
|
throw new AppError("invocation_scope_mismatch", "Lifecycle scope changed before native receipt", 1, { effect: "no_effect" });
|
|
1326
1380
|
if (row.status === "settled")
|
|
1327
|
-
return { eventKey: row.event_key, duplicate: true, ...resolved };
|
|
1381
|
+
return { eventKey: row.event_key, duplicate: true, disposition: row.identity_json === identityJson ? "replay" : "settled_read", ...resolved };
|
|
1328
1382
|
// Observation records native facts only. CLI owns deadline, argument and
|
|
1329
1383
|
// RUN-state validation, including decisions about retrying an invocation.
|
|
1330
1384
|
if (row.status === "expired" || (row.deadline !== null && row.deadline <= Date.now() && !row.event_key)) {
|
|
1331
|
-
return { eventKey: row.event_key, duplicate: true, ...resolved };
|
|
1385
|
+
return { eventKey: row.event_key, duplicate: true, disposition: "expired", ...resolved };
|
|
1332
1386
|
}
|
|
1333
1387
|
if (row.identity_json === identityJson) {
|
|
1334
|
-
return { eventKey: row.event_key, duplicate: true, ...resolved };
|
|
1388
|
+
return { eventKey: row.event_key, duplicate: true, disposition: "replay", ...resolved };
|
|
1335
1389
|
}
|
|
1336
|
-
if (row.identity_json &&
|
|
1390
|
+
if (row.identity_json && sameNativeCall(row.identity_json, identityJson)) {
|
|
1337
1391
|
throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
1338
1392
|
}
|
|
1339
1393
|
const eventKey = input.recordReceipt({ projectRoot: scope.projectRoot, runId: scope.runId, command: initial.command });
|
|
@@ -1353,7 +1407,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
1353
1407
|
const updated = context.db.run("UPDATE lifecycle_invocations SET identity_json = ?, event_key = ?, status = 'observed', updated_at = ? WHERE id = ? AND status IN ('issued', 'observed')", [identityJson, eventKey, context.now(), initial.id]);
|
|
1354
1408
|
// Once execution has been claimed, later PreToolUse observations are
|
|
1355
1409
|
// retained as hook evidence but cannot replace the executor's receipt.
|
|
1356
|
-
return { eventKey: updated.changes === 1 ? eventKey : row.event_key, duplicate: updated.changes !== 1, ...resolved };
|
|
1410
|
+
return { eventKey: updated.changes === 1 ? eventKey : row.event_key, duplicate: updated.changes !== 1, disposition: updated.changes === 1 ? "admitted" : "in_flight", ...resolved };
|
|
1357
1411
|
});
|
|
1358
1412
|
}
|
|
1359
1413
|
/** Correlate exact native argv for diagnostics, independently of assignment
|
|
@@ -11,6 +11,7 @@ import { reconcileUnfinishedChecks } from "./code-checks.js";
|
|
|
11
11
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
12
12
|
import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
|
|
13
13
|
import { eligibleCodexChildOverload } from "./codex-capacity.js";
|
|
14
|
+
import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
14
15
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
15
16
|
/** External concurrency is policy, never evidence of native child qualification. */
|
|
16
17
|
export function getStageFanoutCapacity(context, input) {
|
|
@@ -175,7 +176,11 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
175
176
|
issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
|
|
176
177
|
continue;
|
|
177
178
|
}
|
|
178
|
-
const
|
|
179
|
+
const providerError = safeNativeFailure(child.provider_error);
|
|
180
|
+
const reason = JSON.stringify({ source: "native_child_observation", harness_id: value.harness_id, session_id: child.session_id, work_session_id: link.id, outcome: child.status,
|
|
181
|
+
...(typeof child.terminal_turn_id === "string" ? { terminal_turn_id: child.terminal_turn_id } : {}),
|
|
182
|
+
...(typeof child.terminal_observed_at === "string" ? { terminal_observed_at: child.terminal_observed_at } : {}),
|
|
183
|
+
...(providerError ? { provider_error: providerError } : {}) });
|
|
179
184
|
const result = await (child.status === "failed" ? failWork : cancelWork)(context, link.work_id, reason);
|
|
180
185
|
reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, result });
|
|
181
186
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bindLifecyclePacketAssignments, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
|
+
import { bindLifecyclePacketAssignments, managedLifecycleCommand, prepareWorkStartAuthority, issueLifecycleInvocation, retireWorkPacketAuthorities } from "./lifecycle-invocations.js";
|
|
2
2
|
import { bindStageWork, currentStageWorkIds, stageWorkIds } from "./stage-work-graph.js";
|
|
3
3
|
import { runtimeCommand } from "./runtime-command.js";
|
|
4
4
|
import crypto from "node:crypto";
|
|
@@ -449,7 +449,38 @@ export function shortWorkId(id) { return shortEntityReference(id); }
|
|
|
449
449
|
* PATH is not inherited; using the shared lifecycle command keeps them on the
|
|
450
450
|
* same captured runtime as their parent stage.
|
|
451
451
|
*/
|
|
452
|
-
|
|
452
|
+
function rawWorkStartCommand(context, work) {
|
|
453
|
+
const run = requireRun(context, work.project_id, work.run_id), recovery = recoveryGuard(context, work.project_id, work.run_id);
|
|
454
|
+
return runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"]);
|
|
455
|
+
}
|
|
456
|
+
export function workStartCommand(context, work) {
|
|
457
|
+
const command = rawWorkStartCommand(context, work);
|
|
458
|
+
const retained = context.db.get("SELECT status,current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id]);
|
|
459
|
+
if (retained?.current_start_invocation_id) {
|
|
460
|
+
const prepared = prepareWorkStartAuthority(context, command, retained.current_start_invocation_id);
|
|
461
|
+
return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(prepared.scope) } }, `${command} --invocation-id ${retained.current_start_invocation_id}`);
|
|
462
|
+
}
|
|
463
|
+
// Legacy terminal/running Work is historical, never a fresh dispatch.
|
|
464
|
+
if (retained?.status !== "created")
|
|
465
|
+
return managedLifecycleCommand(context, command);
|
|
466
|
+
const prepared = prepareWorkStartAuthority(context, command);
|
|
467
|
+
if (!prepared)
|
|
468
|
+
return command;
|
|
469
|
+
if (!context.db.writable || context.env.DD_FLOW_INVOCATION_READONLY === "1")
|
|
470
|
+
throw new AppError("invocation_command_unprepared", "Work start authority has not been prepared", 1);
|
|
471
|
+
const id = context.db.writeTransaction(() => {
|
|
472
|
+
assertRunMutationAllowed(context, work.project_id, work.run_id, prepared.scope.generation);
|
|
473
|
+
const current = context.db.get("SELECT status,current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id]);
|
|
474
|
+
if (current?.current_start_invocation_id)
|
|
475
|
+
return current.current_start_invocation_id;
|
|
476
|
+
if (current?.status !== "created")
|
|
477
|
+
throw new AppError("work_retry_conflict", "Work changed before start issuance", 1);
|
|
478
|
+
const issued = issueLifecycleInvocation(context, prepared.command, prepared.scope);
|
|
479
|
+
context.db.run("UPDATE works SET current_start_invocation_id = ? WHERE work_id = ?", [issued.id, work.work_id]);
|
|
480
|
+
return issued.id;
|
|
481
|
+
});
|
|
482
|
+
return managedLifecycleCommand(context, `${command} --invocation-id ${id}`);
|
|
483
|
+
}
|
|
453
484
|
export function assertWorkLaunchReady(context, id) {
|
|
454
485
|
ensureWorkRegistry(context);
|
|
455
486
|
const work = requireWork(context, id);
|
|
@@ -639,7 +670,10 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
639
670
|
if (JSON.stringify(dependencyResults) !== JSON.stringify(preparedDependencies) || JSON.stringify(recovery) !== JSON.stringify(preparedRecovery))
|
|
640
671
|
throw new AppError("work_not_ready", "Work dependencies or recovery segment changed while preparing its packet", 2, { work_id: current.work_id });
|
|
641
672
|
const assignments = new Set();
|
|
642
|
-
const
|
|
673
|
+
const currentStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id])?.current_start_invocation_id;
|
|
674
|
+
if (currentStart && context.env.DD_FLOW_CURRENT_INVOCATION && currentStart !== context.env.DD_FLOW_CURRENT_INVOCATION)
|
|
675
|
+
throw new AppError("work_generation_stale", "Work start authority belongs to an earlier attempt", 1, { work_id: work.work_id });
|
|
676
|
+
const commands = workerLifecycleCommands({ ...context, captureLifecycleAssignment: (id) => assignments.add(id) }, current, workerRun, true);
|
|
643
677
|
const prompt = promptTemplate.replace(finishMarker, commands.finish).replace(failMarker, commands.fail);
|
|
644
678
|
bindLifecyclePacketAssignments(context, prompt, assignments);
|
|
645
679
|
const workContext = { schema_id: "dd-flow/work-context@1", run_id: current.run_id, work_id: current.work_id, parent_work_id: current.parent_work_id, project_root: run.project_root, workspace_root: workerRun.workspace_root, run_root: requireRunHome(run), depends_on: parseDependencies(current), launch_policy: current.launch_policy, result_schema: current.result_schema, read_only: readOnly, ...(recovery ? { recovery } : {}), ...(readOnly ? { workspace_fingerprint: fingerprint, ...(reviewInput ? { review_input: reviewInput } : {}), ...(reviewCopy ? { review_copy: reviewCopy } : {}) } : {}) };
|
|
@@ -771,7 +805,7 @@ export function prepareWorkRetry(context, id, reason) {
|
|
|
771
805
|
throw new AppError("work_retry_invalid", "Retry journal is not valid JSON", 1);
|
|
772
806
|
}
|
|
773
807
|
const item = value;
|
|
774
|
-
if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256)))
|
|
808
|
+
if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json", "result-input.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256)))
|
|
775
809
|
throw new AppError("work_retry_invalid", "Retry journal does not identify this Work", 1);
|
|
776
810
|
pending = item;
|
|
777
811
|
}
|
|
@@ -781,7 +815,7 @@ export function prepareWorkRetry(context, id, reason) {
|
|
|
781
815
|
let number = 1;
|
|
782
816
|
while (fs.existsSync(path.join(attempts, `ATT-${String(number).padStart(3, "0")}`)))
|
|
783
817
|
number += 1;
|
|
784
|
-
const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex") };
|
|
818
|
+
const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json", "result-input.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex") };
|
|
785
819
|
const archive = path.join(attempts, prepared.archive);
|
|
786
820
|
if (work.status === "failed") {
|
|
787
821
|
if (prepared.result_sha256 !== (work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex")))
|
|
@@ -805,6 +839,8 @@ export function retryWork(context, id, reason) {
|
|
|
805
839
|
// live path can be archived for the next attempt.
|
|
806
840
|
materializePendingWorkResults(context, pending.project_id, pending.run_id, pending.work_id);
|
|
807
841
|
const { work, run, directory, journal, pending: retainedJournal, prepared, archive } = prepareWorkRetry(context, id, reason);
|
|
842
|
+
const previousStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [id])?.current_start_invocation_id;
|
|
843
|
+
const startAuthority = work.status === "failed" ? prepareWorkStartAuthority(context, rawWorkStartCommand(context, work), previousStart) : null;
|
|
808
844
|
if (work.status === "failed") {
|
|
809
845
|
// The durable journal and archive are projections of the failed SQL
|
|
810
846
|
// receipt. They must finish before the short transaction opens a new
|
|
@@ -836,15 +872,18 @@ export function retryWork(context, id, reason) {
|
|
|
836
872
|
}
|
|
837
873
|
context.db.writeTransaction(() => {
|
|
838
874
|
const current = requireWork(context, work.work_id);
|
|
839
|
-
assertRunMutationAllowed(context, current.project_id, current.run_id);
|
|
875
|
+
assertRunMutationAllowed(context, current.project_id, current.run_id, startAuthority?.scope.generation);
|
|
840
876
|
if (current.status !== "failed" || current.result !== work.result || current.updated_at !== work.updated_at)
|
|
841
877
|
throw new AppError("work_retry_conflict", "Failed Work receipt changed during retry", 1);
|
|
878
|
+
retireWorkPacketAuthorities(context, work.work_id);
|
|
842
879
|
for (const column of ["prompt_path", "result_path"]) {
|
|
843
880
|
const file = column === "prompt_path" ? "prompt.md" : "result.json";
|
|
844
881
|
context.db.run(`UPDATE work_sessions SET ${column} = ? WHERE work_id = ? AND ${column} = ?`, [path.join(archive, file), work.work_id, path.join(directory, file)]);
|
|
845
882
|
}
|
|
846
883
|
if (context.db.run("UPDATE works SET status = 'created', result = NULL, started_at = NULL, completed_at = NULL, updated_at = ? WHERE work_id = ? AND status = 'failed'", [context.now(), work.work_id]).changes !== 1)
|
|
847
884
|
throw new AppError("work_retry_conflict", "Failed Work changed during retry", 1);
|
|
885
|
+
const startId = startAuthority ? issueLifecycleInvocation(context, startAuthority.command, startAuthority.scope).id : null;
|
|
886
|
+
context.db.run("UPDATE works SET current_start_invocation_id = ? WHERE work_id = ?", [startId, work.work_id]);
|
|
848
887
|
});
|
|
849
888
|
}
|
|
850
889
|
fs.rmSync(journal, { force: true });
|
|
@@ -1138,8 +1177,12 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1138
1177
|
const control = runControlDirective(context, work.project_id, work.run_id);
|
|
1139
1178
|
const newlyReady = control ? [] : context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, launch_policy: candidate.launch_policy, start_command: workStartCommand(context, candidate) }));
|
|
1140
1179
|
appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: `work_${status}`, work_id: work.work_id, session_id: link?.session_id ?? null, ...(coordinationDrift.length ? { coordination_drift: coordinationDrift } : {}) });
|
|
1141
|
-
|
|
1142
|
-
|
|
1180
|
+
if (status === "completed")
|
|
1181
|
+
for (const ready of newlyReady) {
|
|
1182
|
+
const candidate = requireWork(context, ready.work_id);
|
|
1183
|
+
if (parseDependencies(candidate).includes(work.work_id))
|
|
1184
|
+
appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_dependency_unblocked", work_id: ready.work_id, completed_dependency: work.work_id });
|
|
1185
|
+
}
|
|
1143
1186
|
return { ok: true, ...(control ? { control } : {}), work_id: id, status, checks: receipts, coordination: { planned_write_areas: codePacket(work)?.planned_write_areas ?? [], drift: coordinationDrift }, newly_ready: newlyReady, graph: codeWorkGraph(context, work.project_id, work.run_id), usage: { status: "provisional", reason: "final usage is controller-owned" } };
|
|
1144
1187
|
}
|
|
1145
1188
|
function recoveryPrompt(recovery) { return ["<recovery_identity>", `- recovery_id: ${recovery.recoveryId}`, `- execution_generation: ${recovery.generation}`, "This Work was interrupted and is now an authorized recovery segment of the same assignment.", "</recovery_identity>", "", "<recovery_instructions>", `Read the retained pre-interruption evidence at ${recovery.evidencePath}.`, "Before changing files or rerunning a command, inspect the workspace, receipts, logs, and partial artifacts. Do not assume the last command succeeded or failed merely because its reply was lost.", "Preserve accepted work. Do not repeat completed Work, create undeclared Work, alter the original requirements, or edit dd-flow runtime state/evidence.", "Observe an existing check or finish instead of duplicating it. Retry only through an exact runtime retry command after establishing that its prior outcome permits it.", "Record the concrete unresolved operation rather than guessing if an external effect cannot be reconciled.", "</recovery_instructions>"].join("\n"); }
|
|
@@ -1291,11 +1334,11 @@ function renderWorkerPrompt(context, work, run, dependencies, commands, workspac
|
|
|
1291
1334
|
const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
|
|
1292
1335
|
return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, ...sources.map(source => `Accepted ${source.role}: ${path.join(source.root === "run" ? requireRunHome(run) : run.workspace_root, source.path)} (sha256 ${source.sha256}). PROTOCOLIZE includes the effective amendments; preserve SPECIFY non-goals.`), acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
|
|
1293
1336
|
}
|
|
1294
|
-
function workerLifecycleCommands(context, work, run) {
|
|
1337
|
+
function workerLifecycleCommands(context, work, run, freshWorkAttempt = false) {
|
|
1295
1338
|
const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
|
|
1296
1339
|
return {
|
|
1297
|
-
finish: managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"])),
|
|
1298
|
-
fail: managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]))
|
|
1340
|
+
finish: managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"]), { freshWorkAttempt }),
|
|
1341
|
+
fail: managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]), { freshWorkAttempt })
|
|
1299
1342
|
};
|
|
1300
1343
|
}
|
|
1301
1344
|
function renderWorkerPromptBody(context, work, run, dependencies, commands) {
|
package/dist/storage/database.js
CHANGED
|
@@ -301,6 +301,9 @@ export function ensureVnextWorkStorage(db) {
|
|
|
301
301
|
// A committed Work start is authoritative even if publishing its prompt
|
|
302
302
|
// files is interrupted after commit. Keep the exact bytes for repair.
|
|
303
303
|
db.writeTransaction(() => {
|
|
304
|
+
const workColumns = db.all("PRAGMA table_info(works)");
|
|
305
|
+
if (!workColumns.some(column => column.name === "current_start_invocation_id"))
|
|
306
|
+
db.execSchema("ALTER TABLE works ADD COLUMN current_start_invocation_id TEXT");
|
|
304
307
|
const columns = db.all("PRAGMA table_info(work_sessions)");
|
|
305
308
|
if (!columns.some((column) => column.name === "start_receipt_json"))
|
|
306
309
|
db.execSchema("ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
|
|
@@ -1558,6 +1561,7 @@ function migrate(db) {
|
|
|
1558
1561
|
ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
|
|
1559
1562
|
ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
|
|
1560
1563
|
ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
|
|
1564
|
+
ensureColumn(db, "works", "current_start_invocation_id", "ALTER TABLE works ADD COLUMN current_start_invocation_id TEXT");
|
|
1561
1565
|
ensureColumn(db, "work_sessions", "start_receipt_json", "ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
|
|
1562
1566
|
ensureColumn(db, "work_sessions", "start_publication_status", "ALTER TABLE work_sessions ADD COLUMN start_publication_status TEXT");
|
|
1563
1567
|
ensureColumn(db, "work_sessions", "result_receipt_json", "ALTER TABLE work_sessions ADD COLUMN result_receipt_json TEXT");
|