@deksden-com/dd-flow-cli 0.9.0-beta.134 → 0.9.0-beta.136

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.
Files changed (161) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +33 -0
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +1 -0
  5. package/dist/cli/command-metadata.js +144 -0
  6. package/dist/cli/command-option-descriptions.js +186 -0
  7. package/dist/cli/completion-cli.js +70 -0
  8. package/dist/cli/completion.js +57 -0
  9. package/dist/cli/error-output.js +52 -0
  10. package/dist/cli/executor-onboarding.js +71 -0
  11. package/dist/cli/harnesses.js +84 -0
  12. package/dist/cli/models.js +39 -0
  13. package/dist/cli/operator-profiles.js +61 -0
  14. package/dist/cli/output-options.js +35 -0
  15. package/dist/cli/response-file.js +69 -0
  16. package/dist/cli/run-cli.js +5 -0
  17. package/dist/cli/setup.js +147 -0
  18. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  19. package/dist/harness-runtime/bin/dd-opencode.mjs +1 -1
  20. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +13 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +193 -39
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +295 -100
  23. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  24. package/dist/harness-runtime/lib/managed-daemon.mjs +1 -1
  25. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  26. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  27. package/dist/harness-runtime/lib/opencode-plugin.mjs +60 -0
  28. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  29. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  30. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  31. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  32. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  33. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  34. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  35. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  36. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  37. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  38. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  39. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  40. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  41. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  42. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  43. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  44. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  45. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  46. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  47. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  48. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  49. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  50. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  51. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  52. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  53. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  54. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  55. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  56. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  57. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  58. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  59. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  60. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  61. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  62. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  63. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  64. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  65. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  66. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  67. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  68. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  69. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  70. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  71. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  72. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  73. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  74. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  75. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  76. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  77. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  78. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  79. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  80. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  81. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  82. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  83. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  84. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  85. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  86. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  87. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  88. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  89. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  90. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  91. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  92. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  93. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  94. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  95. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  96. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  97. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  98. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  99. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  100. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  101. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  102. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  103. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  104. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  105. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  106. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  107. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  108. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  109. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  110. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  111. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  112. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  113. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  114. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  115. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  116. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  117. package/dist/services/cli-operation-classifier.js +4 -0
  118. package/dist/services/cli-upgrade-allowlist.js +27 -0
  119. package/dist/services/discovery/agy-discovery.js +2 -0
  120. package/dist/services/discovery/codex-discovery.js +2 -0
  121. package/dist/services/discovery/droid-discovery.js +2 -0
  122. package/dist/services/discovery/grok-discovery.js +2 -0
  123. package/dist/services/discovery/opencode-discovery.js +2 -0
  124. package/dist/services/discovery/static.js +31 -0
  125. package/dist/services/discovery/zcode-discovery.js +2 -0
  126. package/dist/services/engines.js +23 -25
  127. package/dist/services/eval-snapshots.js +1 -1
  128. package/dist/services/executor-onboarding.js +56 -0
  129. package/dist/services/external-work-launch.js +1 -1
  130. package/dist/services/harness-discovery.js +29 -0
  131. package/dist/services/harness-doctor.js +75 -0
  132. package/dist/services/harness-probe-effects.js +51 -0
  133. package/dist/services/harness-version-probe.js +100 -0
  134. package/dist/services/hooks.js +1 -1
  135. package/dist/services/hybrid-contracts.js +455 -0
  136. package/dist/services/installation-lock.js +92 -0
  137. package/dist/services/managed-processes.js +30 -0
  138. package/dist/services/merge-server.js +1 -1
  139. package/dist/services/model-catalog.js +109 -0
  140. package/dist/services/model-discovery.js +46 -0
  141. package/dist/services/operator-profiles.js +236 -0
  142. package/dist/services/release-archive-producer.js +52 -0
  143. package/dist/services/release-archive.js +143 -0
  144. package/dist/services/release-cache.js +142 -0
  145. package/dist/services/release-compatibility.js +18 -0
  146. package/dist/services/release-download.js +67 -0
  147. package/dist/services/release-envelope.js +104 -0
  148. package/dist/services/release-manifest.js +45 -0
  149. package/dist/services/release-payload.js +114 -0
  150. package/dist/services/release-producer.js +63 -0
  151. package/dist/services/release-provenance.js +160 -0
  152. package/dist/services/release-registry.js +66 -0
  153. package/dist/services/release-source.js +194 -0
  154. package/dist/services/run-controller-recovery.js +2 -2
  155. package/dist/services/run-controller.js +3 -2
  156. package/dist/services/run-health.js +149 -0
  157. package/dist/services/runs.js +61 -13
  158. package/dist/services/setup.js +377 -0
  159. package/dist/storage/database.js +18 -3
  160. package/dist/storage/writer-contract.js +8 -0
  161. package/package.json +4 -1
@@ -16,6 +16,19 @@ export function parseCapacityOperation(id) {
16
16
  export function normalizeCodexFailure(error, expectedSessionId, expectedTurnId) {
17
17
  let current = error;
18
18
  for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
19
+ // The same continuation authority applies to a proven v2 execution
20
+ // refusal. Never classify transport loss or an active native retry here.
21
+ if (current.code === "provider_capacity_transient") {
22
+ const details = current.details ?? {};
23
+ if (details.native_source !== "opencode.v2.session.execution" || details.terminal_status !== "failed"
24
+ || typeof details.provider_session_id !== "string" || !details.provider_session_id
25
+ || typeof details.turn_id !== "string" || !details.turn_id || details.native_turn_id !== details.turn_id
26
+ || expectedSessionId !== undefined && details.provider_session_id !== expectedSessionId
27
+ || expectedTurnId !== undefined && details.turn_id !== expectedTurnId) return null;
28
+ return { code: "serverOverloaded", session_id: details.provider_session_id, turn_id: details.turn_id,
29
+ native_error: { type: details.native_error_type, status: details.http_status }, observed_at: details.observed_at,
30
+ terminal_at: null, terminal_basis: null, native_turn_items: details.native_turn_items ?? null, primary_error: current };
31
+ }
19
32
  if (current.code === "turn_interrupted") {
20
33
  const details = current.details ?? {}, native = details.provider_error;
21
34
  const code = typeof native?.codexErrorInfo === "string" ? native.codexErrorInfo : native?.codexErrorInfo?.code;
@@ -5,7 +5,7 @@ import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mj
5
5
  import { openCodeToolObservation, journalToolObservations } from "./tool-observations.mjs";
6
6
  import { ObservationClock } from "./observation-clock.mjs";
7
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
- import { appendFile, chmod, copyFile, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
8
+ import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, unlink, writeFile } from "node:fs/promises";
9
9
  import { spawn } from "node:child_process";
10
10
  import net from "node:net";
11
11
  import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
@@ -14,26 +14,35 @@ import { errorRecord } from "./operation-errors.mjs";
14
14
  import { checkObservedProfile, observeModel } from "./model-observations.mjs";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
17
- import { OpenCodeClient, OpenCodeError, canonicalHistory, historyDigest, usageSnapshot } from "./dd-opencode.mjs";
17
+ import { parse as parseJsonc } from "jsonc-parser";
18
+ import { writeJsonAtomic } from "./runner-events.mjs";
19
+ import { OpenCodeClient, OpenCodeError, historyDigest, usageSnapshot, nativeModel } from "./dd-opencode.mjs";
18
20
  import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
19
21
 
20
- const REQUEST_SCHEMA = "dd-opencode/daemon-request@1", RESPONSE_SCHEMA = "dd-opencode/daemon-response@1", STATE_SCHEMA = "dd-opencode/daemon-state@1", ARCHIVE_SCHEMA = "dd-opencode/session-archive@1";
21
- const OPENCODE_BASELINE = Object.freeze({ version: "1.18.25", api_fingerprint: "dfb7d42a555389f0c662fa2b4a8af1d61633c96710cf54bce3ff2404e2e7d896" });
22
+ const REQUEST_SCHEMA = "dd-opencode/daemon-request@1", RESPONSE_SCHEMA = "dd-opencode/daemon-response@1", STATE_SCHEMA = "dd-opencode/daemon-state@2", ARCHIVE_SCHEMA = "dd-opencode/session-archive@2";
22
23
  class DaemonError extends Error { constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; } }
23
24
  const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
24
- function locations(stateDir) { const dir = absolute(stateDir, "--state-dir"), local = path.join(dir, "daemon.sock"); return { dir, socket: Buffer.byteLength(local) < 100 ? local : `/tmp/dd-opencode-${createHash("sha256").update(dir).digest("hex").slice(0, 20)}.sock`, state: path.join(dir, "daemon.json"), journal: path.join(dir, "events.jsonl"), log: path.join(dir, "daemon.log"), data: path.join(dir, "xdg-data"), config: path.join(dir, "xdg-config"), cache: path.join(dir, "xdg-cache"), archive: path.join(dir, "import.json") }; }
25
- async function writeJson(file, value) { const temporary = `${file}.${process.pid}.tmp`; await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(temporary, file); }
25
+ function locations(stateDir) { const dir = absolute(stateDir, "--state-dir"), local = path.join(dir, "daemon.sock"); return { dir, socket: Buffer.byteLength(local) < 100 ? local : `/tmp/dd-opencode-${createHash("sha256").update(dir).digest("hex").slice(0, 20)}.sock`, state: path.join(dir, "daemon.json"), journal: path.join(dir, "events.jsonl"), log: path.join(dir, "daemon.log"), data: path.join(dir, "xdg-data"), config: path.join(dir, "xdg-config"), cache: path.join(dir, "xdg-cache"), archive: path.join(dir, "import.json"), db: path.join(dir, "native.db"), xdgState: path.join(dir, "xdg-state") }; }
26
+ const writeJson = writeJsonAtomic;
26
27
  async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
27
28
  async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
28
29
  function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
29
- function isolatedEnv(config) { return { ...process.env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome, OPENCODE_CONFIG_DIR: config.adapterDir, OPENCODE_DISABLE_PROJECT_CONFIG: "1", OPENCODE_SERVER_PASSWORD: config.password, ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}), ...(config.ddFlowBin ? { DD_OPENCODE_DD_FLOW_BIN: config.ddFlowBin, DD_OPENCODE_NODE_BIN: process.execPath, DD_OPENCODE_PROJECT_ROOT: config.projectRoot } : {}), DD_OPENCODE_DAEMON_ID: config.daemonId, DD_OPENCODE_PROVIDER: config.provider, DD_OPENCODE_MODEL: config.model, DD_OPENCODE_VARIANT: config.variant, DD_OPENCODE_AGENT: config.agent, DD_OPENCODE_SERVER_VERSION: config.version ?? "unknown", DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "unknown" }; }
30
+ function isolatedEnv(config) {
31
+ const env = { ...process.env };
32
+ for (const name of ["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_PASSWORD", "OPENCODE_PTY_HANDOFF", "OPENCODE_SERVER", "OPENCODE_SIMULATE"]) delete env[name];
33
+ return { ...env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome,
34
+ XDG_STATE_HOME: config.stateHome, OPENCODE_DB: config.database, OPENCODE_CONFIG_DIR: config.adapterDir,
35
+ OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_SERVER_PASSWORD: config.password,
36
+ ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}),
37
+ DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "none" };
38
+ }
30
39
  export async function run(bin, args, config, options = {}) {
31
40
  const { stdin, timeoutMs = 30_000, startupMs = options.timeoutMs ?? SETUP_STARTUP_MS } = options;
32
41
  return await commandText(bin, args, { cwd: config.cwd, env: isolatedEnv(config), input: stdin ?? null, timeoutMs, startupMs, phase: args.includes('--version') ? 'native-version' : 'native-utility' });
33
42
  }
34
43
  async function forwardUsage(config, sessionId, snapshot) {
35
44
  if (!config.ddFlowBin) return;
36
- const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: "complete", usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cached_input_tokens, cacheWriteInputTokens: snapshot.cache_write_tokens, totalTokens: snapshot.total_tokens, costUsd: snapshot.cost_usd }, tool_calls: snapshot.tool_calls };
45
+ const payload = { provider_session_id: sessionId, daemon_id: config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: snapshot.completeness, usage: { inputTokens: snapshot.input_tokens, outputTokens: snapshot.output_tokens, reasoningTokens: snapshot.reasoning_tokens, cacheReadInputTokens: snapshot.cached_input_tokens, cacheWriteInputTokens: snapshot.cache_write_tokens, totalTokens: snapshot.total_tokens, ...(snapshot.cost_usd === null ? {} : { costUsd: snapshot.cost_usd }) }, tool_calls: snapshot.tool_calls };
37
46
  await run(config.ddFlowBin, ["opencode", "usage", "ingest", "--project-root", config.projectRoot, "--json"], config, { stdin: `${JSON.stringify(payload)}\n` });
38
47
  }
39
48
  async function freePort() { return await new Promise((resolve, reject) => { const server = net.createServer(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const port = server.address().port; server.close(() => resolve(port)); }); }); }
@@ -41,38 +50,111 @@ const errorPayload = errorRecord;
41
50
 
42
51
  async function writePlugin(paths, config) {
43
52
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
44
- const helper = new URL("./native-hook-command.mjs", import.meta.url).href;
45
- const source = `import { hookBudget, invokeNativeHook } from ${JSON.stringify(helper)};\nconst candidate = command => typeof command === "string" && command.length > 0;\nconst participating = new Set();\nconst invoke = async (payload,budget) => await invokeNativeHook({bin:process.env.DD_OPENCODE_DD_FLOW_BIN,args:["opencode","event","handle","--project-root",process.env.DD_OPENCODE_PROJECT_ROOT,"--json"],payload,home:process.env.DD_FLOW_HOME,timeoutMs:budget.remaining("admission",15000)});\nexport const DdFlowPlugin=async({client,project,directory})=>{ const envelope=async(phase,input,extra,budget)=>{ const response=await budget.run(()=>client.session.get({path:{id:input.sessionID}}),"identity"); if(response?.error||!response?.data) throw Object.assign(new Error("OpenCode Session identity is unavailable"),{code:"opencode_session_identity_unavailable"}); const session=response.data; return {schema_id:"dd-flow/opencode-tool-event@1",event_id:[input.sessionID,input.callID,phase].join(":"),phase,daemon_id:process.env.DD_OPENCODE_DAEMON_ID,server_version:process.env.DD_OPENCODE_SERVER_VERSION,plugin_sha256:process.env.DD_OPENCODE_PLUGIN_SHA256,session:{provider_session_id:input.sessionID,parent_provider_session_id:session.parentID??null,agent_id:process.env.DD_OPENCODE_AGENT,directory:session.directory??directory,project_id:session.projectID??project?.id},message_id:input.messageID??null,tool_call_id:input.callID,tool:input.tool,profile:{provider:process.env.DD_OPENCODE_PROVIDER,model:process.env.DD_OPENCODE_MODEL,variant:process.env.DD_OPENCODE_VARIANT,agent:process.env.DD_OPENCODE_AGENT},...extra}; }; return {"tool.execute.before":async(input,output)=>{ const command=output.args?.command; if(!candidate(command)) return; const budget=hookBudget(); const result=await invoke(await envelope("before",input,{input:output.args},budget),budget); if(result?.observed) participating.add(input.callID); const updated=result?.hookSpecificOutput?.updatedInput; if(updated) Object.assign(output.args,updated); },"tool.execute.after":async(input,output)=>{ if(!participating.delete(input.callID)) return; const budget=hookBudget(); await invoke(await envelope("after",input,{input:input.args,outcome:{status:"completed",title:output.title}},budget),budget); }}; };\n`;
53
+ const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot })});\n`;
46
54
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
47
55
  }
48
56
  async function prepare(paths, config, authSource) {
49
- for (const dir of [paths.data, paths.config, paths.cache, path.join(paths.config, "opencode")]) { await mkdir(dir, { recursive: true, mode: 0o700 }); await chmod(dir, 0o700); }
50
- const authTarget = path.join(paths.data, "opencode", "auth.json"); await mkdir(path.dirname(authTarget), { recursive: true, mode: 0o700 }); try { await copyFile(authSource, authTarget); await chmod(authTarget, 0o600); config.authStatus = "copied"; } catch (error) { if (error.code !== "ENOENT") throw error; config.authStatus = "absent"; }
51
- await writeJson(path.join(paths.config, "opencode", "opencode.json"), { $schema: "https://opencode.ai/config.json", autoupdate: false, permission: "allow" });
52
- if (!config.noFlow) config.pluginSha256 = await writePlugin(paths, config);
57
+ for (const dir of [paths.data, paths.config, paths.cache, paths.xdgState, path.join(paths.config, "opencode")]) { await mkdir(dir, { recursive: true, mode: 0o700 }); await chmod(dir, 0o700); }
58
+ const authTarget = path.join(paths.data, "opencode", "auth.json"); await mkdir(path.dirname(authTarget), { recursive: true, mode: 0o700 });
59
+ try { const source = JSON.parse(await readFile(authSource, "utf8")); const selected = !Array.isArray(source) && source[config.provider];
60
+ if (selected) { await writeJson(authTarget, { [config.provider]: selected }); config.authStatus = "selected_portable"; }
61
+ else config.authStatus = "absent";
62
+ } catch (error) { if (error.code !== "ENOENT") throw error; config.authStatus = "absent"; }
63
+ let portable = {}, policies, permissions = [{ action: "*", resource: "*", effect: "allow" }], agentRestrictions = {};
64
+ try { const errors = [], source = parseJsonc(await readFile(config.configSource, "utf8"), errors, { allowTrailingComma: true }); if (errors.length || !source || typeof source !== "object" || Array.isArray(source)) throw new Error("invalid config");
65
+ portable = Object.fromEntries(["provider", "providers", "permission", "tools"].filter(key => source[key] !== undefined).map(key => [key, source[key]]));
66
+ // Explicit operator policies outrank our permissive empty-home defaults.
67
+ // Let v2 normalize legacy permission fields instead of guessing mappings.
68
+ if (source.permissions !== undefined || source.permission !== undefined || source.tools !== undefined) permissions = source.permissions ?? [];
69
+ policies = source.experimental?.policies;
70
+ for (const [name, agents] of [["agent", source.agent], ["agents", source.agents]]) {
71
+ const selected = agents?.[config.agent];
72
+ if (selected) agentRestrictions[name] = { [config.agent]: Object.fromEntries(["permission", "permissions", "tools"].filter(key => selected[key] !== undefined).map(key => [key, selected[key]])) };
73
+ }
74
+ }
75
+ catch (error) { if (error.code !== "ENOENT") throw new DaemonError("opencode_config_invalid", "Portable OpenCode provider config must be valid JSON"); }
76
+ await writeJson(path.join(paths.config, "opencode", "opencode.json"), { ...portable, ...agentRestrictions, permissions,
77
+ agents: { ...agentRestrictions.agents, "dd-flow-worker": { mode: "subagent", system: "Execute the assigned dd-flow Work. Follow the issued lifecycle instructions. Do not spawn descendants.", model: `${config.provider}/${config.model}${config.variant !== "default" ? `#${config.variant}` : ""}` } },
78
+ experimental: { subagent_depth: 1, ...(policies ? { policies } : {}) } });
79
+ config.pluginSha256 = await writePlugin(paths, config);
53
80
  }
54
- async function importArchive(paths, config, archiveFile) { if (!archiveFile) return null; const manifest = JSON.parse(await readFile(absolute(archiveFile, "--session-archive"), "utf8")); if (manifest.schema_id !== ARCHIVE_SCHEMA || !manifest.payload) throw new DaemonError("invalid_session_archive", "OpenCode archive is incompatible"); if (manifest.opencode_version !== config.version) throw new DaemonError("session_archive_version_mismatch", "OpenCode archive version differs from the daemon version", false, { archive_version: manifest.opencode_version, current_version: config.version }); const payload = Buffer.from(manifest.payload, "base64").toString("utf8"); await writeFile(paths.archive, payload, { mode: 0o600 }); const stdout = await run(config.bin, ["import", paths.archive], config); const match = stdout.match(/ses_[A-Za-z0-9]+/); return { provider_session_id: match?.[0] ?? manifest.provider_session_id, manifest };
81
+ async function bootstrapCredentials(config, client) {
82
+ let source;
83
+ try { source = JSON.parse(await readFile(config.authSource, "utf8")); }
84
+ catch (error) { if (error.code === "ENOENT") return "absent"; throw new DaemonError("opencode_credentials_invalid", "Portable credentials are not valid JSON"); }
85
+ const entry = Array.isArray(source) ? source.find(item => item.integrationID === config.provider && item.active === true) : null;
86
+ const legacy = !Array.isArray(source) ? source[config.provider] : null;
87
+ const value = entry?.value ?? (legacy?.type === "api" ? { type: "key", key: legacy.key } : null);
88
+ // OAuth/external entries imported by the native v1 migration are retained;
89
+ // v2 exports use native credential values, never a copied user database.
90
+ let selected = (await client.call("credential.list")).find(item => item.integrationID === config.provider && item.active);
91
+ if (value && JSON.stringify(selected?.value) !== JSON.stringify(value)) {
92
+ if (selected) await client.call("credential.update", { id: selected.id, value }, { productive: true });
93
+ else await client.call("credential.create", { integrationID: config.provider, label: "dd-flow portable", value, activate: true }, { productive: true });
94
+ selected = (await client.call("credential.list")).find(item => item.integrationID === config.provider && item.active);
95
+ if (!selected || JSON.stringify(selected.value) !== JSON.stringify(value)) throw new DaemonError("opencode_credentials_unproven", "Private credential readback did not confirm the selected entry");
96
+ }
97
+ if ((entry || legacy) && !selected) throw new DaemonError("opencode_credentials_unproven", "Selected portable credential is not active in the private server");
98
+ return selected ? "active_private" : "absent";
99
+ }
100
+ async function importArchive(config, client) {
101
+ if (!config.sessionArchive) return null;
102
+ const manifest = JSON.parse(await readFile(config.sessionArchive, "utf8"));
103
+ if (manifest.schema_id !== ARCHIVE_SCHEMA || !manifest.payload || !/^2\./.test(manifest.opencode_version)) throw new DaemonError("invalid_session_archive", "Use the archived v1 engine for legacy OpenCode archives");
104
+ const payload = JSON.parse(Buffer.from(manifest.payload, "base64").toString("utf8"));
105
+ const transfers = payload.sessions, root = manifest.provider_session_id;
106
+ if (!Array.isArray(transfers) || !transfers.length || transfers[0]?.info?.id !== root || new Set(transfers.map(item => item.info?.id)).size !== transfers.length)
107
+ throw new DaemonError("invalid_session_archive", "Archive does not contain a unique native Session tree");
108
+ const records = [], imported = new Set();
109
+ for (const transfer of transfers) {
110
+ if (!transfer.info?.id || !Array.isArray(transfer.messages) || transfer.info.id !== root && !imported.has(transfer.info.parentID))
111
+ throw new DaemonError("invalid_session_archive", "Archive child is not in the native parent-first tree");
112
+ imported.add(transfer.info.id);
113
+ }
114
+ for (const transfer of transfers) {
115
+ const info = await client.import({ ...transfer, info: { ...transfer.info, location: { directory: config.cwd } } });
116
+ if (info?.id !== transfer.info.id || info.location?.directory !== config.cwd || (info.parentID ?? null) !== (transfer.info.parentID ?? null))
117
+ throw new DaemonError("session_import_unproven", "OpenCode import did not preserve native identity/location");
118
+ records.push({ provider_session_id: info.id, parent_provider_session_id: info.id === root ? null : info.parentID, root_provider_session_id: root, cwd: config.cwd });
119
+ }
120
+ return records;
55
121
  }
56
122
 
57
123
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
124
+ const state = await readState(stateDir);
125
+ if (state && state.schema_id !== STATE_SCHEMA) throw new DaemonError("legacy_opencode_state", "Use archive/opencode-v1-final-beta134 for v1 states; the v2 adapter cannot dispatch to a legacy daemon", false, { effect: "no_effect" });
58
126
  const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
59
127
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'opencode', timeoutMs });
60
128
  }
61
- function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","variant","agent","projectRoot","ddFlowBin","ddFlowHome","sessionArchive","resourceHome","noFlow"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
129
+ function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","variant","agent","projectRoot","ddFlowBin","ddFlowHome","sessionArchive","resourceHome","noFlow","configSource","authSource"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }
62
130
  export async function startDaemon(options) {
63
- const paths = locations(options.stateDir); await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
64
- const previous = await readState(paths.dir), daemonId = randomUUID(), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")); const config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_OPENCODE_BIN ?? "opencode", provider: String(options.provider ?? ""), model: String(options.model ?? ""), variant: String(options.variant ?? "default"), agent: String(options.agent ?? "build"), projectRoot: noFlow ? cwd : await realpath(absolute(options.projectRoot, "--project-root")), ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), resourceHome: noFlow ? null : (process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null), env: noFlow ? {} : { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") }, sessionArchive: options.sessionArchive ? absolute(options.sessionArchive, "--session-archive") : null, dataHome: paths.data, configHome: paths.config, cacheHome: paths.cache, adapterDir: path.join(paths.config, "opencode"), password: randomBytes(24).toString("hex"), daemonId, noFlow };
131
+ const paths = locations(options.stateDir);
132
+ const previous = await readState(paths.dir);
133
+ if (previous && previous.schema_id !== STATE_SCHEMA) throw new DaemonError("legacy_opencode_state", "Use archive/opencode-v1-final-beta134 for v1 states; start v2 in a fresh state directory");
134
+ await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
135
+ const daemonId = randomUUID(), noFlow = options.noFlow === true, cwd = await realpath(absolute(options.cwd, "--cwd")); const config = { cwd, journal: options.journal ? absolute(options.journal, "--journal") : paths.journal, bin: options.bin ?? process.env.DD_OPENCODE_BIN ?? "opencode", provider: String(options.provider ?? ""), model: String(options.model ?? ""), variant: String(options.variant ?? "default"), agent: String(options.agent ?? "build"), projectRoot: noFlow ? cwd : await realpath(absolute(options.projectRoot, "--project-root")), ddFlowBin: noFlow ? null : absolute(options.ddFlowBin, "--dd-flow-bin"), ddFlowHome: noFlow ? null : absolute(options.ddFlowHome, "--dd-flow-home"), resourceHome: noFlow ? null : (process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null), env: noFlow ? {} : { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") }, sessionArchive: options.sessionArchive ? absolute(options.sessionArchive, "--session-archive") : null, dataHome: paths.data, configHome: paths.config, cacheHome: paths.cache, stateHome: paths.xdgState, database: paths.db, adapterDir: path.join(paths.config, "opencode"), configSource: options.configPath ?? process.env.DD_OPENCODE_CONFIG_SOURCE ?? path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "opencode", "opencode.json"), password: randomBytes(24).toString("hex"), daemonId, noFlow };
65
136
  await prepareRuntimeOwner(config, paths.dir);
137
+ config.authSource = options.authPath ? absolute(options.authPath, "--auth-path") : path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
138
+ if (!options.configPath && !process.env.DD_OPENCODE_CONFIG_SOURCE) {
139
+ try { await lstat(config.configSource); } catch (error) { if (error.code === "ENOENT") config.configSource += "c"; else throw error; }
140
+ }
141
+ config.configSource = absolute(config.configSource, "--config-path");
66
142
  assertDaemonRestartable(previous);
67
143
  if (!config.provider || !config.model) throw new DaemonError("profile_required", "--provider and --model are required");
68
144
  try { const status = await callDaemon(paths.dir, "daemon.status", {}, 1000); if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration"); return { ...status, already_running: true }; } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
69
145
  assertDaemonRestartable(await readState(paths.dir));
70
146
  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");
71
- await removeSocket(paths); const authSource = options.authPath ? absolute(options.authPath, "--auth-path") : path.join(os.homedir(), ".local", "share", "opencode", "auth.json"); await prepare(paths, config, authSource); config.version = (await run(config.bin, ["--version"], config)).trim(); if (config.version !== OPENCODE_BASELINE.version) throw new DaemonError("unsupported_version", `unsupported OpenCode version ${config.version}; expected ${OPENCODE_BASELINE.version}`); const imported = await importArchive(paths, config, config.sessionArchive);
147
+ await removeSocket(paths); const authSource = config.authSource;
148
+ const version = (await run(config.bin, ["--version"], config)).trim(); config.version = version.match(/(?:^|\s)v?(2\.\d+\.\d+(?:[-+][\w.-]+)?)(?:\s|$)/)?.[1];
149
+ if (!config.version) throw new DaemonError("unsupported_version", "dd-opencode supports OpenCode v2 only; v1 remains at archive/opencode-v1-final-beta134");
150
+ await prepare(paths, config, authSource);
72
151
  const resourceProcess = await registerDaemonProcess(config, { kind: "opencode-daemon", owner: `opencode:${path.basename(paths.dir)}`, operation: `opencode-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
73
- const state = { schema_id: STATE_SCHEMA, daemon_id: daemonId, shutdown_state: "starting", active_tree: false, auth_status: config.authStatus, versions: { opencode: config.version }, config, sessions: imported ? [{ provider_session_id: imported.provider_session_id, parent_provider_session_id: null, root_provider_session_id: imported.provider_session_id, cwd: config.cwd }] : (previous?.sessions ?? []), resource_process: resourceProcess }; await writeJson(paths.state, state);
152
+ const state = { schema_id: STATE_SCHEMA, native_protocol: 2, daemon_id: daemonId, shutdown_state: "starting", active_tree: false, auth_status: config.authStatus, versions: { opencode: config.version }, config, sessions: previous?.sessions ?? [], operations: previous?.operations ?? {}, resource_process: resourceProcess }; await writeJson(paths.state, state);
74
153
  const log = await open(paths.log, "a", 0o600), entry = absolute(options.entryPath, "dd-opencode entry"); const child = spawn(process.execPath, [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();
75
- try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, state.daemon_id); while (!startupClock.sample(await startupProgress())) { await new Promise(resolve => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; } } throw Object.assign(new DaemonError("daemon_start_failed", "daemon did not become ready", true), { details: observationDetails(startupClock, 'daemon-start', child.pid) }); }
154
+ try { await confirmDaemonProcess(config, resourceProcess, child); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, state.daemon_id); while (!startupClock.sample(await startupProgress())) {
155
+ if (child.exitCode !== null || child.signalCode !== null) { const failed = await readState(paths.dir); throw Object.assign(new DaemonError("daemon_start_failed", "OpenCode daemon exited before readiness"), { cause: failed?.startup_error }); }
156
+ await new Promise(resolve => setTimeout(resolve, 100)); try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); } catch (error) { if (!["daemon_not_running","daemon_timeout"].includes(error.code)) throw error; }
157
+ } throw Object.assign(new DaemonError("daemon_start_failed", "daemon did not become ready", true), { details: observationDetails(startupClock, 'daemon-start', child.pid) }); }
76
158
  catch (error) { await cleanupFailedStart(config, resourceProcess, child, error); throw error; }
77
159
  }
78
160
  export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); };
@@ -81,8 +163,51 @@ export class Runtime {
81
163
  constructor(paths, state, client, child, providerProcess) { this.paths = paths; this.state = state; this.client = client; this.child = child; this.providerProcess = providerProcess; this.sessions = new Map((state.sessions ?? []).map(x => [x.provider_session_id, x])); this.active = null; this.persisting = Promise.resolve(); }
82
164
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
83
165
  async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
84
- async childTree(id, seen = new Set()) { if (seen.has(id)) return []; seen.add(id); const direct = await this.client.children(id) ?? []; const nested = []; for (const child of direct) nested.push(child, ...await this.childTree(child.id, seen)); return nested; }
85
- async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); this.journaledTools ??= new Map(); journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, tree_observations: children.map(child => ({ provider_session_id: child.id, status: statuses?.[child.id]?.type ?? "idle", settled: (statuses?.[child.id]?.type ?? "idle") === "idle" })), children, usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length }; }
166
+ async childTree(id, seen = new Set()) {
167
+ if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
168
+ seen.add(id); const direct = await this.client.children(id) ?? [], nested = [];
169
+ for (const child of direct) {
170
+ if (!child.id || child.parentID !== id) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
171
+ nested.push(child, ...await this.childTree(child.id, seen));
172
+ }
173
+ return nested;
174
+ }
175
+ async describe(id) {
176
+ const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]);
177
+ if (!info.directory || path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace");
178
+ const parent = info.fork ? null : info.parentID ?? null, existing = this.sessions.get(id);
179
+ const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent,
180
+ root_provider_session_id: parent ? this.sessions.get(parent)?.root_provider_session_id ?? parent : existing?.root_provider_session_id ?? id, cwd: info.directory };
181
+ this.sessions.set(id, record);
182
+ for (const child of children) {
183
+ if (!child.directory || path.resolve(child.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode child has another workspace");
184
+ this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID, root_provider_session_id: record.root_provider_session_id, cwd: child.directory });
185
+ }
186
+ const pending = new Map();
187
+ for (const session of [id, ...children.map(child => child.id)]) {
188
+ const value = this.client.pending ? await this.client.pending(session) : { inbox: [], forms: [], permissions: [] };
189
+ pending.set(session, { ...value, shells: this.client.shells ? (await this.client.shells(session)).filter(shell => shell.status === "running") : [] });
190
+ }
191
+ const busy = session => Boolean(statuses[session] && statuses[session].type !== "idle") || Object.values(pending.get(session)).some(items => items.length > 0);
192
+ const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = busy(id) ? "busy" : "idle";
193
+ const lastAssistant = messages.findLast(message => message.info?.role === "assistant");
194
+ const inputIndex = messages.findLastIndex(message => message.info.role === "user");
195
+ const idle = messages.slice(inputIndex + 1).findLast(message => message.info.role === "idle");
196
+ const treeIds = [id, ...children.map(child => child.id)];
197
+ await this.observeModels(id, messages, parent);
198
+ this.journaledTools ??= new Map();
199
+ if (this.state.config.journal) journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools);
200
+ return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? info.model?.providerID ?? null, model: lastAssistant?.info?.modelID ?? info.model?.id ?? null, reasoning: lastAssistant?.info?.variant ?? info.model?.variant ?? "default", mode: info.agent ?? null },
201
+ assistant_text: (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""), info, status,
202
+ settled: status === "idle" && !unsettledChildren.length, unsettled_children: unsettledChildren,
203
+ tree_session_ids: treeIds, unsettled_sessions: treeIds.filter(busy),
204
+ tree_observations: treeIds.map(session => ({ provider_session_id: session, status: busy(session) ? "running" : "idle", settled: !busy(session) })),
205
+ terminal_turn_id: idle && inputIndex >= 0 ? messages[inputIndex].info.id : null,
206
+ terminal_status: idle?.info.outcome === "failed" ? "failed" : idle?.info.outcome === "succeeded" ? "completed" : idle?.info.outcome ?? null,
207
+ native_turn_items: { observed: Boolean(idle), possible_effects: messages.slice(inputIndex + 1).some(message => message.parts.some(part => part.type === "tool")),
208
+ pending: busy(id) || messages.slice(inputIndex + 1).some(message => message.parts.some(part => part.type === "tool" && ["running", "streaming"].includes(part.state?.status))) },
209
+ children, pending: pending.get(id), usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length };
210
+ }
86
211
  async observeModels(id, messages, parent = null) {
87
212
  this.modelMessages ??= new Set();
88
213
  for (const message of messages ?? []) {
@@ -90,17 +215,23 @@ export class Runtime {
90
215
  if (info?.role !== "assistant" || !info.id || this.modelMessages.has(info.id)) continue;
91
216
  if (info.sessionID && info.sessionID !== id) throw new DaemonError("session_identity_mismatch", "native message belongs to another Session");
92
217
  await observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: id, parentSessionId: parent,
93
- requested: { ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent },
218
+ requested: { ...this.state.config, reasoning: this.state.config.variant, mode: parent ? "dd-flow-worker" : this.state.config.agent },
94
219
  observed: { provider: info.providerID, model: info.modelID, reasoning: info.variant, mode: info.mode }, source: { method: "session/message", message_id: info.id, event_sha256: createHash("sha256").update(`${id}:${info.id}:${info.modelID}`).digest("hex") }, evidence: "response" });
95
220
  this.modelMessages.add(info.id);
96
221
  }
97
222
  }
98
- async requireSettled(id) { const value = await this.describe(id); if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status }); return value; }
223
+ async requireSettled(id) {
224
+ const value = await this.describe(id);
225
+ if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status });
226
+ const config = this.state.config;
227
+ if (config.provider && config.model) checkObservedProfile({ ...config, reasoning: config.variant, mode: value.parent_provider_session_id ? "dd-flow-worker" : config.agent }, value.observed_profile);
228
+ return value;
229
+ }
99
230
  async reconcileUsage(observed) { const errors = []; try { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); } catch (error) { errors.push({ session_id: observed.provider_session_id, phase: "usage", ...errorPayload(error) }); } for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) { try { errors.push(...await this.reconcileUsage(await this.describe(child.id))); } catch (error) { errors.push({ session_id: child.id, phase: "usage_child", ...errorPayload(error) }); } } return errors; }
100
231
  async productive(name, task) {
101
232
  assertDaemonDispatch(this.paths?.dir);
102
233
  if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
103
- this.active = name; let result, completed = false;
234
+ this.active = name; let result, completed = false; const priorActive = this.state.active_tree;
104
235
  try {
105
236
  await this.persist({ active_tree: true, active_operation: name });
106
237
  assertDaemonDispatch(this.paths?.dir);
@@ -110,7 +241,7 @@ export class Runtime {
110
241
  } catch (cause) {
111
242
  const error = completed ? new DaemonError("operation_observation_lost", "OpenCode returned but its durable observation could not be saved; do not resend", false, { result, cause: errorPayload(cause) }) : cause;
112
243
  const failures = [];
113
- try { await this.persist({ active_tree: true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
244
+ try { await this.persist({ active_tree: error.details?.effect === "no_effect" ? priorActive : true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
114
245
  try { await this.journal(`${name}.failed`, null); } catch (secondary) { failures.push(errorPayload(secondary)); }
115
246
  if (failures.length) error.details = { ...error.details, observation_errors: failures };
116
247
  throw error;
@@ -134,9 +265,17 @@ export class Runtime {
134
265
  const assertDispatch = captureDispatchGuard(this);
135
266
  if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
136
267
  if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
137
- if (operation === "daemon.doctor") { const doc = await this.client.request("GET", "/doc", undefined, { directory: null, timeoutMs: 5_000 }); const apiFingerprint = createHash("sha256").update(typeof doc === "string" ? doc : JSON.stringify(doc)).digest("hex"); if (apiFingerprint !== OPENCODE_BASELINE.api_fingerprint) throw new DaemonError("api_fingerprint_mismatch", "OpenCode API differs from the qualified baseline", false, { expected: OPENCODE_BASELINE.api_fingerprint, actual: apiFingerprint }); return { compatible: true, versions: this.state.versions, api_fingerprint: apiFingerprint, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status }; }
268
+ if (operation === "daemon.doctor") {
269
+ const info = await this.client.health();
270
+ await this.client.call("model.list", { location: { directory: this.state.config.cwd } });
271
+ const plugins = await this.client.call("plugin.list", { location: { directory: this.state.config.cwd } });
272
+ if (info.pid !== this.child?.pid || info.version !== this.state.versions.opencode) throw new DaemonError("foreign_server", "OpenCode endpoint is not the owned process");
273
+ const pluginID = this.state.config.noFlow ? "dd-flow-security" : "dd-flow";
274
+ if (!plugins.data.some(plugin => plugin.id === pluginID && plugin.state?.status === "active")) throw new DaemonError("opencode_plugin_missing", "Owned v2 plugin is not active");
275
+ return { compatible: true, native_protocol: 2, productive_hooks: !this.state.config.noFlow, versions: this.state.versions, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status };
276
+ }
138
277
  if (operation === "session.create") return this.productive(operation, async () => {
139
- const info = await this.client.create(params.title);
278
+ const info = await this.client.create(params.title, this.state.config);
140
279
  const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? null, root_provider_session_id: info.id, cwd: info.directory };
141
280
  this.sessions.set(info.id, created);
142
281
  try {
@@ -150,32 +289,38 @@ export class Runtime {
150
289
  return { ...created, settled: false, settlement_observation_error: errorPayload(error) };
151
290
  }
152
291
  });
292
+ if (["session.inspect", "session.resume", "session.prompt", "session.fork", "session.archive"].includes(operation) && !this.sessions.has(params.sessionId)) {
293
+ const native = await this.client.inspect(params.sessionId);
294
+ if (!native.parentID || !this.sessions.has(native.parentID) || native.fork) throw new DaemonError("foreign_session", "Session is not a member of the retained owned tree");
295
+ }
153
296
  if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
154
- if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => { assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, provider: this.state.config.provider, model: this.state.config.model, variant: this.state.config.variant, agent: this.state.config.agent,
297
+ if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
298
+ this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
299
+ assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
300
+ save: async receipt => { this.state.operations[key] = receipt; await this.persist(); },
155
301
  assertDispatch, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
156
302
  onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
157
303
  onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
158
304
  onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
159
305
  }); const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
160
- const reply_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: this.state.config.agent ?? null };
161
- checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, reply_profile);
306
+ const expectedAgent = this.sessions.get(params.sessionId)?.parent_provider_session_id ? "dd-flow-worker" : this.state.config.agent;
307
+ const reply_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: reply.info.mode ?? null };
308
+ checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, reply_profile);
162
309
  let result;
163
310
  try { result = await this.describe(params.sessionId); }
164
311
  catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
165
312
  result.assistant_text = assistant_text;
166
313
  result.response_message_id = reply.info.id;
167
314
  result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
168
- checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
315
+ checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
169
316
  if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
170
317
  const info = await this.client.fork(params.sessionId, params.messageId);
171
318
  const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? params.sessionId, root_provider_session_id: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
172
319
  this.sessions.set(info.id, created);
173
320
  try {
174
321
  const fork = await this.describe(info.id);
175
- const sourceMessages = await this.client.messages(params.sessionId), forkMessages = await this.client.messages(info.id);
176
- const prefix = JSON.stringify(canonicalHistory(forkMessages)) === JSON.stringify(canonicalHistory(sourceMessages).slice(0, forkMessages.length));
177
- if (!prefix) throw new DaemonError("fork_continuity_failed", "fork history is not a source prefix");
178
- return { ...fork, fork_receipt: { source_provider_session_id: params.sessionId, source_history_sha256: source.history_sha256, fork_history_sha256: fork.history_sha256, history_prefix_proven: true } };
322
+ if (info.fork?.sessionID !== params.sessionId || params.messageId && info.fork?.boundary?.messageID !== params.messageId) throw new DaemonError("fork_continuity_failed", "OpenCode did not confirm the requested native fork boundary");
323
+ return { ...fork, fork_receipt: { source_provider_session_id: params.sessionId, source_history_sha256: source.history_sha256, fork_history_sha256: fork.history_sha256, boundary: info.fork.boundary, native_boundary_proven: true } };
179
324
  } catch (error) {
180
325
  // A native fork is materialized already. Do not lose its identity or
181
326
  // retry it, but never let an unproven history prefix become usable.
@@ -191,7 +336,9 @@ export class Runtime {
191
336
  result = { ...result, cancellation_errors };
192
337
  await this.persist({ active_tree: Boolean(this.active) || !result.settled }); await this.journal(operation, result); return result;
193
338
  }
194
- if (operation === "session.archive") { const observed = await this.requireSettled(params.sessionId); const payload = await run(this.state.config.bin, ["export", params.sessionId], this.state.config); const manifest = { schema_id: ARCHIVE_SCHEMA, provider_session_id: params.sessionId, source_cwd: observed.cwd, opencode_version: this.state.versions.opencode, history_sha256: observed.history_sha256, exported_at: new Date().toISOString(), payload: Buffer.from(payload).toString("base64") }; const file = absolute(params.archiveFile, "--archive-file"); await writeJson(file, manifest); return { archive_file: file, ...manifest, payload: undefined, sha256: createHash("sha256").update(await readFile(file)).digest("hex") }; }
339
+ if (operation === "session.archive") { const observed = await this.requireSettled(params.sessionId); const sessions = [];
340
+ for (const id of [params.sessionId, ...observed.children.map(child => child.id)]) sessions.push(await this.client.export(id));
341
+ const payload = JSON.stringify({ sessions }); const manifest = { schema_id: ARCHIVE_SCHEMA, native_protocol: 2, provider_session_id: params.sessionId, source_cwd: observed.cwd, opencode_version: this.state.versions.opencode, history_sha256: observed.history_sha256, exported_at: new Date().toISOString(), payload: Buffer.from(payload).toString("base64") }; const file = absolute(params.archiveFile, "--archive-file"); await mkdir(path.dirname(file), { recursive: true, mode: 0o700 }); await writeJson(file, manifest); return { archive_file: file, ...manifest, payload: undefined, sha256: createHash("sha256").update(await readFile(file)).digest("hex") }; }
195
342
  if (operation === "daemon.stop") {
196
343
  return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), async () => {
197
344
  if (params.cancelTree) cancelPendingDispatch(this);
@@ -225,7 +372,13 @@ export async function serveDaemon(stateDir) {
225
372
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
226
373
  scheduleDaemonProcessHeartbeat(state.config, providerProcess);
227
374
  state.provider_process = providerProcess; state.provider_pid = child.pid; await writeJson(paths.state, state);
228
- const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const startupProgress = startupObserver(paths.dir, state.daemon_id); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); while (!startupClock.sample(await startupProgress())) { try { await client.health(); break; } catch { await new Promise(resolve => setTimeout(resolve, 100)); } } await client.health(); const runtime = new Runtime(paths, state, client, child, providerProcess); await removeSocket(paths);
375
+ const client = new OpenCodeClient({ baseUrl: `http://127.0.0.1:${port}`, password: state.config.password, directory: state.config.cwd }); const startupProgress = startupObserver(paths.dir, state.daemon_id); const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); while (!startupClock.sample(await startupProgress())) { try { await client.health(); break; } catch { await new Promise(resolve => setTimeout(resolve, 100)); } }
376
+ const identity = await client.health();
377
+ if (identity.pid !== child.pid || identity.version !== state.versions.opencode) throw new DaemonError("foreign_server", "OpenCode startup returned another server");
378
+ state.auth_status = await bootstrapCredentials(state.config, client);
379
+ const imported = await importArchive(state.config, client); if (imported) state.sessions.push(...imported);
380
+ await chmod(state.config.database, 0o600);
381
+ const runtime = new Runtime(paths, state, client, child, providerProcess); await runtime.dispatch("daemon.doctor", {}); await removeSocket(paths);
229
382
  const server = net.createServer(connection => {
230
383
  connection.setEncoding("utf8"); let buffer = "", consumed = false;
231
384
  connection.on("data", chunk => {
@@ -248,6 +401,7 @@ export async function serveDaemon(stateDir) {
248
401
  trackDaemonConnections(server); runtime.server = server;
249
402
  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, server_port: port }); return await new Promise(() => {});
250
403
  } catch (error) {
404
+ await writeJson(paths.state, { ...state, startup_error: errorPayload(error) });
251
405
  await cleanupFailedStart(state.config, providerProcess, child, error); throw error;
252
406
  }
253
407
  }