@deksden-com/dd-flow-cli 0.9.0-beta.124 → 0.9.0-beta.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +10 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/vnext-code-review.js +10 -9
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +34 -4
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
|
+
import { operationProgressReporter } from './operation-progress.mjs';
|
|
1
3
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
4
|
+
import { errorRecord } from "./operation-errors.mjs";
|
|
2
5
|
import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
|
|
3
6
|
import { constants } from "node:fs";
|
|
4
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,14 +10,15 @@ import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unli
|
|
|
7
10
|
import { spawn } from "node:child_process";
|
|
8
11
|
import net from "node:net";
|
|
9
12
|
import { isDeepStrictEqual } from "node:util";
|
|
10
|
-
import { ObservationClock } from "./observation-clock.mjs";
|
|
13
|
+
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
11
14
|
import { hookBudget } from "./native-hook-command.mjs";
|
|
12
15
|
import { agyWorkspaceHooks } from "./agy-hooks.mjs";
|
|
13
16
|
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
14
17
|
import path from "node:path";
|
|
15
18
|
import { observeModel } from "./model-observations.mjs";
|
|
16
|
-
import { AGY_VERSION_TIMEOUT_MS, AgyError, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
19
|
+
import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, assertProfile, executable, observedProfile, runAgy, usageSnapshot } from "./dd-agy.mjs";
|
|
17
20
|
import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
21
|
+
import { readNativeLines, streamNativeDiagnostics, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
18
22
|
|
|
19
23
|
const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
|
|
20
24
|
class DaemonError extends AgyError {}
|
|
@@ -53,15 +57,15 @@ async function writeJson(file, value) { const temporary = `${file}.${process.pid
|
|
|
53
57
|
async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
|
|
54
58
|
async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
55
59
|
const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
56
|
-
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"])
|
|
57
|
-
|
|
60
|
+
export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"]) env[key] = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
|
|
61
|
+
const errorPayload = errorRecord;
|
|
58
62
|
async function forwardUsage(config, sessionId, result, tools, descendants) {
|
|
59
63
|
if (!config.ddFlowBin || !config.projectRoot) return;
|
|
60
64
|
const snapshot = usageSnapshot(result, tools), hasChildren = descendants.length > 0;
|
|
61
65
|
if (snapshot.total_tokens === null) return { status: "unavailable", reason: "provider_did_not_report_total_tokens" };
|
|
62
66
|
const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: hasChildren ? "unknown" : "physical_session", completeness: hasChildren ? "partial" : "complete", usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cache_read_tokens, totalTokens: snapshot.total_tokens }, tool_calls: snapshot.tool_calls };
|
|
63
67
|
const target = executable(config.ddFlowBin, ["agy", "usage", "ingest", "--project-root", config.projectRoot, "--json"]);
|
|
64
|
-
await
|
|
68
|
+
await commandText(target.command, target.args, { input: `${JSON.stringify(payload)}\n`, env: { DD_FLOW_HOME: config.ddFlowHome }, phase: "usage-ingest" });
|
|
65
69
|
return { status: "ingested" };
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -159,39 +163,11 @@ export async function qualifyAgyHooks(projectRoot, { stateDir = null, install =
|
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, requestId = null) {
|
|
162
|
-
const
|
|
163
|
-
return await
|
|
164
|
-
const client = net.createConnection(paths.socket); let buffer = "", settled = false, timer, checking = false, lastActivity = Date.now();
|
|
165
|
-
const clock = new ObservationClock({ timeoutMs, onGap: gap => { void appendFile(paths.journal, `${JSON.stringify({ kind: "observation_gap", payload: { ...gap, operation_id: request.id } })}\n`, { mode: 0o600 }).catch(() => {}); } });
|
|
166
|
-
const finish = (error, result) => { if (settled) return; settled = true; if (timer) clearInterval(timer); client.destroy(); error ? reject(error) : resolve(result); };
|
|
167
|
-
const poll = Math.min(5_000, Math.max(25, Math.floor(timeoutMs / 20)));
|
|
168
|
-
timer = setInterval(async () => {
|
|
169
|
-
if (settled || checking) return;
|
|
170
|
-
checking = true;
|
|
171
|
-
try {
|
|
172
|
-
if (operation !== "session.prompt") {
|
|
173
|
-
if (clock.sample()) finish(new DaemonError("daemon_timeout", `${operation} timed out`, true));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
let status = null;
|
|
177
|
-
try { status = await callDaemon(stateDir, "daemon.status", {}, Math.min(timeoutMs, 5_000)); }
|
|
178
|
-
catch { /* The original RPC retains the authoritative failure. */ }
|
|
179
|
-
const observed = Date.parse(status?.last_activity_at ?? "");
|
|
180
|
-
const progressed = status?.active_tree === true && Number.isFinite(observed) && observed > lastActivity;
|
|
181
|
-
if (progressed) lastActivity = observed;
|
|
182
|
-
if (!clock.sample(progressed ? lastActivity : undefined, progressed ? lastActivity : undefined)) return;
|
|
183
|
-
finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
|
|
184
|
-
operation_id: request.id,
|
|
185
|
-
last_activity_at: status?.last_activity_at ?? null,
|
|
186
|
-
active_tree: status?.active_tree === true
|
|
187
|
-
}));
|
|
188
|
-
} finally { checking = false; }
|
|
189
|
-
}, poll);
|
|
190
|
-
client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("data", chunk => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; try { const response = JSON.parse(buffer.slice(0, index)); response.ok ? finish(null, response.result) : finish(new DaemonError(response.error.code, response.error.message, response.error.retryable, response.error.details)); } catch (error) { finish(new DaemonError("daemon_protocol_mismatch", error.message)); } }); client.on("error", error => finish(new DaemonError("daemon_not_running", error.message, true))); client.on("end", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true))); client.on("close", () => finish(new DaemonError("daemon_connection_closed", `${operation} connection closed before a response`, true)));
|
|
191
|
-
});
|
|
166
|
+
const request = { schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
167
|
+
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'agy', timeoutMs });
|
|
192
168
|
}
|
|
193
169
|
|
|
194
|
-
function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
170
|
+
function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","reasoning","mode","projectRoot","ddFlowBin","ddFlowHome","resourceHome","noFlow","livenessTimeoutMs"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
|
|
195
171
|
// Called only after authorizeRetainedDaemonResume verifies the old daemon,
|
|
196
172
|
// provider processes and operation ledger. Native outcomes remain historical;
|
|
197
173
|
// their old liveness flags cannot describe the replacement process tree.
|
|
@@ -209,6 +185,8 @@ export function retainedAgyState(previous, sessionId) {
|
|
|
209
185
|
export async function startDaemon(options) {
|
|
210
186
|
const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
|
|
211
187
|
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 };
|
|
188
|
+
config.livenessTimeoutMs = options.livenessTimeoutMs ?? 1_800_000;
|
|
189
|
+
validateObservationDuration(config.livenessTimeoutMs);
|
|
212
190
|
await prepareRuntimeOwner(config, paths.dir);
|
|
213
191
|
assertDaemonRestartable(previous);
|
|
214
192
|
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; }
|
|
@@ -223,7 +201,7 @@ export async function startDaemon(options) {
|
|
|
223
201
|
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();
|
|
224
202
|
try {
|
|
225
203
|
await confirmDaemonProcess(config, resourceProcess, child);
|
|
226
|
-
const
|
|
204
|
+
const startupClock = new ObservationClock({ timeoutMs: 30_000 }); while (!startupClock.sample()) { await new Promise(resolve => setTimeout(resolve, 100));
|
|
227
205
|
const persisted = await readState(paths.dir);
|
|
228
206
|
if (persisted?.shutdown_state === "failed") {
|
|
229
207
|
const failure = persisted.startup_error ?? {};
|
|
@@ -243,7 +221,7 @@ export class Runtime {
|
|
|
243
221
|
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; }
|
|
244
222
|
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; }
|
|
245
223
|
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 }); } }
|
|
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", "
|
|
224
|
+
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", "0", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
|
|
247
225
|
requireSessionIdentity(requestedId) {
|
|
248
226
|
const observedId = this.init?.conversation_id ?? null;
|
|
249
227
|
if (!requestedId || requestedId === observedId) return observedId;
|
|
@@ -251,12 +229,39 @@ export class Runtime {
|
|
|
251
229
|
harness: "antigravity-cli", expected_session_id: requestedId, observed_session_id: observedId
|
|
252
230
|
});
|
|
253
231
|
}
|
|
254
|
-
markActivity() { this.lastActivityAt = new Date().toISOString(); }
|
|
232
|
+
markActivity() { const current = this.active; this.lastActivityAt = new Date().toISOString(); this.progress = { cursor: (this.progress?.cursor ?? 0) + 1, observed_at: Date.now() }; void current?.publishProgress?.('native_progress', { session_id: this.init?.conversation_id, observed_at: this.lastActivityAt }).catch(error => { if(this.active === current) current.reject(error); }); }
|
|
233
|
+
markTerminalActivity(result) {
|
|
234
|
+
const current = this.active;
|
|
235
|
+
if (!current) return;
|
|
236
|
+
const key = createHash("sha256").update(JSON.stringify({ conversation_id: result.conversation_id ?? this.init?.conversation_id, status: result.status, num_turns: result.num_turns, error: result.error, response: result.response ?? result.text })).digest("hex");
|
|
237
|
+
current.terminalProgress ??= new Set();
|
|
238
|
+
if (current.terminalProgress.has(key) || current.terminalProgress.size >= 1_024) return;
|
|
239
|
+
current.terminalProgress.add(key); this.markActivity();
|
|
240
|
+
}
|
|
255
241
|
observeProviderActivity(event) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
242
|
+
if (!this.active) return;
|
|
243
|
+
const root = this.init?.conversation_id, step = event.step_update;
|
|
244
|
+
const id = step?.conversation_id ?? event.result?.conversation_id ?? root;
|
|
245
|
+
let parent = id; const seen = new Set();
|
|
246
|
+
while (parent && parent !== root && !seen.has(parent)) { seen.add(parent); const child = this.descendants.get(parent); if (child?.activity_generation !== this.active.generation) return; parent = child.parent_provider_session_id; }
|
|
247
|
+
if (!root || parent !== root) return;
|
|
248
|
+
if (event.event === "result") { if (["SUCCESS", "ERROR", "CANCELLED"].includes(event.result?.status)) this.markTerminalActivity(event.result); return; }
|
|
249
|
+
if (event.event !== "step_update") return;
|
|
250
|
+
const index = step?.step_index ?? (/^\d+$/.test(step?.step_id ?? "") ? Number(step.step_id) : null);
|
|
251
|
+
if (!Number.isInteger(index)) return;
|
|
252
|
+
const floor = this.sessionObservations.get(id)?.step_floor;
|
|
253
|
+
if (floor != null && index <= floor) return;
|
|
254
|
+
const tool = step.tool_call ?? step.tool_info ?? (step.step_type === "tool" ? step.tool_name : null);
|
|
255
|
+
if (!step.content && !step.text && !step.thought && !tool && !step.error && !(step.subagent_info?.subagents?.length)) return;
|
|
256
|
+
const fingerprint = createHash("sha256").update(JSON.stringify({ content: step.content, text: step.text, thought: step.thought, tool_call: step.tool_call, tool_info: step.tool_info, tool_name: step.tool_name, ...(tool ? { state: step.state, status: step.status } : {}), error: step.error, subagent_info: step.subagent_info })).digest("hex");
|
|
257
|
+
this.progressSteps ??= new Map(); const prior = this.progressSteps.get(id);
|
|
258
|
+
if (prior && index < prior.index) return;
|
|
259
|
+
const snapshots = prior?.index === index ? prior.seen : new Set();
|
|
260
|
+
if (snapshots.has(fingerprint)) return;
|
|
261
|
+
// Native step snapshots can alternate/replay; scope retained dedup to the
|
|
262
|
+
// current step. Too many unique snapshots becomes uncertain, never a grant.
|
|
263
|
+
if (snapshots.size >= 1_024) return;
|
|
264
|
+
snapshots.add(fingerprint); this.progressSteps.set(id, { index, seen: snapshots }); this.markActivity();
|
|
260
265
|
}
|
|
261
266
|
async start(sessionId = null) {
|
|
262
267
|
if (this.drainingError) throw this.drainingError;
|
|
@@ -284,14 +289,15 @@ export class Runtime {
|
|
|
284
289
|
const providerChild = this.child;
|
|
285
290
|
let allowProviderFinalization;
|
|
286
291
|
const providerRegistered = new Promise(resolve => { allowProviderFinalization = resolve; });
|
|
287
|
-
|
|
288
|
-
this.child.
|
|
292
|
+
const observationFailure = error => { this.drainingError ??= error; readyReject(error); this.active?.reject(error); };
|
|
293
|
+
this.nativeDrain = readNativeLines(this.child.stdout, line => { this.buffer = `${line}\n`; return this.draining = this.drain(readyResolve); }, observationFailure);
|
|
294
|
+
this.diagnostics = streamNativeDiagnostics(this.child.stderr, chunk => this.journal("stderr", { text: chunk }), observationFailure);
|
|
289
295
|
this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
|
|
290
296
|
// A dead leader is not yet a finalized tree: stop must also join the
|
|
291
297
|
// pending drain, process-group check and final persistence/lease writes.
|
|
292
298
|
this.providerFinalization = new Promise((resolve, reject) => {
|
|
293
299
|
this.child.on("close", (code, signal) => {
|
|
294
|
-
void this.
|
|
300
|
+
void Promise.all([this.nativeDrain, this.diagnostics]).then(async () => {
|
|
295
301
|
const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
|
|
296
302
|
const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
|
|
297
303
|
if (beforeInit) readyReject(error);
|
|
@@ -316,15 +322,38 @@ export class Runtime {
|
|
|
316
322
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
|
|
317
323
|
finally { allowProviderFinalization(); }
|
|
318
324
|
let readyTimer;
|
|
319
|
-
try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer =
|
|
325
|
+
try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = observedTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000); })]); }
|
|
320
326
|
finally { clearTimeout(readyTimer); }
|
|
321
327
|
}
|
|
322
328
|
async drain(readyResolve) { let index; while ((index = this.buffer.indexOf("\n")) >= 0) { const line = this.buffer.slice(0, index).trim(); this.buffer = this.buffer.slice(index + 1); if (!line) continue; let event; try { event = JSON.parse(line); } catch { await this.journal("invalid_json", { line: line.slice(0, 1000) }); continue; }
|
|
323
329
|
// Record an early provider rejection before journalling: a short-lived
|
|
324
330
|
// child can close while the asynchronous write is still pending.
|
|
325
|
-
if (
|
|
331
|
+
if (event.event === "result" && ["ERROR", "CANCELLED"].includes(event.result?.status)) {
|
|
332
|
+
this.lastTerminalFailure ??= this.state.last_terminal_failure;
|
|
333
|
+
const repeated = Boolean(this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === event.result.error
|
|
334
|
+
&& this.lastTerminalFailure.details.provider_session_id === (event.result.conversation_id || this.init?.conversation_id || null)
|
|
335
|
+
&& (this.active?.previousError === event.result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === event.result.error));
|
|
336
|
+
event.terminal_metadata = repeated ? this.lastTerminalFailure.details : { observed_at: new Date().toISOString(), provider_session_id: event.result.conversation_id || this.init?.conversation_id || null,
|
|
337
|
+
operation_id: this.active?.operationId ?? null, turn_generation: this.active?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" };
|
|
338
|
+
if (!this.init) this.startupError ??= agyTerminalFailure(event.result, event.terminal_metadata);
|
|
339
|
+
this.receivedTerminalMetadata = event.terminal_metadata;
|
|
340
|
+
}
|
|
326
341
|
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 ?? {}); } } }
|
|
327
|
-
observeStep(step) {
|
|
342
|
+
observeStep(step) {
|
|
343
|
+
const sessionId = step.conversation_id ?? this.init?.conversation_id, observation = this.sessionObservations.get(sessionId) ?? {};
|
|
344
|
+
const fresh = Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1) && step.step_index > (observation.step_floor ?? -1);
|
|
345
|
+
const bound = sessionId === this.init?.conversation_id || this.descendants.get(sessionId)?.activity_generation === this.active?.generation;
|
|
346
|
+
// Tree liveness is broader than current-Turn model progress: fresh work
|
|
347
|
+
// from a retained child invalidates settlement without adopting this Turn.
|
|
348
|
+
if (fresh && (sessionId === this.init?.conversation_id || this.descendants.has(sessionId))) this.childActivity(sessionId);
|
|
349
|
+
if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) });
|
|
350
|
+
this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id));
|
|
351
|
+
for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) {
|
|
352
|
+
const prior = this.descendants.get(child.conversation_id);
|
|
353
|
+
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",
|
|
354
|
+
...(fresh && bound && this.active ? { activity_generation: this.active.generation } : {}) });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
328
357
|
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
358
|
async captureChildTools() {
|
|
330
359
|
this.capturedToolFacts ??= new Map();
|
|
@@ -346,7 +375,7 @@ export class Runtime {
|
|
|
346
375
|
}
|
|
347
376
|
}
|
|
348
377
|
}
|
|
349
|
-
async finishTurn(result) {
|
|
378
|
+
async finishTurn(result, retainedMetadata = null) {
|
|
350
379
|
if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
|
|
351
380
|
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 });
|
|
352
381
|
if (result.conversation_id && result.conversation_id !== this.init?.conversation_id) {
|
|
@@ -359,12 +388,18 @@ export class Runtime {
|
|
|
359
388
|
return;
|
|
360
389
|
}
|
|
361
390
|
const current = this.active;
|
|
391
|
+
const terminalMetadata = retainedMetadata ?? this.receivedTerminalMetadata ?? (this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result.error
|
|
392
|
+
&& this.lastTerminalFailure.details.provider_session_id === (result.conversation_id || this.init?.conversation_id || null)
|
|
393
|
+
&& (current?.previousError === result.error || this.lastResult?.status === "ERROR" && this.lastResult.error === result.error) ? this.lastTerminalFailure.details
|
|
394
|
+
: { observed_at: new Date().toISOString(), provider_session_id: result.conversation_id || this.init?.conversation_id || null, operation_id: current?.operationId ?? null,
|
|
395
|
+
turn_generation: current?.generation ?? null, journal_locator: this.paths.journal, native_source: "agy.result" });
|
|
396
|
+
this.receivedTerminalMetadata = null;
|
|
362
397
|
// The ordered stream returns one result per input. Model usage counters
|
|
363
398
|
// are not request IDs: native command rejections can report zero turns.
|
|
364
399
|
// prompt() drains earlier events before accepting another input.
|
|
365
400
|
if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
|
|
366
401
|
await this.captureChildTools();
|
|
367
|
-
this.lastResult = result; this.
|
|
402
|
+
this.lastResult = result; this.markTerminalActivity(result);
|
|
368
403
|
const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
|
|
369
404
|
const pendingTerminal = current && result.status === "SUCCESS";
|
|
370
405
|
if (pendingTerminal) current.terminalResult = result;
|
|
@@ -377,13 +412,9 @@ export class Runtime {
|
|
|
377
412
|
if (this.active !== current) return;
|
|
378
413
|
if (result.status === "ERROR" || result.status === "CANCELLED") {
|
|
379
414
|
this.active = null;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
: /\b429\b/.test(normalized) ? "agy_provider_limit_unknown"
|
|
384
|
-
: /auth|unauthori[sz]ed|forbidden|account/.test(normalized) ? "agy_provider_account"
|
|
385
|
-
: "agy_provider_failed";
|
|
386
|
-
return current.reject(new DaemonError(code, message, code === "agy_provider_rate_limited", { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
|
|
415
|
+
this.lastTerminalFailure = agyTerminalFailure(result, { ...terminalMetadata, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest });
|
|
416
|
+
await this.persist({ last_terminal_failure: errorRecord(this.lastTerminalFailure) });
|
|
417
|
+
return current.reject(this.lastTerminalFailure);
|
|
387
418
|
}
|
|
388
419
|
await this.completePendingTurn();
|
|
389
420
|
}
|
|
@@ -401,6 +432,9 @@ export class Runtime {
|
|
|
401
432
|
current.resolve(receipt);
|
|
402
433
|
}
|
|
403
434
|
receipt(result = this.lastResult) {
|
|
435
|
+
if (this.state.last_terminal_failure && !this.lastTerminalFailure) this.lastTerminalFailure = this.state.last_terminal_failure;
|
|
436
|
+
const terminalMetadata = this.lastTerminalFailure && this.lastTerminalFailure.details?.provider_result?.error === result?.error
|
|
437
|
+
&& this.lastTerminalFailure.details.provider_session_id === (result?.conversation_id || this.init?.conversation_id || null) ? this.lastTerminalFailure.details : null;
|
|
404
438
|
const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
|
|
405
439
|
const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
|
|
406
440
|
const treeSettled = [...this.descendants.values()].every(child => child.tree_settled === true || ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
|
|
@@ -409,13 +443,14 @@ export class Runtime {
|
|
|
409
443
|
// non-idle Stop still wins, and every discovered child must be terminal.
|
|
410
444
|
const rootSettled = observation?.stop?.fullyIdle !== false;
|
|
411
445
|
const terminalPending = this.active?.terminalResult === result;
|
|
412
|
-
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
446
|
+
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(terminalMetadata ? { terminal_metadata: terminalMetadata } : {}), ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
413
447
|
}
|
|
414
448
|
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
415
|
-
async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null) {
|
|
449
|
+
async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null, livenessTimeoutMs = 1_800_000) {
|
|
450
|
+
validateObservationDuration(livenessTimeoutMs);
|
|
416
451
|
if (!text) throw new DaemonError("prompt_required", "prompt is required");
|
|
417
452
|
await this.start(); await this.draining;
|
|
418
|
-
if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running");
|
|
453
|
+
if (this.active || this.promptObservation) throw new DaemonError("operation_in_progress", "an Antigravity turn or its mandatory dispatch observation is already running");
|
|
419
454
|
if (this.state.last_hook_rejection && !this.receipt().settled) throw new DaemonError("tree_not_settled", "Rejected hook does not prove that the prior Antigravity tree stopped", false, { primary_error: this.state.last_hook_rejection });
|
|
420
455
|
if (this.state.unclaimed_activity || (this.lastResult && !this.receipt().settled)) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
|
|
421
456
|
let resolveTurn, rejectTurn;
|
|
@@ -423,28 +458,71 @@ export class Runtime {
|
|
|
423
458
|
// Cancellation can reject while admission persistence is still pending.
|
|
424
459
|
void reply.catch(() => {});
|
|
425
460
|
const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, previousError: this.lastResult?.error ?? null };
|
|
461
|
+
let resolveDispatchJournal;
|
|
462
|
+
const dispatchJournal = current.dispatchJournal = new Promise(resolve => { resolveDispatchJournal = resolve; });
|
|
463
|
+
this.promptObservation = current;
|
|
464
|
+
const finishDispatchJournal = () => {
|
|
465
|
+
current.dispatchJournalSettled = true; resolveDispatchJournal();
|
|
466
|
+
if (current.observerReturned && this.promptObservation === current) this.promptObservation = null;
|
|
467
|
+
};
|
|
426
468
|
// Reserve before persistence so different operation IDs cannot overlap.
|
|
469
|
+
current.publishProgress = operationProgressReporter();
|
|
427
470
|
this.active = current;
|
|
428
471
|
this.hookRequests = new Map();
|
|
429
472
|
const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
|
|
430
473
|
this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
|
|
431
|
-
this.lastProviderEvent = null;
|
|
474
|
+
this.lastProviderEvent = null; this.progressSteps = new Map(); this.progressHooks = new Set(); this.progress = undefined;
|
|
432
475
|
try {
|
|
433
476
|
await this.persist({ active_tree: true, last_hook_rejection: null });
|
|
434
477
|
assertDispatch();
|
|
435
478
|
assertDaemonDispatch(this.paths?.dir);
|
|
436
479
|
if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
|
|
480
|
+
current.dispatched = true;
|
|
437
481
|
this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
|
|
438
|
-
if (error) {
|
|
482
|
+
if (error) { this.drainingError ??= nativeObservationError(error); current.dispatchObservationError = this.drainingError; current.reject(this.drainingError); finishDispatchJournal(); }
|
|
483
|
+
else void Promise.resolve().then(() => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: this.init?.conversation_id, native_request_id: current.generation })).then(finishDispatchJournal, cause => {
|
|
484
|
+
this.drainingError ??= nativeObservationError(cause); current.dispatchObservationError = this.drainingError;
|
|
485
|
+
current.reject(this.drainingError); finishDispatchJournal();
|
|
486
|
+
});
|
|
439
487
|
});
|
|
440
|
-
} catch (error) {
|
|
441
|
-
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (current.dispatched) { this.drainingError ??= nativeObservationError(error); error = this.drainingError; }
|
|
490
|
+
else if (this.active === current) this.active = null;
|
|
491
|
+
current.reject(error); finishDispatchJournal();
|
|
492
|
+
}
|
|
493
|
+
const clock = new ObservationClock({ timeoutMs: livenessTimeoutMs, onGap: gap => { void this.journal("observation_gap", { gap, operation_id: operationId }).catch(() => {}); } });
|
|
494
|
+
let checking = false;
|
|
495
|
+
const timer = setInterval(() => {
|
|
496
|
+
if (this.active !== current || checking || !clock.sample(this.progress)) return;
|
|
497
|
+
checking = true; const cursor = this.progress?.cursor;
|
|
498
|
+
void this.completePendingTurn().then(() => {
|
|
499
|
+
if (this.active !== current || this.progress?.cursor !== cursor || !clock.sample(this.progress)) return;
|
|
500
|
+
current.reject(new DaemonError("operation_observation_lost", "Antigravity native outcome is unknown after the productive silence window", true, { operation_id: operationId, turn_generation: current.generation, native_outcome: "unknown", last_activity_at: this.lastActivityAt ?? null }));
|
|
501
|
+
}).catch(error => {
|
|
502
|
+
if (this.active === current && this.progress?.cursor === cursor) current.reject(new DaemonError("operation_observation_lost", "Antigravity quiet reconciliation could not confirm the native outcome", true, { operation_id: operationId, turn_generation: current.generation, native_outcome: "unknown", observation_error: errorRecord(error) }));
|
|
503
|
+
}).finally(() => { checking = false; });
|
|
504
|
+
}, Math.max(10, Math.min(1_000, livenessTimeoutMs / 4)));
|
|
505
|
+
try {
|
|
506
|
+
const result = await reply;
|
|
507
|
+
await dispatchJournal;
|
|
508
|
+
if (current.dispatchObservationError) {
|
|
509
|
+
const error = current.dispatchObservationError;
|
|
510
|
+
if (error.code !== "operation_observation_lost") throw error;
|
|
511
|
+
throw Object.assign(new Error("Antigravity completed natively but its dispatch journal could not be saved", { cause: error.cause ?? error }), {
|
|
512
|
+
code: "native_outcome_observation_failed", details: { native_outcome: result, settled: false, observation_error: errorRecord(error) }
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return result;
|
|
516
|
+
} finally {
|
|
517
|
+
clearInterval(timer); current.observerReturned = true;
|
|
518
|
+
if (current.dispatchJournalSettled && this.promptObservation === current) this.promptObservation = null;
|
|
519
|
+
}
|
|
442
520
|
}
|
|
443
521
|
childActivity(sessionId) {
|
|
444
522
|
if (sessionId === this.init?.conversation_id && !this.active) this.state.unclaimed_activity = true;
|
|
445
523
|
const child = this.descendants.get(sessionId);
|
|
446
524
|
if (!child) return;
|
|
447
|
-
this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, settlement_daemon_id: null
|
|
525
|
+
this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, settlement_daemon_id: null });
|
|
448
526
|
}
|
|
449
527
|
async childForHook(conversationId) {
|
|
450
528
|
if (conversationId === this.init?.conversation_id) return null;
|
|
@@ -466,8 +544,7 @@ export class Runtime {
|
|
|
466
544
|
const correction = { code: error.code, message: error.message, details: error.details, event, conversation_id: payload.conversationId ?? null,
|
|
467
545
|
daemon_id: this.config.daemonId, operation_id: current.operationId, turn_generation: current.generation };
|
|
468
546
|
try {
|
|
469
|
-
await this.journal("hook_correction", { correction });
|
|
470
|
-
await this.persist({ last_hook_correction: correction });
|
|
547
|
+
await hookBudget(1_000, null).run(async () => { await this.journal("hook_correction", { correction }); await this.persist({ last_hook_correction: correction }); }, "correction_persistence");
|
|
471
548
|
current.hookCorrectionGeneration = current.generation;
|
|
472
549
|
return;
|
|
473
550
|
} catch (storage) { error = Object.assign(new Error("Could not persist native hook correction"), { code: "native_hook_receipt_invalid", details: { effect: "unknown", primary_error: correction, receipt_error: storage.message } }); }
|
|
@@ -475,7 +552,7 @@ export class Runtime {
|
|
|
475
552
|
const rejection = this.state.last_hook_rejection && this.state.last_hook_rejection.turn_generation === current?.generation ? this.state.last_hook_rejection : { code: error.code ?? "agy_hook_rejected", message: error.message ?? String(error), details: error.details, event, conversation_id: payload.conversationId ?? null, tool: payload.toolCall?.name ?? null, daemon_id: this.config.daemonId, operation_id: current?.operationId ?? null, turn_generation: current?.generation ?? null };
|
|
476
553
|
this.state.last_hook_rejection = rejection;
|
|
477
554
|
if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
|
|
478
|
-
try { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }
|
|
555
|
+
try { await hookBudget(1_000, null).run(async () => { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }, "failure_persistence"); }
|
|
479
556
|
catch (storageError) { rejection.receipt_error = { code: storageError.code, message: storageError.message }; }
|
|
480
557
|
}
|
|
481
558
|
async observeHookFailures() {
|
|
@@ -489,7 +566,7 @@ export class Runtime {
|
|
|
489
566
|
await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
|
|
490
567
|
}
|
|
491
568
|
}
|
|
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 (!
|
|
569
|
+
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"); this.progressHooks ??= new Set(); const scoped = this.active && (!child || child.activity_generation === this.active.generation); const progressKey = JSON.stringify([conversationId, step, execution, event, payload.toolCall ?? null, payload.fullyIdle ?? null]); if (scoped && step !== null && (["PreToolUse", "PostToolUse"].includes(event) || event === "Stop" && payload.fullyIdle === true) && !this.progressHooks.has(progressKey) && this.progressHooks.size < 1_024) { this.progressHooks.add(progressKey); 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;
|
|
493
570
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
494
571
|
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" });
|
|
495
572
|
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 };
|
|
@@ -508,6 +585,7 @@ export class Runtime {
|
|
|
508
585
|
await this.providerFinalization;
|
|
509
586
|
}
|
|
510
587
|
}
|
|
588
|
+
await this.promptObservation?.dispatchJournal;
|
|
511
589
|
} catch (error) {
|
|
512
590
|
if (!this.drainingError) throw error;
|
|
513
591
|
this.drainingError.cleanup_error = errorRecord(error);
|
|
@@ -548,7 +626,7 @@ export async function serveDaemon(stateDir) {
|
|
|
548
626
|
let buffer = "", consumed = false; socket.setEncoding("utf8");
|
|
549
627
|
socket.on("data", chunk => {
|
|
550
628
|
if (consumed) return;
|
|
551
|
-
buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
629
|
+
buffer += chunk; if (!admitNativeRequestBuffer(buffer, socket, RESPONSE_SCHEMA)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
552
630
|
void (async () => {
|
|
553
631
|
let request;
|
|
554
632
|
try {
|
|
@@ -580,7 +658,7 @@ async function dispatch(runtime, request) {
|
|
|
580
658
|
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(request.operation)) assertDaemonDispatch(runtime.paths.dir);
|
|
581
659
|
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() }; }
|
|
582
660
|
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(); }
|
|
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); }
|
|
661
|
+
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, params.livenessTimeoutMs ?? runtime.config.livenessTimeoutMs ?? 1_800_000); }
|
|
584
662
|
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
|
|
585
663
|
if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
586
664
|
if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
|
|
@@ -611,3 +689,4 @@ async function dispatch(runtime, request) {
|
|
|
611
689
|
throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
|
|
612
690
|
}
|
|
613
691
|
import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
|
|
692
|
+
import { commandText } from "./process-json.mjs";
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
6
5
|
import { checkObservedProfile } from "./model-observations.mjs";
|
|
7
|
-
import { stopProcessGroup } from "./managed-daemon.mjs";
|
|
8
|
-
import { errorRecord } from "./operation-errors.mjs";
|
|
9
6
|
|
|
10
7
|
const DEFAULT_MODEL = "gemini-3.1-pro-high";
|
|
11
8
|
export const AGY_VERSION_TIMEOUT_MS = 30_000;
|
|
@@ -14,31 +11,21 @@ export class AgyError extends Error {
|
|
|
14
11
|
constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; }
|
|
15
12
|
}
|
|
16
13
|
|
|
14
|
+
export function agyTerminalFailure(result, metadata = {}) {
|
|
15
|
+
const message = String(result.error ?? "Antigravity returned ERROR"), normalized = message.toLowerCase();
|
|
16
|
+
const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
|
|
17
|
+
: /rate.?limit|too many requests/.test(normalized) ? "agy_provider_rate_limited" : /\b429\b/.test(normalized) ? "agy_provider_limit_unknown"
|
|
18
|
+
: /auth|unauthori[sz]ed|forbidden|account|eligib/.test(normalized) ? "agy_provider_account" : "agy_provider_failed";
|
|
19
|
+
return new AgyError(code, message, code === "agy_provider_rate_limited", { provider_result: result, ...metadata });
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function executable(bin, args = []) {
|
|
18
23
|
return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args };
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export async function runAgy(bin, args, options = {}) {
|
|
22
|
-
const
|
|
23
|
-
return
|
|
24
|
-
const child = spawn(target.command, target.args, { cwd: options.cwd, env: options.env ?? process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
25
|
-
let stdout = "", stderr = "", settled = false;
|
|
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);
|
|
31
|
-
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
|
|
32
|
-
child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk);
|
|
33
|
-
child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
|
|
34
|
-
child.on("error", error => finish(error));
|
|
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
|
-
});
|
|
40
|
-
child.stdin.end(options.stdin ?? "");
|
|
41
|
-
});
|
|
27
|
+
const stdout = await commandText(bin, args, { cwd: options.cwd, env: options.env, input: options.stdin ?? "", timeoutMs: options.timeoutMs, phase: "native-doctor" });
|
|
28
|
+
return { stdout, stderr: "", code: 0 };
|
|
42
29
|
}
|
|
43
30
|
|
|
44
31
|
export function usageSnapshot(result = {}, toolCalls = {}) {
|
|
@@ -68,3 +55,4 @@ export async function doctor(options = {}) {
|
|
|
68
55
|
return { compatible: true, observed_runtime: { agy: version, dd_harness_contract: "dd-agy-harness@1" }, isolation: { gemini_dir: "verified", app_data_dir: "verified" }, auth: "available", models: modelIds };
|
|
69
56
|
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
70
57
|
}
|
|
58
|
+
import { commandText } from "./process-json.mjs";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
2
|
+
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
1
3
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
2
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
5
|
import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
6
|
import net from "node:net";
|
|
5
7
|
import { durableDaemonDispatch, completeDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
|
|
6
|
-
import {
|
|
8
|
+
import { ObservationClock } from "./observation-clock.mjs";
|
|
7
9
|
import { parseCapacityOperation } from "./codex-capacity-policy.mjs";
|
|
8
10
|
import path from "node:path";
|
|
9
11
|
import { spawn } from "node:child_process";
|
|
@@ -70,15 +72,8 @@ export async function cancelCodexTree(bridge, options, { remember = () => {}, ac
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
73
|
-
const
|
|
74
|
-
return await
|
|
75
|
-
const client = net.createConnection(target.socket); let buffer = ""; let settled = false; const timer = operation === "session.prompt" ? null : observedTimeout(() => finish(Object.assign(new Error(`${operation} timed out`), { code: "daemon_timeout", retryable: true })), timeoutMs);
|
|
76
|
-
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); client.destroy(); error ? reject(error) : resolve(value); };
|
|
77
|
-
client.setEncoding("utf8"); client.on("connect", () => client.write(`${JSON.stringify(request)}\n`)); client.on("error", (error) => finish(Object.assign(error, { code: "daemon_not_running", retryable: true })));
|
|
78
|
-
client.on("end", () => finish(Object.assign(new Error("daemon closed the request without a response"), { code: "daemon_connection_closed", retryable: true })));
|
|
79
|
-
client.on("close", () => finish(Object.assign(new Error("daemon connection closed without a response"), { code: "daemon_connection_closed", retryable: true })));
|
|
80
|
-
client.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; try { const response = JSON.parse(buffer.slice(0, at)); if (response.schema_id !== responseSchema || response.id !== request.id) throw new Error("invalid daemon response"); response.ok ? finish(null, response.result) : finish(Object.assign(new Error(response.error?.message ?? "daemon error"), response.error)); } catch (error) { finish(Object.assign(error, { code: "daemon_protocol" })); } });
|
|
81
|
-
});
|
|
75
|
+
const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
76
|
+
return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: responseSchema, harness: 'codex', timeoutMs });
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
export async function startDaemon(options) {
|
|
@@ -92,7 +87,7 @@ export async function startDaemon(options) {
|
|
|
92
87
|
if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
93
88
|
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);
|
|
94
89
|
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();
|
|
95
|
-
try { await confirmDaemonProcess(config, resourceProcess, child); const
|
|
90
|
+
try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: 60_000 }); while (!startupClock.sample()) { 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" }); }
|
|
96
91
|
catch (error) { await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.state), state.daemon_id); throw error; }
|
|
97
92
|
}
|
|
98
93
|
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) }); }
|
|
@@ -146,11 +141,11 @@ export async function serveDaemon(stateDir) {
|
|
|
146
141
|
if (/:capacity:[1-9]\d*:session\.prompt$|:child:[a-f0-9]+:followup:[1-9]\d*$/.test(operationId ?? ""))
|
|
147
142
|
await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
|
|
148
143
|
sessions.add(params.sessionId);
|
|
149
|
-
await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch });
|
|
144
|
+
await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, operationId, assertDispatch });
|
|
150
145
|
}
|
|
151
146
|
finally { active = null; await save({ active_operation: null }); }
|
|
152
147
|
}
|
|
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 }); } }
|
|
148
|
+
if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch, operationId }); } finally { active = null; await save({ active_operation: null }); } }
|
|
154
149
|
if (operation === "session.inspect" || operation === "session.resume") {
|
|
155
150
|
const owned = new Set(sessions);
|
|
156
151
|
for (const id of owned) for (const child of bridge.directChildren?.(id) ?? []) owned.add(child.provider_session_id);
|
|
@@ -172,7 +167,7 @@ export async function serveDaemon(stateDir) {
|
|
|
172
167
|
connection.setEncoding("utf8"); let buffer = "", consumed = false;
|
|
173
168
|
connection.on("data", chunk => {
|
|
174
169
|
if (consumed) return;
|
|
175
|
-
buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
170
|
+
buffer += chunk; if (!admitNativeRequestBuffer(buffer, connection, responseSchema)) { consumed = true; return; } const at = buffer.indexOf("\n"); if (at < 0) return; consumed = true;
|
|
176
171
|
void (async () => {
|
|
177
172
|
let request;
|
|
178
173
|
try {
|