Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ Windows では、#32111 クラッシュを回避するために、opencodex は
未検証の領域 — 私たちがテストしていないランタイムで opencodex を実行しています。自己責任で。サービスのインストールにとって重要: オーバーライドは、サービスの開始時ではなく、**サービス アーティファクトの生成時に**読み込まれます。環境変数を設定し、同じシェルから `ocx service repair` を再実行すると、パスが永続サービス定義に組み込まれます。 env を設定するだけでは、すでにインストールされているサービスには何も影響しません。

3. **`streamMode: "eager-relay"` を使用して有界リレーにオプトインします。** 2 つの方法:
`config.json` を編集する (`"streamMode": "eager-relay"` を追加する) か、管理 API を呼び出します。`PUT /api/settings` と `{"streamMode":"eager-relay"}` は、再起動せずに新しいターンに適用されます。 **クラッシュのリスク警告:** Bun 1.3.14 では、#32111 の影響を受けるストリーム形状が使用されており、ストリームの途中でプロセスがクラッシュする可能性があります (Windows に限らず、どの OS でも)。サービス マネージャーはサービスを再起動しますが、実行中のリクエストは失敗します。 `"legacy-tee"` は現在のデフォルトを固定します。 Windows では、`"auto"` (デフォルト) によりランタイム ゲートが決定します。 macOS では、`"auto"` は常に T 上にあります。明示的な `"eager-relay"` はオプトインです
`config.json` を編集する (`"streamMode": "eager-relay"` を追加する) か、管理 API を呼び出します。`PUT /api/settings` と `{"streamMode":"eager-relay"}` は、再起動せずに新しいターンに適用されます。 **クラッシュのリスク警告:** Bun 1.3.14 では、#32111 の影響を受けるストリーム形状が使用されており、ストリームの途中でプロセスがクラッシュする可能性があります (Windows に限らず、どの OS でも)。サービス マネージャーはサービスを再起動しますが、実行中のリクエストは失敗します。通常のストリームでは `"legacy-tee"` が現在のデフォルトを固定します。ただし、クライアント向け payload rewrite が必要なリクエストは、tee/rewrite チェーンの停止を避けるため Windows と macOS で自動的に有界 single-reader relay を使い、この安全上の例外は `streamMode` では上書きできません。Windows の通常ストリームでは `"auto"` (デフォルト) がランタイムゲートに従います。macOS の通常の `"auto"` トラフィックは tee のままで、明示的な `"eager-relay"` がオプトインです

これらのいずれかを実際の Windows ワークロードで試した場合は、[#314](https://github.com/lidge-jun/opencodex/issues/314) の `ocx doctor` メモリ セクションの前後を報告してください。これがまさにこの軽減策が待っている検証です。
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ Windows에서는 opencodex가 #32111 충돌을 피하기 위해 스트리밍 응

2. **`OPENCODEX_BUN_PATH`로 신뢰하는 Bun 런타임을 사용합니다.** 이 경로는 검증되지 않은 영역입니다. opencodex를 아직 테스트하지 않은 런타임에서 실행하는 것이므로, 위험은 사용자에게 있습니다. 서비스 설치에서 특히 중요한 점은 이 override가 서비스 시작 시가 아니라 **서비스 아티팩트를 생성할 때** 읽힌다는 것입니다. 환경 변수를 설정한 뒤, 같은 셸에서 `ocx service repair`를 다시 실행해야 경로가 영구적인 서비스 정의에 반영됩니다. 환경 변수만 설정하면 이미 설치된 서비스에는 아무 영향이 없습니다.

3. **`streamMode: "eager-relay"`로 bounded relay를 opt-in합니다.** 방법은 두 가지입니다. `config.json`을 수정해 `"streamMode": "eager-relay"`를 추가하거나, 관리 API에 `PUT /api/settings`와 `{"streamMode":"eager-relay"}`를 보내 새 턴에 재시작 없이 적용합니다. **충돌 위험 경고:** Bun 1.3.14에서는 이 방식이 #32111의 영향을 받는 스트림 형태를 사용하므로, 어떤 OS에서든 스트림 중간에 프로세스가 충돌할 수 있습니다. 서비스 관리자가 다시 시작하겠지만, 진행 중이던 요청은 실패합니다. `"legacy-tee"` 현재 기본값을 고정합니다. Windows에서는 `"auto"`(기본값)가 런타임 게이트 판단을 따르게 합니다. macOS에서는 `"auto"`가 항상 tee를 유지하고, 명시적 `"eager-relay"`만 opt-in입니다.
3. **`streamMode: "eager-relay"`로 bounded relay를 opt-in합니다.** 방법은 두 가지입니다. `config.json`을 수정해 `"streamMode": "eager-relay"`를 추가하거나, 관리 API에 `PUT /api/settings`와 `{"streamMode":"eager-relay"}`를 보내 새 턴에 재시작 없이 적용합니다. **충돌 위험 경고:** Bun 1.3.14에서는 이 방식이 #32111의 영향을 받는 스트림 형태를 사용하므로, 어떤 OS에서든 스트림 중간에 프로세스가 충돌할 수 있습니다. 서비스 관리자가 다시 시작하겠지만, 진행 중이던 요청은 실패합니다. 일반 스트림에서는 `"legacy-tee"` 현재 기본 경로를 고정합니다. 다만 클라이언트용 payload rewrite가 필요한 요청은 tee/rewrite 체인이 멈출 수 있으므로 Windows와 macOS에서 자동으로 bounded single-reader relay를 사용하며, `streamMode`는 이 전송 안전 예외를 덮어쓰지 않습니다. Windows의 일반 스트림에서는 `"auto"`(기본값)가 런타임 게이트 판단을 따릅니다. macOS의 일반 `"auto"` 트래픽은 tee를 유지하고, 명시적 `"eager-relay"`만 opt-in입니다.

이 중 어떤 방법이든 실제 Windows 워크로드에 적용해 보셨다면, 변경 전후의 `ocx doctor` 메모리 섹션을 [#314](https://github.com/lidge-jun/opencodex/issues/314)에 남겨 주세요. 이것이 바로 이 완화책이 기다리고 있는 검증입니다.
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,12 @@ opencodex поставляет рантайм Bun (сейчас это **1.3.14*
перезапуска. **Предупреждение о риске crash:** на Bun 1.3.14 это использует форму streaming,
затронутую #32111, и процесс может упасть прямо посреди потока (на любой ОС, не только на
Windows). Service manager перезапустит его, но все запросы в полёте потерпят неудачу.
`"legacy-tee"` жёстко фиксирует текущий путь по умолчанию. На Windows `"auto"` (по умолчанию)
позволяет рантайму выбрать путь самому. На macOS `"auto"` всегда остаётся на tee; явный
`"eager-relay"` — это opt-in.
Для обычных потоков `"legacy-tee"` жёстко фиксирует текущий путь по умолчанию.
Запросы, которым нужен клиентский payload rewrite, автоматически используют bounded
single-reader relay на Windows и macOS, потому что цепочка tee/rewrite может зависнуть;
`streamMode` не отменяет это исключение безопасности транспорта. На Windows `"auto"`
(по умолчанию) передаёт выбор runtime-gate для обычных потоков, а на macOS обычный
`"auto"` остаётся на tee; явный `"eager-relay"` — это opt-in.

Если вы попробуете любой из этих вариантов на реальной Windows-нагрузке, пожалуйста, пришлите
разделы памяти из `ocx doctor` до и после в
Expand Down
10 changes: 7 additions & 3 deletions docs-site/src/content/docs/troubleshooting/windows-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,13 @@ restart it.
applies to new turns without a restart. **Crash risk warning:** on Bun
1.3.14 this uses the stream shape affected by #32111, which can crash the
process mid-stream (on any OS, not just Windows). The service manager will
restart it, but in-flight requests fail. `"legacy-tee"` pins the current
default. On Windows, `"auto"` (default) lets the runtime gate decide. On
macOS, `"auto"` always stays on tee; explicit `"eager-relay"` is the opt-in.
restart it, but in-flight requests fail. For ordinary streams,
`"legacy-tee"` pins the current default. Requests that need a client-facing
payload rewrite use the bounded single-reader relay automatically on Windows
and macOS because the tee/rewrite chain can stall; `streamMode` does not
override that transport-safety exception. On Windows, `"auto"` (default)
lets the runtime gate decide for ordinary streams. On macOS, ordinary
`"auto"` traffic stays on tee; explicit `"eager-relay"` is the opt-in.

If you try any of these on a real Windows workload, please report the before
and after `ocx doctor` memory sections on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ opencodex 打包了 Bun 运行时(当前为 **1.3.14**)。这类内存增长

2. **通过 `OPENCODEX_BUN_PATH` 运行你信任的 Bun 运行时。** 这属于未验证区域,你是在一个我们没有测试过的运行时上运行 opencodex,风险自负。对服务安装而言,这个覆盖值是在生成服务产物时读取的,而不是在服务启动时读取的。先设置环境变量,然后在同一个 shell 中重新运行 `ocx service repair`,这样路径才会被写入持久化的服务定义。只设置环境变量对已经安装好的服务没有任何作用。

3. **通过 `streamMode: "eager-relay"` 显式启用有界中继。** 有两种方式:编辑 `config.json`(添加 `"streamMode": "eager-relay"`),或调用管理 API - `PUT /api/settings` 携带 `{"streamMode":"eager-relay"}`,即可对新轮次生效,无需重启。**崩溃风险警告:** 在 Bun 1.3.14 上,这会使用受 #32111 影响的流形态,可能在流中途使进程崩溃(任何操作系统都会受影响,不只是 Windows)。服务管理器会把它重启,但正在进行的请求会失败。`"legacy-tee"` 会固定在当前默认路径。Windows 上,`"auto"`(默认值)会交给运行时门控决定。macOS 上,`"auto"` 始终保持 tee;显式 `"eager-relay"` 才是显式启用选项
3. **通过 `streamMode: "eager-relay"` 显式启用有界中继。** 有两种方式:编辑 `config.json`(添加 `"streamMode": "eager-relay"`),或调用管理 API - `PUT /api/settings` 携带 `{"streamMode":"eager-relay"}`,即可对新轮次生效,无需重启。**崩溃风险警告:** 在 Bun 1.3.14 上,这会使用受 #32111 影响的流形态,可能在流中途使进程崩溃(任何操作系统都会受影响,不只是 Windows)。服务管理器会把它重启,但正在进行的请求会失败。对于普通流,`"legacy-tee"` 会固定当前默认路径;需要面向客户端做 payload rewrite 的请求会在 Windows 和 macOS 上自动使用有界单读者中继,因为 tee/rewrite 链可能卡住,`streamMode` 不会覆盖这个传输安全例外。Windows 上,普通流的 `"auto"`(默认值)会交给运行时门控决定。macOS 上,普通 `"auto"` 流量仍保持 tee;显式 `"eager-relay"` 才会启用有界中继

如果你在真实的 Windows 工作负载上尝试这些方案,请把变更前后 `ocx doctor` 的内存部分发到 [#314](https://github.com/lidge-jun/opencodex/issues/314)——这正是这个缓解措施在等待的验证。
24 changes: 14 additions & 10 deletions src/lib/bun-stream-caps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
* follows this runtime/config decision, preserving the explicit legacy-tee
* safety pin. Darwin no-rewrite traffic stays on tee
* for `auto` regardless of runtime capability and reaches eager relay only via
* explicit `streamMode: "eager-relay"` opt-in (see
* devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
* explicit `streamMode: "eager-relay"` opt-in. On Windows and Darwin, traffic
* that needs a client payload rewrite always uses the eager single-reader path:
* the tee()+JS-pull rewrite chain can stall before delivering any client frames
* (see #864 and devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
*
* Prerelease conservatism: a version carrying a prerelease suffix (e.g.
* `1.4.0-canary.3`) is NEVER treated as fixed even when its numeric triple
Expand Down Expand Up @@ -94,7 +96,8 @@ export function decideEagerRelay(
* Windows preserves the decision for no-rewrite traffic. Darwin permits only
* explicit config opt-in; `auto` remains tee even on a future fixed runtime.
* Returns the normalized effective decision, or null when platform policy,
* rewrite needs, or a Darwin non-config-eager mode selects tee.
* rewrite needs, or a Darwin non-config-eager mode selects tee. Rewrite traffic
* is handled separately by `requiresEagerRewriteRelay`.
*/
export function selectEagerPath(
platform: NodeJS.Platform,
Expand All @@ -113,15 +116,16 @@ export function selectEagerPath(
}

/**
* #864 transport gate: win32 traffic that needs a client payload rewrite must
* use the eager single reader with the rewrite applied inline, because the
* alternative tee()+JS-pull chain is the Bun#32111-unsafe path that loses the
* terminal SSE block on Windows. Independent of the version-based eager
* policy: the pull chain is unsafe on the AFFECTED runtimes by definition.
* Transport safety gate: Windows and Darwin traffic that needs a client payload
* rewrite must use the eager single reader with the rewrite applied inline.
* The alternative tee()+JS-pull chain can lose the terminal block on Windows
* (#864) and can stall before the first client frame on Darwin when a Desktop-
* shaped request produces large SSE events. This overrides `streamMode`, since
* pinning the known-bad chain would reintroduce a turn that never completes.
*/
export function isWin32EagerRewrite(
export function requiresEagerRewriteRelay(
platform: NodeJS.Platform,
needsClientRewrite: boolean,
): boolean {
return platform === "win32" && needsClientRewrite;
return (platform === "win32" || platform === "darwin") && needsClientRewrite;
}
7 changes: 4 additions & 3 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,10 @@ function attachLiveSidebandUpstream(
// if (isEventStream && upstreamResponse.body) {
// const repairConfig = route.provider.responsesItemIdRepair;
// const needsClientRewrite = imageGenCallAliases.size > 0
// #314 gated shape: win32 always uses the terminal-aware eager relay so a keep-alive
// upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
// requires explicit config-eager opt-in (`auto` always stays tee on darwin).
// #314 gated shape: win32 no-rewrite traffic follows runtime/config policy; darwin no-rewrite
// traffic requires explicit config-eager opt-in (`auto` always stays tee on darwin). Rewrite
// traffic on either platform takes the terminal-aware eager single-reader safety path regardless
// of streamMode, so a keep-alive upstream cannot hold Codex open after response.completed.
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
// relaySseEagerBounded(upstreamResponse.body, turnAc,
// new Response(eagerBody,
Expand Down
4 changes: 2 additions & 2 deletions src/server/relay-eager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export type EagerRelayHooks = {
inspectChunk: (chunk: Uint8Array) => void;
/**
* Optional inline client-facing payload rewrite, framed to complete SSE
* blocks inside the single reader. This is what lets win32 rewrite traffic
* (image_gen restore, item-id repair) use this relay instead of the
* blocks inside the single reader. This is what lets Windows/Darwin rewrite
* traffic (image_gen restore, item-id repair) use this relay instead of the
* Bun#32111-unsafe tee()+JS-pull chain (#864).
*/
rewritePayload?: SsePayloadRewrite;
Expand Down
26 changes: 14 additions & 12 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ import {
sanitizePassthroughHeaders,
} from "../relay";
import { relaySseEagerBounded } from "../relay-eager";
import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
import { requiresEagerRewriteRelay, selectEagerPath } from "../../lib/bun-stream-caps";
import { cancelBodyOnAbort } from "../../lib/abort";
import {
createResponsesItemIdPayloadRewrite,
Expand Down Expand Up @@ -1974,16 +1974,17 @@ async function handleResponsesInner(
? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
: undefined,
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
// #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
// (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
// lost). The eager single reader applies the same rewrites inline.
const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
// Rewrite traffic on Windows or Darwin must never enter the tee()+JS-pull
// chain: Windows can lose the terminal block (#864), while Darwin can stall
// before delivering the first large Desktop-shaped SSE frame. The eager
// single reader applies the same bounded rewrites inline on both platforms.
const eagerRewrite = requiresEagerRewriteRelay(process.platform, needsClientRewrite);
const eagerPath = selectEagerPath(
process.platform,
needsClientRewrite,
config.streamMode ?? "auto",
);
if (eagerPath?.useEagerRelay || win32EagerRewrite) {
if (eagerPath?.useEagerRelay || eagerRewrite) {
const turnAc = new AbortController();
linkAbortSignal(upstream, turnAc.signal);
registerTurn(turnAc, options.turnAdmissionLease);
Expand Down Expand Up @@ -2022,7 +2023,7 @@ async function handleResponsesInner(
// Stream lifetime follows the protocol terminal even when this request
// has no outcome callback configured (reported() would stay false).
sawTerminal: () => inspector.terminalSeen(),
...(win32EagerRewrite
...(eagerRewrite
? { rewritePayload: composeSsePayloadRewrites(...payloadRewrites) }
: {}),
onSynthetic: kind => {
Expand All @@ -2038,11 +2039,12 @@ async function handleResponsesInner(
},
onClientCancel: () => options.onNativePassthroughCancel?.(),
onDone: () => unregisterTurn(turnAc),
}, win32EagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
// When selected, this relay closes response.completed even if upstream
// keeps the connection alive. Windows rewrite traffic applies its
// payload transform inline — never via the Bun#32111-unsafe
// tee()+JS-pull chain (#864).
}, eagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
// selectEagerPath admits only no-rewrite traffic on both eligible platforms;
// Windows/Darwin rewrite traffic reaches this relay through the safety gate,
// with the payload rewrite inline rather than an image/item-id JS pull
// wrapper. The relay closes at response.completed even if upstream keeps
// the connection alive.
if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
return markEagerRelaySseResponse(
markNativePassthroughSseResponse(new Response(eagerBody, {
Expand Down
Loading
Loading