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

# Plan - OpenAI rebuilder terminal Close completion repair

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

구현·테스트·evidence만 수행하고 모든 검증의 실제 출력으로 `CODE_REVIEW-*-G??.md` 구현 에이전트 소유 섹션을 채운다. active pair를 그대로 두고 ready for review만 보고한다. 막히면 blocker, 시도 명령/출력, 재개 조건만 구현 evidence에 기록한다. 사용자 질문, user-input 도구, stop 파일, 다음 상태 분류, log archive, `complete.log` 작성은 하지 않는다. 최종화는 code-review skill 전용이다.

## 배경

현재 `Close`는 terminal store drain 전에 `closeDone`을 공개해 concurrent duplicate 호출이 retained patch가 남은 상태에서 반환할 수 있다. in-flight rebuild를 여러 `Close`가 기다릴 때 단일 signal은 waiter를 영구 대기시킬 수 있으며, 기존 회귀 테스트는 Close의 non-return과 Responses schema 경로를 실제로 검증하지 않는다.

## 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_4.log`
- Archived review: `agent-task/m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder/code_review_cloud_G07_4.log`
- Verdict: FAIL
- Findings: Required 2, Suggested 0, Nit 0.
- Required summary: 모든 concurrent `Close`의 terminal store drain completion barrier와 이를 직접 관찰하는 Chat/Responses ordering regression이 필요하다.
- Affected files: `apps/edge/internal/openai/openai_request_rebuilder.go`, `apps/edge/internal/openai/openai_request_rebuilder_test.go`.
- Verification evidence: focused reproducer는 duplicate `Close` 조기 반환 시 patch reservation 11 bytes를 관찰했다. named fresh/race, package fresh/race, OpenAI Ollama smoke는 PASS했다.
- Roadmap carryover: `request-rebuilder`는 미완료이며 Milestone/SDD 잠금은 승인·해제 상태다.
- 구현에 prior context가 꼭 필요할 때만 위 두 archived log를 좁게 다시 읽는다.

## 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_ingress_test.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: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`, `USER_REVIEW.md` 없음.
- 대상 Acceptance Scenario/Evidence Map: S15 `request-rebuilder`의 Chat/Responses endpoint rebuild와 S22 `request-rebuilder`의 request 종료 시 retained object release.
- checklist는 Chat continuation·Responses schema patch가 in-flight인 동안 모든 `Close`가 반환하지 않고, release/store drain 뒤 모두 반환하는 증거를 직접 만들도록 역산했다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`를 읽었다.
- host `go`/`GOROOT` identity, fresh focused/package test, race test와 `make test-openai-ollama`를 적용한다.
- rebuilder constructor는 현재 production handler call site가 없고 private lifecycle만 바뀌므로 manual Edge-Node 2-message full-cycle과 live provider preflight는 이 invariant의 PASS 조건이 아니다. OpenAI smoke는 인접 route 회귀의 보조 증거로 유지한다.

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

- 기존 `TestOpenAIRequestRebuilderCloseWaitsForInFlightPatchedRebuild`는 patch map 삭제만 polling하고 rebuild를 멈추지 않으며 `Close` non-return을 단정하지 않는다.
- schema subtest는 Chat endpoint의 `messages`를 patch해 Responses `input` 경로를 실행하지 않는다.
- duplicate Close는 drain 완료 후 순차 호출만 검증해 concurrent caller의 조기 반환과 waiter wakeup을 놓친다.

### 심볼 참조

- rename/remove 없음. `openAIRequestRebuilder`와 stores는 현재 `apps/edge/internal/openai` 내부 source/test에서만 참조된다.

### 분할 판단

- split 정책을 먼저 평가했다. completion state, in-flight wakeup, duplicate caller와 두 endpoint regression은 같은 private mutex/condition invariant이며 production/test를 나누면 어느 쪽도 독립 PASS가 아니다.
- 새 API, 별도 소유 도메인, 독립 rollout, 다른 검증 profile이 없어 현재 split subtask를 그대로 단일 plan으로 유지한다.
- 기존 subtask 선행 의존성은 이미 같은 경로에서 충족된 상태이며 이번 follow-up은 directory/dependency metadata를 바꾸지 않는다.

### 범위 결정 근거

- 수정은 `openai_request_rebuilder.go`와 대응 test에 한정한다.
- `packages/go/streamgate`, ingress/dispatcher, 외부 OpenAI contract/spec/config/roadmap은 terminal Close completion의 소유자가 아니므로 변경하지 않는다.
- sibling streamgate/runtime 및 agent-task runtime 변경은 현재 finding과 무관하므로 수정하지 않는다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`.
- Build closures: `scope_closed=true`(두 file의 terminal Close invariant), `context_closed=true`(rebuilder/stores와 직접 test를 함께 검토), `verification_closed=true`(결정적 barrier와 focused/race 명령), `evidence_trusted=true`(11-byte 조기 반환 reproducer), `ownership_closed=true`(Edge private lifecycle), `decision_closed=true`(Milestone 잠금 해제·결정 필요 없음).
- Build route: `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 closures: 모두 `true`. Review route: `official-review`; scores `scope_coupling=1`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=2`, `verification_complexity=1`; `cloud/G07`, `CODE_REVIEW-cloud-G07.md`, Codex `gpt-5.6-sol` xhigh; capability gap/floor=`none`.
- Build loop-risk: ordered states 4(open, closing, draining, terminal), adverse paths 4(in-flight, duplicate, nil, partial drain); actors 3(rebuild와 두 Close); components 3(rebuilder/patch store/rebuilt store), consumers 2(rebuilder/dispatcher lifecycle); structured parser/merge mechanism 없음; endpoint와 close timing/cardinality의 독립 variant 축 2개를 조합 검증한다. matched signatures=`temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product`; `triggered=true`이며 route/grade에는 영향을 주지 않는다.

## 구현 체크리스트

- [ ] REVIEW_OPENAI_REBUILDER_TERMINAL_CLOSE-1 `Close` completion을 rebuilt/patch store drain 뒤에만 공개하고 모든 concurrent caller가 같은 terminal completion을 기다리도록 lifecycle barrier를 고친다.
- [ ] REVIEW_OPENAI_REBUILDER_TERMINAL_CLOSE-2 결정적 barrier로 Chat continuation과 Responses schema in-flight ordering, concurrent duplicate Close, waiter wakeup, nil receiver와 release 수렴을 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_OPENAI_REBUILDER_TERMINAL_CLOSE-1] terminal drain completion barrier

- 문제: `apps/edge/internal/openai/openai_request_rebuilder.go:363-383`은 `closeDone=true`를 두 store의 `close()` 전에 설정한다. concurrent caller는 retained lease/patch release 전에 반환할 수 있고, `:400-404`의 단일 `Signal`은 둘 이상의 waiting Close를 모두 깨우지 못한다.
- 해결 방법: rebuilder mutex 아래 `closed`로 새 registration을 막되 terminal completion은 `inFlight==0`과 두 store drain이 모두 끝난 뒤에만 설정한다. 한 caller가 drain을 수행하는 동안 다른 caller는 같은 mutex/condition completion을 기다리게 하고, 마지막 in-flight deregistration은 모든 waiter를 깨운다.

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

```go
r.mu.Lock()
if r.closeDone {
    r.mu.Unlock()
    return
}
r.closed = true
for r.inFlight > 0 {
    r.cond.Wait()
}
r.closeDone = true
r.mu.Unlock()
r.rebuilt.close()
r.patches.close()
```

After:

```go
r.mu.Lock()
if r.closeDone {
    r.mu.Unlock()
    return
}
r.closed = true
for r.inFlight > 0 && !r.closeDone {
    r.cond.Wait()
}
if r.closeDone {
    r.mu.Unlock()
    return
}
r.rebuilt.close()
r.patches.close()
r.closeDone = true
r.cond.Broadcast()
r.mu.Unlock()
```

- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: store drain과 completion 공개를 하나의 terminal owner 구간으로 직렬화한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: 마지막 in-flight deregistration에서 모든 waiting Close가 재평가하도록 broadcast한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: nil/closed registration semantics와 patch-before-inFlight defer 순서를 유지한다.
- 테스트 작성: 작성. 아래 item의 concurrent drain/in-flight regression이 조기 반환과 waiter 고립을 함께 고정한다.
- 중간 검증: `go test -count=1 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilder(ConcurrentCloseWaitsForStoreDrain|NilReceiver)$' -v` — 두 test가 실제 실행되고 PASS.

### [REVIEW_OPENAI_REBUILDER_TERMINAL_CLOSE-2] deterministic endpoint and concurrent Close regressions

- 문제: `apps/edge/internal/openai/openai_request_rebuilder_test.go:431-537`은 patch take 뒤 실행을 멈추지 않고, Responses endpoint와 concurrent duplicate Close를 검증하지 않는다.
- 해결 방법: 두 번째 `ctx.Err()`에서 patch take 이후를 신호·정지하는 test context를 둔다. Chat continuation과 Responses schema table에서 두 concurrent Close가 barrier 해제 전 반환하지 않음을 확인하고, 해제 뒤 rebuild/모든 Close 완료, reservation 0, stores closed/empty를 단정한다. 별도 store-drain lock fixture로 duplicate caller가 실제 drain completion을 기다리는지도 고정한다.
- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: polling helper를 deterministic context barrier로 교체한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: Chat `messages` continuation과 Responses `input` schema variant를 실제 endpoint/body로 실행한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: 두 concurrent Close의 pre-release non-return, post-release completion과 terminal store/reservation 상태를 검증한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: nil receiver와 test-name matching fresh/race command를 유지한다.
- 테스트 작성: 작성. `TestOpenAIRequestRebuilderCloseWaitsForInFlightPatchedRebuild`, `TestOpenAIRequestRebuilderConcurrentCloseWaitsForStoreDrain`, `TestOpenAIRequestRebuilderNilReceiver`가 필수다.
- 중간 검증: `go test -race -count=20 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilder(CloseWaitsForInFlightPatchedRebuild|ConcurrentCloseWaitsForStoreDrain|NilReceiver)$' -v` — zero-test 없이 20회 PASS하고 race 없음.

## 수정 파일 요약

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

## 최종 검증

1. `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` — host Go identity 원문 기록.
2. `gofmt -l apps/edge/internal/openai/openai_request_rebuilder.go apps/edge/internal/openai/openai_request_rebuilder_test.go` — 출력 없음.
3. `go test -count=1 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilder(CloseWaitsForInFlightPatchedRebuild|ConcurrentCloseWaitsForStoreDrain|NilReceiver)$' -v` — named regressions 실제 실행·PASS.
4. `go test -race -count=20 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilder(CloseWaitsForInFlightPatchedRebuild|ConcurrentCloseWaitsForStoreDrain|NilReceiver)$' -v` — 20회 PASS, race 없음.
5. `go test -count=1 ./apps/edge/internal/openai ./packages/go/streamgate ./packages/go/config ./apps/edge/internal/service` — 모두 PASS.
6. `go test -race -count=1 ./apps/edge/internal/openai ./packages/go/streamgate` — 모두 PASS.
7. `make test-openai-ollama` — OpenAI-compatible 보조 smoke PASS.
8. `git diff --check` — 출력 없음.

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