Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs-site/src/content/docs/guides/chatgpt-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Restore leaves the config flag as configured. Set `chatgptDesktop.appServerShim`
to `false` or remove it to disable future explicit shim launches. Normal launches
from Dock or Spotlight do not apply the shim automatically.

If ChatGPT is not installed (no `com.openai.codex` bundle is found), `restore`
only removes the launcher: it cannot relaunch anything and exits with an error.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## Rewrite boundary

Only `account/rateLimits/updated` notifications and responses whose top-level
Expand Down Expand Up @@ -82,9 +85,13 @@ the running ChatGPT bundle process carries the expected launcher override.

## Failure behavior and known limits

A failed precondition or a failed self-test runs the original binary with untouched stdout.
When the platform is not macOS, the OpenCodex runtime is missing, or the filter
self-test fails, the launcher runs the original binary with untouched stdout. A
missing bundled app-server binary is the exception: there is nothing to fall back
to, so the launcher exits with an error (see below).
A filter that passes the self-test and then dies mid-session closes the pipe.
Expected (not yet validated against the bundled app-server): the server gets SIGPIPE or a write error and Desktop respawns it through the same launcher.
What the bundled app-server does after that has not been verified; it may get
SIGPIPE or a write error and be respawned by Desktop through the same launcher.
The filter's passthrough mode limits this to an exit/crash case: a rewrite exception
passes its line through, and an unexpected rewrite-machinery failure switches the
remaining stream to raw bytes.
Expand Down
9 changes: 7 additions & 2 deletions src/chatgpt/app-server-shim/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,13 @@ export function createRpcLineFilter(
let heldLength = previousLength;
for (let i = chunk.indexOf(NEWLINE, start); i !== -1; i = chunk.indexOf(NEWLINE, start)) {
const tail = chunk.subarray(start, i + 1);
const whole = heldLength === 0 ? tail : concatParts([...held, tail], heldLength + tail.length);
out.push(emit(whole.subarray(0, whole.length - 1), whole, true));
if (heldLength + tail.length - 1 > maxLineBytes) {
// A complete line over the cap is passed through as it arrived, never joined or parsed.
out.push(...held, tail);
} else {
const whole = heldLength === 0 ? tail : concatParts([...held, tail], heldLength + tail.length);
out.push(emit(whole.subarray(0, whole.length - 1), whole, true));
}
held = [];
heldLength = 0;
start = i + 1;
Expand Down
4 changes: 2 additions & 2 deletions structure/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ A source area can be described by more than one doc, because these docs are orga
| `src/chatgpt/` | [`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md) |
| `src/claude/` | [`runtime.md`](runtime.md)<br>[`clients/claude-desktop.md`](clients/claude-desktop.md) |
| `src/cli.ts` | [`runtime.md`](runtime.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/cli/` | [`runtime.md`](runtime.md)<br>[`config.md`](config.md)<br>[`clients/integrations.md`](clients/integrations.md)<br>[`clients/claude-desktop.md`](clients/claude-desktop.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/cli/` | [`runtime.md`](runtime.md)<br>[`config.md`](config.md)<br>[`clients/integrations.md`](clients/integrations.md)<br>[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)<br>[`clients/claude-desktop.md`](clients/claude-desktop.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/client/` | [`runtime.md`](runtime.md)<br>[`clients/claude-desktop.md`](clients/claude-desktop.md) |
| `src/clients/` | [`clients/integrations.md`](clients/integrations.md) |
| `src/codex/` | [`runtime.md`](runtime.md)<br>[`config.md`](config.md)<br>[`codex-home.md`](codex-home.md)<br>[`catalog.md`](catalog.md)<br>[`subagents.md`](subagents.md)<br>[`transports/responses-failover.md`](transports/responses-failover.md)<br>[`providers/openai-tiers.md`](providers/openai-tiers.md)<br>[`providers/openai-accounts.md`](providers/openai-accounts.md)<br>[`gui-and-management-api.md`](gui-and-management-api.md)<br>[`dashboard-and-usage.md`](dashboard-and-usage.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/codex/` | [`runtime.md`](runtime.md)<br>[`config.md`](config.md)<br>[`codex-home.md`](codex-home.md)<br>[`catalog.md`](catalog.md)<br>[`subagents.md`](subagents.md)<br>[`transports/responses-failover.md`](transports/responses-failover.md)<br>[`providers/openai-tiers.md`](providers/openai-tiers.md)<br>[`providers/openai-accounts.md`](providers/openai-accounts.md)<br>[`gui-and-management-api.md`](gui-and-management-api.md)<br>[`dashboard-and-usage.md`](dashboard-and-usage.md)<br>[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/combos/` | [`runtime.md`](runtime.md)<br>[`providers-and-adapters.md`](providers-and-adapters.md)<br>[`providers/jev-decision.md`](providers/jev-decision.md) |
| `src/companion/` | [`overview.md`](overview.md)<br>[`gui-and-management-api.md`](gui-and-management-api.md)<br>[`companion.md`](companion.md) |
| `src/compatibility/` | [`runtime.md`](runtime.md)<br>[`adapters/compatibility-contracts.md`](adapters/compatibility-contracts.md) |
Expand Down
19 changes: 11 additions & 8 deletions structure/clients/chatgpt-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ then replaces itself with the bundled app-server using shell exec. Only stdout
passes through the filter. Stdin, stderr, process identity and the real server's
exit status retain the direct app/server relationship.

A failed precondition or a failed self-test runs the original binary with untouched stdout.
A filter that passes the self-test and then dies mid-session closes the pipe.
Expected (not yet validated against the bundled app-server): the server gets SIGPIPE or a write error and Desktop respawns it through the same launcher.
The filter's passthrough mode limits this to an exit/crash case.
When the platform is not macOS, the runtime is missing, or the filter self-test fails,
the launcher runs the original binary with untouched stdout. A missing bundled binary
exits 127 instead (see below). A filter that passes the self-test and then exits
mid-session closes the server's stdout pipe; the filter's passthrough mode limits this
to an exit/crash case.

The pure gate rewrite changes known plain-quota fields only in eligible JSON-RPC
rate-limit notifications and top-level rate-limit results. Workspace, credit,
Expand All @@ -34,9 +35,9 @@ machinery preserves buffered bytes and switches the rest of the stream to raw
passthrough. Output-write failures propagate; they are not rewrite failures.
A partial line is held as a list of chunks and joined once at its newline, so a long
line split across many pipe reads costs linear copying.
A line longer than `MAX_FILTERED_LINE_BYTES` (8 MiB) is never buffered or parsed: the
held bytes and the rest of that line stream through raw, and filtering resumes after
its newline.
A line longer than `MAX_FILTERED_LINE_BYTES` (8 MiB) is never joined or parsed, whether
it arrives across many chunks or whole in one: its bytes stream through raw, and
filtering resumes after its newline.

The app is discovered and confirmed by bundle identifier through
`darwinDesktopAppAdapter.discover` (`src/codex/desktop-app/darwin.ts`). Launch derives
Expand All @@ -49,7 +50,9 @@ rename (never through a symbolic link), quits the bundle by id, waits for this u
instance to exit, then opens the same bundle path with the launcher in CODEX_CLI_PATH.
The launcher itself exits 127 with a stderr hint when the recorded binary is gone.
Restore relaunches without
that override and removes the launcher only after open succeeds. Status reports
that override and removes the launcher only after open succeeds; when no
`com.openai.codex` bundle is found it removes the launcher, relaunches nothing and
exits 1. Status reports
the experimental flag, launcher presence, and the verified bundle process's override
without printing its environment. Other platforms reject all three operations.

Expand Down
4 changes: 3 additions & 1 deletion structure/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,9 @@
"title": "ChatGPT Desktop",
"scope": "Experimental macOS app-server stdout shim, opt-in launch, restore and failure boundaries.",
"documents": [
"src/chatgpt/"
"src/chatgpt/",
"src/cli/",
"src/codex/"
]
},
{
Expand Down
7 changes: 7 additions & 0 deletions tests/clients/desktop-app-server-shim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ describe("app-server line filter", () => {
expect(out[0]).toBe(oversized);
expect(JSON.parse(out[1]!).result.ordinaryUsageAllowed).toBe(true);
expect(seen.some(line => line.includes("padding"))).toBe(false);

// The same oversized line arriving whole, newline included, in one chunk is not parsed either.
seen.length = 0;
const single = collect([enc(`${oversized}\n${rpcResult(EXHAUSTED_RATE_LIMITS)}\n`)], createRpcLineFilter(rewrite, 1024)).split("\n");
expect(single[0]).toBe(oversized);
expect(JSON.parse(single[1]!).result.ordinaryUsageAllowed).toBe(true);
expect(seen.some(line => line.includes("padding"))).toBe(false);
});
});

Expand Down
Loading