@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155

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 (226) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-execution.js +4 -1
  84. package/dist/services/controller-fanout.js +9 -4
  85. package/dist/services/controller-memory-worker.js +6 -4
  86. package/dist/services/controller-result-authority.js +122 -0
  87. package/dist/services/dashboard.js +4 -2
  88. package/dist/services/delivery-access.js +123 -0
  89. package/dist/services/delivery-approval.js +68 -0
  90. package/dist/services/delivery-artifact.js +86 -0
  91. package/dist/services/delivery-checks.js +178 -0
  92. package/dist/services/delivery-execution.js +181 -0
  93. package/dist/services/delivery-intents.js +212 -0
  94. package/dist/services/delivery-operation.js +81 -0
  95. package/dist/services/delivery-policy.js +91 -0
  96. package/dist/services/delivery-procedures.js +43 -0
  97. package/dist/services/delivery-request.js +99 -0
  98. package/dist/services/delivery-result.js +144 -0
  99. package/dist/services/delivery-stage-commands.js +26 -0
  100. package/dist/services/delivery-stage-entry.js +56 -0
  101. package/dist/services/delivery-stage-handlers.js +135 -0
  102. package/dist/services/delivery-stage-results.js +94 -0
  103. package/dist/services/descriptor-coordinator-session.js +3 -1
  104. package/dist/services/descriptor-run-start.js +24 -4
  105. package/dist/services/descriptor-stage-coordinator.js +78 -10
  106. package/dist/services/descriptor-stage-settlement.js +59 -14
  107. package/dist/services/diagnostic-repair-intake.js +123 -0
  108. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  109. package/dist/services/diagnostic-report.js +30 -0
  110. package/dist/services/diagnostic-run-bundle.js +48 -9
  111. package/dist/services/diagnostic-snapshot.js +2 -2
  112. package/dist/services/diagnostic-stage-handlers.js +3 -2
  113. package/dist/services/diagnostic-stage-results.js +26 -0
  114. package/dist/services/diagnostic-start.js +56 -6
  115. package/dist/services/diagnostic-work-collection.js +3 -3
  116. package/dist/services/distill-document-publication.js +280 -0
  117. package/dist/services/distill-promotion.js +197 -0
  118. package/dist/services/distill-publication-custody.js +162 -0
  119. package/dist/services/distill-stage-handlers.js +249 -0
  120. package/dist/services/distill-stage-results.js +105 -0
  121. package/dist/services/distill-user-inputs.js +4 -0
  122. package/dist/services/distill-work-batch.js +42 -0
  123. package/dist/services/distill-work-collection.js +74 -0
  124. package/dist/services/distill-work-registration.js +93 -0
  125. package/dist/services/distill-work-results.js +98 -0
  126. package/dist/services/eval-snapshots.js +17 -7
  127. package/dist/services/external-work-launch.js +8 -2
  128. package/dist/services/flow-catalog.js +24 -0
  129. package/dist/services/frozen-engine.js +19 -2
  130. package/dist/services/generic-execution.js +4 -3
  131. package/dist/services/harness-adapter.js +2 -14
  132. package/dist/services/hook-memory-routing.js +1 -1
  133. package/dist/services/hooks.js +31 -6
  134. package/dist/services/hybrid-contracts.js +6 -1
  135. package/dist/services/installed-flow-source.js +37 -2
  136. package/dist/services/interactive-attached-session.js +215 -0
  137. package/dist/services/interactive-baseline-custody.js +54 -0
  138. package/dist/services/interactive-before-versions.js +148 -0
  139. package/dist/services/interactive-bundle-data.js +85 -0
  140. package/dist/services/interactive-checks.js +179 -0
  141. package/dist/services/interactive-execution.js +33 -0
  142. package/dist/services/interactive-finish-integration.js +236 -0
  143. package/dist/services/interactive-git-baseline.js +230 -0
  144. package/dist/services/interactive-git-dispatch.js +278 -0
  145. package/dist/services/interactive-input-delivery.js +82 -0
  146. package/dist/services/interactive-mutation-scope.js +231 -0
  147. package/dist/services/interactive-request.js +287 -0
  148. package/dist/services/interactive-run-bundle.js +109 -0
  149. package/dist/services/interactive-stage-entry.js +41 -0
  150. package/dist/services/interactive-stage-handlers.js +289 -0
  151. package/dist/services/interactive-stage-observation.js +136 -0
  152. package/dist/services/interactive-stage-results.js +111 -0
  153. package/dist/services/interactive-user-inputs.js +142 -0
  154. package/dist/services/interactive-work-batch.js +43 -0
  155. package/dist/services/interactive-work-collection.js +119 -0
  156. package/dist/services/interactive-work-completion.js +180 -0
  157. package/dist/services/interactive-work-context.js +249 -0
  158. package/dist/services/interactive-work-history.js +112 -0
  159. package/dist/services/interactive-work-registration.js +124 -0
  160. package/dist/services/interactive-work-results.js +115 -0
  161. package/dist/services/interactive-work-start.js +155 -0
  162. package/dist/services/lifecycle-invocations.js +214 -24
  163. package/dist/services/managed-daemon-binding.js +52 -1
  164. package/dist/services/memory-compose-handler.js +2 -22
  165. package/dist/services/memory-finish-integration.js +58 -17
  166. package/dist/services/memory-fix-finish-integration.js +129 -0
  167. package/dist/services/memory-fix-git-dispatch.js +139 -0
  168. package/dist/services/memory-fix-intake-custody.js +11 -0
  169. package/dist/services/memory-fix-intake-settlement.js +127 -0
  170. package/dist/services/memory-fix-publication.js +196 -0
  171. package/dist/services/memory-fix-repair-admission.js +116 -0
  172. package/dist/services/memory-fix-request.js +141 -0
  173. package/dist/services/memory-fix-run-bundle.js +83 -0
  174. package/dist/services/memory-fix-scope.js +78 -0
  175. package/dist/services/memory-fix-sources.js +85 -0
  176. package/dist/services/memory-fix-stage-entry.js +24 -0
  177. package/dist/services/memory-fix-stage-handlers.js +282 -0
  178. package/dist/services/memory-fix-stage-results.js +106 -0
  179. package/dist/services/memory-fix-workspace.js +127 -0
  180. package/dist/services/memory-flow-user-inputs.js +88 -0
  181. package/dist/services/memory-init-contexts.js +55 -5
  182. package/dist/services/memory-init-controller-start.js +20 -4
  183. package/dist/services/memory-layer.js +7 -3
  184. package/dist/services/memory-native-review-copy.js +27 -3
  185. package/dist/services/memory-repair-document-delta.js +1 -0
  186. package/dist/services/project-flow-source.js +195 -0
  187. package/dist/services/project-git-integration.js +92 -0
  188. package/dist/services/repair-intents.js +2 -7
  189. package/dist/services/retained-native-hook.js +245 -0
  190. package/dist/services/review-fix-handoff.js +82 -0
  191. package/dist/services/review-fix-intake-custody.js +17 -0
  192. package/dist/services/review-fix-stage-entry.js +43 -0
  193. package/dist/services/review-fix-stage-handlers.js +176 -0
  194. package/dist/services/review-fix-stage-results.js +86 -0
  195. package/dist/services/run-control-worker.js +42 -1
  196. package/dist/services/run-control.js +40 -5
  197. package/dist/services/run-controller-adapter.js +37 -3
  198. package/dist/services/run-controller-process.js +12 -4
  199. package/dist/services/run-controller-recovery.js +46 -6
  200. package/dist/services/run-controller-state.js +3 -2
  201. package/dist/services/run-controller.js +209 -43
  202. package/dist/services/run-recovery.js +31 -2
  203. package/dist/services/run-timeline.js +59 -0
  204. package/dist/services/runs.js +18 -31
  205. package/dist/services/sessions.js +2 -1
  206. package/dist/services/stage-handlers.js +55 -3
  207. package/dist/services/stage-lifecycle.js +4 -3
  208. package/dist/services/stage-pause.js +26 -9
  209. package/dist/services/stage-report-renderer.js +2 -6
  210. package/dist/services/technical-notes.js +31 -0
  211. package/dist/services/verification-matrix.js +1 -1
  212. package/dist/services/version-status.js +1 -1
  213. package/dist/services/vnext-code-review.js +74 -5
  214. package/dist/services/vnext-code.js +8 -32
  215. package/dist/services/vnext-fanout.js +53 -8
  216. package/dist/services/vnext-merge.js +10 -5
  217. package/dist/services/vnext-plan-review.js +1 -1
  218. package/dist/services/vnext-plan.js +1 -1
  219. package/dist/services/vnext-protocolize.js +1 -1
  220. package/dist/services/vnext-specify.js +10 -5
  221. package/dist/services/work-registry.js +252 -66
  222. package/dist/services/worktrees.js +7 -4
  223. package/dist/storage/database.js +2 -1
  224. package/dist/verification-components.json +82 -0
  225. package/package.json +3 -2
  226. package/tools/verification-jobs.mjs +35 -10
@@ -5,10 +5,10 @@ import { canonicalNativeOperation } from './adapter-timeouts.mjs';
5
5
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
6
6
  import { prepareDaemonRequest } from "./native-request-proof.mjs";
7
7
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
8
- import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
8
+ import { errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
9
9
  import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
10
- import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
11
- import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
10
+ import { acceptNativeHookRequest, readNativeHookFailures, writeNativeHookFailure } from "./native-hook-command.mjs";
11
+ import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
12
12
  import { ObservationClock } from "./observation-clock.mjs";
13
13
  import { createHash, randomUUID } from "node:crypto";
14
14
  import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
@@ -43,7 +43,8 @@ const errorPayload = errorRecord;
43
43
  function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
44
44
 
45
45
  function executable(command, args = []) { const resolved = command ?? process.env.DD_GROK_BIN ?? "grok"; return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args }; }
46
- export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config) }; }
46
+ export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config),
47
+ DD_GROK_STATE_DIR: !config.noFlow && config.daemonId ? config.runtime_owner?.state_dir : undefined }; }
47
48
  function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
48
49
 
49
50
  async function writeIsolatedConfig(paths) {
@@ -185,7 +186,7 @@ export async function startDaemon(options) {
185
186
  export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); }
186
187
 
187
188
  export class Runtime {
188
- constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
189
+ constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.promptCreators = new Map(); this.childCreators = new Map(); this.childSpawnWaiters = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
189
190
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()], descendants: [...this.descendants.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
190
191
  async reduceTerminal(outcome) {
191
192
  const session = this.sessions.get(outcome.session_id), turn = session?.native_prompt_operation;
@@ -234,7 +235,7 @@ export class Runtime {
234
235
  }
235
236
  options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, rootSettled: id => this.settledRoot(id), liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
236
237
  track(result, before = null) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { ...session, native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant({ ...child, stale_activity: Boolean(before && before.get(child.sessionId ?? child.subagentId ?? child.provider_session_id) !== this.descendants.get(child.sessionId ?? child.subagentId ?? child.provider_session_id)) }, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
237
- recordDescendant(value, root, source, fallback) {
238
+ recordDescendant(value, root, source, fallback, nativeSpawn = false) {
238
239
  const id = value?.provider_session_id ?? value?.session_id ?? value?.sessionId ?? value?.childSessionId ?? value?.subagentId ?? value?.child_session_id ?? value?.subagent_id;
239
240
  const parent = value?.parent_provider_session_id ?? value?.parentSessionId ?? value?.parent_session_id ?? root;
240
241
  if (typeof id !== "string" || !id || typeof parent !== "string" || !parent) return;
@@ -273,6 +274,14 @@ export class Runtime {
273
274
  return;
274
275
  }
275
276
  if (!newAttempt && terminal(descendant?.status) && terminal(status) && descendant.status !== status) throw new DaemonError("native_child_outcome_conflict", "Grok child has contradictory terminal outcomes", false, { session_id: id, previous: descendant.status, observed: status });
277
+ if (nativeSpawn && spawned && typeof attempt === "string" && attempt && source === "x.ai/subagent/event") {
278
+ const promptId = value.parent_prompt_id;
279
+ const creator = typeof promptId === "string" && promptId ? this.promptCreators.get(JSON.stringify([parent, promptId])) : null;
280
+ const old = this.childCreators.get(id);
281
+ if (old && old.attempt === attempt && (old.parent !== parent || typeof promptId === "string" && promptId && old.promptId !== promptId)) throw new DaemonError("native_child_execution_ambiguous", "Grok child attempt has conflicting native creator", false, { session_id: id });
282
+ if (creator) this.childCreators.set(id, { parent, promptId, attempt, creator });
283
+ else if (newAttempt) this.childCreators.delete(id);
284
+ }
276
285
  this.sessions.set(id, {
277
286
  ...prior,
278
287
  provider_session_id: id,
@@ -296,6 +305,7 @@ export class Runtime {
296
305
  ...(newAttempt && descendant?.attempt_id ? { retired_attempt_ids: [...retired, descendant.attempt_id] } : retired.length ? { retired_attempt_ids: retired } : {}),
297
306
  ...(!newAttempt && !terminal(status) && descendant?.outcome_error ? { outcome_error: descendant.outcome_error } : {})
298
307
  });
308
+ if (this.childCreators.has(id)) for (const wake of this.childSpawnWaiters.get(id) ?? []) wake();
299
309
  if (spawned && typeof value.model === "string" && value.model.trim()) {
300
310
  return { sessionId: id, parentSessionId: parent, attemptId: attempt ?? null, parentPromptId: value.parent_prompt_id ?? null, model: value.model.trim() };
301
311
  }
@@ -307,7 +317,7 @@ export class Runtime {
307
317
  const modelFacts = [];
308
318
  if (/subagent/i.test(kind)) {
309
319
  for (const value of [update?.subagent, update?.subagentInfo, update?.child, update?.result, update]) {
310
- const fact = this.recordDescendant(value, root, "x.ai/subagent/event", /fail|error/i.test(kind) ? "failed" : /cancel/i.test(kind) ? "cancelled" : /complete|finish|done/i.test(kind) ? "completed" : "running");
320
+ const fact = this.recordDescendant(value, root, "x.ai/subagent/event", /fail|error/i.test(kind) ? "failed" : /cancel/i.test(kind) ? "cancelled" : /complete|finish|done/i.test(kind) ? "completed" : "running", value === update);
311
321
  if (fact) modelFacts.push(fact);
312
322
  }
313
323
  }
@@ -396,7 +406,7 @@ export class Runtime {
396
406
  assertDaemonDispatch(this.paths?.dir);
397
407
  if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
398
408
  this.active = name;
399
- const current = this.nativeHookOperation = { operationId, sessionId, generation: (this.hookGeneration = (this.hookGeneration ?? 0) + 1), requests: new Map() };
409
+ const current = this.nativeHookOperation = { operationId, sessionId, generation: (this.hookGeneration = (this.hookGeneration ?? 0) + 1), requests: new Map(), guard: captureDispatchGuard(this) };
400
410
  let rejectHook;
401
411
  const hookFailed = new Promise((_, reject) => { rejectHook = reject; });
402
412
  void hookFailed.catch(() => {});
@@ -406,15 +416,27 @@ export class Runtime {
406
416
  // and local archive export must not erase an unrelated Session's receipt.
407
417
  if (name === "session.prompt" && this.sessions.has(sessionId)) {
408
418
  const session = this.sessions.get(sessionId);
419
+ if (typeof operationId === "string" && operationId) {
420
+ current.nativePromptId = randomUUID();
421
+ // Retain only for this daemon lifetime: a late native spawn or hook
422
+ // cannot be safely distinguished from an expired one by a timer.
423
+ // After restart the in-memory correlation is deliberately unavailable.
424
+ this.promptCreators.set(JSON.stringify([sessionId, current.nativePromptId]), current);
425
+ }
409
426
  session.native_root_receipt = null;
410
427
  this.bridge.dispatchGeneration = current.generation;
411
- session.native_prompt_operation = { daemon_id: this.state.daemon_id, connection_id: this.bridge.connectionId, operation_id: operationId, generation: current.generation };
428
+ session.native_prompt_operation = { daemon_id: this.state.daemon_id, connection_id: this.bridge.connectionId, operation_id: operationId, generation: current.generation,
429
+ ...(current.nativePromptId ? { native_prompt_id: current.nativePromptId } : {}) };
412
430
  }
413
431
  try {
414
432
  await this.persist({ active_tree: true, active_operation: name });
415
433
  assertDaemonDispatch(this.paths?.dir);
416
434
  const before = new Map(this.descendants);
417
435
  const result = await Promise.race([task(), hookFailed]);
436
+ await this.observeHookFailures(current);
437
+ if (current.error) throw current.error;
438
+ const hookDiagnostics = (this.bridge.lifecycleDiagnostics ?? []).filter(item => item.operation_id === current.operationId && item.generation === current.generation);
439
+ if (hookDiagnostics.length) result.native_hook_diagnostics = [...new Map([...(result.native_hook_diagnostics ?? []), ...hookDiagnostics].map(item => [JSON.stringify(item), item])).values()];
418
440
  await this.drainTerminal();
419
441
  this.track(result, before);
420
442
  const session = this.sessions.get(result.provider_session_id);
@@ -429,41 +451,131 @@ export class Runtime {
429
451
  // root-turn proof, not proof that any descendants have stopped. A lost
430
452
  // response, bridge error, or failure before dispatch earns no receipt.
431
453
  try { await this.drainTerminal(); } catch (storage) { error.cleanup_error ??= errorRecord(storage); }
432
- try { await hookBudget(1_000, null).run(() => this.persist({ active_tree: true, active_operation: null }), "failure_persistence"); }
454
+ try { await this.persist({ active_tree: true, active_operation: null }); }
433
455
  catch (storage) { error.cleanup_error = errorRecord(storage); }
434
456
  throw error;
435
457
  }
436
- finally { clearInterval(timer); current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.active = null; }
458
+ finally { clearInterval(timer); if (!current.nativePromptId) current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.active = null; }
437
459
  }
438
460
  async observeHookFailures(current = this.nativeHookOperation) {
439
- if (!current || this.nativeHookOperation !== current || current.reading) return;
440
- current.reading = true;
441
- try { await readNativeHookFailures(this.paths.dir, current.requests, current, error => {
442
- if (!isProvenShellCorrection(error)) current.reject(error);
443
- }); } finally { current.reading = false; }
461
+ if (!current || this.nativeHookOperation !== current) return;
462
+ while (current.reading) {
463
+ await current.reading;
464
+ if (this.nativeHookOperation !== current) return;
465
+ }
466
+ const reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.acceptHookFailure(current, error));
467
+ current.reading = reading;
468
+ try { await reading; } finally { if (current.reading === reading) current.reading = null; }
469
+ }
470
+ acceptHookFailure(current, error) {
471
+ if (this.nativeHookOperation !== current) return;
472
+ const unconfirmed = error.details?.native_hook_binding?.native_creator === "unconfirmed";
473
+ if (!unconfirmed && isProvenShellCorrection(error)) return;
474
+ if (unconfirmed || isNativeHookObservationLoss(error)) {
475
+ const diagnostic = { ...errorRecord(error), session_id: current.sessionId, operation_id: current.operationId,
476
+ generation: current.generation, delivery_stage: "hook_observation", completeness: "incomplete" };
477
+ this.bridge.lifecycleDiagnostics ??= [];
478
+ this.bridge.lifecycleDiagnostics.push(diagnostic);
479
+ try { void Promise.resolve(this.bridge.journal?.write("native_hook_observation_failure", diagnostic)).catch(() => {}); } catch { /* diagnostic is optional */ }
480
+ return;
481
+ }
482
+ current.reject(error);
444
483
  }
445
484
  async acceptHook(identity, sessionId, payload) {
446
485
  const input = payload?.tool_input ?? payload?.toolInput;
447
486
  const command = input?.command ?? input?.cmd;
448
487
  if ((payload?.hook_event_name ?? payload?.hookEventName) !== "PreToolUse" || typeof command !== "string" || (!["lifecycle", "auxiliary"].includes(parseLifecycleCommand(command).participation) && !continuationCommand(command))) return identity;
449
- const current = this.nativeHookOperation, tool = payload.tool_use_id ?? payload.toolUseId ?? payload.tool_call_id;
450
- const root = current?.sessionId ?? this.loadedSessionId;
488
+ const active = this.nativeHookOperation, tool = payload.tool_use_id ?? payload.toolUseId ?? payload.tool_call_id;
489
+ const root = identity.rootProviderSessionId;
490
+ const source = sessionId !== root ? this.childCreators.get(sessionId) : null;
491
+ // An older Grok may not surface creator metadata. Preserve its ordinary
492
+ // stateful handler, but never turn the current prompt into guessed proof.
493
+ if (sessionId !== root && !source) return identity;
494
+ const current = sessionId !== root ? source?.creator : active;
451
495
  const pending = [...(this.bridge.pending?.values() ?? [])].some(request => request.method === "session/prompt" && request.sessionId === root);
452
- if (!current || !pending || identity.rootProviderSessionId !== root || !tool) throw new DaemonError("native_hook_unproven", "Native hook has no exact active Session operation");
453
- const binding = await hookBudget(1_000, null).run(() => acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation, provider_session_id: sessionId, root_provider_session_id: root, tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null }, current), "hook_binding");
454
- if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
496
+ const nativePrompt = payload.prompt_id ?? payload.promptId;
497
+ if (!current || current.sessionId !== root || !tool || sessionId === root && (!pending || active !== current)
498
+ || sessionId === root && typeof nativePrompt === "string" && nativePrompt && nativePrompt !== current.nativePromptId
499
+ || sessionId !== root && (source.parent !== identity.parentProviderSessionId || this.promptCreators.get(JSON.stringify([source.parent, source.promptId])) !== current))
500
+ throw new DaemonError("native_hook_unproven", "Native hook has no exact source-bound Session operation");
501
+ current.guard(); assertDaemonDispatch(this.paths.dir);
502
+ const nativePayload = { ...payload, _meta: { ...(payload._meta ?? {}), ddGrok: { ...identity } } };
503
+ const binding = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
504
+ provider_session_id: sessionId, root_provider_session_id: root, parent_provider_session_id: identity.parentProviderSessionId ?? null,
505
+ tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null,
506
+ native_creator: source || typeof nativePrompt === "string" && nativePrompt && nativePrompt === current.nativePromptId ? "confirmed" : "unconfirmed",
507
+ ...(source ? { source_harness: "grok-acp", native_command: command, native_payload: nativePayload } : {}) }, current);
508
+ current.guard();
509
+ if (sessionId === root && this.nativeHookOperation !== current || sessionId !== root && this.childCreators.get(sessionId) !== source)
510
+ throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
455
511
  current.requests.set(binding.request_id, { current, binding });
456
- return { ...identity, nativeHookBinding: binding };
512
+ const parsed = parseLifecycleCommand(command);
513
+ const fastAck = Boolean(source && parsed.kind === "standalone" && parsed.invocation.operation === "work_start" && commandOption(parsed.invocation, "invocation-id"));
514
+ return { ...identity, nativeHookBinding: binding, ...(fastAck ? { fastAck: true } : {}) };
515
+ }
516
+ async waitForChildCreator(sessionId, deadlineAt) {
517
+ const remaining = deadlineAt - Date.now();
518
+ if (this.childCreators.has(sessionId) || !Number.isSafeInteger(deadlineAt) || remaining <= 0 || remaining > 30_000) return;
519
+ // Leave most of the existing host budget to the legacy handler if this
520
+ // provider omits the optional spawn correlation; do not poll the provider.
521
+ const waitMs = Math.floor(remaining / 3);
522
+ if (!waitMs) return;
523
+ await new Promise(resolve => {
524
+ const waiters = this.childSpawnWaiters.get(sessionId) ?? new Set();
525
+ const wake = () => { clearTimeout(timer); waiters.delete(wake); if (!waiters.size) this.childSpawnWaiters.delete(sessionId); resolve(); };
526
+ const timer = setTimeout(wake, waitMs);
527
+ waiters.add(wake); this.childSpawnWaiters.set(sessionId, waiters);
528
+ });
529
+ }
530
+ hookOwner(binding) {
531
+ if (!binding?.request_id) return null;
532
+ const owners = new Set([this.nativeHookOperation, ...this.promptCreators.values()]);
533
+ for (const owner of owners) {
534
+ if (!owner || owner.operationId !== binding.operation_id || owner.generation !== binding.turn_generation) continue;
535
+ const accepted = owner.requests.get(binding.request_id);
536
+ if (accepted?.current === owner && JSON.stringify(accepted.binding) === JSON.stringify(binding)) return owner;
537
+ }
538
+ return null;
457
539
  }
458
540
  async dispatch(operation, params, operationId = null) {
459
541
  const assertDispatch = captureDispatchGuard(this);
460
542
  if (["session.create", "session.prompt", "session.fork", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
461
543
  if (operation === "daemon.ready") return { ready: this.ready, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config };
462
544
  if (operation === "daemon.status") { let observationError; if (!this.active && !this.state.shutdown?.phases?.tree) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
463
- if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return await this.acceptHook({ daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }, sessionId, params.payload); }
464
- if (operation === "hook.failed") { await this.observeHookFailures(); return { observed: true }; }
545
+ if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if ((!session || session.parent_provider_session_id) && !this.childCreators.has(sessionId) && this.promptCreators.size) { await this.waitForChildCreator(sessionId, params.hookDeadlineAt); session = this.sessions.get(sessionId); } if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return await this.acceptHook({ daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }, sessionId, params.payload); }
546
+ if (operation === "hook.verify") {
547
+ if (params.daemonId !== this.state.daemon_id) throw new DaemonError("native_hook_unproven", "Native hook daemon identity differs");
548
+ const owner = this.hookOwner(params.binding);
549
+ if (!owner) throw new DaemonError("native_hook_unproven", "Native hook has no matching retained operation");
550
+ owner.guard(); assertDaemonDispatch(this.paths.dir);
551
+ return { verified: true, nativeHookBinding: params.binding };
552
+ }
553
+ if (operation === "hook.failed") {
554
+ if (params.binding !== undefined || params.failure !== undefined) {
555
+ const binding = params.binding, failure = params.failure;
556
+ const current = this.hookOwner(binding);
557
+ while (current?.reading) await current.reading;
558
+ const accepted = current?.requests.get(binding?.request_id);
559
+ if (!current || this.hookOwner(binding) !== current || accepted?.current !== current || JSON.stringify(accepted.binding) !== JSON.stringify(binding)
560
+ || typeof failure?.code !== "string" || typeof failure?.message !== "string")
561
+ throw new DaemonError("native_hook_unproven", "Hook failure has no exact current owned delivery");
562
+ current.guard(); assertDaemonDispatch(this.paths.dir);
563
+ if (this.nativeHookOperation !== current) {
564
+ // The creator has returned. Retain the bound failure under its
565
+ // original operation, without rejecting the newer productive turn.
566
+ await writeNativeHookFailure(this.paths.dir, binding, reportedError(failure, "Native hook failed"));
567
+ return { observed: true, retained: true };
568
+ }
569
+ current.requests.delete(binding.request_id);
570
+ const error = reportedError(failure, "Native hook failed");
571
+ error.details = { ...error.details, native_hook_binding: binding };
572
+ this.acceptHookFailure(current, error);
573
+ return { observed: true };
574
+ }
575
+ await this.observeHookFailures(); return { observed: true };
576
+ }
465
577
  if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; }, null, operationId);
466
- if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
578
+ if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId, nativePromptId: this.nativeHookOperation?.nativePromptId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
467
579
  if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
468
580
  if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); const settled = !this.active && !this.treeRunning(result); const descendants = this.topology(result.provider_session_id); const terminal = this.sessions.get(result.provider_session_id)?.native_root_receipt; return nativeExecutionReceipt({ ...result, settled, descendants, ...settledNativeTree(result.provider_session_id, descendants, settled, !this.active && !this.rootRunning(result)), ...(terminal?.kind === "failed" && terminal.request_id !== undefined ? { terminal_turn_id: String(terminal.request_id) } : {}) }, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 }); }
469
581
  if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
@@ -153,12 +153,14 @@ export async function promptSessionWithBridge(bridge, options) {
153
153
  const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; const initial_usage_error = await forwardUsageBestEffort(local, sessionId, await sessionUsage(bridge, sessionId), sessionToolCalls(local, bridge, sessionId));
154
154
  options.assertDispatch?.();
155
155
  bridge.dispatchOperationId = options.operationId;
156
- const cursor = bridge.toolCursor(); const textCursor = bridge.assistantTextCursor(sessionId); const turn = await bridge.request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, null, { idleSessionId: sessionId, idleTimeoutMs: options.livenessTimeoutMs ?? options.timeoutMs ?? 600_000 });
156
+ const diagnosticsCursor = bridge.lifecycleDiagnostics?.length ?? 0;
157
+ const cursor = bridge.toolCursor(); const textCursor = bridge.assistantTextCursor(sessionId); const turn = await bridge.request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }], ...(options.nativePromptId ? { _meta: { promptId: options.nativePromptId } } : {}) }, null, { idleSessionId: sessionId, idleTimeoutMs: options.livenessTimeoutMs ?? options.timeoutMs ?? 600_000 });
157
158
  try {
158
159
  await bridge.flush();
159
160
  const evidence = await inspect(bridge, sessionId, local); evidence.tool_calls = recordToolCalls(local, bridge, sessionId, cursor); const usage_ingest_error = await forwardUsageBestEffort(local, sessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
160
161
  if (!options.allowBackground && (evidence.subagents?.subagents ?? []).length) { await cancelTree(bridge, sessionId); fail("background Grok Build subagents cannot outlive the one-shot dd-grok connection"); }
161
- return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, assistant_text: bridge.assistantTextSince(sessionId, textCursor), turn, profile: profileReceipt, evidence });
162
+ const diagnostics = (bridge.lifecycleDiagnostics ?? []).slice(diagnosticsCursor).filter(item => item.session_id === sessionId);
163
+ return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, assistant_text: bridge.assistantTextSince(sessionId, textCursor), turn, profile: profileReceipt, evidence, ...(diagnostics.length ? { native_hook_diagnostics: diagnostics } : {}) });
162
164
  } catch (error) { throw Object.assign(new Error("Grok native Turn completed but mandatory observation failed", { cause: error }), { code: "native_outcome_observation_failed", retryable: false,
163
165
  details: { provider_session_id: sessionId, native_outcome: { status: "completed", turn }, observation_error: errorRecord(error), settled: false, journal_locator: options.journal } }); }
164
166
  }
@@ -1,6 +1,7 @@
1
1
  import { managedFlowEnv } from './managed-flow-env.mjs';
2
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
3
3
  import { callDaemonObserved } from './daemon-observation.mjs';
4
+ import { operationProgressReporter } from './operation-progress.mjs';
4
5
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
5
6
  import { prepareDaemonRequest } from "./native-request-proof.mjs";
6
7
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
@@ -13,7 +14,7 @@ import net from "node:net";
13
14
  import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
14
15
  import { waitForSettlement } from "./session-settlement.mjs";
15
16
  import { nativeExecutionReceipt } from "./native-children.mjs";
16
- import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
17
+ import { diagnosticJson, errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
17
18
  import { checkObservedProfile, observeModel, observedFields, readModelObservations } from "./model-observations.mjs";
18
19
  import os from "node:os";
19
20
  import path from "node:path";
@@ -21,7 +22,9 @@ import { parse as parseJsonc } from "jsonc-parser";
21
22
  import { writeJsonAtomic } from "./runner-events.mjs";
22
23
  import { OpenCodeClient, OpenCodeError, historyDigest, usageSnapshot, nativeModel } from "./dd-opencode.mjs";
23
24
  import { safeProviderError } from "./provider-overload-policy.mjs";
24
- import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
25
+ import { acceptNativeHookRequest, hookBudget, readNativeHookFailures, writeNativeHookFailure, retainNativeWorkStart } from "./native-hook-command.mjs";
26
+ import { isDeepStrictEqual } from "node:util";
27
+ import { parseLifecycleCommand } from "./lifecycle-command.mjs";
25
28
  import { assertStartupChild, retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
26
29
 
27
30
  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";
@@ -68,7 +71,7 @@ const errorPayload = errorRecord;
68
71
 
69
72
  async function writePlugin(paths, config) {
70
73
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
71
- 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`;
74
+ 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, nativeWorkStartAdmission: true })});\n`;
72
75
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
73
76
  }
74
77
  async function prepare(paths, config, authSource) {
@@ -188,7 +191,9 @@ export class Runtime {
188
191
  throw new DaemonError("session_identity_mismatch", "OpenCode retained Session identity is invalid or duplicated");
189
192
  this.sessions.set(record.provider_session_id, record);
190
193
  }
191
- this.active = null; this.persisting = Promise.resolve(); }
194
+ // ponytail: retained participating hooks live until daemon retirement;
195
+ // add durable per-tool retirement only if long-lived Sessions need it.
196
+ this.active = null; this.persisting = Promise.resolve(); this.retainedHookRequests = new Map(); }
192
197
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
193
198
  retainSession(info, root) {
194
199
  const id = info.id, parent = info.fork ? null : info.parentID ?? null, retained = this.sessions.get(id);
@@ -211,13 +216,14 @@ export class Runtime {
211
216
  throw new OpenCodeError("opencode_unavailable", "OpenCode child directory binding is not yet observable");
212
217
  return this.retainSession(info, root);
213
218
  }
214
- async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
215
- async childTree(id, seen = new Set()) {
219
+ async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null, ...(result?.native_hook_diagnostic ? { native_hook_diagnostic: JSON.parse(diagnosticJson(result.native_hook_diagnostic)) } : {}) })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
220
+ async childTree(id, seen = new Set(), onProgress = async () => {}) {
216
221
  if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
217
222
  seen.add(id); const direct = await this.ownedChildren(id), nested = [];
223
+ await onProgress(`children:${id}:${createHash('sha256').update(JSON.stringify(direct)).digest('hex')}`);
218
224
  for (const child of direct) {
219
225
  if (!child.id || child.parentID !== id) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
220
- nested.push(child, ...await this.childTree(child.id, seen));
226
+ nested.push(child, ...await this.childTree(child.id, seen, onProgress));
221
227
  }
222
228
  return nested;
223
229
  }
@@ -416,8 +422,27 @@ export class Runtime {
416
422
  current.reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.retainHookFailure(current, error));
417
423
  try { await current.reading; } finally { current.reading = null; }
418
424
  }
419
- retainHookFailure(current, error) {
420
- if (this.nativeHookOperation !== current || isProvenShellCorrection(error)) return;
425
+ async retainHookFailure(current, error, unconfirmed = false) {
426
+ if (this.nativeHookOperation !== current) return;
427
+ if (unconfirmed || error.details?.native_hook_binding?.native_creator === 'unconfirmed') {
428
+ // A Session/tree association is not the native creator Turn. Preserve
429
+ // the real refusal, but do not abort the prompt merely being observed.
430
+ const diagnostic = { code: 'native_hook_failed', native_creator: 'unconfirmed', observer_operation_id: current.operationId, observer_turn_generation: current.generation, primary_error: errorRecord(error) };
431
+ (current.hookDiagnostics ??= []).push(diagnostic);
432
+ try { await this.journal('native_hook_observation_unavailable', { provider_session_id: current.sessionId, native_hook_diagnostic: diagnostic }); }
433
+ catch (storage) { process.stderr.write(`${diagnosticJson({ native_hook_diagnostic: diagnostic, journal_error: errorRecord(storage) })}\n`); }
434
+ return;
435
+ }
436
+ if (isProvenShellCorrection(error)) return;
437
+ if (isNativeHookObservationLoss(error)) {
438
+ // A timed-out observer has not established whether the owned CLI ran.
439
+ // The CLI still owns admission and the native turn owns its outcome.
440
+ const diagnostic = errorRecord(error);
441
+ (current.hookDiagnostics ??= []).push(diagnostic);
442
+ try { await this.journal("native_hook_observation_unavailable", { provider_session_id: current.sessionId, native_hook_diagnostic: diagnostic }); }
443
+ catch (storage) { process.stderr.write(`${diagnosticJson({ native_hook_diagnostic: diagnostic, journal_error: errorRecord(storage) })}\n`); }
444
+ return;
445
+ }
421
446
  error.details = { ...error.details, provider_session_id: current.sessionId, native_outcome: "unknown", native_turn_items: { observed: true, possible_effects: true, pending: true } };
422
447
  current.failure ??= error;
423
448
  }
@@ -425,12 +450,46 @@ export class Runtime {
425
450
  const assertDispatch = captureDispatchGuard(this);
426
451
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
427
452
  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 }; }
453
+ if (operation === "hook.register") {
454
+ if (!this.state.config.runtime_owner || !this.state.config.ddFlowBin) return {};
455
+ assertDaemonDispatch(this.paths.dir);
456
+ const payload = params.payload, observer = this.nativeHookOperation ?? this.lastNativeHookOperation;
457
+ if (typeof params.sessionId !== "string" || !params.sessionId || typeof payload?.message_id !== "string" || !payload.message_id
458
+ || typeof payload?.tool_call_id !== "string" || !payload.tool_call_id || !observer?.sessionId) throw new DaemonError("native_hook_unproven", "Native Work start lacks physical callback identity");
459
+ this.retainedWorkStarts ??= new Map();
460
+ const tool = `${payload.message_id}:${payload.tool_call_id}`;
461
+ const accepted = await retainNativeWorkStart(this.paths.dir, { daemon_id: this.state.daemon_id, source_harness: "opencode-server",
462
+ root_provider_session_id: observer.sessionId, provider_session_id: params.sessionId, parent_provider_session_id: null,
463
+ tool_call_id: tool, event: "PreToolUse", native_command: payload.input?.command, native_cwd: null,
464
+ native_payload: { schema_id: "dd-flow/opencode-tool-event@1", phase: "before", event_id: [this.state.daemon_id, params.sessionId, payload.message_id, payload.tool_call_id].join(":"),
465
+ daemon_id: this.state.daemon_id, message_id: payload.message_id, tool_call_id: tool, tool: "shell", input: payload.input,
466
+ session: { provider_session_id: params.sessionId } } }, observer, this.retainedWorkStarts);
467
+ return accepted ? { fastAck: true, nativeHookBinding: accepted } : {};
468
+ }
428
469
  if (operation === "hook.resolve") {
429
- const current = this.nativeHookOperation, sessionId = params.sessionId, payload = params.payload;
470
+ const payload = params.payload;
471
+ if (params.deferWorkStart === true && !this.state.config.noFlow && this.state.config.runtime_owner && this.state.config.ddFlowBin
472
+ && ["before", "after"].includes(payload?.phase) && typeof params.sessionId === "string" && params.sessionId
473
+ && typeof payload.message_id === "string" && payload.message_id && typeof payload.tool_call_id === "string" && payload.tool_call_id
474
+ && typeof payload.input?.command === "string") {
475
+ const analysis = parseLifecycleCommand(payload.input.command);
476
+ if (analysis.participation === "none" && !analysis.unresolved) {
477
+ // No lifecycle authority is involved. Native SSE/history still own
478
+ // source-bound progress; an unknown child callback is only a hint.
479
+ return { observationOnly: true, daemonId: this.state.daemon_id, sessionId: params.sessionId,
480
+ messageId: payload.message_id, toolCallId: payload.tool_call_id, command: payload.input.command };
481
+ }
482
+ }
483
+ if (params.deferWorkStart === true && params.payload?.phase === "before") {
484
+ const pending = await this.dispatch("hook.register", params);
485
+ if (pending.fastAck === true) return pending;
486
+ }
487
+ const current = this.nativeHookOperation, sessionId = params.sessionId;
430
488
  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))
431
489
  throw new DaemonError("native_hook_unproven", "OpenCode hook has no exact active operation");
432
490
  let owned = sessionId === current.sessionId || this.sessions.get(sessionId)?.root_provider_session_id === current.sessionId;
433
491
  try {
492
+ const guard = current.guard ?? captureDispatchGuard(this); guard();
434
493
  const tree = await this.childTree(current.sessionId);
435
494
  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");
436
495
  owned = true;
@@ -440,29 +499,66 @@ export class Runtime {
440
499
  if (payload.input.workdir !== undefined && typeof payload.input.workdir !== "string") throw new DaemonError("native_hook_unproven", "OpenCode hook workdir must be a string");
441
500
  if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook identity observation");
442
501
  const accepted = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
443
- 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",
502
+ provider_session_id: sessionId, root_provider_session_id: current.sessionId, native_creator: 'unconfirmed', tool_call_id: `${payload.message_id}:${payload.tool_call_id}`, event: payload.phase === "after" ? "PostToolUse" : "PreToolUse",
444
503
  command_sha256: createHash("sha256").update(payload.input.command).digest("hex"), native_cwd: path.resolve(native.directory, payload.input.workdir ?? ".") }, current);
445
504
  if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook acceptance");
446
- current.requests.set(accepted.request_id, { current, binding: accepted }); return { nativeHookBinding: accepted, nativeSession: {
505
+ guard(); current.requests.set(accepted.request_id, { current, binding: accepted, guard }); return { nativeHookBinding: accepted, nativeSession: {
447
506
  id: native.id, directory: native.directory, parentID: native.parentID, projectID: native.projectID,
448
507
  model: { providerID: native.model?.providerID, id: native.model?.id, variant: native.model?.variant }
449
508
  } };
450
509
  } catch (error) {
451
- if (owned) this.retainHookFailure(current, error);
510
+ if (owned) await this.retainHookFailure(current, error, true);
452
511
  throw error;
453
512
  }
454
513
  }
455
514
  if (operation === "hook.verify") {
456
515
  const current = this.nativeHookOperation, binding = params.binding;
516
+ if (binding?.native_admission === "assigned_work_start") {
517
+ const retained = this.retainedWorkStarts?.get(params.requestId);
518
+ if (!retained || !isDeepStrictEqual(retained.binding, binding)) throw new DaemonError("native_hook_unproven", "Unknown native Work start callback");
519
+ retained.guard(); assertDaemonDispatch(this.paths.dir);
520
+ const root = binding.root_provider_session_id, id = binding.provider_session_id;
521
+ const report = operationProgressReporter();
522
+ const checkpoint = cursor => report('verification_progress', { native_request_id: cursor, session_id: id });
523
+ const tree = await this.childTree(root, new Set(), checkpoint), child = tree.find(node => node.id === id);
524
+ if (id !== root && (!child || child.fork)) throw new DaemonError("foreign_session", "Native Work start is outside its controlled tree");
525
+ const native = await this.client.inspect(id);
526
+ await checkpoint(`session:${id}:${createHash('sha256').update(JSON.stringify(native)).digest('hex')}`);
527
+ if (native.id !== id || native.fork || !path.isAbsolute(native.directory ?? "") || child && native.parentID !== child.parentID
528
+ || id === root && native.parentID) throw new DaemonError("native_hook_identity_conflict", "Native Work start ancestry is contradictory");
529
+ const input = binding.native_payload.input;
530
+ if (input.workdir !== undefined && typeof input.workdir !== "string") throw new DaemonError("native_hook_unproven", "Native Work start has invalid workdir");
531
+ retained.guard();
532
+ const cwd = path.resolve(native.directory, input.workdir ?? "."), parent = native.parentID ?? null;
533
+ const verified = { ...binding, parent_provider_session_id: parent, native_cwd: cwd, native_ancestry: "confirmed",
534
+ native_payload: { ...binding.native_payload, input: { ...input, workdir: cwd }, session: { provider_session_id: id, parent_provider_session_id: parent, directory: cwd },
535
+ profile: { provider: native.model?.providerID, model: native.model?.id, variant: native.model?.variant ?? "default" } } };
536
+ return { verified: true, registeredHookBinding: binding, nativeHookBinding: verified };
537
+ }
538
+ if (params.phase === "after") {
539
+ const retained = this.retainedHookRequests.get(binding?.request_id);
540
+ if (!retained || JSON.stringify(retained.binding) !== JSON.stringify(binding)) throw new DaemonError("native_hook_unproven", "OpenCode after hook has no retained original delivery");
541
+ assertDaemonDispatch(this.paths.dir); retained.guard();
542
+ return { current: true, retained: true };
543
+ }
457
544
  if (!current || binding?.operation_id !== current.operationId || binding?.turn_generation !== current.generation
458
545
  || 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");
546
+ const accepted = current.requests.get(binding.request_id); assertDaemonDispatch(this.paths.dir); accepted.guard();
547
+ if (params.retain_after === true && binding.event === "PreToolUse") this.retainedHookRequests.set(binding.request_id, { binding, guard: accepted.guard });
459
548
  return { current: true };
460
549
  }
461
550
  if (operation === "hook.failed") {
462
551
  if (params.failure) {
463
- await this.dispatch("hook.verify", { binding: params.binding });
464
552
  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");
465
- this.retainHookFailure(this.nativeHookOperation, reportedError(params.failure, "Native hook failed"));
553
+ const current = this.nativeHookOperation, binding = params.binding;
554
+ const isCurrent = current && binding?.operation_id === current.operationId && binding?.turn_generation === current.generation
555
+ && JSON.stringify(current.requests.get(binding?.request_id)?.binding) === JSON.stringify(binding);
556
+ await this.dispatch("hook.verify", isCurrent ? { binding } : { binding, phase: "after" });
557
+ // The caller's write failed. Do not acknowledge the fallback until the
558
+ // same owner-bound failure is durable; a late after never poisons the
559
+ // currently active, different prompt.
560
+ await writeNativeHookFailure(this.paths.dir, binding, reportedError(params.failure, "Native hook failed"));
561
+ if (!isCurrent) return { observed: true, retained: true };
466
562
  }
467
563
  await this.observeHookFailures(); return { observed: true };
468
564
  }
@@ -497,8 +593,9 @@ export class Runtime {
497
593
  if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
498
594
  if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
499
595
  this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
500
- const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null };
596
+ const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null, guard: assertDispatch };
501
597
  this.state.hook_generation = current.generation; this.nativeHookOperation = current;
598
+ this.lastNativeHookOperation = current;
502
599
  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; };
503
600
  let reply;
504
601
  try { assertDispatch(); reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
@@ -523,6 +620,7 @@ export class Runtime {
523
620
  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 });
524
621
  result.response_message_id = reply.info.id;
525
622
  result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
623
+ if (current.hookDiagnostics?.length) result.native_hook_diagnostics = current.hookDiagnostics;
526
624
  checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
527
625
  if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
528
626
  const info = await this.client.fork(params.sessionId, params.messageId);
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
3
  import { ToolObservations, openCodeToolObservation } from "./tool-observations.mjs";
4
4
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
5
- import { assertNativeLine } from "./native-streams.mjs";
6
5
  import { operationProgressReporter } from "./operation-progress.mjs";
7
6
  import { classifyRecoveryError as classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
8
7
 
@@ -98,7 +97,7 @@ export class OpenCodeClient {
98
97
  const options = { signal: controller.signal };
99
98
  const pending = input === undefined ? domain[name](options) : domain[name](input, options);
100
99
  void pending.catch(() => {}); await onDispatched?.(); const result = await pending;
101
- assertNativeLine(JSON.stringify(result ?? null)); return result;
100
+ return result;
102
101
  } catch (cause) {
103
102
  if (cause instanceof OpenCodeError || cause.code === "operation_observation_lost") throw cause;
104
103
  if (cause._tag === "SessionNotFoundError" || cause._tag === "NotFoundError") throw new OpenCodeError("session_not_found", "OpenCode native Session is unavailable");
@@ -329,9 +328,13 @@ export class OpenCodeClient {
329
328
  const child = (await childrenOf(id, visited)).find(child => child.id === item.info.metadata.childID);
330
329
  if (child) await observe(child.id, visited);
331
330
  }
332
- if (item.info.role === "user" || item.info.role === "synthetic" && !(
333
- item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) === id
334
- || item.info.metadata?.source === "shell" && shellCursors.has(item.info.metadata.shellID)))
331
+ // Native continuation messages are synthetic, including retries
332
+ // without metadata. Only actual user input crosses the owned
333
+ // operation boundary. Claimed child/shell delivery is validated
334
+ // separately before it can settle a background task.
335
+ if (item.info.role === "user" || item.info.role === "synthetic" && (
336
+ item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) !== id
337
+ || item.info.metadata?.source === "shell" && !shellCursors.has(item.info.metadata.shellID)))
335
338
  throw new OpenCodeError("opencode_foreign_input", "OpenCode received input outside the owned operation", false, { session_id: id, message_id: item.info.id, role: item.info.role, source: item.info.metadata?.source });
336
339
  }
337
340
  const terminal = rootMessages.slice(deliveredIndex + 1).findLast(item => item.info.role === "idle");
@@ -344,8 +347,8 @@ export class OpenCodeClient {
344
347
  // A background child's native completion notification is delivered
345
348
  // asynchronously. An idle parent before that delivery is not the
346
349
  // completion of the coordinating operation.
347
- const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent").map(item => item.info.metadata.childID));
348
- const shellNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "shell").map(item => item.info.metadata.shellID));
350
+ const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) === id).map(item => item.info.metadata.childID));
351
+ const shellNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "shell" && shellCursors.has(item.info.metadata.shellID)).map(item => item.info.metadata.shellID));
349
352
  unsettled ||= [...backgroundShells].some(shell => !shellNotifications.has(shell));
350
353
  for (const item of current) for (const part of item.parts) {
351
354
  if (part.tool === "subagent" && part.state?.input?.background === true) {
@@ -0,0 +1 @@
1
+ export function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;