@deksden-com/dd-flow-cli 0.9.0-beta.151 → 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.
- package/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -4
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +9 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -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
|
-
|
|
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 });
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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))
|
|
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>>;
|