@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.
- package/CHANGELOG.md +12 -0
- package/README.md +33 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +1 -0
- package/dist/cli/command-metadata.js +144 -0
- package/dist/cli/command-option-descriptions.js +186 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +71 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +69 -0
- package/dist/cli/run-cli.js +5 -0
- package/dist/cli/setup.js +147 -0
- package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +1 -1
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +13 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +193 -39
- package/dist/harness-runtime/lib/dd-opencode.mjs +295 -100
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
- package/dist/harness-runtime/lib/managed-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +60 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
- package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
- package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
- package/dist/services/cli-operation-classifier.js +4 -0
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +23 -25
- package/dist/services/eval-snapshots.js +1 -1
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +1 -1
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +1 -1
- package/dist/services/hybrid-contracts.js +455 -0
- package/dist/services/installation-lock.js +92 -0
- package/dist/services/managed-processes.js +30 -0
- package/dist/services/merge-server.js +1 -1
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/run-controller-recovery.js +2 -2
- package/dist/services/run-controller.js +3 -2
- package/dist/services/run-health.js +149 -0
- package/dist/services/runs.js +61 -13
- package/dist/services/setup.js +377 -0
- package/dist/storage/database.js +18 -3
- package/dist/storage/writer-contract.js +8 -0
- 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,
|
|
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 {
|
|
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@
|
|
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
|
-
|
|
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) {
|
|
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:
|
|
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
|
|
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 });
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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);
|
|
64
|
-
const previous = await readState(paths.dir)
|
|
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 =
|
|
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:
|
|
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())) {
|
|
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()) {
|
|
85
|
-
|
|
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) {
|
|
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") {
|
|
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 () => {
|
|
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
|
|
161
|
-
|
|
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:
|
|
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
|
-
|
|
176
|
-
|
|
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
|
|
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)); } }
|
|
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
|
}
|