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