@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.124

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +23 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli/run-cli.js +3 -0
  5. package/dist/domain/stage-catalog.js +1 -1
  6. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  7. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  8. package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
  10. package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
  12. package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
  15. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
  16. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
  17. package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
  18. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  19. package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
  20. package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
  21. package/dist/harness-runtime/lib/model-observations.mjs +16 -9
  22. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  23. package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
  24. package/dist/schemas/flow-run.schema.json +1 -0
  25. package/dist/schemas/plan-review-decision.schema.json +3 -3
  26. package/dist/services/code-checks.js +14 -18
  27. package/dist/services/codex-capacity.js +251 -31
  28. package/dist/services/controller-fanout.js +1 -0
  29. package/dist/services/eval-snapshots.js +13 -8
  30. package/dist/services/external-work-launch.js +36 -11
  31. package/dist/services/harness-adapter.js +21 -15
  32. package/dist/services/managed-processes.js +14 -6
  33. package/dist/services/merge-server.js +35 -12
  34. package/dist/services/review-copy.js +21 -0
  35. package/dist/services/review-deferral.js +23 -0
  36. package/dist/services/run-controller-adapter.js +33 -8
  37. package/dist/services/run-controller-recovery.js +26 -2
  38. package/dist/services/run-controller.js +91 -24
  39. package/dist/services/run-observations.js +7 -2
  40. package/dist/services/run-recovery-runtime.js +49 -3
  41. package/dist/services/runs.js +1 -1
  42. package/dist/services/stage-report-renderer.js +25 -2
  43. package/dist/services/verification-matrix.js +57 -8
  44. package/dist/services/vnext-code-review.js +22 -8
  45. package/dist/services/vnext-merge.js +20 -2
  46. package/dist/services/vnext-plan-review.js +87 -19
  47. package/dist/services/work-registry.js +11 -2
  48. package/package.json +1 -1
@@ -1,34 +1,211 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
- import { terminalCodexOverload } from "../harness-runtime/lib/dd-codex.mjs";
3
- /** Only a terminal, effect-free native Turn may get another productive Turn. */
2
+ import crypto from "node:crypto";
3
+ import { CAPACITY_POLICY, capacityBackoff, nativeItemsSettled, normalizeCodexFailure, overloadBurst, parseCapacityOperation, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
4
+ import { AppError } from "../shared/errors.js";
5
+ /** Permit acquisition must still belong to the original task, not merely an
6
+ * open RUN. Controls/inspection and ordinary successor prompts are unaffected. */
7
+ export function assertCapacityOwnerAdmission(context, record, operationId, binding) {
8
+ if (!operationId || !record.project_id || !record.run_id)
9
+ return;
10
+ const root = parseCapacityOperation(operationId).root;
11
+ const childOrdinal = /:child:[a-f0-9]+:followup:(\d+)$/.exec(root)?.[1];
12
+ if (childOrdinal && (!Number.isSafeInteger(Number(childOrdinal)) || Number(childOrdinal) < 1 || String(Number(childOrdinal)) !== childOrdinal))
13
+ throw new AppError("capacity_continuation_conflict", "Invalid native child continuation ordinal", 1);
14
+ if (root === operationId && !childOrdinal)
15
+ return;
16
+ 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 }); };
17
+ const metadata = JSON.parse(record.metadata_json);
18
+ const hasTable = (name) => Boolean(context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", [name]));
19
+ const parsed = parseCapacityOperation(operationId);
20
+ if (parsed.ordinal > 0 && !["judge", "probe"].includes(metadata.role ?? "")) {
21
+ if (!binding?.nativeSessionId || !binding.promptSha256)
22
+ reject();
23
+ let intent;
24
+ const predecessor = parsed.ordinal === 1 ? root : `${root}:capacity:${parsed.ordinal - 1}:session.prompt`;
25
+ if (hasTable("run_controller_operations")) {
26
+ 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]);
27
+ if (saved)
28
+ intent = JSON.parse(saved.receipt_json).capacity_intent;
29
+ }
30
+ if (!intent && record.work_id)
31
+ for (const table of ["work_launches", "merge_requests"]) {
32
+ if (!hasTable(table))
33
+ continue;
34
+ const rows = table === "work_launches"
35
+ ? 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])
36
+ : 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]);
37
+ for (const saved of rows) {
38
+ const retained = JSON.parse(saved.adapter_receipt_json ?? "null");
39
+ intent = retained?.capacity_intents?.find((item) => item.operation_id === operationId);
40
+ if (intent)
41
+ break;
42
+ }
43
+ if (intent)
44
+ break;
45
+ }
46
+ if (!intent || intent.policy !== CAPACITY_POLICY || intent.root_operation_id !== root || intent.ordinal !== parsed.ordinal
47
+ || intent.predecessor !== predecessor || intent.operation_id !== operationId || intent.refusal?.turn_id !== intent.turn_id
48
+ || intent.refusal?.ordinal !== parsed.ordinal - 1 || intent.refusal?.session_id !== intent.session_id
49
+ || !Number.isFinite(Date.parse(intent.not_before)) || Date.parse(intent.not_before) > Date.parse(context.now())
50
+ || intent.deadline !== null && (typeof intent.deadline !== "number" || intent.deadline <= Date.parse(context.now()))
51
+ || binding?.nativeSessionId !== undefined && intent.session_id !== binding.nativeSessionId
52
+ || binding?.promptSha256 !== undefined && intent.prompt_sha256 !== binding.promptSha256)
53
+ reject();
54
+ }
55
+ if (context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_recovery_bindings'")) {
56
+ 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]))
57
+ reject();
58
+ }
59
+ if (record.work_id) {
60
+ const work = context.db.get("SELECT status FROM works WHERE work_id = ? AND project_id = ? AND run_id = ?", [record.work_id, record.project_id, record.run_id]);
61
+ const merge = context.db.get("SELECT status FROM merge_requests WHERE executor_work_id = ? AND project_id = ? AND run_id = ?", [record.work_id, record.project_id, record.run_id]);
62
+ if (merge && !["dispatching", "active"].includes(merge.status))
63
+ reject();
64
+ // MERGE owns the Stage/request boundary: finishing its Work does not
65
+ // necessarily commit stage finish yet. External workers own only Work.
66
+ const mergeOwner = metadata.role === "merge";
67
+ if (!work || !(merge && mergeOwner ? ["created", "running", "completed"] : ["created", "running"]).includes(work.status))
68
+ reject();
69
+ }
70
+ if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'"))
71
+ return;
72
+ const controller = context.db.get("SELECT state_json FROM run_controllers WHERE controller_id = ? AND project_id = ? AND run_id = ?", [record.owner_id, record.project_id, record.run_id]);
73
+ if (!controller)
74
+ return;
75
+ const state = JSON.parse(controller.state_json);
76
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
77
+ const index = JSON.parse(run?.index_json ?? "{}");
78
+ 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))))
79
+ reject();
80
+ const child = Object.entries(state.child_capacity ?? {}).find(([, intent]) => intent.operation_id === root)?.[0];
81
+ if (childOrdinal && !child)
82
+ reject();
83
+ 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]))
84
+ reject();
85
+ }
86
+ export function capacityDelay(error, ordinal, now = Date.now()) {
87
+ return capacityBackoff(error, ordinal, now);
88
+ }
89
+ /** Continue a confirmed terminal Turn, never replay its completed effects. */
4
90
  export async function continueCodexCapacity(input) {
5
91
  let { error, operationId } = input;
6
- for (let ordinal = 1; ordinal <= 2; ordinal += 1) {
7
- if (!terminalCodexOverload(error, input.sessionId)) {
8
- if (ordinal === 1)
9
- return null;
92
+ const history = [...(input.retainedIntents?.() ?? [])];
93
+ const parsed = parseCapacityOperation(input.operationId);
94
+ const rootId = parsed.root;
95
+ 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.";
96
+ const promptSha = crypto.createHash("sha256").update(text).digest("hex");
97
+ let ordinal = parsed.ordinal + 1;
98
+ const conflict = (message) => { throw new AppError("capacity_continuation_conflict", message, 1, { operation_id: operationId, root_operation_id: rootId, session_id: input.sessionId }); };
99
+ const refusals = [...(input.retainedFailures?.() ?? [])];
100
+ if (parsed.ordinal > history.length)
101
+ conflict("Continuation has no complete retained predecessor chain");
102
+ for (let index = 0; index < history.length; index++) {
103
+ const intent = history[index];
104
+ const expected = index + 1;
105
+ if (intent.ordinal !== expected || intent.operation_id !== `${rootId}:capacity:${expected}:session.prompt`
106
+ || intent.predecessor !== (expected === 1 ? rootId : `${rootId}:capacity:${expected - 1}:session.prompt`)
107
+ || intent.prompt_sha256 !== promptSha || intent.policy !== CAPACITY_POLICY || intent.root_operation_id !== rootId || intent.session_id !== input.sessionId
108
+ || intent.deadline !== (input.deadline ?? null) || !intent.refusal || intent.refusal.ordinal !== expected - 1 || intent.refusal.turn_id !== intent.turn_id
109
+ || !Number.isFinite(Date.parse(intent.not_before)))
110
+ conflict("Retained capacity chain is incomplete or belongs to another policy/task");
111
+ const previous = refusals.find(item => item.ordinal === intent.refusal.ordinal);
112
+ if (previous && JSON.stringify(previous) !== JSON.stringify(intent.refusal))
113
+ conflict("Retained failure observation changed");
114
+ if (!previous)
115
+ refusals.push(intent.refusal);
116
+ }
117
+ refusals.sort((a, b) => a.ordinal - b.ordinal);
118
+ for (let index = 0; index < refusals.length; index++) {
119
+ const refusal = refusals[index];
120
+ if (refusal.ordinal !== index || refusal.policy !== CAPACITY_POLICY || refusal.session_id !== input.sessionId || !refusal.turn_id
121
+ || refusals.slice(0, index).some(item => item.turn_id === refusal.turn_id) || !Number.isFinite(Date.parse(refusal.first_observed_at)))
122
+ conflict("Retained refusal chain is invalid");
123
+ }
124
+ // Reconcile a retained dispatch before preparing another. Native absence is
125
+ // the only safe way to reuse a prepared intent; a lost reply is not absence.
126
+ for (const intent of history) {
127
+ if (intent.ordinal < ordinal)
128
+ continue;
129
+ if (intent.predecessor !== operationId || intent.ordinal !== ordinal || intent.operation_id !== `${rootId}:capacity:${ordinal}:session.prompt`)
130
+ conflict("Retained successor skips its predecessor");
131
+ if (intent.prompt_sha256 !== promptSha || !intent.turn_id || !Number.isFinite(Date.parse(intent.not_before)))
10
132
  throw error;
11
- }
12
- let prior;
133
+ let observed;
13
134
  try {
14
- prior = await input.inspectOperation(operationId);
135
+ observed = await input.inspectOperation(intent.operation_id);
136
+ }
137
+ catch (cause) {
138
+ let code = cause;
139
+ for (let depth = 0; depth < 4 && code?.code === "harness_adapter_failed"; depth++)
140
+ code = code.details?.cause;
141
+ if (code?.code !== "operation_not_found")
142
+ throw cause;
143
+ break;
144
+ }
145
+ if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt" || observed.primary_error)
146
+ throw error;
147
+ if (observed.state === "completed" && observed.result && typeof observed.result === "object") {
148
+ input.signal?.throwIfAborted();
149
+ input.assertCurrent();
150
+ const boundary = await input.decision?.() ?? "continue";
151
+ input.signal?.throwIfAborted();
152
+ input.assertCurrent();
153
+ if (boundary === "reconciliation_blocked")
154
+ throw new AppError("capacity_continuation_blocked", "Retained completion cannot be reconciled with its owner boundary", 1);
155
+ return boundary === "paused" || boundary === "completed" ? { provider_session_id: input.sessionId, capacity_decision: boundary } : observed.result;
15
156
  }
16
- catch {
157
+ if (observed.state !== "failed" || !terminalCodexOverload(observed.error, input.sessionId))
158
+ throw error;
159
+ operationId = intent.operation_id;
160
+ error = observed.error;
161
+ ordinal += 1;
162
+ }
163
+ const decide = async () => {
164
+ input.signal?.throwIfAborted();
165
+ input.assertCurrent();
166
+ const decision = await input.decision?.() ?? "continue";
167
+ input.signal?.throwIfAborted();
168
+ input.assertCurrent();
169
+ if (decision === "reconciliation_blocked")
170
+ 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 });
171
+ if (decision === "continue" && input.deadline !== undefined && Date.now() >= input.deadline)
172
+ 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 });
173
+ return decision === "continue" ? null : { provider_session_id: input.sessionId, capacity_decision: decision };
174
+ };
175
+ for (;; ordinal += 1) {
176
+ if (!Number.isSafeInteger(ordinal))
177
+ conflict("Continuation ordinal overflow");
178
+ if (!terminalCodexOverload(error, input.sessionId)) {
17
179
  if (ordinal === 1)
18
180
  return null;
19
181
  throw error;
20
182
  }
183
+ const completed = await decide();
184
+ if (completed)
185
+ return completed;
186
+ const prior = await input.inspectOperation(operationId);
21
187
  const failure = prior.error;
22
- const turnId = failure?.details?.turn_id;
188
+ const confirmed = normalizeCodexFailure(failure, input.sessionId);
189
+ const turnId = confirmed?.turn_id;
190
+ input.signal?.throwIfAborted();
191
+ input.assertCurrent();
23
192
  if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
24
- || prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)
25
- || failure.details?.native_turn_items?.observed !== true || failure.details.native_turn_items.possible_effects !== false) {
193
+ || prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)) {
26
194
  if (ordinal === 1)
27
195
  return null;
28
196
  throw error;
29
197
  }
30
198
  const settled = async () => {
31
- const seen = await input.inspectSession();
199
+ input.signal?.throwIfAborted();
200
+ input.assertCurrent();
201
+ const seen = await input.inspectSession(turnId);
202
+ input.signal?.throwIfAborted();
203
+ input.assertCurrent();
204
+ if (seen.terminal_turn_id !== undefined && seen.terminal_turn_id !== turnId)
205
+ return false;
206
+ const items = seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : confirmed?.native_turn_items;
207
+ if (!nativeItemsSettled(items))
208
+ return false;
32
209
  const settlement = seen.settlement;
33
210
  const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
34
211
  const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
@@ -38,34 +215,77 @@ export async function continueCodexCapacity(input) {
38
215
  && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0
39
216
  : ids.length === 1 && ids[0] === input.sessionId;
40
217
  return seen.provider_session_id === input.sessionId && seen.settled === true && treeProven
41
- && (!settlement || ["settled", "not_required"].includes(settlement.state ?? ""));
218
+ && (!settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true);
42
219
  };
43
- input.assertCurrent();
44
- try {
45
- if (!await settled()) {
46
- if (ordinal === 1)
47
- return null;
48
- throw error;
49
- }
50
- }
51
- catch {
220
+ if (!await settled()) {
52
221
  if (ordinal === 1)
53
222
  return null;
54
223
  throw error;
55
224
  }
56
- const nextId = `${input.operationId}:capacity:${ordinal}:session.prompt`;
57
- input.retainIntent({ predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, not_before: new Date(Date.now() + (ordinal === 1 ? 5_000 : 15_000)).toISOString() });
58
- await delay(ordinal === 1 ? 5_000 : 15_000);
59
- input.assertCurrent();
60
- if (!await settled().catch(() => false))
225
+ const native = normalizeCodexFailure(failure, input.sessionId, turnId);
226
+ let refusal = refusals.find(item => item.ordinal === ordinal - 1);
227
+ if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
228
+ conflict("Failure ordinal identifies a different native Turn");
229
+ if (refusal) {
230
+ const expected = refusalObservation(failure, ordinal - 1, refusal.first_observed_at);
231
+ if (refusal.native_terminal_at !== expected.native_terminal_at || refusal.native_terminal_basis !== expected.native_terminal_basis)
232
+ conflict("Retained refusal time differs from its native receipt");
233
+ }
234
+ if (!refusal) {
235
+ if (refusals.some(item => item.turn_id === turnId))
236
+ conflict("A cached native Turn cannot be a second refusal");
237
+ // A cached legacy failure cannot be assigned a new time during reattach.
238
+ if (ordinal > 1 && parsed.ordinal > 0 && !native.observed_at)
239
+ conflict("Retained continuation has no original failure time");
240
+ const observedAt = typeof native.observed_at === "string" && Number.isFinite(Date.parse(native.observed_at)) ? native.observed_at : new Date().toISOString();
241
+ refusal = refusalObservation(failure, ordinal - 1, observedAt);
242
+ input.retainFailure?.(operationId, refusal);
243
+ refusals.push(refusal);
244
+ }
245
+ const burst = overloadBurst(refusals);
246
+ if (burst)
247
+ 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 });
248
+ const nextId = `${rootId}:capacity:${ordinal}:session.prompt`;
249
+ const existing = history.find(item => item.operation_id === nextId);
250
+ if (existing && (existing.predecessor !== operationId || existing.turn_id !== turnId || existing.prompt_sha256 !== promptSha || existing.ordinal !== ordinal))
61
251
  throw error;
252
+ const wait = existing ? Math.max(0, Date.parse(existing.not_before) - Date.now()) : capacityDelay(failure, ordinal);
253
+ if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15 || input.deadline !== undefined && Date.now() + wait >= input.deadline)
254
+ throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
255
+ if (!existing) {
256
+ 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 };
257
+ input.retainIntent(intent);
258
+ history.push(intent);
259
+ }
260
+ // Node timers truncate large delays. Bounded slices preserve cancellation
261
+ // and owner checks; monotonic elapsed time does not renew a Retry-After.
262
+ const until = performance.now() + wait;
263
+ while (performance.now() < until) {
264
+ await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: input.signal });
265
+ const boundary = await decide();
266
+ if (boundary)
267
+ return boundary;
268
+ }
269
+ const afterWait = await decide();
270
+ if (afterWait)
271
+ return afterWait;
272
+ if (!await settled())
273
+ throw new AppError("capacity_continuation_blocked", "Native predecessor settlement changed during capacity wait", 1, { primary_error: failure });
274
+ const beforeDispatch = await decide();
275
+ if (beforeDispatch)
276
+ return beforeDispatch;
62
277
  try {
63
- return await input.prompt(nextId, 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.", ordinal);
278
+ return await input.prompt(nextId, text, ordinal);
64
279
  }
65
280
  catch (nextError) {
281
+ // A task can complete/pause while the daemon queues for its permit.
282
+ // Admission then refuses the stale prompt; accept that owner decision
283
+ // before treating the transport's rejection as another provider failure.
284
+ const finished = await decide();
285
+ if (finished)
286
+ return finished;
66
287
  error = nextError;
67
288
  operationId = nextId;
68
289
  }
69
290
  }
70
- throw error;
71
291
  }
@@ -138,6 +138,7 @@ export function controllerNativeChildren(receipt, rootSessionId) {
138
138
  const settled = rootChild && status === "unknown" && settledTree.has(id) ? "settled_by_root" : status;
139
139
  candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }),
140
140
  ...(text(value, ["terminal_turn_id"]) ? { terminal_turn_id: text(value, ["terminal_turn_id"]) } : {}),
141
+ ...(text(value, ["terminal_observed_at"]) ? { terminal_observed_at: text(value, ["terminal_observed_at"]) } : {}),
141
142
  ...(value.provider_error !== undefined ? { provider_error: value.provider_error } : {}),
142
143
  ...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) });
143
144
  };
@@ -409,7 +409,12 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
409
409
  let result, primary, failed = false;
410
410
  try {
411
411
  const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
412
+ const originalRunRoot = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id])?.run_root;
412
413
  rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
414
+ const publicationRun = restored.db.get("SELECT project_id, run_root FROM runs WHERE id = ?", [manifest.run_id]);
415
+ if (!originalRunRoot || !publicationRun?.run_root || !isInside(manifest.dd_flow_home, originalRunRoot) || !isInside(context.ddFlowHome, publicationRun.run_root))
416
+ throw new AppError("snapshot_invalid", "Restored publication roots are not bound to their runtime", 1);
417
+ restored.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.publication_relocation', 0, ?, ?)", [publicationRun.project_id, JSON.stringify({ run_id: manifest.run_id, from: originalRunRoot, to: publicationRun.run_root, source_manifest_sha256: sourceManifestSha256, import_id: importId }), restored.now()]);
413
418
  registerProject(restored, { root: projectRoot });
414
419
  requireProjectByRoot(restored, projectRoot);
415
420
  if (selectedProfile && routingInput && runtimeLineage) {
@@ -423,14 +428,6 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
423
428
  fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
424
429
  importFlowRunExecutionProfile(restored, { projectRoot, runId: manifest.run_id, stage: manifest.stage_entry, lineageFile: runtimeLineage, profile: selectedProfile });
425
430
  }
426
- const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
427
- if (!recovery)
428
- assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
429
- else
430
- restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
431
- if (!status.run.run_root)
432
- throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
433
- const runHome = status.run.run_root;
434
431
  const relocation = recovery ? {
435
432
  schema_id: "dd-flow/recovery-relocation@1", physical_runtime_id: crypto.randomUUID(), recovery_id: input.recoveryId, run_id: manifest.run_id,
436
433
  path_mapping: [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }]
@@ -440,6 +437,14 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
440
437
  fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
441
438
  writeJson(relocationFile, relocation);
442
439
  }
440
+ const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
441
+ if (!recovery)
442
+ assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
443
+ else
444
+ restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
445
+ if (!status.run.run_root)
446
+ throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
447
+ const runHome = status.run.run_root;
443
448
  const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
444
449
  if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
445
450
  throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
@@ -93,7 +93,7 @@ export async function launchExternalWork(context, input) {
93
93
  const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
94
94
  const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
95
95
  try {
96
- const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
96
+ const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
97
97
  await call(["daemon", "start", ...common]);
98
98
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
99
99
  const created = await call(["session", "create", ...common], runtimeHarness(resolved.worker) === "zcode-acp" ? 180_000 : undefined);
@@ -111,6 +111,8 @@ export async function launchExternalWork(context, input) {
111
111
  const promptFile = path.join(root, "launch.md");
112
112
  fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n\n${lifecycleRetryInstruction}${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
113
113
  const promptOperationId = `${operationId}:session.prompt`;
114
+ const promptDeadline = Date.now() + 45 * 60_000;
115
+ context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, prompt_deadline: promptDeadline }), context.now(), operationId]);
114
116
  let prompted;
115
117
  try {
116
118
  prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
@@ -118,37 +120,60 @@ export async function launchExternalWork(context, input) {
118
120
  catch (error) {
119
121
  if (runtimeHarness(resolved.worker) !== "codex-desktop")
120
122
  throw error;
121
- const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId,
123
+ const retained = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
124
+ const capacityIntents = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_intents ?? [] : [];
125
+ const capacityFailures = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_failures ?? [] : [];
126
+ const retainCapacity = () => {
127
+ const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, prompt_deadline: promptDeadline, capacity_intents: capacityIntents, capacity_failures: capacityFailures }), context.now(), operationId]);
128
+ if (changed.changes !== 1)
129
+ throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
130
+ };
131
+ const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId, allowSettledDescendants: true,
132
+ ...(input.signal ? { signal: input.signal } : {}), deadline: promptDeadline,
133
+ continuationText: `Continue only Work ${work.work_id} in RUN ${work.run_id}, Stage ${stage ?? "current"}, in the same Session. Inspect its current durable state and the retained Work packet. Never repeat Work start or a completed tool, accepted answer, check or lifecycle command. Use only the current exact issued successor and finish at the original Work boundary.`,
134
+ retainedIntents: () => capacityIntents,
135
+ retainedFailures: () => capacityFailures,
136
+ retainFailure: (_id, refusal) => { capacityFailures.push(refusal); retainCapacity(); },
122
137
  inspectOperation: id => call(["daemon", "operation", "--state-dir", root, "--operation-id", id, "--json"], 30_000, `${id}:capacity-observe`),
123
- inspectSession: () => call(["session", "inspect", "--session-id", sessionId, ...common], 30_000, `${operationId}:capacity-inspect`),
138
+ inspectSession: turnId => call(["session", "inspect", "--session-id", sessionId, ...(turnId ? ["--turn-id", turnId] : []), ...common], 30_000, `${operationId}:capacity-inspect`),
124
139
  assertCurrent: () => {
125
140
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
126
141
  const current = context.db.get("SELECT w.status, l.status AS launch_status FROM works w JOIN work_launches l ON l.work_id = w.work_id WHERE w.work_id = ? AND l.operation_id = ?", [work.work_id, operationId]);
127
- if (current?.status !== "created" || current.launch_status !== "launching")
142
+ if (!current || !["created", "running", "paused", "completed"].includes(current.status) || current.launch_status !== "launching")
128
143
  throw new AppError("capacity_continuation_blocked", "External Work changed before its native continuation", 1, { work_id: work.work_id, state: current });
129
144
  if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
130
145
  throw new AppError("read_only_input_changed", "Authoritative review workspace changed during capacity wait", 2);
131
146
  },
147
+ decision: () => {
148
+ const status = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id])?.status;
149
+ return status === "completed" ? "completed" : status === "paused" ? "paused" : "continue";
150
+ },
132
151
  retainIntent: intent => {
133
- const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, capacity_intent: intent }), context.now(), operationId]);
134
- if (changed.changes !== 1)
135
- throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
152
+ capacityIntents.push(intent);
153
+ retainCapacity();
136
154
  },
137
155
  prompt: (id, text, ordinal) => {
138
156
  const file = path.join(root, `capacity-${ordinal}.md`);
139
- fs.writeFileSync(file, `${text}\n`, { flag: "wx", mode: 0o600 });
140
- return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], 45 * 60_000, id);
157
+ if (fs.existsSync(file)) {
158
+ if (fs.readFileSync(file, "utf8") !== text)
159
+ throw new AppError("capacity_continuation_conflict", "Retained Work continuation prompt differs", 1);
160
+ }
161
+ else
162
+ fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
163
+ return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], Math.max(1, promptDeadline - Date.now()), id);
141
164
  } });
142
165
  if (!resumed)
143
166
  throw error;
144
167
  prompted = resumed;
145
168
  }
146
169
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
147
- context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created, prompt: prompted }), context.now(), operationId]);
170
+ const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
171
+ context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), create: created, prompt: prompted }), context.now(), operationId]);
148
172
  return { ok: true, work_id: work.work_id, operation_id: operationId, status: "start_requested", session: { harness_id: runtimeHarness(resolved.worker), session_id: sessionId }, profile_hash: resolved.profile_hash, journal_path: journal };
149
173
  }
150
174
  catch (error) {
151
- context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ error: errorRecord(error) }), context.now(), operationId]);
175
+ const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
176
+ context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), error: errorRecord(error) }), context.now(), operationId]);
152
177
  throw error;
153
178
  }
154
179
  }
@@ -2,7 +2,8 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { AppError } from "../shared/errors.js";
5
+ import { AppError, errorRecord } from "../shared/errors.js";
6
+ import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
6
7
  import { recoveryRpcTimeout } from "./recovery-observation-budget.js";
7
8
  const maxAdapterOutputBytes = 1_000_000;
8
9
  /** Native hooks need a real executable, not a PATH-dependent shell expression. */
@@ -64,18 +65,26 @@ export function runHarnessAdapter(input) {
64
65
  let outputTooLarge = false;
65
66
  let timedOut = false;
66
67
  let aborted = false;
67
- let killTimer;
68
+ let cleanup;
68
69
  let settled = false;
69
70
  let callbackError;
70
71
  let spawnError;
71
- const terminate = (signal) => { try {
72
- if (detached && child.pid)
73
- process.kill(-child.pid, signal);
74
- else
75
- child.kill(signal);
76
- }
77
- catch { /* The process already settled. */ } };
78
- const stopTransport = () => { terminate("SIGTERM"); killTimer ??= setTimeout(() => terminate("SIGKILL"), 5_000); };
72
+ const stopTransport = () => {
73
+ cleanup ??= stopProcessGroup(child, 5_000, { detached }).catch(error => {
74
+ if (timer)
75
+ clearTimeout(timer);
76
+ if (heartbeat)
77
+ clearInterval(heartbeat);
78
+ input.signal?.removeEventListener("abort", abort);
79
+ const details = { cleanup_error: errorRecord(error) };
80
+ if (callbackError instanceof Error) {
81
+ Object.assign(callbackError, { cleanup_error: details.cleanup_error, details: { ...callbackError.details, ...details } });
82
+ reject(callbackError);
83
+ }
84
+ else
85
+ reject(new AppError(outputTooLarge ? "harness_adapter_output_limit" : timedOut ? "harness_adapter_timeout" : aborted ? "harness_adapter_aborted" : "harness_adapter_cleanup_unconfirmed", "Adapter transport cleanup could not be confirmed", 1, details));
86
+ });
87
+ };
79
88
  const deliver = (callback) => {
80
89
  try {
81
90
  const result = callback();
@@ -109,7 +118,7 @@ export function runHarnessAdapter(input) {
109
118
  child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout = append(stdout, chunk); });
110
119
  child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr = append(stderr, chunk); });
111
120
  child.once("error", (error) => { spawnError = error; });
112
- child.once("close", (status, signal) => {
121
+ child.once("close", async (status, signal) => {
113
122
  if (settled)
114
123
  return;
115
124
  settled = true;
@@ -118,10 +127,7 @@ export function runHarnessAdapter(input) {
118
127
  input.signal?.removeEventListener("abort", abort);
119
128
  if (heartbeat)
120
129
  clearInterval(heartbeat);
121
- if (killTimer) {
122
- clearTimeout(killTimer);
123
- terminate("SIGKILL");
124
- }
130
+ await cleanup;
125
131
  const progressFailure = callbackError;
126
132
  deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
127
133
  if (spawnError)
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import net from "node:net";
6
6
  import { getResourceDatabase } from "../storage/database.js";
7
- import { AppError } from "../shared/errors.js";
7
+ import { AppError, errorRecord } from "../shared/errors.js";
8
8
  export function managedProcessBinding(record, home) {
9
9
  return { resource_home: home, process_id: record.id, registration_sha256: crypto.createHash("sha256").update(JSON.stringify([home, record.id, record.kind, record.owner_id, record.lease_token, record.pid, record.pid_started_at, record.project_id, record.run_id, record.work_id, record.check_id, record.operation_id, record.metadata_json])).digest("hex") };
10
10
  }
@@ -339,14 +339,19 @@ export async function stopManagedProcess(context, input) {
339
339
  return record;
340
340
  db.run("UPDATE managed_processes SET state = 'stopping', updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','orphaned')", [context.now(), record.id, record.lease_token]);
341
341
  if (processTreeIsAlive(record)) {
342
- terminateOwnedProcess(record, "SIGTERM");
342
+ const signalErrors = [];
343
+ const termError = terminateOwnedProcess(record, "SIGTERM");
344
+ if (termError)
345
+ signalErrors.push(errorRecord(termError));
343
346
  await delay(graceMs);
344
- if (input.force !== false && processTreeIsAlive(record)) {
345
- terminateOwnedProcess(record, "SIGKILL");
347
+ if (!termError && input.force !== false && processTreeIsAlive(record)) {
348
+ const killError = terminateOwnedProcess(record, "SIGKILL");
349
+ if (killError)
350
+ signalErrors.push(errorRecord(killError));
346
351
  await delay(250);
347
352
  }
348
353
  if (processTreeIsAlive(record))
349
- throw Object.assign(new Error("Owned process tree could not be safely stopped; resources retained"), { code: "process_tree_not_settled", details: { process_id: record.id } });
354
+ throw Object.assign(new Error("Owned process tree could not be safely stopped; resources retained"), { code: "process_tree_not_settled", details: { process_id: record.id, signal_errors: signalErrors } });
350
355
  }
351
356
  if (!finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: "stopped", reason: input.reason ?? "requested_stop" }))
352
357
  throw Object.assign(new Error("Managed process ownership changed before settlement"), { code: "process_lease_lost" });
@@ -460,7 +465,10 @@ function terminateOwnedProcess(record, signal) {
460
465
  try {
461
466
  process.kill(process.platform === "win32" || group !== record.pid ? record.pid : -group, signal);
462
467
  }
463
- catch { /* inspect again below */ }
468
+ catch (error) {
469
+ if (error.code !== "ESRCH")
470
+ return Object.assign(error, { details: { process_id: record.id, pid: record.pid, pgid: group ?? null, signal, phase: "owned_process_stop" } });
471
+ }
464
472
  }
465
473
  function parseMetadata(value) { try {
466
474
  const parsed = JSON.parse(value);