@deksden-com/dd-flow-cli 0.9.0-beta.96 → 0.9.0-beta.98

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.96",
4
- "cli_commit": "cb6c669ff3f24115ac0a4d2c04c8877eb8683bc6",
5
- "built_at": "2026-09-22T12:33:21.451Z",
3
+ "cli_version": "0.9.0-beta.98",
4
+ "cli_commit": "a06d4c113d22f31d344bfdf5b740ce5371da8ae2",
5
+ "built_at": "2026-09-22T19:40:49.454Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.1",
8
8
  "commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
@@ -3,8 +3,8 @@ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, s
3
3
  import { cancelSession, createSession, doctor, inspectSession, promptFromFile, promptSession, startSession } from "../lib/dd-codex.mjs";
4
4
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-codex-daemon.mjs";
5
5
 
6
- function parse(argv) { const positional = []; const options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree"].includes(key)) { options[key] = true; continue; } const value = argv[++i]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
7
- function common(options) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
6
+ function parse(argv) { const positional = []; const options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; } const value = argv[++i]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
7
+ function common(options) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
8
8
  try {
9
9
  const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
10
10
  if (family === "doctor") result = await doctor(shared);
@@ -27,7 +27,7 @@ try {
27
27
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(config.stateDir, options["operation-id"]);
28
28
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: config.stateDir, cancelTree: options["cancel-tree"] });
29
29
  else if (family === "hook" && command === "handle") { const payload = JSON.parse(await input()); const eventId = randomUUID(); result = await callDaemon(config.stateDir, "hook.observe", { payload, eventId }, 25_000, eventId); }
30
- else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], ...(prompt ? { prompt } : {}) }); }
30
+ else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], ...(config.noFlow ? { noFlow: true } : {}), ...(prompt ? { prompt } : {}) }); }
31
31
  else throw new Error("usage: dd-droid doctor | daemon start|status|operation|stop | session create|resume|prompt|inspect|cancel");
32
32
  process.stdout.write(`${JSON.stringify(family === "hook" ? { ...(result.hookSpecificOutput ? { hookSpecificOutput: result.hookSpecificOutput } : {}), ...(typeof result.continue === "boolean" ? { continue: result.continue } : {}), ...(result.stopReason ? { stopReason: result.stopReason } : {}) } : { ok: true, ...result })}\n`);
33
33
  } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: errorRecord(error) })}\n`); process.exitCode = process.argv[2] === "hook" ? 2 : 1; }
@@ -9,7 +9,7 @@ function parse(argv) {
9
9
  for (let index = 0; index < argv.length; index += 1) {
10
10
  const token = argv[index];
11
11
  if (!token.startsWith("--")) { positional.push(token); continue; }
12
- const key = token.slice(2); if (["json", "cancel-tree"].includes(key)) { options[key] = true; continue; }
12
+ const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; }
13
13
  const value = argv[++index]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value;
14
14
  }
15
15
  return { positional, options };
@@ -29,7 +29,7 @@ function common(options) {
29
29
  childSessionId: options["child-session-id"],
30
30
  model: options.model, reasoning: options.reasoning, mode: options.mode,
31
31
  permission: options.permission ?? "deny", ddFlowBin: options["dd-flow-bin"],
32
- ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"],
32
+ ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true,
33
33
  env: options["dd-flow-home"] ? { DD_FLOW_HOME: options["dd-flow-home"] } : undefined,
34
34
  timeoutMs: seconds("timeout"),
35
35
  livenessTimeoutMs: seconds("liveness-timeout"),
@@ -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
  }
@@ -63,7 +63,7 @@ export async function serveDaemon(stateDir) {
63
63
  const params = request.params ?? {}, operation = request.operation;
64
64
  if (operation === "daemon.status") { let observationError; try { await runtime.refreshTopology(); } catch (error) { observationError = errorRecord(error); } return { observation_error: observationError ?? null, daemon_id: state.daemon_id, pid: process.pid, config: state.config, shutdown_state: state.shutdown_state, provider_ready: Boolean(runtime.child && !runtime.closed), active_operation: busy, active_tree: Boolean(runtime.active || runtime.workingState !== "idle" || runtime.topologyIncomplete || [...runtime.topology.values()].some(child => !["completed", "cancelled", "failed"].includes(child.status))), sessions: runtime.rootId ? [runtime.rootId] : [], provider_pid: runtime.closed ? null : runtime.child?.pid }; }
65
65
  if (operation === "hook.observe") return await runtime.observeHook(params.payload, params.eventId);
66
- if (operation === "session.inspect") return await runtime.inspect(params.sessionId);
66
+ if (operation === "session.inspect") return await runtime.inspect(params.sessionId, { ingestUsage: params.noFlow !== true });
67
67
  if (operation === "session.cancel") {
68
68
  runtime.requireIdentity(params.sessionId); const cleanup = await runtime.closeTree(true); let inspected;
69
69
  try { inspected = await runtime.inspect(params.sessionId, { ignoreActive: true }); }
@@ -372,13 +372,13 @@ export class DroidRuntime {
372
372
  }
373
373
  return [...this.topology.values()];
374
374
  }
375
- async snapshotUsage(id) {
375
+ async snapshotUsage(id, { ingest = true } = {}) {
376
376
  const native = await readDroidSession(this.paths.factory, id); if (!native) return { status: "unavailable", provider_session_id: id };
377
377
  this.journaledTools ??= new Map();
378
378
  journalToolObservations(this.config.journal, [...native.records.flatMap(record => droidToolObservations(record, id)), ...native.tool_calls.reasons.map(reason => ({ reason }))], this.journaledTools);
379
379
  const measured = native.usage;
380
380
  const payload = { provider_session_id: id, daemon_id: this.config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: native.complete && measured ? "complete" : "partial", usage: measured, tool_calls: native.tool_calls, source: { path: native.transcript_path, sha256: native.source_sha256 } };
381
- if (measured && this.config.ddFlowBin) await this.flow("usage", "ingest", payload);
381
+ if (ingest && measured && this.config.ddFlowBin) await this.flow("usage", "ingest", payload);
382
382
  return { status: measured ? "measured" : "unavailable", ...payload };
383
383
  }
384
384
  async flow(family, command, payload) {
@@ -390,9 +390,9 @@ export class DroidRuntime {
390
390
  child.on("error", error => { clearTimeout(timer); reject(error); }); child.on("close", code => { clearTimeout(timer); if (code !== 0) return reject(new DroidError("droid_flow_rejected", stderr || "dd-flow rejected Droid evidence")); try { resolve(JSON.parse(stdout)); } catch (error) { reject(error); } }); child.stdin.end(`${JSON.stringify(payload)}\n`);
391
391
  });
392
392
  }
393
- async inspect(id, { ignoreActive = false } = {}) {
393
+ async inspect(id, { ignoreActive = false, ingestUsage = true } = {}) {
394
394
  this.requireIdentity(id); await this.captureProcesses(); const children = await this.refreshTopology(); const source = await readDroidSession(this.paths.factory, id);
395
- const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId));
395
+ const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId, { ingest: ingestUsage }));
396
396
  const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && children.every(child => terminal(child.status));
397
397
  return { harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null };
398
398
  }
@@ -82,7 +82,7 @@ async function inspect(bridge, sessionId, options) {
82
82
  }
83
83
 
84
84
  async function forwardUsage(options, providerSessionId, usage, toolCalls) {
85
- if (!options.ddFlowBin || !usage || usage.unavailable) return;
85
+ if (options.noFlow || !options.ddFlowBin || !usage || usage.unavailable) return;
86
86
  const measured = usage.usage && typeof usage.usage === "object" ? usage.usage : usage;
87
87
  const input = JSON.stringify({ provider_session_id: providerSessionId, daemon_id: options.daemonId ?? null, observed_at: new Date().toISOString(), usage: measured, tool_calls: toolCalls, usage_scope: usageScope(options, providerSessionId), completeness: measured.usageIsIncomplete ? "partial" : "complete" });
88
88
  const target = executable(options.ddFlowBin, ["grok", "usage", "ingest", "--project-root", absolute(options.projectRoot, "--project-root"), "--json"]);
@@ -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
  }
@@ -743,9 +743,10 @@ export function handleAgyEvent(context, input) {
743
743
  throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
744
744
  const rawInput = objectRecord(event.input);
745
745
  const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
746
- if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command))
747
- && (!Number.isInteger(event.execution_num) || !Number.isInteger(event.step_index))) {
748
- throw new AppError("agy_native_call_identity_missing", "Participating Antigravity lifecycle calls require native execution and step identity", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
746
+ const invalidStep = !Number.isSafeInteger(event.step_index) || Number(event.step_index) < 0;
747
+ const invalidExecution = event.execution_num !== undefined && (!Number.isSafeInteger(event.execution_num) || Number(event.execution_num) < 0);
748
+ if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command)) && (invalidStep || invalidExecution)) {
749
+ throw new AppError("agy_native_call_identity_missing", "Participating Antigravity lifecycle calls require a native step identity and a valid execution identity when provided", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
749
750
  }
750
751
  const translated = {
751
752
  schema_id: "dd-flow/opencode-tool-event@1", source_harness: "antigravity-cli", phase: event.phase, event_id: eventId,
@@ -29,7 +29,7 @@ export function assertNativeCancellationReceipt(value, sessionId) {
29
29
  /** Native observation is shared by RUN and non-Work EVAL owners. */
30
30
  export async function inspectControlledNativeSession(input) {
31
31
  input.assertCurrent();
32
- const receipt = await runHarnessAdapter({ executable: input.executable, args: ["session", "inspect", "--session-id", input.sessionId, "--state-dir", input.stateDir, "--json"], env: input.env, operationId: input.operationId, timeoutMs: 30_000 });
32
+ const receipt = await runHarnessAdapter({ executable: input.executable, args: ["session", "inspect", "--session-id", input.sessionId, "--state-dir", input.stateDir, ...(input.settlementObservation ? ["--no-flow"] : []), "--json"], env: input.env, operationId: input.operationId, timeoutMs: 30_000 });
33
33
  if (adapterSessionId(receipt) !== input.sessionId)
34
34
  throw new AppError("controller_operation_mismatch", "Session observation belongs to another target", 1);
35
35
  if (input.harness && receipt.harness !== undefined && receipt.harness !== input.harness)
@@ -474,7 +474,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
474
474
  : { session_id: session.id, state_dir: session.state_dir, harness, status: "pending", reason: "shutdown_session_unproven" };
475
475
  if (!interrupt) {
476
476
  try {
477
- const receipt = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent });
477
+ const receipt = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent, settlementObservation: true });
478
478
  return { session_id: session.id, state_dir: session.state_dir, harness: harness ?? (typeof receipt.harness === "string" ? receipt.harness : null), status: receipt.settled === true ? "completed" : "pending", receipt };
479
479
  }
480
480
  catch (error) {
@@ -511,7 +511,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
511
511
  return { operation_id: operationId, session_id: session.id, status: "outcome_unknown", error: cancellationError };
512
512
  }
513
513
  try {
514
- const observed = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent });
514
+ const observed = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent, settlementObservation: true });
515
515
  return { operation_id: operationId, session_id: session.id, status: observed.settled === true ? "completed" : "pending", receipt: observed, cancellation, reused: !inserted.changes, ...(cancellationError ? { cancellation_error: cancellationError } : {}) };
516
516
  }
517
517
  catch (error) {
@@ -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.96",
3
+ "version": "0.9.0-beta.98",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {