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

# Plan - OpenAI request backing 소유권과 실제 peak 복구

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

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

## 배경

첫 리뷰에서 snapshot ledger가 실제로 동시에 소유하는 canonical 방어 복사, rebuild output, commit 복사, dispatch 복사를 계상하지 않는 결함을 확인했다. continuation/schema patch 저장소도 request 상한 밖에서 payload를 복제하고 종료 시 해제하지 않으며, OpenAI 사용자 실행 경로의 smoke/full-cycle evidence가 없다. SDD S15/S22의 단일 canonical, 보수적 logical retained/peak, overflow zero-dispatch, 종료 release 조건을 실제 소유권과 일치시킨다.

## 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_G09_0.log`
- Archived review: `agent-task/m-stream-evidence-gate-core/17+13,16_openai_request_rebuilder/code_review_cloud_G09_0.log`
- Verdict: FAIL
- Findings: Required 3, Suggested 0, Nit 0.
- Required summary: 실제 full-body 복사들이 retained/peak에서 누락됨; patch map이 상한 밖에서 복제되고 `Close`에서 해제되지 않음; OpenAI smoke와 실제 entrypoint full-cycle evidence가 없음.
- Affected files: `packages/go/streamgate/ingress_snapshot.go`, `apps/edge/internal/openai/{stream_gate_ingress.go,openai_request_rebuilder.go,provider_model_rewrite.go,input_estimator.go,chat_handler.go,responses_handler.go,provider_tunnel.go}` 및 대응 테스트.
- Verification evidence: fresh package tests와 race는 PASS했고 `git diff --check`도 PASS했지만, 해당 출력은 실제 backing 누락을 관찰하지 못했고 smoke/full-cycle은 실행되지 않았다.
- 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`
- `packages/go/streamgate/ingress_snapshot.go`
- `packages/go/streamgate/ingress_snapshot_test.go`
- `apps/edge/internal/service/provider_tunnel.go`
- `apps/edge/internal/service/provider_pool.go`
- `apps/edge/internal/openai/stream_gate_ingress.go`
- `apps/edge/internal/openai/stream_gate_ingress_test.go`
- `apps/edge/internal/openai/openai_request_rebuilder.go`
- `apps/edge/internal/openai/openai_request_rebuilder_test.go`
- `apps/edge/internal/openai/json_field_patch.go`
- `apps/edge/internal/openai/provider_model_rewrite.go`
- `apps/edge/internal/openai/input_estimator.go`
- `apps/edge/internal/openai/input_estimator_test.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/provider_tunnel.go`
- `apps/edge/internal/openai/dispatch_context.go`
- `apps/edge/internal/openai/dispatch_context_test.go`
- `apps/edge/internal/openai/stream_gate_dispatcher.go`
- `apps/edge/internal/openai/stream_gate_dispatcher_test.go`
- `scripts/e2e-openai-ollama.sh`
- `Makefile`

### 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을 검증한다. Evidence Map은 caller-body equality, retained-byte accounting, host admission/path switch를 요구한다.
- S22 / Task `request-rebuilder`: pre-read limit-1/limit/limit+1, initial typed overflow, configured smaller limit, rebuild pre/post current peak, release/raw absence, no dispatch/no budget, 16 MiB cap을 요구한다.
- 따라서 owned transfer API, patch reservation/one-shot release, near-limit 실제 backing fixture를 구현 checklist에 넣고, package/race 뒤 OpenAI smoke와 실제 entrypoint cycle을 최종 evidence로 고정했다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`를 읽었고 `edge-smoke`, `platform-common-smoke`, `testing-smoke` profile을 적용한다. `<확인 필요>` 값은 없다.
- Host Go preflight는 `/config/.local/bin/go` → `/config/opt/go/bin/go`, `go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`로 일치했다.
- fresh package/race는 현재 checkout에서 재현 가능하다. cached 결과는 허용하지 않고 모두 `-count=1`을 사용한다.
- `make test-openai-ollama`는 임시 설정·포트·fake Ollama와 실제 `scripts/dev/edge.sh`/`scripts/dev/node.sh` entrypoint를 사용하지만 보조 smoke이므로 full-cycle을 대체하지 않는다.
- 실제 full-cycle은 두 dev script를 별도 프로세스로 실행하고 Edge console에서 `/nodes`, 같은 session 메시지 2회, `/capabilities`, `/transport`, `/sessions`와 payload 순서/terminal을 관찰한다. 필요한 local adapter/provider가 없으면 실행 명령, stdout/stderr, 환경 blocker와 재개 조건을 그대로 남기며 PASS evidence로 간주하지 않는다.

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

- 기존 Core tests는 defensive-copy `IssueBackingHandle`/`CommitTyped`와 guard lifecycle을 검증하지만 ownership-transfer backing identity와 no-copy commit은 없다.
- 기존 OpenAI tests는 self-reported `retained <= peak <= max`만 확인해 실제 live canonical/patch/output/dispatch backing 수를 검증하지 못한다.
- patch oversized/duplicate/take/close/cancel release와 zero-dispatch fixture가 없다.
- byte-based token estimator 동등성 fixture가 없어 `string([]byte)` full-body 복사를 방지하지 못한다.
- package/race는 통과했지만 OpenAI smoke/full-cycle evidence가 없다.

### 심볼 참조

- 제거/rename은 없다.
- 새 ownership-transfer API의 직접 caller는 `buildOpenAIIngressSnapshot`과 OpenAI rebuild commit 경로로 제한한다. 기존 defensive-copy API caller는 호환 유지한다.
- `canonicalBody` caller는 `dispatch_context.go`, `chat_handler.go`, `responses_handler.go`, `provider_model_rewrite.go`, `openai_request_rebuilder.go`에 있고 모두 read-only alias/lease 계약을 적용한다.
- `rewriteChatCompletionModelFromIngress`와 `rewriteResponsesModelFromIngress` caller는 `chat_handler.go`, `responses_handler.go`, `provider_tunnel.go`의 `BuildBody` closure다.

### 분할 판단

- 기존 split task `17+13,16_openai_request_rebuilder`를 그대로 후속한다. 선행 `13+11,12_recovery_plan_contract`와 `16+15_ingress_snapshot_allocation`은 각각 `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`로 충족됐다.
- Core ownership transfer와 OpenAI callsite/patch ledger는 동일 “request-owned backing을 한 번 계상하고 한 번 해제” 불변조건을 함께 바꾸므로 따로 배포하면 중간 상태가 올바르지 않다. smoke/full-cycle은 이 production slice의 PASS evidence이므로 test-only sibling으로 분리하지 않는다.

### 범위 결정 근거

- 기존 defensive-copy `Canonical`, `TypedView`, `IssueBackingHandle`, `CommitTyped` 의미는 호환을 위해 유지한다.
- Core Coordinator/filter/recovery budget, Node codec, provider queue policy, public OpenAI response schema, config 기본값/상한은 바꾸지 않는다.
- service `BuildBody` 계약은 바꾸지 않고 OpenAI wrapper가 synchronous submission 동안 body lease를 보유·해제한다.
- unrelated dirty sibling task와 중앙 관리 `agent-ops/rules/common/**`, `agent-ops/skills/common/**`은 수정하지 않는다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`.
- Build closures: scope/context/verification/evidence/ownership/decision 모두 true. 근거는 affected call graph와 S15/S22 invariant가 닫혔고, fresh unit/race·smoke·full-cycle의 성공/차단 판정 기준이 구체적이며 외부 결정이 없다는 점이다.
- Build base route=`grade-boundary`; scores=`scope_coupling:2, state_concurrency:2, blast_irreversibility:2, evidence_diagnosis:2, verification_complexity:2`; final=`cloud/G10`, `PLAN-cloud-G10.md`; capability gap=`none`; grade/signature floor=`none`.
- Build loop-risk observations: ordered transitions=`5`(reserved/committed/cancelled/overflow/released, close/cancel/overflow adverse paths), concurrent consistency actors=`3`(snapshot owner, patch/rebuild guard, dispatch/release; exactly-once release와 lock order), boundary contract components=`3`, consumers=`3`(Core/OpenAI/service와 Chat/Responses/recovery), structured mechanisms=`top-level JSON byte-span patch + ownership transfer`, hazards=`nesting/quoting/alias lifetime`, variant axes=`endpoint/directive/admission/overflow phase`, combination verification required=`true`. Matched signatures=`temporal_state,concurrent_consistency,boundary_contract,structured_interpretation,variant_product`; triggered=`true`.
- Review closures도 모두 true; base route=`official-review`; scores=`2/2/2/2/2`; final=`cloud/G10`, `CODE_REVIEW-cloud-G10.md`, Codex `gpt-5.6-sol` xhigh; capability gap/floor=`none`.

## 구현 체크리스트

- [ ] REVIEW_OPENAI_REBUILDER-1 Core owned backing/commit과 OpenAI canonical/rebuilt dispatch lease를 추가해 실제 live backing과 retained/peak를 일치시킨다.
- [ ] REVIEW_OPENAI_REBUILDER-2 continuation/schema patch를 동일 상한에 one-shot 예약하고 duplicate/overflow/close/cancel에서 즉시 해제한다.
- [ ] REVIEW_OPENAI_REBUILDER-3 near-limit/release 회귀, fresh unit/race, OpenAI smoke와 실제 entrypoint full-cycle evidence를 완성한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REVIEW_OPENAI_REBUILDER-1] 실제 backing과 ledger 일치

- 문제: `packages/go/streamgate/ingress_snapshot.go:143`, `packages/go/streamgate/ingress_snapshot.go:534`는 caller buffer를 다시 복사하고, `apps/edge/internal/openai/stream_gate_ingress.go:111`, `apps/edge/internal/openai/openai_request_rebuilder.go:123`, `apps/edge/internal/openai/provider_model_rewrite.go:227`은 accessor 방어 복사를 더 만든다. `openai_request_rebuilder.go:277,311`과 `responses_handler.go:172`의 `string([]byte)`도 full-body copy다. 보고 peak는 이 live buffers를 포함하지 않는다.
- 해결 방법: 기존 copy API는 유지하고 명시적 ownership-transfer API를 추가한다. OpenAI ingress는 HTTP read buffer와 semantic marshal buffer를 owned handle로 넘기고 동일 backing alias를 request-local owner 안에서만 read-only로 유지한다. rebuild output은 reserve 뒤 한 번 할당해 owned commit으로 넘기고 `openAIRebuiltLease`가 exact canonical 또는 rebuilt output alias와 Core snapshot/guard를 함께 release한다. provider `BuildBody`는 wrapper가 synchronous service submission이 끝날 때 body lease를 해제한다. byte token estimator를 추가해 full-body string 변환을 제거한다.

Before (`packages/go/streamgate/ingress_snapshot.go:143`, `:534`):

```go
b.backings[hdl] = &snapshotBacking{payload: append([]byte(nil), data...)}
sb := &snapshotBacking{payload: append([]byte(nil), actualOutput...)}
```

After:

```go
func (b *IngressSnapshotBuilder) IssueOwnedBackingHandle(data []byte) (BackingHandle, error)
func (g *IngressSnapshotRebuildGuard) CommitOwnedTyped(name string, output []byte) (*IngressSnapshot, error)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/ingress_snapshot.go`: copy API와 분리된 owned transfer 경로, pre/post check, terminal release를 구현한다.
  - [ ] `packages/go/streamgate/ingress_snapshot_test.go`: backing identity, caller-mutation 계약, owned commit, overflow/close/race를 검증한다.
  - [ ] `apps/edge/internal/openai/stream_gate_ingress.go`: canonical/semantic owned handle과 read-only alias lifecycle을 적용한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: exact/rebuilt body alias lease와 actual peak를 적용한다.
  - [ ] `apps/edge/internal/openai/provider_model_rewrite.go`, `chat_handler.go`, `responses_handler.go`, `provider_tunnel.go`: synchronous dispatch body lease를 적용한다.
  - [ ] `apps/edge/internal/openai/input_estimator.go`, `input_estimator_test.go`: `[]byte` rune estimator와 string estimator 동등성을 추가한다.
- 테스트 작성: 작성. `TestIngressSnapshot_OwnedBackingTransferAndCommit`, `TestOpenAIRequestRebuilderActualOwnedPeakBoundaries`, `TestOpenAIProviderBodyLeaseRelease`, `TestEstimateInputTokensBytesMatchesString`에서 pointer/backing count, limit 경계, no copy, release, estimate 동등성을 검증한다.
- 중간 검증: `gofmt -w packages/go/streamgate/ingress_snapshot.go packages/go/streamgate/ingress_snapshot_test.go apps/edge/internal/openai/stream_gate_ingress.go apps/edge/internal/openai/openai_request_rebuilder.go apps/edge/internal/openai/provider_model_rewrite.go apps/edge/internal/openai/input_estimator.go apps/edge/internal/openai/input_estimator_test.go apps/edge/internal/openai/chat_handler.go apps/edge/internal/openai/responses_handler.go apps/edge/internal/openai/provider_tunnel.go && go test -count=1 ./packages/go/streamgate ./apps/edge/internal/openai` — PASS, fresh package tests.

### [REVIEW_OPENAI_REBUILDER-2] bounded one-shot patch lifecycle

- 문제: `apps/edge/internal/openai/openai_request_rebuilder.go:48,69,90,102,112`가 patch를 저장·조회할 때 복제하지만 Core limit에 포함하지 않고, `openai_request_rebuilder.go:219`의 `Close`는 patch map을 해제하지 않는다.
- 해결 방법: patch API는 validated `json.RawMessage` backing을 request-local ownership으로 이전하고, ingress `ReserveRebuild(len(patch))` guard를 entry에 결합한다. duplicate key는 거부하고 조회는 map에서 제거하는 one-shot take로 바꾼다. output reservation은 기존 patch reservation을 포함한 current peak를 검사하며, success/error/cancel/`Close`에서 patch guard와 slice를 정확히 한 번 release한다.

Before (`apps/edge/internal/openai/openai_request_rebuilder.go:30-33`):

```go
type openAIRecoveryPatchStore struct {
    continuation map[string]openAIContinuationPatch
    schema       map[string][]byte
}
```

After:

```go
type openAIRecoveryPatchEntry struct {
    value []byte
    guard *streamgate.IngressSnapshotRebuildGuard
}
func (s *openAIRecoveryPatchStore) takeSchema(...) (*openAIRecoveryPatchEntry, error)
func (s *openAIRecoveryPatchStore) close()
```

- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder.go`: reserve-before-own, duplicate rejection, one-shot take, close/release를 구현한다.
  - [ ] `apps/edge/internal/openai/stream_gate_ingress.go`: existing reservations까지 포함한 conservative peak를 기록한다.
  - [ ] `apps/edge/internal/openai/openai_request_rebuilder_test.go`: oversized/duplicate patch, patch+output peak, zero dispatch, take/close/cancel release를 검증한다.
- 테스트 작성: 작성. `TestOpenAIRequestRebuilderPatchStoreBoundedOneShotRelease`와 `TestOpenAIRequestRebuilderPatchPlusOutputPeakOverflow`에서 limit, retained/reserved bytes, empty stores/leases를 직접 assertion한다.
- 중간 검증: `go test -count=1 ./apps/edge/internal/openai -run '^TestOpenAI(RequestRebuilder|Ingress|Provider)' && go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai` — focused tests와 race PASS.

### [REVIEW_OPENAI_REBUILDER-3] 통합 검증 신뢰도 복구

- 문제: archived review의 evidence는 package/race/diff뿐이며 `agent-ops/rules/project/domain/testing/rules.md:62,86,147`이 요구하는 OpenAI smoke와 실제 사용자 실행 cycle이 없다.
- 해결 방법: 새 경계 fixtures를 fresh로 실행한 뒤 self-contained OpenAI Ollama smoke를 실행한다. 이어 두 dev entrypoint를 별도 프로세스로 실행해 Edge console의 node 확인, 같은 session 메시지 2회, capability/transport/session 조회와 ordered payload/terminal을 기록한다. 환경 blocker는 성공으로 바꾸지 않는다.
- 수정 파일 및 체크리스트:
  - [ ] production/test 변경 전체의 fresh Go/race/diff 결과를 review stub에 원문으로 기록한다.
  - [ ] `make test-openai-ollama` stdout/stderr와 exit 0을 기록한다.
  - [ ] 실제 `scripts/dev/edge.sh` + `scripts/dev/node.sh` console cycle evidence 또는 정확한 blocker/재개 조건을 기록한다. blocker면 checklist를 완료 처리하지 않는다.
- 테스트 작성: 별도 script 변경은 하지 않는다. 기존 smoke와 dev entrypoint가 실제 경로를 실행하며, 누락된 메모리 invariant는 REVIEW_OPENAI_REBUILDER-1/2의 deterministic tests가 담당한다.
- 중간 검증: `make test-openai-ollama` — `[openai-ollama] OpenAI-compatible Ollama serving test PASSED.`와 exit 0.

## 의존 관계 및 구현 순서

선행 `13`, `16` complete evidence 확인 → REVIEW_OPENAI_REBUILDER-1 owned transfer/lease → REVIEW_OPENAI_REBUILDER-2 patch ledger → REVIEW_OPENAI_REBUILDER-3 fresh/integrated evidence. 새 public Core API와 OpenAI caller를 같은 변경에서 검증한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/ingress_snapshot.go`, `ingress_snapshot_test.go` | REVIEW_OPENAI_REBUILDER-1 |
| `apps/edge/internal/openai/stream_gate_ingress.go`, `openai_request_rebuilder.go`, `provider_model_rewrite.go` | REVIEW_OPENAI_REBUILDER-1, REVIEW_OPENAI_REBUILDER-2 |
| `apps/edge/internal/openai/chat_handler.go`, `responses_handler.go`, `provider_tunnel.go` | REVIEW_OPENAI_REBUILDER-1 |
| `apps/edge/internal/openai/input_estimator.go`, `input_estimator_test.go` | REVIEW_OPENAI_REBUILDER-1 |
| `apps/edge/internal/openai/openai_request_rebuilder_test.go`, `stream_gate_ingress_test.go`, `stream_gate_dispatcher_test.go` | REVIEW_OPENAI_REBUILDER-1, REVIEW_OPENAI_REBUILDER-2 |
| 검증 evidence only | REVIEW_OPENAI_REBUILDER-3 |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` — host Go/GOROOT 일치.
- `gofmt -l packages/go/streamgate/ingress_snapshot.go packages/go/streamgate/ingress_snapshot_test.go apps/edge/internal/openai/stream_gate_ingress.go apps/edge/internal/openai/openai_request_rebuilder.go apps/edge/internal/openai/openai_request_rebuilder_test.go apps/edge/internal/openai/provider_model_rewrite.go apps/edge/internal/openai/input_estimator.go apps/edge/internal/openai/input_estimator_test.go apps/edge/internal/openai/chat_handler.go apps/edge/internal/openai/responses_handler.go apps/edge/internal/openai/provider_tunnel.go` — 출력 없음.
- `go test -count=1 ./packages/go/streamgate ./packages/go/config ./apps/edge/internal/openai ./apps/edge/internal/service` — fresh PASS.
- `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai` — race PASS.
- `if rg --sort path -n 'estimateInputTokens\(string\((body|canonicalBody|output)\)\)|Accessor\(\)\.(Canonical|TypedView)\(' apps/edge/internal/openai/openai_request_rebuilder.go apps/edge/internal/openai/provider_model_rewrite.go apps/edge/internal/openai/responses_handler.go; then exit 1; fi` — 미계상 full-body 변환/방어 복사 callsite 없음.
- `make test-openai-ollama` — OpenAI Chat/Responses/stream과 실제 Edge/Node entrypoint smoke PASS.
- 별도 프로세스로 `./scripts/dev/edge.sh`와 `./scripts/dev/node.sh`를 실행하고 Edge console에서 `/nodes`, 같은 session 메시지 2회, `/capabilities`, `/transport`, `/sessions`를 순서대로 입력한다 — 각 run의 Node payload와 Edge payload 내용/순서 일치, complete가 마지막 payload 뒤 한 번. 실제 stdout/stderr를 기록하며 실행 불가 시 PASS하지 않는다.
- `git diff --check` — 출력 없음.

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