@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
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 +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
|
+
import { canonicalNativeOperation } from './adapter-timeouts.mjs';
|
|
3
|
+
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
1
4
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
5
|
+
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
2
6
|
import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
|
|
3
|
-
import {
|
|
7
|
+
import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
|
|
8
|
+
import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
|
|
9
|
+
import { ObservationClock } from "./observation-clock.mjs";
|
|
4
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
11
|
import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
6
12
|
import { spawn } from "node:child_process";
|
|
7
13
|
import net from "node:net";
|
|
8
|
-
import { durableDaemonDispatch as dispatchOperation } from "./daemon-operations.mjs";
|
|
14
|
+
import { durableDaemonDispatch as dispatchOperation, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
9
15
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
10
16
|
import os from "node:os";
|
|
11
17
|
import path from "node:path";
|
|
@@ -13,14 +19,14 @@ import path from "node:path";
|
|
|
13
19
|
import { AcpBridge } from "./dd-zcode.mjs";
|
|
14
20
|
import { observeModel, readModelObservations } from "./model-observations.mjs";
|
|
15
21
|
import { cancelSessionWithBridge, createSessionWithBridge, doctor, forkSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge } from "./dd-grok.mjs";
|
|
16
|
-
import { prepareRuntimeOwner, cleanupFailedDaemonStart,
|
|
22
|
+
import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
17
23
|
|
|
18
24
|
const REQUEST_SCHEMA = "dd-grok/daemon-request@1";
|
|
19
25
|
const RESPONSE_SCHEMA = "dd-grok/daemon-response@1";
|
|
20
26
|
const STATE_SCHEMA = "dd-grok/daemon-state@1";
|
|
21
27
|
// Normalize raw socket callers too, before the shared layer reserves capacity.
|
|
22
28
|
export function durableDaemonDispatch(root, request, action, observe) {
|
|
23
|
-
return dispatchOperation(root,
|
|
29
|
+
return dispatchOperation(root, { ...request, operation: canonicalNativeOperation("grok", request.operation) }, action, observe);
|
|
24
30
|
}
|
|
25
31
|
class DaemonError extends Error { constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; } }
|
|
26
32
|
function absolute(value, label) { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); }
|
|
@@ -28,11 +34,11 @@ function locations(stateDir) { const dir = absolute(stateDir, "--state-dir"); co
|
|
|
28
34
|
async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
29
35
|
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); }
|
|
30
36
|
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; } }
|
|
31
|
-
|
|
37
|
+
const errorPayload = errorRecord;
|
|
32
38
|
function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
|
|
33
39
|
|
|
34
40
|
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 }; }
|
|
35
|
-
export function isolatedEnv(config) { const env = { ...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 } : {}) };
|
|
41
|
+
export function isolatedEnv(config) { const env = { ...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 } : {}) }; env.DD_FLOW_DAEMON_ID = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
|
|
36
42
|
function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
|
|
37
43
|
|
|
38
44
|
async function writeIsolatedConfig(paths) {
|
|
@@ -42,11 +48,7 @@ async function writeIsolatedConfig(paths) {
|
|
|
42
48
|
|
|
43
49
|
async function inspectIsolatedConfig(config) {
|
|
44
50
|
const target = executable(config.bin, ["inspect", "--json"]);
|
|
45
|
-
const raw = await
|
|
46
|
-
const child = spawn(target.command, target.args, { cwd: config.cwd, env: isolatedEnv(config), stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = "";
|
|
47
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk)); child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
48
|
-
child.on("error", reject); child.on("close", (code) => code === 0 ? resolve(stdout) : reject(new DaemonError("grok_inspect_failed", stderr.trim() || `${target.command} exited ${code}`)));
|
|
49
|
-
});
|
|
51
|
+
const raw = await commandText(target.command, target.args, { cwd: config.cwd, env: isolatedEnv(config), phase: "native-inspect" });
|
|
50
52
|
let report; try { report = JSON.parse(raw); } catch (error) { throw new DaemonError("grok_inspect_invalid", `grok inspect did not return JSON: ${error.message}`); }
|
|
51
53
|
const foreign = [];
|
|
52
54
|
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 }); };
|
|
@@ -147,15 +149,9 @@ async function materializeSessionArchive(paths, archiveDir) {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return await new Promise((resolve, reject) => {
|
|
154
|
-
const client = net.createConnection(socket); let buffer = ""; let settled = false;
|
|
155
|
-
const timer = operation === "session.prompt" ? null : observedTimeout(() => finish(new DaemonError("daemon_timeout", `${operation} timed out`, true)), timeoutMs);
|
|
156
|
-
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(value); };
|
|
157
|
-
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)));
|
|
158
|
-
});
|
|
152
|
+
operation = canonicalNativeOperation('grok', operation);
|
|
153
|
+
const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
154
|
+
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'grok', timeoutMs });
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
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)); }
|
|
@@ -169,21 +165,23 @@ export async function startDaemon(options) {
|
|
|
169
165
|
await prepareRuntimeOwner(config, paths.dir);
|
|
170
166
|
if (!config.model || !config.reasoning) throw new DaemonError("profile_required", "--model and --reasoning are required");
|
|
171
167
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
168
|
+
let previous = await readState(paths.dir); assertDaemonRestartable(previous);
|
|
172
169
|
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; }
|
|
173
|
-
|
|
170
|
+
previous = await readState(paths.dir); assertDaemonRestartable(previous);
|
|
171
|
+
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 });
|
|
174
172
|
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);
|
|
175
173
|
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 });
|
|
176
174
|
const daemonId = randomUUID();
|
|
177
175
|
const state = { descendants: previous?.descendants ?? [], schema_id: STATE_SCHEMA, daemon_id: daemonId, 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: { ...config, daemonId }, 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 };
|
|
178
176
|
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(state.config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
179
|
-
try { await confirmDaemonProcess(config, resourceProcess, child); const
|
|
177
|
+
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: 120_000 }); while (!startupClock.sample()) { await new Promise((resolve) => setTimeout(resolve, 100)); const failure = grokStartupFailure(await readState(paths.dir)); if (failure) throw failure; 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); }
|
|
180
178
|
catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.dir), daemonId); throw error; }
|
|
181
179
|
}
|
|
182
|
-
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); }
|
|
180
|
+
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, { readState: () => readState(options.stateDir) }); }
|
|
183
181
|
|
|
184
182
|
export class Runtime {
|
|
185
183
|
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(); }
|
|
186
|
-
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; }
|
|
184
|
+
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.catch(() => {}).then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
|
|
187
185
|
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) }; }
|
|
188
186
|
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"); }
|
|
189
187
|
recordDescendant(value, root, source, fallback) {
|
|
@@ -234,7 +232,7 @@ export class Runtime {
|
|
|
234
232
|
cwd,
|
|
235
233
|
native_root_receipt: prior?.native_root_receipt ?? null
|
|
236
234
|
});
|
|
237
|
-
this.bridge.registerModelSession?.(id, id, this.state.config, parent);
|
|
235
|
+
this.bridge.registerModelSession?.(id, id, this.state.config, parent, true, newAttempt);
|
|
238
236
|
this.descendants.set(id, {
|
|
239
237
|
provider_session_id: id,
|
|
240
238
|
parent_provider_session_id: parent,
|
|
@@ -338,16 +336,24 @@ export class Runtime {
|
|
|
338
336
|
return running;
|
|
339
337
|
}
|
|
340
338
|
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 }); }
|
|
341
|
-
async productive(name, task, sessionId) {
|
|
339
|
+
async productive(name, task, sessionId, operationId = null) {
|
|
340
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
342
341
|
if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
|
|
343
342
|
this.active = name;
|
|
343
|
+
const current = this.nativeHookOperation = { operationId, sessionId, generation: (this.hookGeneration = (this.hookGeneration ?? 0) + 1), requests: new Map() };
|
|
344
|
+
let rejectHook;
|
|
345
|
+
const hookFailed = new Promise((_, reject) => { rejectHook = reject; });
|
|
346
|
+
void hookFailed.catch(() => {});
|
|
347
|
+
current.reject = error => { if (this.nativeHookOperation !== current || current.error) return; current.error = error; this.bridge.lifecycleFailure ??= error; this.bridge.notificationError = this.bridge.lifecycleFailure; this.bridge.rejectAll(this.bridge.lifecycleFailure); rejectHook(this.bridge.lifecycleFailure); };
|
|
348
|
+
const timer = setInterval(() => { void this.observeHookFailures(current).catch(error => current.reject(error)); }, 100);
|
|
344
349
|
// Only a new native turn invalidates its own root proof. Creation, fork
|
|
345
350
|
// and local archive export must not erase an unrelated Session's receipt.
|
|
346
351
|
if (name === "session.prompt" && this.sessions.has(sessionId)) this.sessions.get(sessionId).native_root_receipt = null;
|
|
347
352
|
try {
|
|
348
353
|
await this.persist({ active_tree: true, active_operation: name });
|
|
354
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
349
355
|
const before = new Map(this.descendants);
|
|
350
|
-
const result = await task();
|
|
356
|
+
const result = await Promise.race([task(), hookFailed]);
|
|
351
357
|
this.track(result, before);
|
|
352
358
|
const session = this.sessions.get(result.provider_session_id);
|
|
353
359
|
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 };
|
|
@@ -365,34 +371,63 @@ export class Runtime {
|
|
|
365
371
|
const session = this.sessions.get(sessionId);
|
|
366
372
|
if (session) session.native_root_receipt = { kind: "failed", daemon_id: this.state.daemon_id, request_id: response.request_id };
|
|
367
373
|
}
|
|
368
|
-
await this.persist({ active_tree: true, active_operation: null });
|
|
374
|
+
try { await hookBudget(1_000, null).run(() => this.persist({ active_tree: true, active_operation: null }), "failure_persistence"); }
|
|
375
|
+
catch (storage) { error.cleanup_error = errorRecord(storage); }
|
|
376
|
+
throw error;
|
|
369
377
|
}
|
|
370
|
-
finally { this.active = null; }
|
|
378
|
+
finally { clearInterval(timer); current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.active = null; }
|
|
379
|
+
}
|
|
380
|
+
async observeHookFailures(current = this.nativeHookOperation) {
|
|
381
|
+
if (!current || this.nativeHookOperation !== current || current.reading) return;
|
|
382
|
+
current.reading = true;
|
|
383
|
+
try { await readNativeHookFailures(this.paths.dir, current.requests, current, error => {
|
|
384
|
+
if (!isProvenShellCorrection(error)) current.reject(error);
|
|
385
|
+
}); } finally { current.reading = false; }
|
|
386
|
+
}
|
|
387
|
+
async acceptHook(identity, sessionId, payload) {
|
|
388
|
+
const input = payload?.tool_input ?? payload?.toolInput;
|
|
389
|
+
const command = input?.command ?? input?.cmd;
|
|
390
|
+
if ((payload?.hook_event_name ?? payload?.hookEventName) !== "PreToolUse" || typeof command !== "string" || (!["lifecycle", "auxiliary"].includes(parseLifecycleCommand(command).participation) && !continuationCommand(command))) return identity;
|
|
391
|
+
const current = this.nativeHookOperation, tool = payload.tool_use_id ?? payload.toolUseId ?? payload.tool_call_id;
|
|
392
|
+
const root = current?.sessionId ?? this.loadedSessionId;
|
|
393
|
+
const pending = [...(this.bridge.pending?.values() ?? [])].some(request => request.method === "session/prompt" && request.sessionId === root);
|
|
394
|
+
if (!current || !pending || identity.rootProviderSessionId !== root || !tool) throw new DaemonError("native_hook_unproven", "Native hook has no exact active Session operation");
|
|
395
|
+
const binding = await hookBudget(1_000, null).run(() => acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation, provider_session_id: sessionId, root_provider_session_id: root, tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null }, current), "hook_binding");
|
|
396
|
+
if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
|
|
397
|
+
current.requests.set(binding.request_id, { current, binding });
|
|
398
|
+
return { ...identity, nativeHookBinding: binding };
|
|
371
399
|
}
|
|
372
|
-
async dispatch(operation, params) {
|
|
400
|
+
async dispatch(operation, params, operationId = null) {
|
|
373
401
|
const assertDispatch = captureDispatchGuard(this);
|
|
402
|
+
if (["session.create", "session.prompt", "session.fork", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
374
403
|
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 };
|
|
375
|
-
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 }; }
|
|
376
|
-
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) ?? {} }; }
|
|
377
|
-
if (operation === "
|
|
378
|
-
if (operation === "session.
|
|
404
|
+
if (operation === "daemon.status") { let observationError; if (!this.active && !this.state.shutdown?.phases?.tree) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
|
|
405
|
+
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 await this.acceptHook({ daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }, sessionId, params.payload); }
|
|
406
|
+
if (operation === "hook.failed") { await this.observeHookFailures(); return { observed: true }; }
|
|
407
|
+
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; }, null, operationId);
|
|
408
|
+
if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
|
|
379
409
|
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; }); }
|
|
380
410
|
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) }; }
|
|
381
411
|
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; }
|
|
382
412
|
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); }); }
|
|
383
413
|
if (operation === "daemon.stop") {
|
|
414
|
+
return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), async () => {
|
|
384
415
|
if (params.cancelTree) cancelPendingDispatch(this);
|
|
385
416
|
await waitForSettlement({
|
|
386
417
|
knownSessions: () => [...this.sessions.values()].filter(s => !s.parent_provider_session_id).map(s => s.provider_session_id),
|
|
387
418
|
observe: async () => ({ sessions: await this.refreshTree(), active: Boolean(this.active), active_operation: this.active }),
|
|
388
419
|
...(params.cancelTree ? { cancel: id => cancelSessionWithBridge(this.bridge, this.options({ sessionId: id, liveSession: true })) } : {})
|
|
389
420
|
});
|
|
390
|
-
|
|
391
|
-
|
|
421
|
+
return { clean: true };
|
|
422
|
+
}, [
|
|
423
|
+
["provider", () => this.bridge.close()],
|
|
424
|
+
["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
|
|
425
|
+
["auth", () => unlink(path.join(this.paths.home, "auth.json")).catch(error => { if (error.code !== "ENOENT") throw error; })],
|
|
426
|
+
["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
|
|
427
|
+
]);
|
|
392
428
|
}
|
|
393
429
|
throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
|
|
394
430
|
}
|
|
395
|
-
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); }
|
|
396
431
|
}
|
|
397
432
|
|
|
398
433
|
export async function serveDaemon(stateDir) {
|
|
@@ -426,6 +461,29 @@ export async function serveDaemon(stateDir) {
|
|
|
426
461
|
}
|
|
427
462
|
}
|
|
428
463
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
429
|
-
await removeSocket(paths.socket, paths.dir);
|
|
464
|
+
await removeSocket(paths.socket, paths.dir);
|
|
465
|
+
const server = net.createServer(connection => {
|
|
466
|
+
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
467
|
+
connection.on("data", chunk => {
|
|
468
|
+
if (consumed) return;
|
|
469
|
+
buffer += chunk; if (!admitNativeRequestBuffer(buffer, connection, RESPONSE_SCHEMA)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
470
|
+
void (async () => {
|
|
471
|
+
let request;
|
|
472
|
+
try {
|
|
473
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
474
|
+
if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request");
|
|
475
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}, request.id), () => runtime.dispatch("session.inspect", request.params ?? {}));
|
|
476
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
477
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
|
|
480
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
481
|
+
}
|
|
482
|
+
})();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
trackDaemonConnections(server); runtime.server = server;
|
|
486
|
+
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(() => {});
|
|
430
487
|
}
|
|
431
488
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
489
|
+
import { commandText } from "./process-json.mjs";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { readFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
5
4
|
import { AcpBridge } from "./dd-zcode.mjs";
|
|
6
5
|
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
6
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
7
|
+
import { nativeObservationError } from "./native-streams.mjs";
|
|
7
8
|
|
|
8
9
|
const GROK_HARNESS_CONTRACT = "dd-grok-harness@1";
|
|
9
10
|
|
|
@@ -14,11 +15,7 @@ function executable(command, args = []) { const resolved = command ?? process.en
|
|
|
14
15
|
|
|
15
16
|
async function commandOutput(command, args) {
|
|
16
17
|
const target = executable(command, args);
|
|
17
|
-
return await
|
|
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
|
-
});
|
|
18
|
+
return await commandText(target.command, target.args, { phase: "native-version" });
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
function profile(options) { return { provider: "xai", model: options.model ?? null, reasoning: options.reasoning ?? null, mode: options.mode ?? "bypassPermissions" }; }
|
|
@@ -56,8 +53,10 @@ function bridgeOptions(options) {
|
|
|
56
53
|
|
|
57
54
|
async function withBridge(options, operation) {
|
|
58
55
|
const bridge = new AcpBridge(bridgeOptions(options));
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
let primaryError, result;
|
|
57
|
+
try { const initialized = await bridge.start(); result = await operation(bridge, initialized); return result; }
|
|
58
|
+
catch (error) { primaryError = error; throw error; }
|
|
59
|
+
finally { try { await bridge.close(); } catch (error) { if (!primaryError) throw nativeObservationError(error, result === undefined ? {} : { result }); primaryError.cleanup_error = errorRecord(error); primaryError.details = { ...primaryError.details, cleanup_error: primaryError.cleanup_error }; } }
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
export function observedProfile(initialized, requested) {
|
|
@@ -88,10 +87,7 @@ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
|
|
|
88
87
|
const measured = usage.usage && typeof usage.usage === "object" ? usage.usage : usage;
|
|
89
88
|
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" });
|
|
90
89
|
const target = executable(options.ddFlowBin, ["grok", "usage", "ingest", "--project-root", absolute(options.projectRoot, "--project-root"), "--json"]);
|
|
91
|
-
await
|
|
92
|
-
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 = "";
|
|
93
|
-
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`);
|
|
94
|
-
});
|
|
90
|
+
await commandText(target.command, target.args, { input: `${input}\n`, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") } : {}, phase: "usage-ingest" });
|
|
95
91
|
}
|
|
96
92
|
|
|
97
93
|
async function forwardUsageBestEffort(options, providerSessionId, usage, toolCalls) {
|
|
@@ -153,10 +149,15 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
153
149
|
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));
|
|
154
150
|
const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; const initial_usage_error = await forwardUsageBestEffort(local, sessionId, await sessionUsage(bridge, sessionId), sessionToolCalls(local, bridge, sessionId));
|
|
155
151
|
options.assertDispatch?.();
|
|
156
|
-
|
|
152
|
+
bridge.dispatchOperationId = options.operationId;
|
|
153
|
+
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.livenessTimeoutMs ?? options.timeoutMs ?? 600_000 });
|
|
154
|
+
try {
|
|
155
|
+
await bridge.flush();
|
|
157
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;
|
|
158
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"); }
|
|
159
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
|
+
} catch (error) { throw Object.assign(new Error("Grok native Turn completed but mandatory observation failed", { cause: error }), { code: "native_outcome_observation_failed", retryable: false,
|
|
160
|
+
details: { provider_session_id: sessionId, native_outcome: { status: "completed", turn }, observation_error: errorRecord(error), settled: false, journal_locator: options.journal } }); }
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
export async function inspectSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await inspectSessionWithBridge(bridge, { ...options, initialized })); }
|
|
@@ -173,3 +174,4 @@ export async function forkSessionWithBridge(bridge, options) {
|
|
|
173
174
|
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 });
|
|
174
175
|
}
|
|
175
176
|
export async function promptFromFile(file) { return await readFile(absolute(file, "--prompt-file"), "utf8"); }
|
|
177
|
+
import { commandText } from "./process-json.mjs";
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
|
+
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
1
3
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
2
4
|
import { openCodeToolObservation, journalToolObservations } from "./tool-observations.mjs";
|
|
3
|
-
import {
|
|
5
|
+
import { ObservationClock } from "./observation-clock.mjs";
|
|
4
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
7
|
import { appendFile, chmod, copyFile, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
6
8
|
import { spawn } from "node:child_process";
|
|
7
9
|
import net from "node:net";
|
|
8
|
-
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
10
|
+
import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
9
11
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
12
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
10
13
|
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
11
14
|
import os from "node:os";
|
|
12
15
|
import path from "node:path";
|
|
@@ -23,14 +26,16 @@ async function readState(dir) { try { return JSON.parse(await readFile(locations
|
|
|
23
26
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
24
27
|
function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
|
|
25
28
|
function isolatedEnv(config) { return { ...process.env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome, OPENCODE_CONFIG_DIR: config.adapterDir, OPENCODE_DISABLE_PROJECT_CONFIG: "1", OPENCODE_SERVER_PASSWORD: config.password, ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}), ...(config.ddFlowBin ? { DD_OPENCODE_DD_FLOW_BIN: config.ddFlowBin, DD_OPENCODE_NODE_BIN: process.execPath, DD_OPENCODE_PROJECT_ROOT: config.projectRoot } : {}), DD_OPENCODE_DAEMON_ID: config.daemonId, DD_OPENCODE_PROVIDER: config.provider, DD_OPENCODE_MODEL: config.model, DD_OPENCODE_VARIANT: config.variant, DD_OPENCODE_AGENT: config.agent, DD_OPENCODE_SERVER_VERSION: config.version ?? "unknown", DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "unknown" }; }
|
|
26
|
-
async function run(bin, args, config, { stdin, timeoutMs = 30_000 } = {}) {
|
|
29
|
+
export async function run(bin, args, config, { stdin, timeoutMs = 30_000 } = {}) {
|
|
30
|
+
return await commandText(bin, args, { cwd: config.cwd, env: isolatedEnv(config), input: stdin ?? null, timeoutMs, phase: "native-utility" });
|
|
31
|
+
}
|
|
27
32
|
async function forwardUsage(config, sessionId, snapshot) {
|
|
28
33
|
if (!config.ddFlowBin) return;
|
|
29
34
|
const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: "complete", usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cached_input_tokens, cacheWriteInputTokens: snapshot.cache_write_tokens, totalTokens: snapshot.total_tokens, costUsd: snapshot.cost_usd }, tool_calls: snapshot.tool_calls };
|
|
30
35
|
await run(config.ddFlowBin, ["opencode", "usage", "ingest", "--project-root", config.projectRoot, "--json"], config, { stdin: `${JSON.stringify(payload)}\n` });
|
|
31
36
|
}
|
|
32
37
|
async function freePort() { return await new Promise((resolve, reject) => { const server = net.createServer(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const port = server.address().port; server.close(() => resolve(port)); }); }); }
|
|
33
|
-
|
|
38
|
+
const errorPayload = errorRecord;
|
|
34
39
|
|
|
35
40
|
async function writePlugin(paths, config) {
|
|
36
41
|
const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
@@ -47,27 +52,32 @@ async function prepare(paths, config, authSource) {
|
|
|
47
52
|
async function importArchive(paths, config, archiveFile) { if (!archiveFile) return null; const manifest = JSON.parse(await readFile(absolute(archiveFile, "--session-archive"), "utf8")); if (manifest.schema_id !== ARCHIVE_SCHEMA || !manifest.payload) throw new DaemonError("invalid_session_archive", "OpenCode archive is incompatible"); if (manifest.opencode_version !== config.version) throw new DaemonError("session_archive_version_mismatch", "OpenCode archive version differs from the daemon version", false, { archive_version: manifest.opencode_version, current_version: config.version }); const payload = Buffer.from(manifest.payload, "base64").toString("utf8"); await writeFile(paths.archive, payload, { mode: 0o600 }); const stdout = await run(config.bin, ["import", paths.archive], config); const match = stdout.match(/ses_[A-Za-z0-9]+/); return { provider_session_id: match?.[0] ?? manifest.provider_session_id, manifest };
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
55
|
+
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
56
|
+
const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
57
|
+
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'opencode', timeoutMs });
|
|
58
|
+
}
|
|
51
59
|
function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","variant","agent","projectRoot","ddFlowBin","ddFlowHome","sessionArchive","resourceHome","noFlow"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
52
60
|
export async function startDaemon(options) {
|
|
53
61
|
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
54
62
|
const previous = await readState(paths.dir), daemonId = randomUUID(), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")); const config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_OPENCODE_BIN ?? "opencode", provider: String(options.provider ?? ""), model: String(options.model ?? ""), variant: String(options.variant ?? "default"), agent: String(options.agent ?? "build"), projectRoot: noFlow ? cwd : await realpath(absolute(options.projectRoot, "--project-root")), ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), 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: options.sessionArchive ? absolute(options.sessionArchive, "--session-archive") : null, dataHome: paths.data, configHome: paths.config, cacheHome: paths.cache, adapterDir: path.join(paths.config, "opencode"), password: randomBytes(24).toString("hex"), daemonId, noFlow };
|
|
55
63
|
await prepareRuntimeOwner(config, paths.dir);
|
|
64
|
+
assertDaemonRestartable(previous);
|
|
56
65
|
if (!config.provider || !config.model) throw new DaemonError("profile_required", "--provider and --model are required");
|
|
57
66
|
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 (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
|
|
67
|
+
assertDaemonRestartable(await readState(paths.dir));
|
|
58
68
|
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");
|
|
59
69
|
await removeSocket(paths); const authSource = options.authPath ? absolute(options.authPath, "--auth-path") : path.join(os.homedir(), ".local", "share", "opencode", "auth.json"); await prepare(paths, config, authSource); config.version = (await run(config.bin, ["--version"], config)).trim(); if (config.version !== OPENCODE_BASELINE.version) throw new DaemonError("unsupported_version", `unsupported OpenCode version ${config.version}; expected ${OPENCODE_BASELINE.version}`); const imported = await importArchive(paths, config, config.sessionArchive);
|
|
60
70
|
const resourceProcess = await registerDaemonProcess(config, { kind: "opencode-daemon", owner: `opencode:${path.basename(paths.dir)}`, operation: `opencode-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
|
|
61
71
|
const state = { schema_id: STATE_SCHEMA, daemon_id: daemonId, shutdown_state: "starting", active_tree: false, auth_status: config.authStatus, versions: { opencode: config.version }, config, sessions: imported ? [{ provider_session_id: imported.provider_session_id, parent_provider_session_id: null, root_provider_session_id: imported.provider_session_id, cwd: config.cwd }] : (previous?.sessions ?? []), resource_process: resourceProcess }; await writeJson(paths.state, state);
|
|
62
72
|
const log = await open(paths.log, "a", 0o600), entry = absolute(options.entryPath, "dd-opencode entry"); const child = spawn(process.execPath, [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();
|
|
63
|
-
try { await confirmDaemonProcess(config, resourceProcess, child); const
|
|
73
|
+
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: 20_000 }); while (!startupClock.sample()) { await new Promise(resolve => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; } } throw new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
|
|
64
74
|
catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
|
|
65
75
|
}
|
|
66
|
-
export const stopDaemon = async 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); };
|
|
76
|
+
export const stopDaemon = async 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, { readState: () => readState(options.stateDir) }); };
|
|
67
77
|
|
|
68
78
|
export class Runtime {
|
|
69
79
|
constructor(paths, state, client, child, providerProcess) { this.paths = paths; this.state = state; this.client = client; this.child = child; this.providerProcess = providerProcess; this.sessions = new Map((state.sessions ?? []).map(x => [x.provider_session_id, x])); this.active = null; this.persisting = Promise.resolve(); }
|
|
70
|
-
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
|
|
80
|
+
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
|
|
71
81
|
async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
72
82
|
async childTree(id, seen = new Set()) { if (seen.has(id)) return []; seen.add(id); const direct = await this.client.children(id) ?? []; const nested = []; for (const child of direct) nested.push(child, ...await this.childTree(child.id, seen)); return nested; }
|
|
73
83
|
async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); this.journaledTools ??= new Map(); journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, tree_observations: children.map(child => ({ provider_session_id: child.id, status: statuses?.[child.id]?.type ?? "idle", settled: (statuses?.[child.id]?.type ?? "idle") === "idle" })), children, usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length }; }
|
|
@@ -86,10 +96,12 @@ export class Runtime {
|
|
|
86
96
|
async requireSettled(id) { const value = await this.describe(id); if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status }); return value; }
|
|
87
97
|
async reconcileUsage(observed) { const errors = []; try { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); } catch (error) { errors.push({ session_id: observed.provider_session_id, phase: "usage", ...errorPayload(error) }); } for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) { try { errors.push(...await this.reconcileUsage(await this.describe(child.id))); } catch (error) { errors.push({ session_id: child.id, phase: "usage_child", ...errorPayload(error) }); } } return errors; }
|
|
88
98
|
async productive(name, task) {
|
|
99
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
89
100
|
if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
|
|
90
101
|
this.active = name; let result, completed = false;
|
|
91
102
|
try {
|
|
92
103
|
await this.persist({ active_tree: true, active_operation: name });
|
|
104
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
93
105
|
result = await task(); completed = true;
|
|
94
106
|
await this.persist({ active_tree: !result?.settled, active_operation: null });
|
|
95
107
|
await this.journal(name, result); return result;
|
|
@@ -116,8 +128,9 @@ export class Runtime {
|
|
|
116
128
|
try { await this.client.abort(id); } catch (error) { errors.push({ session_id: id, phase: "abort", ...errorPayload(error) }); }
|
|
117
129
|
return errors;
|
|
118
130
|
}
|
|
119
|
-
async dispatch(operation, params) {
|
|
131
|
+
async dispatch(operation, params, operationId = null) {
|
|
120
132
|
const assertDispatch = captureDispatchGuard(this);
|
|
133
|
+
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
121
134
|
if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
|
|
122
135
|
if (operation === "daemon.doctor") { const doc = await this.client.request("GET", "/doc", undefined, { directory: null, timeoutMs: 5_000 }); const apiFingerprint = createHash("sha256").update(typeof doc === "string" ? doc : JSON.stringify(doc)).digest("hex"); if (apiFingerprint !== OPENCODE_BASELINE.api_fingerprint) throw new DaemonError("api_fingerprint_mismatch", "OpenCode API differs from the qualified baseline", false, { expected: OPENCODE_BASELINE.api_fingerprint, actual: apiFingerprint }); return { compatible: true, versions: this.state.versions, api_fingerprint: apiFingerprint, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status }; }
|
|
123
136
|
if (operation === "session.create") return this.productive(operation, async () => {
|
|
@@ -137,7 +150,8 @@ export class Runtime {
|
|
|
137
150
|
});
|
|
138
151
|
if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
|
|
139
152
|
if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => { assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, provider: this.state.config.provider, model: this.state.config.model, variant: this.state.config.variant, agent: this.state.config.agent,
|
|
140
|
-
assertDispatch,
|
|
153
|
+
assertDispatch, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
|
|
154
|
+
onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
|
|
141
155
|
onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
|
|
142
156
|
onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
|
|
143
157
|
}); const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
|
|
@@ -177,6 +191,7 @@ export class Runtime {
|
|
|
177
191
|
}
|
|
178
192
|
if (operation === "session.archive") { const observed = await this.requireSettled(params.sessionId); const payload = await run(this.state.config.bin, ["export", params.sessionId], this.state.config); const manifest = { schema_id: ARCHIVE_SCHEMA, provider_session_id: params.sessionId, source_cwd: observed.cwd, opencode_version: this.state.versions.opencode, history_sha256: observed.history_sha256, exported_at: new Date().toISOString(), payload: Buffer.from(payload).toString("base64") }; const file = absolute(params.archiveFile, "--archive-file"); await writeJson(file, manifest); return { archive_file: file, ...manifest, payload: undefined, sha256: createHash("sha256").update(await readFile(file)).digest("hex") }; }
|
|
179
193
|
if (operation === "daemon.stop") {
|
|
194
|
+
return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), async () => {
|
|
180
195
|
if (params.cancelTree) cancelPendingDispatch(this);
|
|
181
196
|
await waitForSettlement({
|
|
182
197
|
knownSessions: () => [...this.sessions.keys()],
|
|
@@ -187,18 +202,15 @@ export class Runtime {
|
|
|
187
202
|
},
|
|
188
203
|
...(params.cancelTree ? { cancel: id => this.cancelTree(id) } : {})
|
|
189
204
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
205
|
+
return { clean: true };
|
|
206
|
+
}, [
|
|
207
|
+
["provider", () => stopProcessGroup(this.child)],
|
|
208
|
+
["provider_resource", () => finishDaemonProcess(this.state.config, this.providerProcess)],
|
|
209
|
+
["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
|
|
210
|
+
]);
|
|
194
211
|
}
|
|
195
212
|
throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
|
|
196
213
|
}
|
|
197
|
-
async shutdown() {
|
|
198
|
-
try { await stopProcessGroup(this.child); await finishDaemonProcess(this.state.config, this.providerProcess, "stopped", "daemon_stopped"); }
|
|
199
|
-
catch (error) { await this.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); throw error; }
|
|
200
|
-
await finishDaemonProcess(this.state.config, this.state.resource_process); await this.persist({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await new Promise(resolve => this.server.close(resolve)); await removeSocket(this.paths);
|
|
201
|
-
}
|
|
202
214
|
}
|
|
203
215
|
export async function serveDaemon(stateDir) {
|
|
204
216
|
const paths = locations(stateDir), state = await readState(paths.dir); if (state?.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing");
|
|
@@ -211,9 +223,31 @@ export async function serveDaemon(stateDir) {
|
|
|
211
223
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
212
224
|
scheduleDaemonProcessHeartbeat(state.config, providerProcess);
|
|
213
225
|
state.provider_process = providerProcess; state.provider_pid = child.pid; await writeJson(paths.state, state);
|
|
214
|
-
const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const
|
|
226
|
+
const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const startupClock = new ObservationClock({ timeoutMs: 15_000 }); while (!startupClock.sample()) { try { await client.health(); break; } catch { await new Promise(resolve => setTimeout(resolve, 100)); } } await client.health(); const runtime = new Runtime(paths, state, client, child, providerProcess); await removeSocket(paths);
|
|
227
|
+
const server = net.createServer(connection => {
|
|
228
|
+
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
229
|
+
connection.on("data", chunk => {
|
|
230
|
+
if (consumed) return;
|
|
231
|
+
buffer += chunk; if (!admitNativeRequestBuffer(buffer, connection, RESPONSE_SCHEMA)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
232
|
+
void (async () => {
|
|
233
|
+
let request;
|
|
234
|
+
try {
|
|
235
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
236
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}, request.id), () => runtime.dispatch("session.inspect", request.params ?? {}));
|
|
237
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
238
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
|
|
241
|
+
connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
trackDaemonConnections(server); runtime.server = server;
|
|
247
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await runtime.persist({ shutdown_state: "running", active_tree: false, server_port: port }); return await new Promise(() => {});
|
|
215
248
|
} catch (error) {
|
|
216
249
|
await cleanupFailedStart(state.config, providerProcess, child, error); throw error;
|
|
217
250
|
}
|
|
218
251
|
}
|
|
219
252
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
253
|
+
import { commandText } from "./process-json.mjs";
|