Repository navigation
Add durable jobs, modern MCP support, and reliability improvements - #28
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5850395ac2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }; | ||
| export const runShape = { | ||
| ...dispatchShape, | ||
| maxDurationSeconds: z.number().positive().max(3600).optional() |
There was a problem hiding this comment.
Permit zero-duration runs in the shared schema
Allow maxDurationSeconds: 0 here (for example, with nonnegative()), because opencode_run explicitly implements zero as “dispatch and return immediately” in waitForSnapshot and the run handler. The previous tool schema also accepted zero, but requests now fail validation before reaching those branches; the regression test calls the handler directly and therefore does not catch the actual MCP rejection.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0d044f316
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| messageId: z.string().optional(), directory: z.string().optional(), status: jobStatusSchema, | ||
| result: z.unknown().optional(), error: z.unknown().optional() }).loose(); | ||
| } | ||
| if (name === "opencode_session_create") return z.object({ ...common, sessionId: z.string(), session: z.unknown() }).loose(); |
There was a problem hiding this comment.
Allow session creation error outputs in the schema
When /session fails, opencode_session_create returns toolError(e), so the wrapper produces structured content containing only text and isError. This output schema nevertheless requires both sessionId and session, causing SDK v2 output validation to reject the intended tool-error result and surface a protocol-level validation error instead of the actionable backend failure. Make these fields optional or include suitable values only on successful results.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0de817a919
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (response.action !== "accept") return [{ id: item.id, kind: item.kind, | ||
| ...(item.kind === "permission" ? { reply: "reject" as const } : { reject: true }) }]; |
There was a problem hiding this comment.
Preserve form cancellation instead of rejecting input
When a form-capable client returns the distinct action: "cancel" result, this branch converts it into an explicit OpenCode rejection just like decline. opencode_job_input and tasks/update then submit that rejection, so merely closing or cancelling the form consumes the pending permission/question and can alter the running task despite the user never choosing to reject it. Handle cancel without producing an input answer (or propagate cancellation), and reserve backend rejection for decline.
Useful? React with 👍 / 👎.
| if (owner.host !== hostname() || !Number.isInteger(owner.pid) || owner.pid <= 0) return; | ||
| try { process.kill(owner.pid, 0); return; } | ||
| catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") return; } |
There was a problem hiding this comment.
Identify lock owners beyond their reusable PID
After an MCP process dies while holding a job lock, the OS can reuse its PID on the same host, especially after a reboot. In that case process.kill(pid, 0) succeeds for an unrelated process and this code permanently treats the stale lock as live, causing every get, update, or cancel operation to time out until that process exits or the job expires. Persist and verify a process-start/boot identity, or otherwise provide a safe stale-lock recovery criterion.
Useful? React with 👍 / 👎.
| if (data.reduce((sum, item) => sum + item.length, 0) + line.length > 1_000_000) { | ||
| throw new Error("SSE event exceeds the 1MB limit"); | ||
| } | ||
| if (line === "") { | ||
| if (data.length > 0) yield { event: event || "message", data: data.join("\n") }; | ||
| event = ""; | ||
| data = []; | ||
| } else if (!line.startsWith(":")) { | ||
| const separator = line.indexOf(":"); | ||
| const field = separator < 0 ? line : line.slice(0, separator); | ||
| let content = separator < 0 ? "" : line.slice(separator + 1); | ||
| if (content.startsWith(" ")) content = content.slice(1); | ||
| if (field === "event") event = content; | ||
| if (field === "data") data.push(content); |
There was a problem hiding this comment.
Bound SSE events by accumulated framing size
If an SSE peer emits many empty or tiny data: lines without a blank event terminator, every line appends an array entry while the 1 MB checks count only content characters, not delimiters or array overhead; empty lines therefore bypass the limit entirely. Recomputing the total with reduce for every line also makes a many-line event quadratic to parse. Track a cumulative event size (including framing or a line-count bound) so a malformed remote stream cannot grow memory and CPU without hitting the advertised cap.
Useful? React with 👍 / 👎.
Summary
Async work could be submitted twice after a lost response, or reported complete from an idle session before the new turn finished. This change gives each dispatch a durable job/message identity, preserves uncertain outcomes without resubmitting, and reports pending input, completion, failure, and cancellation from the correlated turn.
This PR includes the requested audit fixes and recommended features together:
opencode_run,tasks/get,tasks/update,tasks/cancel) through a small stdio extension adapter. Core protocol handling uses SDK v2; legacy stdio clients retain the ordinary workflows.Compatibility and limits
Validation
git diff --checkpassed.npm audit: 0 vulnerabilities.f0d044f. The generated-document check accepts both LF and CRLF checkouts while still rejecting stale content.Additional live inference verification
The Codex app-server MCP client loaded the local build and delegated real inference to OpenCode 1.18.31 using
opencode/big-pickle. Thefire→waitflow returned an exact expected reply,runreturned the expected arithmetic result, and a separate coding task created a JavaScript module whose three assertions passed. Test sessions, files, and job records were removed. This verifies the real client/bridge/backend/model path for that provider; it does not claim every provider credential or model is usable.OpenAI live verification (2026-09-15)
The local build was available as MCP tools directly in the active Codex conversation. With OpenCode 1.18.31 and the authenticated OpenAI
gpt-5.6-lunamodel:opencode_askreturned the exact expected reply.opencode_fire→opencode_waitcompleted with the expected arithmetic result.opencode_runcreated a JavaScript module in a disposable project; all three executed assertions passed.ask,fire→wait,run, and recovery of an existing structured-output job all returned the expected validated JSON.All six test sessions, both durable job records, and the disposable project were removed. These checks verify real authenticated inference and file editing, without claiming every model or output mode works.
Structured-output fixes
A deny-all OpenCode permission policy hid its
StructuredOutputtool, causing the initial live schema test to stall. The documentation and tool descriptions now explain the explicit permission required; the MCP never grants it automatically.Two adapter fixes address successful structured answers: the observer accepts OpenCode's completed structured result even when its finish reason is
tool-calls, and readable output shows the validated JSON instead of the tool acknowledgement. OpenCode 1.18.31 also fails to serialize history containing persisted response formats. For that specific HTTP 400, observation with a known message ID retries a latest-message-only read and verifies parent correlation before accepting completion. Incomplete history remains unknown without exposing pending input; ownership checks for cancellation remain conservative.Regression coverage includes stale/unrelated turns, busy sessions, unavailable history, exact error matching, readable JSON, and no resubmission. Four live checks passed through a fresh stdio MCP process using OpenAI
gpt-5.6-luna; all owned test sessions, jobs, and files were removed. Full coverage suite: 503 passed, 3 skipped.