@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1342 -375
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +553 -285
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +828 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +342 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +65 -23
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +707 -156
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -0,0 +1,274 @@
|
|
|
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 } 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, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, heartbeatDaemonProcess, registerDaemonProcess, 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
|
+
class DaemonError extends Error { constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; } }
|
|
21
|
+
function absolute(value, label) { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); }
|
|
22
|
+
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") }; }
|
|
23
|
+
async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
24
|
+
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); }
|
|
25
|
+
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; } }
|
|
26
|
+
function errorPayload(error) { return { code: error.code ?? "operation_failed", message: error.message ?? String(error), retryable: error.retryable === true, ...(error.details === undefined ? {} : { details: error.details }) }; }
|
|
27
|
+
function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
|
|
28
|
+
|
|
29
|
+
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 }; }
|
|
30
|
+
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 } : {}) }; }
|
|
31
|
+
function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
|
|
32
|
+
|
|
33
|
+
async function writeIsolatedConfig(paths) {
|
|
34
|
+
const disabled = ["skills", "rules", "agents", "mcps", "hooks", "sessions"].map((key) => `${key} = false`).join("\n");
|
|
35
|
+
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 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function inspectIsolatedConfig(config) {
|
|
39
|
+
const target = executable(config.bin, ["inspect", "--json"]);
|
|
40
|
+
const raw = await new Promise((resolve, reject) => {
|
|
41
|
+
const child = spawn(target.command, target.args, { cwd: config.cwd, env: isolatedEnv(config), stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = "";
|
|
42
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk)); child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
43
|
+
child.on("error", reject); child.on("close", (code) => code === 0 ? resolve(stdout) : reject(new DaemonError("grok_inspect_failed", stderr.trim() || `${target.command} exited ${code}`)));
|
|
44
|
+
});
|
|
45
|
+
let report; try { report = JSON.parse(raw); } catch (error) { throw new DaemonError("grok_inspect_invalid", `grok inspect did not return JSON: ${error.message}`); }
|
|
46
|
+
const foreign = [];
|
|
47
|
+
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 }); };
|
|
48
|
+
for (const layer of report.configSources?.layers ?? []) checkSource("config", layer, [config.grokHome]);
|
|
49
|
+
for (const hook of report.hooks ?? []) checkSource("hook", hook, [config.grokHome]);
|
|
50
|
+
for (const skill of report.skills ?? []) checkSource("skill", skill, [config.grokHome]);
|
|
51
|
+
for (const agent of report.agents ?? []) checkSource("agent", agent, [config.grokHome]);
|
|
52
|
+
for (const instruction of report.projectInstructions ?? []) checkSource("instruction", instruction, [config.cwd, config.projectRoot]);
|
|
53
|
+
const compatEnabled = (report.externalCompat?.cells ?? []).filter((cell) => ["claude", "cursor", "codex"].includes(cell.vendor) && cell.enabled).map((cell) => `${cell.vendor}.${cell.surface}`);
|
|
54
|
+
const violations = [
|
|
55
|
+
...(foreign.length ? [{ kind: "foreign_sources", foreign }] : []),
|
|
56
|
+
...((report.plugins ?? []).length ? [{ kind: "plugins_loaded", count: report.plugins.length }] : []),
|
|
57
|
+
...((report.mcpServers ?? []).length ? [{ kind: "mcp_loaded", count: report.mcpServers.length }] : []),
|
|
58
|
+
...((report.permissions?.sources ?? []).length ? [{ kind: "permission_sources", count: report.permissions.sources.length }] : []),
|
|
59
|
+
...(report.externalCompat?.remoteSettingsLoaded ? [{ kind: "remote_compat" }] : []),
|
|
60
|
+
...(compatEnabled.length ? [{ kind: "compat_enabled", values: compatEnabled }] : []),
|
|
61
|
+
...((report.configWarnings ?? []).length ? [{ kind: "config_warnings", warnings: report.configWarnings }] : [])
|
|
62
|
+
];
|
|
63
|
+
if (violations.length) throw new DaemonError("grok_config_not_isolated", "Grok Build loaded configuration outside the managed isolation boundary", false, { violations });
|
|
64
|
+
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" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function writeHook(paths, config, entryPath) {
|
|
68
|
+
const hookDir = path.join(paths.home, "hooks"); await mkdir(hookDir, { recursive: true, mode: 0o700 });
|
|
69
|
+
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(" ");
|
|
70
|
+
const hook = { type: "command", command, timeout: 30 };
|
|
71
|
+
// ACP exposes the native terminal as run_terminal_command; interactive
|
|
72
|
+
// Grok clients still use Bash. Register both rather than assuming one UI.
|
|
73
|
+
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 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function observeAcpToolCall(config, identity, message) {
|
|
77
|
+
// Grok's synchronous native hook owns admission. This ACP mirror is
|
|
78
|
+
// observational only; forwarding it used to create a second receipt after
|
|
79
|
+
// the command had already run.
|
|
80
|
+
void config; void identity; void message;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function copyAuth(paths, source) {
|
|
84
|
+
const target = path.join(paths.home, "auth.json");
|
|
85
|
+
try { await copyFile(source, target); await chmod(target, 0o600); return target; }
|
|
86
|
+
catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ARCHIVE_SCHEMA = "dd-grok/session-archive@1";
|
|
90
|
+
function sessionDirectory(home, sourceCwd, sessionId) {
|
|
91
|
+
if (!/^[A-Za-z0-9-]+$/.test(String(sessionId))) throw new DaemonError("invalid_session_id", "Grok Build session id is unsafe");
|
|
92
|
+
return path.join(home, "sessions", encodeURIComponent(absolute(sourceCwd, "Grok Build source cwd")), String(sessionId));
|
|
93
|
+
}
|
|
94
|
+
async function privateTree(root) {
|
|
95
|
+
const info = await lstat(root);
|
|
96
|
+
if (info.isSymbolicLink()) throw new DaemonError("unsafe_session_archive", "session archive must not contain symbolic links");
|
|
97
|
+
if (info.isDirectory()) {
|
|
98
|
+
await chmod(root, 0o700);
|
|
99
|
+
for (const entry of await readdir(root)) await privateTree(path.join(root, entry));
|
|
100
|
+
} else if (info.isFile()) await chmod(root, 0o600);
|
|
101
|
+
else throw new DaemonError("unsafe_session_archive", "session archive contains an unsupported filesystem entry");
|
|
102
|
+
}
|
|
103
|
+
async function mustNotExist(target, code, message) {
|
|
104
|
+
try { await lstat(target); throw new DaemonError(code, message); }
|
|
105
|
+
catch (error) { if (error.code === "ENOENT") return; throw error; }
|
|
106
|
+
}
|
|
107
|
+
async function exportSessionArchive(paths, sessionId, sourceCwd, archiveDir, versions) {
|
|
108
|
+
const destination = absolute(archiveDir, "--archive-dir");
|
|
109
|
+
await mustNotExist(destination, "archive_exists", "session archive directory already exists");
|
|
110
|
+
const source = sessionDirectory(paths.home, sourceCwd, sessionId);
|
|
111
|
+
const sourceInfo = await lstat(source);
|
|
112
|
+
if (!sourceInfo.isDirectory()) throw new DaemonError("session_archive_missing", "Grok Build session files are unavailable for export");
|
|
113
|
+
await privateTree(source);
|
|
114
|
+
await mkdir(destination, { recursive: false, mode: 0o700 });
|
|
115
|
+
await cp(source, path.join(destination, "session"), { recursive: true, errorOnExist: true });
|
|
116
|
+
const manifest = { schema_id: ARCHIVE_SCHEMA, provider_session_id: sessionId, source_cwd: sourceCwd, grok_version: versions.grok, exported_at: new Date().toISOString() };
|
|
117
|
+
await writeFile(path.join(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
118
|
+
await privateTree(destination);
|
|
119
|
+
return { archive_dir: destination, ...manifest };
|
|
120
|
+
}
|
|
121
|
+
async function readSessionArchiveManifest(archiveDir) {
|
|
122
|
+
const source = absolute(archiveDir, "--session-archive");
|
|
123
|
+
let manifest;
|
|
124
|
+
try { manifest = JSON.parse(await readFile(path.join(source, "manifest.json"), "utf8")); }
|
|
125
|
+
catch (error) { throw new DaemonError("invalid_session_archive", `cannot read session archive manifest: ${error.message}`); }
|
|
126
|
+
if (manifest?.schema_id !== ARCHIVE_SCHEMA || !manifest.provider_session_id || !manifest.source_cwd) throw new DaemonError("invalid_session_archive", "session archive manifest is incompatible");
|
|
127
|
+
return { source, manifest };
|
|
128
|
+
}
|
|
129
|
+
async function materializeSessionArchive(paths, archiveDir) {
|
|
130
|
+
if (!archiveDir) return null;
|
|
131
|
+
const { source, manifest } = await readSessionArchiveManifest(archiveDir);
|
|
132
|
+
const target = sessionDirectory(paths.home, manifest.source_cwd, manifest.provider_session_id);
|
|
133
|
+
await mustNotExist(target, "session_archive_collision", "managed Grok home already contains this session");
|
|
134
|
+
const archiveSession = path.join(source, "session");
|
|
135
|
+
const sourceInfo = await lstat(archiveSession);
|
|
136
|
+
if (!sourceInfo.isDirectory()) throw new DaemonError("invalid_session_archive", "session archive payload is missing");
|
|
137
|
+
await privateTree(archiveSession);
|
|
138
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
139
|
+
await cp(archiveSession, target, { recursive: true, errorOnExist: true });
|
|
140
|
+
await privateTree(target);
|
|
141
|
+
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 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
145
|
+
const { socket } = locations(stateDir); const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
146
|
+
return await new Promise((resolve, reject) => {
|
|
147
|
+
const client = net.createConnection(socket); let buffer = ""; let settled = false;
|
|
148
|
+
const timer = operation === "session.prompt" ? null : observedTimeout(() => finish(new DaemonError("daemon_timeout", `${operation} timed out`, true)), timeoutMs);
|
|
149
|
+
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(value); };
|
|
150
|
+
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)));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
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)); }
|
|
155
|
+
export async function startDaemon(options) {
|
|
156
|
+
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 };
|
|
157
|
+
await prepareRuntimeOwner(config, paths.dir);
|
|
158
|
+
if (!config.model || !config.reasoning) throw new DaemonError("profile_required", "--model and --reasoning are required");
|
|
159
|
+
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
160
|
+
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); return { ...status, already_running: true }; } catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
|
|
161
|
+
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 });
|
|
162
|
+
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);
|
|
163
|
+
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 });
|
|
164
|
+
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 };
|
|
165
|
+
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();
|
|
166
|
+
try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 15_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); } 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); }
|
|
167
|
+
catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
|
|
168
|
+
}
|
|
169
|
+
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); }
|
|
170
|
+
|
|
171
|
+
export class Runtime {
|
|
172
|
+
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.persisting = Promise.resolve(); }
|
|
173
|
+
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; }
|
|
174
|
+
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) }; }
|
|
175
|
+
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"); }
|
|
176
|
+
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 }); }
|
|
177
|
+
observeSubagentEvent(message) {
|
|
178
|
+
const root = message?.params?.sessionId, update = message?.params?.update;
|
|
179
|
+
const kind = String(update?.sessionUpdate ?? update?.type ?? message?.method ?? "");
|
|
180
|
+
if (!root) return;
|
|
181
|
+
if (/subagent/i.test(kind)) {
|
|
182
|
+
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");
|
|
183
|
+
}
|
|
184
|
+
// list_running omits completed children. Retain native tool receipts, never
|
|
185
|
+
// extract identities from the model's prose or from arbitrary shell output.
|
|
186
|
+
this.subagentCalls ??= new Map();
|
|
187
|
+
const key = `${root}:${update?.toolCallId}`;
|
|
188
|
+
const tool = update?._meta?.["x.ai/tool"]?.name ?? (kind === "tool_call" ? update.title : null);
|
|
189
|
+
if (tool) this.subagentCalls.set(key, tool);
|
|
190
|
+
const name = this.subagentCalls.get(key), output = update?.rawOutput;
|
|
191
|
+
if (kind !== "tool_call_update" || update.status !== "completed") return;
|
|
192
|
+
if (name === "spawn_subagent" && output?.type === "Text") {
|
|
193
|
+
const id = /^subagent_id:\s*(\S+)\s*$/m.exec(output.text ?? "")?.[1];
|
|
194
|
+
if (id) this.recordDescendant({ session_id: id }, root, "x.ai/tool/spawn_subagent", "running");
|
|
195
|
+
}
|
|
196
|
+
if (name === "get_command_or_subagent_output" && output?.type === "TaskOutput") {
|
|
197
|
+
const result = output.Result;
|
|
198
|
+
if (this.descendants.has(result?.task_id)) this.recordDescendant({ session_id: result.task_id, status: result.status }, root, "x.ai/tool/TaskOutput", "unknown");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
topology(root) { return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
|
|
202
|
+
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)); }
|
|
203
|
+
running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
|
|
204
|
+
treeRunning(result) {
|
|
205
|
+
const evidence = result?.evidence ?? result;
|
|
206
|
+
const children = evidence?.subagents?.subagents;
|
|
207
|
+
return this.rootRunning({ ...result, ...evidence }) || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error);
|
|
208
|
+
}
|
|
209
|
+
rootRunning(result) {
|
|
210
|
+
const status = String(result?.info?.status ?? result?.info?.state ?? result?.info?.data?.status ?? "unknown").toLowerCase();
|
|
211
|
+
// Grok 1.0.17 omits status from session/info. Only a durable native
|
|
212
|
+
// creation/end_turn receipt can then establish an idle root. Dispatch
|
|
213
|
+
// clears that receipt before sending any subsequent productive request.
|
|
214
|
+
const receipt = this.sessions.get(result?.provider_session_id)?.native_root_receipt;
|
|
215
|
+
if (status === "unknown" && ["created", "end_turn", "cancelled"].includes(receipt?.kind)) return false;
|
|
216
|
+
return !["completed", "failed", "cancelled", "stopped", "idle"].includes(status);
|
|
217
|
+
}
|
|
218
|
+
async refreshTree(cancel = false) {
|
|
219
|
+
const running = [];
|
|
220
|
+
for (const session of [...this.sessions.values()].filter(item => !item.parent_provider_session_id)) {
|
|
221
|
+
const id = session.provider_session_id;
|
|
222
|
+
let observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
|
|
223
|
+
this.track(observed);
|
|
224
|
+
const rootActive = Boolean(this.active);
|
|
225
|
+
if (cancel && (rootActive || this.treeRunning(observed))) {
|
|
226
|
+
await cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
|
|
227
|
+
observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true }));
|
|
228
|
+
this.track(observed);
|
|
229
|
+
}
|
|
230
|
+
if (this.active || this.treeRunning(observed)) running.push(id);
|
|
231
|
+
}
|
|
232
|
+
await this.persist({ active_tree: running.length > 0 || Boolean(this.active) });
|
|
233
|
+
return running;
|
|
234
|
+
}
|
|
235
|
+
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 }); }
|
|
236
|
+
async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const running = this.treeRunning(result); const session = this.sessions.get(result.provider_session_id); 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 }; await this.persist({ active_tree: running, active_operation: null }); return { ...result, descendants: this.topology(result.provider_session_id), settled: !running }; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; } finally { this.active = null; } }
|
|
237
|
+
async dispatch(operation, params) {
|
|
238
|
+
const assertDispatch = captureDispatchGuard(this);
|
|
239
|
+
if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { observation_error: observationError ?? null, 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 }; }
|
|
240
|
+
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"); session = { provider_session_id: sessionId, adapter_session_id: sessionId, parent_provider_session_id: roots[0].provider_session_id, root_provider_session_id: roots[0].provider_session_id }; this.sessions.set(sessionId, session); await this.persist(); await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); } 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) ?? {} }; }
|
|
241
|
+
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; });
|
|
242
|
+
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; }); }
|
|
243
|
+
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; }); }
|
|
244
|
+
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) }; }
|
|
245
|
+
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; }
|
|
246
|
+
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); }); }
|
|
247
|
+
if (operation === "daemon.stop") {
|
|
248
|
+
if (params.cancelTree) cancelPendingDispatch(this);
|
|
249
|
+
await waitForSettlement({
|
|
250
|
+
knownSessions: () => [...this.sessions.values()].filter(s => !s.parent_provider_session_id).map(s => s.provider_session_id),
|
|
251
|
+
observe: async () => ({ sessions: await this.refreshTree(), active: Boolean(this.active), active_operation: this.active }),
|
|
252
|
+
...(params.cancelTree ? { cancel: id => cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true })) } : {})
|
|
253
|
+
});
|
|
254
|
+
await this.persist({ active_tree: false, shutdown_state: "stopping" });
|
|
255
|
+
return { daemon_id: this.state.daemon_id, stopped: true, clean: true, _shutdown: true };
|
|
256
|
+
}
|
|
257
|
+
throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
|
|
258
|
+
}
|
|
259
|
+
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); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function serveDaemon(stateDir) {
|
|
263
|
+
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;
|
|
264
|
+
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) => {
|
|
265
|
+
const sessionId = message?.params?.sessionId;
|
|
266
|
+
if (!runtime || !sessionId) return;
|
|
267
|
+
runtime.observeToolCall(message); runtime.observeSubagentEvent(message);
|
|
268
|
+
const session = runtime.sessions.get(sessionId);
|
|
269
|
+
const rootProviderSessionId = session?.root_provider_session_id ?? sessionId;
|
|
270
|
+
await observeAcpToolCall(state.config, { daemonId: state.daemon_id, rootProviderSessionId, parentProviderSessionId: session?.parent_provider_session_id ?? null, observedProfile: bridge.modelProfiles?.get(sessionId) ?? {} }, message);
|
|
271
|
+
} }); const initialized = await bridge.start(); runtime = new Runtime(paths, state, bridge, initialized);
|
|
272
|
+
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 }); return await new Promise(() => {});
|
|
273
|
+
}
|
|
274
|
+
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.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"); }
|