@deksden-com/dd-flow-cli 0.9.0-beta.138 → 0.9.0-beta.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/build-info.json +6 -6
  3. package/dist/cli/runtime-process.js +1 -1
  4. package/dist/cli.js +2 -0
  5. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +49 -20
  6. package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
  7. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +1 -1
  8. package/dist/harness-runtime/lib/dd-codex.mjs +2 -2
  9. package/dist/harness-runtime/lib/dd-droid.mjs +30 -8
  10. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +7 -1
  11. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +95 -12
  12. package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
  13. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +13 -1
  14. package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
  15. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  16. package/dist/harness-runtime/lib/delegation-instructions.mjs +11 -0
  17. package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
  18. package/dist/harness-runtime/lib/native-children.d.mts +2 -0
  19. package/dist/harness-runtime/lib/native-children.mjs +20 -1
  20. package/dist/harness-runtime/lib/opencode-plugin.mjs +30 -9
  21. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
  22. package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
  23. package/dist/schemas/flow-run.schema.json +4 -0
  24. package/dist/services/controller-child-capacity.js +13 -0
  25. package/dist/services/controller-child-observer.js +18 -2
  26. package/dist/services/controller-execution.js +6 -6
  27. package/dist/services/external-work-launch.js +2 -4
  28. package/dist/services/frozen-engine.js +16 -8
  29. package/dist/services/managed-processes.js +31 -10
  30. package/dist/services/memory-init-native-capacity.js +65 -10
  31. package/dist/services/memory-init-preflight.js +21 -3
  32. package/dist/services/memory-init-project-bootstrap.js +3 -3
  33. package/dist/services/memory-init-start.js +6 -0
  34. package/dist/services/memory-init-worktrunk-capability.js +9 -3
  35. package/dist/services/merge-server.js +2 -4
  36. package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
  37. package/dist/services/recovery-snapshot-database.js +1 -1
  38. package/dist/services/run-controller-adapter.js +1 -1
  39. package/dist/services/run-controller-recovery.js +2 -4
  40. package/dist/services/run-controller.js +59 -26
  41. package/dist/services/run-recovery-guard.js +3 -3
  42. package/dist/services/run-recovery-runtime.js +3 -3
  43. package/dist/services/runs.js +102 -13
  44. package/dist/services/vnext-code-review.js +1 -1
  45. package/dist/services/vnext-code.js +3 -3
  46. package/dist/services/vnext-fanout.js +3 -2
  47. package/dist/services/vnext-plan-review.js +1 -1
  48. package/dist/services/vnext-plan.js +3 -3
  49. package/dist/services/vnext-protocolize.js +20 -6
  50. package/dist/services/vnext-workspace-policy.js +7 -2
  51. package/package.json +1 -1
  52. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
  53. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
@@ -12,13 +12,15 @@ import { spawn } from "node:child_process";
12
12
  import net from "node:net";
13
13
  import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
14
14
  import { waitForSettlement } from "./session-settlement.mjs";
15
- import { errorRecord } from "./operation-errors.mjs";
15
+ import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
16
16
  import { checkObservedProfile, observeModel } from "./model-observations.mjs";
17
17
  import os from "node:os";
18
18
  import path from "node:path";
19
19
  import { parse as parseJsonc } from "jsonc-parser";
20
20
  import { writeJsonAtomic } from "./runner-events.mjs";
21
21
  import { OpenCodeClient, OpenCodeError, historyDigest, usageSnapshot, nativeModel } from "./dd-opencode.mjs";
22
+ import { safeProviderError } from "./provider-overload-policy.mjs";
23
+ import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
22
24
  import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
23
25
 
24
26
  const REQUEST_SCHEMA = "dd-opencode/daemon-request@1", RESPONSE_SCHEMA = "dd-opencode/daemon-response@1", STATE_SCHEMA = "dd-opencode/daemon-state@2", ARCHIVE_SCHEMA = "dd-opencode/session-archive@2";
@@ -65,7 +67,7 @@ const errorPayload = errorRecord;
65
67
 
66
68
  async function writePlugin(paths, config) {
67
69
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
68
- const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome })});\n`;
70
+ const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ stateDir: paths.dir, daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome })});\n`;
69
71
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
70
72
  }
71
73
  async function prepare(paths, config, authSource) {
@@ -189,13 +191,15 @@ export class Runtime {
189
191
  }
190
192
  async describe(id) {
191
193
  const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]);
192
- if (!info.directory || path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace");
193
194
  const parent = info.fork ? null : info.parentID ?? null, existing = this.sessions.get(id);
195
+ if (info.id !== id || !existing && (!parent || !this.sessions.has(parent) || info.fork)
196
+ || existing && (existing.parent_provider_session_id ?? null) !== parent) throw new DaemonError("foreign_session", "OpenCode Session is not in the retained owned tree");
194
197
  const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent,
195
198
  root_provider_session_id: parent ? this.sessions.get(parent)?.root_provider_session_id ?? parent : existing?.root_provider_session_id ?? id, cwd: info.directory };
196
199
  this.sessions.set(id, record);
197
200
  for (const child of children) {
198
- if (!child.directory || path.resolve(child.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode child has another workspace");
201
+ const retained = this.sessions.get(child.id);
202
+ if (child.fork || retained && retained.parent_provider_session_id !== child.parentID) throw new DaemonError("session_identity_mismatch", "OpenCode child ancestry changed");
199
203
  this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID, root_provider_session_id: record.root_provider_session_id, cwd: child.directory });
200
204
  }
201
205
  const pending = new Map();
@@ -204,6 +208,18 @@ export class Runtime {
204
208
  pending.set(session, { ...value, shells: this.client.shells ? (await this.client.shells(session)).filter(shell => shell.status === "running") : [] });
205
209
  }
206
210
  const busy = session => Boolean(statuses[session] && statuses[session].type !== "idle") || Object.values(pending.get(session)).some(items => items.length > 0);
211
+ for (const child of children) {
212
+ const history = await this.client.messages(child.id);
213
+ const index = history.findLastIndex(message => message.info.role === "user" && message.info.sessionID === child.id);
214
+ const terminal = index >= 0 && history.slice(index + 1).findLast(message => message.info.role === "idle" && message.info.sessionID === child.id);
215
+ if (!terminal || busy(child.id)) continue;
216
+ const outcome = terminal.info.outcome;
217
+ if (!["succeeded", "failed", "interrupted"].includes(outcome)) continue;
218
+ Object.assign(child, { status: outcome === "succeeded" ? "completed" : outcome === "failed" ? "failed" : "cancelled",
219
+ terminal_turn_id: history[index].info.id, native_source: "opencode.v2.session.execution",
220
+ native_turn_items: { observed: true, possible_effects: true, pending: false },
221
+ ...(terminal.info.error ? { provider_error: safeProviderError(terminal.info.error) } : {}) });
222
+ }
207
223
  const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = busy(id) ? "busy" : "idle";
208
224
  const lastAssistant = messages.findLast(message => message.info?.role === "assistant");
209
225
  const inputIndex = messages.findLastIndex(message => message.info.role === "user");
@@ -214,6 +230,8 @@ export class Runtime {
214
230
  if (this.state.config.journal) journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools);
215
231
  return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? info.model?.providerID ?? null, model: lastAssistant?.info?.modelID ?? info.model?.id ?? null, reasoning: lastAssistant?.info?.variant ?? info.model?.variant ?? "default", mode: info.agent ?? null },
216
232
  assistant_text: (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""), info, status,
233
+ route_conformance: { matches: [info, ...children].every(session => session.directory && path.resolve(session.directory) === path.resolve(this.state.config.cwd)), expected: this.state.config.cwd, actual: info.directory ?? null, actual_provenance: "native_session_readback" },
234
+ route_violations: [info, ...children].filter(session => !session.directory || path.resolve(session.directory) !== path.resolve(this.state.config.cwd)).map(session => ({ provider_session_id: session.id, expected: this.state.config.cwd, actual: session.directory ?? null, actual_provenance: "native_session_readback" })),
217
235
  settled: status === "idle" && !unsettledChildren.length, unsettled_children: unsettledChildren,
218
236
  tree_session_ids: treeIds, unsettled_sessions: treeIds.filter(busy),
219
237
  tree_observations: treeIds.map(session => ({ provider_session_id: session, status: busy(session) ? "running" : "idle", settled: !busy(session) })),
@@ -237,6 +255,7 @@ export class Runtime {
237
255
  }
238
256
  async requireSettled(id) {
239
257
  const value = await this.describe(id);
258
+ if (!value.route_conformance.matches) throw new DaemonError("invocation_directory_mismatch", "Owned OpenCode Session has an unexpected workspace", false, { effect: "no_effect", ...value.route_violations[0], native_outcome: value.terminal_status, settled: value.settled });
240
259
  if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status });
241
260
  const config = this.state.config;
242
261
  if (config.provider && config.model) checkObservedProfile({ ...config, reasoning: config.variant, mode: value.parent_provider_session_id ? "dd-flow-worker" : config.agent }, value.observed_profile);
@@ -256,30 +275,87 @@ export class Runtime {
256
275
  } catch (cause) {
257
276
  const error = completed ? new DaemonError("operation_observation_lost", "OpenCode returned but its durable observation could not be saved; do not resend", false, { result, cause: errorPayload(cause) }) : cause;
258
277
  const failures = [];
259
- try { await this.persist({ active_tree: error.details?.effect === "no_effect" ? priorActive : true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
278
+ try { await this.persist({ active_tree: error.details?.effect === "no_effect" && error.details?.native_turn_items?.possible_effects !== true ? priorActive : true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
260
279
  try { await this.journal(`${name}.failed`, null); } catch (secondary) { failures.push(errorPayload(secondary)); }
261
280
  if (failures.length) error.details = { ...error.details, observation_errors: failures };
262
281
  throw error;
263
282
  } finally { this.active = null; }
264
283
  }
265
284
  async cancelTree(id, seen = new Set()) {
285
+ if (!this.sessions.has(id)) throw new DaemonError("foreign_session", "cannot cancel a Session without daemon ownership");
266
286
  if (seen.has(id)) return [];
287
+ const native = await this.client.inspect(id), retained = this.sessions.get(id);
288
+ if (native.id !== id || (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("session_identity_mismatch", "OpenCode cancellation Session ancestry changed");
267
289
  seen.add(id);
268
290
  const errors = []; let children = [];
269
291
  try { children = await this.client.children(id); } catch (error) { errors.push({ session_id: id, phase: "children", ...errorPayload(error) }); }
270
292
  for (const child of children) {
271
- if (child.parentID !== id || !child.directory || path.resolve(child.directory) !== path.resolve(this.state.config.cwd)) {
293
+ if (child.parentID !== id || child.fork || this.sessions.has(child.id) && this.sessions.get(child.id).parent_provider_session_id !== id) {
272
294
  errors.push({ session_id: child.id, phase: "identity", code: "session_identity_mismatch" }); continue;
273
295
  }
274
- errors.push(...await this.cancelTree(child.id, seen));
296
+ this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: id, root_provider_session_id: this.sessions.get(id)?.root_provider_session_id ?? id, cwd: child.directory });
297
+ try { errors.push(...await this.cancelTree(child.id, seen)); }
298
+ catch (error) { errors.push({ session_id: child.id, phase: "cancel_child", ...errorPayload(error) }); }
275
299
  }
276
300
  try { await this.client.abort(id); } catch (error) { errors.push({ session_id: id, phase: "abort", ...errorPayload(error) }); }
277
301
  return errors;
278
302
  }
303
+ async observeHookFailures(current = this.nativeHookOperation) {
304
+ if (!current || this.nativeHookOperation !== current) return;
305
+ if (current.reading) return await current.reading;
306
+ current.reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.retainHookFailure(current, error));
307
+ try { await current.reading; } finally { current.reading = null; }
308
+ }
309
+ retainHookFailure(current, error) {
310
+ if (this.nativeHookOperation !== current || isProvenShellCorrection(error)) return;
311
+ error.details = { ...error.details, provider_session_id: current.sessionId, native_outcome: "unknown", native_turn_items: { observed: true, possible_effects: true, pending: true } };
312
+ current.failure ??= error;
313
+ }
279
314
  async dispatch(operation, params, operationId = null) {
280
315
  const assertDispatch = captureDispatchGuard(this);
281
316
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
282
317
  if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
318
+ if (operation === "hook.resolve") {
319
+ const current = this.nativeHookOperation, sessionId = params.sessionId, payload = params.payload;
320
+ if (!current || !sessionId || typeof payload?.input?.command !== "string" || typeof payload.tool_call_id !== "string" || !payload.tool_call_id || typeof payload.message_id !== "string" || !payload.message_id || !["before", "after"].includes(payload.phase))
321
+ throw new DaemonError("native_hook_unproven", "OpenCode hook has no exact active operation");
322
+ let owned = sessionId === current.sessionId || this.sessions.get(sessionId)?.root_provider_session_id === current.sessionId;
323
+ try {
324
+ const tree = await this.childTree(current.sessionId);
325
+ if (sessionId !== current.sessionId && !tree.some(child => child.id === sessionId && !child.fork)) throw new DaemonError("foreign_session", "OpenCode hook Session is outside the owned native tree");
326
+ owned = true;
327
+ const native = await this.client.inspect(sessionId), retained = this.sessions.get(sessionId), child = tree.find(item => item.id === sessionId);
328
+ if (native.id !== sessionId || child && (native.parentID !== child.parentID || native.fork) || retained && (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("native_hook_identity_conflict", "OpenCode hook Session ancestry changed");
329
+ if (!native.directory) throw new DaemonError("native_hook_unproven", "OpenCode hook Session directory is unavailable");
330
+ if (payload.input.workdir !== undefined && typeof payload.input.workdir !== "string") throw new DaemonError("native_hook_unproven", "OpenCode hook workdir must be a string");
331
+ if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook identity observation");
332
+ const accepted = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
333
+ provider_session_id: sessionId, root_provider_session_id: current.sessionId, tool_call_id: `${payload.message_id}:${payload.tool_call_id}`, event: payload.phase === "after" ? "PostToolUse" : "PreToolUse",
334
+ command_sha256: createHash("sha256").update(payload.input.command).digest("hex"), native_cwd: path.resolve(native.directory, payload.input.workdir ?? ".") }, current);
335
+ if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook acceptance");
336
+ current.requests.set(accepted.request_id, { current, binding: accepted }); return { nativeHookBinding: accepted, nativeSession: {
337
+ id: native.id, directory: native.directory, parentID: native.parentID, projectID: native.projectID,
338
+ model: { providerID: native.model?.providerID, id: native.model?.id, variant: native.model?.variant }
339
+ } };
340
+ } catch (error) {
341
+ if (owned) this.retainHookFailure(current, error);
342
+ throw error;
343
+ }
344
+ }
345
+ if (operation === "hook.verify") {
346
+ const current = this.nativeHookOperation, binding = params.binding;
347
+ if (!current || binding?.operation_id !== current.operationId || binding?.turn_generation !== current.generation
348
+ || JSON.stringify(current.requests.get(binding?.request_id)?.binding) !== JSON.stringify(binding)) throw new DaemonError("native_hook_unproven", "OpenCode hook generation is no longer current");
349
+ return { current: true };
350
+ }
351
+ if (operation === "hook.failed") {
352
+ if (params.failure) {
353
+ await this.dispatch("hook.verify", { binding: params.binding });
354
+ if (JSON.stringify(params.failure.details?.native_hook_binding) !== JSON.stringify(params.binding) || !params.failure.details?.receipt_error || typeof params.failure.code !== "string" || typeof params.failure.message !== "string") throw new DaemonError("native_hook_receipt_invalid", "OpenCode fallback hook error is not bound");
355
+ this.retainHookFailure(this.nativeHookOperation, reportedError(params.failure, "Native hook failed"));
356
+ }
357
+ await this.observeHookFailures(); return { observed: true };
358
+ }
283
359
  if (operation === "daemon.doctor") {
284
360
  const info = await this.client.health();
285
361
  await this.client.call("model.list", { location: { directory: this.state.config.cwd } });
@@ -311,26 +387,33 @@ export class Runtime {
311
387
  if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
312
388
  if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
313
389
  this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
314
- assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
390
+ const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null };
391
+ this.state.hook_generation = current.generation; this.nativeHookOperation = current;
392
+ const checkHookFailures = async () => { await this.observeHookFailures(current); if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode prompt generation changed"); if (current.failure) throw current.failure; };
393
+ let reply;
394
+ try { assertDispatch(); reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
315
395
  save: async receipt => { this.state.operations[key] = receipt; await this.persist(); },
316
- assertDispatch, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
396
+ assertDispatch, checkHookFailures, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
317
397
  onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
318
398
  onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
319
399
  onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
320
- }); const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
400
+ }); await checkHookFailures(); }
401
+ finally { current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; }
402
+ const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
321
403
  const expectedAgent = this.sessions.get(params.sessionId)?.parent_provider_session_id ? "dd-flow-worker" : this.state.config.agent;
322
404
  const reply_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: reply.info.mode ?? null };
323
405
  checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, reply_profile);
324
406
  let result;
325
407
  try { result = await this.describe(params.sessionId); }
326
- catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
408
+ catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, terminal_turn_id: reply.admission_message_id, terminal_status: "completed", native_outcome: "succeeded", observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
327
409
  result.assistant_text = assistant_text;
410
+ if (!result.route_conformance.matches) throw new DaemonError("invocation_directory_mismatch", "Owned OpenCode Session moved during execution", false, { ...result.route_violations[0], native_outcome: result.terminal_status, settled: result.settled, result });
328
411
  result.response_message_id = reply.info.id;
329
412
  result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
330
413
  checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
331
414
  if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
332
415
  const info = await this.client.fork(params.sessionId, params.messageId);
333
- const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? params.sessionId, root_provider_session_id: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
416
+ const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.fork ? null : info.parentID ?? params.sessionId, root_provider_session_id: info.fork ? info.id : this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
334
417
  this.sessions.set(info.id, created);
335
418
  try {
336
419
  const fork = await this.describe(info.id);
@@ -4,6 +4,7 @@ import { ToolObservations, openCodeToolObservation } from "./tool-observations.m
4
4
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
5
5
  import { assertNativeLine } from "./native-streams.mjs";
6
6
  import { operationProgressReporter } from "./operation-progress.mjs";
7
+ import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
7
8
 
8
9
  export const OPENCODE_PROTOCOL = 2;
9
10
  export class OpenCodeError extends Error {
@@ -27,17 +28,16 @@ export function normalizeMessage(message, sessionID) {
27
28
  }
28
29
 
29
30
  export function providerFailure(native, sessionId, turnId) {
30
- const message = String(native?.message ?? "OpenCode execution failed"), status = native?.status, type = String(native?.type ?? "unknown");
31
- const quota = status === 402 || /quota|balance exhausted|insufficient.?credit|billing|payment required/i.test(message);
32
- const code = quota ? "provider_quota_exhausted" : status === 401 || status === 403 ? "provider_auth_failed"
33
- : /overload|server.?busy/i.test(`${type} ${message}`) || status === 503 ? "provider_capacity_transient"
34
- : /rate.?limit|too many requests/i.test(message) ? "provider_rate_limited"
35
- : status === 429 ? "provider_limit_unknown" : "opencode_provider_failed";
31
+ const status = native?.status, type = String(native?.type ?? "unknown");
32
+ const category = classifyProviderError(native), code = category === "provider_failed" ? "opencode_provider_failed" : category;
36
33
  // Never forward response.body or provider credentials into controller/Judge.
37
34
  return new OpenCodeError(code, `OpenCode native execution failed (${type}${status ? `, HTTP ${status}` : ""})`,
38
35
  ["provider_capacity_transient", "provider_rate_limited"].includes(code), {
39
36
  provider_session_id: sessionId, session_id: sessionId, turn_id: turnId, native_error_type: type,
40
37
  native_turn_id: turnId, terminal_status: "failed",
38
+ provider_error: safeProviderError(native),
39
+ will_retry: native?.willRetry === true, native_retry_active: native?.willRetry === true,
40
+ native_turn_items: { observed: true, possible_effects: true, pending: false },
41
41
  ...(Number.isInteger(status) ? { http_status: status } : {}), observed_at: new Date().toISOString(), native_source: "opencode.v2.session.execution" });
42
42
  }
43
43
 
@@ -48,11 +48,16 @@ export class OpenCodeClient {
48
48
  baseUrl: this.baseUrl.href, headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }
49
49
  }));
50
50
  }
51
- async call(method, input, { timeoutMs, progress, onDispatched, productive = false } = {}) {
51
+ async call(method, input, { timeoutMs, progress, onDispatched, productive = false, signal } = {}) {
52
52
  const observation = this.readObservation?.();
53
53
  timeoutMs ??= observation?.timeoutMs ?? this.timeoutMs; progress ??= observation?.progress;
54
54
  const api = await this.ready, controller = new AbortController();
55
- const timer = observedTimeout(() => controller.abort(), timeoutMs, { progress });
55
+ let timer;
56
+ const abort = () => { controller.abort(); clearInterval(timer); };
57
+ signal?.addEventListener("abort", abort, { once: true });
58
+ if (signal?.aborted) abort();
59
+ timer = observedTimeout(() => controller.abort(), timeoutMs, { progress });
60
+ if (signal?.aborted) clearInterval(timer);
56
61
  try {
57
62
  const keys = method.split("."), name = keys.pop(), domain = keys.reduce((value, key) => value?.[key], api);
58
63
  if (typeof domain?.[name] !== "function") throw new OpenCodeError("opencode_capability_missing", `OpenCode v2 does not expose ${method}`);
@@ -68,7 +73,7 @@ export class OpenCodeClient {
68
73
  if (productive || controller.signal.aborted) throw Object.assign(lost(`OpenCode observer lost during ${method}`, { method }), { cause });
69
74
  throw Object.assign(new OpenCodeError("opencode_unavailable", `OpenCode ${method} observation failed`, true,
70
75
  { method, native_type: cause._tag ?? cause.name ?? null, native_reason: cause.reason ?? null }), { cause });
71
- } finally { clearInterval(timer); }
76
+ } finally { clearInterval(timer); signal?.removeEventListener("abort", abort); }
72
77
  }
73
78
  async health() {
74
79
  const info = await this.call("server.info", undefined, { timeoutMs: 5_000 });
@@ -95,7 +100,8 @@ export class OpenCodeClient {
95
100
  async children(id) { return (await this.pages("session.list", { parentID: id })).map(info => ({ ...info, directory: info.location?.directory })); }
96
101
  async messages(id) { return (await this.pages("message.list", { sessionID: id })).map(message => normalizeMessage(message, id)); }
97
102
  inbox(id) { return this.call("session.inbox.list", { sessionID: id }); }
98
- async shells(id) { const result = await this.call("shell.list", { location: { directory: this.directory } }); return result.data.filter(item => item.metadata?.sessionID === id); }
103
+ async shellDirectory(id) { const api = await this.ready; return typeof api.session?.get === "function" ? (await this.inspect(id)).directory : this.directory; }
104
+ async shells(id) { const directory = await this.shellDirectory(id); const result = await this.call("shell.list", { location: { directory } }); return result.data.filter(item => item.metadata?.sessionID === id).map(item => ({ ...item, location: item.location ?? { directory } })); }
99
105
  async pending(id) {
100
106
  const [inbox, forms, permissions] = await Promise.all([this.inbox(id), this.call("session.form.list", { sessionID: id }), this.call("permission.list", { sessionID: id })]);
101
107
  const value = { inbox, forms: forms.data ?? forms, permissions: permissions.data ?? permissions };
@@ -117,7 +123,7 @@ export class OpenCodeClient {
117
123
  const result = await this.call("session.interrupt", { sessionID: id, resume: false }, { productive: true });
118
124
  const api = await this.ready;
119
125
  if (typeof api.shell?.list === "function") for (const shell of await this.shells(id)) {
120
- if (shell.status === "running") await this.call("shell.remove", { id: shell.id, location: { directory: this.directory } }, { productive: true });
126
+ if (shell.status === "running") await this.call("shell.remove", { id: shell.id, location: shell.location }, { productive: true });
121
127
  }
122
128
  return result;
123
129
  }
@@ -127,7 +133,7 @@ export class OpenCodeClient {
127
133
  import(data) { return this.call("session.import", data, { productive: true }); }
128
134
 
129
135
  async prompt(id, { prompt, messageId = `msg_${randomUUID().replaceAll("-", "")}`, retained,
130
- save, onModelMessages, onObservationError, assertDispatch, onDispatched, livenessTimeoutMs = this.timeoutMs }) {
136
+ save, onModelMessages, onObservationError, assertDispatch, onDispatched, checkHookFailures, livenessTimeoutMs = this.timeoutMs }) {
131
137
  validateObservationDuration(livenessTimeoutMs);
132
138
  const request = { sessionID: id, id: messageId, text: prompt, delivery: "queue", resume: true }, digest = requestDigest(request);
133
139
  if (retained && (retained.request_sha256 !== digest || retained.message_id !== messageId)) throw new OpenCodeError("operation_request_mismatch", "OpenCode operation already binds another prompt", false, { effect: "no_effect" });
@@ -144,6 +150,21 @@ export class OpenCodeClient {
144
150
  const publish = operationProgressReporter(), known = new Set([id, ...baseline.keys()]), signatures = new Map(), shellCursors = new Map(), liveSeen = new Set();
145
151
  let progress = 0, marker, stream;
146
152
  const liveParts = new Map(), liveShells = new Map(), backgroundShells = new Set();
153
+ const completedTools = new Set(), outputReads = new Map(), shellLocations = new Map();
154
+ const observationErrors = { count: 0 };
155
+ const observeOutput = (shellID, location) => {
156
+ if (outputReads.has(shellID) || !location?.directory) return;
157
+ const cursor = shellCursors.get(shellID) ?? 0;
158
+ const pending = this.call("shell.output", { id: shellID, location, cursor, limit: 65536 }, { signal: streamAbort.signal })
159
+ .then(async output => { const data = output.data ?? output; if (!streamAbort.signal.aborted && data.cursor > cursor) { shellCursors.set(shellID, data.cursor); await advance(); } })
160
+ .catch(async error => {
161
+ if (streamAbort.signal.aborted) return;
162
+ const diagnostic = { method: "shell.output", shell_id: shellID, native_type: error.details?.native_type ?? error.cause?._tag ?? null, http_status: error.cause?.status ?? null };
163
+ observationErrors.count++; observationErrors.first ??= diagnostic; observationErrors.last = diagnostic;
164
+ if (observationErrors.count === 1) await onObservationError?.(error);
165
+ }).finally(() => outputReads.delete(shellID));
166
+ outputReads.set(shellID, pending); void pending.catch(() => {});
167
+ };
147
168
  const advance = async (observedAt = Date.now()) => { marker = { cursor: ++progress, observed_at: observedAt }; await publish("native_progress", { session_id: id, observed_at: new Date(marker.observed_at).toISOString() }); };
148
169
  let connected;
149
170
  const connection = new Promise(resolve => { connected = resolve; });
@@ -157,7 +178,8 @@ export class OpenCodeClient {
157
178
  && ["session.text.delta", "session.reasoning.delta", "session.tool.input.delta", "session.tool.progress"].includes(event.type)) {
158
179
  liveSeen.add(event.id); if (liveSeen.size > 1024) liveSeen.delete(liveSeen.values().next().value);
159
180
  const key = `${data.sessionID}:${data.assistantMessageID}:${data.id ?? ""}`, signature = requestDigest(data.metadata ?? null);
160
- if (event.type === "session.tool.progress" && typeof data.metadata?.shellID === "string") liveShells.set(key, data.metadata.shellID);
181
+ if (event.type === "session.tool.progress" && completedTools.has(key)) continue;
182
+ if (event.type === "session.tool.progress" && typeof data.metadata?.shellID === "string" && !completedTools.has(key)) liveShells.set(key, data.metadata.shellID);
161
183
  if (typeof data.delta === "string" && data.delta.length || event.type === "session.tool.progress" && liveParts.get(key) !== signature) {
162
184
  liveParts.set(key, signature); await advance(event.created);
163
185
  }
@@ -189,6 +211,7 @@ export class OpenCodeClient {
189
211
  }
190
212
  for (;;) {
191
213
  try {
214
+ await checkHookFailures?.();
192
215
  const events = await this.logs(id, state.cursor);
193
216
  let delivered = state.delivered === true, terminal = state.terminal;
194
217
  for (const event of events) {
@@ -198,6 +221,7 @@ export class OpenCodeClient {
198
221
  if (delivered && ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"].includes(event.type)) terminal = { type: event.type, error: event.data.error };
199
222
  state = { ...state, cursor: event.durable.seq, delivered, terminal }; await save?.(state);
200
223
  }
224
+ let runningTools = false;
201
225
  const observe = async (session, visited = new Set()) => {
202
226
  if (visited.has(session)) throw lost("OpenCode topology contains a cycle"); visited.add(session); known.add(session);
203
227
  const messages = await this.messages(session);
@@ -211,19 +235,24 @@ export class OpenCodeClient {
211
235
  signatures.set(key, { signature: next, seen, highWater: Math.max(before?.highWater ?? 0, encoded.length) });
212
236
  }
213
237
  for (const part of item.parts) {
214
- let shellID = part.type === "tool" ? part.state?.metadata?.shellID ?? liveShells.get(`${session}:${item.info.id}:${part.id}`) : null;
238
+ const toolKey = `${session}:${item.info.id}:${part.id}`;
239
+ if (part.type === "tool" && ["running", "streaming", "pending"].includes(part.state?.status)) runningTools = true;
240
+ if (part.type === "tool" && !["running", "streaming", "pending"].includes(part.state?.status)) { completedTools.add(toolKey); liveShells.delete(toolKey); }
241
+ let shellID = part.type === "tool" ? part.state?.metadata?.shellID ?? liveShells.get(toolKey) : null;
215
242
  if (!shellID && part.tool === "shell" && part.state?.status === "running") {
216
243
  // Tool progress metadata is volatile in the CLI. Reconcile
217
244
  // only an unambiguous native shell in this current message.
218
245
  const candidates = (await this.shells(session)).filter(shell => shell.time.started >= item.info.created);
219
- if (candidates.length === 1) shellID = candidates[0].id;
246
+ if (candidates.length === 1) { shellID = candidates[0].id; shellLocations.set(shellID, candidates[0].location); }
220
247
  }
221
248
  if (!shellID) continue;
222
249
  const cursor = shellCursors.get(shellID) ?? 0;
223
250
  shellCursors.set(shellID, cursor); // Zero-output shells still own their native notification.
224
251
  if (part.tool === "shell" && part.state?.metadata?.status === "running") backgroundShells.add(shellID);
225
- const output = await this.call("shell.output", { id: shellID, location: { directory: this.directory }, cursor, limit: 65536 }), data = output.data ?? output;
226
- if (data.cursor > cursor) { shellCursors.set(shellID, data.cursor); await advance(); }
252
+ if (!completedTools.has(toolKey)) {
253
+ if (!shellLocations.has(shellID)) shellLocations.set(shellID, (await this.shells(session)).find(shell => shell.id === shellID)?.location);
254
+ observeOutput(shellID, shellLocations.get(shellID));
255
+ }
227
256
  }
228
257
  }
229
258
  for (const child of await this.children(session)) {
@@ -258,7 +287,7 @@ export class OpenCodeClient {
258
287
  state = { ...state, delivered: true, terminal: { type: `session.execution.${terminal.info.outcome}`, error: terminal.info.error } }; await save?.(state);
259
288
  }
260
289
  }
261
- const statuses = await this.statuses(); let unsettled = false;
290
+ const statuses = await this.statuses(); let unsettled = runningTools;
262
291
  // A background child's native completion notification is delivered
263
292
  // asynchronously. An idle parent before that delivery is not the
264
293
  // completion of the coordinating operation.
@@ -278,18 +307,20 @@ export class OpenCodeClient {
278
307
  if (typeof api.shell?.list === "function") unsettled ||= (await this.shells(session)).some(shell => shell.status === "running");
279
308
  }
280
309
  if (state.delivered && state.terminal && !unsettled) {
310
+ await checkHookFailures?.();
281
311
  if (state.terminal.type === "session.execution.failed") throw providerFailure(state.terminal.error, id, messageId);
282
312
  if (state.terminal.type === "session.execution.interrupted") throw new OpenCodeError("opencode_execution_interrupted", "OpenCode execution was interrupted", false, { provider_session_id: id, turn_id: messageId });
283
313
  const final = current.findLast(item => item.info.completed !== undefined && item.info.finish && item.info.finish !== "tool-calls");
284
314
  if (!final) throw lost("OpenCode terminal has no current assistant response", { session_id: id, message_id: messageId });
285
315
  if (final.info.error) throw providerFailure(final.info.error, id, messageId);
286
- return { ...final, admission_message_id: messageId, log_cursor: state.cursor };
316
+ return { ...final, admission_message_id: messageId, log_cursor: state.cursor, ...(observationErrors.count ? { observation_errors: observationErrors } : {}) };
287
317
  }
288
318
  } catch (error) {
319
+ await checkHookFailures?.();
289
320
  if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)) throw error;
290
321
  await onObservationError?.(error);
291
322
  }
292
- if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state() });
323
+ if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state(), ...(observationErrors.count ? { observation_errors: observationErrors } : {}) });
293
324
  await delay(Math.max(5, Math.min(500, livenessTimeoutMs / 4)));
294
325
  }
295
326
  } finally { clearInterval(connectionTimer); this.readObservation = priorObservation; streamAbort.abort(); await stream; }
@@ -30,6 +30,7 @@ import {
30
30
  } from "./dd-zcode.mjs";
31
31
 
32
32
  const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
33
+ import { settledNativeTree } from "./native-children.mjs";
33
34
  const RESPONSE_SCHEMA = "dd-zcode/daemon-response@1";
34
35
  const STATE_SCHEMA = "dd-zcode/daemon-state@1";
35
36
  // ACP silence starts diagnostics; it is not a provider-failure deadline.
@@ -374,6 +375,7 @@ export class DaemonRuntime {
374
375
  current.reject = error => { if (this.nativeHookOperation !== current || current.error) return; current.error = error; this.bridge.lifecycleFailure ??= error; this.bridge.notificationError = this.bridge.lifecycleFailure; this.bridge.rejectAll(this.bridge.lifecycleFailure); rejectHook(this.bridge.lifecycleFailure); };
375
376
  const timer = setInterval(() => { void this.observeHookFailures(current).catch(error => current.reject(error)); }, 100);
376
377
  try {
378
+ if (name === "session.prompt") this.state.last_native_failure = null;
377
379
  await this.persist({ active_tree: true, active_operation: name, ...(request ? { active_request: request } : {}) });
378
380
  const result = await Promise.race([task(), hookFailed]);
379
381
  this.track(result);
@@ -386,6 +388,9 @@ export class DaemonRuntime {
386
388
  // after a late/partial create: the native Session existed, but the
387
389
  // daemon state no longer carried the only correlation to it.
388
390
  const active = this.state?.active_request;
391
+ const response = error?.details?.terminal_response;
392
+ if (name === "session.prompt" && response?.method === "session/prompt" && error.details.provider_session_id)
393
+ this.state.last_native_failure = { provider_session_id: error.details.provider_session_id, request_id: response.request_id };
389
394
  try {
390
395
  await hookBudget(1_000, null).run(() => this.persist({
391
396
  active_operation: null,
@@ -572,7 +577,14 @@ export class DaemonRuntime {
572
577
  if (this.sessions.has(result.provider_session_id)) this.track(result, result.subagents);
573
578
  else if (result.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
574
579
  await this.persist({ active_tree: this.treeRunning(result) || Boolean(this.activeProductive) });
575
- return { ...result, settled: !this.treeRunning(result) && !this.activeProductive };
580
+ const settled = !this.treeRunning(result) && !this.activeProductive;
581
+ const failure = this.state.last_native_failure;
582
+ const nativeChildren = (result.evidence ?? result).subagents;
583
+ const childIds = [...new Set([...(this.sessions.get(result.provider_session_id)?.known_child_session_ids ?? []), ...zcodeChildSessionIds(nativeChildren)])];
584
+ const childStates = new Map([...(nativeChildren?.ended?.items ?? nativeChildren?.completed ?? []), ...(nativeChildren?.running ?? []).map(child => ({ ...child, status: "running" }))].map(child => [child.childSessionId ?? child.sessionId, child.status]));
585
+ const rootSettled = !this.activeProductive && failure?.provider_session_id === result.provider_session_id && (result.evidence ?? result).read?.projection?.status === "idle";
586
+ return { ...result, settled, ...settledNativeTree(result.provider_session_id, childIds.map(childSessionId => ({ childSessionId, status: childStates.get(childSessionId) ?? "unknown" })), settled, rootSettled),
587
+ ...(failure?.provider_session_id === result.provider_session_id ? { terminal_turn_id: String(failure.request_id) } : {}) };
576
588
  }
577
589
  if (operation === "session.cancel") {
578
590
  if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon");
@@ -15,6 +15,7 @@ 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
17
  import { safeNativeFailure } from "./native-children.mjs";
18
+ import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
18
19
 
19
20
  import { readZcodeUsage } from "./zcode-usage.mjs";
20
21
  const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
@@ -218,9 +219,12 @@ export class AcpBridge {
218
219
  // ZCode emits this as `session/update`; Grok Build wraps the same ACP
219
220
  // payload in `_x.ai/session_notification`. Preserve either provider's
220
221
  // native error before its generic JSON-RPC terminal response arrives.
221
- if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state" && update.type === "failed") {
222
+ if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state") {
222
223
  for (const pending of this.pending.values()) {
223
- if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId && !pending.providerError) pending.providerError = { ...update, observed_at: now(), provider_session_id: pending.sessionId, source: message.method };
224
+ if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId) {
225
+ pending.nativeRetryActive = update.type !== "failed";
226
+ if (update.type === "failed") pending.providerError = { ...update, observed_at: now(), provider_session_id: pending.sessionId, source: message.method };
227
+ }
224
228
  }
225
229
  }
226
230
  if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
@@ -283,11 +287,17 @@ export class AcpBridge {
283
287
  const error = new Error(messageText);
284
288
  const nativeCode = typeof data?.code === "string" ? data.code : typeof data?.native_code === "string" ? data.native_code : null;
285
289
  const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
286
- error.code = unknown ? nativeCode : /payment required|quota|usage balance exhausted|insufficient.?credit|billing/i.test(messageText) ? "provider_quota_exhausted" : /rate.?limit|too many requests/i.test(messageText) ? "provider_rate_limited" : /\b429\b/.test(messageText) ? "provider_limit_unknown" : "acp_request_failed";
287
- error.retryable = error.code === "provider_rate_limited";
290
+ const nativeFailure = { ...(provider ?? {}), ...(data ?? {}),
291
+ code: [provider?.code, data?.code, data?.native_code].filter(value => typeof value === "string").join(" "), message: messageText };
292
+ const category = pending.method === "session/prompt" ? classifyProviderError(nativeFailure) : "provider_failed";
293
+ error.code = unknown ? nativeCode : category === "provider_failed" ? "acp_request_failed" : category;
294
+ error.retryable = ["provider_rate_limited", "provider_capacity_transient"].includes(error.code);
288
295
  error.details = { acp_code: message.error.code ?? null,
289
296
  observed_at: provider?.observed_at ?? now(), provider_session_id: pending.sessionId ?? null, source: "acp.error_response", request_id: message.id,
290
297
  terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
298
+ ...(pending.method === "session/prompt" ? { native_source: "acp.error_response", terminal_status: "failed", turn_id: String(message.id), native_turn_id: String(message.id), provider_error: safeProviderError(nativeFailure),
299
+ native_retry_active: pending.nativeRetryActive === true, will_retry: nativeFailure.willRetry === true,
300
+ native_turn_items: { observed: this.toolSummary(pending.sessionId).completeness === "complete", possible_effects: this.toolSummary(pending.sessionId).total > 0, pending: this.toolSummary(pending.sessionId).pending > 0 } } : {}),
291
301
  ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
292
302
  pending.reject(error);
293
303
  }
@@ -1046,6 +1056,9 @@ export async function promptSessionWithBridge(bridge, options) {
1046
1056
  idleSessionId: identity.adapterSessionId,
1047
1057
  idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
1048
1058
  inactivityMode: "diagnostic",
1059
+ }).catch(error => {
1060
+ if (error.details?.terminal_response?.session_id === identity.adapterSessionId) error.details.provider_session_id = identity.providerSessionId;
1061
+ throw error;
1049
1062
  });
1050
1063
  const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
1051
1064
  const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
@@ -1,6 +1,7 @@
1
1
  export type DelegationWork = { work_id: string; start_command: string; launch_policy?: string };
2
2
  export type DelegationInput = { harness: string; stage: string; capacity: number; works?: DelegationWork[]; totalWorks?: number; remainingWorks?: number };
3
3
  export function delegationAdapter(harness: string): string | null;
4
+ export function renderNativeChildFollowupInstruction(harness: string, childId: string): string | null;
4
5
  export function delegationContract(harness: string): string;
5
6
  export const lifecycleRetryInstruction: string;
6
7
  export const lifecycleCommandCompletionInstruction: string;
@@ -37,6 +37,17 @@ export function delegationAdapter(harness) {
37
37
  return Object.hasOwn(aliases, harness) ? aliases[harness] : null;
38
38
  }
39
39
 
40
+ /** Only documented same-child APIs may continue an existing native worker. */
41
+ export function renderNativeChildFollowupInstruction(harness, childId) {
42
+ if (typeof childId !== "string" || !childId) return null;
43
+ const adapter = delegationAdapter(harness), target = JSON.stringify(childId);
44
+ if (adapter === "codex") return `Use collaboration.followup_task with target: ${target} and message: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Do not spawn a replacement.`;
45
+ if (adapter === "opencode") return `Use the native subagent tool with sessionID: ${target} and prompt: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Keep the same model and reasoning. Do not spawn a replacement.`;
46
+ if (adapter === "droid") return `Use the native Task tool with resume: ${target}, subagent_type: "dd-flow-worker", and prompt: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Omit model and complexity overrides. Do not spawn a replacement.`;
47
+ if (adapter === "grok") return `Use the native send_subagent_message tool with subagent_id: ${target}, delivery: "queue", and text: "Continue the same assigned Work after the transient provider overload; do not restart it or repeat committed lifecycle commands." Retain its accepted message_id and wait on the same subagent. If admission is uncertain, do not resend. Do not spawn a replacement.`;
48
+ return null;
49
+ }
50
+
40
51
  function requireAdapter(harness) {
41
52
  const key = delegationAdapter(harness);
42
53
  if (!key) throw Object.assign(new Error(`Unsupported delegation adapter: ${harness}`), { code: "delegation_contract_unsupported" });
@@ -25,7 +25,8 @@ export function maintenanceRetryable(error) {
25
25
  let retryable = false;
26
26
  // Inspect only bounded typed causes from our maintenance transport, never text.
27
27
  for (let depth = 0; error && depth < 8; depth++, error = error.cause ?? error.details?.cause) {
28
- if (error.details?.cleanup_unconfirmed === true) return false;
28
+ if (error.details?.cleanup_unconfirmed === true || error.cleanup_error || error.details?.cleanup_error
29
+ || error.details?.store_cleanup_errors?.length || error.details?.transaction_cleanup_error || error.details?.transaction_close_error) return false;
29
30
  if (error.details?.effect === "unknown") return false;
30
31
  if (["process_lease_lost", "controller_lease_lost", "process_maintenance_receipt_invalid", "runtime_owner_mismatch"].includes(error.code)) return false;
31
32
  if (error.code === "process_ownership_unknown" && error.details?.reason === "physical_identity_unavailable" && ["process_confirmation", "observer_registration"].includes(error.details.phase) && error.details.effect === "no_effect" && error.details.native_dispatch_started === false) retryable = true;
@@ -1,4 +1,5 @@
1
1
  export const NATIVE_CHILD_CONTRACT_VERSION: "native-children@1";
2
+ export function settledNativeTree(root: string, children: Record<string, unknown>[], settled: boolean, rootSettled?: boolean): Record<string, unknown>;
2
3
  export function safeNativeFailure(value: unknown): Record<string, unknown> | undefined;
3
4
  export interface NativeChild {
4
5
  session_id: string;
@@ -9,6 +10,7 @@ export interface NativeChild {
9
10
  terminal_turn_id?: string;
10
11
  terminal_observed_at?: string;
11
12
  provider_error?: unknown;
13
+ native_source?: "opencode.v2.session.execution" | "agy.result" | "acp.error_response" | "droid.agent_turn_outcome" | "grok.subagent.event";
12
14
  native_turn_items?: { observed?: boolean; possible_effects?: boolean; pending?: boolean };
13
15
  }
14
16
  export function normalizeNativeChildren(receipt: Record<string, unknown>, rootSessionId: string): NativeChild[];