@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.124
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 +18 -0
- package/README.md +23 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +3 -0
- package/dist/domain/stage-catalog.js +1 -1
- 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-operations.mjs +100 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
- package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
- package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/services/code-checks.js +14 -18
- package/dist/services/codex-capacity.js +251 -31
- package/dist/services/controller-fanout.js +1 -0
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +36 -11
- package/dist/services/harness-adapter.js +21 -15
- package/dist/services/managed-processes.js +14 -6
- package/dist/services/merge-server.js +35 -12
- package/dist/services/review-copy.js +21 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-controller-adapter.js +33 -8
- package/dist/services/run-controller-recovery.js +26 -2
- package/dist/services/run-controller.js +91 -24
- package/dist/services/run-observations.js +7 -2
- package/dist/services/run-recovery-runtime.js +49 -3
- package/dist/services/runs.js +1 -1
- package/dist/services/stage-report-renderer.js +25 -2
- package/dist/services/verification-matrix.js +57 -8
- package/dist/services/vnext-code-review.js +22 -8
- package/dist/services/vnext-merge.js +20 -2
- package/dist/services/vnext-plan-review.js +87 -19
- package/dist/services/work-registry.js +11 -2
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
2
|
-
import { ToolObservations, agyToolObservation } from "./tool-observations.mjs";
|
|
2
|
+
import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
6
|
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
5
7
|
import { spawn } from "node:child_process";
|
|
@@ -8,7 +10,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
8
10
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
9
11
|
import { hookBudget } from "./native-hook-command.mjs";
|
|
10
12
|
import { agyWorkspaceHooks } from "./agy-hooks.mjs";
|
|
11
|
-
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
13
|
+
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
12
14
|
import path from "node:path";
|
|
13
15
|
import { observeModel } from "./model-observations.mjs";
|
|
14
16
|
import { AGY_VERSION_TIMEOUT_MS, AgyError, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
@@ -16,6 +18,29 @@ import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, conf
|
|
|
16
18
|
|
|
17
19
|
const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
|
|
18
20
|
class DaemonError extends AgyError {}
|
|
21
|
+
export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId]) {
|
|
22
|
+
const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
|
|
23
|
+
let handle;
|
|
24
|
+
try {
|
|
25
|
+
if (!rootSessionId || !child.provider_session_id || !ownedSessionIds.includes(child.parent_provider_session_id) || !child.log_uri || !/^[\w-]+$/.test(child.provider_session_id)) throw new Error("unbound native locator");
|
|
26
|
+
const expected = path.join(runtimeRoot, "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
|
|
27
|
+
const canonical = path.join(await realpath(runtimeRoot), "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
|
|
28
|
+
if (![expected, canonical].includes(fileURLToPath(child.log_uri)) || await realpath(expected) !== canonical) throw new Error("foreign or symlink native locator");
|
|
29
|
+
const before = await lstat(expected);
|
|
30
|
+
if (!before.isFile()) throw new Error("native locator is not a regular file");
|
|
31
|
+
handle = await open(expected, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
32
|
+
const stat = await handle.stat();
|
|
33
|
+
if (!before.isFile() || !stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || await realpath(expected) !== canonical) throw new Error("native locator changed during open");
|
|
34
|
+
const maximum = 8 * 1024 * 1024, buffer = Buffer.alloc(Math.min(stat.size, maximum));
|
|
35
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
36
|
+
const bytes = buffer.subarray(0, bytesRead), prefix = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
|
|
37
|
+
const source = { locator: child.log_uri, byte_start: 0, byte_end: prefix.length, sha256: createHash("sha256").update(prefix).digest("hex") };
|
|
38
|
+
const events = agyTranscriptObservations(prefix.toString("utf8"), child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
|
|
39
|
+
if (prefix.length !== bytesRead || stat.size > maximum) events.push({ session, reason: stat.size > maximum ? "agy_transcript_size_limit" : "agy_transcript_partial_tail" });
|
|
40
|
+
return events;
|
|
41
|
+
} catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : "agy_transcript_locator_rejected" }]; }
|
|
42
|
+
finally { await handle?.close(); }
|
|
43
|
+
}
|
|
19
44
|
const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
|
|
20
45
|
function locations(stateDir) {
|
|
21
46
|
const dir = absolute(stateDir, "--state-dir"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 22), local = path.join(dir, "daemon.sock");
|
|
@@ -185,14 +210,16 @@ export async function startDaemon(options) {
|
|
|
185
210
|
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
186
211
|
const previous = await readState(paths.dir), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")), config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_AGY_BIN ?? "agy", provider: String(options.provider ?? "google"), model: String(options.model ?? "gemini-3.1-pro-high"), reasoning: String(options.reasoning ?? "high"), mode: String(options.mode ?? "accept-edits"), 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") }, entryPath: absolute(options.entryPath, "dd-agy entry"), daemonId: randomUUID(), resumeSessionId: options.sessionId ?? null, geminiDir: paths.gemini, appDataDir: "runtime", temporary: paths.temporary, noFlow };
|
|
187
212
|
await prepareRuntimeOwner(config, paths.dir);
|
|
213
|
+
assertDaemonRestartable(previous);
|
|
188
214
|
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; }
|
|
215
|
+
assertDaemonRestartable(await readState(paths.dir));
|
|
189
216
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
190
217
|
if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous Antigravity daemon died with an active or unproven tree");
|
|
191
218
|
await removeSocket(paths); await prepare(paths, config);
|
|
192
219
|
const version = (await runAgy(config.bin, ["--version"], { cwd: config.cwd, env: sanitizedEnv(config), timeoutMs: AGY_VERSION_TIMEOUT_MS })).stdout.trim();
|
|
193
220
|
const resourceProcess = await registerDaemonProcess(config, { kind: "agy-daemon", owner: `agy:${path.basename(paths.dir)}`, operation: `agy-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
|
|
194
221
|
const retained = retainedAgyState(previous, config.resumeSessionId);
|
|
195
|
-
await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
|
|
222
|
+
await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], ...(Array.isArray(retained.tool_observations) ? { tool_observations: retained.tool_observations } : {}), tool_observation_gaps: retained.tool_observation_gaps ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
|
|
196
223
|
const log = await open(paths.log, "a", 0o600), child = spawn(process.execPath, [config.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: sanitizedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
197
224
|
try {
|
|
198
225
|
await confirmDaemonProcess(config, resourceProcess, child);
|
|
@@ -210,11 +237,11 @@ export async function startDaemon(options) {
|
|
|
210
237
|
throw error;
|
|
211
238
|
}
|
|
212
239
|
}
|
|
213
|
-
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); };
|
|
240
|
+
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) }); };
|
|
214
241
|
|
|
215
242
|
export class Runtime {
|
|
216
243
|
constructor(paths, state) { this.paths = paths; this.state = state; this.config = state.config; this.child = null; this.providerProcess = state.provider_process ?? null; this.providerFinalization = Promise.resolve(); this.buffer = ""; this.init = null; this.readyPromise = null; this.active = null; this.exited = false; this.toolObservations = new ToolObservations(); for (const reason of state.tool_observation_gaps ?? []) this.toolObservations.gaps.add(reason); for (const event of state.tool_observations ?? []) this.toolObservations.add(event); if (state.tool_calls?.length && !state.tool_observations) this.toolObservations.gaps.add("legacy_tools_not_attributed"); this.turnGeneration = state.turn_generation ?? 0; this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); this.lastResult = state.last_result ?? null; this.sessionObservations = new Map(state.session_observations ?? []); this.startupError = null; this.persisting = Promise.resolve(); this.draining = Promise.resolve(); this.lastActivityAt = state.last_activity_at ?? null; this.usageIngest = null; this.lastProviderEvent = null; }
|
|
217
|
-
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); const queued = performance.now(); this.persisting = this.persisting.then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
|
|
244
|
+
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values(), ...this.toolObservations.sources.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); const queued = performance.now(); this.persisting = this.persisting.catch(() => {}).then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
|
|
218
245
|
async journal(kind, payload = {}) { const line = `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), kind, ...payload })}\n`; for (const journal of new Set([this.paths.journal, this.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
219
246
|
args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", "--model", this.config.model, "--effort", this.config.reasoning, "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "6h", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
|
|
220
247
|
requireSessionIdentity(requestedId) {
|
|
@@ -232,12 +259,16 @@ export class Runtime {
|
|
|
232
259
|
if (["init", "step_update", "result"].includes(event.event)) this.markActivity();
|
|
233
260
|
}
|
|
234
261
|
async start(sessionId = null) {
|
|
262
|
+
if (this.drainingError) throw this.drainingError;
|
|
263
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
235
264
|
if (this.child && !this.exited) {
|
|
236
265
|
await this.readyPromise;
|
|
237
266
|
this.requireSessionIdentity(sessionId);
|
|
238
267
|
return this.init;
|
|
239
268
|
}
|
|
240
269
|
await this.providerFinalization;
|
|
270
|
+
if (this.drainingError) throw this.drainingError;
|
|
271
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
241
272
|
const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
|
|
242
273
|
let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
|
|
243
274
|
// The provider can reject before durable registration finishes. Keep a
|
|
@@ -248,12 +279,12 @@ export class Runtime {
|
|
|
248
279
|
const providerProcess = this.providerProcess;
|
|
249
280
|
try { await this.persist({ provider_process: providerProcess }); }
|
|
250
281
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
|
|
251
|
-
try { this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
|
|
282
|
+
try { assertDaemonDispatch(this.paths?.dir); this.child = spawn(target.command, target.args, { cwd: this.config.cwd, env: sanitizedEnv(this.config), detached: true, stdio: ["pipe", "pipe", "pipe"] }); }
|
|
252
283
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, null, error); throw error; }
|
|
253
284
|
const providerChild = this.child;
|
|
254
285
|
let allowProviderFinalization;
|
|
255
286
|
const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
|
|
256
|
-
this.child.stdout.setEncoding("utf8").on("data", chunk => { this.buffer += chunk; this.draining = this.draining.then(() => this.drain(readyResolve)).catch(error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } }); });
|
|
287
|
+
this.child.stdout.setEncoding("utf8").on("data", chunk => { this.buffer += chunk; this.draining = this.draining.then(() => this.drain(readyResolve)).catch(error => { this.drainingError ??= error; readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } }); });
|
|
257
288
|
this.child.stderr.setEncoding("utf8").on("data", chunk => void this.journal("stderr", { text: String(chunk).slice(0, 4000) }));
|
|
258
289
|
this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
|
|
259
290
|
// A dead leader is not yet a finalized tree: stop must also join the
|
|
@@ -295,6 +326,26 @@ export class Runtime {
|
|
|
295
326
|
this.observeProviderActivity(event); await this.journal("provider_event", { event }); if (event.event === "init") { this.init = { conversation_id: event.conversation_id, ...(event.init ?? {}) }; if (!this.init.conversation_id) throw new DaemonError("agy_init_missing", "Antigravity init has no conversation ID"); const requested = { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode, permission_mode: "always-proceed" }; this.profile = assertProfile(requested, observedProfile(this.init, requested)); await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: this.init.conversation_id, requested, observed: this.profile.observed, source: "native.init" }); await this.persist({ shutdown_state: "running", active_tree: false }); readyResolve(this.init); } else if (event.event === "step_update") this.observeStep(event.step_update ?? {}); else if (event.event === "result") { if (this.startupError) throw this.startupError; await this.finishTurn(event.result ?? {}); } } }
|
|
296
327
|
observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(sessionId) ?? {}; if (Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1)) this.childActivity(sessionId); if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) }); this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id)); for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) { const prior = this.descendants.get(child.conversation_id); this.descendants.set(child.conversation_id, { ...prior, provider_session_id: child.conversation_id, parent_provider_session_id: sessionId ?? null, role: child.role ?? null, subagent_type: child.type_name ?? null, log_uri: child.log_uri ?? null, workspace_uris: child.workspace_uris ?? [], status: prior?.status ?? "unknown" }); } }
|
|
297
328
|
toolSnapshot() { const summary = this.toolObservations.summary(); const failures_by_name = {}; for (const event of this.toolObservations.calls.values()) if (event.status === "failed") failures_by_name[event.name] = (failures_by_name[event.name] ?? 0) + 1; return { ...summary, by_name: summary.by_tool, failures_by_name }; }
|
|
329
|
+
async captureChildTools() {
|
|
330
|
+
this.capturedToolFacts ??= new Map();
|
|
331
|
+
const root = this.init?.conversation_id ?? this.state.sessions?.[0]?.provider_session_id ?? this.lastResult?.conversation_id;
|
|
332
|
+
for (const child of this.descendants.values()) {
|
|
333
|
+
const ancestors = new Set(); let parent = child.parent_provider_session_id;
|
|
334
|
+
while (parent && parent !== root && this.descendants.has(parent) && !ancestors.has(parent)) {
|
|
335
|
+
ancestors.add(parent); parent = this.descendants.get(parent).parent_provider_session_id;
|
|
336
|
+
}
|
|
337
|
+
const events = parent === root && root
|
|
338
|
+
? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors])
|
|
339
|
+
: [{ session: { harness_id: "antigravity-cli", session_id: child.provider_session_id }, reason: "agy_transcript_ancestry_unproven" }];
|
|
340
|
+
for (const event of events) {
|
|
341
|
+
this.toolObservations.add(event);
|
|
342
|
+
const key = JSON.stringify([event.session, event.call_id, event.reason]), encoded = JSON.stringify(event);
|
|
343
|
+
if (this.capturedToolFacts.get(key) === encoded) continue;
|
|
344
|
+
await this.journal("tool_observation", { schema_id: "dd-flow/tool-observation@1", event });
|
|
345
|
+
this.capturedToolFacts.set(key, encoded);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
298
349
|
async finishTurn(result) {
|
|
299
350
|
if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
|
|
300
351
|
if (!["SUCCESS", "ERROR", "CANCELLED"].includes(result.status)) throw new DaemonError("agy_terminal_result_invalid", "Antigravity result has no recognized terminal status", false, { provider_result: result });
|
|
@@ -312,6 +363,7 @@ export class Runtime {
|
|
|
312
363
|
// are not request IDs: native command rejections can report zero turns.
|
|
313
364
|
// prompt() drains earlier events before accepting another input.
|
|
314
365
|
if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
|
|
366
|
+
await this.captureChildTools();
|
|
315
367
|
this.lastResult = result; this.markActivity();
|
|
316
368
|
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
317
369
|
const pendingTerminal = current && result.status === "SUCCESS";
|
|
@@ -380,6 +432,7 @@ export class Runtime {
|
|
|
380
432
|
try {
|
|
381
433
|
await this.persist({ active_tree: true, last_hook_rejection: null });
|
|
382
434
|
assertDispatch();
|
|
435
|
+
assertDaemonDispatch(this.paths?.dir);
|
|
383
436
|
if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
|
|
384
437
|
this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
|
|
385
438
|
if (error) { if (this.active === current) this.active = null; current.reject(error); }
|
|
@@ -438,37 +491,79 @@ export class Runtime {
|
|
|
438
491
|
}
|
|
439
492
|
async observeHook(event, payload, budget = null) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); budget?.remaining("identity"); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}), ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const parent = child?.parent_provider_session_id ?? null;
|
|
440
493
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
441
|
-
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: "native.hook", evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
494
|
+
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: { channel: "native.hook", scope: "configured", ...(!payload.model ? { non_asserting: true } : {}) }, evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
442
495
|
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null, persistence_timing: this.persistTiming ?? null }); await this.completePendingTurn(); return { event_id: eventId, daemon_id: this.config.daemonId, launch_cwd: this.config.cwd, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
443
496
|
}
|
|
444
497
|
async finalizeProviderForControl() {
|
|
445
498
|
const pending = this.providerFinalization;
|
|
446
|
-
try {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
499
|
+
try {
|
|
500
|
+
try { await pending; }
|
|
501
|
+
catch (error) {
|
|
502
|
+
// Control retries only retained physical/resource cleanup, never a
|
|
503
|
+
// missing mandatory stream record or a productive provider request.
|
|
504
|
+
if (this.providerFinalization !== pending) await this.providerFinalization;
|
|
505
|
+
else {
|
|
506
|
+
if (!this.retryProviderFinalization) throw error;
|
|
507
|
+
this.providerFinalization = this.retryProviderFinalization(5_000);
|
|
508
|
+
await this.providerFinalization;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (!this.drainingError) throw error;
|
|
513
|
+
this.drainingError.cleanup_error = errorRecord(error);
|
|
514
|
+
this.drainingError.details = { ...this.drainingError.details, cleanup_error: this.drainingError.cleanup_error };
|
|
515
|
+
throw this.drainingError;
|
|
454
516
|
}
|
|
517
|
+
if (this.drainingError) throw this.drainingError;
|
|
518
|
+
}
|
|
519
|
+
async prepareStop(cancelTree) {
|
|
520
|
+
const current = this.active, priorResult = this.lastResult;
|
|
521
|
+
const emptyTree = !current && !this.state.active_tree && !this.state.unclaimed_activity && !priorResult && !this.descendants.size;
|
|
522
|
+
if (cancelTree) await this.cancel();
|
|
523
|
+
else if (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running");
|
|
524
|
+
// A physical kill is settlement, not a native clean terminal receipt.
|
|
525
|
+
// In particular, the previous Turn's SUCCESS cannot certify a cancelled
|
|
526
|
+
// in-flight Turn that exited without its own result.
|
|
527
|
+
const currentResult = !current || current.terminalResult === this.lastResult || this.lastResult !== priorResult;
|
|
528
|
+
const clean = emptyTree || (currentResult && this.receipt().settled);
|
|
529
|
+
return { clean, ...(clean ? {} : { native_settlement: { confirmed: false, reason: "native_terminal_unconfirmed" } }) };
|
|
455
530
|
}
|
|
456
531
|
async cancel() { cancelPendingDispatch(this); if (!this.child) return { ...this.receipt(), settled: Boolean(this.exited) }; if (!this.exited) { try { process.kill(-this.child.pid, "SIGINT"); } catch (error) { if (error.code !== "ESRCH") throw error; } const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await stopProcessGroup(this.child, 5_000); } await this.finalizeProviderForControl(); await this.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
|
|
457
532
|
}
|
|
458
|
-
async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled))) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running"); if (cancelTree) await this.cancel(); else if (this.child && !this.exited) this.child.stdin.end(); const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await this.cancel(); await this.
|
|
533
|
+
async close(cancelTree) { if (cancelTree) cancelPendingDispatch(this); if (!cancelTree && !this.exited && (this.active || this.state.active_tree || this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled))) throw new DaemonError("tree_not_settled", "Antigravity root or child tree is still running"); if (cancelTree) await this.cancel(); else if (this.child && !this.exited) this.child.stdin.end(); const deadline = Date.now() + 5000; while (!this.exited && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); if (!this.exited) await this.cancel(); await this.finalizeProviderForControl(); }
|
|
459
534
|
async abortProvider(reason) {
|
|
460
535
|
const child = this.child, providerProcess = this.providerProcess;
|
|
461
536
|
await stopProcessGroup(child);
|
|
462
537
|
await this.finalizeProviderForControl();
|
|
463
538
|
await finishDaemonProcess(this.config, providerProcess, "failed", reason);
|
|
464
539
|
}
|
|
465
|
-
async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
|
|
540
|
+
async finishResource(state = "stopped", reason = "daemon_stopped") { await this.captureChildTools(); await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
|
|
466
541
|
}
|
|
467
542
|
|
|
468
543
|
export async function serveDaemon(stateDir) {
|
|
469
544
|
const paths = locations(stateDir), state = await readState(paths.dir); if (!state?.config) throw new DaemonError("daemon_state_missing", "Antigravity daemon state is missing");
|
|
470
545
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
471
|
-
const runtime = new Runtime(paths, state); await removeSocket(paths);
|
|
546
|
+
const runtime = new Runtime(paths, state); await removeSocket(paths);
|
|
547
|
+
const server = net.createServer(socket => {
|
|
548
|
+
let buffer = "", consumed = false; socket.setEncoding("utf8");
|
|
549
|
+
socket.on("data", chunk => {
|
|
550
|
+
if (consumed) return;
|
|
551
|
+
buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
552
|
+
void (async () => {
|
|
553
|
+
let request;
|
|
554
|
+
try {
|
|
555
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
556
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(runtime, request), () => dispatch(runtime, { operation: "session.inspect", params: request.params ?? {} }));
|
|
557
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
558
|
+
socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
|
|
561
|
+
socket.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
562
|
+
}
|
|
563
|
+
})();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
trackDaemonConnections(server); runtime.server = server;
|
|
472
567
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600);
|
|
473
568
|
try { await runtime.start(state.config.resumeSessionId); runtime.requireSessionIdentity(state.config.resumeSessionId); }
|
|
474
569
|
catch (error) {
|
|
@@ -482,10 +577,11 @@ export async function serveDaemon(stateDir) {
|
|
|
482
577
|
|
|
483
578
|
async function dispatch(runtime, request) {
|
|
484
579
|
if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
|
|
580
|
+
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(request.operation)) assertDaemonDispatch(runtime.paths.dir);
|
|
485
581
|
if (request.operation === "daemon.status") { await runtime.observeHookFailures(); return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, active_operation: runtime.active?.operationId ?? null, last_hook_rejection: runtime.state.last_hook_rejection ?? null, last_activity_at: runtime.lastActivityAt, provider_ready: Boolean(runtime.init && !runtime.exited), pid: process.pid, provider_pid: runtime.child?.pid ?? null, versions: runtime.state.versions, config: runtime.config, sessions: runtime.state.sessions ?? [], receipt: runtime.receipt() }; }
|
|
486
582
|
if (request.operation === "session.create") { if (params.sessionId) throw new DaemonError("invalid_create", "create does not accept a Session ID"); if (params.prompt) throw new DaemonError("invalid_create", "create does not execute a prompt; use session.prompt"); await runtime.start(); await runtime.zeroUsage(); return runtime.receipt(); }
|
|
487
583
|
if (request.operation === "session.prompt") { const assertDispatch = captureDispatchGuard(runtime); await runtime.start(params.sessionId ?? null); runtime.requireSessionIdentity(params.sessionId); return await runtime.prompt(params.prompt, assertDispatch, request.id); }
|
|
488
|
-
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
584
|
+
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
|
|
489
585
|
if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
490
586
|
if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
|
|
491
587
|
if (request.operation === "hook.observe") {
|
|
@@ -511,7 +607,7 @@ async function dispatch(runtime, request) {
|
|
|
511
607
|
}
|
|
512
608
|
catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}, current); throw error; }
|
|
513
609
|
}
|
|
514
|
-
if (request.operation === "daemon.stop")
|
|
610
|
+
if (request.operation === "daemon.stop") return await completeDaemonShutdown(runtime.paths.dir, runtime.state, patch => runtime.persist(patch), () => runtime.prepareStop(params.cancelTree === true), [["provider", () => runtime.close(false)], ["daemon_resource", () => runtime.finishResource()]]);
|
|
515
611
|
throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
|
|
516
612
|
}
|
|
517
613
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -4,6 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { checkObservedProfile } from "./model-observations.mjs";
|
|
7
|
+
import { stopProcessGroup } from "./managed-daemon.mjs";
|
|
8
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
7
9
|
|
|
8
10
|
const DEFAULT_MODEL = "gemini-3.1-pro-high";
|
|
9
11
|
export const AGY_VERSION_TIMEOUT_MS = 30_000;
|
|
@@ -19,14 +21,22 @@ export function executable(bin, args = []) {
|
|
|
19
21
|
export async function runAgy(bin, args, options = {}) {
|
|
20
22
|
const target = executable(bin, args);
|
|
21
23
|
return await new Promise((resolve, reject) => {
|
|
22
|
-
const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
24
|
+
const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
23
25
|
let stdout = "", stderr = "", settled = false;
|
|
24
|
-
|
|
26
|
+
let timeoutError, inputError, cleanup;
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
timeoutError = new AgyError("agy_timeout", `${args[0] ?? "agy"} timed out`, true);
|
|
29
|
+
cleanup = stopProcessGroup(child, 1_000).catch(error => { timeoutError.cleanup_error = errorRecord(error); timeoutError.details = { cleanup_error: timeoutError.cleanup_error }; finish(timeoutError); });
|
|
30
|
+
}, options.timeoutMs ?? 30_000);
|
|
25
31
|
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
|
|
26
32
|
child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk);
|
|
27
33
|
child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
|
|
28
34
|
child.on("error", error => finish(error));
|
|
29
|
-
child.on("close", code => code === 0
|
|
35
|
+
child.on("close", async code => { await cleanup; if (timeoutError) finish(timeoutError); else if (inputError) finish(inputError); else if (code === 0) finish(null, { stdout, stderr, code }); else finish(new AgyError("agy_cli_failed", stderr.trim() || stdout.trim() || `agy exited ${code}`, false, { exit_code: code })); });
|
|
36
|
+
child.stdin.on("error", error => {
|
|
37
|
+
inputError ??= error;
|
|
38
|
+
cleanup ??= stopProcessGroup(child, 1_000).catch(cause => { inputError.details = { ...inputError.details, cleanup_error: errorRecord(cause) }; finish(timeoutError ?? inputError); });
|
|
39
|
+
});
|
|
30
40
|
child.stdin.end(options.stdin ?? "");
|
|
31
41
|
});
|
|
32
42
|
}
|
|
@@ -2,14 +2,15 @@ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mj
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
5
|
+
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
6
6
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
7
|
+
import { parseCapacityOperation } from "./codex-capacity-policy.mjs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
10
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
10
11
|
import { errorRecord } from "./operation-errors.mjs";
|
|
11
12
|
import { CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
|
|
12
|
-
import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
13
|
+
import { assertDaemonOwnership, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
|
|
13
14
|
|
|
14
15
|
const requestSchema = "dd-codex/daemon-request@1";
|
|
15
16
|
const responseSchema = "dd-codex/daemon-response@1";
|
|
@@ -38,7 +39,7 @@ async function observeCodexSessions(bridge, options, sessions, active, remember
|
|
|
38
39
|
}
|
|
39
40
|
const unsettled = [], observations = [];
|
|
40
41
|
for (const sessionId of sessions) {
|
|
41
|
-
const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId });
|
|
42
|
+
const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId, turnId: sessionId === options.sessionId ? options.turnId : undefined });
|
|
42
43
|
for (const child of observed.descendants ?? []) { sessions.add(child.provider_session_id); remember(child.provider_session_id); }
|
|
43
44
|
const status = observed.status?.type ?? observed.status;
|
|
44
45
|
const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
|
|
@@ -84,24 +85,24 @@ export async function startDaemon(options) {
|
|
|
84
85
|
const paths = locations(options.stateDir); const cwd = absolute(options.cwd, "--cwd"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? null, ddFlowBin: options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : null, ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null, projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : cwd, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {} };
|
|
85
86
|
await prepareRuntimeOwner(config, paths.dir);
|
|
86
87
|
await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
88
|
+
let previous = await readState(paths.state); assertDaemonRestartable(previous);
|
|
87
89
|
try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1_000); if (JSON.stringify(status.config) !== JSON.stringify(config)) throw Object.assign(new Error("running daemon has a different configuration"), { code: "daemon_config_mismatch" }); return { ...status, already_running: true }; } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; }
|
|
88
|
-
|
|
90
|
+
previous = await readState(paths.state); assertDaemonRestartable(previous);
|
|
91
|
+
if (previous?.shutdown_state === "running") throw Object.assign(new Error("previous dd-codex daemon died; do not reuse its state directory"), { code: "invalid_harness_crash" });
|
|
89
92
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
90
93
|
await removeSocket(paths.socket); const resourceProcess = await registerDaemonProcess(config, { kind: "codex-daemon", owner: `codex:${path.basename(paths.dir)}`, operation: `codex-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log }); const state = { schema_id: stateSchema, daemon_id: randomUUID(), pid: null, cwd: config.cwd, journal: config.journal, config, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", sessions: previous?.sessions ?? [], resource_process: resourceProcess }; await writeState(paths.state, state);
|
|
91
94
|
const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [options.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
|
|
92
95
|
try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 60_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 5_000); } catch (error) { if (!["daemon_not_running", "daemon_timeout"].includes(error.code)) throw error; } } throw Object.assign(new Error("daemon did not become ready"), { code: "daemon_start_failed" }); }
|
|
93
96
|
catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.state), state.daemon_id); throw error; }
|
|
94
97
|
}
|
|
95
|
-
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); }
|
|
98
|
+
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(locations(options.stateDir).state) }); }
|
|
96
99
|
|
|
97
100
|
export async function serveDaemon(stateDir) {
|
|
98
101
|
const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
|
|
99
102
|
const bridge = new CodexBridge({ ...state.config, env: { ...state.config.env, CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_DAEMON_ID: state.daemon_id, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot) }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
|
|
100
|
-
const closeBridge = bridge.close.bind(bridge);
|
|
101
|
-
bridge.close = async () => { await closeBridge(); await finishDaemonProcess(state.config, state.provider_process, "stopped", "daemon_stopped"); };
|
|
102
103
|
scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
|
|
103
|
-
const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.then(() => writeState(paths.state, snapshot)); return persist; };
|
|
104
|
-
const dispatch = async (operation, params) => {
|
|
104
|
+
const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
|
|
105
|
+
const dispatch = async (operation, params, operationId) => {
|
|
105
106
|
const assertDispatch = captureDispatchGuard(bridge);
|
|
106
107
|
if (operation === "hook.verify") {
|
|
107
108
|
if (params.daemonId !== state.daemon_id) throw Object.assign(new Error("Hook daemon identity differs"), { code: "native_hook_unproven" });
|
|
@@ -113,6 +114,7 @@ export async function serveDaemon(stateDir) {
|
|
|
113
114
|
// available while a request is marked active; callers must first prove the
|
|
114
115
|
// provider Session itself is idle.
|
|
115
116
|
if (operation === "daemon.stop") {
|
|
117
|
+
return await completeDaemonShutdown(paths.dir, state, save, async () => {
|
|
116
118
|
if (params.cancelTree) cancelPendingDispatch(bridge);
|
|
117
119
|
if (active && !params.cancelTree) throw Object.assign(new Error("productive operation is running"), { code: "tree_not_settled" });
|
|
118
120
|
await waitForSettlement({
|
|
@@ -120,12 +122,34 @@ export async function serveDaemon(stateDir) {
|
|
|
120
122
|
observe: () => observeCodexSessions(bridge, state.config, sessions, () => active),
|
|
121
123
|
...(params.cancelTree ? { cancel: async sessionId => { try { await cancelSessionWithBridge(bridge, { ...state.config, sessionId }); } catch (error) { if (error.code !== "turn_not_active") throw error; } } } : {})
|
|
122
124
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
return { clean: true };
|
|
126
|
+
}, [
|
|
127
|
+
["provider", () => bridge.close()],
|
|
128
|
+
["provider_resource", () => finishDaemonProcess(state.config, state.provider_process)],
|
|
129
|
+
["daemon_resource", () => finishDaemonProcess(state.config, state.resource_process)]
|
|
130
|
+
]);
|
|
125
131
|
}
|
|
132
|
+
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(paths.dir);
|
|
126
133
|
if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
|
|
127
134
|
if (operation === "session.create") { active = operation; try { await save({ active_operation: active }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
|
|
128
|
-
if (operation === "session.prompt") {
|
|
135
|
+
if (operation === "session.prompt") {
|
|
136
|
+
parseCapacityOperation(operationId);
|
|
137
|
+
// Admission can wait for a permit. Observe again after it returns, not
|
|
138
|
+
// just before the caller's backoff, so no child Turn races this dispatch.
|
|
139
|
+
// Reserve before the first await: distinct operation IDs are independent
|
|
140
|
+
// journal entries and must not both pass the idle check while inspecting.
|
|
141
|
+
active = operation;
|
|
142
|
+
try {
|
|
143
|
+
const tree = await inspectCodexTree(bridge, { ...state.config, ...params });
|
|
144
|
+
assertDispatch();
|
|
145
|
+
if (tree.settled !== true) throw Object.assign(new Error("native Session tree changed while waiting for admission"), { code: "tree_not_settled", details: { provider_session_id: params.sessionId, unsettled_sessions: tree.unsettled_sessions } });
|
|
146
|
+
if (/:capacity:[1-9]\d*:session\.prompt$|:child:[a-f0-9]+:followup:[1-9]\d*$/.test(operationId ?? ""))
|
|
147
|
+
await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
|
|
148
|
+
sessions.add(params.sessionId);
|
|
149
|
+
await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch });
|
|
150
|
+
}
|
|
151
|
+
finally { active = null; await save({ active_operation: null }); }
|
|
152
|
+
}
|
|
129
153
|
if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
|
|
130
154
|
if (operation === "session.inspect" || operation === "session.resume") {
|
|
131
155
|
const owned = new Set(sessions);
|
|
@@ -143,7 +167,28 @@ export async function serveDaemon(stateDir) {
|
|
|
143
167
|
}
|
|
144
168
|
throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
|
|
145
169
|
};
|
|
146
|
-
await removeSocket(paths.socket);
|
|
170
|
+
await removeSocket(paths.socket);
|
|
171
|
+
server = net.createServer(connection => {
|
|
172
|
+
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
173
|
+
connection.on("data", chunk => {
|
|
174
|
+
if (consumed) return;
|
|
175
|
+
buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
176
|
+
void (async () => {
|
|
177
|
+
let request;
|
|
178
|
+
try {
|
|
179
|
+
request = JSON.parse(buffer.slice(0, at));
|
|
180
|
+
if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" });
|
|
181
|
+
const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}, request.id), () => dispatch("session.inspect", request.params ?? {}));
|
|
182
|
+
if (result._shutdown) retireDaemonEndpoint(server);
|
|
183
|
+
connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
await retireFailedDaemon(server, state, error, save);
|
|
186
|
+
connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
trackDaemonConnections(server);
|
|
147
192
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await save({ shutdown_state: "running", active_operation: null }); return await new Promise(() => {});
|
|
148
193
|
}
|
|
149
194
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
@@ -2,7 +2,8 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
|
-
import {
|
|
5
|
+
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
6
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
6
7
|
import { ObservationClock, observedTimeout } from "./observation-clock.mjs";
|
|
7
8
|
import { observeModel } from "./model-observations.mjs";
|
|
8
9
|
|
|
@@ -230,7 +231,7 @@ export class CodexBridge {
|
|
|
230
231
|
this.descendants.set(threadId, { ...previous, status: "running", active_turn_id: turn.id, terminal_turn_id: null, provider_error: null });
|
|
231
232
|
} else if (value.method === "turn/completed" && (!previous.active_turn_id || previous.active_turn_id === turn.id)) {
|
|
232
233
|
this.descendants.set(threadId, { ...previous, status: turn.status === "completed" ? "completed" : turn.status ?? "unknown",
|
|
233
|
-
active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn) });
|
|
234
|
+
active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: previous.terminal_turn_id === turn.id ? previous.terminal_observed_at ?? now() : now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn) });
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
}
|
|
@@ -312,16 +313,16 @@ export class CodexBridge {
|
|
|
312
313
|
}
|
|
313
314
|
rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); } this.pending.clear(); }
|
|
314
315
|
async close() {
|
|
315
|
-
|
|
316
|
-
this.child.stdin.end();
|
|
317
|
-
await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2_000))]);
|
|
318
|
-
await stopProcessGroup(this.child, 2_000);
|
|
319
|
-
await this.modelObservations;
|
|
320
|
-
await this.journal.flush();
|
|
316
|
+
return await closeBridgeProcess(this, async () => { await this.modelObservations; await this.journal.flush(); });
|
|
321
317
|
}
|
|
322
318
|
}
|
|
323
319
|
|
|
324
|
-
async function withBridge(options, action) {
|
|
320
|
+
async function withBridge(options, action) {
|
|
321
|
+
const bridge = new CodexBridge(options); let primary;
|
|
322
|
+
try { await bridge.start(); return await action(bridge); }
|
|
323
|
+
catch (error) { primary = error; throw error; }
|
|
324
|
+
finally { try { await bridge.close(); } catch (error) { if (!primary) throw error; primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
|
|
325
|
+
}
|
|
325
326
|
function profile(options) { return { harness: "codex-desktop", model: options.model ?? null, reasoning: options.reasoning ?? null }; }
|
|
326
327
|
function threadId(result) { const id = result?.thread?.id ?? result?.threadId; return required(id, "Codex thread id"); }
|
|
327
328
|
function directChildren(bridge, sessionId) { return typeof bridge?.directChildren === "function" ? bridge.directChildren(sessionId) : []; }
|
|
@@ -542,25 +543,12 @@ function nativeTurnError(bridge, sessionId, turn) {
|
|
|
542
543
|
function turnItemEvidence(turn) {
|
|
543
544
|
const items = turn?.items;
|
|
544
545
|
return Array.isArray(items) && turn?.itemsView !== "notLoaded" ? { observed: true, count: items.length, types: items.map(item => item?.type ?? "unknown"),
|
|
546
|
+
items: items.map(item => ({ id: item?.id ?? null, type: item?.type ?? "unknown", status: item?.status ?? null })),
|
|
547
|
+
pending: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type) && !["completed", "failed", "declined"].includes(item?.status)),
|
|
545
548
|
possible_effects: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type)) } : { observed: false };
|
|
546
549
|
}
|
|
547
550
|
|
|
548
|
-
export
|
|
549
|
-
let current = error;
|
|
550
|
-
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
551
|
-
if (current.code === "turn_interrupted") {
|
|
552
|
-
const details = current.details ?? {}, native = details.provider_error;
|
|
553
|
-
const code = native?.codexErrorInfo;
|
|
554
|
-
return details.terminal_status === "failed" && typeof details.provider_session_id === "string" && details.provider_session_id === expectedSessionId
|
|
555
|
-
&& typeof details.turn_id === "string" && Boolean(details.turn_id) && details.native_turn_id === details.turn_id
|
|
556
|
-
&& (expectedTurnId === undefined || details.turn_id === expectedTurnId)
|
|
557
|
-
&& (code === "serverOverloaded" || code?.code === "serverOverloaded");
|
|
558
|
-
}
|
|
559
|
-
if (current.code !== "harness_adapter_failed") return false;
|
|
560
|
-
current = current.details?.cause ?? current.cause;
|
|
561
|
-
}
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
551
|
+
export { terminalCodexOverload } from "./codex-capacity-policy.mjs";
|
|
564
552
|
|
|
565
553
|
function isTerminalTurnStatus(status) { return ["completed", "failed", "interrupted", "cancelled"].includes(status); }
|
|
566
554
|
function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore) {
|
|
@@ -615,7 +603,9 @@ export async function inspectSession(options) {
|
|
|
615
603
|
|
|
616
604
|
export async function inspectSessionWithBridge(bridge, options) {
|
|
617
605
|
const sessionId = required(options.sessionId, "--session-id"); const value = await bridge.request("thread/read", { threadId: sessionId, includeTurns: false }); const thread = value?.thread ?? value;
|
|
618
|
-
|
|
606
|
+
const terminal = options.turnId ? (await readTurn(bridge, sessionId, options.turnId, true))?.turn : null;
|
|
607
|
+
const exact = terminal?.id === options.turnId && terminal?.status === "failed" ? { terminal_turn_id: terminal.id, terminal_status: terminal.status, provider_error: nativeTurnError(bridge, sessionId, terminal), native_turn_items: turnItemEvidence(terminal) } : {};
|
|
608
|
+
return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), ...exact, descendants: await observedChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
|
|
619
609
|
}
|
|
620
610
|
|
|
621
611
|
export async function cancelSession(options) {
|