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 { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { ensureRemoteOpenCodeModelConfiguredAndAvailable } from "./execute.js";
|
import { ensureRemoteOpenCodeModelConfiguredAndAvailable } from "./execute.js";
|
||||||
|
import {
|
||||||
|
OPENCODE_PROMPT_MIN_LENGTH_CHARS,
|
||||||
|
padShortPrompt,
|
||||||
|
} from "./execute.js";
|
||||||
|
|
||||||
describe("ensureRemoteOpenCodeModelConfiguredAndAvailable", () => {
|
describe("ensureRemoteOpenCodeModelConfiguredAndAvailable", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -60,3 +64,56 @@ describe("ensureRemoteOpenCodeModelConfiguredAndAvailable", () => {
|
|||||||
).rejects.toThrow();
|
).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 {
|
function parseModelProvider(model: string | null): string | null {
|
||||||
if (!model) return null;
|
if (!model) return null;
|
||||||
const trimmed = model.trim();
|
const trimmed = model.trim();
|
||||||
@@ -546,20 +579,29 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||||
const prompt = joinPromptSections([
|
const joinedSections = joinPromptSections([
|
||||||
instructionsPrefix,
|
instructionsPrefix,
|
||||||
renderedBootstrapPrompt,
|
renderedBootstrapPrompt,
|
||||||
wakePrompt,
|
wakePrompt,
|
||||||
sessionHandoffNote,
|
sessionHandoffNote,
|
||||||
renderedPrompt,
|
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,
|
promptChars: prompt.length,
|
||||||
instructionsChars: instructionsPrefix.length,
|
instructionsChars: instructionsPrefix.length,
|
||||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||||
wakePromptChars: wakePrompt.length,
|
wakePromptChars: wakePrompt.length,
|
||||||
sessionHandoffChars: sessionHandoffNote.length,
|
sessionHandoffChars: sessionHandoffNote.length,
|
||||||
heartbeatPromptChars: renderedPrompt.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
|
// Optional diagnostic: surface OpenCode's own logs on stderr (captured into the
|
||||||
|
|||||||
Reference in New Issue
Block a user