fix(runner): retain closed OpenCode finish diagnostics

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-06 00:12:00 -06:00
1 parent f5692c57da
commit e72803a2b6
3 files changed
+117 -1

No files matched your search

@@ -20,6 +20,7 @@ import {
shouldForwardOpenCodeProxyItem,
} from "./opencode-proxy-events.js";
import { enqueueOpenCodeProxyInput, openCodeProxyCompletionFeedback } from "./opencode-proxy-input.js";
import { createOpenCodeFinishDiagnosticForwarder } from "./opencode-proxy-finish-diagnostics.js";
import { createOpenCodeProxyOutput } from "./opencode-proxy-output.js";
import {
assertOpenCodeProxyCollaborationMode,
@@ -113,6 +114,7 @@ async function open(
JSON.parse(readFileSync(runtimeContextPath, "utf8")),
)
: null;
const finishDiagnostics = createOpenCodeFinishDiagnosticForwarder(send);
driver = new OpenCodeServerDriver({
model,
permissionMode: parseOpenCodeProxyPermissionMode(
@@ -146,7 +148,10 @@ async function open(
tool: call.tool,
arguments: call.arguments,
}),
onDiagnostic: (message) => process.stderr.write(`[opencode] ${message}\n`),
onDiagnostic: (message) => {
process.stderr.write(`[opencode] ${message}\n`);
finishDiagnostics.diagnostic(message);
},
// runnerd gives this proxy its own process group. Keep `opencode serve` in
// that same group so runnerd's TERM/KILL fallback cannot orphan it.
isolateProcessGroup: false,
@@ -175,6 +180,7 @@ async function open(
workingDirectory: cwd,
});
}
finishDiagnostics.bind(session.ids().driverSessionId);
eventPump = pumpEvents(session);
void eventPump.catch((error) => failProxy(error));
return threadResponse(
@@ -0,0 +1,68 @@
import { expect, it } from "vitest";
import { stripTypeScriptTypes } from "node:module";
import { readFile } from "node:fs/promises";
import { createOpenCodeFinishDiagnosticForwarder } from "./opencode-proxy-finish-diagnostics.js";
import { createOpenCodeProxyOutput } from "./opencode-proxy-output.js";
const offered = { schema: "paperclip.opencode.finish-input-diagnostic/v1", stage: "offered", schemaSha256: "a".repeat(64) };
const rejected = { ...offered, stage: "input_rejected", callIdSha256: "b".repeat(64), validationCategory: "continuation", rootShape: "object", continuationShape: "string" };
it("retains closed diagnostics through normal warning output before an empty-response failure", async () => {
const frames: unknown[] = [], output = createOpenCodeProxyOutput(frame => frames.push(frame));
const forward = createOpenCodeFinishDiagnosticForwarder(output.send);
forward.diagnostic(JSON.stringify(offered));
expect(frames).toEqual([]);
expect(forward.bind("actual-session")).toBe(true);
forward.diagnostic(JSON.stringify(rejected));
output.send({ method: "turn/completed", params: { threadId: "actual-session", turn: { status: "failed", error: { code: "opencode_empty_response" } } } });
expect(frames).toEqual([
{ method: "warning", params: { threadId: "actual-session", message: JSON.stringify(offered) } },
{ method: "warning", params: { threadId: "actual-session", message: JSON.stringify(rejected) } },
expect.objectContaining({ method: "turn/completed" }),
]);
const source = await readFile(new URL("./opencode-app-server-proxy.ts", import.meta.url), "utf8");
expect(source).toContain("finishDiagnostics.diagnostic(message)");
expect(source).toContain("finishDiagnostics.bind(session.ids().driverSessionId)");
expect(source).toContain('process.stderr.write(`[opencode] ${message}\\n`)');
});
it("rejects unbound rejections, foreign binding/schema, unknown fields and arbitrary text", () => {
const frames: unknown[] = [], forward = createOpenCodeFinishDiagnosticForwarder(frame => frames.push(frame));
forward.diagnostic(JSON.stringify(rejected));
forward.diagnostic("arbitrary private text");
forward.diagnostic(JSON.stringify({ ...offered, threadId: "foreign" }));
expect(frames).toEqual([]);
forward.diagnostic(JSON.stringify(offered));
expect(forward.bind("actual-session")).toBe(true);
expect(forward.bind("foreign-session")).toBe(false);
for (const invalid of [{ ...rejected, schemaSha256: "c".repeat(64) }, { ...rejected, input: "private" }, { ...rejected, rootShape: {} }, { ...rejected, rootShape: ["object"] }, { ...rejected, continuationShape: "invalid" }]) forward.diagnostic(JSON.stringify(invalid));
expect(frames).toHaveLength(1);
for (let i = 0; i < 12; i++) forward.diagnostic(JSON.stringify(rejected));
forward.diagnostic(JSON.stringify(offered));
expect(frames).toHaveLength(9);
expect(JSON.stringify(frames)).not.toMatch(/private|foreign/);
});
it("forwards from the actual proxy open callback instead of losing normal semantic failure diagnostics to stderr", async () => {
const source=await readFile(new URL("./opencode-app-server-proxy.ts",import.meta.url),"utf8");
const body=stripTypeScriptTypes(source.slice(source.indexOf("async function open("),source.indexOf("function threadResponse(")));
const frames:unknown[]=[],stderr:string[]=[],output=createOpenCodeProxyOutput(frame=>frames.push(frame));
let diagnostic:(message:string)=>void=()=>{};
class Driver {
constructor(options:{onDiagnostic:(message:string)=>void}) { diagnostic=options.onDiagnostic; }
async openSession() { diagnostic(JSON.stringify(offered));return {ids:()=>({driverSessionId:"actual-session",providerSessionId:"actual-session"})}; }
}
const invoke=new Function("OpenCodeServerDriver","createOpenCodeFinishDiagnosticForwarder","send","process",`
let session=null,driver,eventPump,cwd,activeModel;
const activeRunId="run",launchBinding={},resolve=x=>x,text=(x,f="")=>typeof x==="string"?x:f,record=x=>x;
const runtimeDirectory=()=>"/synthetic",withoutAmbientOpenCodeCommand=()=>({}),parseOpenCodeProxyPermissionMode=()=>"allow",openCodeProxyTaskEnvelope=()=>({});
const openCodeProxyCompletionFeedback=()=>()=>{},requestController=()=>{},pumpEvents=async()=>{},failProxy=()=>{},threadResponse=id=>({thread:{id}});
${body}
return open({model:"provider/model",cwd:"/synthetic"},false);
`);
await invoke(Driver,createOpenCodeFinishDiagnosticForwarder,output.send,{env:{},cwd:()=>"/synthetic",stderr:{write:(message:string)=>stderr.push(message)}});
diagnostic(JSON.stringify(rejected));
diagnostic("unrelated private diagnostic");
output.send({method:"turn/completed",params:{threadId:"actual-session",turn:{status:"failed",error:{code:"opencode_empty_response"}}}});
expect(frames).toHaveLength(3);
expect(frames.slice(0,2)).toEqual([offered,rejected].map(value=>({method:"warning",params:{threadId:"actual-session",message:JSON.stringify(value)}})));
expect(stderr).toHaveLength(3);
expect(JSON.stringify(frames)).not.toContain("unrelated private diagnostic");
});
@@ -0,0 +1,42 @@
type Warning = { method: "warning"; params: { threadId: string; message: string } };
export function createOpenCodeFinishDiagnosticForwarder(send: (frame: Warning) => void) {
let threadId: string | undefined;
let offered: Record<string, unknown> | undefined;
let rejections = 0;
const emit = (value: Record<string, unknown>) => {
if (!threadId) return;
try { send({ method: "warning", params: { threadId, message: JSON.stringify(value) } }); } catch {}
};
return {
bind(id: string) {
if (!id || id.length > 512 || threadId) return false;
threadId = id;
if (offered) emit(offered);
return true;
},
diagnostic(message: string) {
if (message.length > 2048) return;
let value: Record<string, unknown>;
try { value = JSON.parse(message); } catch { return; }
if (!value || typeof value !== "object" || Array.isArray(value)
|| value.schema !== "paperclip.opencode.finish-input-diagnostic/v1"
|| typeof value.schemaSha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.schemaSha256)) return;
const keys = Object.keys(value).sort().join(",");
if (value.stage === "offered") {
if (offered || keys !== "schema,schemaSha256,stage") return;
offered = value;
emit(value);
} else if (value.stage === "input_rejected") {
if (!threadId || !offered || rejections >= 8 || value.schemaSha256 !== offered.schemaSha256
|| keys !== "callIdSha256,continuationShape,rootShape,schema,schemaSha256,stage,validationCategory"
|| typeof value.callIdSha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.callIdSha256)
|| typeof value.validationCategory !== "string" || typeof value.rootShape !== "string" || typeof value.continuationShape !== "string"
|| !["root", "continuation", "other"].includes(String(value.validationCategory))
|| !["object", "array", "string", "number", "boolean", "null", "omitted"].includes(String(value.rootShape))
|| !["object", "array", "string", "number", "boolean", "null", "omitted"].includes(String(value.continuationShape))) return;
rejections += 1;
emit(value);
}
},
};
}