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

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 (46) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli/hook-ingress.js +5 -10
  3. package/dist/harness-runtime/bin/dd-agy.mjs +20 -7
  4. package/dist/harness-runtime/bin/dd-droid.mjs +2 -2
  5. package/dist/harness-runtime/bin/dd-grok.mjs +4 -4
  6. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  7. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  8. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  9. package/dist/harness-runtime/lib/daemon-operations.mjs +13 -2
  10. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  11. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +89 -36
  12. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -4
  13. package/dist/harness-runtime/lib/dd-codex.mjs +1 -0
  14. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
  15. package/dist/harness-runtime/lib/dd-droid.mjs +24 -17
  16. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +107 -20
  17. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +5 -3
  18. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +6 -10
  19. package/dist/harness-runtime/lib/dd-zcode.mjs +63 -26
  20. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
  21. package/dist/harness-runtime/lib/delegation-instructions.mjs +6 -1
  22. package/dist/harness-runtime/lib/managed-daemon.mjs +32 -3
  23. package/dist/harness-runtime/lib/native-hook-command.mjs +87 -13
  24. package/dist/harness-runtime/lib/process-json.mjs +3 -2
  25. package/dist/services/code-checks.js +53 -16
  26. package/dist/services/controller-fanout.js +37 -6
  27. package/dist/services/eval-snapshots.js +148 -38
  28. package/dist/services/external-work-launch.js +60 -5
  29. package/dist/services/prompts.js +2 -0
  30. package/dist/services/review-copy.js +136 -0
  31. package/dist/services/run-control-worker.js +16 -11
  32. package/dist/services/run-controller-adapter.js +9 -0
  33. package/dist/services/runs.js +3 -0
  34. package/dist/services/runtime-scope-stop.js +66 -6
  35. package/dist/services/vnext-code-review.js +6 -2
  36. package/dist/services/vnext-code.js +1 -0
  37. package/dist/services/vnext-fanout.js +9 -1
  38. package/dist/services/vnext-merge.js +8 -4
  39. package/dist/services/vnext-plan-review.js +4 -15
  40. package/dist/services/vnext-plan.js +4 -0
  41. package/dist/services/vnext-protocolize.js +4 -2
  42. package/dist/services/vnext-specify.js +7 -0
  43. package/dist/services/work-registry.js +208 -22
  44. package/dist/services/workspace-files.js +83 -0
  45. package/dist/services/worktrees.js +3 -1
  46. package/package.json +1 -1
@@ -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 = "";
@@ -188,6 +188,7 @@ export class AcpBridge {
188
188
  this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
189
189
  this.child.on("exit", (code) => { const error = new Error(`zcode-acp exited ${code}${this.bridgeStderr.trim() ? `: ${this.bridgeStderr.trim()}` : ""}`); error.code = "bridge_exited"; this.rejectAll(error); });
190
190
  readline.createInterface({ input: this.child.stdout }).on("line", (line) => this.receive(line));
191
+ if (this.options.onSpawn) await this.options.onSpawn(this.child);
191
192
  return await this.request("initialize", {
192
193
  protocolVersion: 1,
193
194
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, elicitation: { form: {} } },
@@ -455,10 +456,47 @@ async function resume(bridge, sessionId, cwd) {
455
456
  return sessionId;
456
457
  }
457
458
 
459
+ /** Ended children are paginated by the native API (20 by default). A partial
460
+ * directory is never settlement evidence, including during teardown. */
461
+ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false) {
462
+ const deadline = performance.now() + timeoutMs;
463
+ const cursors = new Set(), ended = new Map();
464
+ let first, cursor;
465
+ for (;;) {
466
+ const remaining = deadline - performance.now();
467
+ if (remaining <= 0) fail("ZCode child observation deadline expired", "native_topology_incomplete");
468
+ const response = await bridge.request(retained ? "zcode/session/retainedSubagents" : "zcode/session/subagents", { sessionId, ...(cursor ? { endedCursor: cursor } : {}) }, remaining);
469
+ if (retained && response?.sessionId !== sessionId) fail("Retained ZCode child directory has a foreign Session identity", "native_topology_incomplete");
470
+ const page = retained ? response.topology : response;
471
+ if (performance.now() >= deadline) fail("ZCode child observation deadline expired", "native_topology_incomplete");
472
+ if (!page || !Array.isArray(page.running)) fail("ZCode child directory is unavailable", "native_topology_incomplete");
473
+ if (!first) first = page;
474
+ if (!page.ended) {
475
+ if (cursor) fail("ZCode child page omitted ended outcomes", "native_topology_incomplete");
476
+ return page; // Older qualified bridges expose an unpaginated completed array.
477
+ }
478
+ if (!Array.isArray(page.ended.items) || !Number.isSafeInteger(page.ended.total) || page.ended.total < 0
479
+ || page.revision !== first.revision || page.ended.total !== first.ended.total) fail("ZCode child directory changed or is malformed during pagination", "native_topology_incomplete");
480
+ for (const child of page.ended.items) {
481
+ if (typeof child.childSessionId !== "string" || !child.childSessionId) fail("ZCode ended child has no identity", "native_topology_incomplete");
482
+ const prior = ended.get(child.childSessionId);
483
+ if (prior && JSON.stringify(prior) !== JSON.stringify(child)) fail("ZCode child pages contain conflicting outcomes", "native_topology_incomplete");
484
+ ended.set(child.childSessionId, child);
485
+ }
486
+ cursor = page.ended.nextCursor;
487
+ if (!cursor) {
488
+ if (ended.size !== first.ended.total) fail("ZCode child directory is incomplete", "native_topology_incomplete");
489
+ return { ...first, ended: { total: first.ended.total, items: [...ended.values()] } };
490
+ }
491
+ if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
492
+ cursors.add(cursor);
493
+ }
494
+ }
495
+
458
496
  async function inspect(bridge, sessionId) {
459
497
  const [read, subagents] = await Promise.all([
460
498
  bridge.request("zcode/session/read", { sessionId }),
461
- bridge.request("zcode/session/subagents", { sessionId }),
499
+ readZcodeSubagents(bridge, sessionId),
462
500
  ]);
463
501
  let usage = null, usage_observation_error = null;
464
502
  try { usage = await readZcodeUsage(bridge, sessionId); }
@@ -521,9 +559,8 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
521
559
  const resident = await bridge.request("zcode/session/resident", { sessionId }, timeout());
522
560
  if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
523
561
  residents.push(resident);
524
- const retained = await bridge.request("zcode/session/retainedSubagents", { sessionId }, timeout());
525
- const tree = retained?.topology;
526
- if (retained?.sessionId !== sessionId || !Array.isArray(tree?.childSessionIds)
562
+ const tree = await readZcodeSubagents(bridge, sessionId, deadline - performance.now(), true);
563
+ if (!Array.isArray(tree?.childSessionIds)
527
564
  || tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
528
565
  || !Array.isArray(tree?.running) || !Array.isArray(tree?.ended?.items)
529
566
  || !Number.isSafeInteger(tree.ended.total) || tree.ended.total < 0
@@ -562,7 +599,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
562
599
  await new Promise((resolve) => setTimeout(resolve, 1000));
563
600
  const timeout = Math.max(1, deadline - Date.now());
564
601
  [after, read] = await Promise.all([
565
- bridge.request("zcode/session/subagents", { sessionId }, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
602
+ readZcodeSubagents(bridge, sessionId, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
566
603
  bridge.request("zcode/session/read", { sessionId }, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
567
604
  ]);
568
605
  root_status = read?.projection?.status ?? read?.session?.status ?? null;
@@ -588,7 +625,7 @@ export async function cancelChildWithBridge(bridge, options) {
588
625
  requireJournal(options);
589
626
  const identity = await controlledIdentity(bridge, options);
590
627
  const childSessionId = text(options.childSessionId, "--child-session-id");
591
- const before = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId });
628
+ const before = await readZcodeSubagents(bridge, identity.adapterSessionId);
592
629
  const child = (before.running ?? []).find((item) => item.childSessionId === childSessionId);
593
630
  if (!child) fail(`ZCode has no running child ${childSessionId}`, "child_not_running");
594
631
  const taskId = child.taskId ?? child.agentId;
@@ -597,7 +634,7 @@ export async function cancelChildWithBridge(bridge, options) {
597
634
  let after = before;
598
635
  for (let attempt = 0; attempt < 6; attempt += 1) {
599
636
  await new Promise((resolve) => setTimeout(resolve, 1000));
600
- after = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId });
637
+ after = await readZcodeSubagents(bridge, identity.adapterSessionId);
601
638
  if (!(after.running ?? []).some((item) => item.childSessionId === childSessionId)) break;
602
639
  }
603
640
  if ((after.running ?? []).some((item) => item.childSessionId === childSessionId)) {
@@ -752,22 +789,19 @@ async function forwardUsageBestEffort(options, providerSessionId, usage, toolCal
752
789
  }
753
790
 
754
791
  export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContract) {
755
- // Behavioural evidence: dd-eval production-invocations probes on 2026-09-13
756
- // and 2026-09-22. The latter exercised public commands, file-backed JSON,
757
- // concurrent children and continuation on bridge contract @2.
758
- // Pin both providers of native event identity, not a string in the bundle.
759
- // 60af0d3 adds retainedSubagents without changing native event forwarding.
760
- const qualifiedTuples = new Map([
761
- ["e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8", new Set(["e0600fe3c46e215695f257e65aa6f674ced998d8", "60af0d31e13076a313d9770f10aa70f7c94742cf"])],
762
- ["500ae84fa2cb8dd74c1264e5f3d3709c004c59b58899f1b0c3e205137a0ef466", new Set(["8c0a893f3c26a0b96cb138acf762db84c800298b", "f4d47b517073f9bdb8d4eba3d0d59ce66c6a2833"])],
763
- // ZCode Desktop 3.14.3 retains CLI 0.16.9; the published-engine native probe
764
- // passed root, concurrent child, and same-child continuation on this binary.
765
- ["b1df2ef3e5bd76c4af3ecb296bc003a10d3f13191a26610bd0ba940feadad529", new Set(["f4d47b517073f9bdb8d4eba3d0d59ce66c6a2833"])],
792
+ // The native app bundle is an auto-updated runtime. Its whole-file SHA is
793
+ // evidence for provenance, not a protocol identity: an unchanged bridge
794
+ // must not reject every normal ZCode patch release. The bridge contract is
795
+ // the compatibility boundary; native event identity is checked by the
796
+ // lifecycle observer and receipts during the bounded qualification probe.
797
+ const qualifiedBridgeCommits = new Map([
798
+ ["e0600fe3c46e215695f257e65aa6f674ced998d8", ZCODE_HARNESS_CONTRACT],
799
+ ["60af0d31e13076a313d9770f10aa70f7c94742cf", ZCODE_HARNESS_CONTRACT],
800
+ ["8c0a893f3c26a0b96cb138acf762db84c800298b", ZCODE_HARNESS_CONTRACT],
801
+ ["f4d47b517073f9bdb8d4eba3d0d59ce66c6a2833", "dd-zcode-harness@2"],
802
+ ["5a8c89ab716a4b2ccc50df81cb4d204ff5c06368", "dd-zcode-harness@2"],
766
803
  ]);
767
- const qualified = qualifiedTuples.get(sha256)?.has(bridgeCommit) === true
768
- && (bridgeCommit === "f4d47b517073f9bdb8d4eba3d0d59ce66c6a2833"
769
- ? harnessContract === "dd-zcode-harness@2"
770
- : harnessContract === ZCODE_HARNESS_CONTRACT);
804
+ const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
771
805
  return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
772
806
  capabilities: qualified ? ["root", "concurrent_children", "child_continuation"] : [],
773
807
  native_nested_children: false, reason: qualified ? null : "native_event_contract_unqualified" };
@@ -801,8 +835,11 @@ export async function createSessionWithBridge(bridge, options, initialized) {
801
835
  // fact that allocation was already admitted.
802
836
  await options.onSessionAliasAllocated?.({ adapter_session_id: created.sessionId, cwd });
803
837
  bridge.allocationObservers?.set(created.sessionId, async identity => {
804
- if (identity.cwd !== cwd) fail("ZCode allocation workspace mismatch", "session_identity_mismatch");
805
- await options.onSessionAllocated?.(identity);
838
+ // ZCode 0.46.x emits adapter/provider identities on this notification but
839
+ // does not repeat the workspace. The cwd from session/new is authoritative
840
+ // for this live bridge; reject only an explicitly conflicting workspace.
841
+ if (identity.cwd !== undefined && identity.cwd !== cwd) fail("ZCode allocation workspace mismatch", "session_identity_mismatch");
842
+ await options.onSessionAllocated?.({ ...identity, cwd });
806
843
  });
807
844
  const resolved = await bridge.request("zcode/session/resolve", { sessionId: created.sessionId });
808
845
  const providerSessionId = resolved.providerSessionId;
@@ -918,7 +955,7 @@ export async function cancelSession(options) {
918
955
  export async function cancelSessionWithBridge(bridge, options) {
919
956
  requireJournal(options);
920
957
  const identity = await controlledIdentity(bridge, options);
921
- const before = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId }).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
958
+ const before = await readZcodeSubagents(bridge, identity.adapterSessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
922
959
  return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...await cancelTree(bridge, identity.adapterSessionId, before, identity.providerSessionId, options.knownChildSessionIds) });
923
960
  }
924
961
 
@@ -931,7 +968,7 @@ export async function forkSessionWithBridge(bridge, options) {
931
968
  requireJournal(options);
932
969
  if (!options.target || typeof options.target !== "object") fail("explicit --target-json is required");
933
970
  const identity = await controlledIdentity(bridge, options);
934
- const topology = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId });
971
+ const topology = await readZcodeSubagents(bridge, identity.adapterSessionId);
935
972
  if ((topology.running ?? []).length) fail("cannot fork while background children are running");
936
973
  const result = await bridge.request("session/fork", { sessionId: identity.adapterSessionId, target: options.target });
937
974
  if (!result.forkedSessionId) fail("ZCode fork returned no provider Session ID");
@@ -1,5 +1,5 @@
1
1
  export type DelegationWork = { work_id: string; start_command: string; launch_policy?: string };
2
- export type DelegationInput = { harness: string; stage: string; capacity: number; works?: DelegationWork[] };
2
+ export type DelegationInput = { harness: string; stage: string; capacity: number; works?: DelegationWork[]; totalWorks?: number; remainingWorks?: number };
3
3
  export function delegationAdapter(harness: string): string | null;
4
4
  export function delegationContract(harness: string): string;
5
5
  export function workerDelegationTask(input: { workId: string; startCommand: string; launchPolicy?: string }): string;