@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
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 +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -1,16 +1,85 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { terminalCodexOverload } from "../harness-runtime/lib/
|
|
3
|
+
import { CAPACITY_POLICY, capacityBackoff, nativeItemsSettled, normalizeCodexFailure, overloadBurst, parseCapacityOperation, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
6
|
+
export function eligibleCodexChildOverload(child, parentSessionId) {
|
|
7
|
+
return typeof child.session_id === "string" && child.session_id !== parentSessionId && child.parent_session_id === parentSessionId && child.provenance !== "parent_mismatch" && child.status === "failed" && typeof child.terminal_turn_id === "string" && nativeItemsSettled(child.native_turn_items)
|
|
8
|
+
&& terminalCodexOverload({ code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: child.terminal_turn_id, native_turn_id: child.terminal_turn_id, terminal_status: "failed", provider_error: child.provider_error } }, child.session_id, child.terminal_turn_id);
|
|
9
|
+
}
|
|
10
|
+
/** A terminal root is not a settled tree. Only the daemon's exact owned tree
|
|
11
|
+
* with a settled root and positively live descendants permits waiting. */
|
|
12
|
+
export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false) {
|
|
13
|
+
if (seen.provider_session_id !== sessionId || seen.terminal_turn_id !== undefined && seen.terminal_turn_id !== turnId || !nativeItemsSettled(items))
|
|
14
|
+
return "unproven";
|
|
15
|
+
const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
|
|
16
|
+
if (!ids.length || ids[0] !== sessionId || ids.some(id => typeof id !== "string" || !id) || new Set(ids).size !== ids.length)
|
|
17
|
+
return "unproven";
|
|
18
|
+
const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
|
|
19
|
+
const exactTree = observations.length === ids.length && ids.every(id => observations.filter(item => item.provider_session_id === id).length === 1);
|
|
20
|
+
const settlement = seen.settlement;
|
|
21
|
+
const budgetSettled = !settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true;
|
|
22
|
+
const treeSettled = allowDescendants ? exactTree && observations.every(item => item.settled === true) && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0 : ids.length === 1;
|
|
23
|
+
if (seen.settled === true && treeSettled && budgetSettled)
|
|
24
|
+
return "ready";
|
|
25
|
+
if (!allowDescendants || seen.settled !== false || !exactTree || ids.length < 2 || observations[0]?.provider_session_id !== sessionId || observations[0].settled !== true
|
|
26
|
+
|| settlement && settlement.state !== "not_required" && !(settlement.state === "pending" && settlement.reason === "tree_unsettled"))
|
|
27
|
+
return "unproven";
|
|
28
|
+
const live = observations.filter(item => item.settled === false);
|
|
29
|
+
const unsettled = seen.unsettled_sessions;
|
|
30
|
+
const knownLive = live.length > 0 && live.every(item => item.provider_session_id !== sessionId && ["active", "running", "inProgress"].includes(String(typeof item.status === "object" && item.status ? item.status.type : item.status)));
|
|
31
|
+
return knownLive && observations.every(item => typeof item.settled === "boolean") && Array.isArray(unsettled) && unsettled.length === live.length && live.every(item => unsettled.includes(item.provider_session_id)) ? "owned_children_live" : "unproven";
|
|
32
|
+
}
|
|
5
33
|
/** Permit acquisition must still belong to the original task, not merely an
|
|
6
34
|
* open RUN. Controls/inspection and ordinary successor prompts are unaffected. */
|
|
7
|
-
export function assertCapacityOwnerAdmission(context, record, operationId) {
|
|
35
|
+
export function assertCapacityOwnerAdmission(context, record, operationId, binding) {
|
|
8
36
|
if (!operationId || !record.project_id || !record.run_id)
|
|
9
37
|
return;
|
|
10
|
-
const root = operationId.
|
|
11
|
-
|
|
38
|
+
const root = parseCapacityOperation(operationId).root;
|
|
39
|
+
const childOrdinal = /:child:[a-f0-9]+:followup:(\d+)$/.exec(root)?.[1];
|
|
40
|
+
if (childOrdinal && (!Number.isSafeInteger(Number(childOrdinal)) || Number(childOrdinal) < 1 || String(Number(childOrdinal)) !== childOrdinal))
|
|
41
|
+
throw new AppError("capacity_continuation_conflict", "Invalid native child continuation ordinal", 1);
|
|
42
|
+
if (root === operationId && !childOrdinal)
|
|
12
43
|
return;
|
|
13
44
|
const reject = () => { throw new AppError("capacity_owner_boundary_changed", "Queued continuation no longer belongs to an unfinished task; reconcile its durable state", 1, { operation_id: operationId, work_id: record.work_id, run_id: record.run_id }); };
|
|
45
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
46
|
+
const hasTable = (name) => Boolean(context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", [name]));
|
|
47
|
+
const parsed = parseCapacityOperation(operationId);
|
|
48
|
+
if (parsed.ordinal > 0 && !["judge", "probe"].includes(metadata.role ?? "")) {
|
|
49
|
+
if (!binding?.nativeSessionId || !binding.promptSha256)
|
|
50
|
+
reject();
|
|
51
|
+
let intent;
|
|
52
|
+
const predecessor = parsed.ordinal === 1 ? root : `${root}:capacity:${parsed.ordinal - 1}:session.prompt`;
|
|
53
|
+
if (hasTable("run_controller_operations")) {
|
|
54
|
+
const saved = context.db.get("SELECT receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'failed'", [predecessor, record.owner_id, metadata.run_generation ?? 0]);
|
|
55
|
+
if (saved)
|
|
56
|
+
intent = JSON.parse(saved.receipt_json).capacity_intent;
|
|
57
|
+
}
|
|
58
|
+
if (!intent && record.work_id)
|
|
59
|
+
for (const table of ["work_launches", "merge_requests"]) {
|
|
60
|
+
if (!hasTable(table))
|
|
61
|
+
continue;
|
|
62
|
+
const rows = table === "work_launches"
|
|
63
|
+
? context.db.all("SELECT l.adapter_receipt_json FROM work_launches l JOIN works w ON w.work_id = l.work_id WHERE l.work_id = ? AND w.project_id = ? AND w.run_id = ?", [record.work_id, record.project_id, record.run_id])
|
|
64
|
+
: context.db.all("SELECT adapter_receipt_json FROM merge_requests WHERE executor_work_id = ? AND project_id = ? AND run_id = ?", [record.work_id, record.project_id, record.run_id]);
|
|
65
|
+
for (const saved of rows) {
|
|
66
|
+
const retained = JSON.parse(saved.adapter_receipt_json ?? "null");
|
|
67
|
+
intent = retained?.capacity_intents?.find((item) => item.operation_id === operationId);
|
|
68
|
+
if (intent)
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (intent)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (!intent || intent.policy !== CAPACITY_POLICY || intent.root_operation_id !== root || intent.ordinal !== parsed.ordinal
|
|
75
|
+
|| intent.predecessor !== predecessor || intent.operation_id !== operationId || intent.refusal?.turn_id !== intent.turn_id
|
|
76
|
+
|| intent.refusal?.ordinal !== parsed.ordinal - 1 || intent.refusal?.session_id !== intent.session_id
|
|
77
|
+
|| !Number.isFinite(Date.parse(intent.not_before)) || Date.parse(intent.not_before) > Date.parse(context.now())
|
|
78
|
+
|| intent.deadline !== null && (typeof intent.deadline !== "number" || intent.deadline <= Date.parse(context.now()))
|
|
79
|
+
|| binding?.nativeSessionId !== undefined && intent.session_id !== binding.nativeSessionId
|
|
80
|
+
|| binding?.promptSha256 !== undefined && intent.prompt_sha256 !== binding.promptSha256)
|
|
81
|
+
reject();
|
|
82
|
+
}
|
|
14
83
|
if (context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_recovery_bindings'")) {
|
|
15
84
|
if (context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.status = 'recovered' AND b.status = 'accepted' AND json_extract(b.runtime_dispatch_json, '$.operation_id') = ?", [record.project_id, record.run_id, root]))
|
|
16
85
|
reject();
|
|
@@ -22,7 +91,7 @@ export function assertCapacityOwnerAdmission(context, record, operationId) {
|
|
|
22
91
|
reject();
|
|
23
92
|
// MERGE owns the Stage/request boundary: finishing its Work does not
|
|
24
93
|
// necessarily commit stage finish yet. External workers own only Work.
|
|
25
|
-
const mergeOwner =
|
|
94
|
+
const mergeOwner = metadata.role === "merge";
|
|
26
95
|
if (!work || !(merge && mergeOwner ? ["created", "running", "completed"] : ["created", "running"]).includes(work.status))
|
|
27
96
|
reject();
|
|
28
97
|
}
|
|
@@ -37,70 +106,140 @@ export function assertCapacityOwnerAdmission(context, record, operationId) {
|
|
|
37
106
|
if (state.stage && (index.current_stage && index.current_stage !== state.stage || index.stage_runs?.some(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status))))
|
|
38
107
|
reject();
|
|
39
108
|
const child = Object.entries(state.child_capacity ?? {}).find(([, intent]) => intent.operation_id === root)?.[0];
|
|
109
|
+
if (childOrdinal && !child)
|
|
110
|
+
reject();
|
|
40
111
|
if (child && context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND s.provider_session_id = ? AND w.status <> 'running'", [record.project_id, record.run_id, child]))
|
|
41
112
|
reject();
|
|
42
113
|
}
|
|
43
114
|
export function capacityDelay(error, ordinal, now = Date.now()) {
|
|
44
|
-
|
|
45
|
-
const headers = native?.headers;
|
|
46
|
-
const header = headers && Object.entries(headers).find(([key]) => key.toLowerCase() === "retry-after")?.[1];
|
|
47
|
-
const value = header ?? native?.retryAfter ?? native?.retry_after;
|
|
48
|
-
const seconds = typeof value === "number" || typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim()) ? Number(value) * 1000 : typeof value === "string" ? Date.parse(value) - now : NaN;
|
|
49
|
-
return Math.max(ordinal === 1 ? 5_000 : 15_000, Number.isFinite(seconds) ? Math.max(0, seconds) : 0);
|
|
115
|
+
return capacityBackoff(error, ordinal, now);
|
|
50
116
|
}
|
|
51
117
|
/** Continue a confirmed terminal Turn, never replay its completed effects. */
|
|
52
118
|
export async function continueCodexCapacity(input) {
|
|
53
119
|
let { error, operationId } = input;
|
|
54
|
-
const history = input.retainedIntents?.() ?? [];
|
|
55
|
-
const
|
|
120
|
+
const history = [...(input.retainedIntents?.() ?? [])];
|
|
121
|
+
const parsed = parseCapacityOperation(input.operationId);
|
|
122
|
+
const rootId = parsed.root;
|
|
56
123
|
const text = input.continuationText ?? "Continue the same assigned task in this Session. Check current durable RUN, Work and Stage state first. Never repeat an observed lifecycle command, accepted HITL answer, check or merge apply; use only an exact issued successor. Stop at the assigned boundary.";
|
|
57
124
|
const promptSha = crypto.createHash("sha256").update(text).digest("hex");
|
|
58
|
-
let ordinal =
|
|
125
|
+
let ordinal = parsed.ordinal + 1;
|
|
126
|
+
const conflict = (message) => { throw new AppError("capacity_continuation_conflict", message, 1, { operation_id: operationId, root_operation_id: rootId, session_id: input.sessionId }); };
|
|
127
|
+
const blocked = (message) => new AppError("capacity_continuation_blocked", message, 1, { capacity_uncertainty: true, operation_id: operationId, root_operation_id: rootId, provider_session_id: input.sessionId, primary_error: error });
|
|
128
|
+
const observe = async (action, observedOperationId = operationId) => {
|
|
129
|
+
try {
|
|
130
|
+
return await action();
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
let primary = cause;
|
|
134
|
+
for (let depth = 0; depth < 4 && primary?.code === "harness_adapter_failed"; depth++)
|
|
135
|
+
primary = (primary.cause ?? primary.details?.cause);
|
|
136
|
+
if (isObservationLoss(primary))
|
|
137
|
+
throw new AppError("operation_observation_lost", "Capacity native observation was lost; do not cancel or resend", 1, { capacity_uncertainty: true, root_operation_id: rootId, operation_id: observedOperationId, provider_session_id: input.sessionId, predecessor_error: error, cause });
|
|
138
|
+
throw cause;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const refusals = [...(input.retainedFailures?.() ?? [])];
|
|
142
|
+
if (parsed.ordinal > history.length)
|
|
143
|
+
conflict("Continuation has no complete retained predecessor chain");
|
|
144
|
+
for (let index = 0; index < history.length; index++) {
|
|
145
|
+
const intent = history[index];
|
|
146
|
+
const expected = index + 1;
|
|
147
|
+
if (intent.ordinal !== expected || intent.operation_id !== `${rootId}:capacity:${expected}:session.prompt`
|
|
148
|
+
|| intent.predecessor !== (expected === 1 ? rootId : `${rootId}:capacity:${expected - 1}:session.prompt`)
|
|
149
|
+
|| intent.prompt_sha256 !== promptSha || intent.policy !== CAPACITY_POLICY || intent.root_operation_id !== rootId || intent.session_id !== input.sessionId
|
|
150
|
+
|| intent.deadline !== (input.deadline ?? null) || !intent.refusal || intent.refusal.ordinal !== expected - 1 || intent.refusal.turn_id !== intent.turn_id
|
|
151
|
+
|| !Number.isFinite(Date.parse(intent.not_before)))
|
|
152
|
+
conflict("Retained capacity chain is incomplete or belongs to another policy/task");
|
|
153
|
+
const previous = refusals.find(item => item.ordinal === intent.refusal.ordinal);
|
|
154
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(intent.refusal))
|
|
155
|
+
conflict("Retained failure observation changed");
|
|
156
|
+
if (!previous)
|
|
157
|
+
refusals.push(intent.refusal);
|
|
158
|
+
}
|
|
159
|
+
refusals.sort((a, b) => a.ordinal - b.ordinal);
|
|
160
|
+
for (let index = 0; index < refusals.length; index++) {
|
|
161
|
+
const refusal = refusals[index];
|
|
162
|
+
if (refusal.ordinal !== index || refusal.policy !== CAPACITY_POLICY || refusal.session_id !== input.sessionId || !refusal.turn_id
|
|
163
|
+
|| refusals.slice(0, index).some(item => item.turn_id === refusal.turn_id) || !Number.isFinite(Date.parse(refusal.first_observed_at)))
|
|
164
|
+
conflict("Retained refusal chain is invalid");
|
|
165
|
+
}
|
|
59
166
|
// Reconcile a retained dispatch before preparing another. Native absence is
|
|
60
167
|
// the only safe way to reuse a prepared intent; a lost reply is not absence.
|
|
61
168
|
for (const intent of history) {
|
|
62
|
-
if (intent.
|
|
169
|
+
if (intent.ordinal < ordinal)
|
|
63
170
|
continue;
|
|
171
|
+
if (intent.predecessor !== operationId || intent.ordinal !== ordinal || intent.operation_id !== `${rootId}:capacity:${ordinal}:session.prompt`)
|
|
172
|
+
conflict("Retained successor skips its predecessor");
|
|
64
173
|
if (intent.prompt_sha256 !== promptSha || !intent.turn_id || !Number.isFinite(Date.parse(intent.not_before)))
|
|
65
174
|
throw error;
|
|
66
175
|
let observed;
|
|
67
176
|
try {
|
|
68
|
-
observed = await input.inspectOperation(intent.operation_id);
|
|
177
|
+
observed = await observe(() => input.inspectOperation(intent.operation_id), intent.operation_id);
|
|
69
178
|
}
|
|
70
179
|
catch (cause) {
|
|
71
180
|
let code = cause;
|
|
72
181
|
for (let depth = 0; depth < 4 && code?.code === "harness_adapter_failed"; depth++)
|
|
73
182
|
code = code.details?.cause;
|
|
74
183
|
if (code?.code !== "operation_not_found")
|
|
75
|
-
throw
|
|
184
|
+
throw cause;
|
|
76
185
|
break;
|
|
77
186
|
}
|
|
78
|
-
if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt"
|
|
79
|
-
|
|
187
|
+
if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt")
|
|
188
|
+
conflict("Retained native successor identity changed");
|
|
189
|
+
if (observed.primary_error)
|
|
190
|
+
throw observed.primary_error;
|
|
191
|
+
while (["prepared", "dispatching", "running", "awaiting_provider", "outcome_unknown"].includes(String(observed.state))) {
|
|
192
|
+
input.signal?.throwIfAborted();
|
|
193
|
+
input.assertCurrent();
|
|
194
|
+
const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
|
|
195
|
+
if (boundary === "completed" || boundary === "paused")
|
|
196
|
+
return { provider_session_id: input.sessionId, capacity_decision: boundary };
|
|
197
|
+
if (boundary === "reconciliation_blocked")
|
|
198
|
+
throw new AppError("capacity_continuation_blocked", "Retained native successor has an unresolved owner boundary", 1, { operation_id: intent.operation_id, primary_error: error });
|
|
199
|
+
if (input.deadline !== undefined && Date.now() >= input.deadline)
|
|
200
|
+
throw new AppError("capacity_owner_deadline_exceeded", "Retained successor exhausted its original deadline", 1);
|
|
201
|
+
await delay(250, undefined, { signal: input.signal });
|
|
202
|
+
observed = await observe(() => input.inspectOperation(intent.operation_id), intent.operation_id);
|
|
203
|
+
input.signal?.throwIfAborted();
|
|
204
|
+
input.assertCurrent();
|
|
205
|
+
if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt")
|
|
206
|
+
conflict("Retained native successor identity changed");
|
|
207
|
+
if (observed.primary_error)
|
|
208
|
+
throw observed.primary_error;
|
|
209
|
+
}
|
|
80
210
|
if (observed.state === "completed" && observed.result && typeof observed.result === "object") {
|
|
81
211
|
input.signal?.throwIfAborted();
|
|
82
212
|
input.assertCurrent();
|
|
83
|
-
|
|
213
|
+
const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
|
|
214
|
+
input.signal?.throwIfAborted();
|
|
215
|
+
input.assertCurrent();
|
|
216
|
+
if (boundary === "reconciliation_blocked")
|
|
217
|
+
throw new AppError("capacity_continuation_blocked", "Retained completion cannot be reconciled with its owner boundary", 1);
|
|
218
|
+
return boundary === "paused" || boundary === "completed" ? { provider_session_id: input.sessionId, capacity_decision: boundary } : observed.result;
|
|
84
219
|
}
|
|
220
|
+
if (observed.state === "failed" && observed.error && !terminalCodexOverload(observed.error, input.sessionId))
|
|
221
|
+
throw observed.error;
|
|
85
222
|
if (observed.state !== "failed" || !terminalCodexOverload(observed.error, input.sessionId))
|
|
86
|
-
throw error;
|
|
223
|
+
throw new AppError("operation_observation_lost", "Retained successor outcome is not proven; do not resend", 1, { operation_id: intent.operation_id, primary_error: error, observed });
|
|
87
224
|
operationId = intent.operation_id;
|
|
88
225
|
error = observed.error;
|
|
89
226
|
ordinal += 1;
|
|
90
227
|
}
|
|
91
|
-
const decide = async () => {
|
|
228
|
+
const decide = async (observedOperationId = operationId) => {
|
|
92
229
|
input.signal?.throwIfAborted();
|
|
93
230
|
input.assertCurrent();
|
|
94
|
-
const decision = await input.decision?.() ?? "continue";
|
|
231
|
+
const decision = await observe(async () => await input.decision?.() ?? "continue", observedOperationId);
|
|
95
232
|
input.signal?.throwIfAborted();
|
|
96
233
|
input.assertCurrent();
|
|
97
234
|
if (decision === "reconciliation_blocked")
|
|
98
|
-
throw error;
|
|
235
|
+
throw new AppError("capacity_continuation_blocked", "Native refusal cannot be reconciled with its durable owner boundary", 1, { primary_error: error, root_operation_id: rootId, provider_session_id: input.sessionId });
|
|
99
236
|
if (decision === "continue" && input.deadline !== undefined && Date.now() >= input.deadline)
|
|
100
|
-
throw error;
|
|
237
|
+
throw new AppError("capacity_owner_deadline_exceeded", "Continuation exhausted its original owner deadline", 1, { primary_error: error, deadline: input.deadline, root_operation_id: rootId, provider_session_id: input.sessionId });
|
|
101
238
|
return decision === "continue" ? null : { provider_session_id: input.sessionId, capacity_decision: decision };
|
|
102
239
|
};
|
|
103
|
-
for (
|
|
240
|
+
for (;; ordinal += 1) {
|
|
241
|
+
if (!Number.isSafeInteger(ordinal))
|
|
242
|
+
conflict("Continuation ordinal overflow");
|
|
104
243
|
if (!terminalCodexOverload(error, input.sessionId)) {
|
|
105
244
|
if (ordinal === 1)
|
|
106
245
|
return null;
|
|
@@ -109,82 +248,120 @@ export async function continueCodexCapacity(input) {
|
|
|
109
248
|
const completed = await decide();
|
|
110
249
|
if (completed)
|
|
111
250
|
return completed;
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
prior = await input.inspectOperation(operationId);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
if (ordinal === 1)
|
|
118
|
-
return null;
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
251
|
+
const prior = await observe(() => input.inspectOperation(operationId));
|
|
121
252
|
const failure = prior.error;
|
|
122
|
-
const
|
|
253
|
+
const confirmed = normalizeCodexFailure(failure, input.sessionId);
|
|
254
|
+
const turnId = confirmed?.turn_id;
|
|
255
|
+
input.signal?.throwIfAborted();
|
|
256
|
+
input.assertCurrent();
|
|
257
|
+
if (prior.primary_error)
|
|
258
|
+
throw prior.primary_error;
|
|
123
259
|
if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
|
|
124
260
|
|| prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)) {
|
|
125
|
-
|
|
126
|
-
return null;
|
|
127
|
-
throw error;
|
|
261
|
+
throw blocked("Retained predecessor does not prove the exact terminal overloaded Turn");
|
|
128
262
|
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
const treeProven = input.allowSettledDescendants
|
|
138
|
-
? ids.length > 0 && ids[0] === input.sessionId && observations.length === ids.length
|
|
139
|
-
&& ids.every(id => observations.some(item => item.provider_session_id === id && item.settled === true))
|
|
140
|
-
&& Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0
|
|
141
|
-
: ids.length === 1 && ids[0] === input.sessionId;
|
|
142
|
-
return seen.provider_session_id === input.sessionId && seen.settled === true && treeProven
|
|
143
|
-
&& (!settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true);
|
|
263
|
+
const readiness = async () => {
|
|
264
|
+
input.signal?.throwIfAborted();
|
|
265
|
+
input.assertCurrent();
|
|
266
|
+
const seen = await observe(() => input.inspectSession(turnId));
|
|
267
|
+
input.signal?.throwIfAborted();
|
|
268
|
+
input.assertCurrent();
|
|
269
|
+
const items = (seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : confirmed?.native_turn_items);
|
|
270
|
+
return capacityReadiness(seen, input.sessionId, operationId, turnId, items, input.allowSettledDescendants);
|
|
144
271
|
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
272
|
+
const ready = await readiness();
|
|
273
|
+
if (ready === "unproven")
|
|
274
|
+
throw blocked("Native predecessor readiness is unproven; do not cancel or replay to manufacture proof");
|
|
275
|
+
const native = normalizeCodexFailure(failure, input.sessionId, turnId);
|
|
276
|
+
let refusal = refusals.find(item => item.ordinal === ordinal - 1);
|
|
277
|
+
if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
|
|
278
|
+
conflict("Failure ordinal identifies a different native Turn");
|
|
279
|
+
if (refusal) {
|
|
280
|
+
const expected = refusalObservation(failure, ordinal - 1, refusal.first_observed_at);
|
|
281
|
+
if (refusal.native_terminal_at !== expected.native_terminal_at || refusal.native_terminal_basis !== expected.native_terminal_basis)
|
|
282
|
+
conflict("Retained refusal time differs from its native receipt");
|
|
151
283
|
}
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
284
|
+
if (!refusal) {
|
|
285
|
+
if (refusals.some(item => item.turn_id === turnId))
|
|
286
|
+
conflict("A cached native Turn cannot be a second refusal");
|
|
287
|
+
// A cached legacy failure cannot be assigned a new time during reattach.
|
|
288
|
+
if (ordinal > 1 && parsed.ordinal > 0 && !native.observed_at)
|
|
289
|
+
conflict("Retained continuation has no original failure time");
|
|
290
|
+
const observedAt = typeof native.observed_at === "string" && Number.isFinite(Date.parse(native.observed_at)) ? native.observed_at : new Date().toISOString();
|
|
291
|
+
refusal = refusalObservation(failure, ordinal - 1, observedAt);
|
|
292
|
+
if (!input.atomicIntent)
|
|
293
|
+
input.retainFailure?.(operationId, refusal);
|
|
294
|
+
refusals.push(refusal);
|
|
295
|
+
}
|
|
296
|
+
const burst = overloadBurst(refusals);
|
|
297
|
+
if (burst) {
|
|
298
|
+
if (input.atomicIntent)
|
|
299
|
+
input.retainFailure?.(operationId, refusal);
|
|
300
|
+
throw new AppError("provider_overload_burst", "Two consecutive continuation Turns were overloaded within two minutes", 1, { ...burst, root_operation_id: rootId, provider_session_id: input.sessionId, continuation_count: ordinal - 1, primary_error: failure });
|
|
156
301
|
}
|
|
157
302
|
const nextId = `${rootId}:capacity:${ordinal}:session.prompt`;
|
|
158
303
|
const existing = history.find(item => item.operation_id === nextId);
|
|
159
304
|
if (existing && (existing.predecessor !== operationId || existing.turn_id !== turnId || existing.prompt_sha256 !== promptSha || existing.ordinal !== ordinal))
|
|
160
305
|
throw error;
|
|
161
306
|
const wait = existing ? Math.max(0, Date.parse(existing.not_before) - Date.now()) : capacityDelay(failure, ordinal);
|
|
162
|
-
if (!Number.isFinite(wait) || wait >
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
307
|
+
if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15 || input.deadline !== undefined && Date.now() + wait >= input.deadline) {
|
|
308
|
+
if (input.atomicIntent)
|
|
309
|
+
input.retainFailure?.(operationId, refusal);
|
|
310
|
+
throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
|
|
311
|
+
}
|
|
312
|
+
if (!existing) {
|
|
313
|
+
const intent = { predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString(), policy: CAPACITY_POLICY, root_operation_id: rootId, session_id: input.sessionId, deadline: input.deadline ?? null, refusal };
|
|
314
|
+
input.retainIntent(intent);
|
|
315
|
+
history.push(intent);
|
|
316
|
+
}
|
|
317
|
+
// Intent/refusal are durable before the first wait; no permit or writer is
|
|
318
|
+
// held here, and live siblings are never cancelled as a settlement proof.
|
|
319
|
+
const waitForOwnedChildren = async (initial) => {
|
|
320
|
+
let current = initial;
|
|
321
|
+
while (current === "owned_children_live") {
|
|
322
|
+
await delay(250, undefined, { signal: input.signal });
|
|
323
|
+
const boundary = await decide();
|
|
324
|
+
if (boundary)
|
|
325
|
+
return boundary;
|
|
326
|
+
current = await readiness();
|
|
327
|
+
}
|
|
328
|
+
if (current !== "ready")
|
|
329
|
+
throw blocked("Owned native child readiness became unproven");
|
|
330
|
+
return null;
|
|
331
|
+
};
|
|
332
|
+
const childBoundary = await waitForOwnedChildren(ready);
|
|
333
|
+
if (childBoundary)
|
|
334
|
+
return childBoundary;
|
|
335
|
+
// Node timers truncate large delays. Bounded slices preserve cancellation
|
|
336
|
+
// and owner checks; monotonic elapsed time does not renew a Retry-After.
|
|
337
|
+
const until = performance.now() + Math.max(0, Date.parse(history.find(item => item.operation_id === nextId).not_before) - Date.now());
|
|
338
|
+
while (performance.now() < until) {
|
|
339
|
+
await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: input.signal });
|
|
340
|
+
const boundary = await decide();
|
|
341
|
+
if (boundary)
|
|
342
|
+
return boundary;
|
|
343
|
+
}
|
|
167
344
|
const afterWait = await decide();
|
|
168
345
|
if (afterWait)
|
|
169
346
|
return afterWait;
|
|
170
|
-
|
|
171
|
-
|
|
347
|
+
const refreshedBoundary = await waitForOwnedChildren(await readiness());
|
|
348
|
+
if (refreshedBoundary)
|
|
349
|
+
return refreshedBoundary;
|
|
172
350
|
const beforeDispatch = await decide();
|
|
173
351
|
if (beforeDispatch)
|
|
174
352
|
return beforeDispatch;
|
|
175
353
|
try {
|
|
176
|
-
return await input.prompt(nextId, text, ordinal);
|
|
354
|
+
return await observe(() => input.prompt(nextId, text, ordinal), nextId);
|
|
177
355
|
}
|
|
178
356
|
catch (nextError) {
|
|
179
357
|
// A task can complete/pause while the daemon queues for its permit.
|
|
180
358
|
// Admission then refuses the stale prompt; accept that owner decision
|
|
181
359
|
// before treating the transport's rejection as another provider failure.
|
|
182
|
-
const finished = await decide();
|
|
360
|
+
const finished = await decide(nextId);
|
|
183
361
|
if (finished)
|
|
184
362
|
return finished;
|
|
185
363
|
error = nextError;
|
|
186
364
|
operationId = nextId;
|
|
187
365
|
}
|
|
188
366
|
}
|
|
189
|
-
throw error;
|
|
190
367
|
}
|
|
@@ -5,8 +5,8 @@ import { commandOption, lifecycleCommandFromPayload, parseLifecycleCommand, cont
|
|
|
5
5
|
/** The hook asks the existing daemon about native evidence; it cannot mint it. */
|
|
6
6
|
export async function verifyCodexHookDelivery(env, payload) {
|
|
7
7
|
const command = lifecycleCommandFromPayload(payload);
|
|
8
|
-
const parsed = command ? parseLifecycleCommand(command) : { kind: "none" };
|
|
9
|
-
if (parsed.kind === "none" && !parsed.
|
|
8
|
+
const parsed = command ? parseLifecycleCommand(command) : { kind: "none", participation: "none" };
|
|
9
|
+
if (parsed.kind === "none" && !["lifecycle", "auxiliary"].includes(parsed.participation) && !(command && continuationCommand(command)))
|
|
10
10
|
return undefined;
|
|
11
11
|
const managed = (parsed.kind !== "none" && commandOption(parsed.invocation, "invocation-id")) || env.DD_FLOW_DAEMON_ID;
|
|
12
12
|
if (!managed)
|
|
@@ -11,7 +11,10 @@ export function preflightCliCompatibility(context, args, env = process.env, reso
|
|
|
11
11
|
if (!projectRoot) {
|
|
12
12
|
return { ok: true, classification, compatibility: null };
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// Compatibility checks the executing build/manifest identity. Routing and
|
|
15
|
+
// binding validate snapshot bytes before dispatch; maintenance must not
|
|
16
|
+
// rehash the dependency closure or spawn health probes on every lease ACK.
|
|
17
|
+
const selection = selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true, verifyCurrentContent: false }, classification);
|
|
15
18
|
const compatibility = compatibilityReport(selection, classification);
|
|
16
19
|
if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
|
|
17
20
|
return { ok: true, classification, compatibility };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { AppError } from "../shared/errors.js";
|
|
1
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
|
+
import { normalizeNativeChildren } from "../harness-runtime/lib/native-children.mjs";
|
|
2
3
|
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
|
|
3
4
|
import { getFlowRunVariables } from "./runs.js";
|
|
4
5
|
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
@@ -57,7 +58,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
57
58
|
throw new AppError("stage_graph_compatibility_required", "The retained graph cannot be dispatched by this controller; use the RUN's pinned engine", 1, { stage: input.stage, run_id: input.runId, graph: fanout });
|
|
58
59
|
if (children.length) {
|
|
59
60
|
const reconciled = await reconcileVnextFanout(context, { ...input, observations });
|
|
60
|
-
const issues = reconciled.issues.filter(issue => issue.code
|
|
61
|
+
const issues = reconciled.issues.filter(issue => !["child_check_in_progress", "child_capacity_pending"].includes(issue.code));
|
|
61
62
|
if (issues.length)
|
|
62
63
|
throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
|
|
63
64
|
if (children.some(child => child.status === "running" || child.status === "unknown"))
|
|
@@ -106,80 +107,13 @@ export async function nextControllerFanout(context, input) {
|
|
|
106
107
|
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
107
108
|
return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
|
|
108
109
|
}
|
|
109
|
-
/**
|
|
110
|
+
/** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
|
|
110
111
|
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const list = (value) => Array.isArray(value) ? value : [];
|
|
114
|
-
const text = (value, keys) => { for (const key of keys)
|
|
115
|
-
if (typeof value[key] === "string" && value[key])
|
|
116
|
-
return value[key]; return null; };
|
|
117
|
-
const settledTree = new Set(list(receipt.tree_observations)
|
|
118
|
-
.filter(value => object(value).settled === true)
|
|
119
|
-
.map(value => text(object(value), ["provider_session_id", "session_id"]))
|
|
120
|
-
.filter((id) => id !== null));
|
|
121
|
-
const add = (raw, source, scopedParent = null, fallback = "unknown") => {
|
|
122
|
-
const value = object(raw);
|
|
123
|
-
const id = text(value, ["provider_session_id", "session_id", "sessionId", "childSessionId", "conversation_id", "conversationId", "id", "subagentId"]);
|
|
124
|
-
const parent = text(value, ["parent_provider_session_id", "parent_session_id", "parentSessionId", "parentID", "parentId"]) ?? scopedParent;
|
|
125
|
-
if (!id)
|
|
126
|
-
return;
|
|
127
|
-
const outcomeError = object(value.outcome_error);
|
|
128
|
-
if (typeof outcomeError.code === "string")
|
|
129
|
-
throw new AppError(outcomeError.code, String(outcomeError.message ?? "Native child outcome is unconfirmed"), 1, { ...object(outcomeError.details), session_id: id, source });
|
|
130
|
-
const state = String(value.status ?? value.state ?? fallback).toLowerCase();
|
|
131
|
-
if (state === "lost")
|
|
132
|
-
throw new AppError("native_child_outcome_lost", "Native child ended without a recoverable outcome", 1, { session_id: id, parent_session_id: parent, source, native_status: state });
|
|
133
|
-
// Provider idle means no current activity, not a Work result. Only an
|
|
134
|
-
// explicit terminal result or a separately proven root settlement may be
|
|
135
|
-
// terminal here; unknown states stay unknown for reconciliation.
|
|
136
|
-
const status = ["completed", "complete", "done", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled", "interrupted"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
|
|
137
|
-
const rootChild = parent === rootSessionId;
|
|
138
|
-
const settled = rootChild && status === "unknown" && settledTree.has(id) ? "settled_by_root" : status;
|
|
139
|
-
candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }),
|
|
140
|
-
...(text(value, ["terminal_turn_id"]) ? { terminal_turn_id: text(value, ["terminal_turn_id"]) } : {}),
|
|
141
|
-
...(value.provider_error !== undefined ? { provider_error: value.provider_error } : {}),
|
|
142
|
-
...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) });
|
|
143
|
-
};
|
|
144
|
-
for (const value of list(receipt.descendants))
|
|
145
|
-
add(value, "adapter.descendants");
|
|
146
|
-
for (const value of list(receipt.children))
|
|
147
|
-
add(value, "opencode.session.children");
|
|
148
|
-
const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
|
|
149
|
-
for (const value of list(subagents.running))
|
|
150
|
-
add(value, "zcode/session/subagents", rootSessionId, "running");
|
|
151
|
-
for (const value of list(subagents.completed))
|
|
152
|
-
add(value, "zcode/session/subagents", rootSessionId, "completed");
|
|
153
|
-
for (const value of list(object(subagents.ended).items)) {
|
|
154
|
-
const status = String(object(value).status ?? "unknown");
|
|
155
|
-
if (!["success", "failed", "cancelled", "lost"].includes(status))
|
|
156
|
-
throw new AppError("native_child_outcome_unavailable", "ZCode ended child has no recognized native outcome", 1, { child: value });
|
|
157
|
-
add(value, "zcode/session/subagents", rootSessionId);
|
|
112
|
+
try {
|
|
113
|
+
return normalizeNativeChildren(receipt, rootSessionId);
|
|
158
114
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const previous = unique.get(child.session_id);
|
|
163
|
-
if (previous && previous.parent_session_id !== child.parent_session_id) {
|
|
164
|
-
// Keep the contradictory parent visible to the existing provenance gate,
|
|
165
|
-
// independently of input order or which source happened to say success.
|
|
166
|
-
const foreign = [previous, child].filter(item => item.parent_session_id !== rootSessionId).sort((a, b) => a.parent_session_id.localeCompare(b.parent_session_id))[0];
|
|
167
|
-
unique.set(child.session_id, { ...foreign, provenance: "parent_mismatch" });
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (previous && rank[previous.status] === 3 && rank[child.status] === 3 && previous.status !== child.status)
|
|
171
|
-
throw new AppError("native_child_outcome_conflict", "Native child has contradictory terminal outcomes", 1, { session_id: child.session_id, observations: [previous, child] });
|
|
172
|
-
if (previous && previous.status === child.status && previous.terminal_turn_id && child.terminal_turn_id && previous.terminal_turn_id !== child.terminal_turn_id)
|
|
173
|
-
throw new AppError("native_child_turn_conflict", "Native child has contradictory terminal Turn identities", 1, { session_id: child.session_id, observations: [previous, child] });
|
|
174
|
-
if (previous && previous.status === child.status) {
|
|
175
|
-
unique.set(child.session_id, { ...previous, ...child,
|
|
176
|
-
...(child.terminal_turn_id ?? previous.terminal_turn_id ? { terminal_turn_id: child.terminal_turn_id ?? previous.terminal_turn_id } : {}),
|
|
177
|
-
...(child.provider_error ?? previous.provider_error ? { provider_error: child.provider_error ?? previous.provider_error } : {}),
|
|
178
|
-
...(child.native_turn_items ?? previous.native_turn_items ? { native_turn_items: child.native_turn_items ?? previous.native_turn_items } : {}) });
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (!previous || rank[child.status] > rank[previous.status])
|
|
182
|
-
unique.set(child.session_id, child);
|
|
115
|
+
catch (error) {
|
|
116
|
+
const record = errorRecord(error);
|
|
117
|
+
throw Object.assign(new AppError(record.code, record.message, record.exitCode, record.details), record);
|
|
183
118
|
}
|
|
184
|
-
return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
|
|
185
119
|
}
|