@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
|
@@ -1,128 +1,323 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
3
|
import { ToolObservations, openCodeToolObservation } from "./tool-observations.mjs";
|
|
3
|
-
import { observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
4
|
+
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
4
5
|
import { assertNativeLine } from "./native-streams.mjs";
|
|
5
6
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
6
7
|
|
|
8
|
+
export const OPENCODE_PROTOCOL = 2;
|
|
7
9
|
export class OpenCodeError extends Error {
|
|
8
|
-
constructor(code, message, retryable = false, details) { super(message); this
|
|
10
|
+
constructor(code, message, retryable = false, details) { super(message); Object.assign(this, { code, retryable, details }); }
|
|
11
|
+
}
|
|
12
|
+
const lost = (message, details) => new OpenCodeError("operation_observation_lost", message, false, { native_outcome: "unknown", ...details });
|
|
13
|
+
export const requestDigest = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14
|
+
export const nativeModel = ({ provider, model, variant }) => ({ providerID: provider, id: model, ...(variant && variant !== "default" ? { variant } : {}) });
|
|
15
|
+
|
|
16
|
+
/** info/parts is our normalized projection, not a second v1 native protocol. */
|
|
17
|
+
export function normalizeMessage(message, sessionID) {
|
|
18
|
+
if (!message?.id || !message.type) throw lost("OpenCode message has no native identity");
|
|
19
|
+
const { model, time = {} } = message;
|
|
20
|
+
const parts = (message.content ?? []).map(part => part.type === "tool"
|
|
21
|
+
? { ...part, tool: part.name, callID: part.id, state: { ...part.state, output: part.state?.result ?? part.state?.output } }
|
|
22
|
+
: { ...part });
|
|
23
|
+
if (message.type !== "assistant" && typeof message.text === "string") parts.push({ type: "text", text: message.text });
|
|
24
|
+
return { info: { id: message.id, sessionID, role: message.type, providerID: model?.providerID, modelID: model?.id,
|
|
25
|
+
variant: model?.variant ?? "default", mode: message.agent, finish: message.finish, error: message.error,
|
|
26
|
+
cost: message.cost, tokens: message.tokens, created: time.created, completed: time.completed, metadata: message.metadata, outcome: message.outcome }, parts };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function providerFailure(native, sessionId, turnId) {
|
|
30
|
+
const message = String(native?.message ?? "OpenCode execution failed"), status = native?.status, type = String(native?.type ?? "unknown");
|
|
31
|
+
const quota = status === 402 || /quota|balance exhausted|insufficient.?credit|billing|payment required/i.test(message);
|
|
32
|
+
const code = quota ? "provider_quota_exhausted" : status === 401 || status === 403 ? "provider_auth_failed"
|
|
33
|
+
: /overload|server.?busy/i.test(`${type} ${message}`) || status === 503 ? "provider_capacity_transient"
|
|
34
|
+
: /rate.?limit|too many requests/i.test(message) ? "provider_rate_limited"
|
|
35
|
+
: status === 429 ? "provider_limit_unknown" : "opencode_provider_failed";
|
|
36
|
+
// Never forward response.body or provider credentials into controller/Judge.
|
|
37
|
+
return new OpenCodeError(code, `OpenCode native execution failed (${type}${status ? `, HTTP ${status}` : ""})`,
|
|
38
|
+
["provider_capacity_transient", "provider_rate_limited"].includes(code), {
|
|
39
|
+
provider_session_id: sessionId, session_id: sessionId, turn_id: turnId, native_error_type: type,
|
|
40
|
+
native_turn_id: turnId, terminal_status: "failed",
|
|
41
|
+
...(Number.isInteger(status) ? { http_status: status } : {}), observed_at: new Date().toISOString(), native_source: "opencode.v2.session.execution" });
|
|
9
42
|
}
|
|
10
43
|
|
|
11
44
|
export class OpenCodeClient {
|
|
12
|
-
constructor({ baseUrl, password, directory, timeoutMs = 1_800_000 }) {
|
|
13
|
-
this
|
|
45
|
+
constructor({ baseUrl, password, directory, timeoutMs = 1_800_000, api }) {
|
|
46
|
+
Object.assign(this, { baseUrl: new URL(baseUrl), password, directory, timeoutMs });
|
|
47
|
+
this.ready = api ? Promise.resolve(api) : import("../vendor/opencode-client/promise/index.js").then(({ OpenCode }) => OpenCode.make({
|
|
48
|
+
baseUrl: this.baseUrl.href, headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }
|
|
49
|
+
}));
|
|
14
50
|
}
|
|
15
|
-
async
|
|
16
|
-
const
|
|
17
|
-
|
|
51
|
+
async call(method, input, { timeoutMs, progress, onDispatched, productive = false } = {}) {
|
|
52
|
+
const observation = this.readObservation?.();
|
|
53
|
+
timeoutMs ??= observation?.timeoutMs ?? this.timeoutMs; progress ??= observation?.progress;
|
|
54
|
+
const api = await this.ready, controller = new AbortController();
|
|
55
|
+
const timer = observedTimeout(() => controller.abort(), timeoutMs, { progress });
|
|
18
56
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} finally {
|
|
57
|
+
const keys = method.split("."), name = keys.pop(), domain = keys.reduce((value, key) => value?.[key], api);
|
|
58
|
+
if (typeof domain?.[name] !== "function") throw new OpenCodeError("opencode_capability_missing", `OpenCode v2 does not expose ${method}`);
|
|
59
|
+
const options = { signal: controller.signal };
|
|
60
|
+
const pending = input === undefined ? domain[name](options) : domain[name](input, options);
|
|
61
|
+
void pending.catch(() => {}); await onDispatched?.(); const result = await pending;
|
|
62
|
+
assertNativeLine(JSON.stringify(result ?? null)); return result;
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
if (cause instanceof OpenCodeError || cause.code === "operation_observation_lost") throw cause;
|
|
65
|
+
if (cause._tag === "SessionNotFoundError" || cause._tag === "NotFoundError") throw new OpenCodeError("session_not_found", "OpenCode native Session is unavailable");
|
|
66
|
+
if (productive && ["InvalidRequestError", "ConflictError", "UnauthorizedError", "ForbiddenError"].includes(cause._tag))
|
|
67
|
+
throw new OpenCodeError("opencode_admission_rejected", "OpenCode rejected native admission", false, { effect: "no_effect", native_type: cause._tag });
|
|
68
|
+
if (productive || controller.signal.aborted) throw Object.assign(lost(`OpenCode observer lost during ${method}`, { method }), { cause });
|
|
69
|
+
throw Object.assign(new OpenCodeError("opencode_unavailable", `OpenCode ${method} observation failed`, true,
|
|
70
|
+
{ method, native_type: cause._tag ?? cause.name ?? null, native_reason: cause.reason ?? null }), { cause });
|
|
71
|
+
} finally { clearInterval(timer); }
|
|
72
|
+
}
|
|
73
|
+
async health() {
|
|
74
|
+
const info = await this.call("server.info", undefined, { timeoutMs: 5_000 });
|
|
75
|
+
if (!/^2\./.test(info?.version) || !Number.isInteger(info.pid)) throw new OpenCodeError("unsupported_version", "OpenCode v2 server identity is unavailable");
|
|
76
|
+
return info;
|
|
77
|
+
}
|
|
78
|
+
async pages(method, input) {
|
|
79
|
+
const items = new Map(), seen = new Set(); let cursor;
|
|
80
|
+
do {
|
|
81
|
+
const page = await this.call(method, { ...input, limit: 100, ...(cursor ? { cursor } : { order: "asc" }) });
|
|
82
|
+
if (!Array.isArray(page?.data)) throw lost(`OpenCode ${method} returned an invalid page`);
|
|
83
|
+
for (const item of page.data) { if (!item?.id) throw lost("OpenCode page item has no identity"); items.set(item.id, item); }
|
|
84
|
+
cursor = page.cursor?.next;
|
|
85
|
+
if (cursor && seen.has(cursor)) throw lost("OpenCode pagination cursor repeated");
|
|
86
|
+
if (cursor) seen.add(cursor);
|
|
87
|
+
} while (cursor);
|
|
88
|
+
return [...items.values()];
|
|
89
|
+
}
|
|
90
|
+
create(title, profile = {}, id) { return this.call("session.create", { ...(title ? { title } : {}), ...(id ? { id } : {}),
|
|
91
|
+
location: { directory: this.directory }, agent: profile.agent ?? "build", model: nativeModel(profile) }, { productive: true }); }
|
|
92
|
+
async inspect(id) { const info = await this.call("session.get", { sessionID: id }); return { ...info, directory: info.location?.directory }; }
|
|
93
|
+
list() { return this.pages("session.list", { directory: this.directory }); }
|
|
94
|
+
async statuses() { const active = await this.call("session.active"); return Object.fromEntries(Object.keys(active).map(id => [id, { type: "busy" }])); }
|
|
95
|
+
async children(id) { return (await this.pages("session.list", { parentID: id })).map(info => ({ ...info, directory: info.location?.directory })); }
|
|
96
|
+
async messages(id) { return (await this.pages("message.list", { sessionID: id })).map(message => normalizeMessage(message, id)); }
|
|
97
|
+
inbox(id) { return this.call("session.inbox.list", { sessionID: id }); }
|
|
98
|
+
async shells(id) { const result = await this.call("shell.list", { location: { directory: this.directory } }); return result.data.filter(item => item.metadata?.sessionID === id); }
|
|
99
|
+
async pending(id) {
|
|
100
|
+
const [inbox, forms, permissions] = await Promise.all([this.inbox(id), this.call("session.form.list", { sessionID: id }), this.call("permission.list", { sessionID: id })]);
|
|
101
|
+
const value = { inbox, forms: forms.data ?? forms, permissions: permissions.data ?? permissions };
|
|
102
|
+
if (Object.values(value).some(items => !Array.isArray(items))) throw lost("OpenCode returned invalid pending-input evidence", { session_id: id });
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
async logs(id, after = 0) {
|
|
106
|
+
const api = await this.ready, controller = new AbortController(), events = [];
|
|
107
|
+
const observation = this.readObservation?.();
|
|
108
|
+
const timer = observedTimeout(() => controller.abort(), observation?.timeoutMs ?? this.timeoutMs, { progress: observation?.progress });
|
|
109
|
+
try { for await (const event of api.session.log({ sessionID: id, after, follow: false }, { signal: controller.signal })) events.push(event); }
|
|
110
|
+
catch (cause) { throw Object.assign(lost("OpenCode durable log observation failed", { session_id: id }), { cause }); }
|
|
111
|
+
finally { clearInterval(timer); }
|
|
112
|
+
return events;
|
|
113
|
+
}
|
|
114
|
+
async abort(id) {
|
|
115
|
+
// Queued input must not become fresh work during interruption cleanup.
|
|
116
|
+
for (const item of await this.inbox(id)) await this.call("session.inbox.cancel", { sessionID: id, inboxID: item.id }, { productive: true });
|
|
117
|
+
const result = await this.call("session.interrupt", { sessionID: id, resume: false }, { productive: true });
|
|
118
|
+
const api = await this.ready;
|
|
119
|
+
if (typeof api.shell?.list === "function") for (const shell of await this.shells(id)) {
|
|
120
|
+
if (shell.status === "running") await this.call("shell.remove", { id: shell.id, location: { directory: this.directory } }, { productive: true });
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
34
123
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
async prompt(id, { prompt, provider, model, variant, agent, onModelMessages, onObservationError, assertDispatch, onDispatched, livenessTimeoutMs = this.timeoutMs }) {
|
|
124
|
+
fork(id, messageID) { return this.call("session.fork", { sessionID: id, ...(messageID ? { before: messageID } : {}) }, { productive: true }); }
|
|
125
|
+
delete(id) { return this.call("session.remove", { sessionID: id }, { productive: true }); }
|
|
126
|
+
export(id) { return this.call("session.export", { sessionID: id }); }
|
|
127
|
+
import(data) { return this.call("session.import", data, { productive: true }); }
|
|
128
|
+
|
|
129
|
+
async prompt(id, { prompt, messageId = `msg_${randomUUID().replaceAll("-", "")}`, retained,
|
|
130
|
+
save, onModelMessages, onObservationError, assertDispatch, onDispatched, livenessTimeoutMs = this.timeoutMs }) {
|
|
43
131
|
validateObservationDuration(livenessTimeoutMs);
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
marker = { cursor: ++cursor, observed_at: Date.now() };
|
|
77
|
-
await publish("native_progress", { session_id: id, observed_at: new Date(marker.observed_at).toISOString() });
|
|
132
|
+
const request = { sessionID: id, id: messageId, text: prompt, delivery: "queue", resume: true }, digest = requestDigest(request);
|
|
133
|
+
if (retained && (retained.request_sha256 !== digest || retained.message_id !== messageId)) throw new OpenCodeError("operation_request_mismatch", "OpenCode operation already binds another prompt", false, { effect: "no_effect" });
|
|
134
|
+
let state = retained ?? { message_id: messageId, request_sha256: digest, phase: "prepared", cursor: 0 };
|
|
135
|
+
const previous = await this.messages(id), baseline = new Map([[id, new Set(previous.map(item => item.info.id))]]), parents = new Map();
|
|
136
|
+
const capture = async session => { for (const child of await this.children(session)) { parents.set(child.id, child.parentID); baseline.set(child.id, new Set((await this.messages(child.id)).map(item => item.info.id))); await capture(child.id); } };
|
|
137
|
+
await capture(id);
|
|
138
|
+
if (retained) {
|
|
139
|
+
const index = previous.findIndex(item => item.info.id === messageId);
|
|
140
|
+
if (index >= 0) baseline.set(id, new Set(previous.slice(0, index + 1).map(item => item.info.id)));
|
|
141
|
+
}
|
|
142
|
+
assertDispatch?.(); // Cancellation/ownership wins before observer setup.
|
|
143
|
+
const api = await this.ready, streamAbort = new AbortController(), clock = new ObservationClock({ timeoutMs: livenessTimeoutMs });
|
|
144
|
+
const publish = operationProgressReporter(), known = new Set([id, ...baseline.keys()]), signatures = new Map(), shellCursors = new Map(), liveSeen = new Set();
|
|
145
|
+
let progress = 0, marker, stream;
|
|
146
|
+
const liveParts = new Map(), liveShells = new Map(), backgroundShells = new Set();
|
|
147
|
+
const advance = async (observedAt = Date.now()) => { marker = { cursor: ++progress, observed_at: observedAt }; await publish("native_progress", { session_id: id, observed_at: new Date(marker.observed_at).toISOString() }); };
|
|
148
|
+
let connected;
|
|
149
|
+
const connection = new Promise(resolve => { connected = resolve; });
|
|
150
|
+
const connectionTimer = api.event?.subscribe ? observedTimeout(() => { connected(false); streamAbort.abort(); }, livenessTimeoutMs) : null;
|
|
151
|
+
if (api.event?.subscribe) stream = (async () => {
|
|
152
|
+
try { for await (const event of api.event.subscribe({ signal: streamAbort.signal, onActivity: () => connected(true) })) {
|
|
153
|
+
connected(true); // Stream establishment is not model progress.
|
|
154
|
+
const data = event.data, floor = baseline.get(data?.sessionID);
|
|
155
|
+
if (known.has(data?.sessionID) && data?.assistantMessageID && !floor?.has(data.assistantMessageID)
|
|
156
|
+
&& event.created >= clock.startedWall && event.created <= Date.now() && event.id && !liveSeen.has(event.id)
|
|
157
|
+
&& ["session.text.delta", "session.reasoning.delta", "session.tool.input.delta", "session.tool.progress"].includes(event.type)) {
|
|
158
|
+
liveSeen.add(event.id); if (liveSeen.size > 1024) liveSeen.delete(liveSeen.values().next().value);
|
|
159
|
+
const key = `${data.sessionID}:${data.assistantMessageID}:${data.id ?? ""}`, signature = requestDigest(data.metadata ?? null);
|
|
160
|
+
if (event.type === "session.tool.progress" && typeof data.metadata?.shellID === "string") liveShells.set(key, data.metadata.shellID);
|
|
161
|
+
if (typeof data.delta === "string" && data.delta.length || event.type === "session.tool.progress" && liveParts.get(key) !== signature) {
|
|
162
|
+
liveParts.set(key, signature); await advance(event.created);
|
|
163
|
+
}
|
|
78
164
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// content has had a chance to renew it.
|
|
88
|
-
void refreshObservation();
|
|
89
|
-
const timer = setInterval(() => { void refreshObservation(); }, Math.max(10, Math.min(5_000, livenessTimeoutMs / 4)));
|
|
165
|
+
} } catch (error) { if (!streamAbort.signal.aborted) await onObservationError?.(error); }
|
|
166
|
+
finally { connected(false); }
|
|
167
|
+
})();
|
|
168
|
+
const priorObservation = this.readObservation;
|
|
169
|
+
this.readObservation = () => {
|
|
170
|
+
if (clock.sample(marker)) throw lost("OpenCode operation observation expired", { session_id: id, message_id: messageId, observation: clock.state() });
|
|
171
|
+
return { timeoutMs: Math.max(1, Math.ceil(livenessTimeoutMs - clock.elapsed)), progress: () => marker };
|
|
172
|
+
};
|
|
90
173
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!
|
|
94
|
-
|
|
174
|
+
if (api.event?.subscribe && !await connection) throw new OpenCodeError("opencode_unavailable", "Native observer stream could not be established before dispatch", true, { effect: "no_effect" });
|
|
175
|
+
clearInterval(connectionTimer);
|
|
176
|
+
if (!retained) {
|
|
177
|
+
await save?.(state); assertDispatch?.();
|
|
178
|
+
// Subscribe before dispatch; persisted intent precedes HTTP. Unknown
|
|
179
|
+
// ack permits reconciliation only, never another same-ID wake.
|
|
180
|
+
state = { ...state, phase: "dispatched" }; await save?.(state);
|
|
181
|
+
try {
|
|
182
|
+
const admission = await this.call("session.prompt", request, { productive: true, onDispatched });
|
|
183
|
+
if (admission?.id !== messageId || admission.sessionID !== id) throw lost("OpenCode prompt acknowledgement has another identity");
|
|
184
|
+
state = { ...state, phase: "admitted" }; await save?.(state);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.details?.effect === "no_effect") throw error;
|
|
187
|
+
await onObservationError?.(error);
|
|
188
|
+
}
|
|
95
189
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
190
|
+
for (;;) {
|
|
191
|
+
try {
|
|
192
|
+
const events = await this.logs(id, state.cursor);
|
|
193
|
+
let delivered = state.delivered === true, terminal = state.terminal;
|
|
194
|
+
for (const event of events) {
|
|
195
|
+
if (!Number.isInteger(event.durable?.seq) || event.durable.seq <= state.cursor) continue;
|
|
196
|
+
if (event.type === "session.inbox.delivered" && event.data.inboxID === messageId) { delivered = true; terminal = null; }
|
|
197
|
+
if (delivered && event.type === "session.execution.started") terminal = null;
|
|
198
|
+
if (delivered && ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"].includes(event.type)) terminal = { type: event.type, error: event.data.error };
|
|
199
|
+
state = { ...state, cursor: event.durable.seq, delivered, terminal }; await save?.(state);
|
|
200
|
+
}
|
|
201
|
+
const observe = async (session, visited = new Set()) => {
|
|
202
|
+
if (visited.has(session)) throw lost("OpenCode topology contains a cycle"); visited.add(session); known.add(session);
|
|
203
|
+
const messages = await this.messages(session);
|
|
204
|
+
await onModelMessages?.(session, messages, parents.get(session) ?? null);
|
|
205
|
+
const floor = baseline.get(session) ?? new Set(), current = messages.filter(item => item.info.role === "assistant" && !floor.has(item.info.id));
|
|
206
|
+
for (const item of current) {
|
|
207
|
+
const key = `${session}:${item.info.id}`, encoded = JSON.stringify(item.parts.map(stablePart)), next = requestDigest(encoded), before = signatures.get(key);
|
|
208
|
+
if (before?.signature !== next && !before?.seen.has(next) && (!before || before.seen.size < 128 || encoded.length > before.highWater)) {
|
|
209
|
+
if (item.parts.length) await advance();
|
|
210
|
+
const seen = before?.seen ?? new Set(); if (seen.size < 128) seen.add(next);
|
|
211
|
+
signatures.set(key, { signature: next, seen, highWater: Math.max(before?.highWater ?? 0, encoded.length) });
|
|
212
|
+
}
|
|
213
|
+
for (const part of item.parts) {
|
|
214
|
+
let shellID = part.type === "tool" ? part.state?.metadata?.shellID ?? liveShells.get(`${session}:${item.info.id}:${part.id}`) : null;
|
|
215
|
+
if (!shellID && part.tool === "shell" && part.state?.status === "running") {
|
|
216
|
+
// Tool progress metadata is volatile in the CLI. Reconcile
|
|
217
|
+
// only an unambiguous native shell in this current message.
|
|
218
|
+
const candidates = (await this.shells(session)).filter(shell => shell.time.started >= item.info.created);
|
|
219
|
+
if (candidates.length === 1) shellID = candidates[0].id;
|
|
220
|
+
}
|
|
221
|
+
if (!shellID) continue;
|
|
222
|
+
const cursor = shellCursors.get(shellID) ?? 0;
|
|
223
|
+
shellCursors.set(shellID, cursor); // Zero-output shells still own their native notification.
|
|
224
|
+
if (part.tool === "shell" && part.state?.metadata?.status === "running") backgroundShells.add(shellID);
|
|
225
|
+
const output = await this.call("shell.output", { id: shellID, location: { directory: this.directory }, cursor, limit: 65536 }), data = output.data ?? output;
|
|
226
|
+
if (data.cursor > cursor) { shellCursors.set(shellID, data.cursor); await advance(); }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const child of await this.children(session)) {
|
|
230
|
+
if (child.parentID !== session) throw lost("OpenCode child has a foreign parent");
|
|
231
|
+
parents.set(child.id, session); await observe(child.id, visited);
|
|
232
|
+
}
|
|
233
|
+
return current;
|
|
234
|
+
};
|
|
235
|
+
const current = await observe(id), rootMessages = await this.messages(id);
|
|
236
|
+
// CLI v2 defaults to non-persistent event payloads. The native
|
|
237
|
+
// projected user + idle records remain durable and carry the
|
|
238
|
+
// exact terminal error; do not invent a 'latest assistant' outcome.
|
|
239
|
+
const deliveredIndex = rootMessages.findIndex(item => item.info.id === messageId && item.info.role === "user");
|
|
240
|
+
if (deliveredIndex >= 0) {
|
|
241
|
+
for (const item of rootMessages.slice(deliveredIndex + 1)) {
|
|
242
|
+
if (item.info.role === "synthetic" && item.info.metadata?.source === "subagent"
|
|
243
|
+
&& typeof item.info.metadata.childID === "string" && parents.get(item.info.metadata.childID) !== id) {
|
|
244
|
+
// Native messages and topology are separate reads: a child can
|
|
245
|
+
// finish between them. Reconcile its ancestry before rejecting
|
|
246
|
+
// the notification, never trust the notification alone.
|
|
247
|
+
const child = (await this.children(id)).find(child => child.id === item.info.metadata.childID && child.parentID === id);
|
|
248
|
+
if (child) { parents.set(child.id, id); await observe(child.id); }
|
|
249
|
+
}
|
|
250
|
+
if (item.info.role === "user" || item.info.role === "synthetic" && !(
|
|
251
|
+
item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) === id
|
|
252
|
+
|| item.info.metadata?.source === "shell" && shellCursors.has(item.info.metadata.shellID)))
|
|
253
|
+
throw new OpenCodeError("opencode_foreign_input", "OpenCode received input outside the owned operation", false, { session_id: id, message_id: item.info.id, role: item.info.role, source: item.info.metadata?.source });
|
|
254
|
+
}
|
|
255
|
+
const terminal = rootMessages.slice(deliveredIndex + 1).findLast(item => item.info.role === "idle");
|
|
256
|
+
if (terminal) {
|
|
257
|
+
if (!["succeeded", "failed", "interrupted"].includes(terminal.info.outcome)) throw lost("OpenCode idle record has no proven native outcome");
|
|
258
|
+
state = { ...state, delivered: true, terminal: { type: `session.execution.${terminal.info.outcome}`, error: terminal.info.error } }; await save?.(state);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const statuses = await this.statuses(); let unsettled = false;
|
|
262
|
+
// A background child's native completion notification is delivered
|
|
263
|
+
// asynchronously. An idle parent before that delivery is not the
|
|
264
|
+
// completion of the coordinating operation.
|
|
265
|
+
const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent").map(item => item.info.metadata.childID));
|
|
266
|
+
const shellNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "shell").map(item => item.info.metadata.shellID));
|
|
267
|
+
unsettled ||= [...backgroundShells].some(shell => !shellNotifications.has(shell));
|
|
268
|
+
for (const item of current) for (const part of item.parts) {
|
|
269
|
+
if (part.tool === "subagent" && part.state?.input?.background === true) {
|
|
270
|
+
const child = part.state?.metadata?.sessionID;
|
|
271
|
+
if (!child || !deliveredNotifications.has(child)) unsettled = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const session of known) {
|
|
275
|
+
const pending = await this.pending(session);
|
|
276
|
+
if (pending.forms.length || pending.permissions.length) throw new OpenCodeError("opencode_native_hitl", "OpenCode requires native operator input", false, { provider_session_id: session, form_ids: pending.forms.map(item => item.id), permission_ids: pending.permissions.map(item => item.id) });
|
|
277
|
+
unsettled ||= Boolean(statuses[session]) || pending.inbox.length > 0;
|
|
278
|
+
if (typeof api.shell?.list === "function") unsettled ||= (await this.shells(session)).some(shell => shell.status === "running");
|
|
279
|
+
}
|
|
280
|
+
if (state.delivered && state.terminal && !unsettled) {
|
|
281
|
+
if (state.terminal.type === "session.execution.failed") throw providerFailure(state.terminal.error, id, messageId);
|
|
282
|
+
if (state.terminal.type === "session.execution.interrupted") throw new OpenCodeError("opencode_execution_interrupted", "OpenCode execution was interrupted", false, { provider_session_id: id, turn_id: messageId });
|
|
283
|
+
const final = current.findLast(item => item.info.completed !== undefined && item.info.finish && item.info.finish !== "tool-calls");
|
|
284
|
+
if (!final) throw lost("OpenCode terminal has no current assistant response", { session_id: id, message_id: messageId });
|
|
285
|
+
if (final.info.error) throw providerFailure(final.info.error, id, messageId);
|
|
286
|
+
return { ...final, admission_message_id: messageId, log_cursor: state.cursor };
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)) throw error;
|
|
290
|
+
await onObservationError?.(error);
|
|
291
|
+
}
|
|
292
|
+
if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state() });
|
|
293
|
+
await delay(Math.max(5, Math.min(500, livenessTimeoutMs / 4)));
|
|
103
294
|
}
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
finally { clearInterval(timer); await observation; }
|
|
295
|
+
} finally { clearInterval(connectionTimer); this.readObservation = priorObservation; streamAbort.abort(); await stream; }
|
|
107
296
|
}
|
|
108
|
-
fork(id, messageID) { return this.request("POST", `/session/${encodeURIComponent(id)}/fork`, messageID ? { messageID } : {}); }
|
|
109
|
-
abort(id) { return this.request("POST", `/session/${encodeURIComponent(id)}/abort`); }
|
|
110
|
-
delete(id) { return this.request("DELETE", `/session/${encodeURIComponent(id)}`); }
|
|
111
297
|
}
|
|
112
298
|
|
|
113
299
|
function stablePart(part) {
|
|
114
|
-
if (part.type === "text" || part.type === "reasoning") return { type: part.type, text: part.text
|
|
115
|
-
if (part.type === "tool") return { type: "tool", tool: part.tool, state:
|
|
116
|
-
if (part.type === "subtask") return { type: "subtask", prompt: part.prompt, description: part.description, agent: part.agent };
|
|
300
|
+
if (part.type === "text" || part.type === "reasoning") return { type: part.type, text: part.text };
|
|
301
|
+
if (part.type === "tool") return { type: "tool", tool: part.tool, state: part.state };
|
|
117
302
|
return { type: part.type };
|
|
118
303
|
}
|
|
119
304
|
export function canonicalHistory(messages) {
|
|
120
|
-
return messages.map(({ info, parts }) => ({ role: info.role, ...(info.role === "assistant" ? { provider: info.providerID, model: info.modelID, mode: info.mode, finish: info.finish ?? null, error: info.error?.
|
|
305
|
+
return messages.map(({ info, parts }) => ({ role: info.role, ...(info.role === "assistant" ? { provider: info.providerID, model: info.modelID, mode: info.mode, finish: info.finish ?? null, error: info.error?.type ?? null } : {}), parts: parts.map(stablePart) }));
|
|
121
306
|
}
|
|
122
|
-
export function historyDigest(messages) { return
|
|
307
|
+
export function historyDigest(messages) { return requestDigest(canonicalHistory(messages)); }
|
|
123
308
|
export function usageSnapshot(messages, sessionId) {
|
|
124
|
-
const assistants = messages.filter(
|
|
309
|
+
const assistants = messages.filter(item => item.info?.role === "assistant"), tokens = { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 };
|
|
310
|
+
let complete = true, cost = 0, costComplete = true;
|
|
311
|
+
for (const item of assistants) {
|
|
312
|
+
const value = item.info.tokens; complete &&= Boolean(value && [value.input, value.output, value.reasoning, value.cache?.read, value.cache?.write].every(Number.isFinite));
|
|
313
|
+
for (const name of ["input", "output", "reasoning"]) tokens[name] += value?.[name] ?? 0;
|
|
314
|
+
tokens.cache_read += value?.cache?.read ?? 0; tokens.cache_write += value?.cache?.write ?? 0;
|
|
315
|
+
costComplete &&= Number.isFinite(item.info.cost); cost += item.info.cost ?? 0;
|
|
316
|
+
}
|
|
125
317
|
const tools = new ToolObservations();
|
|
126
318
|
for (const item of messages) for (const part of item.parts ?? []) tools.add(openCodeToolObservation(part, item.info?.sessionID ?? sessionId, item.info?.id));
|
|
127
|
-
return { input_tokens: tokens.input, output_tokens: tokens.output, reasoning_tokens: tokens.reasoning,
|
|
319
|
+
return { input_tokens: tokens.input, output_tokens: tokens.output, reasoning_tokens: tokens.reasoning,
|
|
320
|
+
cached_input_tokens: tokens.cache_read, cache_write_tokens: tokens.cache_write,
|
|
321
|
+
total_tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache_read + tokens.cache_write,
|
|
322
|
+
tool_calls: tools.summary(), cost_usd: costComplete ? cost : null, completeness: complete ? "complete" : "partial" };
|
|
128
323
|
}
|
|
@@ -22,8 +22,8 @@ const ADAPTERS = {
|
|
|
22
22
|
},
|
|
23
23
|
opencode: {
|
|
24
24
|
name: "OpenCode",
|
|
25
|
-
launch: "Use
|
|
26
|
-
wait: "Await each native
|
|
25
|
+
launch: "Use OpenCode v2's native `subagent` tool with agent: \"dd-flow-worker\", description, and prompt containing the assignment. Omit sessionID for a fresh child and omit model overrides. For independent fanout assignments use background: true. Do not create a new root Session or use an external runner. Use `shell` with explicit workdir and timeout: 0 for issued lifecycle calls, never background those CLI calls.",
|
|
26
|
+
wait: "Await each native subagent result or its automatic background completion notification. Retain the real returned sessionID. Do not invent a wait tool or poll by sending child prompts. Native notifications can continue the parent automatically; a running tool handle is not completion. Never synthesize a Session ID or relaunch a quiet child."
|
|
27
27
|
},
|
|
28
28
|
antigravity: {
|
|
29
29
|
name: "Antigravity",
|
|
@@ -46,7 +46,7 @@ function requireAdapter(harness) {
|
|
|
46
46
|
export function delegationContract(harness) {
|
|
47
47
|
const key = delegationAdapter(harness);
|
|
48
48
|
requireAdapter(harness);
|
|
49
|
-
return `dd-flow/delegation/${key}@${
|
|
49
|
+
return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
|
|
@@ -294,7 +294,7 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
|
|
|
294
294
|
|
|
295
295
|
export async function registerDaemonProcess(config, input) {
|
|
296
296
|
const owner = bindRuntimeOwner(config);
|
|
297
|
-
const harness = ({ "codex-daemon": "codex-desktop", "codex-provider": "codex-desktop", "zcode-daemon": "zcode-acp", "zcode-provider": "zcode-acp", "grok-daemon": "grok-acp", "grok-provider": "grok-acp", "agy-daemon": "antigravity-cli", "opencode-daemon": "opencode-server", "droid-daemon": "droid-cli" })[input.kind];
|
|
297
|
+
const harness = ({ "codex-daemon": "codex-desktop", "codex-provider": "codex-desktop", "zcode-daemon": "zcode-acp", "zcode-provider": "zcode-acp", "grok-daemon": "grok-acp", "grok-provider": "grok-acp", "agy-daemon": "antigravity-cli", "opencode-daemon": "opencode-server", "opencode-provider": "opencode-server", "droid-daemon": "droid-cli" })[input.kind];
|
|
298
298
|
const scope = owner ? { owner: owner.owner_id, "project-id": owner.project_id, run: owner.run_id, work: owner.work_id, role: owner.role, "state-dir": owner.state_dir, "adapter-executable": owner.adapter_executable, ...(owner.budget ? { "budget-json": JSON.stringify(owner.budget), harness } : {}), ...(owner.project_id && owner.run_id || owner.budget ? { "dispatch-barrier": "run-generation@1" } : {}), operation: `${owner.operation_id}:${input.operation ?? input.kind}` } : {};
|
|
299
299
|
const options = { ...input, ...scope, id: `PROC-${input.kind}-${randomUUID()}`, "owner-pid": process.pid, "lease-ms": 300_000 };
|
|
300
300
|
const renewal = createRenewalState();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export function invokeNativeHook(input: { bin: string; args: string[]; payload: unknown; home?: string | undefined; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number }): Promise<Record<string, unknown> & { timing?: Record<string, number> }>;
|
|
1
|
+
export function invokeNativeHook(input: { bin: string; args: string[]; nodeBin?: string; payload: unknown; home?: string | undefined; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number }): Promise<Record<string, unknown> & { timing?: Record<string, number> }>;
|
|
2
2
|
export function hookBudget(timeoutMs?: number, deadlineAt?: number | null): { deadlineAt: number; remaining(phase: string, maximum?: number): number; run<T>(action: () => T | Promise<T>, phase: string): Promise<T> };
|
|
3
3
|
export function readNativeHookInput(stream: NodeJS.ReadStream, budget: { remaining(phase: string): number }, maxBytes?: number): Promise<string>;
|
|
4
4
|
export function acceptNativeHookRequest(stateDir: string, binding: Record<string, unknown>, current?: { requests: Map<string, { binding: Record<string, unknown> }>; accepting?: Map<string, Promise<Record<string, unknown>>> } | null): Promise<Record<string, unknown>>;
|
|
5
5
|
export function writeNativeHookFailure(stateDir: string, binding: Record<string, unknown> | undefined, error: unknown, budget: ReturnType<typeof hookBudget>): Promise<void>;
|
|
6
6
|
export function readNativeHookFailures(stateDir: string, requests: Map<string, { current: unknown; binding: Record<string, unknown> }>, current: unknown, reject: (error: Error & { code?: string; details?: Record<string, unknown> }) => void): Promise<void>;
|
|
7
|
-
export function runBoundedCommand(input: { bin: string; args: string[]; input?: string | null; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number; timeoutCode?: string; inheritDeadline?: boolean; cleanupWithinBudget?: boolean; maintenanceProgress?: { action: string; processId: string | null; startupMs?: number; inactivityMs: number } | null; signal?: AbortSignal }): Promise<string>;
|
|
7
|
+
export function runBoundedCommand(input: { bin: string; args: string[]; nodeBin?: string; input?: string | null; cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number; timeoutCode?: string; inheritDeadline?: boolean; cleanupWithinBudget?: boolean; maintenanceProgress?: { action: string; processId: string | null; startupMs?: number; inactivityMs: number } | null; signal?: AbortSignal }): Promise<string>;
|
|
@@ -117,7 +117,7 @@ export async function terminateCommandTree(child, { platform = process.platform,
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/** Preserve the native hook protocol across the CLI process boundary. */
|
|
120
|
-
export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}, timeoutMs = 15_000, timeoutCode = "native_hook_timeout", inheritDeadline = true, cleanupWithinBudget = false, maintenanceProgress = null, signal }) {
|
|
120
|
+
export async function runBoundedCommand({ bin, args, nodeBin = process.execPath, input = null, cwd, env = {}, timeoutMs = 15_000, timeoutCode = "native_hook_timeout", inheritDeadline = true, cleanupWithinBudget = false, maintenanceProgress = null, signal }) {
|
|
121
121
|
if (signal?.aborted) throw Object.assign(new Error("Bounded CLI request cancelled before launch"), { code: "process_maintenance_cancelled", details: { effect: "no_effect" } });
|
|
122
122
|
const inherited = inheritDeadline ? env.DD_FLOW_HOOK_DEADLINE_AT ?? process.env.DD_FLOW_HOOK_DEADLINE_AT : undefined;
|
|
123
123
|
if (inherited !== undefined) {
|
|
@@ -137,7 +137,7 @@ export async function runBoundedCommand({ bin, args, input = null, cwd, env = {}
|
|
|
137
137
|
const deadlineAt = Math.floor(Date.now() + Math.max(0, commandMs - Math.min(250, commandMs / 10)));
|
|
138
138
|
const script = /\.[cm]?js$/.test(bin);
|
|
139
139
|
const output = await new Promise((resolve, reject) => {
|
|
140
|
-
const child = spawn(script ?
|
|
140
|
+
const child = spawn(script ? nodeBin : bin, [...(script ? [bin] : []), ...args], {
|
|
141
141
|
cwd, env: { ...process.env, ...env, DD_FLOW_HOOK_DEADLINE_AT: String(deadlineAt) }, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"]
|
|
142
142
|
});
|
|
143
143
|
let stdout = "", stderr = "", failure, cleanupTimer, settled = false, termination;
|
|
@@ -241,8 +241,8 @@ function hookDiagnostics(stderr) {
|
|
|
241
241
|
}).slice(-16);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
export async function invokeNativeHook({ bin, args, payload, home, cwd, env, timeoutMs = 15_000 }) {
|
|
245
|
-
const output = await runBoundedCommand({ bin, args, input: `${JSON.stringify(payload)}\n`, cwd, env: { ...env, DD_FLOW_HOME: home }, timeoutMs });
|
|
244
|
+
export async function invokeNativeHook({ bin, args, nodeBin, payload, home, cwd, env, timeoutMs = 15_000 }) {
|
|
245
|
+
const output = await runBoundedCommand({ bin, args, nodeBin, input: `${JSON.stringify(payload)}\n`, cwd, env: { ...env, DD_FLOW_HOME: home }, timeoutMs });
|
|
246
246
|
let result;
|
|
247
247
|
try { result = JSON.parse(output); } catch { throw Object.assign(new Error("Invalid native hook JSON response"), { code: "native_hook_response_invalid" }); }
|
|
248
248
|
if (result?.ok === false && typeof result.error?.code === "string") throw Object.assign(new Error(result.error.message ?? "Native hook failed"), { code: result.error.code, details: result.error.details });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import * as Plugin from "../vendor/opencode-plugin.mjs";
|
|
3
|
+
import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
|
|
4
|
+
|
|
5
|
+
/** Launch-bound server plugin; never uses a process-global 'last Session'. */
|
|
6
|
+
export function makeOpenCodePlugin(binding, invoke = invokeNativeHook) {
|
|
7
|
+
return Plugin.define({ id: binding.ddFlowBin ? "dd-flow" : "dd-flow-security", async setup(ctx) {
|
|
8
|
+
const participating = new Map();
|
|
9
|
+
const key = event => JSON.stringify([binding.daemonId, event.sessionID, event.messageID, event.id]);
|
|
10
|
+
const envelope = async (phase, event, budget, outcome) => {
|
|
11
|
+
const info = await budget.run(() => ctx.session.get({ sessionID: event.sessionID }), "identity");
|
|
12
|
+
if (!info?.id || info.id !== event.sessionID || !info.location?.directory) throw new Error("OpenCode Session identity is unavailable");
|
|
13
|
+
const input = event.input;
|
|
14
|
+
if (!input || typeof input !== "object" || typeof input.command !== "string") throw new Error("OpenCode shell input is unavailable");
|
|
15
|
+
const cwd = path.resolve(info.location.directory, input.workdir ?? ".");
|
|
16
|
+
return { schema_id: "dd-flow/opencode-tool-event@1", native_protocol: 2,
|
|
17
|
+
event_id: [binding.daemonId, event.sessionID, event.messageID, event.id].join(":"), phase,
|
|
18
|
+
daemon_id: binding.daemonId, server_version: binding.version, plugin_sha256: process.env.DD_OPENCODE_PLUGIN_SHA256,
|
|
19
|
+
session: { provider_session_id: info.id, parent_provider_session_id: info.parentID ?? null,
|
|
20
|
+
agent_id: event.agent, directory: cwd, project_id: info.projectID },
|
|
21
|
+
message_id: event.messageID, tool_call_id: event.id, tool: "shell", native_tool: event.tool,
|
|
22
|
+
profile: { provider: info.model?.providerID, model: info.model?.id, variant: info.model?.variant ?? "default", agent: event.agent },
|
|
23
|
+
input: { ...input, workdir: cwd }, ...(outcome ? { outcome } : {}) };
|
|
24
|
+
};
|
|
25
|
+
const deliver = (payload, budget) => invoke({ bin: binding.ddFlowBin, nodeBin: binding.nodeBin,
|
|
26
|
+
args: ["opencode", "event", "handle", "--project-root", binding.projectRoot, "--json"],
|
|
27
|
+
payload, home: binding.ddFlowHome, timeoutMs: budget.remaining("admission", 15000) });
|
|
28
|
+
await ctx.shell.hook("create.before", event => {
|
|
29
|
+
delete event.env.OPENCODE_PASSWORD; delete event.env.OPENCODE_SERVER_PASSWORD;
|
|
30
|
+
});
|
|
31
|
+
await ctx.tool.hook("execute.before", async event => {
|
|
32
|
+
if (event.tool !== "shell" || !binding.ddFlowBin) return;
|
|
33
|
+
const budget = hookBudget(), payload = await envelope("before", event, budget);
|
|
34
|
+
event.input = { ...event.input, workdir: payload.session.directory };
|
|
35
|
+
const result = await deliver({ ...payload, input: event.input }, budget);
|
|
36
|
+
if (result?.permissionDecision === "deny" || result?.hookSpecificOutput?.permissionDecision === "deny") throw new Error("dd-flow denied native shell admission");
|
|
37
|
+
if (result?.observed) {
|
|
38
|
+
participating.set(key(event), payload);
|
|
39
|
+
await ctx.storage.set(key(event), JSON.parse(JSON.stringify(payload)));
|
|
40
|
+
// The common observer, not OpenCode's fixed 120-second cutoff, owns
|
|
41
|
+
// flow CLI inactivity. No lifecycle process is detached here.
|
|
42
|
+
event.input = { ...event.input, ...(result.hookSpecificOutput?.updatedInput ?? {}), timeout: 0, background: false };
|
|
43
|
+
} else if (event.input.timeout === undefined) event.input.timeout = 0;
|
|
44
|
+
});
|
|
45
|
+
await ctx.tool.hook("execute.after", async event => {
|
|
46
|
+
if (event.tool !== "shell") return;
|
|
47
|
+
const budget = hookBudget(), original = participating.get(key(event)) ?? await ctx.storage.get(key(event));
|
|
48
|
+
if (!original) return; // Ordinary shell tools do not create lifecycle receipts.
|
|
49
|
+
const output = event.result?.output;
|
|
50
|
+
const success = event.status === "completed" && output?.status !== "running" && output?.exit === 0 && !output?.timeout && !output?.signal;
|
|
51
|
+
const outcome = { status: success ? "completed" : output?.status === "running" ? "running" : "failed",
|
|
52
|
+
exit_code: output?.exit ?? null, signal: output?.signal ?? null, shell_id: output?.shellID ?? null };
|
|
53
|
+
const payload = await envelope("after", event, budget, outcome);
|
|
54
|
+
await deliver({ ...payload, ...(original ? { event_id: original.event_id } : {}) }, budget);
|
|
55
|
+
participating.delete(key(event));
|
|
56
|
+
await ctx.storage.remove(key(event));
|
|
57
|
+
});
|
|
58
|
+
return () => participating.clear();
|
|
59
|
+
} });
|
|
60
|
+
}
|