@deksden-com/dd-flow-cli 0.9.0-beta.133 → 0.9.0-beta.135
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 +27 -0
- package/README.md +31 -0
- package/dist/build-info.json +7 -7
- 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/dd-zcode.mjs +21 -1
- 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-children.d.mts +1 -0
- package/dist/harness-runtime/lib/native-children.mjs +16 -0
- 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/controller-fanout.js +20 -3
- package/dist/services/engines.js +22 -24
- package/dist/services/eval-snapshots.js +1 -1
- package/dist/services/external-work-launch.js +1 -1
- package/dist/services/hooks.js +17 -11
- package/dist/services/lifecycle-invocations.js +66 -12
- package/dist/services/merge-server.js +1 -1
- package/dist/services/run-controller-recovery.js +2 -2
- package/dist/services/run-controller.js +2 -2
- package/dist/services/vnext-fanout.js +6 -1
- package/dist/services/work-registry.js +54 -11
- package/dist/storage/database.js +4 -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
|
}
|
|
@@ -14,6 +14,7 @@ import { checkObservedProfile, observeModel, observedFields } from "./model-obse
|
|
|
14
14
|
import { commandText } from "./process-json.mjs";
|
|
15
15
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
16
16
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
17
|
+
import { safeNativeFailure } from "./native-children.mjs";
|
|
17
18
|
|
|
18
19
|
import { readZcodeUsage } from "./zcode-usage.mjs";
|
|
19
20
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
@@ -569,7 +570,25 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
569
570
|
cursor = page.ended.nextCursor;
|
|
570
571
|
if (!cursor) {
|
|
571
572
|
if (ended.size !== first.ended.total) fail("ZCode child directory is incomplete", "native_topology_incomplete");
|
|
572
|
-
|
|
573
|
+
const items = [];
|
|
574
|
+
for (const child of ended.values()) {
|
|
575
|
+
if (child.status !== "failed" || !Number.isSafeInteger(child.startedAt) || child.startedAt <= 0
|
|
576
|
+
|| !Number.isSafeInteger(child.endedAt) || child.endedAt < child.startedAt || child.endedAt > 8.64e15) { items.push(child); continue; }
|
|
577
|
+
// Closed children must never be loaded merely to explain their failure.
|
|
578
|
+
let terminal;
|
|
579
|
+
try { terminal = await bridge.request("zcode/session/retainedTerminal", { sessionId: child.childSessionId, startedAt: child.startedAt, endedAt: child.endedAt }, timeoutMs); }
|
|
580
|
+
catch (error) { items.push({ ...child, terminal_metadata_unavailable: error.code ?? "terminal_metadata_unavailable" }); continue; }
|
|
581
|
+
if (terminal?.available !== true) { items.push({ ...child, terminal_metadata_unavailable: "terminal_metadata_unavailable" }); continue; }
|
|
582
|
+
const observedAt = Date.parse(terminal.terminal_observed_at);
|
|
583
|
+
if (terminal.sessionId !== child.childSessionId || typeof terminal.terminal_turn_id !== "string" || !terminal.terminal_turn_id
|
|
584
|
+
|| terminal.startedAt !== child.startedAt || terminal.endedAt !== child.endedAt
|
|
585
|
+
|| !Number.isFinite(observedAt) || observedAt < child.startedAt || observedAt > child.endedAt) fail("ZCode terminal failure metadata has foreign identity", "native_child_turn_conflict");
|
|
586
|
+
const providerError = safeNativeFailure(terminal.provider_error);
|
|
587
|
+
items.push({ ...child, terminal_turn_id: terminal.terminal_turn_id, terminal_observed_at: terminal.terminal_observed_at,
|
|
588
|
+
...(providerError ? { provider_error: providerError } : {}) });
|
|
589
|
+
clock.sample(child.childSessionId);
|
|
590
|
+
}
|
|
591
|
+
return { ...first, ended: { total: first.ended.total, items } };
|
|
573
592
|
}
|
|
574
593
|
if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
|
|
575
594
|
cursors.add(cursor);
|
|
@@ -923,6 +942,7 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
|
|
|
923
942
|
["b9e0f5106dddb439b76186278bd9be5f45dbf3ef", "dd-zcode-harness@2"],
|
|
924
943
|
["636b14181d5c9a95476055079836fd6b58169f58", "dd-zcode-harness@2"],
|
|
925
944
|
["4621fabd3f517ecdd349dfebebae9077c01d2f2b", "dd-zcode-harness@2"],
|
|
945
|
+
["9c5ca6cc81c4eb5065e43f8b7ab6b7cc5ab7ae47", "dd-zcode-harness@2"],
|
|
926
946
|
]);
|
|
927
947
|
const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
|
|
928
948
|
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
@@ -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();
|
|
@@ -3,6 +3,22 @@ import { errorRecord } from "./operation-errors.mjs";
|
|
|
3
3
|
|
|
4
4
|
function nativeFailure(code, message, exitCode, details) { return Object.assign(new Error(message), { code, exitCode, details }); }
|
|
5
5
|
|
|
6
|
+
/** Retain typed failure facts, never provider headers, request bodies or free-form messages. */
|
|
7
|
+
export function safeNativeFailure(value, depth = 0) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const key of ["type", "code", "source", "reason", "errorPhase", "exceptionKind", "providerId", "modelId", "providerKind", "transport", "retryable", "willRetry", "statusCode", "resets_at", "retryAfter", "retry_after", "retryAfterMs", "resetAt"]) {
|
|
11
|
+
const scalar = value[key];
|
|
12
|
+
if (typeof scalar === "boolean" || typeof scalar === "number" && Number.isFinite(scalar)
|
|
13
|
+
|| typeof scalar === "string" && scalar.length <= 256 && /^[\w.:/ -]+$/.test(scalar)) result[key] = scalar;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value.codexErrorInfo === "string" && /^[\w-]{1,128}$/.test(value.codexErrorInfo)) result.codexErrorInfo = value.codexErrorInfo;
|
|
16
|
+
else if (value.codexErrorInfo && typeof value.codexErrorInfo.code === "string" && /^[\w-]{1,128}$/.test(value.codexErrorInfo.code)) result.codexErrorInfo = { code: value.codexErrorInfo.code };
|
|
17
|
+
const attribution = depth === 0 && value.attribution && safeNativeFailure(value.attribution, 1);
|
|
18
|
+
if (attribution && Object.keys(attribution).length) result.attribution = attribution;
|
|
19
|
+
return Object.keys(result).length ? result : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
|
|
7
23
|
/** Normalize provider-observed topology only; never parse assistant prose. */
|
|
8
24
|
export function normalizeNativeChildren(receipt, rootSessionId) {
|
|
@@ -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>;
|