-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(responses): recover expired forward continuation and fence late WebSocket quota #3937
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # 000_plan.md — B트랙 대화 복구·quota 스택 배송 | ||
|
|
||
| ## 목표 | ||
| #3889(만료된 forward continuation의 WebSocket 복구)과 #3934(자격증명 세대 기반 늦은 WS quota 차단)를 | ||
| 원저자 기여를 보존한 수동 종속 브랜치 체인으로 재구성하고, 최종 tip 한 곳에서만 CI를 태워 | ||
| green이면 tip을 dev에 통합한다. | ||
|
|
||
| ## 제약 (사용자 지시) | ||
| - 로컬 스위트 절대 실행 금지: bun run test / test:changed / typecheck / build / install 모두 NOT RUN. | ||
| - 푸시는 `--no-verify`. | ||
| - CI는 최종 tip에만 트리거한다. 하위 레이어에는 PR을 열지 않는다. | ||
| - 원작 PR이 있으면 원저자를 Co-authored-by로 보존한다. | ||
| - tip이 dev에 머지되는 순간 연결 이슈도 닫는다. | ||
|
|
||
| ## CI 트리거 계약 (근거) | ||
| `.github/workflows/ci.yml`의 `on.pull_request`에는 base 브랜치 필터가 없다(주석에 stacked child PR을 | ||
| 일부러 포함시켰다고 명시). 따라서 **PR을 여는 것 자체가 CI run을 만든다.** | ||
| `push:`는 `branches: [main, preview, dev]`로 제한되므로 포크/작업 브랜치 푸시는 CI를 만들지 않는다. | ||
| 결론: 하위 레이어 L1은 **브랜치 푸시만** 하고 PR을 열지 않는다. tip L2에만 PR을 연다. | ||
|
|
||
| ## 의존성 정렬 (PHASE-SPLIT-01) | ||
| 효율이 아니라 의존 구조로 나눈다. 두 변경 모두 `src/server/responses/core.ts`를 만지므로 | ||
| 같은 파일 위에서 순서를 가진 체인으로 쌓는다. | ||
|
|
||
| - L1 = #3889 continuation 복구 (core.ts:3598 부근 오류 코드 계약) | ||
| - L2 = #3934 WS quota 세대 펜싱 (core.ts:1004 부근 observer) — L1 위에 쌓는다 | ||
|
|
||
| 텍스트 충돌은 없다(두 훅 사이 거리 약 2600줄). 체인 순서는 리뷰 단위 분리를 위한 것이다. | ||
|
|
||
| ## 파일 변경 맵 | ||
| ### L1 (#3889, 원저자 ykvv / y2ambition-ai) | ||
| - MODIFY `src/server/responses/core.ts` — 400 응답 코드를 `invalid_request_error` → `previous_response_not_found`, | ||
| 메시지를 "전체 대화를 다시 보내라"로 변경. HTTP 상태와 인증 전 거부 위치는 유지. | ||
| - MODIFY `tests/codex-integration/issue-702-expired-replay-state.test.ts` — 기존 HTTP 기대값의 code 갱신 + | ||
| expired/missing 두 모드의 WebSocket 재연결·전체 도구 이력 재전송 회귀 추가. | ||
| - MODIFY `docs-site/src/content/docs/guides/codex-integration.md`, `.../ko/guides/codex-integration.md` | ||
|
|
||
| ### L2 (#3934, 원저자 luvs01) | ||
| - MODIFY `src/server/responses/core.ts` — `codexWsQuotaObserver`에서 pool 자격증명 generation을 포착하고 | ||
| `isCodexAccountGenerationLive`가 false면 늦게 도착한 quota 프레임을 무시. | ||
| - MODIFY `tests/responses/responses-account-label.test.ts` — 교체된 자격증명의 늦은 quota가 지워진 상태를 | ||
| 되살리지 못하는 회귀 추가. | ||
|
|
||
| ## 범위 밖 (OUT) | ||
| - `REPLAY_TTL_MS` 등 캐시 보존 기간 변경 | ||
| - 인증/자격증명 회전 정책 변경 | ||
| - main-pool writer 소유권 규칙 변경 | ||
| - B트랙 외 항목(#3906/#3886/#3922/#3917/#3900/#3896/#3924/#3930/#3890) | ||
|
|
||
| ## 검증자 (PLAN-VERIFIER-REAL-01) | ||
| 로컬 스위트가 금지되었으므로 **로컬 검증자는 NOT RUN으로 기록한다**. 유일한 실행 검증자는 | ||
| tip PR head SHA에 대한 hosted Cross-platform CI다. 관측 대상: 4 Linux shard, Windows, | ||
| macOS lane, gates(typecheck/lint/privacy scan), packaging. | ||
| - `gh api repos/lidge-jun/opencodex/actions/runs?head_sha=<tip>` → conclusion=success | ||
| - 이 CI는 `src/**`와 `tests/**`를 changes 필터에 포함하므로 실제로 이번 변경 대상을 관측한다. | ||
|
Comment on lines
+54
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 19440 🏁 Script executed: #!/bin/bash
set -eu
file='devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md'
sed -n '40,65p' "$file"
printf '\n--- matching GitHub Actions query references ---\n'
rg -n -C 2 'gh api|actions/runs|head_sha|conclusion|tip' "$file"Repository: lidge-jun/opencodex Length of output: 2774 🏁 Script executed: #!/bin/bash
set -eu
file='devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md'
sed -n '1,120p' "$file"Repository: lidge-jun/opencodex Length of output: 2969 🏁 Script executed: #!/bin/bash
set -eu
file='.github/workflows/ci.yml'
printf '%s\n' '--- workflow identity and triggers ---'
sed -n '1,90p' "$file"
printf '%s\n' '--- workflow run query usage in repository ---'
rg -n -C 3 'actions/runs|workflow_id|workflow name|head_sha|conclusion' --glob '*.md' --glob '*.yml' --glob '*.yaml' --glob '*.sh' --glob '*.ts' --glob '*.js' .Repository: lidge-jun/opencodex Length of output: 50378 🌐 Web query:
💡 Result: To list workflow runs for a repository filtered by a specific commit SHA using the GitHub REST API, use the GET /repos/{owner}/{repo}/actions/runs endpoint with the head_sha query parameter [1][2]. When making this request, the API returns a response containing a workflow_runs array, where each object includes the following relevant fields [3]: - name: The name of the workflow run (string or null) [3]. - conclusion: The conclusion of the workflow run, such as success, failure, or neutral (string or null) [3]. - head_sha: The commit SHA the workflow run is associated with (string) [1][3]. By providing the head_sha as a query parameter (e.g., GET /repos/{owner}/{repo}/actions/runs?head_sha=YOUR_SHA), the API will filter and return only those workflow runs associated with that specific commit [1][2]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
sed -n '1,90p' .github/workflows/ci.ymlRepository: lidge-jun/opencodex Length of output: 3895 Make the hosted CI verification command exact. Line 54 queries the 🤖 Prompt for AI Agents |
||
|
|
||
| ## 수용 기준 | ||
| 1. L1/L2 커밋 각각에 원저자 Co-authored-by 트레일러가 살아 있다. | ||
| 2. L1에는 PR이 없고 CI run도 없다. CI run은 tip 하나뿐이다. | ||
| 3. tip head SHA의 CI conclusion이 success다. | ||
| 4. 로컬 스위트 미실행, 푸시는 --no-verify. | ||
| 5. tip이 dev 조상이 되고, #3889/#3934가 정리되며 연결 이슈가 닫힌다. | ||
|
|
||
| ## 우회 경로 (PLAN-BYPASS-NAMED-01) | ||
| - tier: E2 (hosted CI 게이트) | ||
| - 실행 주체: GitHub Actions + maintainer 통합 | ||
| - 알려진 우회: admin 권한 보유자는 CI 미완료 상태에서도 머지 가능. 이 계획은 그러지 않는다. | ||
| - 잔여 위험: 하위 레이어 L1은 자체 CI 없이 tip 누적 CI로만 증명된다. 사용자 지시에 따른 의도된 선택. | ||
| - 문구 하향: 없음. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # 010_phase1_l1_continuation_recovery.md — L1 (#3889) 브랜치 구성 | ||
|
|
||
| ## 목적 | ||
| 만료·부재한 forward continuation 상태를 Codex WebSocket 클라이언트가 스스로 복구할 수 있게, | ||
| 프록시가 돌려주는 400 오류의 코드를 클라이언트가 인식하는 `previous_response_not_found`로 바꾼다. | ||
|
|
||
| ## 브랜치 | ||
| `codex/b-stack-l1-continuation-recovery`, base = `origin/dev`. | ||
|
|
||
| ## 커밋 계약 | ||
| 원저자 보존이 필수다. 체리픽으로 원 커밋의 author를 그대로 유지한다. | ||
|
|
||
| ``` | ||
| git cherry-pick -x e8d82a181ea0daa06c5111c09e0148475e45458f | ||
| ``` | ||
|
|
||
| 체리픽은 원 커밋의 author(ykvv <229483879+y2ambition-ai@users.noreply.github.com>)를 보존한다. | ||
| squash 병합 시 author가 소실될 수 있으므로 커밋 메시지에 트레일러도 추가한다: | ||
|
|
||
| ``` | ||
| Co-authored-by: ykvv <229483879+y2ambition-ai@users.noreply.github.com> | ||
| ``` | ||
|
|
||
| ## 정확한 변경 (before → after) | ||
| `src/server/responses/core.ts` 약 3598행: | ||
|
|
||
| ```diff | ||
| if ( | ||
| hasUnexpandedPreviousResponse | ||
| && isCanonicalOpenAiForwardProvider(route.provider) | ||
| ) { | ||
| return formatErrorResponse( | ||
| 400, | ||
| - "invalid_request_error", | ||
| - "OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.", | ||
| + "previous_response_not_found", | ||
| + "OpenAI forward continuation state is unavailable or expired; resend the full conversation without previous_response_id.", | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| 가드 위치(인증·어댑터·upstream I/O 이전)는 바뀌지 않는다. HTTP 상태 400도 유지한다. | ||
|
|
||
| 테스트: `tests/codex-integration/issue-702-expired-replay-state.test.ts` | ||
| - 기존 HTTP 케이스: `code`를 `previous_response_not_found`로 갱신, `type`은 `invalid_request_error` 유지. | ||
| - 신규: expired/missing 두 모드로 WebSocket 연결 → 거부 확인 → upstream 요청 0건 확인 → | ||
| 재연결 후 전체 이력 재전송 → upstream 1건 + `previous_response_id` 없음 + 도구 호출/결과 쌍 보존. | ||
|
|
||
| 문서: `docs-site/src/content/docs/guides/codex-integration.md` 및 한국어 페이지에 복구 경계 문단 추가. | ||
|
|
||
| ## 검증 | ||
| 로컬 스위트 NOT RUN(사용자 금지). 이 레이어는 PR을 열지 않으므로 자체 CI도 없다. | ||
| 증명은 L2 tip의 누적 CI가 담당한다. | ||
|
|
||
| ## 감사 반영 | ||
| 서브에이전트 audit-3889의 결과에 따라 문서의 TTL 수치와 error type/code 매핑을 확정한다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 020_phase2_l2_ws_quota_generation_fence.md — L2 (#3934) tip 레이어 | ||
|
|
||
| ## 목적 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Fix the reported Markdown lint violations. Add blank lines before and after each heading. Add blank lines around the command fence. Set the fence language to Proposed fix ## 커밋 계약
+
-```
+```bash
git cherry-pick -x e5c01f44e9736baba5b3a993c7f489f6b60d5ddd원저자 luvs01 luvs01@hanmail.net 보존 + Also applies to: 7-7, 11-12, 17-17, 41-41, 45-45, 51-51 🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 3-3: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| pool 자격증명이 교체된 뒤 이전 WebSocket 연결에서 늦게 도착한 quota 프레임이, | ||
| 새 자격증명을 위해 비워둔 quota 상태를 되살리지 못하게 막는다. | ||
|
|
||
| ## 브랜치 | ||
| `codex/b-stack-l2-ws-quota-generation`, base = `codex/b-stack-l1-continuation-recovery` (L1 위에 쌓음). | ||
| 이 브랜치가 스택의 tip이며, **PR은 여기에만 연다.** | ||
|
|
||
| ## 커밋 계약 | ||
| ``` | ||
| git cherry-pick -x e5c01f44e9736baba5b3a993c7f489f6b60d5ddd | ||
| ``` | ||
| 원저자 luvs01 <luvs01@hanmail.net> 보존 + `Co-authored-by: luvs01 <luvs01@hanmail.net>` 트레일러. | ||
|
|
||
| ## 정확한 변경 (before → after) | ||
| `src/server/responses/core.ts` 약 1004행: | ||
|
|
||
| ```diff | ||
| +import { isCodexAccountGenerationLive } from "../../codex/account-store"; | ||
|
|
||
| function codexWsQuotaObserver(authCtx, provider): CodexWsQuotaObserver | undefined { | ||
| if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined; | ||
| const { accountId, writerGeneration } = authCtx; | ||
| + const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined; | ||
| const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined; | ||
| - return headers => applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter); | ||
| + return headers => { | ||
| + if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return; | ||
| + applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter); | ||
| + }; | ||
| } | ||
| ``` | ||
|
|
||
| `credentialGeneration === undefined`면 기존 동작을 그대로 유지한다(main-pool·비pool 경로 무변경). | ||
|
|
||
| 테스트: `tests/responses/responses-account-label.test.ts` | ||
| - quota 10 전달 → 자격증명 교체 → quota clear → 옛 연결에서 quota 100 전달 → 최종 상태가 null인지 확인. | ||
|
|
||
| ## L1과의 관계 | ||
| 같은 파일이지만 서로 다른 함수(약 2600줄 간격)라 텍스트 충돌이 없다. | ||
| 체인 순서는 리뷰 단위를 나누기 위한 것이며, L2 diff는 이 변경만 보여준다. | ||
|
|
||
| ## CI 계약 | ||
| `.github/workflows/ci.yml`의 `on.pull_request`는 base 필터가 없어 PR 생성 즉시 CI가 붙는다. | ||
| 따라서 L1에는 PR을 열지 않고, tip인 L2에만 PR을 연다 → CI run 정확히 1개. | ||
| `changes` 필터가 `src/**`, `tests/**`, `docs-site` 외 경로를 보므로 이 변경 세트는 `ci=true`가 되어 | ||
| 4개 Linux shard, Windows, macOS lane, gates가 모두 돈다. | ||
|
|
||
| ## 머지 후 처리 | ||
| - tip PR 머지 → `git merge-base --is-ancestor`로 dev 조상 확인 | ||
| - #3889, #3934: 내용이 dev에 들어갔으므로 원저자 크레딧을 명시하며 닫는다 | ||
| - 연결 이슈: dev 머지 시점에 닫는다 (PR base가 dev라 GitHub 자동 종료가 안 됨 — AGENTS.md 명시) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -213,6 +213,15 @@ provider advertises `supports_websockets = true` only when `"websockets": true`; | |
| built-in provider may try WebSocket first, and a disabled proxy returns `426` so Codex falls back to | ||
| HTTP/SSE. | ||
|
|
||
| If a canonical ChatGPT forward continuation references expired or missing local replay state, | ||
| opencodex returns `previous_response_not_found` before sending anything upstream. Codex's | ||
| WebSocket client recognizes this error and can reconnect with its full retained context, | ||
| including completed tool calls and their results, within its normal stream retry budget. An | ||
| idle task therefore does not need a new task solely because the proxy's one-hour cache expired. | ||
| The cache remains bounded; this does not extend retention or recover history the client no | ||
| longer has. HTTP clients must handle the error explicitly and resend their full context without | ||
| `previous_response_id`. Retrying only the same ID cannot recover missing state. | ||
|
Comment on lines
+216
to
+223
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 12937 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate documentation files ---'
git ls-files 'docs-site/src/content/docs/guides/codex-integration.md' 'docs-site/src/content/docs/**/codex-integration.md' '*codex-integration*'
printf '%s\n' '--- relevant English and locale sections ---'
for f in \
docs-site/src/content/docs/guides/codex-integration.md \
docs-site/src/content/docs/ja/guides/codex-integration.md \
docs-site/src/content/docs/ko/guides/codex-integration.md \
docs-site/src/content/docs/ru/guides/codex-integration.md \
docs-site/src/content/docs/zh-cn/guides/codex-integration.md
do
if [ -f "$f" ]; then
echo "### $f"
rg -n -C 8 'previous_response_not_found|one-hour|one hour|WebSocket|websocket|retained context|previous_response_id|cache|full context|tool calls' "$f" || true
else
echo "### MISSING $f"
fi
done
printf '%s\n' '--- changed files/stat ---'
git diff --stat -- docs-site/src/content/docs/guides/codex-integration.md docs-site/src/content/docsRepository: lidge-jun/opencodex Length of output: 50377 🏁 Script executed: #!/bin/bash
set -eu
for f in \
docs-site/src/content/docs/guides/codex-integration.md \
docs-site/src/content/docs/ja/guides/codex-integration.md \
docs-site/src/content/docs/ko/guides/codex-integration.md \
docs-site/src/content/docs/ru/guides/codex-integration.md \
docs-site/src/content/docs/zh-cn/guides/codex-integration.md
do
echo "### $f"
rg -n -i -C 3 \
'previous_response_not_found|previous_response_id|replay state|replay|retained context|full context|tool calls|one.hour|1.?hour|час|小时|時間|캐시.*시간|кэш.*час' \
"$f" || true
done
printf '%s\n' '--- implementation and test bindings ---'
rg -n -i -C 3 \
'previous_response_not_found|replay.*ttl|ttl.*replay|replay.*cache|cache.*replay|3600|one.?hour|hour' \
src tests --glob '*.{ts,tsx,js,json}' | head -n 500Repository: lidge-jun/opencodex Length of output: 45982 Synchronize the recovery boundary in the remaining locale pages The Korean page already documents this behavior at 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| ### Authless Codex Desktop (opt-in) | ||
|
|
||
| In **Dashboard → Overview**, **Open Codex without signing in** controls this existing | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Resolve the Markdown lint violations in both plan files.
devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md#L3-L3: add blank lines around the headings reported by MD022.devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md#L3-L3: add the MD022 blank lines and label the fenced blocks at Lines 13 and 20 withbashandtext.🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
📍 Affects 2 files
devlog/_plan/260908_b_track_quota_recovery_stack/000_plan.md#L3-L3(this comment)devlog/_plan/260908_b_track_quota_recovery_stack/010_phase1_l1_continuation_recovery.md#L3-L3🤖 Prompt for AI Agents
Source: Linters/SAST tools