<!-- task=m-openai-compatible-raw-tunnel-sideband-passthrough/04_assembled_log plan=1 tag=REVIEW_OBSLOG -->

# Plan - REVIEW_OBSLOG

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

이 follow-up은 이전 코드리뷰 `FAIL`의 Required 2건만 닫는다. 구현 후 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 반드시 실제 구현 내용과 검증 출력으로 채운다. 검증을 실행하고, active 파일은 그대로 두고, 리뷰 준비 상태를 보고한다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단하면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/service blocker, 일반 범위 변경, evidence gap은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan으로 처리한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- Task ids:
  - `assembled-log`: 실행 로그에는 raw chunk payload 대신 assembled `content`, `reasoning`, `tool_calls`, sideband summary, transformed output을 남긴다.
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- Archived plan: `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/04_assembled_log/plan_local_G06_0.log`
- Archived review: `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/04_assembled_log/code_review_local_G06_0.log`
- Verdict: FAIL
- Issue summary: Required 2, Suggested 0, Nit 0
- Required findings:
  - `apps/edge/internal/openai/stream.go:668`: non-streaming `providerChatAssembler.observation()` mutates builder state on every call; `stream.go:928` and `stream.go:940` call it twice in sideband response, so execution log can duplicate assembled content/reasoning/tool names.
  - `apps/edge/internal/openai/server_test.go:5588`: assembled-log fixtures only check content and zero tool count; SDD S08 evidence still needs reasoning, non-empty tool call names/count, sideband log/summary, and transformed response-mode log assertions.
- Affected files:
  - `apps/edge/internal/openai/stream.go`
  - `apps/edge/internal/openai/server_test.go`
  - `apps/edge/internal/openai/log_safety_test.go`
- Verification evidence from failed loop:
  - `go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Passthrough|Sideband|Transformed)|TestLog'`: PASS
  - `go test -count=1 ./apps/edge/internal/openai`: PASS
  - `rg --sort path -n 'zap\.(String|ByteString|Any)\("(raw_body|provider_body|raw_chunk|body|content_preview|reasoning_preview|delta_preview)"' apps/edge/internal/openai`: output empty
  - `git diff --check`: PASS
- Allowed archive reread: read only the archived plan/review paths above if exact prior wording is needed. Do not search `agent-task/archive/**`.

## 범위 결정 근거

포함 범위는 S08 assembled execution log evidence를 정확히 닫는 최소 수정이다. 이미 추가된 raw tunnel/sideband/transformed routing 자체, 계약/spec/docs/config 변경, provider-pool dispatch, backpressure/cancel은 이 follow-up 범위가 아니다.

## 구현 체크리스트

- [ ] `providerChatAssembler.observation()`을 idempotent하게 만들거나 non-streaming sideband END 처리에서 observation을 한 번만 계산해 envelope와 log에 재사용한다.
- [ ] `apps/edge/internal/openai/server_test.go`의 assembled-log fixture를 확장해 `assembled_reasoning`, non-empty `assembled_tool_calls`, `assembled_tool_call_count`, non-streaming sideband response log/summary, transformed `response_mode=transformed` log를 assertion한다.
- [ ] `apps/edge/internal/openai/log_safety_test.go` 또는 기존 forbidden-field 검증이 새 assembled log field 추가 후에도 raw provider body/chunk zap field를 금지하는지 확인하고 필요 시 보강한다.
- [ ] `go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Passthrough|Sideband|Transformed)|TestLog'`를 통과시킨다.
- [ ] `go test -count=1 ./apps/edge/internal/openai`를 통과시킨다.
- [ ] `rg --sort path -n 'zap\.(String|ByteString|Any)\("(raw_body|provider_body|raw_chunk|body|content_preview|reasoning_preview|delta_preview)"' apps/edge/internal/openai`가 출력 없음임을 확인한다.
- [ ] `git diff --check`를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_OBSLOG-1] Idempotent Non-streaming Observation

문제: non-streaming assembler observation이 호출마다 내부 state를 다시 누적한다. Sideband non-streaming response는 envelope 작성과 log 작성에서 observation을 두 번 호출하므로 로그가 실제 provider assembled output보다 크게 기록될 수 있다.

해결 방법: 둘 중 더 작은 변경을 선택한다.

- `providerChatAssembler`에 non-streaming parse 완료 상태를 두어 `observation()`을 여러 번 호출해도 같은 값을 반환하게 한다.
- 또는 `writeProviderTunnelSidebandResponse` END 처리에서 `obs := assembler.observation()`을 한 번만 계산하고 envelope와 log가 같은 `obs`를 재사용하게 한다. 단, 다른 caller가 나중에 같은 문제를 만들지 않도록 함수 idempotence가 더 선호된다.

테스트 작성: `TestChatCompletionsAssembledLogsPassthrough` 또는 별도 non-streaming sideband log test에서 provider body에 content/reasoning/tool call을 넣고, log `assembled_content`와 `assembled_reasoning`이 중복되지 않는지 확인한다.

중간 검증:

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Sideband)'
```

### [REVIEW_OBSLOG-2] S08 Log Evidence Hardening

문제: 현재 assembled-log fixture는 content와 zero tool count 중심이라 SDD S08의 reasoning/tool_calls/transformed output evidence를 충분히 검증하지 못한다.

해결 방법: 기존 fixture를 확장하거나 새 fixture를 추가한다.

- normalized/transformed path: `metadata.iop_response_mode="transformed"` 요청의 log에서 `response_mode=transformed`, `assembled_content`, 필요 시 `assembled_reasoning`을 확인한다.
- pure passthrough 또는 sideband path: provider body/SSE에 `reasoning_content`와 `tool_calls[].function.name`을 넣고 log의 `assembled_reasoning`, `assembled_tool_calls`, `assembled_tool_call_count`를 확인한다.
- sideband path: streaming 또는 non-streaming sideband response가 route/usage/assembled summary를 response surface와 execution log 양쪽에서 검증 가능한 상태인지 확인한다.
- raw payload guard: forbidden zap field 목록에는 raw provider body/chunk field가 남지 않아야 하며, assembled fields는 `assembled_*` 이름으로만 남긴다.

테스트 작성: 작성. 테스트명은 기존 `TestChatCompletionsAssembledLogs*` 계열을 유지해 final `-run` 정규식에 포함되게 한다.

중간 검증:

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Passthrough|Sideband|Transformed)|TestLog'
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/stream.go` | REVIEW_OBSLOG-1 |
| `apps/edge/internal/openai/server_test.go` | REVIEW_OBSLOG-1, REVIEW_OBSLOG-2 |
| `apps/edge/internal/openai/log_safety_test.go` | REVIEW_OBSLOG-2 |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Passthrough|Sideband|Transformed)|TestLog'
go test -count=1 ./apps/edge/internal/openai
rg --sort path -n 'zap\.(String|ByteString|Any)\("(raw_body|provider_body|raw_chunk|body|content_preview|reasoning_preview|delta_preview)"' apps/edge/internal/openai
git diff --check
```

`rg` 명령은 출력이 없어야 한다. 출력이 있으면 raw payload zap field를 operational summary field로 바꾸거나 테스트의 금지 목록을 정확히 갱신한다.
