Repository navigation
[architecture][memory] Make 32 concurrent tool-recall sessions protocol-safe and memory-bounded #820
Description
Activity
github-actions commented
on Jul 31, 2026 on Jul 31, 2026 – with GitHub ActionsContributorMore actionsAutomated translation bookkeeping — detected language: unknown.
- addedstreamingSSE, WebSocket, terminal stream framesSSE, WebSocket, terminal stream framestoolstool_calls, MCP, web-search / sidecar toolstool_calls, MCP, web-search / sidecar toolsproviderProvider adapters, OpenAI-compat presets, upstream API quirksProvider adapters, OpenAI-compat presets, upstream API quirks
on 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.
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.
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.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.
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.
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.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.
- added a commit that references this issue
on Aug 2, 2026 - addedroadmapAccepted long-term work item; needs its own project cycleAccepted long-term work item; needs its own project cycle
on Aug 3, 2026 2 remaining items
Related implementation tracks
- [Feature]: add durable stream-stage timeline and failure attribution to request history #1217 — correlation-safe streaming stage timeline and observability
- [Research]: benchmark bounded streaming hot paths before any native/Go port #1177 — optional native/Go hot-path experiment
#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 commented
on Aug 11, 2026 ContributorMore actionsRelated 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.
리뷰 · 우선순위 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이 작성했습니다
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 commented
on Aug 25, 2026 More actionsField 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:
- 1,998 upstream requests
- 616 terminal 502 rows
- 589 failures correlated to one conversation
- failed rows had
streamAborted=true,upstream_server_error, and no first output
The 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:
- current-turn / first-use images remain byte-identical;
- only historical
input_image.image_urlvalues beginning withdata:are replaced during localprevious_response_idreplay by short metadata carrying the media type and encoded length; - text, ordering, IDs,
call_id, tool call/result pairing,file_id, and ordinary URLs are preserved; - the redacted historical prefix has an 8 MiB byte ceiling; overflow returns a local 413 before auth or upstream I/O.
-
Conversation-local abort circuit:
- open after six terminal 502 +
streamAborted=trueoutcomes within ten minutes; - keep 499 neutral and reset on 200;
- block continuation for one hour with a non-retryable local 409;
- a fresh request without
previous_response_idclears the circuit.
- open after six terminal 502 +
These are local prototype values, not proposed upstream defaults.
Reproducible acceptance evidence
With a local fake upstream and synthetic image data:
- first-use request reaching upstream: 15,728,788 bytes
- next replay after historical redaction: 402 bytes
- oversized redacted replay: local 413, zero upstream calls
- six injected stream resets: six upstream calls
- seventh continuation: local 409, zero upstream calls
- fresh checkpoint: admitted and reached upstream
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:- an image-heavy continuation/replay regression harness plus the agreed payload/budget policy;
- a separate conversation-scoped stream-abort circuit with explicit 200/499/fresh-checkpoint semantics.
- added a commit that references this issue
on 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.- addedlanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainOriginal PR closed after landing via a maintainer merge train
on Aug 25, 2026 - added a commit that references this issue
on Sep 14, 2026 - added 4 commits that reference this issue
on Sep 17, 2026
Summary
OpenCodex should remain TypeScript/Bun-based, but it needs an explicit concurrency and memory architecture for the real workload we must support:
model stream -> 1..N parallel tool/MCP calls -> tool results -> API recallThis 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
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:
{ namespace, name }on the return path.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.output_item.added -> arguments.delta* -> arguments.done -> output_item.done -> response terminal.{}rather than an empty invalid JSON string.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/andtests/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.tool_call_start -> one full tool_call_delta -> tool_call_endsequence.bridgeToResponsesSSE()intentionally holds onecurrentToolCall, because the canonicalAdapterEventcontract is sequential and non-overlapping.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:
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 consumersThe native Responses passthrough path still has a client branch and a background inspection branch created from
upstreamResponse.body.tee()insrc/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
devcreateSseInspector()currently:response.output_item.doneentry until completion;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:
PendingToolCall.argsis grown by string concatenation..doneand final item snapshots.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.tshas a 64 MiB aggregate accounting cap, but eviction currently stops whilestates.size > 1. One oversized final entry can remain above the cap. Forcedstore:falseexpansion 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.tsstores content-addressedUint8Arrayvalues in a process-global map with: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.tscurrently defaults to: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.tstracks only aSet<AbortController>. It does not model:A permit must be released on terminal/cancel/error/shutdown, not when the handler returns a
Responseobject.8. Account selection is not active-load-aware
src/codex/pool-rotation.tsimplements 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:
Also, oven-sh/bun#33368 remains open and documents why
process.memoryUsage().heapUsedcan exceedheapTotaldramatically on current Bun.heapUsedalone 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
tee()branch-rate mismatch amplifies external/native buffersexternal/arrayBuffersrise with slow or inspection-skewed clientsresponseState.totalBytesand largest entry rise by roundTarget architecture
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:
No production behavior change in this PR.
PR 2 — Bounded, single-parse inspection
Implement the existing Phase 2 plan:
PR 3 — Tool/output byte accounting without changing the canonical contract
args += fragmentwith chunks + join-once where A/B tests show lower peak/CPU;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
Bun.revisioncapabilities in the release manifest;PR 5 — Turn coordinator and session lanes
TurnScope;PR 6 — Continuation, Cursor blob, and request-body byte budgets
PR 7 — Bootstrap-only retry, circuit breaker, and active-aware account selection
Acceptance criteria
Correctness
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:
Memory outcome
Success is not defined as RSS returning immediately to process-start values. Success requires:
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
heapUsedalone to attribute Windows memory;Source pointers
OpenCodex pinned source and prior design records:
Runtime and comparative evidence:
fetch().bodypiped throughTransformStreamdoes not propagate backpressure oven-sh/bun#28035RequestContext.onAbortwhen returning async ReadableStream + client aborts oven-sh/bun#32111InMemoryEventStoreis never evicted and cannot be disabled punkpeye/mcp-proxy#72Definition 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.