<!-- task=m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder plan=6 tag=REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY -->

# Plan - OpenAI rebuilder deterministic Close-entry regression repair

## 이 파일을 읽는 구현 에이전트에게

구현·테스트·evidence만 수행하고 `CODE_REVIEW-*-G??.md` 구현 소유 섹션을 실제 출력으로 채운다. active pair는 그대로 두고 ready for review만 보고한다. blocker는 구현 evidence에만 남긴다. 사용자 질문, 다음 상태 분류, archive, `complete.log`, `WORK_LOG.md` 수정은 하지 않는다.

## 배경

현재 `Close`의 terminal drain 구현은 early-return 결함을 보완했지만 회귀 테스트가 close goroutine의 실제 lifecycle 진입을 보장하지 않는다. request-local condition state와 test barrier를 연결해 Chat continuation·Responses schema의 wait/drain 순서를 결정적으로 증명한다.

## Archive Evidence Snapshot

- Prior task: `agent-task/m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder`
- Archived plan: `agent-task/m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder/plan_local_G06_5.log`
- Archived review: `agent-task/m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder/code_review_cloud_G07_5.log`
- Verdict: FAIL; Required 1, Suggested 0, Nit 0.
- Required summary: close caller entry/wait state와 patch-take barrier가 time/scheduler에 의존한다.
- Affected files: `apps/edge/internal/openai/openai_request_rebuilder.go`, `apps/edge/internal/openai/openai_request_rebuilder_test.go`.
- Verification evidence: named fresh/race와 package/race는 PASS했으나 default `GOCACHE`는 permission denied여서 host Go를 유지한 external temporary cache가 필요했다.
- Roadmap carryover: `request-rebuilder`는 미완료이며 Milestone/SDD 잠금은 승인·해제 상태다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md)
- Task ids:
  - `request-rebuilder`: endpoint별 bounded lossless rebuild와 host re-admission
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `apps/edge/internal/openai/openai_request_rebuilder.go`, `apps/edge/internal/openai/openai_request_rebuilder_test.go`
- `apps/edge/internal/openai/stream_gate_ingress.go`, `apps/edge/internal/openai/stream_gate_dispatcher.go`, `apps/edge/internal/openai/stream_gate_dispatcher_test.go`
- `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- `agent-contract/outer/openai-compatible-api.md`, `agent-spec/input/openai-compatible-surface.md`

### SDD 기준

- SDD는 `[승인됨]`, 잠금은 `해제`다. `request-rebuilder`의 S15는 Chat/Responses bounded lossless rebuild와 host re-admission, S22는 종료 시 retained object release를 요구한다.
- Evidence Map S15/S22에 맞춰 두 endpoint의 actual close waiter·barrier release·reservation 0을 직접 단정하고 fresh/race evidence를 남긴다.

### 테스트 환경 규칙

- `test_env=local`; local rules와 edge/testing smoke profiles를 읽었다. host Go identity와 fresh focused/package·race 검증을 적용한다.
- 기본 `GOCACHE=/tmp/iop-go-build-cache`는 permission denied였다. `mktemp -d` external temporary cache만 사용하며 host Go/GOROOT는 바꾸지 않는다.
- `newOpenAIRequestRebuilder`는 production handler call site가 없으므로 full-cycle console은 private lifecycle의 PASS 조건이 아니다. `make test-openai-ollama`는 인접 route 보조 smoke다.

### 테스트 커버리지 공백

- 현재 Chat/Responses test는 patch take와 `time.Sleep`만 확인한다. 두 close caller의 actual wait state가 증명되지 않아 scheduler delayed caller와 early-return을 구별하지 못한다.
- close waiter condition notification과 context `entered` signal, nonblocking close-result assertion으로 공백을 닫는다.

### 심볼 참조

- rename/remove 없음. `openAIRequestRebuilder` 생성·RebuildRequest call site는 rebuilder/dispatcher test에만 있고 production handler call site는 없다.

### 분할 판단

- split 정책을 평가했다. lifecycle state 관찰과 Chat/Responses regression은 같은 mutex/condition invariant와 race profile이라 분리하면 독립 PASS가 불가능하다. API/call-site rollout, 다른 domain, 별도 integration slice가 없어 current single subtask를 유지한다.

### 범위 결정 근거

- `openai_request_rebuilder.go`와 대응 test만 수정한다. ingress, dispatcher, streamgate, config, OpenAI contract/spec/roadmap, production handler rollout은 current Required의 소유자가 아니므로 제외한다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`.
- Build/review closure는 모두 true다: scope는 deterministic close ordering, context는 source/test/SDD/contract, verification은 fresh/race/package/smoke, ownership은 Edge private lifecycle, decision은 Milestone 잠금 해제로 닫혔다.
- Build: `local-fit`, scores `scope_coupling=1`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=1`, `verification_complexity=1`; `local/G06`, `PLAN-local-G06.md`, capability gap/floor=`none`.
- Review: `official-review`, 같은 scores, `cloud/G06`, `CODE_REVIEW-cloud-G06.md`, Codex `gpt-5.6-sol` xhigh, capability gap/floor=`none`.
- Loop risk: ordered states 4, close/rebuild actors 3, components 3, consumer boundary 2, structured mechanism 없음, endpoint×close-order two variant axes; `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product`가 matched이고 route에는 영향이 없다.

## 구현 체크리스트

- [ ] REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY-1 lifecycle mutex 아래 concurrent Close waiter registration/notification을 추가해 test가 actual Close entry를 관찰할 수 있게 한다.
- [ ] REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY-2 context entry barrier와 condition/channel assertion으로 Chat continuation·Responses schema의 two-Close non-return, post-release drain/wake-up을 time polling 없이 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY-1] close waiter lifecycle observation

- 문제: `apps/edge/internal/openai/openai_request_rebuilder.go:367-384`는 Close waiter가 cond에 도달했는지 test가 알 수 없어 time sleep으로 추정한다.
- 해결 방법: same mutex 안에서 terminal fast path를 제외한 Close caller를 count하고 increment/decrement마다 `cond.Broadcast()`한다. drain owner·`closed`·`closeDone` semantics와 public surface는 바꾸지 않는다.

Before (`apps/edge/internal/openai/openai_request_rebuilder.go:367`):

```go
r.closed = true
for r.inFlight > 0 && !r.closeDone {
	r.cond.Wait()
}
```

After:

```go
r.closeWaiters++
r.cond.Broadcast()
defer r.leaveCloseWaiter()
r.closed = true
for r.inFlight > 0 && !r.closeDone {
	r.cond.Wait()
}
```

- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: count와 decrement helper를 same mutex/condition lifecycle에 넣는다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: terminal store drain과 duplicate Close 반환 순서를 유지한다.
- 테스트 작성: 작성. test가 두 active close waiter를 condition으로 기다린다.
- 중간 검증: named fresh test가 Chat/Responses subtest를 실제 실행하고 PASS.

### [REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY-2] deterministic endpoint ordering regression

- 문제: `apps/edge/internal/openai/openai_request_rebuilder_test.go:540-645`의 `waitForPatchTake` polling과 `time.Sleep`은 rebuild block·close waiter entry를 scheduler에 맡긴다.
- 해결 방법: second `Err()`가 patch take 뒤 `entered` channel을 먼저 signal하고 release channel에서 block하게 한다. test는 two `Close` launch 뒤 condition으로 waiter count 2를 확인하고, release 전 done channel이 닫히지 않음을 확인한 뒤 release와 rebuild/store/reservation 0을 단정한다.
- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: `blockingRebuildContext`에 patch-take entry signal을 추가하고 polling/time import를 제거한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: continuation과 Responses schema에서 actual waiter entry, release 전 non-return, release 후 two Close wake-up/store drain을 확인한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: nil receiver와 one-shot release regression을 유지한다.
- 테스트 작성: 작성. `TestOpenAIRequestRebuilderConcurrentCloseWaitsForStoreDrain`가 두 endpoint variant를 deterministic barrier로 고정한다.
- 중간 검증: named race command가 `-count=20`에서 zero-test 없이 PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/openai_request_rebuilder.go` | REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY-1 |
| `apps/edge/internal/openai/openai_request_rebuilder_test.go` | REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY-1, REVIEW_OPENAI_REBUILDER_CLOSE_ENTRY-2 |

## 최종 검증

1. `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` — host identity 확인.
2. `gofmt -l apps/edge/internal/openai/openai_request_rebuilder.go apps/edge/internal/openai/openai_request_rebuilder_test.go` — 출력 없음.
3. `iop_rebuilder_gocache="$(mktemp -d -t iop-s0-openai-rebuilder.XXXXXX)"; GOCACHE="$iop_rebuilder_gocache" go test -count=1 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilder(CloseWaitsForInFlightPatchedRebuild|ConcurrentCloseWaitsForStoreDrain|NilReceiver)$' -v; GOCACHE="$iop_rebuilder_gocache" go test -race -count=20 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilder(CloseWaitsForInFlightPatchedRebuild|ConcurrentCloseWaitsForStoreDrain|NilReceiver)$' -v; rm -rf "$iop_rebuilder_gocache"` — named regressions PASS, race 없음.
4. `iop_rebuilder_gocache="$(mktemp -d -t iop-s0-openai-package.XXXXXX)"; GOCACHE="$iop_rebuilder_gocache" go test -count=1 ./apps/edge/internal/openai ./packages/go/streamgate ./packages/go/config ./apps/edge/internal/service; GOCACHE="$iop_rebuilder_gocache" go test -race -count=1 ./apps/edge/internal/openai ./packages/go/streamgate; rm -rf "$iop_rebuilder_gocache"` — package/race PASS.
5. `make test-openai-ollama` — 보조 OpenAI smoke PASS.
6. `git diff --check` — 출력 없음.

**모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.**
