@deksden-com/dd-flow-cli 0.9.0-beta.97 → 0.9.0-beta.99

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.97",
4
- "cli_commit": "4d478c7dd9549a5b81ed5cd77d4f04befebf6f1c",
5
- "built_at": "2026-09-22T15:35:15.503Z",
3
+ "cli_version": "0.9.0-beta.99",
4
+ "cli_commit": "a68a8ac243ceef7806094964106ebe38c5d384fd",
5
+ "built_at": "2026-09-22T21:30:25.559Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.1",
8
8
  "commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
@@ -276,7 +276,7 @@ export class Runtime {
276
276
  // child can close while the asynchronous write is still pending.
277
277
  if (!this.init && event.event === "result" && event.result?.status === "ERROR") this.startupError = new DaemonError("agy_provider_rejected", String(event.result.error ?? "Antigravity rejected the Session before initialization"), false, { provider_result: event.result });
278
278
  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 ?? {}); } } }
279
- observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(sessionId) ?? {}; if (Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1)) this.childActivity(sessionId); if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) }); this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id)); for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) { const prior = this.descendants.get(child.conversation_id); this.descendants.set(child.conversation_id, { ...prior, provider_session_id: child.conversation_id, parent_provider_session_id: this.init?.conversation_id ?? 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" }); } }
279
+ observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(sessionId) ?? {}; if (Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1)) this.childActivity(sessionId); if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) }); this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id)); for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) { const prior = this.descendants.get(child.conversation_id); this.descendants.set(child.conversation_id, { ...prior, provider_session_id: child.conversation_id, parent_provider_session_id: sessionId ?? null, role: child.role ?? null, subagent_type: child.type_name ?? null, log_uri: child.log_uri ?? null, workspace_uris: child.workspace_uris ?? [], status: prior?.status ?? "unknown" }); } }
280
280
  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 }; }
281
281
  async finishTurn(result) {
282
282
  if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
@@ -341,7 +341,7 @@ export class Runtime {
341
341
  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) };
342
342
  }
343
343
  async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
344
- async prompt(text, assertDispatch = captureDispatchGuard(this)) {
344
+ async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null) {
345
345
  if (!text) throw new DaemonError("prompt_required", "prompt is required");
346
346
  await this.start(); await this.draining;
347
347
  if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running");
@@ -350,14 +350,14 @@ export class Runtime {
350
350
  const reply = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject; });
351
351
  // Cancellation can reject while admission persistence is still pending.
352
352
  void reply.catch(() => {});
353
- const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, previousError: this.lastResult?.error ?? null };
353
+ const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, previousError: this.lastResult?.error ?? null };
354
354
  // Reserve before persistence so different operation IDs cannot overlap.
355
355
  this.active = current;
356
356
  const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
357
357
  this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
358
358
  this.lastProviderEvent = null;
359
359
  try {
360
- await this.persist({ active_tree: true });
360
+ await this.persist({ active_tree: true, last_hook_rejection: null });
361
361
  assertDispatch();
362
362
  if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
363
363
  this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
@@ -378,8 +378,16 @@ export class Runtime {
378
378
  while (!this.descendants.has(conversationId) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 10));
379
379
  const child = this.descendants.get(conversationId);
380
380
  if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
381
+ if (child.parent_provider_session_id !== this.init?.conversation_id) throw new DaemonError("agy_child_parent_unqualified", "Nested Antigravity worker is not a direct child of the controlled Session", false, { conversation_id: conversationId, parent_conversation_id: child.parent_provider_session_id });
381
382
  return child;
382
383
  }
384
+ async rejectHook(error, event, payload) {
385
+ const current = this.active;
386
+ const rejection = { code: error.code ?? "agy_hook_rejected", message: error.message ?? String(error), 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 };
387
+ try { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }
388
+ catch (storageError) { if (current && this.active === current) { this.active = null; current.reject(storageError); } throw storageError; }
389
+ if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
390
+ }
383
391
  async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}), ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const parent = child?.parent_provider_session_id ?? null;
384
392
  const observed = payload.model ? observedProfile(payload) : {};
385
393
  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" });
@@ -427,11 +435,14 @@ async function dispatch(runtime, request) {
427
435
  if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
428
436
  if (request.operation === "daemon.status") return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, 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() };
429
437
  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(); }
430
- 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); }
438
+ 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); }
431
439
  if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
432
440
  if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
433
441
  if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
434
- if (request.operation === "hook.observe") return await runtime.observeHook(params.event, params.payload ?? {});
442
+ if (request.operation === "hook.observe") {
443
+ try { return await runtime.observeHook(params.event, params.payload ?? {}); }
444
+ catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}); throw error; }
445
+ }
435
446
  if (request.operation === "daemon.stop") { await runtime.close(params.cancelTree === true); await runtime.persist({ shutdown_state: "clean", active_tree: false }); await runtime.finishResource(); setImmediate(() => runtime.server?.close()); return { stopped: true, clean: true, settled: true, daemon_id: runtime.config.daemonId }; }
436
447
  throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
437
448
  }
@@ -163,19 +163,19 @@ export async function startDaemon(options) {
163
163
  await prepareRuntimeOwner(config, paths.dir);
164
164
  if (!config.model || !config.reasoning) throw new DaemonError("profile_required", "--model and --reasoning are required");
165
165
  await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
166
- 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 (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
166
+ try { const status = await callDaemon(paths.dir, "daemon.ready", {}, 5000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); if (!status.ready) throw new DaemonError("daemon_start_in_progress", "an existing daemon is still starting", true); return { ...status, already_running: true }; } catch (error) { if (error.code !== "daemon_not_running") throw error; }
167
167
  const previous = await readState(paths.dir); if (previous?.shutdown_state === "clean") await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config); if (previous?.shutdown_state === "running" && previous.active_tree) throw new DaemonError("invalid_harness_crash", "previous daemon died with an active or unproven Session tree", false, { daemon_id: previous.daemon_id });
168
168
  await removeSocket(paths.socket, paths.dir); await mkdir(paths.home, { recursive: true, mode: 0o700 }); const importedSession = await materializeSessionArchive(paths, config.sessionArchive); const copiedAuth = await copyAuth(paths, authSource); await writeIsolatedConfig(paths); if (!noFlow) await writeHook(paths, config, absolute(options.entryPath, "dd-grok entry")); const versions = await doctor({ bin: config.bin }); if (importedSession && importedSession.grok_version !== versions.versions.grok) throw new DaemonError("session_archive_version_mismatch", "Grok Build session archive was created by a different CLI version", false, { archive_version: importedSession.grok_version, current_version: versions.versions.grok }); const configIsolation = await inspectIsolatedConfig(config);
169
169
  const resourceProcess = await registerDaemonProcess(config, { kind: "grok-daemon", owner: `grok:${path.basename(paths.dir)}`, operation: `grok-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
170
170
  const state = { schema_id: STATE_SCHEMA, daemon_id: randomUUID(), pid: null, socket: paths.socket, started_at: new Date().toISOString(), shutdown_state: "starting", active_tree: false, recovery_status: previous?.shutdown_state === "running" ? "recovered_idle" : "clean_start", auth_status: copiedAuth ? "copied" : "absent", ...(importedSession ? { imported_session: importedSession } : {}), versions: versions.versions, config_isolation: configIsolation, config, sessions: importedSession ? [{ provider_session_id: importedSession.provider_session_id, adapter_session_id: importedSession.provider_session_id, parent_provider_session_id: null, root_provider_session_id: importedSession.provider_session_id, cwd: importedSession.source_cwd }] : (previous?.sessions ?? []), resource_process: resourceProcess };
171
171
  await writeState(paths.state, state); const log = await open(paths.log, "a", 0o600); const child = spawn(process.execPath, [absolute(options.entryPath, "dd-grok entry"), "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: isolatedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
172
- try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 15_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); } catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; } } throw new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
172
+ try { await confirmDaemonProcess(config, resourceProcess, child); const deadline = Date.now() + 30_000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 100)); try { const status = await callDaemon(paths.dir, "daemon.ready", {}, 5000); if (status.ready) return status; } catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; } } throw new DaemonError("daemon_start_failed", "daemon did not become ready", true); }
173
173
  catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
174
174
  }
175
175
  export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); }
176
176
 
177
177
  export class Runtime {
178
- constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map(); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.persisting = Promise.resolve(); }
178
+ constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map(); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
179
179
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(() => writeState(this.paths.state, snapshot)); if (snapshot.resource_process) this.persisting = this.persisting.then(() => heartbeatDaemonProcess(snapshot.config, snapshot.resource_process).catch(() => {})); return this.persisting; }
180
180
  options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
181
181
  track(result) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant(child, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
@@ -261,7 +261,8 @@ export class Runtime {
261
261
  }
262
262
  async dispatch(operation, params) {
263
263
  const assertDispatch = captureDispatchGuard(this);
264
- if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { observation_error: observationError ?? null, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config }; }
264
+ if (operation === "daemon.ready") return { ready: this.ready, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config };
265
+ if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? null }; }
265
266
  if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }; }
266
267
  if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; });
267
268
  if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId); }
@@ -294,6 +295,6 @@ export async function serveDaemon(stateDir) {
294
295
  const rootProviderSessionId = session?.root_provider_session_id ?? sessionId;
295
296
  await observeAcpToolCall(state.config, { daemonId: state.daemon_id, rootProviderSessionId, parentProviderSessionId: session?.parent_provider_session_id ?? null, observedProfile: bridge.modelProfiles?.get(sessionId) ?? {} }, message);
296
297
  } }); const initialized = await bridge.start(); runtime = new Runtime(paths, state, bridge, initialized);
297
- await removeSocket(paths.socket, paths.dir); const server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const newline = buffer.indexOf("\n"); if (newline < 0) return; const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request"); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => void runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await runtime.persist({ shutdown_state: "running", active_tree: false }); return await new Promise(() => {});
298
+ await removeSocket(paths.socket, paths.dir); const server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const newline = buffer.indexOf("\n"); if (newline < 0) return; const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request"); const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}), () => runtime.dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result?._shutdown) setImmediate(() => void runtime.shutdown().then(() => process.exit(0)).catch(error => runtime.persist({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }))); } catch (error) { connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); }); runtime.server = server; await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await runtime.persist({ shutdown_state: "running", active_tree: false }); runtime.ready = true; return await new Promise(() => {});
298
299
  }
299
300
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -54,8 +54,8 @@ export const lifecycleRetryInstruction = "If the CLI rejects the call with effec
54
54
  export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
55
55
  if (!workId || !startCommand) throw new Error("ready fan-out Work lacks its exact start command");
56
56
  const context = launchPolicy === "fresh_agent_required"
57
- ? "This Work requires empty, non-inherited conversation context; the coordinator must select the native fresh-child mode before launching it."
58
- : "Use inherited context only through the harness's declared native mechanism.";
57
+ ? "You are the already-selected fresh leaf worker for this Work. Execute its lifecycle start yourself; do not delegate it to another child."
58
+ : "You are the already-selected worker. Execute this Work yourself; do not delegate it to another child.";
59
59
  return [
60
60
  `Complete one already-declared Work: ${workId}.`,
61
61
  context,
@@ -31,7 +31,9 @@ export function controllerStageEntryPrompt(stage, command) {
31
31
  }
32
32
  /** The shared controller owns this former eval-only Work-graph decision. */
33
33
  export async function nextControllerFanout(context, input) {
34
- const children = input.children.filter(child => child.status !== "settled_by_root");
34
+ // Tree settlement is not a Work result. Keep these children visible to the
35
+ // association check, especially when a hook rejected work start before CLI.
36
+ const children = input.children;
35
37
  const observations = { harness_id: input.harness, parent_session_id: input.sessionId, children };
36
38
  if (children.length) {
37
39
  const observed = observeVnextNativeChildren(context, { ...input, observations });
@@ -75,12 +75,13 @@ export function restoreEvalBootstrapSnapshot(_context, input, prepared = prepare
75
75
  export function createEvalRunSnapshot(context, input) {
76
76
  const { output } = prepareEvalRunSnapshotCreation(context, input);
77
77
  const temporary = `${output}.partial-${crypto.randomUUID()}`;
78
+ let sourceVersion = null;
78
79
  try {
79
80
  input.boundary?.verify();
80
- const sourceVersion = input.boundary ? snapshotSourceVersion(context, input) : null;
81
+ sourceVersion = input.boundary ? snapshotSourceVersion(context, input) : null;
81
82
  const result = captureEvalRunSnapshot(context, { ...input, output: temporary });
82
83
  if (input.boundary && sourceVersion) {
83
- Object.assign(result, { consistency: "controller_boundary_writer_barrier@1", boundary_capture: { controller_id: input.boundary.controllerId, operation_id: input.boundary.operationId, boundary_key: input.boundary.boundaryKey, source_sha256: sourceVersion, observation: input.boundary.observation } });
84
+ Object.assign(result, { consistency: "controller_boundary_writer_barrier@1", boundary_capture: { controller_id: input.boundary.controllerId, operation_id: input.boundary.operationId, boundary_key: input.boundary.boundaryKey, source_sha256: sourceVersion.sha256, observation: input.boundary.observation } });
84
85
  writeJson(path.join(temporary, "snapshot.json"), Object.fromEntries(Object.entries(result).filter(([key]) => key !== "ok" && key !== "snapshot")));
85
86
  }
86
87
  if (input.recoveryId) {
@@ -93,8 +94,20 @@ export function createEvalRunSnapshot(context, input) {
93
94
  throw new AppError("snapshot_exists", "Snapshot output already exists", 1, { output });
94
95
  syncSnapshotTree(temporary);
95
96
  if (input.boundary) {
96
- if (sourceVersion !== snapshotSourceVersion(context, input))
97
- throw new AppError("snapshot_source_changed", "Snapshot source files or Git state changed during boundary capture", 1);
97
+ if (sourceVersion) {
98
+ let after;
99
+ try {
100
+ after = snapshotSourceVersion(context, input, sourceVersion.databases);
101
+ }
102
+ catch (error) {
103
+ if (error.code !== "ENOENT")
104
+ throw error;
105
+ const missing = error.path;
106
+ throw new AppError("snapshot_source_changed", "Snapshot source disappeared during boundary capture", 1, { components: [missing && path.isAbsolute(missing) && isInside(context.ddFlowHome, missing) ? `runtime/${path.relative(context.ddFlowHome, missing)}` : "source_path_disappeared"] });
107
+ }
108
+ if (sourceVersion.sha256 !== after.sha256)
109
+ throw new AppError("snapshot_source_changed", "Snapshot source files or Git state changed during boundary capture", 1, { components: snapshotSourceChanges(sourceVersion.entries, after.entries), changes: snapshotSourceChangeProofs(sourceVersion.entries, after.entries) });
110
+ }
98
111
  input.boundary.verify();
99
112
  }
100
113
  fs.renameSync(temporary, output);
@@ -105,6 +118,10 @@ export function createEvalRunSnapshot(context, input) {
105
118
  fs.rmSync(temporary, { recursive: true, force: true });
106
119
  throw error;
107
120
  }
121
+ finally {
122
+ for (const db of sourceVersion?.databases.values() ?? [])
123
+ db.close();
124
+ }
108
125
  }
109
126
  export function prepareEvalRunSnapshotCreation(context, input) {
110
127
  if (Number(Boolean(input.candidate)) + Number(Boolean(input.incomplete)) + Number(input.stageEntry !== undefined) + Number(input.recoveryId !== undefined) !== 1)
@@ -801,23 +818,101 @@ function snapshotPayloadAllowed(source) {
801
818
  function runtimePayloadAllowed(source) {
802
819
  return snapshotPayloadAllowed(source) && !["auth.json", "auth.bk", "credentials.json", ".credentials.json", "credentials", ".credentials", "id_rsa", "id_ed25519", "engines", "checkouts"].includes(path.basename(source));
803
820
  }
804
- /** Productive writers are fenced separately. Recheck copied bytes and Git
805
- * state so an uncoordinated file writer cannot publish a mixed boundary. */
806
- function snapshotSourceVersion(context, input) {
821
+ function sqliteFile(file) {
822
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile())
823
+ return false;
824
+ const descriptor = fs.openSync(file, "r");
825
+ try {
826
+ const header = Buffer.alloc(16);
827
+ return fs.readSync(descriptor, header, 0, 16, 0) === 16 && header.toString() === "SQLite format 3\0";
828
+ }
829
+ finally {
830
+ fs.closeSync(descriptor);
831
+ }
832
+ }
833
+ /** A WAL is copied through its SQLite read snapshot, not as raw bytes. */
834
+ function runtimeSnapshotKind(file, excluded) {
835
+ if (!runtimePayloadAllowed(file) || excluded.includes(file))
836
+ return "excluded";
837
+ if (fs.lstatSync(file).isFile()) {
838
+ if (/-(?:wal|shm|journal)$/.test(file) && sqliteFile(file.replace(/-(?:wal|shm|journal)$/, "")))
839
+ return "sidecar";
840
+ if (sqliteFile(file))
841
+ return "sqlite";
842
+ }
843
+ return "ordinary";
844
+ }
845
+ /** Productive writers are fenced separately. Compare exactly the copied
846
+ * source set; SQLite serialization includes committed WAL pages without
847
+ * mistaking a physical checkpoint for new logical content. */
848
+ function snapshotSourceVersion(context, input, retainedDatabases) {
849
+ const databases = retainedDatabases ?? new Map();
807
850
  const projectRoot = resolveProjectRoot(input.projectRoot);
808
851
  const project = requireProjectByRoot(context, projectRoot);
809
852
  const run = context.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
810
853
  if (!run)
811
854
  throw new AppError("not_found", "Snapshot RUN is not registered", 1);
812
- const resources = path.join(context.ddFlowHome, "runtime.sqlite");
813
- // Resource heartbeats are not productive writers. Their semantic identities
814
- // are rechecked by the owner; each resource DB is copied consistently.
815
- const runtimeFilter = (file) => runtimePayloadAllowed(file) && !file.endsWith("-shm") && ![resources, `${resources}-wal`, `${resources}-journal`].includes(file);
816
- const roots = [...new Set([projectRoot, run.workspace_root])];
817
- return crypto.createHash("sha256").update(JSON.stringify([
818
- treeHash(context.ddFlowHome, runtimeFilter), treeModesHash(context.ddFlowHome, runtimeFilter),
819
- ...roots.map(root => [treeHash(root, true), treeModesHash(root, true), git(root, ["symbolic-ref", "--quiet", "HEAD"]), gitBytes(root, ["show-ref", "--head"]).toString("base64"), indexEntries(root).toString("base64")])
820
- ])).digest("hex");
855
+ const entries = new Map();
856
+ const excluded = codexHomeSnapshotExclusions(context, project.id, input.runId);
857
+ const visit = (directory) => {
858
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
859
+ const file = path.join(directory, entry.name);
860
+ const kind = runtimeSnapshotKind(file, excluded);
861
+ if (kind === "excluded" || kind === "sidecar")
862
+ continue;
863
+ const key = `runtime/${path.relative(context.ddFlowHome, file)}`;
864
+ const stat = fs.lstatSync(file);
865
+ if (stat.isDirectory()) {
866
+ entries.set(key, "directory");
867
+ visit(file);
868
+ }
869
+ else if (stat.isSymbolicLink())
870
+ entries.set(key, `link:${fs.readlinkSync(file)}`);
871
+ else if (kind === "sqlite") {
872
+ // Resource heartbeats are copied consistently but are not productive
873
+ // RUN writers; owner identity is fenced by boundary.verify().
874
+ if (file === path.join(context.ddFlowHome, "runtime.sqlite")) {
875
+ entries.set(key, `resource-sqlite:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
876
+ continue;
877
+ }
878
+ let db = databases.get(file);
879
+ if (!db) {
880
+ db = new DatabaseSync(file, { readOnly: true });
881
+ databases.set(file, db);
882
+ }
883
+ entries.set(key, `sqlite:${crypto.createHash("sha256").update(db.serialize()).digest("hex")}:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
884
+ }
885
+ else if (stat.isFile())
886
+ entries.set(key, `file:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}:${stat.mode & 0o111}`);
887
+ }
888
+ };
889
+ try {
890
+ visit(context.ddFlowHome);
891
+ for (const root of new Set([projectRoot, run.workspace_root]))
892
+ entries.set(`source/${root}`, JSON.stringify([
893
+ treeHash(root, true), treeModesHash(root, true), git(root, ["symbolic-ref", "--quiet", "HEAD"]), gitBytes(root, ["show-ref", "--head"]).toString("base64"), indexEntries(root).toString("base64")
894
+ ]));
895
+ return { entries, databases, sha256: crypto.createHash("sha256").update(JSON.stringify([...entries].sort(([a], [b]) => a.localeCompare(b)))).digest("hex") };
896
+ }
897
+ catch (error) {
898
+ if (!retainedDatabases)
899
+ for (const db of databases.values())
900
+ try {
901
+ db.close();
902
+ }
903
+ catch { /* Preserve the original source error. */ }
904
+ throw error;
905
+ }
906
+ }
907
+ function snapshotSourceChanges(before, after) {
908
+ return [...new Set([...before.keys(), ...after.keys()])].filter(key => before.get(key) !== after.get(key)).sort().slice(0, 8);
909
+ }
910
+ function snapshotSourceChangeProofs(before, after) {
911
+ return snapshotSourceChanges(before, after).map(component => ({
912
+ component,
913
+ before: before.has(component) ? crypto.createHash("sha256").update(before.get(component)).digest("hex") : null,
914
+ after: after.has(component) ? crypto.createHash("sha256").update(after.get(component)).digest("hex") : null
915
+ }));
821
916
  }
822
917
  /** Managed Codex homes share host configuration, not owned snapshot payload. */
823
918
  function codexHomeSnapshotExclusions(context, projectId, runId) {
@@ -856,23 +951,12 @@ function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
856
951
  if (isInside(root, physicalPath(destination)))
857
952
  throw new AppError("snapshot_output_inside_runtime", "Recovery output must be outside its source artifact tree", 1);
858
953
  const databases = [];
859
- const sqlite = (file) => {
860
- if (!fs.existsSync(file) || !fs.statSync(file).isFile())
861
- return false;
862
- const descriptor = fs.openSync(file, "r");
863
- try {
864
- const header = Buffer.alloc(16);
865
- return fs.readSync(descriptor, header, 0, 16, 0) === 16 && header.toString() === "SQLite format 3\0";
866
- }
867
- finally {
868
- fs.closeSync(descriptor);
869
- }
870
- };
871
954
  fs.mkdirSync(path.dirname(destination), { recursive: true });
872
955
  fs.cpSync(source, destination, {
873
956
  recursive: true, verbatimSymlinks: true,
874
957
  filter: file => {
875
- if (!runtimePayloadAllowed(file) || excluded.includes(file))
958
+ const kind = runtimeSnapshotKind(file, excluded);
959
+ if (kind === "excluded" || kind === "sidecar")
876
960
  return false;
877
961
  const stat = fs.lstatSync(file);
878
962
  if (stat.isSymbolicLink()) {
@@ -883,9 +967,7 @@ function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
883
967
  if (!stat.isDirectory() && !stat.isFile())
884
968
  return false;
885
969
  if (stat.isFile()) {
886
- if (/-(?:wal|shm|journal)$/.test(file) && sqlite(file.replace(/-(?:wal|shm|journal)$/, "")))
887
- return false;
888
- if (sqlite(file)) {
970
+ if (kind === "sqlite") {
889
971
  databases.push(file);
890
972
  return false;
891
973
  }
@@ -80,10 +80,11 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
80
80
  }
81
81
  else {
82
82
  let lastPending = "";
83
+ let lastCaptureMismatch = null;
83
84
  for (;;) {
84
85
  if (observationBudget.exhausted()) {
85
- context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(COALESCE(receipt_json, '{}'), '$.recovery_observation', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(observationBudget.state()), context.now(), operationId]);
86
- throw new AppError("recovery_observation_budget_exhausted", "Boundary capture has no stable settlement within the active observation budget", 1, { recovery_observation: observationBudget.state() });
86
+ context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(json_set(COALESCE(receipt_json, '{}'), '$.recovery_observation', json(?)), '$.last_capture_mismatch', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(observationBudget.state()), JSON.stringify(lastCaptureMismatch), context.now(), operationId]);
87
+ throw new AppError("recovery_observation_budget_exhausted", "Boundary capture has no stable settlement within the active observation budget", 1, { recovery_observation: observationBudget.state(), last_capture_mismatch: lastCaptureMismatch });
87
88
  }
88
89
  assertCurrent();
89
90
  let verify = () => { throw new AppError("controller_boundary_unsettled", "Boundary has no stable writer observation", 1); };
@@ -99,14 +100,15 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
99
100
  throw error;
100
101
  observation.settled = false;
101
102
  observation.pending_reasons = [error.code];
103
+ lastCaptureMismatch = { code: error.code, components: error.details?.components ?? [], changes: error.details?.changes ?? [] };
102
104
  }
103
105
  }
104
106
  observationBudget.exhausted();
105
- const published = { ...observation, recovery_observation: observationBudget.state() };
107
+ const published = { ...observation, recovery_observation: observationBudget.state(), last_capture_mismatch: lastCaptureMismatch };
106
108
  const pending = digest(observation.pending_reasons);
107
109
  context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(published), context.now(), operationId]);
108
110
  if (pending !== lastPending) {
109
- appendEvent(context, row.controller_id, "boundary_capture_pending", { operation_id: operationId, pending_reasons: observation.pending_reasons });
111
+ appendEvent(context, row.controller_id, "boundary_capture_pending", { operation_id: operationId, pending_reasons: observation.pending_reasons, last_capture_mismatch: lastCaptureMismatch });
110
112
  lastPending = pending;
111
113
  }
112
114
  await delay(observationBudget.nextDelay());
@@ -158,6 +158,10 @@ export async function reconcileVnextFanout(context, input) {
158
158
  issues.push({ code: "child_descendants_unsettled", work_id: link.work_id });
159
159
  continue;
160
160
  }
161
+ if (child.status === "settled_by_root") {
162
+ issues.push({ code: "child_execution_outcome_unconfirmed", work_id: link.work_id, session_id: child.session_id });
163
+ continue;
164
+ }
161
165
  if (child.status === "completed") {
162
166
  issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
163
167
  continue;
@@ -178,7 +182,7 @@ export async function reconcileVnextFanout(context, input) {
178
182
  export function prepareNativeObservations(observations) {
179
183
  const value = observations;
180
184
  if (!value || typeof value.harness_id !== "string" || !value.harness_id.trim() || typeof value.parent_session_id !== "string" || !value.parent_session_id.trim() || !Array.isArray(value.children)
181
- || value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(child.status)))
185
+ || value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled", "settled_by_root"].includes(child.status)))
182
186
  throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
183
187
  if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
184
188
  throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
@@ -880,6 +880,8 @@ function bindSession(context, work, run, identity, now) {
880
880
  });
881
881
  if (work.parent_work_id && !parentSession)
882
882
  throw new AppError("parent_session_required", "Child Work requires a confirmed parent Work/Session link", 1, { work_id: work.work_id, parent_work_id: work.parent_work_id });
883
+ if (work.launch_policy === "fresh_agent_required" && inferredParentSession && identity.parentSessionId && identity.parentSessionId !== inferredParentSession)
884
+ throw new AppError("native_work_parent_mismatch", "Fresh native Work must be started by a direct child of its assigned parent Session", 1, { work_id: work.work_id, effect: "no_effect", ...identityDetails(), observed_provider_parent_session_key: identity.parentSessionId });
883
885
  if (work.launch_policy === "fresh_agent_required" && (identity.sessionId === parentSession || context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? LIMIT 1", [work.project_id, work.run_id, identity.sessionId])))
884
886
  throw new AppError("fresh_session_required", "This Work requires a fresh Session in this RUN", 1, { work_id: work.work_id, effect: "no_effect", recoverable: true, handoff_required: true, ...identityDetails() });
885
887
  const existing = context.db.get("SELECT session_id, parent_session_id, provider_parent_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [work.project_id, identity.sessionId]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.97",
3
+ "version": "0.9.0-beta.99",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {