Skip to content

Add durable jobs, modern MCP support, and reliability improvements - #28

Merged
AlaeddineMessadi merged 4 commits into
mainfrom
codex/reliability-and-mcp-tasks
Sep 16, 2026
Merged

AlaeddineMessadi merged 4 commits into
mainfrom
codex/reliability-and-mcp-tasks

Conversation

@AlaeddineMessadi

@AlaeddineMessadi AlaeddineMessadi commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

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:

  • Durable scoped jobs with restart recovery, result retention, get/list/cancel/input tools, and cross-process reply deduplication.
  • July 2026 MCP Tasks (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.
  • Explicit question/permission handling and modern input forms, structured tool results/output schemas, OpenCode JSON-schema response formats, and full/essential tool profiles (87/25 tools).
  • Shared HTTP deadlines/cancellation, read-only retries, safe SSE cleanup/framing/project scope, and cheaper progress checks without full diffs.
  • Configuration/provider redaction, valid truncated JSON, corrected symbol/OAuth contracts, complete behavior annotations, and four project/session resource templates.
  • A smoke runner that only mutates its own disposable fixture/session, generated tool documentation, refreshed client/release guidance, and CI documentation/package checks.

Compatibility and limits

  • Requires Node.js 22+ and moves runtime MCP dependencies to the split SDK v2 packages. Release metadata is prepared for 3.0.0, including migration notes and the Node.js 22 minimum. npm publication is performed separately after merge.
  • Native Tasks and multi-round input require compatible July 2026 clients. Existing clients can use fire/check/wait and explicit input arguments. Task status is polled; push notifications are not advertised.
  • Job records default to 24-hour retention. They recover observation/results, not stopped inference. Keep OpenCode externally managed if execution must survive an MCP disconnect.
  • OpenCode abort is session-wide. Cancellation rejects a known newer turn, but an external submission can race the check; dedicated sessions are the default. Native cancellation acknowledgements signal intent, with eventual state available through polling.

Validation

  • Build/type checking, generated-doc check, and git diff --check passed.
  • Full coverage run: 503 passed, 3 skipped across 17 test files. Includes modern and legacy child-process interoperability, restart recovery, explicit input round trips, two-process response deduplication, cancellation, deadline, redaction, and smoke ownership regressions.
  • npm audit: 0 vulnerabilities.
  • Isolated live OpenCode 1.18.31 smoke runs, both without auth and with synthetic Basic auth: 24 PASS, 64 intentional SKIP, 0 FAIL each. Those isolated API checks did not perform inference; both servers and disposable data were cleaned up.
  • Actual npm archive installed with production dependencies only; both protocol generations passed discovery, fixture-backed health, and clean stdio shutdown.
  • CI passed on Linux and Windows with Node 22 and 24 at commit 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. The fire → wait flow returned an exact expected reply, run returned 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-luna model:

  • opencode_ask returned the exact expected reply.
  • opencode_fire → opencode_wait completed with the expected arithmetic result.
  • opencode_run created a JavaScript module in a disposable project; all three executed assertions passed.
  • JSON-schema output now passes with the rebuilt MCP: synchronous 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 StructuredOutput tool, 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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-16T09:32:46.690308Z 0de817a New commits
🔒 Security Review ✅ Completed 2026-09-14T23:38:24.653585Z 5850395 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/job-contract.ts
};
export const runShape = {
...dispatchShape,
maxDurationSeconds: z.number().positive().max(3600).optional()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/mcp-server.ts
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@AlaeddineMessadi
AlaeddineMessadi merged commit 6f1f62f into main Sep 16, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/task-input.ts
Comment on lines +55 to +56
if (response.action !== "accept") return [{ id: item.id, kind: item.kind,
...(item.kind === "permission" ? { reply: "reject" as const } : { reject: true }) }];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/jobs.ts
Comment on lines +327 to +329
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; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/client.ts
Comment on lines +250 to +263
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant