@deksden-com/dd-flow-cli 0.9.0-beta.36 → 0.9.0-beta.37

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.37
4
+
5
+ ### Patch Changes
6
+
7
+ - Wait for Antigravity provider-exit finalization before acknowledging clean shutdown or replacing the provider, preserving interrupted outcomes and cleanup failures. Treat signal-terminated process leaders as exited when checking remaining group ownership, and release completed initialization timers.
8
+
3
9
  ## 0.9.0-beta.36
4
10
 
5
11
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.36",
4
- "cli_commit": "af66ec6e0c2e101f6966c96196d862599bde97eb",
5
- "built_at": "2026-09-08T00:24:29.896Z",
3
+ "cli_version": "0.9.0-beta.37",
4
+ "cli_commit": "04f94a5faa271bc0e837d6d3020f3a3668835e17",
5
+ "built_at": "2026-09-08T02:59:50.109Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.0.6",
8
8
  "commit": "c6fc50cb3b5526ea0162ee4454d1ee36f17be2da",
@@ -161,7 +161,7 @@ export async function startDaemon(options) {
161
161
  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); };
162
162
 
163
163
  export class Runtime {
164
- constructor(paths, state) { this.paths = paths; this.state = state; this.config = state.config; this.child = null; this.providerProcess = state.provider_process ?? null; this.buffer = ""; this.init = null; this.readyPromise = null; this.active = null; this.exited = false; this.toolCalls = new Map(state.tool_calls ?? []); this.completedToolSteps = new Set(state.completed_tool_steps ?? []); this.toolIdentityComplete = state.tool_identity_complete !== false; 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; }
164
+ 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.toolCalls = new Map(state.tool_calls ?? []); this.completedToolSteps = new Set(state.completed_tool_steps ?? []); this.toolIdentityComplete = state.tool_identity_complete !== false; 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; }
165
165
  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_calls: [...this.toolCalls], completed_tool_steps: [...this.completedToolSteps], tool_identity_complete: this.toolIdentityComplete, 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); this.persisting = this.persisting.then(() => writeJson(this.paths.state, snapshot)); for (const processRecord of [this.state.resource_process, this.providerProcess]) if (processRecord?.id && processRecord.lease_token) this.persisting = this.persisting.then(async () => { try { await heartbeatDaemonProcess(this.config, processRecord); } catch {} }); return this.persisting; }
166
166
  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 }); } }
167
167
  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"])]; }
@@ -185,6 +185,7 @@ export class Runtime {
185
185
  this.requireSessionIdentity(sessionId);
186
186
  return this.init;
187
187
  }
188
+ await this.providerFinalization;
188
189
  const target = executable(this.config.bin, this.args(sessionId)); this.exited = false; this.buffer = ""; this.init = null;
189
190
  let readyResolve, readyReject; this.readyPromise = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
190
191
  // The provider can reject before durable registration finishes. Keep a
@@ -200,24 +201,33 @@ export class Runtime {
200
201
  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; } }); });
201
202
  this.child.stderr.setEncoding("utf8").on("data", chunk => void this.journal("stderr", { text: String(chunk).slice(0, 4000) }));
202
203
  this.child.on("error", error => { readyReject(error); if (this.active) { this.active.reject(error); this.active = null; } });
203
- this.child.on("close", (code, signal) => { void this.draining.then(() => { const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
204
- const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
205
- if (beforeInit) readyReject(error);
206
- if (this.active) { this.active.reject(error); this.active = null; }
207
- const patch = beforeInit ? { shutdown_state: "failed", startup_error: errorPayload(error), provider_exit: { code, signal }, active_tree: false } : { provider_exit: { code, signal }, active_tree: interrupted };
208
- void providerRegistered.then(async () => {
209
- try {
210
- await stopProcessGroup(providerChild);
211
- await this.persist(patch);
212
- await finishDaemonProcess(this.config, providerProcess, beforeInit || interrupted ? "failed" : "stopped", beforeInit || interrupted ? "provider_interrupted" : "provider_stopped");
213
- } catch (cleanup) { await this.persist({ shutdown_state: "cleanup_failed", active_tree: true, cleanup_error: errorPayload(cleanup) }); }
214
- }).finally(() => { if (beforeInit) this.server?.close(); });
215
- });
204
+ // A dead leader is not yet a finalized tree: stop must also join the
205
+ // pending drain, process-group check and final persistence/lease writes.
206
+ this.providerFinalization = new Promise((resolve, reject) => {
207
+ this.child.on("close", (code, signal) => {
208
+ void this.draining.then(async () => {
209
+ const interrupted = Boolean(this.active); this.exited = true; const beforeInit = !this.init;
210
+ const error = this.startupError ?? new DaemonError(beforeInit ? "agy_init_missing" : "agy_terminal_result_missing", `Antigravity exited ${beforeInit ? "before init" : "before result"} (${code ?? signal})`);
211
+ if (beforeInit) readyReject(error);
212
+ if (this.active) { this.active.reject(error); this.active = null; }
213
+ const patch = beforeInit ? { shutdown_state: "failed", startup_error: errorPayload(error), provider_exit: { code, signal }, active_tree: false } : { provider_exit: { code, signal }, active_tree: interrupted };
214
+ await providerRegistered;
215
+ try {
216
+ await stopProcessGroup(providerChild);
217
+ await this.persist(patch);
218
+ await finishDaemonProcess(this.config, providerProcess, beforeInit || interrupted ? "failed" : "stopped", beforeInit || interrupted ? "provider_interrupted" : "provider_stopped");
219
+ } catch (cleanup) { await this.persist({ shutdown_state: "cleanup_failed", active_tree: true, cleanup_error: errorPayload(cleanup) }); throw cleanup; }
220
+ finally { if (beforeInit) this.server?.close(); }
221
+ }).then(resolve, reject);
222
+ });
216
223
  });
224
+ void this.providerFinalization.catch(() => {});
217
225
  try { await confirmDaemonProcess(this.config, providerProcess, this.child); await this.persist({ provider_process: providerProcess, provider_pid: this.child.pid }); }
218
226
  catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
219
227
  finally { allowProviderFinalization(); }
220
- return await Promise.race([this.readyPromise, new Promise((_, reject) => setTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000))]);
228
+ let readyTimer;
229
+ try { return await Promise.race([this.readyPromise, new Promise((_, reject) => { readyTimer = setTimeout(() => reject(new DaemonError("agy_init_timeout", "Antigravity init timed out", true)), 30_000); })]); }
230
+ finally { clearTimeout(readyTimer); }
221
231
  }
222
232
  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; }
223
233
  // Record an early provider rejection before journalling: a short-lived
@@ -318,12 +328,13 @@ export class Runtime {
318
328
  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" });
319
329
  const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, turn_generation: this.turnGeneration, 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(completed ? { active_tree: false } : {}); 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 }); completed?.resolve(settled); return { event_id: eventId, daemon_id: this.config.daemonId, 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 };
320
330
  }
321
- async cancel() { cancelPendingDispatch(this); if (!this.child || this.exited) return { ...this.receipt(), settled: Boolean(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.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
331
+ 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.providerFinalization; await this.persist({ active_tree: false }); return { ...this.receipt(), cancelled: true, settled: Boolean(this.exited) };
322
332
  }
323
- 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(); }
333
+ 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.providerFinalization; }
324
334
  async abortProvider(reason) {
325
335
  const child = this.child, providerProcess = this.providerProcess;
326
336
  await stopProcessGroup(child);
337
+ await this.providerFinalization;
327
338
  await finishDaemonProcess(this.config, providerProcess, "failed", reason);
328
339
  }
329
340
  async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
@@ -87,7 +87,7 @@ export async function stopProcessGroup(child, graceMs = 1_000) {
87
87
  const target = process.platform === "win32" ? child.pid : -child.pid;
88
88
  const alive = () => { try { process.kill(target, 0); return true; } catch (error) { if (error.code === "ESRCH") return false; if (error.code === "EPERM") return true; throw error; } };
89
89
  if (!alive()) return;
90
- if (child.exitCode !== undefined && child.exitCode !== null) {
90
+ if ((child.exitCode !== undefined && child.exitCode !== null) || (child.signalCode !== undefined && child.signalCode !== null)) {
91
91
  // The leader can exit a few ticks before its helpers. Observe their normal
92
92
  // shutdown before failing ownership; never signal a leaderless group.
93
93
  const deadline = Date.now() + Math.max(1_000, graceMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.36",
3
+ "version": "0.9.0-beta.37",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {