From cdca784e6dfa87429bead21d00d7a9d7ad2fc2fd Mon Sep 17 00:00:00 2001 From: Paperclip Date: Mon, 22 Jun 2026 01:05:11 +0000 Subject: [PATCH] fix(opencode-local): pad prompts below the opencode 1.16.2 silent-exit floor (ORA-284) opencode 1.16.2 has a non-deterministic silent-exit mode that fires on short stdin prompts (observed 0-byte / <200-byte run logs, ~32% stall rate on FoundingEngineer wake-payload-only runs). The root cause lives in the opencode binary, not the adapter. As a server-side workaround, ensure every prompt is at least 256 bytes by appending a deterministic Paperclip Runtime Context block when the joined sections fall short. - New exported helper padShortPrompt(input) in execute.ts - New exported constant OPENCODE_PROMPT_MIN_LENGTH_CHARS = 256 - New promptMetrics.promptPadded (0/1) so the server can observe how often the workaround fires - Four new unit tests cover the long-prompt, short-prompt, empty-cwd, and trailing-newline paths Refs: ORA-284, ORA-275, ORA-276 Co-Authored-By: Paperclip --- .../opencode-local/src/server/execute.test.ts | 57 +++++++++++++++++++ .../opencode-local/src/server/execute.ts | 46 ++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/adapters/opencode-local/src/server/execute.test.ts b/packages/adapters/opencode-local/src/server/execute.test.ts index 3b7bcadd..c1badf82 100644 --- a/packages/adapters/opencode-local/src/server/execute.test.ts +++ b/packages/adapters/opencode-local/src/server/execute.test.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it } from "vitest"; import { ensureRemoteOpenCodeModelConfiguredAndAvailable } from "./execute.js"; +import { + OPENCODE_PROMPT_MIN_LENGTH_CHARS, + padShortPrompt, +} from "./execute.js"; describe("ensureRemoteOpenCodeModelConfiguredAndAvailable", () => { afterEach(() => { @@ -60,3 +64,56 @@ describe("ensureRemoteOpenCodeModelConfiguredAndAvailable", () => { ).rejects.toThrow(); }); }); + +describe("padShortPrompt (ORA-284)", () => { + // opencode 1.16.2 has a non-deterministic silent-exit mode that fires on + // short stdin prompts. The pad forces every prompt above the threshold so + // the binary boots into a normal session instead of exiting cleanly with + // 0 bytes of output. + it("returns the input untouched when it is already at or above the floor", () => { + const longPrompt = "x".repeat(OPENCODE_PROMPT_MIN_LENGTH_CHARS); + const out = padShortPrompt({ + prompt: longPrompt, + agentId: "agent-1", + runId: "run-1", + executionCwd: "/tmp", + }); + expect(out).toBe(longPrompt); + }); + + it("pads prompts that are below the floor and embeds run / agent context", () => { + const out = padShortPrompt({ + prompt: "wake payload only", + agentId: "agent-1", + runId: "run-1", + executionCwd: "/tmp/oracle", + }); + expect(out.length).toBeGreaterThanOrEqual(OPENCODE_PROMPT_MIN_LENGTH_CHARS); + expect(out).toContain("run-1"); + expect(out).toContain("agent-1"); + expect(out).toContain("/tmp/oracle"); + expect(out).toContain("ORA-284"); + }); + + it("uses a default cwd marker when executionCwd is empty", () => { + const out = padShortPrompt({ + prompt: "tiny", + agentId: "agent-2", + runId: "run-2", + executionCwd: "", + }); + expect(out).toContain("(default)"); + }); + + it("does not insert a duplicate blank line when the prompt already ends with newline", () => { + const out = padShortPrompt({ + prompt: "tiny\n", + agentId: "agent-3", + runId: "run-3", + executionCwd: "/tmp", + }); + // Pad block already begins with a leading blank line, so we tolerate one + // extra newline rather than concatenating two of them. + expect(out.startsWith("tiny\n\n## Paperclip Runtime Context")).toBe(true); + }); +}); diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 247480b1..8d9e58ea 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -66,6 +66,39 @@ function firstNonEmptyLine(text: string): string { ); } +// ORA-284: opencode 1.16.2 has a non-deterministic silent-exit mode that fires +// on short stdin prompts (observed 0-byte / <200-byte run logs in production, +// ~32% stall rate on FoundingEngineer wake-payload-only runs). The root cause +// lives in the opencode binary, not in the adapter. As a server-side workaround +// we pad any prompt shorter than this floor with a deterministic, content- +// meaningful block drawn from the run context so the prompt is well above the +// observed failure threshold (~50 bytes, with a 4x safety margin). +export const OPENCODE_PROMPT_MIN_LENGTH_CHARS = 256; + +export function padShortPrompt(input: { + prompt: string; + agentId: string; + runId: string; + executionCwd: string; +}): string { + const { prompt, agentId, runId, executionCwd } = input; + if (prompt.length >= OPENCODE_PROMPT_MIN_LENGTH_CHARS) return prompt; + const padBlock = [ + "", + "## Paperclip Runtime Context", + "", + `Run: \`${runId}\``, + `Agent: \`${agentId}\``, + `Cwd: \`${executionCwd || "(default)"}\``, + "", + "You are running as a Paperclip-managed agent. The previous prompt was", + `shorter than the ${OPENCODE_PROMPT_MIN_LENGTH_CHARS}-byte opencode 1.16.2`, + "stability floor (ORA-284), so this metadata block was appended to avoid the", + "binary's silent-exit mode. Continue with the task described above.", + ].join("\n"); + return prompt.endsWith("\n") ? prompt + padBlock : prompt + "\n" + padBlock; +} + function parseModelProvider(model: string | null): string | null { if (!model) return null; const trimmed = model.trim(); @@ -546,20 +579,29 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0; const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); - const prompt = joinPromptSections([ + const joinedSections = joinPromptSections([ instructionsPrefix, renderedBootstrapPrompt, wakePrompt, sessionHandoffNote, renderedPrompt, ]); - const promptMetrics = { + const promptWasPadded = joinedSections.length < OPENCODE_PROMPT_MIN_LENGTH_CHARS; + const prompt = padShortPrompt({ + prompt: joinedSections, + agentId: agent.id, + runId, + executionCwd: effectiveExecutionCwd, + }); + const promptMetrics: Record = { promptChars: prompt.length, instructionsChars: instructionsPrefix.length, bootstrapPromptChars: renderedBootstrapPrompt.length, wakePromptChars: wakePrompt.length, sessionHandoffChars: sessionHandoffNote.length, heartbeatPromptChars: renderedPrompt.length, + // ORA-284: 1 when the short-prompt pad block was appended, 0 otherwise. + promptPadded: promptWasPadded ? 1 : 0, }; // Optional diagnostic: surface OpenCode's own logs on stderr (captured into the