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

# Plan - OpenAI rebuilder terminal Close 경계

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

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

## 배경

리빌드 body의 소유권·peak 계상과 통합 검증은 확인됐지만, `openAIRequestRebuilder.Close()`가 rebuilt store를 비우기만 해 이후 exact rebuild가 새 lease를 다시 저장한다. request 종료 뒤 retained object가 없어야 하는 SDD S22와 현재 계획의 terminal release 조건을 만족하도록 Close와 rebuild 저장을 하나의 terminal lifecycle로 선형화한다.

## 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_cloud_G10_1.log`
- Archived review: `agent-task/m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder/code_review_cloud_G10_1.log`
- Verdict: FAIL
- Findings: Required 1, Suggested 0, Nit 0.
- Required summary: rebuilt store가 폐쇄 상태를 보존하지 않아 `Close()` 뒤 exact `RebuildRequest`가 성공하고 request-local lease를 다시 보존한다.
- Affected files: `apps/edge/internal/openai/openai_request_rebuilder.go`, `apps/edge/internal/openai/openai_request_rebuilder_test.go`.
- Verification evidence: 깨끗한 HEAD에서 fresh unit/race와 `make test-openai-ollama`는 PASS했고, 임시 집중 테스트 `TestReviewRebuilderCloseRejectsExactRebuild`는 `RebuildRequest succeeded after rebuilder Close`로 재현 실패했다. 이전 full-cycle transcript와 payload/order equality evidence는 archived review에 보존돼 있다.
- 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

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.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-contract/inner/edge-config-runtime-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-task/archive/2026/07/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/complete.log`
- `agent-task/archive/2026/07/m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation/complete.log`
- `apps/edge/internal/openai/openai_request_rebuilder.go`
- `apps/edge/internal/openai/openai_request_rebuilder_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`.
- S15 / Task `request-rebuilder`: Chat/Responses의 canonical source, exact/continuation/schema, actual admission/AttemptBinding을 요구한다. 이전 archived review의 byte identity, smoke, entrypoint full-cycle evidence는 신뢰 가능하며 이번 변경이 해당 경로를 바꾸지 않는다.
- S22 / Task `request-rebuilder`: request termination에서 raw/accessor/recovery retained object와 reservation을 모두 해제해야 한다. 이번 checklist와 회귀/race 검증은 Close 이후 새 lease가 생기지 않고 in-flight lease도 zero-retained로 수렴하는 것을 직접 판정한다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`와 매칭 profile `edge-smoke.md`, `platform-common-smoke.md`, `testing-smoke.md`를 읽었다. 구조적 blank, 누락 route, `<확인 필요>` 값은 없다.
- 적용 명령은 fresh Go unit(`-count=1`), race detector, OpenAI smoke, format/diff 검사다. Go test cache 결과는 허용하지 않는다.
- 외부 checkout·device·secret이 필요한 검증은 없다. 실제 entrypoint full-cycle은 직전 archived review의 원문 evidence를 carry하며, terminal store fix가 transport/entrypoint를 변경하지 않으므로 다시 실행하지 않는다.
- shared dirty checkout의 sibling `packages/go/streamgate` 변경에서 별도 테스트 실패가 관찰됐지만 깨끗한 HEAD의 대상 package 검증은 PASS했다. 이 task는 sibling 소스를 수정하지 않고 최종 fresh 명령의 실제 결과만 기록한다.

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

- 기존 `TestOpenAIRequestRebuilderPatchStoreBoundedOneShotRelease`는 patch store가 비워지고 닫히는지만 확인하며 rebuilt store의 terminal 상태와 Close 이후 exact rebuild를 검사하지 않는다.
- Close 이전/이후 rebuilt `put` 선형화와 Close-vs-rebuild 경쟁에서 lease release를 확인하는 테스트가 없다.
- fresh package/race와 OpenAI smoke는 기존 경로를 실행하지만 이 lifecycle edge를 직접 관찰하지 않으므로 focused sequential/race 회귀가 필요하다.

### 심볼 참조

- none. 공개 또는 기존 심볼을 rename/remove하지 않는다.

### 분할 판단

- split decision policy를 평가했다. 변경은 `apps/edge/internal/openai` 한 ownership 경계의 rebuilt store/rebuilder terminal lifecycle과 그 회귀 테스트 한 단위다. 새 공개 API, 광범위 call-site rollout, 별도 환경 검증, 독립 완료 가능한 하위 slice가 없어 분할은 인위적이다.
- 현재 subtask `17+13,16_openai_request_rebuilder`의 predecessor 13은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/complete.log`, predecessor 16은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation/complete.log`의 PASS로 충족됐다.

### 범위 결정 근거

- `packages/go/streamgate/**`, handler/provider rewrite, 공개 contract/spec, roadmap 문서는 변경하지 않는다. 결함은 OpenAI request-local rebuilt store 종료 상태에 국한되고 기존 API와 S15 통합 경로 evidence는 유효하다.
- sibling task의 dirty runtime/evidence-tail 변경과 테스트 실패는 소유 범위 밖이므로 수정하지 않는다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`.
- Build closures: `scope_closed=true`(두 source/test 파일과 terminal invariant 확정), `context_closed=true`(store/rebuilder/lease와 회귀 fixture를 함께 검토), `verification_closed=true`(focused sequential/race, fresh package, smoke로 판정), `evidence_trusted=true`(깨끗한 HEAD 재현과 기존 명령 독립 재실행), `ownership_closed=true`(OpenAI 내부 ownership, sibling 제외), `decision_closed=true`(사용자 결정·외부 권한 없음).
- 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과 grade floor는 `none`.
- Review closures: 여섯 항목 모두 `true`; 동일 파일·invariant와 deterministic verification으로 공식 리뷰 범위가 닫혔다.
- Review route: `official-review`; scores `scope_coupling=1`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=1`, `verification_complexity=1`; `cloud/G06`, `CODE_REVIEW-cloud-G06.md`, Codex `gpt-5.6-sol` xhigh. capability gap과 grade floor는 `none`.
- Build loop-risk observations: ordered transitions는 `open → in-flight → closed` 3상태와 close-before/close-during/duplicate-close 경로, concurrent consistency는 Close와 Rebuild/put 2 actor 및 terminal/atomic/exactly-once 제약, boundary contract는 rebuilder/store/lease/ingress 4 component의 release invariant다. structured interpretation은 없음. variant product는 endpoint, directive, close ordering의 3축이며 조합 검증이 필요하다. matched signatures는 `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product`; `triggered=true`이며 route/grade에는 영향을 주지 않았다.

## 구현 체크리스트

- [x] REVIEW_REVIEW_OPENAI_REBUILDER-1 rebuilt store와 rebuilder Close를 terminal state로 선형화해 Close 이후 저장/조회/rebuild를 거부하고 모든 lease를 exactly-once 해제한다.
- [x] REVIEW_REVIEW_OPENAI_REBUILDER-2 Close 이후 exact rebuild와 Close-vs-put/rebuild 경쟁의 zero-lease/guard/reservation 회귀를 추가하고 fresh unit/race/smoke를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_REVIEW_OPENAI_REBUILDER-1] rebuilt store와 rebuilder Close terminal 선형화

- 문제: `apps/edge/internal/openai/openai_request_rebuilder.go:271-309`의 rebuilt store에는 `closed` 상태가 없고 `close`는 map만 교체한다. `:340-358`의 rebuilder도 Close 여부를 보존하지 않아 순차 Close 뒤 exact rebuild가 새 lease를 저장한다.
- 해결 방법:

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

```go
type openAIRebuiltRequestStore struct {
    mu     sync.Mutex
    leases map[string]*openAIRebuiltLease
}

func (s *openAIRebuiltRequestStore) put(ref string, lease *openAIRebuiltLease) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.leases[ref] = lease
    return nil
}
```

After:

```go
type openAIRebuiltRequestStore struct {
    mu     sync.Mutex
    leases map[string]*openAIRebuiltLease
    closed bool
}

func (s *openAIRebuiltRequestStore) put(ref string, lease *openAIRebuiltLease) error {
    s.mu.Lock()
    if s.closed {
        s.mu.Unlock()
        return streamgate.ErrIngressSnapshotClosed
    }
    if _, exists := s.leases[ref]; exists {
        s.mu.Unlock()
        return fmt.Errorf("duplicate rebuilt OpenAI request reference")
    }
    s.leases[ref] = lease
    s.mu.Unlock()
    return nil
}
```

`close`가 같은 mutex 아래 `closed=true`와 기존 lease 분리를 한 번만 수행하고, rebuilder가 atomic terminal 상태를 먼저 세운 뒤 rebuilt/patch store를 닫는다. `RebuildRequest`는 Close 이후 즉시 `ErrIngressSnapshotClosed`를 반환하고, Close와 경쟁해 마지막 `put`에서 거절된 lease는 기존 오류 경로에서 release한다.
- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: rebuilt store `closed` guard, idempotent close, closed take/put 오류를 구현한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: rebuilder terminal 상태와 RebuildRequest early guard를 추가하고 in-flight 오류 release를 유지한다.
- 테스트 작성: 작성. `apps/edge/internal/openai/openai_request_rebuilder_test.go`의 `TestOpenAIRequestRebuilderCloseIsTerminal`이 Close 뒤 exact rebuild 오류, empty rebuilt store, zero reservation을 검증한다.
- 중간 검증: `gofmt -l apps/edge/internal/openai/openai_request_rebuilder.go apps/edge/internal/openai/openai_request_rebuilder_test.go && go test -count=1 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilderCloseIsTerminal$'` — PASS.

### [REVIEW_REVIEW_OPENAI_REBUILDER-2] Close 경쟁 회귀와 fresh 검증

- 문제: `apps/edge/internal/openai/openai_request_rebuilder_test.go:286-350`은 patch store Close만 확인한다. rebuilt `put`과 Close의 순서 경쟁에서 성공 lease가 Close로 해제되거나 closed store가 거부하는지 직접 검증하지 않는다.
- 해결 방법: deterministic start gate로 Close와 exact rebuild/put을 경쟁시키고 두 goroutine 종료 뒤 store closed, lease map empty, captured lease released, ingress reservation zero를 반복 검증한다. 정상 순서 결과와 무관하게 terminal state가 유일한 최종 상태여야 한다.
- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: `TestOpenAIRequestRebuilderCloseRace`와 필요한 fixture를 추가한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: chat/responses 및 exact/patched 대표 조합이 closed state를 우회하지 않는 assertion을 포함한다.
- 테스트 작성: 작성. `TestOpenAIRequestRebuilderCloseRace`를 race detector에서 20회 실행해 data race, retained lease, unreleased guard를 거부한다.
- 중간 검증: `go test -race -count=20 ./apps/edge/internal/openai -run '^TestOpenAIRequestRebuilderCloseRace$'` — PASS.

## 수정 파일 요약

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

## 최종 검증

1. `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` — host Go 경로와 버전을 원문 기록한다.
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 ./packages/go/streamgate ./packages/go/config ./apps/edge/internal/openai ./apps/edge/internal/service` — 모두 PASS; cache 불허.
4. `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai` — 모두 PASS.
5. `make test-openai-ollama` — OpenAI-compatible Ollama smoke PASS.
6. `git diff --check` — 출력 없음.

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