@deksden-com/dd-flow-cli 0.9.0-beta.36 → 0.9.0-beta.38
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,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.38
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 44e3c80: Allow control operations to recheck failed provider cleanup after a leaderless process group exits naturally. Coalesce concurrent finalization retries, preserve the original exit and process lease, and keep live or unproven groups blocked without sending them signals.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.37
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 0.9.0-beta.36
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1025,6 +1037,7 @@ record` command with the bounded 15-probe contract.
|
|
|
1025
1037
|
- 16f68a3: Add router-native engine snapshot commands and routed project-command dispatch.
|
|
1026
1038
|
|
|
1027
1039
|
Also include the runtime/dashboard compatibility wave needed by the current Memory Bank canon:
|
|
1040
|
+
|
|
1028
1041
|
- target-based dashboard `open`/`refresh` commands and related-command help;
|
|
1029
1042
|
- persisted FIFO lane waiters and merge wait-acquire specialization;
|
|
1030
1043
|
- `migration plan/report/verify` evidence helpers for controlled `mb-upgrade` runtime migrations;
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.38",
|
|
4
|
+
"cli_commit": "e1d723e620ee5aeec961c2bee1de1e72fcfb71a9",
|
|
5
|
+
"built_at": "2026-09-08T03:57:03.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,37 @@ 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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
this.retryProviderFinalization = async (graceMs = 1_000) => {
|
|
216
|
+
try {
|
|
217
|
+
await stopProcessGroup(providerChild, graceMs);
|
|
218
|
+
await this.persist(patch);
|
|
219
|
+
await finishDaemonProcess(this.config, providerProcess, beforeInit || interrupted ? "failed" : "stopped", beforeInit || interrupted ? "provider_interrupted" : "provider_stopped");
|
|
220
|
+
this.retryProviderFinalization = null;
|
|
221
|
+
} catch (cleanup) { await this.persist({ shutdown_state: "cleanup_failed", active_tree: true, cleanup_error: errorPayload(cleanup) }); throw cleanup; }
|
|
222
|
+
finally { if (beforeInit) this.server?.close(); }
|
|
223
|
+
};
|
|
224
|
+
await this.retryProviderFinalization();
|
|
225
|
+
}).then(resolve, reject);
|
|
226
|
+
});
|
|
216
227
|
});
|
|
228
|
+
void this.providerFinalization.catch(() => {});
|
|
217
229
|
try { await confirmDaemonProcess(this.config, providerProcess, this.child); await this.persist({ provider_process: providerProcess, provider_pid: this.child.pid }); }
|
|
218
230
|
catch (error) { await cleanupFailedStart(this.config, providerProcess, this.child, error); throw error; }
|
|
219
231
|
finally { allowProviderFinalization(); }
|
|
220
|
-
|
|
232
|
+
let readyTimer;
|
|
233
|
+
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); })]); }
|
|
234
|
+
finally { clearTimeout(readyTimer); }
|
|
221
235
|
}
|
|
222
236
|
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
237
|
// Record an early provider rejection before journalling: a short-lived
|
|
@@ -318,12 +332,25 @@ export class Runtime {
|
|
|
318
332
|
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
333
|
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
334
|
}
|
|
321
|
-
async
|
|
335
|
+
async finalizeProviderForControl() {
|
|
336
|
+
const pending = this.providerFinalization;
|
|
337
|
+
try { await pending; }
|
|
338
|
+
catch (error) {
|
|
339
|
+
// A leaderless group may have exited naturally since the last check.
|
|
340
|
+
// Only control may recheck it; startup never retries failed cleanup.
|
|
341
|
+
if (this.providerFinalization !== pending) return await this.providerFinalization;
|
|
342
|
+
if (error.code !== "process_group_ownership_unknown" || !this.retryProviderFinalization) throw error;
|
|
343
|
+
this.providerFinalization = this.retryProviderFinalization(5_000);
|
|
344
|
+
await this.providerFinalization;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
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) };
|
|
322
348
|
}
|
|
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(); }
|
|
349
|
+
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
350
|
async abortProvider(reason) {
|
|
325
351
|
const child = this.child, providerProcess = this.providerProcess;
|
|
326
352
|
await stopProcessGroup(child);
|
|
353
|
+
await this.finalizeProviderForControl();
|
|
327
354
|
await finishDaemonProcess(this.config, providerProcess, "failed", reason);
|
|
328
355
|
}
|
|
329
356
|
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);
|