diff --git a/docs-site/src/content/docs/guides/chatgpt-desktop.md b/docs-site/src/content/docs/guides/chatgpt-desktop.md
index 2a1b0a9d43..dfa3f62725 100644
--- a/docs-site/src/content/docs/guides/chatgpt-desktop.md
+++ b/docs-site/src/content/docs/guides/chatgpt-desktop.md
@@ -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.
+
## Rewrite boundary
Only `account/rateLimits/updated` notifications and responses whose top-level
@@ -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.
diff --git a/src/chatgpt/app-server-shim/filter.ts b/src/chatgpt/app-server-shim/filter.ts
index f806c028d3..df9e88db7f 100644
--- a/src/chatgpt/app-server-shim/filter.ts
+++ b/src/chatgpt/app-server-shim/filter.ts
@@ -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;
diff --git a/structure/INDEX.md b/structure/INDEX.md
index 66dc5e4dbd..2f7e3eea32 100644
--- a/structure/INDEX.md
+++ b/structure/INDEX.md
@@ -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)
[`clients/claude-desktop.md`](clients/claude-desktop.md) |
| `src/cli.ts` | [`runtime.md`](runtime.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
-| `src/cli/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`clients/integrations.md`](clients/integrations.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
+| `src/cli/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`clients/integrations.md`](clients/integrations.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/client/` | [`runtime.md`](runtime.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md) |
| `src/clients/` | [`clients/integrations.md`](clients/integrations.md) |
-| `src/codex/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`codex-home.md`](codex-home.md)
[`catalog.md`](catalog.md)
[`subagents.md`](subagents.md)
[`transports/responses-failover.md`](transports/responses-failover.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`providers/openai-accounts.md`](providers/openai-accounts.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`dashboard-and-usage.md`](dashboard-and-usage.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
+| `src/codex/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`codex-home.md`](codex-home.md)
[`catalog.md`](catalog.md)
[`subagents.md`](subagents.md)
[`transports/responses-failover.md`](transports/responses-failover.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`providers/openai-accounts.md`](providers/openai-accounts.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`dashboard-and-usage.md`](dashboard-and-usage.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/combos/` | [`runtime.md`](runtime.md)
[`providers-and-adapters.md`](providers-and-adapters.md)
[`providers/jev-decision.md`](providers/jev-decision.md) |
| `src/companion/` | [`overview.md`](overview.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`companion.md`](companion.md) |
| `src/compatibility/` | [`runtime.md`](runtime.md)
[`adapters/compatibility-contracts.md`](adapters/compatibility-contracts.md) |
diff --git a/structure/clients/chatgpt-desktop.md b/structure/clients/chatgpt-desktop.md
index 32305d6d4d..9f3f1328e5 100644
--- a/structure/clients/chatgpt-desktop.md
+++ b/structure/clients/chatgpt-desktop.md
@@ -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,
@@ -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
@@ -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.
diff --git a/structure/manifest.json b/structure/manifest.json
index 46faf49ff6..48d655727f 100644
--- a/structure/manifest.json
+++ b/structure/manifest.json
@@ -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/"
]
},
{
diff --git a/tests/clients/desktop-app-server-shim.test.ts b/tests/clients/desktop-app-server-shim.test.ts
index 7d2f554c8c..d0974969cb 100644
--- a/tests/clients/desktop-app-server-shim.test.ts
+++ b/tests/clients/desktop-app-server-shim.test.ts
@@ -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);
});
});