@deksden-com/dd-flow-cli 0.9.0-beta.100 → 0.9.0-beta.104

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 (47) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli/hook-ingress.js +5 -10
  3. package/dist/cli/run-cli.js +9 -3
  4. package/dist/harness-runtime/bin/dd-agy.mjs +20 -7
  5. package/dist/harness-runtime/bin/dd-droid.mjs +2 -2
  6. package/dist/harness-runtime/bin/dd-grok.mjs +4 -4
  7. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  8. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  9. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  10. package/dist/harness-runtime/lib/daemon-operations.mjs +13 -2
  11. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  12. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +89 -36
  13. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -4
  14. package/dist/harness-runtime/lib/dd-codex.mjs +1 -0
  15. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
  16. package/dist/harness-runtime/lib/dd-droid.mjs +24 -17
  17. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +107 -20
  18. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +5 -3
  19. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +6 -10
  20. package/dist/harness-runtime/lib/dd-zcode.mjs +63 -26
  21. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
  22. package/dist/harness-runtime/lib/delegation-instructions.mjs +6 -1
  23. package/dist/harness-runtime/lib/managed-daemon.mjs +38 -4
  24. package/dist/harness-runtime/lib/native-hook-command.mjs +87 -13
  25. package/dist/harness-runtime/lib/process-json.mjs +3 -2
  26. package/dist/services/code-checks.js +53 -16
  27. package/dist/services/controller-fanout.js +37 -6
  28. package/dist/services/eval-snapshots.js +148 -38
  29. package/dist/services/external-work-launch.js +60 -5
  30. package/dist/services/prompts.js +2 -0
  31. package/dist/services/review-copy.js +136 -0
  32. package/dist/services/run-control-worker.js +16 -11
  33. package/dist/services/run-controller-adapter.js +9 -0
  34. package/dist/services/runs.js +3 -0
  35. package/dist/services/runtime-scope-stop.js +66 -6
  36. package/dist/services/vnext-code-review.js +6 -2
  37. package/dist/services/vnext-code.js +1 -0
  38. package/dist/services/vnext-fanout.js +9 -1
  39. package/dist/services/vnext-merge.js +8 -4
  40. package/dist/services/vnext-plan-review.js +4 -15
  41. package/dist/services/vnext-plan.js +4 -0
  42. package/dist/services/vnext-protocolize.js +4 -2
  43. package/dist/services/vnext-specify.js +7 -0
  44. package/dist/services/work-registry.js +208 -22
  45. package/dist/services/workspace-files.js +83 -0
  46. package/dist/services/worktrees.js +3 -1
  47. package/package.json +1 -1
@@ -62,7 +62,7 @@ export async function serveDaemon(stateDir) {
62
62
  const dispatch = async request => {
63
63
  const params = request.params ?? {}, operation = request.operation;
64
64
  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 }; }
65
- if (operation === "hook.observe") return await runtime.observeHook(params.payload, params.eventId);
65
+ if (operation === "hook.observe") return await runtime.observeHook(params.payload, params.eventId, params.deadlineAt);
66
66
  if (operation === "session.inspect") return await runtime.inspect(params.sessionId, { ingestUsage: params.noFlow !== true });
67
67
  if (operation === "session.cancel") {
68
68
  runtime.requireIdentity(params.sessionId); const cleanup = await runtime.closeTree(true); let inspected;
@@ -11,6 +11,7 @@ import path from "node:path";
11
11
  import { ObservationClock, observedTimeout } from "./observation-clock.mjs";
12
12
  import { writeJsonAtomic } from "./runner-events.mjs";
13
13
  import { errorRecord, reportedError } from "./operation-errors.mjs";
14
+ import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
14
15
  import { checkObservedProfile, observeModel } from "./model-observations.mjs";
15
16
  import { DroidMetadataReader, readDroidRoutingLog } from "./droid-observation.mjs";
16
17
  const metadataReaders = new Map();
@@ -381,14 +382,9 @@ export class DroidRuntime {
381
382
  if (ingest && measured && this.config.ddFlowBin) await this.flow("usage", "ingest", payload);
382
383
  return { status: measured ? "measured" : "unavailable", ...payload };
383
384
  }
384
- async flow(family, command, payload) {
385
- const target = executable(this.config.ddFlowBin, ["droid", family, command, "--project-root", this.config.projectRoot, "--json"]);
386
- return await new Promise((resolve, reject) => {
387
- const child = spawn(target.command, target.args, { cwd: this.config.cwd, env: droidEnvironment(this.config), stdio: ["pipe", "pipe", "pipe"] }); let stdout = "", stderr = "";
388
- const timer = observedTimeout(() => { child.kill("SIGTERM"); reject(new DroidError("droid_flow_timeout", "dd-flow did not acknowledge Droid evidence")); }, 25_000);
389
- child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk); child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
390
- child.on("error", error => { clearTimeout(timer); reject(error); }); child.on("close", code => { clearTimeout(timer); if (code !== 0) return reject(new DroidError("droid_flow_rejected", stderr || "dd-flow rejected Droid evidence")); try { resolve(JSON.parse(stdout)); } catch (error) { reject(error); } }); child.stdin.end(`${JSON.stringify(payload)}\n`);
391
- });
385
+ async flow(family, command, payload, timeoutMs = 25_000) {
386
+ return await invokeNativeHook({ bin: this.config.ddFlowBin, args: ["droid", family, command, "--project-root", this.config.projectRoot, "--json"], payload,
387
+ cwd: this.config.cwd, env: droidEnvironment(this.config), home: this.config.ddFlowHome, timeoutMs });
392
388
  }
393
389
  async inspect(id, { ignoreActive = false, ingestUsage = true } = {}) {
394
390
  this.requireIdentity(id); await this.captureProcesses(); const children = await this.refreshTopology(); const source = await readDroidSession(this.paths.factory, id);
@@ -396,20 +392,22 @@ export class DroidRuntime {
396
392
  const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && children.every(child => terminal(child.status));
397
393
  return { 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, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null };
398
394
  }
399
- async observeHook(payload, eventId) {
400
- try { return await this.observeHookChecked(payload, eventId); }
401
- catch (error) { await this.handleObservationError(error); throw error; }
395
+ async observeHook(payload, eventId, deadlineAt = null) {
396
+ try { return await this.observeHookChecked(payload, eventId, hookBudget(23_000, deadlineAt)); }
397
+ catch (error) { if (error.details?.scope !== "foreign_hook") await this.handleObservationError(error); throw error; }
402
398
  }
403
- async observeHookChecked(payload, eventId) {
399
+ async observeHookChecked(payload, eventId, budget = hookBudget(23_000)) {
404
400
  const id = payload.session_id, event = payload.hook_event_name;
405
401
  if (!id || !event || !eventId) throw new DroidError("droid_hook_invalid", "Hook lacks native identity or event ID");
406
402
  // Hooks can precede the root initialize response or the child notification.
407
403
  const deadline = Date.now() + 3_000; let native;
408
404
  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);
409
- if (!native || path.resolve(native.header.cwd) !== this.config.cwd || path.resolve(payload.cwd) !== this.config.cwd || await realpath(payload.transcript_path) !== await realpath(native.transcript_path)) throw new DroidError("droid_hook_identity_invalid", "Hook is outside its controlled native Session/workspace");
405
+ const foreign = message => new DroidError("droid_hook_identity_invalid", message, { scope: "foreign_hook", provider_session_id: id });
406
+ if (!native || path.resolve(payload.cwd) !== this.config.cwd || await realpath(payload.transcript_path) !== await realpath(native.transcript_path)) throw foreign("Hook is outside its controlled native Session/workspace");
410
407
  const parent = native.header.callingSessionId ?? null;
411
- if (parent && parent !== this.rootId) throw new DroidError("droid_hook_identity_invalid", "Hook child belongs to another root");
412
- if (!parent && this.rootId && id !== this.rootId) throw new DroidError("droid_hook_identity_invalid", "Hook came from an unrelated root");
408
+ if (parent && parent !== this.rootId) throw foreign("Hook child belongs to another root");
409
+ if (!parent && this.rootId && id !== this.rootId) throw foreign("Hook came from an unrelated root");
410
+ if (path.resolve(native.header.cwd) !== this.config.cwd) throw new DroidError("droid_hook_identity_invalid", "Owned native Session workspace changed");
413
411
  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" });
414
412
  this.activity += 1; await this.journal("hook", { event_id: eventId, event, session_id: id, parent_session_id: parent }); await this.save();
415
413
  if (!["PreToolUse", "PostToolUse"].includes(event)) return {};
@@ -418,8 +416,17 @@ export class DroidRuntime {
418
416
  try { profile = await this.observeSettings(id, native.settings, { path: native.transcript_path, sha256: native.source_sha256 }); }
419
417
  catch (error) { await this.handleObservationError(error); return { continue: false, stopReason: error.message }; }
420
418
  if (!this.config.ddFlowBin || payload.tool_name !== "Execute") return {};
421
- await this.snapshotUsage(id);
422
- return await this.flow("event", "handle", { schema_id: "dd-flow/droid-tool-event@1", phase: event === "PreToolUse" ? "before" : "after", event_id: eventId, daemon_id: this.config.daemonId, session: { provider_session_id: id, parent_provider_session_id: parent, directory: this.config.cwd }, tool: payload.tool_name, input: payload.tool_input, profile, transcript_path: native.transcript_path, outcome: { status: payload.tool_response?.is_error ? "error" : "completed" } });
419
+ // Usage is ingested by inspect/turn completion, never ahead of admission.
420
+ try {
421
+ return await this.flow("event", "handle", { schema_id: "dd-flow/droid-tool-event@1", phase: event === "PreToolUse" ? "before" : "after", event_id: eventId, daemon_id: this.config.daemonId, session: { provider_session_id: id, parent_provider_session_id: parent, directory: this.config.cwd }, tool: payload.tool_name, input: payload.tool_input, profile, transcript_path: native.transcript_path, outcome: { status: payload.tool_response?.is_error ? "error" : "completed" } }, budget.remaining("admission", 15_000));
422
+ } catch (error) {
423
+ // Identity is proven above; only this owned admission can fail its turn.
424
+ this.fatalError ??= Object.assign(error, { details: { ...error.details, provider_session_id: id, event_id: eventId, effect: "unknown" } });
425
+ this.rejectPending(this.fatalError);
426
+ try { await this.save({ fatal_error: errorRecord(this.fatalError) }); await this.journal("hook_rejected", { error: errorRecord(this.fatalError) }); }
427
+ catch (storage) { this.fatalError.details.receipt_error = errorRecord(storage); }
428
+ throw this.fatalError;
429
+ }
423
430
  }
424
431
  async captureProcesses() {
425
432
  const rows = await processSnapshot();
@@ -12,7 +12,7 @@ import path from "node:path";
12
12
 
13
13
  import { AcpBridge } from "./dd-zcode.mjs";
14
14
  import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
15
- import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
15
+ import { prepareRuntimeOwner, cleanupFailedStart, closeManagedBridge, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
16
16
 
17
17
  const REQUEST_SCHEMA = "dd-grok/daemon-request@1";
18
18
  const RESPONSE_SCHEMA = "dd-grok/daemon-response@1";
@@ -167,7 +167,7 @@ export async function startDaemon(options) {
167
167
  const previous = await readState(paths.dir); if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config); if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous daemon died with an active or unproven Session tree", false, { daemon_id: previous.daemon_id });
168
168
  await removeSocket(paths.socket, paths.dir); await mkdir(paths.home, { recursive: true, mode: 0o700 }); const importedSession = await materializeSessionArchive(paths, config.sessionArchive); const copiedAuth = await copyAuth(paths, authSource); await writeIsolatedConfig(paths); if (!noFlow) await writeHook(paths, config, absolute(options.entryPath, "dd-grok entry")); const versions = await doctor({ bin: config.bin }); if (importedSession && importedSession.grok_version !== versions.versions.grok) throw new DaemonError("session_archive_version_mismatch", "Grok Build session archive was created by a different CLI version", false, { archive_version: importedSession.grok_version, current_version: versions.versions.grok }); const configIsolation = await inspectIsolatedConfig(config);
169
169
  const resourceProcess = await registerDaemonProcess(config, { kind: "grok-daemon", owner: `grok:${path.basename(paths.dir)}`, operation: `grok-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
170
- const state = { schema_id: STATE_SCHEMA, daemon_id: randomUUID(), pid: null, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", active_tree: false, recovery_status: previous?.shutdown_state === "running" ? "recovered_idle" : "clean_start", auth_status: copiedAuth ? "copied" : "absent", ...(importedSession ? { imported_session: importedSession } : {}), versions: versions.versions, config_isolation: configIsolation, config, sessions: importedSession ? [{ provider_session_id: importedSession.provider_session_id, adapter_session_id: importedSession.provider_session_id, parent_provider_session_id: null, root_provider_session_id: importedSession.provider_session_id, cwd: importedSession.source_cwd }] : (previous?.sessions ?? []), resource_process: resourceProcess };
170
+ const state = { descendants: previous?.descendants ?? [], schema_id: STATE_SCHEMA, daemon_id: randomUUID(), pid: null, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", active_tree: false, recovery_status: previous?.shutdown_state === "running" ? "recovered_idle" : "clean_start", auth_status: copiedAuth ? "copied" : "absent", ...(importedSession ? { imported_session: importedSession } : {}), versions: versions.versions, config_isolation: configIsolation, config, sessions: importedSession ? [{ provider_session_id: importedSession.provider_session_id, adapter_session_id: importedSession.provider_session_id, parent_provider_session_id: null, root_provider_session_id: importedSession.provider_session_id, cwd: importedSession.source_cwd }] : (previous?.sessions ?? []), resource_process: resourceProcess };
171
171
  await writeState(paths.state, state); const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [absolute(options.entryPath, "dd-grok entry"), "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: isolatedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
172
172
  try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 30_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { const status = await callDaemon(paths.dir, "daemon.ready", {}, 5000); if (status.ready) return status; } catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; } } throw new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
173
173
  catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
@@ -175,11 +175,69 @@ export async function startDaemon(options) {
175
175
  export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); }
176
176
 
177
177
  export class Runtime {
178
- constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map(); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
179
- persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeState(this.paths.state, snapshot)); if (snapshot.resource_process) this.persisting = this.persisting.then(() => heartbeatDaemonProcess(snapshot.config, snapshot.resource_process).catch(() => {})); return this.persisting; }
178
+ constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
179
+ persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()], descendants: [...this.descendants.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
180
180
  options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
181
- track(result) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant(child, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
182
- recordDescendant(value, root, source, fallback) { const id = value?.provider_session_id ?? value?.session_id ?? value?.sessionId ?? value?.childSessionId ?? value?.subagentId; const parent = value?.parent_provider_session_id ?? value?.parentSessionId ?? root; if (typeof id !== "string" || !id || typeof parent !== "string" || !parent) return; this.bridge.registerModelSession?.(id, id, this.state.config, parent); const prior = this.descendants.get(id); this.descendants.set(id, { provider_session_id: id, parent_provider_session_id: parent, status: value?.status ?? value?.state ?? (fallback === "unknown" ? prior?.status ?? fallback : fallback), source }); }
181
+ track(result, before = null) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant({ ...child, stale_activity: Boolean(before && before.get(child.sessionId ?? child.subagentId ?? child.provider_session_id) !== this.descendants.get(child.sessionId ?? child.subagentId ?? child.provider_session_id)) }, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
182
+ recordDescendant(value, root, source, fallback) {
183
+ const id = value?.provider_session_id ?? value?.session_id ?? value?.sessionId ?? value?.childSessionId ?? value?.subagentId ?? value?.child_session_id ?? value?.subagent_id;
184
+ const parent = value?.parent_provider_session_id ?? value?.parentSessionId ?? value?.parent_session_id ?? root;
185
+ if (typeof id !== "string" || !id || typeof parent !== "string" || !parent) return;
186
+ if (id === parent) throw new DaemonError("session_parent_conflict", "Grok child Session cannot be its own native parent", false, { provider_session_id: id, observed_parent_provider_session_id: parent });
187
+ const prior = this.sessions.get(id);
188
+ if (prior?.parent_provider_session_id && prior.parent_provider_session_id !== parent) {
189
+ throw new DaemonError("session_parent_conflict", "Grok child Session was observed with conflicting native parents", false, {
190
+ provider_session_id: id,
191
+ retained_parent_provider_session_id: prior.parent_provider_session_id,
192
+ observed_parent_provider_session_id: parent
193
+ });
194
+ }
195
+ const parentSession = this.sessions.get(parent);
196
+ const rootProviderSessionId = prior?.root_provider_session_id
197
+ ?? parentSession?.root_provider_session_id
198
+ ?? root;
199
+ const cwd = value?.cwd ?? prior?.cwd ?? parentSession?.cwd ?? this.state.config?.cwd ?? null;
200
+ // A descendant is a managed native Session, not merely a liveness item.
201
+ // Register it in the same registry used by inspect/hook.resolve so a
202
+ // parentless inspection cannot downgrade it into a second root.
203
+ this.sessions.set(id, {
204
+ ...prior,
205
+ provider_session_id: id,
206
+ adapter_session_id: prior?.adapter_session_id ?? id,
207
+ parent_provider_session_id: parent,
208
+ root_provider_session_id: rootProviderSessionId,
209
+ cwd,
210
+ native_root_receipt: prior?.native_root_receipt ?? null
211
+ });
212
+ this.bridge.registerModelSession?.(id, id, this.state.config, parent);
213
+ const descendant = this.descendants.get(id);
214
+ const attempt = value.attempt_id;
215
+ const retired = descendant?.retired_attempt_ids ?? [];
216
+ if (attempt && retired.includes(attempt)) return;
217
+ const spawned = value.sessionUpdate === "subagent_spawned";
218
+ const newAttempt = spawned && typeof attempt === "string" && attempt !== descendant?.attempt_id;
219
+ const terminal = status => ["completed", "failed", "cancelled"].includes(status);
220
+ const status = value?.status ?? value?.state ?? (fallback === "unknown" ? descendant?.status ?? fallback : fallback);
221
+ if (attempt && descendant?.attempt_id && attempt !== descendant.attempt_id && !newAttempt) throw new DaemonError("native_child_execution_ambiguous", "Grok child event has no matching native spawn attempt", false, { session_id: id, attempt_id: attempt });
222
+ if (source === "x.ai/subagent/event" && terminal(status) && descendant?.attempt_id && !attempt) throw new DaemonError("native_child_execution_ambiguous", "Grok terminal event omits the retained child attempt identity", false, { session_id: id });
223
+ if (!newAttempt && terminal(descendant?.status) && status === "running") {
224
+ if (value.stale_activity || attempt === descendant.attempt_id && attempt) return;
225
+ const error = { code: "native_child_execution_ambiguous", message: "Grok child activity follows a terminal result without native new-attempt evidence", details: { session_id: id } };
226
+ this.descendants.set(id, { ...descendant, activity_status: "running", outcome_error: error });
227
+ if (spawned) throw new DaemonError(error.code, error.message, false, error.details);
228
+ return;
229
+ }
230
+ if (!newAttempt && terminal(descendant?.status) && terminal(status) && descendant.status !== status) throw new DaemonError("native_child_outcome_conflict", "Grok child has contradictory terminal outcomes", false, { session_id: id, previous: descendant.status, observed: status });
231
+ this.descendants.set(id, {
232
+ provider_session_id: id,
233
+ parent_provider_session_id: parent,
234
+ status,
235
+ source,
236
+ ...(attempt || descendant?.attempt_id ? { attempt_id: attempt ?? descendant.attempt_id } : {}),
237
+ ...(newAttempt && descendant?.attempt_id ? { retired_attempt_ids: [...retired, descendant.attempt_id] } : retired.length ? { retired_attempt_ids: retired } : {}),
238
+ ...(!newAttempt && !terminal(status) && descendant?.outcome_error ? { outcome_error: descendant.outcome_error } : {})
239
+ });
240
+ }
183
241
  observeSubagentEvent(message) {
184
242
  const root = message?.params?.sessionId, update = message?.params?.update;
185
243
  const kind = String(update?.sessionUpdate ?? update?.type ?? message?.method ?? "");
@@ -192,19 +250,37 @@ export class Runtime {
192
250
  this.subagentCalls ??= new Map();
193
251
  const key = `${root}:${update?.toolCallId}`;
194
252
  const tool = update?._meta?.["x.ai/tool"]?.name ?? (kind === "tool_call" ? update.title : null);
195
- if (tool) this.subagentCalls.set(key, tool);
196
- const name = this.subagentCalls.get(key), output = update?.rawOutput;
253
+ if (["spawn_subagent", "get_command_or_subagent_output"].includes(tool) && !this.subagentCalls.has(key)) this.subagentCalls.set(key, { name: tool, input: update.rawInput, attempts: new Map([...this.descendants].map(([id, child]) => [id, child.attempt_id])) });
254
+ const call = this.subagentCalls.get(key), name = call?.name, output = update?.rawOutput;
255
+ if (call && update.rawInput) call.input = update.rawInput;
197
256
  if (kind !== "tool_call_update" || update.status !== "completed") return;
198
257
  if (name === "spawn_subagent" && output?.type === "Text") {
199
258
  const id = /^subagent_id:\s*(\S+)\s*$/m.exec(output.text ?? "")?.[1];
200
- if (id) this.recordDescendant({ session_id: id }, root, "x.ai/tool/spawn_subagent", "running");
259
+ if (id) this.recordDescendant({ session_id: id, stale_activity: !call.attempts.has(id) || call.attempts.get(id) !== this.descendants.get(id)?.attempt_id }, root, "x.ai/tool/spawn_subagent", "running");
201
260
  }
202
261
  if (name === "get_command_or_subagent_output" && output?.type === "TaskOutput") {
203
- const result = output.Result;
204
- if (this.descendants.has(result?.task_id)) this.recordDescendant({ session_id: result.task_id, status: result.status }, root, "x.ai/tool/TaskOutput", "unknown");
262
+ const results = output.Result ? [output.Result] : output.MultiResult?.results ?? [];
263
+ if (!Array.isArray(results)) throw new DaemonError("native_child_outcome_unavailable", "Grok TaskOutput has a malformed result list");
264
+ // TaskNotFound is prose in the native contract; identity comes from the
265
+ // matching tool input, never by scraping its diagnostic message.
266
+ if (output.TaskNotFound && typeof call.input?.task_id === "string") results.push({ task_id: call.input.task_id, status: "not_found" });
267
+ let firstError;
268
+ for (const result of results) {
269
+ const child = this.descendants.get(result?.task_id);
270
+ if (!child || call.attempts.get(result.task_id) !== child.attempt_id) continue;
271
+ if (result.status === "not_found") {
272
+ const error = new DaemonError("native_child_outcome_unavailable", "Grok cannot retrieve the owned child's native outcome", false, { session_id: result.task_id, tool_call_id: update.toolCallId });
273
+ this.descendants.set(result.task_id, { ...child, outcome_error: errorPayload(error) });
274
+ firstError ??= error;
275
+ continue;
276
+ }
277
+ if (!["running", "completed", "failed", "cancelled"].includes(result.status)) throw new DaemonError("native_child_outcome_unavailable", "Grok child returned an unknown native outcome", false, { session_id: result.task_id, status: result.status });
278
+ this.recordDescendant({ session_id: result.task_id, status: result.status, attempt_id: child.attempt_id }, root, "x.ai/tool/TaskOutput", "unknown");
279
+ }
280
+ if (firstError) throw firstError;
205
281
  }
206
282
  }
207
- topology(root) { return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
283
+ topology(root) { if (this.state.observation_error) { const error = this.state.observation_error; throw new DaemonError(error.code, error.message, false, error.details); } return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
208
284
  observeToolCall(message) { this.toolObservations ??= new ToolObservations(); this.toolObservations.add(acpToolObservation(message, { harness: "grok-acp" })); const sessionId = message?.params?.sessionId; if (sessionId) this.toolUsage.set(sessionId, this.toolObservations.summary(sessionId)); }
209
285
  running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
210
286
  treeRunning(result) {
@@ -225,13 +301,15 @@ export class Runtime {
225
301
  const running = [];
226
302
  for (const session of [...this.sessions.values()].filter(item => !item.parent_provider_session_id)) {
227
303
  const id = session.provider_session_id;
304
+ const before = new Map(this.descendants);
228
305
  let observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
229
- this.track(observed);
306
+ this.track(observed, before);
230
307
  const rootActive = Boolean(this.active);
231
308
  if (cancel && (rootActive || this.treeRunning(observed))) {
232
309
  await cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
310
+ const beforeCancelledInspection = new Map(this.descendants);
233
311
  observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
234
- this.track(observed);
312
+ this.track(observed, beforeCancelledInspection);
235
313
  }
236
314
  if (this.active || this.treeRunning(observed)) running.push(id);
237
315
  }
@@ -247,8 +325,9 @@ export class Runtime {
247
325
  if (name === "session.prompt" && this.sessions.has(sessionId)) this.sessions.get(sessionId).native_root_receipt = null;
248
326
  try {
249
327
  await this.persist({ active_tree: true, active_operation: name });
328
+ const before = new Map(this.descendants);
250
329
  const result = await task();
251
- this.track(result);
330
+ this.track(result, before);
252
331
  const session = this.sessions.get(result.provider_session_id);
253
332
  if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id };
254
333
  // Archive requires an idle tree before entering this exclusive section
@@ -262,12 +341,12 @@ export class Runtime {
262
341
  async dispatch(operation, params) {
263
342
  const assertDispatch = captureDispatchGuard(this);
264
343
  if (operation === "daemon.ready") return { ready: this.ready, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config };
265
- if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? null }; }
344
+ if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
266
345
  if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }; }
267
346
  if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; });
268
347
  if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId); }
269
348
  if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
270
- if (operation === "session.inspect" || operation === "session.resume") { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); await this.persist(); return { ...result, settled: !this.active && !this.treeRunning(result), descendants: this.topology(result.provider_session_id) }; }
349
+ if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); return { ...result, settled: !this.active && !this.treeRunning(result), descendants: this.topology(result.provider_session_id) }; }
271
350
  if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
272
351
  if (operation === "session.archive") { await this.requireSettled(); return await this.productive(operation, async () => { let session = this.sessions.get(params.sessionId); if (!session?.cwd) { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); session = this.sessions.get(result.provider_session_id); } return await exportSessionArchive(this.paths, session.provider_session_id, absolute(session.cwd, "Grok Build session cwd"), params.archiveDir, this.state.versions); }); }
273
352
  if (operation === "daemon.stop") {
@@ -282,7 +361,7 @@ export class Runtime {
282
361
  }
283
362
  throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
284
363
  }
285
- async shutdown() { await this.bridge.close(); await finishDaemonProcess(this.state.config, this.state.resource_process); await unlink(path.join(this.paths.home, "auth.json")).catch(error => { if (error.code !== "ENOENT") throw error; }); await this.persist({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await new Promise((resolve) => this.server.close(resolve)); await removeSocket(this.paths.socket, this.paths.dir); }
364
+ async shutdown() { await closeManagedBridge(this.state.config, this.state, this.bridge); await finishDaemonProcess(this.state.config, this.state.resource_process); await unlink(path.join(this.paths.home, "auth.json")).catch(error => { if (error.code !== "ENOENT") throw error; }); await this.persist({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await new Promise((resolve) => this.server.close(resolve)); await removeSocket(this.paths.socket, this.paths.dir); }
286
365
  }
287
366
 
288
367
  export async function serveDaemon(stateDir) {
@@ -290,11 +369,19 @@ export async function serveDaemon(stateDir) {
290
369
  const bridge = new AcpBridge({ ...state.config, commandArgs: ["agent", "--no-leader", "--always-approve", "--model", state.config.model, "--reasoning-effort", state.config.reasoning, "stdio"], env: isolatedEnv(state.config), clientInfo: { name: "dd-grok", version: "0.1.0" }, permission: "allow", onNotification: async (message) => {
291
370
  const sessionId = message?.params?.sessionId;
292
371
  if (!runtime || !sessionId) return;
293
- runtime.observeToolCall(message); runtime.observeSubagentEvent(message);
372
+ try {
373
+ runtime.observeToolCall(message); runtime.observeSubagentEvent(message);
374
+ } catch (error) {
375
+ // Native notifications are fire-and-forget. Persist topology conflicts
376
+ // for the next inspect/status call instead of creating an unhandled
377
+ // rejection that can kill the daemon without a causal receipt.
378
+ try { await runtime.persist({ observation_error: errorPayload(error) }); } catch { /* status/inspect will report the original native failure */ }
379
+ }
294
380
  const session = runtime.sessions.get(sessionId);
295
381
  const rootProviderSessionId = session?.root_provider_session_id ?? sessionId;
296
382
  await observeAcpToolCall(state.config, { daemonId: state.daemon_id, rootProviderSessionId, parentProviderSessionId: session?.parent_provider_session_id ?? null, observedProfile: bridge.modelProfiles?.get(sessionId) ?? {} }, message);
297
- } }); const initialized = await bridge.start(); runtime = new Runtime(paths, state, bridge, initialized);
383
+ } }); const initialized = await startManagedBridge({ config: state.config, state, bridge, kind: "grok-provider", publish: snapshot => writeState(paths.state, snapshot) }); runtime = new Runtime(paths, state, bridge, initialized);
384
+ await heartbeatDaemonProcess(state.config, state.resource_process);
298
385
  await removeSocket(paths.socket, paths.dir); const server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const newline = buffer.indexOf("\n"); if (newline < 0) return; const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request"); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => void runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await runtime.persist({ shutdown_state: "running", active_tree: false }); runtime.ready = true; return await new Promise(() => {});
299
386
  }
300
387
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -35,7 +35,7 @@ function errorPayload(error) { return { code: error.code ?? "operation_failed",
35
35
  async function writePlugin(paths, config) {
36
36
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
37
37
  const helper = new URL("./native-hook-command.mjs", import.meta.url).href;
38
- const source = `import { invokeNativeHook } from ${JSON.stringify(helper)};\nconst candidate = command => typeof command === "string" && command.length > 0;\nconst participating = new Set();\nconst invoke = async payload => await invokeNativeHook({bin:process.env.DD_OPENCODE_DD_FLOW_BIN,args:["opencode","event","handle","--project-root",process.env.DD_OPENCODE_PROJECT_ROOT,"--json"],payload,home:process.env.DD_FLOW_HOME,timeoutMs:15000});\nexport const DdFlowPlugin=async({client,project,directory})=>{ const envelope=async(phase,input,extra={})=>{ const response=await client.session.get({path:{id:input.sessionID}}); if(response?.error||!response?.data) throw Object.assign(new Error("OpenCode Session identity is unavailable"),{code:"opencode_session_identity_unavailable"}); const session=response.data; return {schema_id:"dd-flow/opencode-tool-event@1",event_id:[input.sessionID,input.callID,phase].join(":"),phase,daemon_id:process.env.DD_OPENCODE_DAEMON_ID,server_version:process.env.DD_OPENCODE_SERVER_VERSION,plugin_sha256:process.env.DD_OPENCODE_PLUGIN_SHA256,session:{provider_session_id:input.sessionID,parent_provider_session_id:session.parentID??null,agent_id:process.env.DD_OPENCODE_AGENT,directory:session.directory??directory,project_id:session.projectID??project?.id},message_id:input.messageID??null,tool_call_id:input.callID,tool:input.tool,profile:{provider:process.env.DD_OPENCODE_PROVIDER,model:process.env.DD_OPENCODE_MODEL,variant:process.env.DD_OPENCODE_VARIANT,agent:process.env.DD_OPENCODE_AGENT},...extra}; }; return {"tool.execute.before":async(input,output)=>{ const command=output.args?.command; if(!candidate(command)) return; const result=await invoke(await envelope("before",input,{input:output.args})); if(result?.observed) participating.add(input.callID); const updated=result?.hookSpecificOutput?.updatedInput; if(updated) Object.assign(output.args,updated); },"tool.execute.after":async(input,output)=>{ if(!participating.delete(input.callID)) return; await invoke(await envelope("after",input,{input:input.args,outcome:{status:"completed",title:output.title}})); }}; };\n`;
38
+ const source = `import { hookBudget, invokeNativeHook } from ${JSON.stringify(helper)};\nconst candidate = command => typeof command === "string" && command.length > 0;\nconst participating = new Set();\nconst invoke = async (payload,budget) => await invokeNativeHook({bin:process.env.DD_OPENCODE_DD_FLOW_BIN,args:["opencode","event","handle","--project-root",process.env.DD_OPENCODE_PROJECT_ROOT,"--json"],payload,home:process.env.DD_FLOW_HOME,timeoutMs:budget.remaining("admission",15000)});\nexport const DdFlowPlugin=async({client,project,directory})=>{ const envelope=async(phase,input,extra,budget)=>{ const response=await budget.run(()=>client.session.get({path:{id:input.sessionID}}),"identity"); if(response?.error||!response?.data) throw Object.assign(new Error("OpenCode Session identity is unavailable"),{code:"opencode_session_identity_unavailable"}); const session=response.data; return {schema_id:"dd-flow/opencode-tool-event@1",event_id:[input.sessionID,input.callID,phase].join(":"),phase,daemon_id:process.env.DD_OPENCODE_DAEMON_ID,server_version:process.env.DD_OPENCODE_SERVER_VERSION,plugin_sha256:process.env.DD_OPENCODE_PLUGIN_SHA256,session:{provider_session_id:input.sessionID,parent_provider_session_id:session.parentID??null,agent_id:process.env.DD_OPENCODE_AGENT,directory:session.directory??directory,project_id:session.projectID??project?.id},message_id:input.messageID??null,tool_call_id:input.callID,tool:input.tool,profile:{provider:process.env.DD_OPENCODE_PROVIDER,model:process.env.DD_OPENCODE_MODEL,variant:process.env.DD_OPENCODE_VARIANT,agent:process.env.DD_OPENCODE_AGENT},...extra}; }; return {"tool.execute.before":async(input,output)=>{ const command=output.args?.command; if(!candidate(command)) return; const budget=hookBudget(); const result=await invoke(await envelope("before",input,{input:output.args},budget),budget); if(result?.observed) participating.add(input.callID); const updated=result?.hookSpecificOutput?.updatedInput; if(updated) Object.assign(output.args,updated); },"tool.execute.after":async(input,output)=>{ if(!participating.delete(input.callID)) return; const budget=hookBudget(); await invoke(await envelope("after",input,{input:input.args,outcome:{status:"completed",title:output.title}},budget),budget); }}; };\n`;
39
39
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
40
40
  }
41
41
  async function prepare(paths, config, authSource) {
@@ -67,10 +67,10 @@ export const stopDaemon = async options => { const timeoutMs = options.timeoutMs
67
67
 
68
68
  export class Runtime {
69
69
  constructor(paths, state, client, child, providerProcess) { this.paths = paths; this.state = state; this.client = client; this.child = child; this.providerProcess = providerProcess; this.sessions = new Map((state.sessions ?? []).map(x => [x.provider_session_id, x])); this.active = null; this.persisting = Promise.resolve(); }
70
- persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(async () => { await writeJson(this.paths.state, snapshot); await heartbeatDaemonProcess(snapshot.config, snapshot.resource_process).catch(() => {}); await heartbeatDaemonProcess(snapshot.config, this.providerProcess).catch(() => {}); }); return this.persisting; }
70
+ persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
71
71
  async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
72
72
  async childTree(id, seen = new Set()) { if (seen.has(id)) return []; seen.add(id); const direct = await this.client.children(id) ?? []; const nested = []; for (const child of direct) nested.push(child, ...await this.childTree(child.id, seen)); return nested; }
73
- async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); this.journaledTools ??= new Map(); journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, children, usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length }; }
73
+ async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); this.journaledTools ??= new Map(); journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, tree_observations: children.map(child => ({ provider_session_id: child.id, status: statuses?.[child.id]?.type ?? "idle", settled: (statuses?.[child.id]?.type ?? "idle") === "idle" })), children, usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length }; }
74
74
  async observeModels(id, messages, parent = null) {
75
75
  this.modelMessages ??= new Set();
76
76
  for (const message of messages ?? []) {
@@ -208,6 +208,8 @@ export async function serveDaemon(stateDir) {
208
208
  providerProcess = await registerDaemonProcess(state.config, { kind: "opencode-provider", owner: `opencode:${path.basename(paths.dir)}`, operation: `opencode-provider:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
209
209
  child = spawn(target.command, target.args, { cwd: state.config.cwd, env: isolatedEnv(state.config), detached: true, stdio: ["ignore", "inherit", "inherit"] });
210
210
  await confirmDaemonProcess(state.config, providerProcess, child);
211
+ await heartbeatDaemonProcess(state.config, state.resource_process);
212
+ await heartbeatDaemonProcess(state.config, providerProcess);
211
213
  state.provider_process = providerProcess; state.provider_pid = child.pid; await writeJson(paths.state, state);
212
214
  const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const deadline = Date.now() + 15_000; while (Date.now() < deadline) { try { await client.health(); break; } catch { await new Promise(resolve => setTimeout(resolve, 100)); } } await client.health(); const runtime = new Runtime(paths, state, client, child, providerProcess); await removeSocket(paths); const server = net.createServer(connection => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", chunk => { buffer += chunk; const i = buffer.indexOf("\n"); if (i < 0) return; void (async () => { let request; try { request = JSON.parse(buffer.slice(0, i)); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await runtime.persist({ shutdown_state: "running", active_tree: false, server_port: port }); return await new Promise(() => {});
213
215
  } catch (error) {
@@ -5,7 +5,7 @@ import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "
5
5
  import net from "node:net";
6
6
  import { durableDaemonDispatch } from "./daemon-operations.mjs";
7
7
  import path from "node:path";
8
- import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
8
+ import { prepareRuntimeOwner, cleanupFailedStart, closeManagedBridge, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
9
9
 
10
10
  import {
11
11
  AcpBridge,
@@ -228,13 +228,6 @@ export class DaemonRuntime {
228
228
  Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()] });
229
229
  const snapshot = structuredClone(this.state);
230
230
  this.persisting = this.persisting.then(() => writeState(this.paths.state, snapshot));
231
- const resourceProcess = this.state.resource_process;
232
- if (resourceProcess?.id && resourceProcess.lease_token) {
233
- this.persisting = this.persisting.then(async () => {
234
- try { await heartbeatDaemonProcess(this.state.config, resourceProcess); }
235
- catch { /* A crashed client is recovered through the durable lease. */ }
236
- });
237
- }
238
231
  return this.persisting;
239
232
  }
240
233
 
@@ -262,6 +255,8 @@ export class DaemonRuntime {
262
255
  // be non-resident and unsubscribable while their request is alive.
263
256
  const [resident, topology] = await Promise.all([
264
257
  this.bridge.request("zcode/session/resident", { sessionId: adapterSessionId }, 5_000),
258
+ // childSessionIds is complete on every page. This diagnostic reads
259
+ // identities only, not ended outcomes or a settlement verdict.
265
260
  this.bridge.request("zcode/session/retainedSubagents", { sessionId: adapterSessionId }, 5_000),
266
261
  ]);
267
262
  diagnostics = {
@@ -586,7 +581,7 @@ export class DaemonRuntime {
586
581
  async shutdown() {
587
582
  // prepareStop has already closed the owned bridge. Signal shutdown may get
588
583
  // here directly, so close remains idempotent.
589
- await this.bridge.close();
584
+ await closeManagedBridge(this.state.config, this.state, this.bridge);
590
585
  await this.persist({ active_tree: false, shutdown_state: "clean", stopped_at: new Date().toISOString() });
591
586
  const resourceProcess = this.state.resource_process;
592
587
  if (resourceProcess?.id && resourceProcess.lease_token) {
@@ -603,8 +598,9 @@ export async function serveDaemon(stateDir) {
603
598
  const state = await readState(paths.dir);
604
599
  if (!state || state.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing or incompatible");
605
600
  const bridge = new AcpBridge({ ...state.config, env: { ...state.config.env, DD_FLOW_DAEMON_ID: state.daemon_id } });
606
- const initialized = await bridge.start();
601
+ const initialized = await startManagedBridge({ config: state.config, state, bridge, kind: "zcode-provider", publish: snapshot => writeState(paths.state, snapshot) });
607
602
  const runtime = new DaemonRuntime(paths, state, bridge, initialized);
603
+ await heartbeatDaemonProcess(state.config, state.resource_process);
608
604
  await removeSocket(paths.socket, paths.dir);
609
605
  const server = net.createServer((connection) => {
610
606
  connection.setEncoding("utf8"); let buffer = "";