Repository navigation
[architecture][memory] Make 32 concurrent tool-recall sessions protocol-safe and memory-bounded #820
Description
Summary
OpenCodex should remain TypeScript/Bun-based, but it needs an explicit concurrency and memory architecture for the real workload we must support:
- 32 sustained independent agent sessions
- 64-session burst
- repeated
model stream -> 1..N parallel tool/MCP calls -> tool results -> API recall - no OpenCodex-imposed serialization below that target
- no loss of Codex / Responses / Chat Completions / Anthropic / MCP compatibility
This is not a proposal to raw-passthrough provider bytes. OpenCodex is a semantic protocol translator. The required change is to keep that translation while replacing unbounded or duplicated retention with bounded state, byte accounting, and stream-lifetime leases.
Audit baseline: dev@18545f87e92b856be180ba01e8b2c44d67163879.
This issue is related to the field symptoms in #314 and #509, but is not a duplicate. Those issues discuss observed Windows memory behavior and imperfect Bun counters. This issue defines the architecture, hypotheses, implementation slices, and acceptance harness needed to distinguish OpenCodex-owned retention from Bun allocator/runtime retention under multi-session tool recall.
Hard requirements
Concurrency contract
sustained independent sessions: 32
burst independent sessions: 64
global emergency active cap: 96
same-session active turns: 1
same-session queued turns: 0 by default
Different sessions must remain parallel. Only overlapping turns for the same logical session/thread may be rejected or serialized. Tool execution performed by the external client must not keep an OpenCodex model-turn lease occupied after the model stream reaches a terminal.
Protocol invariants that must not be removed
Any memory fix must preserve all of the following:
- Codex namespace/MCP tools are flattened for chat providers and restored as
{ namespace, name }on the return path. - Codex custom/freeform tools such as
apply_patchare exposed to chat models through a temporary{ input: string }function schema, then restored ascustom_tool_call, notfunction_call. tool_searchremains distinct from an ordinary function call.call_id, Responsesitem_id,output_index, and Chat Completions tool indexes remain stable and correctly correlated.- The complete lifecycle remains valid:
output_item.added -> arguments.delta* -> arguments.done -> output_item.done -> response terminal. - No-argument calls finalize as
{}rather than an empty invalid JSON string. - Malformed or truncated arguments must not be reported as a completed usable call.
- Reasoning signatures, redacted thinking, tool results, image content, continuation metadata, and provider-specific replay behavior remain intact.
Raw provider SSE passthrough would violate these contracts.
Important correction: the existing parallel-tool contract is deliberate
The repository already solved provider-interleaved OpenAI Chat tool deltas in devlog/_fin/260709_parallel_tool_calls/ and tests/openai-chat-parallel-stream.test.ts.
The current design is:
openai-chatkeys provider fragments by index/id and buffers all pending calls while text/reasoning continues.- At a valid terminal boundary, it emits each completed call as one non-overlapping:
tool_call_start -> one full tool_call_delta -> tool_call_endsequence. bridgeToResponsesSSE()intentionally holds onecurrentToolCall, because the canonicalAdapterEventcontract is sequential and non-overlapping.- Finish-less EOF with pending calls fails closed rather than fabricating a completed call.
Therefore a keyed, live, multi-call bridge is not a required P0 change and must not be introduced merely to reduce memory. It would reopen previously closed correctness hazards: text/reasoning barriers, item ordering, partial-call failure, and custom-tool unwrapping.
The measured optimization path should be:
- preserve the sequential canonical contract;
- replace fragment-by-fragment string concatenation with chunk arrays and byte counters where it matters;
- clear each adapter call as soon as its atomic sequence has been consumed;
- add per-call, per-turn, and process-wide argument-byte metrics/limits;
- consider an optional atomic
tool_call_completeevent only if profiling proves material duplicate reassembly between an adapter that already has a final snapshot and the bridge.
A keyed-live event redesign remains a separately reviewed alternative, not the default plan.
Current confirmed pressure points
1. tee() still creates two independently paced consumers
The native Responses passthrough path still has a client branch and a background inspection branch created from upstreamResponse.body.tee() in src/server/responses/core.ts.
The existing macOS RSS plan measured a large external-memory difference between rate-mismatched tee and one-reader variants. The remaining work is to make the one-reader path protocol-safe, runtime-qualified, compatible with required payload rewrites, and valid for the 32-session workload.
2. The inspector is not fully bounded on current dev
createSseInspector() currently:
- appends decoded chunks into an unterminated string without a frame-byte cap;
- parses the same payload in multiple consumers;
- retains every reconstructed
response.output_item.doneentry until completion; - does not clear the reconstruction map immediately after the completed response is copied.
The repository already contains a concrete Phase 2 plan for a 1 MiB frame cap, 256 reconstructed items, single parse, and post-callback cleanup. Add a reconstructed-byte cap as well.
3. Tool conversion is correct, but its retained bytes are not explicitly bounded
Confirmed current behavior:
- OpenAI Chat buffers all parallel calls until a valid flush boundary.
PendingToolCall.argsis grown by string concatenation.- The Responses bridge accumulates the active call again to create required
.doneand final item snapshots. - custom/freeform input may be repeatedly decoded from a growing wrapper.
- Responses -> Chat outbound retains per-index argument strings until
output_item.done/terminal to avoid duplicate append-only deltas.
These states are protocol-motivated. The open question is their measured byte cost, not whether they can simply be deleted.
4. Continuation bytes have a single-entry escape hatch
src/responses/state.ts has a 64 MiB aggregate accounting cap, but eviction currently stops while states.size > 1. One oversized final entry can remain above the cap. Forced store:false expansion also retains full expanded input for each response ID, which can grow approximately quadratically by chain length.
A final design needs a hard-cap policy plus explicit oversize/tombstone behavior, and likely parent-linked deltas and/or content-addressed large payloads so continuity never fails silently.
5. Cursor blobs are count-bounded, not byte-bounded
src/adapters/cursor/native-exec.ts stores content-addressed Uint8Array values in a process-global map with:
- 15-minute TTL
- 4,096-entry cap
- no per-blob byte cap
- no process-wide byte cap
- no active-request pin/lease accounting
Count-only eviction does not bound memory when one or more tool/history blobs are large. Selection/budgeting should happen before insertion, and live blobs need pins so byte eviction cannot remove data still needed by an active Cursor turn.
6. The eager relay is bounded per stream, but not globally or precisely
src/server/relay-eager.ts currently defaults to:
- 8 MiB client queue per stream;
- 15-second post-cancel drain;
- 32 MiB post-cancel drain bytes per stream;
- approximate queue accounting that resets
queuedBytesto zero onpull().
At 32 sessions, configured per-stream limits can multiply into a large process-wide envelope. The target design needs process-wide byte accounting and either pull-paced reading or exact queued-byte release.
7. Active-turn tracking is not a coordinator
src/server/lifecycle.ts tracks only a Set<AbortController>. It does not model:
- logical session lanes;
- admitted/bootstrap/committed phases;
- global and weighted byte permits;
- provider/account active load;
- queue age;
- stream-lifetime ownership.
A permit must be released on terminal/cancel/error/shutdown, not when the handler returns a Response object.
8. Account selection is not active-load-aware
src/codex/pool-rotation.ts implements sticky/smooth weighted selection but does not include current active turns, TTFT EWMA, or transient-failure pressure. Under synchronized recall bursts, many sessions can select the same account before outcomes update.
Affinity should remain authoritative when continuation is account-scoped, but portable requests should prefer an eligible target with lower active saturation.
9. Bundled Bun 1.3.14 cannot be assumed safe for the target relay
The package pins Bun 1.3.14. Two upstream fixes directly affect the proposed shape:
- http: couple fetch() receive backpressure to JS body consumption (h1/h2/h3) oven-sh/bun#29831: fetch receive backpressure coupled to JS body consumption; merged after 1.3.14.
- Fix crash when a client aborts a streaming response under backpressure oven-sh/bun#32120: crash when a client aborts an async streaming response under backpressure; merged after 1.3.14, with a 20-concurrent-client regression test.
Also, oven-sh/bun#33368 remains open and documents why process.memoryUsage().heapUsed can exceed heapTotal dramatically on current Bun. heapUsed alone must not be used as the leak discriminator.
The release must pin an exact verified Bun.revision, not trust a semver guess or moving canary.
Expanded hypotheses and discriminating measurements
| ID | Hypothesis | Expected signature | Discriminating test |
|---|---|---|---|
| H1 | tee() branch-rate mismatch amplifies external/native buffers |
external/arrayBuffers rise with slow or inspection-skewed clients |
same fixture under legacy tee vs qualified one-reader relay |
| H2 | protocol-required tool/output assembly creates avoidable string-copy peaks | tool-assembly counters and JSC heap track large arguments | string concat vs chunks/join-once with identical AdapterEvent and wire output |
| H3 | full-history continuation entries retain chain prefixes | responseState.totalBytes and largest entry rise by round |
32 sessions x 10 recalls with parent-linked vs full-expanded state |
| H4 | Cursor blob map retains large payloads despite entry cap | blob count stays bounded while blob bytes rise | skewed large-blob workload with byte metrics and pin accounting |
| H5 | recall request parsing creates simultaneous raw/string/object copies | request-cost budget tracks synchronized recall spikes | barrier-synchronized large tool-result POSTs |
| H6 | retry/account selection creates a herd | attempts and active load concentrate on one target | synchronized 503/429/reset fixture with full jitter and active-aware routing |
| H7 | application state returns to baseline but RSS remains high | app counters/readers/leases return to idle; RSS plateaus | repeated identical waves after warm-up |
| H8 | a real OpenCodex leak remains after bounded state | one or more app-owned counters/object populations rise each wave | heap-retainer comparison after H1-H6 are bounded |
Target architecture
request
-> logical session lane (one active turn per session)
-> global active-turn permit + weighted request-memory permit
-> affinity-aware, active-load-aware account selection
-> bootstrap-only retry / circuit policy
-> provider adapter
-> provider-specific parallel-call assembly where required
-> sequential canonical AdapterEvent contract
-> protocol bridge with bounded current-item/full-response accounting
-> one qualified upstream reader
-> bounded inline inspector
-> required payload rewrite
-> downstream response
-> idempotent terminal cleanup releases every lease/pin/reader
The one-reader invariant means one read of the upstream body, not zero transformation. Inspection and payload rewriting must be stages on the same semantic data path.
Proposed PR series
PR 1 — Acceptance harness and observe-only metrics
Add the real recall-burst harness before changing behavior:
- 32 sessions, 10 rounds each;
- 1..8 parallel function/MCP/custom/tool-search calls per round;
- provider-interleaved OpenAI Chat deltas while preserving the canonical sequential output contract;
- random tool latency 10..500 ms;
- barrier-synchronized recalls;
- slow consumers;
- 25% random cancellation;
- pre-first-byte resets, 503 outage, account 429;
- application-owned byte/readers/turn metrics and RSS samples.
No production behavior change in this PR.
PR 2 — Bounded, single-parse inspection
Implement the existing Phase 2 plan:
- 1 MiB unterminated frame cap with correct SSE resynchronization;
- one parse per payload;
- 256 reconstructed output items plus a reconstructed-byte cap;
- immediate map cleanup after synchronous continuation copy;
- no payload contents in overflow warnings.
PR 3 — Tool/output byte accounting without changing the canonical contract
- instrument adapter pending-tool bytes, bridge active-tool bytes, final output bytes, and outbound per-index bytes;
- replace hot-path
args += fragmentwith chunks + join-once where A/B tests show lower peak/CPU; - add explicit per-call/per-turn/process budgets and truthful oversize failure;
- make custom-tool JSON-string unwrapping incremental;
- clear adapter call buffers as each atomic sequence is consumed;
- optionally add an atomic
tool_call_completeevent only if it materially removes duplicate reassembly and passes the full protocol matrix.
PR 4 — Runtime qualification and protocol-safe one-reader relay
- add reproducible fixtures for Bun fetch receive backpressure and async-pull abort safety;
- record exact validated
Bun.revisioncapabilities in the release manifest; - keep legacy tee as an explicit compatibility fallback;
- compose inspection and required payload rewrites into one reader path;
- preserve synthetic failed-tail semantics;
- default post-cancel drain to zero, or a very small opt-in byte/time budget;
- add a process-wide stream byte budget.
PR 5 — Turn coordinator and session lanes
- replace controller-only tracking with metadata-rich
TurnScope; - one active turn per logical session, independent sessions parallel;
- global active and request-memory weighted permits;
- release only at stream terminal/cancel/error/shutdown;
- expose active/queued/by-phase/by-provider/by-account metrics.
PR 6 — Continuation, Cursor blob, and request-body byte budgets
- hard aggregate and per-entry bounds without the single-entry escape hatch;
- explicit oversize/tombstone behavior;
- parent-linked continuation deltas or content-addressed payloads;
- Cursor blob total bytes, per-blob limit, pins, expiry and eviction metrics;
- request cost admission before retaining large decoded bodies.
PR 7 — Bootstrap-only retry, circuit breaker, and active-aware account selection
- retry only before downstream commit;
- max two attempts / bounded elapsed time / full jitter;
- no retry after first downstream byte;
- provider+model transient circuit, account-scoped quota handling;
- affinity-safe active-load score.
Acceptance criteria
Correctness
- 32 sustained and 64 burst independent sessions complete without OpenCodex-imposed serialization below target.
- No call IDs, item IDs, indexes, namespaces, names, arguments, outputs, or session state cross-contaminate.
- Provider-interleaved fragments for at least 32 OpenAI Chat calls still emerge as the existing non-overlapping canonical sequences.
- Function, MCP namespace, custom/freeform,
tool_search, image-bearing result, malformed args, no-arg call, and interrupted-call cases all preserve existing contracts. transport.postCommitRetryAttemptsremains exactly zero.
Cleanup
After every wave, within the declared teardown window:
turns.active -> 0
turns.queued -> 0
stream.activeReaders -> 0
stream.pendingBytes -> idle baseline
stream.inspectorBytes -> idle baseline
tool.pendingArgumentBytes -> 0
tool.bridgeActiveBytes -> 0
memory.requestCostInUse -> idle baseline
memory.continuationBytes -> configured bound
memory.cursorBlobPinnedBytes -> idle baseline
budget waiters -> 0
Memory outcome
Success is not defined as RSS returning immediately to process-start values. Success requires:
- all OpenCodex-owned byte/readers/lease counters return to the expected idle envelope;
- repeated identical workload waves do not retain additional application state;
- after warm-up, RSS does not maintain a continuously positive per-wave slope.
If application-owned state returns to baseline while RSS remains on a stable high plateau, classify the residual as runtime/allocator retention and attach the exact Bun revision and fixture result.
Explicit non-goals
- raw provider-byte passthrough for translated routes;
- reducing normal concurrency below 32 sessions;
- setting a default per-account concurrency of 1 or 2;
- treating Bun 1.3.14 as qualified for the new relay;
- using
heapUsedalone to attribute Windows memory; - replacing the existing sequential canonical tool-event contract without measured justification;
- a Go/Rust rewrite;
- one giant implementation PR.
Source pointers
OpenCodex pinned source and prior design records:
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/types.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/responses/parser.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/bridge.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/adapters/openai-chat.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/tests/openai-chat-parallel-stream.test.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/devlog/_fin/260709_parallel_tool_calls/001_audit_round1.md
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/devlog/_fin/260709_parallel_tool_calls/010_phase1_stream_assembly.md
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/chat/outbound.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/claude/outbound.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/server/responses/core.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/server/relay.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/server/relay-eager.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/server/lifecycle.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/responses/state.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/src/adapters/cursor/native-exec.ts
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/devlog/_plan/260731_macos_rss_retention/020_inspection_bounds.md
- https://github.com/lidge-jun/opencodex/blob/18545f87e92b856be180ba01e8b2c44d67163879/devlog/_plan/260731_macos_rss_retention/030_macos_single_reader.md
Runtime and comparative evidence:
fetch().bodypiped throughTransformStreamdoes not propagate backpressure oven-sh/bun#28035- http: couple fetch() receive backpressure to JS body consumption (h1/h2/h3) oven-sh/bun#29831
- Native segfault in
RequestContext.onAbortwhen returning async ReadableStream + client aborts oven-sh/bun#32111 - Fix crash when a client aborts a streaming response under backpressure oven-sh/bun#32120
- process.memoryUsage: source heapTotal and heapUsed from the same heap oven-sh/bun#33368
- Unbounded memory growth: default
InMemoryEventStoreis never evicted and cannot be disabled punkpeye/mcp-proxy#72 - https://github.com/Portkey-AI/gateway/blob/669825cbe89ee51569918b8f78a9db486fd69dd4/src/handlers/streamHandler.ts
- https://github.com/musistudio/claude-code-router/blob/4a152d959c016b476220339e856c9f4f94624c42/packages/core/src/gateway/request/pipeline.ts
Definition of done
Close only after the protocol matrix and 32-session recall harness pass on every supported OS/runtime combination, the application-owned state is bounded and observable, and the bundled Bun revision is explicitly qualified for the selected default relay mode.
Activity
github-actions commentedon Jul 31, 2026
Automated translation bookkeeping — detected language: unknown.
Ingwannu commentedon Aug 1, 2026
Maintainer status after reviewing the current implementation attempt (#829): the architecture goal is valid, but that PR is not a mergeable slice of this plan yet. It spans 79 files rather than starting with the observe-only acceptance harness, and its current Windows run exposes two concrete blockers: the new state-store fixtures exceed their declared 5-second budgets (about 17.7s and 34.2s), and ACL memo lifecycle state leaks across the combined suite (expected one harden operation, observed five).
I requested changes on #829. Before implementation continues, please isolate the harness environment/ACL memo state, make the Windows timing contract deterministic, and split the work along the PR series defined here so the metrics/harness can land independently of storage behavior. Keeping this issue open; no architecture acceptance is implied by the current PR.
Ingwannu commentedon Aug 1, 2026
Confirmed Windows-specific process-lifetime retention in the ACL memo sets: atomic config/response-state writes, migration backups, management-token creation, and tray replacement mint unique hardened temp paths that were never released.
I opened focused PR #840. It keys timeout memoization by the stable destination, releases only temp entries whose rename/unlink/ENOENT proves the file is absent, and deliberately retains memo state for residual hardened temps. Focused Windows/config/management/tray tests pass (167/167).
I am keeping the other confirmed memory bounds in separate PRs so each can be reviewed and rolled back independently.
Ingwannu commentedon Aug 1, 2026
Focused PR #841 fixes the confirmed single-entry escape hatch in the Responses continuation store.
The old byte pruner stopped at
states.size > 1, so one expanded/image-heavy entry could remain above the nominal 64 MiB cap. #841 measures the complete retained map entry in UTF-8 bytes before mutation, rejects an individually over-cap candidate without evicting unrelated chains, removes stale same-ID state, and applies the same rule on snapshot load. Focused tests pass 43/43.Ingwannu commentedon Aug 1, 2026
Focused PR #843 fixes the confirmed Antigravity replay retention: raw canonical tool arguments were retained as Map keys and each session had no call/byte bound.
#843 hashes model/session/call identities, limits signatures to 64 KiB, sessions to 256 calls / 2 MiB, and the cache to 32 MiB, with nested LRU and bounded TTL sweeping. The existing long sequential replay behavior remains covered; focused Google tests pass 56/56.
Ingwannu commentedon Aug 1, 2026
Focused PR #844 fixes the confirmed Cursor Connect active-turn buffer peak. The decoder accepted a uint32 payload declaration and the transport repeatedly concatenated pending bytes with no inbound cap; partial EOF was also accepted as success after any prior frame.
#844 rejects >32 MiB declarations from the five-byte header, incrementally completes one bounded pending frame, preserves outbound uint32 compatibility, and fails incomplete EOF. Focused transport/framing tests pass 45/45.
Ingwannu commentedon Aug 1, 2026
Focused PR #845 fixes the confirmed Cursor blob-store retention. The old store capped only entry count (4,096), so 64 one-megabyte blobs already retained roughly 64 MiB and larger entries had no byte bound; pre-pruned external roots were also stored.
#845 adds 16 MiB per-entry / 64 MiB total limits, request-lifetime pins for every advertised root/step/turn, unpinned LRU/TTL eviction, post-prune root storage, explicit preparation failure on pinned saturation, and truthful
setBlobArgsprotobuf errors. All 398 Cursor tests pass.Ingwannu commentedon Aug 1, 2026
The remaining code-confirmed unbounded streaming path from this audit is now isolated in draft PR #847.
It adds a 4 MiB SSE-record cap and UTF-8 argument budgets of 8 MiB per call / 32 MiB per turn across OpenAI Chat buffering, the Responses streaming and batch bridges, Responses-to-Chat translation, and the non-stream Chat collector. Overflow is a typed 502 failure: an open function call becomes incomplete and no arguments-done, completed tool item, successful finish reason, or Chat DONE marker is fabricated. The existing sequential AdapterEvent contract is unchanged.
Validation: 206 related tests pass, plus typecheck and privacy scan.
Together with focused PRs #840, #841, #843, #844, and #845, every unbounded application-owned retention point confirmed in this review now has a narrow dev-targeted PR. The remaining tee/decompression/eager-relay observations are bounded runtime peaks or profiling-dependent architecture work, so I did not label them as confirmed leaks or change them in this series.
docs(plan): allocate 12 must-fix bugs across 5 bugfix worktrees
Merge pull request #892 from lidge-jun/codex/wt2-zero-leak-impl
fix(repo): untrack the retired Go file that reached dev, and guard it…
2 remaining items
Wibias commentedon Aug 8, 2026
Related implementation tracks
#1177 is explicitly blocked on profiling evidence from this program. A native implementation should not proceed unless profiling demonstrates a material CPU/RSS/allocation win on a bounded hot path.
Wibias commentedon Aug 11, 2026
Related research tracker: #1177.
#1177 is explicitly blocked on the profiling/acceptance evidence produced by this issue. It is not a competing architecture or a commitment to introduce Go/native code.
Native evaluation should begin only if this issue identifies a bounded hotspot that remains material after normal TypeScript/Bun optimization and can be differential-tested without changing OpenCodex wire/event semantics. If that evidence never materializes, #1177 should close without implementation.
lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 75 / 80
현재
dev는 TypeScript/Bun을 유지한 채, 이 이슈가 감사한 일부 보유 경로에 바이트 상한을 넣었습니다. Responses continuation store, Antigravity replay 캐시, Cursor 프레임/블롭, SSE 레코드 한도 같은 초점 PR이 트리에 흔적을 남겼습니다. 그래도 이슈의 계약인 지속 32 세션 / 버스트 64 / 비상 96, 세션당 활성 턴 1, 도구 실행이 모델 턴 리스를 붙잡지 않음은 아키텍처 전체가 끝난 상태가 아닙니다.이슈가 원하는 것은 원시 패스스루가 아닙니다. OpenCodex는 의미 프로토콜 번역기이고, 그 번역을 유지한 채 무한 보유를 리스·바이트 회계로 바꾸자는 요청입니다. Codex 네임스페이스/MCP flatten,
apply_patch복원,tool_search구분,call_id/item_id상관, 빈 인자{}같은 불변식은 메모리 패치가 지우면 안 됩니다.네이티브 신뢰성과 Windows/서비스 RSS가 점수입니다. #314/#509 증상과 맞닿아 있고, Bun 카운터가 부정확해도 OpenCodex 보유와 런타임 보유를 가르는 하네스가 필요합니다. 관찰 전용 수락이 먼저여야 한다는 메인테이너 지적은 여전히 유효합니다. 79파일짜리 한 PR은 이 계획의 슬라이스가 아닙니다.
위생적으로 #1217 타임라인과 #1177 네이티브 실험은 자식/연구입니다. Go 핫패스는 이 프로그램이 유의미한 핫스팟을 남길 때만입니다. 상한을 프로토콜 실패로 바꾸면 프로바이더 호환이 깨지므로, overflow는 typed 502 같은 명시적 실패여야 합니다.
해결방안은 observe-only 하네스 → 남은 무제한 스트림 경로 상한 → 32 세션 도구-재호출 수락 순입니다. 이미 머지된 바이트 캡을 다시 쓰지 마십시오. 패치는 추측하지 않으며, 현재 트리를 32 세션 계약 완료로 보지 않습니다.
이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Aug 23, 2026
Backlog review 2026-08-23 — retained as roadmap.
This defines a concurrency and memory architecture for 32 sustained sessions with a 64-session burst, no OpenCodex-imposed serialization below that target, and no loss of Codex, Responses, Chat Completions, Anthropic, or MCP compatibility. That is a program with its own design and measurement phases, not an issue a backlog pass should touch.
Re-confirmed as accurate against
devtoday. Staying open deliberately.Hermes663 commentedon Aug 25, 2026
Field evidence: image-heavy continuation replay + conversation-local abort storm
I hit a production failure mode on macOS with Codex App through OpenCodex 2.32.0 that seems directly relevant to PR 6 and PR 7 in this roadmap.
Observed behavior
A long tool-heavy conversation accumulated local Responses continuation states of approximately 14.4 MiB, 14.5 MiB, and 20.3 MiB. The dominant payload was historical
input_imagecontent carried asdata:URLs, including images nested in message content andcustom_tool_call_output/function_call_output.Once the upstream stream began aborting, the client kept issuing distinct continuation requests for the same conversation. In one day:
streamAborted=true,upstream_server_error, and no first outputThe same logical failure crossed account failover, so this did not look account-specific.
PR #1698 correctly prevents replay compounding when a full stored prefix can be proven, but its bounded fingerprint comparison deliberately treats oversized items as non-comparable. Large inline data URLs therefore remain a practical continuation pressure point.
Local safety prototype
I tested a version-pinned local prototype with two deliberately separate controls:
Replay multimedia/budget:
input_image.image_urlvalues beginning withdata:are replaced during localprevious_response_idreplay by short metadata carrying the media type and encoded length;call_id, tool call/result pairing,file_id, and ordinary URLs are preserved;Conversation-local abort circuit:
streamAborted=trueoutcomes within ten minutes;previous_response_idclears the circuit.These are local prototype values, not proposed upstream defaults.
Reproducible acceptance evidence
With a local fake upstream and synthetic image data:
The regression slice also covered messages,
function_call_output,custom_tool_call_output, preservation of text/call IDs/tool pairs/file IDs/external URLs, conversation isolation, 499 neutrality, 200 reset, expiry, and late in-flight aborts. Local results: 9/9 Bun tests, 46 expectations, full bundle build, forward/reverse patch dry-runs, idempotence, and rollback.After activating the prototype, the acceptance window had 40 production log rows with 36 successful 200s, four controlled malformed-request probes, and zero 502 /
streamAbortedrows; health/ready was 20/20 and a real model request completed with 200.Design caveat / request for direction
I do not think the whole local patch should be upstreamed as one PR. Dropping historical image bytes can change semantics when a later turn needs to inspect the original image again. A content-addressed payload, durable image reference, cached vision description, or another continuation-aware representation may be the better PR 6 design. Likewise, a conversation-local breaker should be coordinated with the PR 7 turn/circuit architecture instead of treated as an isolated logging hook.
Would this field evidence and fake-upstream harness be useful for the PR 6 / PR 7 work? If so, I can rebase against current
devand prepare two focused, repository-native changes:Related: #1698, #1652, #919.
feat(lifecycle): bound concurrent tool-recall sessions by logical lane (
lidge-jun commentedon Aug 25, 2026
Closed on
devby #2611 (squash07401301a14562d5542d42e1b428d164d58b2ef9).Admission leases are now keyed by a fixed-size opaque session lane derived at ingress, claimed atomically before the existing global permit. Independent lanes stay parallel up to the 256-turn cap; an overlapping turn on one identified lane gets the existing structured
server_busy. This is admission, not scheduling — no waiters, no queue memory. The scheduler this issue also sketched stays deferred, deliberately.The 32/64-session recall harness exists and measures:
lanePeakBytes1024 at 32 sessions, 2048 at 64, both returning to zero. The oracle is lifecycle-owned byte accounting, not RSS — RSS is recorded but is not a valid mutation oracle, since allocator retention is nondeterministic and a test asserting on it would pass or fail for unrelated reasons.Worth recording because it nearly shipped: the first lane derivation preferred
x-codex-parent-thread-id, mirroringcodexPoolAffinityKey. That is right for affinity and wrong for a lane — every child of a parallel subagent fan-out carries the same parent id, so all siblings collapsed into one lane and every one after the first got a 503. Reproduced before fixing. A lane wants the most specific identity, which is the opposite of what affinity wants; keyed on the pair, the parent qualifies the lane rather than defining it.Falsified four ways: widening the 64-lane bound, removing byte accounting, reverting HTTP lane ingress, and restoring the parent-first derivation each turn a specific test red.
GitHub only auto-closes on merges into
main; PRs here targetdev, so closing manually.lidge-jun commentedon Aug 25, 2026
Landed via #2611 at 0740130
feat(lifecycle): bound concurrent tool-recall sessions by logical lane (
docs(plan): allocate 12 must-fix bugs across 5 bugfix worktrees
Merge pull request lidge-jun#892 from lidge-jun/codex/wt2-zero-leak-impl
fix(repo): untrack the retired Go file that reached dev, and guard it…
feat(lifecycle): bound concurrent tool-recall sessions by logical lane (