@deksden-com/dd-flow-cli 0.9.0-beta.10 → 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 (151) hide show
  1. package/CHANGELOG.md +491 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +77 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1437 -384
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  18. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  19. package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
  20. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  21. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +502 -0
  22. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  23. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  24. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
  25. package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
  26. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  27. package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
  28. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +387 -0
  29. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  30. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
  31. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  32. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +640 -0
  33. package/dist/harness-runtime/lib/dd-zcode.mjs +978 -0
  34. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +135 -0
  36. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  37. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  38. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  39. package/dist/harness-runtime/lib/managed-daemon.mjs +296 -0
  40. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  41. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  42. package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
  43. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  44. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  45. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  46. package/dist/harness-runtime/lib/process-json.mjs +71 -0
  47. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  48. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  49. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  50. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  51. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  52. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  53. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  54. package/dist/schemas/agent-profile.schema.json +1 -1
  55. package/dist/schemas/code-review-result.schema.json +1 -1
  56. package/dist/schemas/code-work-batch.schema.json +3 -3
  57. package/dist/schemas/code-work-result.schema.json +3 -3
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +57 -13
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +583 -99
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +165 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +121 -76
  73. package/dist/services/eval-snapshots.js +952 -83
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +165 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +2 -0
  78. package/dist/services/hooks.js +596 -300
  79. package/dist/services/lanes.js +61 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +283 -27
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +73 -28
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/projects.js +18 -5
  93. package/dist/services/prompts.js +25 -16
  94. package/dist/services/protocols.js +64 -20
  95. package/dist/services/recovery-observation-budget.js +61 -0
  96. package/dist/services/recovery-snapshot-database.js +107 -0
  97. package/dist/services/repair-intents.js +40 -0
  98. package/dist/services/review-copy.js +136 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +368 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +231 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +1 -1
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-recovery-runtime.js +69 -0
  112. package/dist/services/run-recovery.js +343 -0
  113. package/dist/services/runs.js +371 -74
  114. package/dist/services/runtime-budget.js +439 -0
  115. package/dist/services/runtime-command.js +43 -0
  116. package/dist/services/runtime-scope-capture.js +50 -0
  117. package/dist/services/runtime-scope-control.js +450 -0
  118. package/dist/services/runtime-scope-resume.js +565 -0
  119. package/dist/services/runtime-scope-stop.js +159 -0
  120. package/dist/services/runtime-scope-worker.js +243 -0
  121. package/dist/services/runtime-service.js +97 -0
  122. package/dist/services/schema-validation.js +10 -8
  123. package/dist/services/sessions.js +35 -69
  124. package/dist/services/stage-blocker.js +17 -7
  125. package/dist/services/stage-context.js +44 -18
  126. package/dist/services/stage-lifecycle.js +99 -102
  127. package/dist/services/stage-pause.js +84 -55
  128. package/dist/services/stage-work-graph.js +35 -0
  129. package/dist/services/usage.js +37 -93
  130. package/dist/services/vnext-code-review.js +190 -80
  131. package/dist/services/vnext-code.js +283 -248
  132. package/dist/services/vnext-execution-profile.js +5 -3
  133. package/dist/services/vnext-fanout.js +172 -17
  134. package/dist/services/vnext-merge.js +296 -105
  135. package/dist/services/vnext-plan-review.js +141 -94
  136. package/dist/services/vnext-plan.js +142 -69
  137. package/dist/services/vnext-protocolize.js +132 -57
  138. package/dist/services/vnext-specify.js +95 -65
  139. package/dist/services/work-registry.js +891 -162
  140. package/dist/services/workspace-bootstrap.js +76 -0
  141. package/dist/services/workspace-files.js +83 -0
  142. package/dist/services/worktrees.js +88 -36
  143. package/dist/shared/entity-references.js +8 -0
  144. package/dist/shared/errors.js +12 -0
  145. package/dist/storage/database.js +459 -32
  146. package/dist/storage/paths.js +17 -4
  147. package/dist/storage/work-references.js +23 -0
  148. package/dist/storage/writer-contract.js +77 -0
  149. package/dist/storage/writer-migration.js +102 -0
  150. package/package.json +24 -13
  151. package/tools/repair-paused-run-status.mjs +59 -0
@@ -0,0 +1,387 @@
1
+ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
+ import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
3
+ import { observedTimeout } from "./observation-clock.mjs";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
6
+ import { spawn } from "node:child_process";
7
+ import net from "node:net";
8
+ import { durableDaemonDispatch as dispatchOperation } from "./daemon-operations.mjs";
9
+ import { waitForSettlement } from "./session-settlement.mjs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+
13
+ import { AcpBridge } from "./dd-zcode.mjs";
14
+ import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
15
+ import { prepareRuntimeOwner, cleanupFailedStart, closeManagedBridge, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
16
+
17
+ const REQUEST_SCHEMA = "dd-grok/daemon-request@1";
18
+ const RESPONSE_SCHEMA = "dd-grok/daemon-response@1";
19
+ const STATE_SCHEMA = "dd-grok/daemon-state@1";
20
+ // Normalize raw socket callers too, before the shared layer reserves capacity.
21
+ export function durableDaemonDispatch(root, request, action, observe) {
22
+ return dispatchOperation(root, request.operation === "session.resume" ? { ...request, operation: "session.inspect" } : request, action, observe);
23
+ }
24
+ class DaemonError extends Error { constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; } }
25
+ function absolute(value, label) { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); }
26
+ function locations(stateDir) { const dir = absolute(stateDir, "--state-dir"); const localSocket = path.join(dir, "daemon.sock"); return { dir, socket: Buffer.byteLength(localSocket) < 100 ? localSocket : `/tmp/dd-grok-${createHash("sha256").update(dir).digest("hex").slice(0, 24)}.sock`, state: path.join(dir, "daemon.json"), log: path.join(dir, "daemon.log"), home: path.join(dir, "grok-home") }; }
27
+ async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
28
+ async function writeState(file, value) { const temporary = `${file}.${process.pid}.tmp`; await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(temporary, file); }
29
+ async function removeSocket(socket, dir) { if (socket !== locations(dir).socket) throw new DaemonError("unsafe_socket", "daemon socket does not match its state directory"); try { const info = await lstat(socket); if (!info.isSocket()) throw new DaemonError("unsafe_socket", "refusing to remove a non-socket daemon path"); await unlink(socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
30
+ function errorPayload(error) { return { code: error.code ?? "operation_failed", message: error.message ?? String(error), retryable: error.retryable === true, ...(error.details === undefined ? {} : { details: error.details }) }; }
31
+ function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
32
+
33
+ function executable(command, args = []) { const resolved = command ?? process.env.DD_GROK_BIN ?? "grok"; return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args }; }
34
+ function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}), ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}) }; }
35
+ function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
36
+
37
+ async function writeIsolatedConfig(paths) {
38
+ const disabled = ["skills", "rules", "agents", "mcps", "hooks", "sessions"].map((key) => `${key} = false`).join("\n");
39
+ await writeFile(path.join(paths.home, "config.toml"), `# Managed by dd-grok. Do not add user configuration here.\n[cli]\nauto_update = false\n\n[compat.claude]\n${disabled}\n\n[compat.cursor]\n${disabled}\n\n[compat.codex]\nsessions = false\n`, { mode: 0o600 });
40
+ }
41
+
42
+ async function inspectIsolatedConfig(config) {
43
+ const target = executable(config.bin, ["inspect", "--json"]);
44
+ const raw = await new Promise((resolve, reject) => {
45
+ const child = spawn(target.command, target.args, { cwd: config.cwd, env: isolatedEnv(config), stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = "";
46
+ child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk)); child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
47
+ child.on("error", reject); child.on("close", (code) => code === 0 ? resolve(stdout) : reject(new DaemonError("grok_inspect_failed", stderr.trim() || `${target.command} exited ${code}`)));
48
+ });
49
+ let report; try { report = JSON.parse(raw); } catch (error) { throw new DaemonError("grok_inspect_invalid", `grok inspect did not return JSON: ${error.message}`); }
50
+ const foreign = [];
51
+ const checkSource = (kind, value, roots) => { const candidate = value?.source?.path ?? value?.path ?? value?.filePath; if (candidate && !roots.some((root) => within(candidate, root))) foreign.push({ kind, path: candidate }); };
52
+ for (const layer of report.configSources?.layers ?? []) checkSource("config", layer, [config.grokHome]);
53
+ for (const hook of report.hooks ?? []) checkSource("hook", hook, [config.grokHome]);
54
+ for (const skill of report.skills ?? []) checkSource("skill", skill, [config.grokHome]);
55
+ for (const agent of report.agents ?? []) checkSource("agent", agent, [config.grokHome]);
56
+ for (const instruction of report.projectInstructions ?? []) checkSource("instruction", instruction, [config.cwd, config.projectRoot]);
57
+ const compatEnabled = (report.externalCompat?.cells ?? []).filter((cell) => ["claude", "cursor", "codex"].includes(cell.vendor) && cell.enabled).map((cell) => `${cell.vendor}.${cell.surface}`);
58
+ const violations = [
59
+ ...(foreign.length ? [{ kind: "foreign_sources", foreign }] : []),
60
+ ...((report.plugins ?? []).length ? [{ kind: "plugins_loaded", count: report.plugins.length }] : []),
61
+ ...((report.mcpServers ?? []).length ? [{ kind: "mcp_loaded", count: report.mcpServers.length }] : []),
62
+ ...((report.permissions?.sources ?? []).length ? [{ kind: "permission_sources", count: report.permissions.sources.length }] : []),
63
+ ...(report.externalCompat?.remoteSettingsLoaded ? [{ kind: "remote_compat" }] : []),
64
+ ...(compatEnabled.length ? [{ kind: "compat_enabled", values: compatEnabled }] : []),
65
+ ...((report.configWarnings ?? []).length ? [{ kind: "config_warnings", warnings: report.configWarnings }] : [])
66
+ ];
67
+ if (violations.length) throw new DaemonError("grok_config_not_isolated", "Grok Build loaded configuration outside the managed isolation boundary", false, { violations });
68
+ return { checked_at: new Date().toISOString(), config_path: path.join(config.grokHome, "config.toml"), hooks: (report.hooks ?? []).length, skills: (report.skills ?? []).length, agents: (report.agents ?? []).length, config_layers: (report.configSources?.layers ?? []).map((layer) => layer.path), compatibility: "disabled" };
69
+ }
70
+
71
+ async function writeHook(paths, config, entryPath) {
72
+ const hookDir = path.join(paths.home, "hooks"); await mkdir(hookDir, { recursive: true, mode: 0o700 });
73
+ const command = [shellQuote(process.execPath), shellQuote(entryPath), "hook", "handle", "--state-dir", shellQuote(paths.dir), "--project-root", shellQuote(config.projectRoot), "--dd-flow-bin", shellQuote(config.ddFlowBin), "--dd-flow-home", shellQuote(config.ddFlowHome)].join(" ");
74
+ const hook = { type: "command", command, timeout: 30 };
75
+ // ACP exposes the native terminal as run_terminal_command; interactive
76
+ // Grok clients still use Bash. Register both rather than assuming one UI.
77
+ await writeFile(path.join(hookDir, "dd-flow.json"), `${JSON.stringify({ hooks: { PreToolUse: ["Bash", "run_terminal_command"].map((matcher) => ({ matcher, hooks: [hook] })) } }, null, 2)}\n`, { mode: 0o600 });
78
+ }
79
+
80
+ async function observeAcpToolCall(config, identity, message) {
81
+ // Grok's synchronous native hook owns admission. This ACP mirror is
82
+ // observational only; forwarding it used to create a second receipt after
83
+ // the command had already run.
84
+ void config; void identity; void message;
85
+ }
86
+
87
+ async function copyAuth(paths, source) {
88
+ const target = path.join(paths.home, "auth.json");
89
+ try { await copyFile(source, target); await chmod(target, 0o600); return target; }
90
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
91
+ }
92
+
93
+ const ARCHIVE_SCHEMA = "dd-grok/session-archive@1";
94
+ function sessionDirectory(home, sourceCwd, sessionId) {
95
+ if (!/^[A-Za-z0-9-]+$/.test(String(sessionId))) throw new DaemonError("invalid_session_id", "Grok Build session id is unsafe");
96
+ return path.join(home, "sessions", encodeURIComponent(absolute(sourceCwd, "Grok Build source cwd")), String(sessionId));
97
+ }
98
+ async function privateTree(root) {
99
+ const info = await lstat(root);
100
+ if (info.isSymbolicLink()) throw new DaemonError("unsafe_session_archive", "session archive must not contain symbolic links");
101
+ if (info.isDirectory()) {
102
+ await chmod(root, 0o700);
103
+ for (const entry of await readdir(root)) await privateTree(path.join(root, entry));
104
+ } else if (info.isFile()) await chmod(root, 0o600);
105
+ else throw new DaemonError("unsafe_session_archive", "session archive contains an unsupported filesystem entry");
106
+ }
107
+ async function mustNotExist(target, code, message) {
108
+ try { await lstat(target); throw new DaemonError(code, message); }
109
+ catch (error) { if (error.code === "ENOENT") return; throw error; }
110
+ }
111
+ async function exportSessionArchive(paths, sessionId, sourceCwd, archiveDir, versions) {
112
+ const destination = absolute(archiveDir, "--archive-dir");
113
+ await mustNotExist(destination, "archive_exists", "session archive directory already exists");
114
+ const source = sessionDirectory(paths.home, sourceCwd, sessionId);
115
+ const sourceInfo = await lstat(source);
116
+ if (!sourceInfo.isDirectory()) throw new DaemonError("session_archive_missing", "Grok Build session files are unavailable for export");
117
+ await privateTree(source);
118
+ await mkdir(destination, { recursive: false, mode: 0o700 });
119
+ await cp(source, path.join(destination, "session"), { recursive: true, errorOnExist: true });
120
+ const manifest = { schema_id: ARCHIVE_SCHEMA, provider_session_id: sessionId, source_cwd: sourceCwd, grok_version: versions.grok, exported_at: new Date().toISOString() };
121
+ await writeFile(path.join(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
122
+ await privateTree(destination);
123
+ return { archive_dir: destination, ...manifest };
124
+ }
125
+ async function readSessionArchiveManifest(archiveDir) {
126
+ const source = absolute(archiveDir, "--session-archive");
127
+ let manifest;
128
+ try { manifest = JSON.parse(await readFile(path.join(source, "manifest.json"), "utf8")); }
129
+ catch (error) { throw new DaemonError("invalid_session_archive", `cannot read session archive manifest: ${error.message}`); }
130
+ if (manifest?.schema_id !== ARCHIVE_SCHEMA || !manifest.provider_session_id || !manifest.source_cwd) throw new DaemonError("invalid_session_archive", "session archive manifest is incompatible");
131
+ return { source, manifest };
132
+ }
133
+ async function materializeSessionArchive(paths, archiveDir) {
134
+ if (!archiveDir) return null;
135
+ const { source, manifest } = await readSessionArchiveManifest(archiveDir);
136
+ const target = sessionDirectory(paths.home, manifest.source_cwd, manifest.provider_session_id);
137
+ await mustNotExist(target, "session_archive_collision", "managed Grok home already contains this session");
138
+ const archiveSession = path.join(source, "session");
139
+ const sourceInfo = await lstat(archiveSession);
140
+ if (!sourceInfo.isDirectory()) throw new DaemonError("invalid_session_archive", "session archive payload is missing");
141
+ await privateTree(archiveSession);
142
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
143
+ await cp(archiveSession, target, { recursive: true, errorOnExist: true });
144
+ await privateTree(target);
145
+ return { schema_id: ARCHIVE_SCHEMA, provider_session_id: manifest.provider_session_id, source_cwd: manifest.source_cwd, grok_version: manifest.grok_version ?? null, archive_dir: source };
146
+ }
147
+
148
+ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
149
+ // Grok resume observes an already-owned Session; it never starts a turn.
150
+ if (operation === "session.resume") operation = "session.inspect";
151
+ const { socket } = locations(stateDir); const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
152
+ return await new Promise((resolve, reject) => {
153
+ const client = net.createConnection(socket); let buffer = ""; let settled = false;
154
+ const timer = operation === "session.prompt" ? null : observedTimeout(() => finish(new DaemonError("daemon_timeout", `${operation} timed out`, true)), timeoutMs);
155
+ const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(value); };
156
+ client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("data", (chunk) => { buffer += chunk; const newline = buffer.indexOf("\n"); if (newline < 0) return; try { const response = JSON.parse(buffer.slice(0, newline)); if (response.schema_id !== RESPONSE_SCHEMA || response.id !== request.id) return finish(new DaemonError("daemon_protocol_mismatch", "invalid daemon response")); if (!response.ok) return finish(new DaemonError(response.error?.code ?? "operation_failed", response.error?.message ?? "daemon operation failed", response.error?.retryable, response.error?.details)); finish(null, response.result); } catch (error) { finish(new DaemonError("daemon_protocol_mismatch", error.message)); } }); client.on("error", (error) => finish(new DaemonError("daemon_not_running", error.message, true))); client.on("end", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true))); client.on("close", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true)));
157
+ });
158
+ }
159
+
160
+ function sameConfig(state, config) { return ["cwd", "journal", "bin", "authSource", "model", "reasoning", "mode", "ddFlowBin", "ddFlowHome", "projectRoot", "sessionArchive", "resourceHome", "noFlow"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null)); }
161
+ export async function startDaemon(options) {
162
+ const paths = locations(options.stateDir); const archive = options.sessionArchive ? await readSessionArchiveManifest(options.sessionArchive) : null; const archiveCwd = archive ? absolute(archive.manifest.source_cwd, "Grok Build archive source cwd") : null; const cwd = options.cwd ? absolute(options.cwd, "--cwd") : archiveCwd; if (!cwd) throw new DaemonError("invalid_path", "--cwd must be an absolute path when no session archive is supplied"); if (archiveCwd && cwd !== archiveCwd) throw new DaemonError("session_archive_cwd_mismatch", "Grok Build session archives must start from their recorded source cwd", false, { archive_cwd: archiveCwd, requested_cwd: cwd }); const noFlow = options.noFlow === true; const authSource = options.authPath ? absolute(options.authPath, "--auth-path") : path.join(os.homedir(), ".grok", "auth.json"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? process.env.DD_GROK_BIN ?? "grok", authSource, authPath: path.join(paths.home, "auth.json"), model: String(options.model ?? ""), reasoning: String(options.reasoning ?? ""), mode: options.mode ?? "bypassPermissions", ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), projectRoot: noFlow ? cwd : absolute(options.projectRoot, "--project-root"), resourceHome: noFlow ? null : (process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null), env: noFlow ? {} : { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") }, sessionArchive: archive?.source ?? null, grokHome: paths.home, noFlow };
163
+ await prepareRuntimeOwner(config, paths.dir);
164
+ if (!config.model || !config.reasoning) throw new DaemonError("profile_required", "--model and --reasoning are required");
165
+ await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
166
+ try { const status = await callDaemon(paths.dir, "daemon.ready", {}, 5000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); if (!status.ready) throw new DaemonError("daemon_start_in_progress", "an existing daemon is still starting", true); return { ...status, already_running: true }; } catch (error) { if (error.code !== "daemon_not_running") throw error; }
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
+ 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
+ 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 = { 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
+ 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
+ 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
+ catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
174
+ }
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
+
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((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
+ 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, 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
+ }
241
+ observeSubagentEvent(message) {
242
+ const root = message?.params?.sessionId, update = message?.params?.update;
243
+ const kind = String(update?.sessionUpdate ?? update?.type ?? message?.method ?? "");
244
+ if (!root) return;
245
+ if (/subagent/i.test(kind)) {
246
+ for (const value of [update?.subagent, update?.subagentInfo, update?.child, update?.result, update]) this.recordDescendant(value, root, "x.ai/subagent/event", /fail|error/i.test(kind) ? "failed" : /cancel/i.test(kind) ? "cancelled" : /complete|finish|done/i.test(kind) ? "completed" : "running");
247
+ }
248
+ // list_running omits completed children. Retain native tool receipts, never
249
+ // extract identities from the model's prose or from arbitrary shell output.
250
+ this.subagentCalls ??= new Map();
251
+ const key = `${root}:${update?.toolCallId}`;
252
+ const tool = update?._meta?.["x.ai/tool"]?.name ?? (kind === "tool_call" ? update.title : null);
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;
256
+ if (kind !== "tool_call_update" || update.status !== "completed") return;
257
+ if (name === "spawn_subagent" && output?.type === "Text") {
258
+ const id = /^subagent_id:\s*(\S+)\s*$/m.exec(output.text ?? "")?.[1];
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");
260
+ }
261
+ if (name === "get_command_or_subagent_output" && output?.type === "TaskOutput") {
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;
281
+ }
282
+ }
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); }
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)); }
285
+ running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
286
+ treeRunning(result) {
287
+ const evidence = result?.evidence ?? result;
288
+ const children = evidence?.subagents?.subagents;
289
+ return this.rootRunning({ ...result, ...evidence }) || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error);
290
+ }
291
+ rootRunning(result) {
292
+ const status = String(result?.info?.status ?? result?.info?.state ?? result?.info?.data?.status ?? "unknown").toLowerCase();
293
+ // Grok 1.0.17 omits status from session/info. Only a durable native
294
+ // creation/end_turn receipt can then establish an idle root. Dispatch
295
+ // clears that receipt before sending any subsequent productive request.
296
+ const receipt = this.sessions.get(result?.provider_session_id)?.native_root_receipt;
297
+ if (status === "unknown" && ["created", "end_turn", "cancelled"].includes(receipt?.kind)) return false;
298
+ return !["completed", "failed", "cancelled", "stopped", "idle"].includes(status);
299
+ }
300
+ async refreshTree(cancel = false) {
301
+ const running = [];
302
+ for (const session of [...this.sessions.values()].filter(item => !item.parent_provider_session_id)) {
303
+ const id = session.provider_session_id;
304
+ const before = new Map(this.descendants);
305
+ let observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
306
+ this.track(observed, before);
307
+ const rootActive = Boolean(this.active);
308
+ if (cancel && (rootActive || this.treeRunning(observed))) {
309
+ await cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
310
+ const beforeCancelledInspection = new Map(this.descendants);
311
+ observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
312
+ this.track(observed, beforeCancelledInspection);
313
+ }
314
+ if (this.active || this.treeRunning(observed)) running.push(id);
315
+ }
316
+ await this.persist({ active_tree: running.length > 0 || Boolean(this.active) });
317
+ return running;
318
+ }
319
+ async requireSettled() { const running = await this.refreshTree(); if (running.length) throw new DaemonError("tree_not_settled", "daemon still owns a running Session tree", false, { sessions: running }); }
320
+ async productive(name, task, sessionId) {
321
+ if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
322
+ this.active = name;
323
+ // Only a new native turn invalidates its own root proof. Creation, fork
324
+ // and local archive export must not erase an unrelated Session's receipt.
325
+ if (name === "session.prompt" && this.sessions.has(sessionId)) this.sessions.get(sessionId).native_root_receipt = null;
326
+ try {
327
+ await this.persist({ active_tree: true, active_operation: name });
328
+ const before = new Map(this.descendants);
329
+ const result = await task();
330
+ this.track(result, before);
331
+ const session = this.sessions.get(result.provider_session_id);
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 };
333
+ // Archive requires an idle tree before entering this exclusive section
334
+ // and performs no native work. Its file receipt is not tree evidence.
335
+ const running = name === "session.archive" ? false : this.treeRunning(result);
336
+ await this.persist({ active_tree: running, active_operation: null });
337
+ return { ...result, descendants: this.topology(result.provider_session_id), settled: !running };
338
+ } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; }
339
+ finally { this.active = null; }
340
+ }
341
+ async dispatch(operation, params) {
342
+ const assertDispatch = captureDispatchGuard(this);
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 };
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 }; }
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) ?? {} }; }
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; });
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); }
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; }); }
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) }; }
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; }
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); }); }
352
+ if (operation === "daemon.stop") {
353
+ if (params.cancelTree) cancelPendingDispatch(this);
354
+ await waitForSettlement({
355
+ knownSessions: () => [...this.sessions.values()].filter(s => !s.parent_provider_session_id).map(s => s.provider_session_id),
356
+ observe: async () => ({ sessions: await this.refreshTree(), active: Boolean(this.active), active_operation: this.active }),
357
+ ...(params.cancelTree ? { cancel: id => cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true })) } : {})
358
+ });
359
+ await this.persist({ active_tree: false, shutdown_state: "stopping" });
360
+ return { daemon_id: this.state.daemon_id, stopped: true, clean: true, _shutdown: true };
361
+ }
362
+ throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
363
+ }
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); }
365
+ }
366
+
367
+ export async function serveDaemon(stateDir) {
368
+ const paths = locations(stateDir); const state = await readState(paths.dir); if (!state || state.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing or incompatible"); let runtime;
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) => {
370
+ const sessionId = message?.params?.sessionId;
371
+ if (!runtime || !sessionId) return;
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
+ }
380
+ const session = runtime.sessions.get(sessionId);
381
+ const rootProviderSessionId = session?.root_provider_session_id ?? sessionId;
382
+ await observeAcpToolCall(state.config, { daemonId: state.daemon_id, rootProviderSessionId, parentProviderSessionId: session?.parent_provider_session_id ?? null, observedProfile: bridge.modelProfiles?.get(sessionId) ?? {} }, message);
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);
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(() => {});
386
+ }
387
+ import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -0,0 +1,174 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { AcpBridge } from "./dd-zcode.mjs";
6
+ import { checkObservedProfile, observeModel } from "./model-observations.mjs";
7
+
8
+ const GROK_HARNESS_CONTRACT = "dd-grok-harness@1";
9
+
10
+ function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
11
+ function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
12
+ function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
13
+ function executable(command, args = []) { const resolved = command ?? process.env.DD_GROK_BIN ?? "grok"; return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args }; }
14
+
15
+ async function commandOutput(command, args) {
16
+ const target = executable(command, args);
17
+ return await new Promise((resolve, reject) => {
18
+ const child = spawn(target.command, target.args, { stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = "";
19
+ child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk)); child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
20
+ child.on("error", reject); child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
21
+ });
22
+ }
23
+
24
+ function profile(options) { return { provider: "xai", model: options.model ?? null, reasoning: options.reasoning ?? null, mode: options.mode ?? "bypassPermissions" }; }
25
+ function requiredProfile(options) { const result = profile(options); text(result.model, "--model"); text(result.reasoning, "--reasoning"); return result; }
26
+ function requireJournal(options) { absolute(options.journal, "--journal"); }
27
+ function receipt(options, value) { return options.daemonId ? { daemon_id: options.daemonId, ...value } : value; }
28
+ function unwrap(value) { return value?.result ?? value ?? {}; }
29
+ function extension(method) { return method.startsWith("_") ? method : `_${method}`; }
30
+ function usageScope(options, providerSessionId) { return providerSessionId === options.rootProviderSessionId ? "execution_tree_inclusive" : "physical_session"; }
31
+ function emptyToolCalls() { return { total: 0, failures: 0, by_tool: {} }; }
32
+ function sessionToolCalls(options, bridge, sessionId) { return options.toolUsage ? (options.toolUsage.get(sessionId) ?? emptyToolCalls()) : bridge.toolSummary(sessionId); }
33
+ function recordToolCalls(options, bridge, sessionId, cursor) {
34
+ const result = options.toolUsage
35
+ ? sessionToolCalls(options, bridge, sessionId)
36
+ : typeof bridge.toolSummarySince === "function"
37
+ ? bridge.toolSummarySince(cursor, sessionId)
38
+ : bridge.toolSummary(sessionId);
39
+ options.toolUsage?.set(sessionId, result);
40
+ return result;
41
+ }
42
+
43
+ function bridgeOptions(options) {
44
+ const requested = profile(options);
45
+ const grokHome = options.grokHome ? absolute(options.grokHome, "--grok-home") : null;
46
+ return {
47
+ ...options,
48
+ observationHarness: "grok-acp",
49
+ // AcpBridge defaults to zcode-acp; Grok must always name its own executable.
50
+ bin: options.bin ?? process.env.DD_GROK_BIN ?? "grok",
51
+ commandArgs: ["agent", "--no-leader", "--always-approve", "--model", requested.model ?? "grok-4.6", "--reasoning-effort", requested.reasoning ?? "high", "stdio"],
52
+ env: { ...(grokHome ? { HOME: grokHome, GROK_HOME: grokHome } : {}), ...(options.authPath ? { GROK_AUTH_PATH: absolute(options.authPath, "--auth-path") } : {}) },
53
+ clientInfo: { name: "dd-grok", version: "0.1.0" }, permission: "allow"
54
+ };
55
+ }
56
+
57
+ async function withBridge(options, operation) {
58
+ const bridge = new AcpBridge(bridgeOptions(options));
59
+ try { const initialized = await bridge.start(); return await operation(bridge, initialized); }
60
+ finally { await bridge.close(); }
61
+ }
62
+
63
+ export function observedProfile(initialized, requested) {
64
+ const state = initialized?._meta?.modelState ?? {};
65
+ const model = state.currentModelId ?? null;
66
+ const available = (state.availableModels ?? []).find((item) => item?.modelId === model);
67
+ return { provider: null, model, reasoning: available?._meta?.reasoningEffort ?? null, mode: null };
68
+ }
69
+
70
+ export function assertProfile(requested, observed) { return checkObservedProfile(requested, observed); }
71
+
72
+ async function sessionInfo(bridge, sessionId) { return unwrap(await bridge.request(extension("x.ai/session/info"), { sessionId })); }
73
+ async function sessionUsage(bridge, sessionId) { try { return unwrap(await bridge.request(extension("x.ai/session/usage"), { sessionId })); } catch (error) { return { unavailable: true, error: error.message }; } }
74
+ async function subagents(bridge, sessionId) { return unwrap(await bridge.request(extension("x.ai/subagent/list_running"), { sessionId })); }
75
+
76
+ async function inspect(bridge, sessionId, options) {
77
+ const [info, topology, usage] = await Promise.all([sessionInfo(bridge, sessionId), subagents(bridge, sessionId), sessionUsage(bridge, sessionId)]);
78
+ const native = info?.data ?? info ?? {}, model = typeof native.model === "string" ? native.model : native.model?.modelId ?? null;
79
+ const current = model ? { model, provider: native.provider ?? null, reasoning: native.reasoningEffort ?? null, mode: native.mode ?? null } : bridge.modelProfiles?.get(sessionId) ?? { model: null, provider: null, reasoning: null, mode: null };
80
+ await observeModel({ journal: options.journal, harness: "grok-acp", sessionId, requested: profile(options), observed: current, source: model ? "x.ai/session/info" : "native.model_source_incomplete", evidence: model ? "configured" : "unavailable", reason: model ? null : "session_info_omits_model" });
81
+ return { info, subagents: topology, usage, observed_profile: model ? current : { model: null, provider: null, reasoning: null, mode: null } };
82
+ }
83
+
84
+ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
85
+ if (options.noFlow || !options.ddFlowBin || !usage || usage.unavailable) return;
86
+ const measured = usage.usage && typeof usage.usage === "object" ? usage.usage : usage;
87
+ const input = JSON.stringify({ provider_session_id: providerSessionId, daemon_id: options.daemonId ?? null, observed_at: new Date().toISOString(), usage: measured, tool_calls: toolCalls, usage_scope: usageScope(options, providerSessionId), completeness: measured.usageIsIncomplete ? "partial" : "complete" });
88
+ const target = executable(options.ddFlowBin, ["grok", "usage", "ingest", "--project-root", absolute(options.projectRoot, "--project-root"), "--json"]);
89
+ await new Promise((resolve, reject) => {
90
+ const child = spawn(target.command, target.args, { env: { ...process.env, ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") } : {}) }, stdio: ["pipe", "ignore", "pipe"] }); let stderr = "";
91
+ child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk)); child.on("error", reject); child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || "dd-flow grok usage failed"))); child.stdin.end(`${input}\n`);
92
+ });
93
+ }
94
+
95
+ async function forwardUsageBestEffort(options, providerSessionId, usage, toolCalls) {
96
+ try { await forwardUsage(options, providerSessionId, usage, toolCalls); return null; }
97
+ catch (error) { return { code: error.code ?? "usage_ingest_failed", message: error.message ?? String(error) }; }
98
+ }
99
+
100
+ async function identity(bridge, options) {
101
+ const sessionId = text(options.sessionId, "--session-id");
102
+ if (!options.liveSession) await bridge.request("session/load", { sessionId, cwd: absolute(options.cwd, "--cwd"), mcpServers: [] });
103
+ bridge.registerModelSession?.(sessionId, sessionId, profile(options), options.rootProviderSessionId && options.rootProviderSessionId !== sessionId ? options.rootProviderSessionId : null);
104
+ return sessionId;
105
+ }
106
+
107
+ async function cancelTree(bridge, sessionId) {
108
+ const before = await subagents(bridge, sessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } })); const cancellations = [];
109
+ for (const child of before.subagents ?? []) {
110
+ const childId = child.sessionId ?? child.subagentId;
111
+ if (childId) {
112
+ try { cancellations.push(await bridge.request(extension("x.ai/subagent/cancel"), { sessionId, subagentId: childId })); }
113
+ catch (error) { cancellations.push({ session_id: childId, cancelled: false, error: { code: error.code, message: error.message } }); }
114
+ }
115
+ }
116
+ bridge.notify("session/cancel", { sessionId });
117
+ for (let attempt = 0; attempt < 50; attempt += 1) {
118
+ await new Promise((resolve) => setTimeout(resolve, 100));
119
+ const after = await subagents(bridge, sessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
120
+ if (after.observation_error) return { before, after, cancellations, settled: false };
121
+ if (!(after.subagents ?? []).length) {
122
+ const root = await sessionInfo(bridge, sessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
123
+ const status = root?.status ?? root?.state ?? root?.data?.status ?? null;
124
+ return { before, after, cancellations, root_status: status, settled: ["completed", "failed", "cancelled", "stopped", "idle"].includes(String(status).toLowerCase()) };
125
+ }
126
+ }
127
+ const root = await sessionInfo(bridge, sessionId).catch(() => null);
128
+ return { before, after: await subagents(bridge, sessionId).catch(() => ({ subagents: [] })), cancellations, root_status: root?.status ?? root?.state ?? null, settled: false };
129
+ }
130
+
131
+ export async function doctor(options = {}) {
132
+ const raw = await commandOutput(options.bin, ["version", "--json"]); const version = JSON.parse(raw).currentVersion?.split(" ")[0] ?? raw.trim();
133
+ return { compatible: true, observed_runtime: { grok: version, acp: 1, dd_harness_contract: GROK_HARNESS_CONTRACT } };
134
+ }
135
+
136
+ export async function createSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await createSessionWithBridge(bridge, options, initialized)); }
137
+ export async function createSessionWithBridge(bridge, options, initialized) {
138
+ requireJournal(options); const cwd = absolute(options.cwd, "--cwd"); const requested = requiredProfile(options);
139
+ const created = await bridge.request("session/new", { cwd, mcpServers: [], ...(options.authMethodId ? { authMethodId: options.authMethodId } : {}) });
140
+ const providerSessionId = text(created.sessionId, "Grok Build sessionId");
141
+ bridge.registerModelSession?.(providerSessionId, providerSessionId, requested);
142
+ await observeModel({ journal: options.journal, harness: "grok-acp", sessionId: providerSessionId, requested, observed: observedProfile(initialized, requested), source: "acp.initialize.modelState" });
143
+ const profileReceipt = assertProfile(requested, observedProfile(initialized, requested));
144
+ await options.onSessionCreated?.({ provider_session_id: providerSessionId, adapter_session_id: providerSessionId, cwd });
145
+ const local = { ...options, initialized, rootProviderSessionId: providerSessionId }; const initial_usage_error = await forwardUsageBestEffort(local, providerSessionId, await sessionUsage(bridge, providerSessionId), sessionToolCalls(local, bridge, providerSessionId));
146
+ await bridge.flush(); const evidence = await inspect(bridge, providerSessionId, local); evidence.tool_calls = sessionToolCalls(local, bridge, providerSessionId); const usage_ingest_error = await forwardUsageBestEffort(local, providerSessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
147
+ return receipt(options, { harness: "grok-acp", runtime_family: "grok", provider_session_id: providerSessionId, adapter_session_id: providerSessionId, cwd, initialized, profile: profileReceipt, evidence });
148
+ }
149
+
150
+ export async function promptSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await promptSessionWithBridge(bridge, { ...options, initialized })); }
151
+ export async function promptSessionWithBridge(bridge, options) {
152
+ requireJournal(options); const sessionId = await identity(bridge, options); const prompt = text(options.prompt, "prompt"); const requested = requiredProfile(options); const profileReceipt = assertProfile(requested, observedProfile(options.initialized, requested));
153
+ const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; const initial_usage_error = await forwardUsageBestEffort(local, sessionId, await sessionUsage(bridge, sessionId), sessionToolCalls(local, bridge, sessionId));
154
+ options.assertDispatch?.();
155
+ const cursor = bridge.toolCursor(); const textCursor = bridge.assistantTextCursor(sessionId); const turn = await bridge.request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, null, { idleSessionId: sessionId, idleTimeoutMs: options.timeoutMs ?? 600_000 }); await bridge.flush();
156
+ const evidence = await inspect(bridge, sessionId, local); evidence.tool_calls = recordToolCalls(local, bridge, sessionId, cursor); const usage_ingest_error = await forwardUsageBestEffort(local, sessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
157
+ if (!options.allowBackground && (evidence.subagents?.subagents ?? []).length) { await cancelTree(bridge, sessionId); fail("background Grok Build subagents cannot outlive the one-shot dd-grok connection"); }
158
+ return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, assistant_text: bridge.assistantTextSince(sessionId, textCursor), turn, profile: profileReceipt, evidence });
159
+ }
160
+
161
+ export async function inspectSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await inspectSessionWithBridge(bridge, { ...options, initialized })); }
162
+ export async function inspectSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); const evidence = await inspect(bridge, sessionId, options); evidence.tool_calls = sessionToolCalls(options, bridge, sessionId); const usage_ingest_error = await forwardUsageBestEffort(options, sessionId, evidence.usage, evidence.tool_calls); if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error; return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...evidence }); }
163
+ export async function cancelSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await cancelSessionWithBridge(bridge, options)); }
164
+ export async function cancelSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...await cancelTree(bridge, sessionId) }); }
165
+ export async function forkSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await forkSessionWithBridge(bridge, options)); }
166
+ export async function forkSessionWithBridge(bridge, options) {
167
+ requireJournal(options); const sessionId = await identity(bridge, options); const target = options.target;
168
+ if (!target || typeof target !== "object" || !target.newCwd) fail("explicit --target-json with newCwd is required");
169
+ if ((await subagents(bridge, sessionId)).subagents?.length) fail("cannot fork while Grok Build subagents are running", "tree_not_settled");
170
+ const source = await sessionInfo(bridge, sessionId);
171
+ const result = unwrap(await bridge.request(extension("x.ai/session/fork"), { sourceSessionId: sessionId, sourceCwd: source.cwd ? absolute(source.cwd, "Grok Build source cwd") : absolute(options.cwd, "--cwd"), ...target }));
172
+ const child = text(result.newSessionId, "Grok Build fork newSessionId"); return receipt(options, { harness: "grok-acp", parent_provider_session_id: sessionId, provider_session_id: child, adapter_session_id: child, target, fork: result });
173
+ }
174
+ export async function promptFromFile(file) { return await readFile(absolute(file, "--prompt-file"), "utf8"); }