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

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 (227) hide show
  1. package/CHANGELOG.md +31 -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 +274 -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 +115 -17
  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 +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  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-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -4,6 +4,7 @@ import { callDaemonObserved } from './daemon-observation.mjs';
4
4
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
5
5
  import { prepareDaemonRequest } from "./native-request-proof.mjs";
6
6
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
7
+ import { acceptNativeHookRequest } from "./native-hook-command.mjs";
7
8
  import { createHash, randomUUID } from "node:crypto";
8
9
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
9
10
  import net from "node:net";
@@ -138,18 +139,54 @@ export async function prepareCodexStop({ bridge, state, sessions, active, save,
138
139
 
139
140
  export async function serveDaemon(stateDir) {
140
141
  const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
141
- const bridge = new CodexBridge({ ...state.config, env: { ...codexBridgeEnv(state.config, state.daemon_id), CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot), log: paths.log }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
142
+ const bridge = new CodexBridge({ ...state.config, env: { ...codexBridgeEnv(state.config, state.daemon_id), CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot), log: paths.log }); const sessions = new Set(state.sessions ?? []); let active = null, activePrompt = null; const verifiedHookFacts = new Map(), rootTurnCreators = new Map(); let server; let persist = Promise.resolve();
142
143
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
143
144
  const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
144
145
  bridge.options.onNativeActivity = () => {
145
146
  state.native_execution_revision = (state.native_execution_revision ?? 0) + 1;
146
147
  if (state.active_tree !== true) void save({ active_tree: true }).catch(error => { bridge.rejectAll(error); });
147
148
  };
149
+ bridge.options.onOwnedRootTurn = (sessionId, turnId) => {
150
+ if (activePrompt?.sessionId !== sessionId) return;
151
+ const key = JSON.stringify([sessionId, turnId]), prior = rootTurnCreators.get(key);
152
+ if (prior && prior !== activePrompt) return;
153
+ activePrompt.turnGeneration = bridge.hookPromptGenerations?.get(sessionId) ?? 0;
154
+ rootTurnCreators.set(key, activePrompt);
155
+ };
148
156
  const dispatch = async (operation, params, operationId) => {
149
157
  const assertDispatch = captureDispatchGuard(bridge);
150
158
  if (operation === "hook.verify") {
151
159
  if (params.daemonId !== state.daemon_id) throw Object.assign(new Error("Hook daemon identity differs"), { code: "native_hook_unproven" });
152
- return { ...bridge.verifyNativeHook(params), daemonId: state.daemon_id, ddFlowHome: state.config.ddFlowHome, projectRoot: state.config.projectRoot };
160
+ if (params.requestId) {
161
+ assertDaemonDispatch(paths.dir);
162
+ const retained = verifiedHookFacts.get(params.requestId);
163
+ if (!retained || JSON.stringify(retained.binding) !== JSON.stringify(params.binding)) throw Object.assign(new Error("Hook fact is not bound to a verified native delivery"), { code: "native_hook_unproven" });
164
+ retained.guard();
165
+ return { ...retained.binding.codex_proof, verified: true, nativeHookBinding: retained.binding };
166
+ }
167
+ const proof = { ...bridge.verifyNativeHook(params), daemonId: state.daemon_id, ddFlowHome: state.config.ddFlowHome, projectRoot: state.config.projectRoot };
168
+ const childKey = proof.sessionId !== proof.rootSessionId && proof.creatorRootTurnId ? JSON.stringify([proof.rootSessionId, proof.creatorRootTurnId]) : null;
169
+ const current = proof.sessionId === proof.rootSessionId ? activePrompt : childKey ? rootTurnCreators.get(childKey) : null, payload = params.nativePayload, command = params.nativeCommand;
170
+ if (current && current.sessionId === proof.rootSessionId && typeof command === "string" && command && payload && typeof payload === "object"
171
+ && payload.session_id === proof.rootSessionId && (payload.agent_id ?? payload.session_id) === proof.sessionId
172
+ && payload.tool_use_id === proof.toolCallId && payload.turn_id === params.turnId) {
173
+ try {
174
+ const binding = await acceptNativeHookRequest(paths.dir, {
175
+ daemon_id: state.daemon_id, operation_id: current.operationId, turn_generation: current.turnGeneration ?? bridge.hookPromptGenerations?.get(proof.rootSessionId) ?? 0,
176
+ provider_session_id: proof.sessionId, root_provider_session_id: proof.rootSessionId,
177
+ parent_provider_session_id: proof.parentSessionId, tool_call_id: proof.toolCallId, event: "PreToolUse",
178
+ turn_id: params.turnId, native_cwd: typeof payload.cwd === "string" ? path.resolve(payload.cwd) : state.config.cwd,
179
+ command_sha256: createHash("sha256").update(command).digest("hex"), native_command: command,
180
+ source_harness: "codex-desktop", native_payload: payload, codex_proof: { ...proof }
181
+ }, current);
182
+ if (proof.sessionId === proof.rootSessionId ? activePrompt === current : childKey && rootTurnCreators.get(childKey) === current) { current.guard(); current.requests.set(binding.request_id, { binding }); verifiedHookFacts.set(binding.request_id, { binding, guard: current.guard }); proof.nativeHookBinding = binding; }
183
+ } catch (error) {
184
+ // Legacy hook still handles admission. This optional producer must
185
+ // not replace its result with a bookkeeping failure.
186
+ process.stderr.write(`${JSON.stringify({ kind: "native_hook_fact_unavailable", code: error.code ?? "hook_fact_write_failed" })}\n`);
187
+ }
188
+ }
189
+ return proof;
153
190
  }
154
191
  if (operation === "daemon.status") return { daemon_id: state.daemon_id, pid: process.pid, cwd: state.cwd, journal: state.journal, config: state.config, sessions: [...sessions], active_operation: active, active_tree: state.active_tree ?? null, shutdown_state: state.shutdown_state };
155
192
  // The runner may need to retire an orphaned daemon after its caller was
@@ -183,7 +220,10 @@ export async function serveDaemon(stateDir) {
183
220
  if (/:capacity:[1-9]\d*:session\.prompt$|:child:[a-f0-9]+:followup:[1-9]\d*$/.test(operationId ?? ""))
184
221
  await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
185
222
  sessions.add(params.sessionId);
186
- await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, operationId, assertDispatch });
223
+ await save({ active_operation: active });
224
+ const promptBinding = { operationId, sessionId: params.sessionId, requests: new Map(), guard: assertDispatch }; activePrompt = promptBinding;
225
+ try { return await promptSessionWithBridge(bridge, { ...state.config, ...params, operationId, assertDispatch }); }
226
+ finally { if (activePrompt === promptBinding) activePrompt = null; }
187
227
  }
188
228
  finally { active = null; await save({ active_operation: null }); }
189
229
  }
@@ -207,6 +207,7 @@ export class CodexBridge {
207
207
  this.currentTurns ??= new Map(); const id = message.params?.turn?.id, thread = message.params?.threadId;
208
208
  const pendingStart = [...this.pending.values()].find(value => value.method === "turn/start" && value.threadId === thread);
209
209
  const dispatching = pendingStart && !pendingStart.turnFloor?.has(id);
210
+ if (id && dispatching && this.requestedProfiles.has(thread)) this.options.onOwnedRootTurn?.(thread, id);
210
211
  const child = this.descendants.get(thread), parentCurrent = child?.parent_turn_id && child.parent_turn_id === this.currentTurns.get(child.parent_provider_session_id);
211
212
  if (id && this.turns.get(id)?.status !== "completed" && (dispatching || parentCurrent && child.status === "running" || !this.currentTurns.has(thread) && !this.requestedProfiles.has(thread))) {
212
213
  if (this.currentTurns.get(thread) !== id) this.usageMarkers?.delete(thread);
@@ -349,7 +350,20 @@ export class CodexBridge {
349
350
  || active.rootGeneration !== (this.hookPromptGenerations?.get(rootSessionId) ?? 0)) {
350
351
  fail("No matching active managed native hook", "native_hook_unproven", { provider_session_id: sessionId, tool_call_id: toolCallId });
351
352
  }
352
- return { rootSessionId, sessionId, toolCallId, parentSessionId: sessionId === rootSessionId ? null : this.descendants.get(sessionId)?.parent_provider_session_id };
353
+ let creatorRootTurnId = null;
354
+ if (sessionId !== rootSessionId) {
355
+ let child = sessionId; const visited = new Set();
356
+ while (child !== rootSessionId && !visited.has(child)) {
357
+ visited.add(child);
358
+ const edge = this.descendants.get(child), parent = edge?.parent_provider_session_id, turn = edge?.parent_turn_id;
359
+ if (!parent || !turn || this.creatorEdges?.get(child) !== JSON.stringify([parent, turn])) break;
360
+ if (parent === rootSessionId) { creatorRootTurnId = turn; break; }
361
+ const observed = this.turns.get(turn);
362
+ if (!observed || observed.threadId !== parent) break;
363
+ child = parent;
364
+ }
365
+ }
366
+ return { rootSessionId, sessionId, toolCallId, parentSessionId: sessionId === rootSessionId ? null : this.descendants.get(sessionId)?.parent_provider_session_id, ...(creatorRootTurnId ? { creatorRootTurnId } : {}) };
353
367
  }
354
368
 
355
369
  observeHookFailure(message) {
@@ -387,6 +401,16 @@ export class CodexBridge {
387
401
  catch (storage) { error.cleanup_error ??= errorRecord(storage); }
388
402
  return;
389
403
  }
404
+ if (["native_hook_timeout", "native_hook_failed"].includes(error.code)) {
405
+ // Codex reports a failed hook independently of the shell tool: exit 1
406
+ // does not prove that the real CLI failed or even that it did not run.
407
+ // Retain the observation without rejecting the productive Turn.
408
+ const diagnostic = { ...errorRecord(error), root_generation: active.rootGeneration, root_turn_id: active.rootTurnId };
409
+ this.hookDiagnostics ??= new Map();
410
+ const notes = this.hookDiagnostics.get(root) ?? []; notes.push(diagnostic); this.hookDiagnostics.set(root, notes);
411
+ try { void Promise.resolve(this.journal.write("native_hook_observation_failure", diagnostic)).catch(() => {}); } catch { /* optional diagnostic */ }
412
+ return;
413
+ }
390
414
  this.hookFailures.set(root, error);
391
415
  for (const [id, pending] of this.pending) {
392
416
  if (ancestors.has(pending.threadId) && pending.method !== "turn/interrupt") {
@@ -427,12 +451,18 @@ export class CodexBridge {
427
451
  if (child === parent || this.requestedProfiles.has(child) || previous?.parent_provider_session_id && previous.parent_provider_session_id !== parent) {
428
452
  fail("Conflicting native child parent identity", "native_topology_conflict", { child_session_id: child, parent_session_id: parent, retained_parent: previous?.parent_provider_session_id ?? null });
429
453
  }
454
+ const creator = this.creatorEdges?.get(child);
455
+ if (creator && facts.creator_turn_exact && facts.parent_turn_id && creator !== JSON.stringify([parent, facts.parent_turn_id])) {
456
+ fail("Conflicting native child creator Turn", "native_topology_conflict", { child_session_id: child, parent_session_id: parent, retained_turn: previous.parent_turn_id, observed_turn: facts.parent_turn_id });
457
+ }
430
458
  const seen = new Set([child]);
431
459
  for (let ancestor = parent; ancestor; ancestor = this.descendants.get(ancestor)?.parent_provider_session_id) {
432
460
  if (seen.has(ancestor)) fail("Native child ancestry contains a cycle", "native_topology_cycle", { child_session_id: child, parent_session_id: parent });
433
461
  seen.add(ancestor);
434
462
  }
435
- this.descendants.set(child, { ...previous, ...facts, parent_turn_id: facts.parent_turn_id ?? previous?.parent_turn_id ?? null, provider_session_id: child, parent_provider_session_id: parent });
463
+ if (facts.creator_turn_exact && facts.parent_turn_id) { this.creatorEdges ??= new Map(); this.creatorEdges.set(child, JSON.stringify([parent, facts.parent_turn_id])); }
464
+ const { creator_turn_exact: _sourceIsExact, ...publicFacts } = facts;
465
+ this.descendants.set(child, { ...previous, ...publicFacts, parent_turn_id: publicFacts.parent_turn_id ?? previous?.parent_turn_id ?? null, provider_session_id: child, parent_provider_session_id: parent });
436
466
  }
437
467
 
438
468
  // Collaboration child Threads are provider facts. Keep only objects that
@@ -462,7 +492,7 @@ export class CodexBridge {
462
492
  // Parent activity has no child Turn identity on some app-server builds.
463
493
  // It can discover a child, but cannot reopen or close a later known Turn.
464
494
  const status = previous?.active_turn_id || previous?.status === "completed" ? previous.status : observed;
465
- if (activity.kind === "started") this.bindChild(activity.agentThreadId, activityParent, { status, parent_turn_id: value.params?.turnId === this.currentTurns?.get(activityParent) ? value.params.turnId : null, agent_path: activity.agentPath ?? previous?.agent_path ?? null, source: "codex.app_server.subAgentActivity" });
495
+ if (activity.kind === "started") this.bindChild(activity.agentThreadId, activityParent, { status, parent_turn_id: value.params?.turnId === this.currentTurns?.get(activityParent) ? value.params.turnId : null, ...(value.params?.turnId && value.params.turnId === this.currentTurns?.get(activityParent) ? { creator_turn_exact: true } : {}), agent_path: activity.agentPath ?? previous?.agent_path ?? null, source: "codex.app_server.subAgentActivity" });
466
496
  else if (["completed", "interrupted"].includes(activity.kind) && activity.agentThreadId !== activityParent && !this.requestedProfiles.has(activity.agentThreadId)) {
467
497
  this.descendants.set(activity.agentThreadId, { ...previous, provider_session_id: activity.agentThreadId, status });
468
498
  }
@@ -474,6 +504,7 @@ export class CodexBridge {
474
504
  const previous = this.descendants.get(child);
475
505
  this.bindChild(child, value.senderThreadId, {
476
506
  parent_turn_id: parentTurn === this.currentTurns?.get(value.senderThreadId) ? parentTurn : null,
507
+ ...(parentTurn && parentTurn === this.currentTurns?.get(value.senderThreadId) ? { creator_turn_exact: true } : {}),
477
508
  status: previous?.status === "completed" || previous?.active_turn_id ? previous.status : value.agentsStates?.[child]?.status ?? previous?.status ?? "unknown", source: "codex.app_server" });
478
509
  }
479
510
  }
@@ -731,7 +762,8 @@ export async function promptSessionWithBridge(bridge, options) {
731
762
  fail("Codex completed but its current output/items remain unavailable", "operation_observation_lost", { history_error: history.error, provider_session_id: sessionId, turn_id: turnId });
732
763
  if (!completeHistory && (options.model && !thread.model || options.reasoning && !thread.reasoningEffort))
733
764
  fail("Codex completed but its applied profile remains unavailable", "operation_observation_lost", { history_error: history.error, provider_session_id: sessionId, turn_id: turnId });
734
- const result = { ...nativeOutcome, ...(thread ? { thread: threadReceipt(thread) } : {}), descendants: await observedChildren(bridge, sessionId, phase), ...(assistantText ? { assistant_text: assistantText } : {}), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread?.model ?? null, reasoning: thread?.reasoningEffort ?? null, provider: thread?.modelProvider ?? null }, "thread/read.after_turn") };
765
+ const diagnostics = (bridge.hookDiagnostics?.get(sessionId) ?? []).filter(item => item.root_generation === bridge.hookPromptGenerations?.get(sessionId));
766
+ const result = { ...nativeOutcome, ...(thread ? { thread: threadReceipt(thread) } : {}), descendants: await observedChildren(bridge, sessionId, phase), ...(assistantText ? { assistant_text: assistantText } : {}), ...(diagnostics.length ? { native_hook_diagnostics: diagnostics } : {}), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread?.model ?? null, reasoning: thread?.reasoningEffort ?? null, provider: thread?.modelProvider ?? null }, "thread/read.after_turn") };
735
767
  await bridge.journal?.flush?.();
736
768
  return result;
737
769
  } catch (error) {
@@ -16,6 +16,24 @@ import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, confirmD
16
16
 
17
17
  const REQUEST = "dd-droid/daemon-request@1", RESPONSE = "dd-droid/daemon-response@1";
18
18
  async function removeSocket(file) { try { if (!(await lstat(file)).isSocket()) throw new DroidError("unsafe_socket", "Refusing to replace non-socket daemon path"); await unlink(file); } catch (error) { if (error.code !== "ENOENT") throw error; } }
19
+
20
+ /** Capture native execution settlement before resource retirement. Explicit
21
+ * cancellation still owns the physical stop needed to settle a live writer. */
22
+ export async function prepareDroidStop(runtime, { cancelTree, busy, stateDir, save }) {
23
+ if (!cancelTree) {
24
+ if (busy) throw new DroidError("tree_not_settled", "Droid still owns unfinished work");
25
+ const observed = runtime.rootId ? await runtime.inspect(runtime.rootId) : null;
26
+ if (observed && (observed.settled !== true || observed.native_settled !== true)) throw new DroidError("tree_not_settled", "Droid native execution settlement is unproven");
27
+ commitDaemonShutdown(stateDir);
28
+ await save({ active_tree: false });
29
+ return { ...(observed ?? {}), clean: true, settled: true, native_settled: true };
30
+ }
31
+ commitDaemonShutdown(stateDir);
32
+ const cleanup = await runtime.closeTree(true);
33
+ if (runtime.rootId) try { await runtime.inspect(runtime.rootId, { ignoreActive: true }); } catch (error) { cleanup.inspection_error = errorRecord(error); }
34
+ const clean = cleanup.native_settled === true && !cleanup.inspection_error;
35
+ return { clean, settled: cleanup.native_settled === true, native_settled: cleanup.native_settled === true, physical_stopped: cleanup.physical_stopped, cleanup };
36
+ }
19
37
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, operationId = process.env.DD_EVAL_OPERATION_ID ?? randomUUID()) {
20
38
  const request = prepareDaemonRequest({ schema_id: REQUEST, id: operationId, operation, params });
21
39
  return await callDaemonObserved({ stateDir, socket: droidPaths(stateDir).socket, request, responseSchema: RESPONSE, harness: 'droid', timeoutMs });
@@ -61,6 +79,7 @@ export async function serveDaemon(stateDir) {
61
79
  const params = request.params ?? {}, operation = request.operation;
62
80
  if (operation === "daemon.status") { let observationError; try { await runtime.refreshTopology(); } catch (error) { observationError = errorRecord(error); } return { observation_error: observationError ?? null, daemon_id: state.daemon_id, pid: process.pid, config: state.config, shutdown_state: state.shutdown_state, provider_ready: Boolean(runtime.child && !runtime.closed), active_operation: busy, active_tree: Boolean(runtime.active || runtime.workingState !== "idle" || runtime.topologyIncomplete || [...runtime.topology.values()].some(child => !["completed", "cancelled", "failed"].includes(child.status))), sessions: runtime.rootId ? [runtime.rootId] : [], provider_pid: runtime.closed ? null : runtime.child?.pid }; }
63
81
  if (operation === "hook.observe") return await runtime.observeHook(params.payload, params.eventId, params.deadlineAt);
82
+ if (operation === "hook.verify") { assertDaemonDispatch(paths.dir); return await runtime.verifyWorkStart(params); }
64
83
  if (operation === "session.inspect") return await runtime.inspect(params.sessionId, { ingestUsage: params.noFlow !== true });
65
84
  if (operation === "session.cancel") {
66
85
  runtime.requireIdentity(params.sessionId); const cleanup = await runtime.closeTree(true); let inspected;
@@ -71,17 +90,12 @@ export async function serveDaemon(stateDir) {
71
90
  if (operation === "daemon.stop") {
72
91
  stopping = true;
73
92
  try {
74
- return await completeDaemonShutdown(paths.dir, state, save, async () => {
75
- if (!params.cancelTree && (busy || (runtime.rootId && !(await runtime.inspect(runtime.rootId)).settled))) throw new DroidError("tree_not_settled", "Droid still owns unfinished work");
76
- commitDaemonShutdown(paths.dir);
77
- const cleanup = await runtime.closeTree(params.cancelTree === true);
78
- if (runtime.rootId) try { await runtime.inspect(runtime.rootId, { ignoreActive: true }); } catch (error) { cleanup.inspection_error = errorRecord(error); }
79
- // Native evidence failures remain visible, but cannot strand copied
80
- // credentials or the daemon after the owned process tree is gone.
81
- const clean = cleanup.native_settled === true && !cleanup.inspection_error;
82
- return { clean, settled: cleanup.native_settled === true, native_settled: cleanup.native_settled === true, physical_stopped: cleanup.physical_stopped, cleanup };
83
- }, [
84
- ["provider", async () => { if (!runtime.closed) throw new DroidError("provider_process_stop_incomplete", "Droid provider has not closed"); }],
93
+ return await completeDaemonShutdown(paths.dir, state, save,
94
+ () => prepareDroidStop(runtime, { cancelTree: params.cancelTree === true, busy, stateDir: paths.dir, save }), [
95
+ ["provider", async () => {
96
+ if (!runtime.closed) await runtime.closeTree(false);
97
+ if (!runtime.closed) throw new DroidError("provider_process_stop_incomplete", "Droid provider has not closed");
98
+ }],
85
99
  ["auth", () => removeDroidAuth(state.config)],
86
100
  ["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
87
101
  ]);
@@ -11,8 +11,11 @@ import path from "node:path";
11
11
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
12
12
  import { operationProgressReporter } from "./operation-progress.mjs";
13
13
  import { writeJsonAtomic } from "./runner-events.mjs";
14
- import { errorRecord, reportedError, isObservationLoss, isProvenShellCorrection } from "./operation-errors.mjs";
15
- import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
14
+ import { errorRecord, reportedError, isObservationLoss, isNativeHookObservationLoss, isProvenShellCorrection } from "./operation-errors.mjs";
15
+ import { hookBudget, invokeNativeHook, retainNativeWorkStart, isStandaloneWorkStart } from "./native-hook-command.mjs";
16
+ import { assertDaemonDispatch } from "./daemon-operations.mjs";
17
+ import { isDeepStrictEqual } from "node:util";
18
+ import { parseLifecycleCommand } from "./lifecycle-command.mjs";
16
19
  import { checkObservedProfile, observeModel } from "./model-observations.mjs";
17
20
  import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
18
21
  import { commandText } from "./process-json.mjs";
@@ -116,7 +119,7 @@ function descendants(rows, rootPid) { const ids = new Set([rootPid]); for (;;) {
116
119
 
117
120
  export class DroidRuntime {
118
121
  constructor(config, state, persist) {
119
- this.config = config; this.state = state; this.paths = droidPaths(config.stateDir); this.persist = persist; this.pending = new Map(); this.lateResponses = new Map(); this.active = null; this.child = null; this.closed = true; this.loaded = false; this.ownedProcesses = state.owned_processes ?? []; this.closing = false; this.workingState = "idle"; this.protocol = null; this.rootId = state.root_session_id ?? null; this.topology = new Map(state.descendants?.map(child => [child.provider_session_id, child]) ?? []); this.draining = Promise.resolve(); this.journalling = Promise.resolve(); this.activity = 0; this.providerProcess = null; this.cancelled = false; this.lastResult = state.last_result ?? null; this.metadata = metadataReader(this.paths.factory); this.fatalError = state.fatal_error ? reportedError(state.fatal_error) : null;
122
+ this.config = config; this.state = state; this.paths = droidPaths(config.stateDir); this.persist = persist; this.pending = new Map(); this.lateResponses = new Map(); this.active = null; this.child = null; this.closed = true; this.loaded = false; this.ownedProcesses = state.owned_processes ?? []; this.closing = false; this.workingState = "idle"; this.protocol = null; this.rootId = state.root_session_id ?? null; this.topology = new Map(state.descendants?.map(child => [child.provider_session_id, child]) ?? []); this.nativeCreatorTurns = new Map(); this.draining = Promise.resolve(); this.journalling = Promise.resolve(); this.activity = 0; this.providerProcess = null; this.cancelled = false; this.lastResult = state.last_result ?? null; this.metadata = metadataReader(this.paths.factory); this.fatalError = state.fatal_error ? reportedError(state.fatal_error) : null;
120
123
  }
121
124
  async journal(kind, payload) { const line = JSON.stringify({ schema_id: "dd-droid/event@1", observed_at: new Date().toISOString(), kind, ...payload }); this.journalling = this.journalling.then(() => appendFile(this.config.journal, `${line}\n`, { mode: 0o600 })); return await this.journalling; }
122
125
  async save(patch = {}) { return await this.persist({ root_session_id: this.rootId, sessions: this.rootId ? [this.rootId] : [], descendants: [...this.topology.values()], working_state: this.workingState, last_activity: this.activity, ...patch }); }
@@ -169,7 +172,9 @@ export class DroidRuntime {
169
172
  const fingerprint = createHash("sha256").update(JSON.stringify(event.params)).digest("hex");
170
173
  if (sessionId && this.rootId && sessionId !== this.rootId && !this.topology.has(sessionId)) throw new DroidError("session_identity_mismatch", "Unrelated native Session notification");
171
174
  const current = this.active;
172
- const scoped = current && sessionId && (sessionId === this.rootId || this.topology.get(sessionId)?.operation_id === current.operationId)
175
+ let currentGuarded = Boolean(current && typeof current.guard === "function");
176
+ try { if (currentGuarded) current.guard(); } catch { currentGuarded = false; }
177
+ const scoped = currentGuarded && sessionId && (sessionId === this.rootId || this.topology.get(sessionId)?.operation_id === current.operationId)
173
178
  && (!n.turnId || n.turnId === current.turnId) && (!n.requestId || n.requestId === current.requestId);
174
179
  let productive = ["assistant_text_delta", "assistant_thought_delta"].includes(n.type) && Boolean(n.textDelta ?? n.thoughtDelta)
175
180
  || ["agent_turn_completed", "create_message"].includes(n.type);
@@ -209,7 +214,28 @@ export class DroidRuntime {
209
214
  if (n.type === "droid_working_state_changed" && (!sessionId || sessionId === this.rootId)) this.workingState = n.newState;
210
215
  if (n.type === "child_session_available") {
211
216
  if (!this.rootId || sessionId !== this.rootId || !n.childSessionId || !n.toolUseId) throw new DroidError("droid_child_identity_invalid", "Native child event lacks physical parent or tool identity");
212
- this.topology.set(n.childSessionId, { provider_session_id: n.childSessionId, parent_provider_session_id: sessionId, tool_use_id: n.toolUseId, operation_id: current?.operationId ?? null, status: "running", source: "droid.child_session_available" }); await this.save();
217
+ // A notification may arrive after another prompt starts. Session and
218
+ // child identity alone do not identify the originating operation.
219
+ const byTurn = typeof n.turnId === "string" && n.turnId ? this.nativeCreatorTurns.get(`turn:${n.turnId}`) : null;
220
+ const byRequest = typeof n.requestId === "string" && n.requestId ? this.nativeCreatorTurns.get(`request:${n.requestId}`) : null;
221
+ const candidate = byTurn ?? byRequest ?? current;
222
+ const exact = candidate && (typeof n.turnId === "string" && n.turnId && n.turnId === candidate.turnId
223
+ || typeof n.requestId === "string" && n.requestId && n.requestId === candidate.requestId)
224
+ && (!n.turnId || n.turnId === candidate.turnId) && (!n.requestId || n.requestId === candidate.requestId)
225
+ && (!byTurn || !byRequest || byTurn === byRequest);
226
+ const previous = this.topology.get(n.childSessionId), sameTool = previous?.tool_use_id === n.toolUseId;
227
+ if (previous && ((previous.parent_provider_session_id && previous.parent_provider_session_id !== sessionId)
228
+ || previous.tool_use_id && !sameTool))
229
+ throw new DroidError("droid_child_identity_invalid", "Native child parent or creation tool contradicts the retained identity");
230
+ const retainedCreator = sameTool ? previous.creator_operation_id ?? previous.operation_id : null;
231
+ if (retainedCreator && exact && retainedCreator !== candidate.operationId)
232
+ throw new DroidError("droid_child_identity_invalid", "Native child creator contradicts the retained operation");
233
+ let proven = Boolean(exact && typeof candidate.guard === "function");
234
+ try { if (proven) candidate.guard(); } catch { proven = false; }
235
+ const creator = retainedCreator || (proven ? candidate.operationId : null);
236
+ this.topology.set(n.childSessionId, { ...(previous ?? {}), provider_session_id: n.childSessionId, parent_provider_session_id: sessionId, tool_use_id: n.toolUseId,
237
+ creator_operation_id: creator, operation_id: previous?.latest_tool_use_id && previous.latest_tool_use_id !== n.toolUseId ? null : creator,
238
+ status: "running", source: "droid.child_session_available" }); await this.save();
213
239
  }
214
240
  if (n.type === "settings_updated" && this.loaded) await this.observeSettings(sessionId ?? this.rootId, n.settings, { kind: "native.settings_updated", event_sha256: fingerprint });
215
241
  if (n.type === "create_message" && n.requestId === current?.requestId) {
@@ -354,8 +380,11 @@ export class DroidRuntime {
354
380
  const before = await this.inspect(id); if (!before.settled) throw new DroidError("tree_not_settled", "Prior Droid tree has not settled");
355
381
  let resolve, reject; const completed = new Promise((a, b) => { resolve = a; reject = b; }); void completed.catch(() => {});
356
382
  const requestId = randomUUID(), turnId = randomUUID();
357
- const current = { operationId, requestId, turnId, resolve, reject, text: "", publish: operationProgressReporter(), saved: { operation: "session.prompt", provider_session_id: id, request_id: requestId, turn_id: turnId, accepted: false } };
383
+ const current = { operationId, requestId, turnId, sessionId: id, generation: (this.state.hook_generation ?? 0) + 1, resolve, reject, guard: assertDispatch, text: "", publish: operationProgressReporter(), saved: { operation: "session.prompt", provider_session_id: id, request_id: requestId, turn_id: turnId, accepted: false } };
384
+ this.state.hook_generation = current.generation; this.lastNativeHookOperation = current;
358
385
  this.active = current; this.cancelled = false; await this.saveBinding(current); await this.save({ active_tree: true });
386
+ const creator = { operationId, turnId, requestId, guard: assertDispatch };
387
+ this.nativeCreatorTurns.set(`turn:${turnId}`, creator); this.nativeCreatorTurns.set(`request:${requestId}`, creator);
359
388
  const clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal("observation_gap", { gap }).catch(() => {}); } });
360
389
  let checkingTerminal = false;
361
390
  const timer = setInterval(() => {
@@ -387,11 +416,11 @@ export class DroidRuntime {
387
416
  // A terminal native outcome is durable independently of a later
388
417
  // filesystem/topology observation. Preserve it and let settlement
389
418
  // reconcile; do not convert it into a failed prompt.
390
- return { provider_session_id: id, adapter_session_id: id, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId, settled: false, settlement_observation_error: errorRecord(error) };
419
+ return { provider_session_id: id, adapter_session_id: id, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId, settled: false, settlement_observation_error: errorRecord(error), ...(current.hookDiagnostics?.length ? { native_hook_diagnostics: current.hookDiagnostics } : {}) };
391
420
  }
392
421
  const providerFailure = droidTerminalFailure(outcome, id, receipt.settled);
393
422
  if (providerFailure) throw providerFailure;
394
- return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
423
+ return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId, ...(current.hookDiagnostics?.length ? { native_hook_diagnostics: current.hookDiagnostics } : {}) };
395
424
  } catch (error) {
396
425
  if (current.outcome && isObservationLoss(error)) error = Object.assign(new Error("Droid completed natively but its dispatch acknowledgement was lost", { cause: error }), {
397
426
  code: "native_outcome_observation_failed", details: { native_outcome: { provider_session_id: id, turn_id: turnId, result: current.outcome }, settled: false, observation_error: errorRecord(error) }
@@ -508,7 +537,14 @@ export class DroidRuntime {
508
537
  ...(exactTurn ? { terminal_turn_id: nativeTurn } : {}), native_source: "droid.agent_turn_outcome",
509
538
  native_turn_items: { observed: native.complete && native.tool_calls.completeness === "complete", possible_effects: native.tool_calls.total > 0, pending: native.tool_calls.pending > 0 },
510
539
  ...(exactTurn && outcome.error && typeof outcome.error === "object" ? { provider_error: safeProviderError(outcome.error) } : {}) } : {};
511
- this.topology.set(id, { provider_session_id: id, parent_provider_session_id: parentId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId, task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: running ? "running" : status(latest.status), source: "droid.task_invocations", transcript_path: native.transcript_path, ...evidence });
540
+ const previous = this.topology.get(id);
541
+ if (previous && (previous.parent_provider_session_id && previous.parent_provider_session_id !== parentId
542
+ || previous.tool_use_id && previous.tool_use_id !== creationTool))
543
+ throw new DroidError("droid_child_identity_invalid", "Native task inventory contradicts the retained child creation edge");
544
+ const creator = previous?.creator_operation_id ?? (previous?.latest_tool_use_id && previous.latest_tool_use_id !== creationTool ? null : previous?.operation_id) ?? null;
545
+ this.topology.set(id, { provider_session_id: id, parent_provider_session_id: parentId, tool_use_id: creationTool, latest_tool_use_id: latest.parentToolUseId,
546
+ creator_operation_id: creator, operation_id: latest.parentToolUseId === creationTool ? creator : null,
547
+ task_id: latest.taskInvocationId, task_invocation_ids: ordered.map(task => task.taskInvocationId), status: running ? "running" : status(latest.status), source: "droid.task_invocations", transcript_path: native.transcript_path, ...evidence });
512
548
  }
513
549
  return [...this.topology.values()];
514
550
  }
@@ -530,16 +566,82 @@ export class DroidRuntime {
530
566
  const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId, { ingest: ingestUsage }));
531
567
  const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && source?.tool_calls.pending === 0 && children.every(child => terminal(child.status) && child.native_turn_items?.observed === true && child.native_turn_items.pending === false);
532
568
  const rootSettled = source?.complete && source.tool_calls.completeness === "complete" && source.tool_calls.pending === 0 && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && terminal((source?.outcomes?.at(-1) ?? this.lastResult)?.reason);
533
- return nativeExecutionReceipt({ harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, ...settledNativeTree(id, children, settled, rootSettled && !this.topologyIncomplete), terminal_turn_id: source?.outcomes?.at(-1)?.turnId ?? this.lastResult?.turnId, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null,
569
+ const neverPrompted = source?.complete && !this.active && !this.lastResult && source.records.every(record => record.type === "session_start");
570
+ const nativeSettled = Boolean(settled && (rootSettled || neverPrompted));
571
+ return nativeExecutionReceipt({ harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, native_settled: nativeSettled, ...settledNativeTree(id, children, settled, rootSettled && !this.topologyIncomplete), terminal_turn_id: source?.outcomes?.at(-1)?.turnId ?? this.lastResult?.turnId, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null,
534
572
  ...(this.topologyIncomplete ? { native_settlement: { confirmed: false, state: "unproven", reason: "native_task_inventory_incomplete" } } : {}) }, { daemonId: this.config.daemonId });
535
573
  }
536
574
  async observeHook(payload, eventId, deadlineAt = null) {
575
+ if (!this.config.noFlow && this.config.runtime_owner && this.config.ddFlowBin && this.rootId
576
+ && ["PreToolUse", "PostToolUse"].includes(payload.hook_event_name)
577
+ && typeof payload.tool_name === "string" && payload.tool_name && typeof payload.session_id === "string" && payload.session_id
578
+ && typeof eventId === "string" && eventId) {
579
+ if (payload.tool_name !== "Execute") return {}; // Read/Edit/etc. never carry lifecycle commands.
580
+ const analysis = typeof payload.tool_input?.command === "string" ? parseLifecycleCommand(payload.tool_input.command) : null;
581
+ // No RUN/Work receipt or native ancestry is needed for ordinary shell.
582
+ // Real notification progress remains source-bound in observe().
583
+ if (analysis?.participation === "none" && !analysis.unresolved) return {};
584
+ }
585
+ if (payload.hook_event_name === "PreToolUse" && payload.tool_name === "Execute" && this.config.runtime_owner && this.config.ddFlowBin
586
+ && typeof payload.session_id === "string" && payload.session_id && typeof eventId === "string" && eventId && this.rootId
587
+ && isStandaloneWorkStart(payload.tool_input?.command)) {
588
+ assertDaemonDispatch(this.paths.dir);
589
+ this.retainedWorkStarts ??= new Map();
590
+ const command = payload.tool_input?.command;
591
+ const accepted = await retainNativeWorkStart(this.paths.dir, { daemon_id: this.config.daemonId, source_harness: "droid-cli",
592
+ root_provider_session_id: this.rootId, provider_session_id: payload.session_id, parent_provider_session_id: null,
593
+ tool_call_id: payload.tool_use_id ?? eventId, native_tool_identity: payload.tool_use_id ? "native" : "callback_request",
594
+ event: "PreToolUse", native_command: command, native_cwd: payload.cwd ?? null,
595
+ native_payload: { schema_id: "dd-flow/droid-tool-event@1", phase: "before", event_id: eventId, tool_call_id: payload.tool_use_id ?? eventId,
596
+ daemon_id: this.config.daemonId, session: { provider_session_id: payload.session_id }, tool: "Execute", input: payload.tool_input,
597
+ transcript_path: payload.transcript_path, callback_cwd: payload.cwd } }, this.active ?? this.lastNativeHookOperation, this.retainedWorkStarts);
598
+ if (accepted) return {};
599
+ }
537
600
  try { return await this.observeHookChecked(payload, eventId, hookBudget(23_000, deadlineAt)); }
538
601
  catch (error) { if (error.details?.scope !== "foreign_hook") await this.handleObservationError(error); throw error; }
539
602
  }
603
+ async verifyWorkStart(params) {
604
+ const retained = this.retainedWorkStarts?.get(params.requestId);
605
+ if (!retained || !isDeepStrictEqual(retained.binding, params.binding)) throw new DroidError("native_hook_unproven", "Unknown native Work start callback");
606
+ retained.guard();
607
+ const binding = retained.binding, id = binding.provider_session_id, payload = binding.native_payload;
608
+ if (binding.root_provider_session_id !== this.rootId) throw new DroidError("droid_hook_identity_invalid", "Native Work start root changed");
609
+ const report = operationProgressReporter();
610
+ const checkpoint = cursor => report('verification_progress', { native_request_id: cursor, session_id: id });
611
+ const native = await this.metadata.read(id, checkpoint);
612
+ if (!native || !path.isAbsolute(payload.callback_cwd ?? "") || path.resolve(payload.callback_cwd) !== this.config.cwd
613
+ || path.resolve(native.header.cwd) !== this.config.cwd || await realpath(payload.transcript_path) !== await realpath(native.transcript_path))
614
+ throw new DroidError("droid_hook_identity_invalid", "Native Work start has no verified physical Session/workspace");
615
+ const parent = native.header.callingSessionId ?? null, seen = new Set([id]);
616
+ let ancestor = parent;
617
+ while (ancestor && ancestor !== this.rootId) {
618
+ if (seen.has(ancestor)) throw new DroidError("droid_hook_identity_invalid", "Native Work start ancestry contains a cycle");
619
+ seen.add(ancestor);
620
+ const owner = await this.metadata.read(ancestor, checkpoint);
621
+ if (!owner || path.resolve(owner.header.cwd) !== this.config.cwd) throw new DroidError("droid_hook_identity_invalid", "Native Work start ancestry is unproven");
622
+ ancestor = owner.header.callingSessionId ?? null;
623
+ }
624
+ if (id !== this.rootId && ancestor !== this.rootId || id === this.rootId && parent)
625
+ throw new DroidError("droid_hook_identity_invalid", "Native Work start belongs to another root");
626
+ const previous = this.topology.get(id);
627
+ if (previous && (previous.parent_provider_session_id !== parent || previous.tool_use_id && previous.tool_use_id !== native.header.callingToolUseId))
628
+ throw new DroidError("droid_hook_identity_invalid", "Native Work start contradicts retained ancestry");
629
+ const profile = await this.observeSettings(id, native.settings, { path: native.transcript_path, sha256: native.source_sha256 ?? native.source?.sha256 });
630
+ retained.guard();
631
+ return { verified: true, registeredHookBinding: binding, nativeHookBinding: { ...binding, parent_provider_session_id: parent,
632
+ native_ancestry: "confirmed", native_payload: { ...payload, session: { provider_session_id: id, parent_provider_session_id: parent, directory: this.config.cwd }, profile } } };
633
+ }
540
634
  async observeHookChecked(payload, eventId, budget = hookBudget(23_000)) {
541
635
  const id = payload.session_id, event = payload.hook_event_name;
542
636
  if (!id || !event || !eventId) throw new DroidError("droid_hook_invalid", "Hook lacks native identity or event ID");
637
+ // SessionStart can be invoked synchronously by initialize/load before the
638
+ // provider creates its transcript. Acknowledge only the outstanding owned
639
+ // root request; this is not evidence that initialization succeeded.
640
+ const initPending = [...this.pending.values()].some(item => ["droid.initialize_session", "droid.load_session"].includes(item.method));
641
+ if (event === "SessionStart" && initPending && id === this.rootId && typeof payload.cwd === "string" && path.resolve(payload.cwd) === this.config.cwd && typeof payload.transcript_path === "string") {
642
+ const sessions = path.join(this.paths.factory, "sessions"), transcript = path.resolve(payload.transcript_path), relative = path.relative(sessions, transcript);
643
+ if (path.isAbsolute(payload.transcript_path) && relative && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative) && path.basename(transcript) === `${id}.jsonl`) return {};
644
+ }
543
645
  // Hooks can precede the root initialize response or the child notification.
544
646
  const deadline = Date.now() + 3_000; let native;
545
647
  do { native = ["PreToolUse", "PostToolUse"].includes(event) ? await this.metadata.read(id) : await readDroidSession(this.paths.factory, id); if (native) break; await sleep(25); } while (Date.now() < deadline);
@@ -550,7 +652,11 @@ export class DroidRuntime {
550
652
  if (parent && (id === parent || this.topology.has(id) && this.topology.get(id).parent_provider_session_id !== parent)) throw foreign("Hook child ancestry changed");
551
653
  if (!parent && this.rootId && id !== this.rootId) throw foreign("Hook came from an unrelated root");
552
654
  if (path.resolve(native.header.cwd) !== this.config.cwd) throw new DroidError("droid_hook_identity_invalid", "Owned native Session workspace changed");
553
- if (parent && !terminal(this.topology.get(id)?.status)) this.topology.set(id, { ...(this.topology.get(id) ?? {}), provider_session_id: id, parent_provider_session_id: parent, tool_use_id: native.header.callingToolUseId, status: "running", source: "droid.session_start" });
655
+ if (parent && !terminal(this.topology.get(id)?.status)) {
656
+ const previous = this.topology.get(id), creationTool = native.header.callingToolUseId;
657
+ if (previous?.tool_use_id && previous.tool_use_id !== creationTool) throw foreign("Hook child creation tool contradicts retained native identity");
658
+ this.topology.set(id, { ...(previous ?? {}), provider_session_id: id, parent_provider_session_id: parent, tool_use_id: creationTool, status: "running", source: "droid.session_start" });
659
+ }
554
660
  this.activity += 1; await this.journal("hook", { event_id: eventId, event, session_id: id, parent_session_id: parent }); await this.save();
555
661
  if (!["PreToolUse", "PostToolUse"].includes(event)) return {};
556
662
  if (this.fatalError) return { continue: false, stopReason: this.fatalError.message };
@@ -568,6 +674,13 @@ export class DroidRuntime {
568
674
  await this.save();
569
675
  return { continue: false, stopReason: error.message };
570
676
  }
677
+ if (isNativeHookObservationLoss(error)) {
678
+ // A lost hook observation does not establish the tool or CLI outcome.
679
+ // Do not authorize this pre-tool request, but do not poison the Turn.
680
+ if (this.active) (this.active.hookDiagnostics ??= []).push(errorRecord(error));
681
+ try { await this.journal("native_hook_observation_unavailable", { event_id: eventId, session_id: id, error: errorRecord(error) }); } catch { /* optional diagnostic */ }
682
+ return event === "PreToolUse" ? { continue: false, stopReason: "Lifecycle observation unavailable; retry the command." } : {};
683
+ }
571
684
  // Identity is proven above; only this owned admission can fail its turn.
572
685
  this.fatalError ??= Object.assign(error, { details: { ...error.details, provider_session_id: id, event_id: eventId, effect: error.details?.effect ?? "unknown", native_turn_items: { observed: false, possible_effects: true, pending: true } } });
573
686
  this.rejectPending(this.fatalError);
@@ -0,0 +1 @@
1
+ export function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;