fix(opencode-local): pad prompts below the opencode 1.16.2 silent-exit floor (ORA-284) #1
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<AdapterExec
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 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<string, number> = {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user