<!-- task=reasoning_delta_log_preservation/02_edge_opsconsole plan=0 tag=BUG_EDGE -->

# Plan - BUG_EDGE Edge Ops Console Reasoning Delta Preservation

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

구현 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 검증 명령을 실행하고 실제 출력, 구현 내용, 계획 대비 변경 사항을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 정확한 근거로 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, 서비스 준비, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan으로 처리한다.

## 배경

Node가 `reasoning_delta`를 Edge로 전송하더라도 Edge ops console은 `delta`만 응답 stream으로 출력하고 나머지는 `detail=event.Message`로 출력한다. 그래서 reasoning delta 본문이 `detail=""`로 사라지고, 사용자가 콘솔 로그에서 실제 think/reasoning 텍스트를 확인할 수 없다. 이 subtask는 Edge background async 출력과 foreground `SendRun` 출력 모두에서 reasoning delta를 content 응답과 분리해 보존한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 질문은 금지이며, code-review가 사용자 리뷰 요청의 정당성 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md`
- `agent-roadmap/sdd/operational-observability-provider-management/request-execution-log-usage-ledger-foundation/SDD.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/sink_test.go`
- `apps/edge/internal/opsconsole/events.go`
- `apps/edge/internal/opsconsole/events_test.go`
- `apps/edge/internal/opsconsole/console.go`
- `apps/edge/internal/opsconsole/console_test.go`

### SDD 기준

not applicable. 이 작업은 활성 로그/ledger Milestone 완료를 주장하지 않는 비로드맵 버그픽스다. locked SDD의 request ledger owner, raw provider chunk capture, retention/redaction 결정은 범위 밖이며 `Roadmap Targets`를 두지 않는다.

### 테스트 환경 규칙

- `test_env`: local
- `agent-test/local/rules.md`: present/read
- matched profile: `agent-test/local/edge-smoke.md`
- 적용 명령: 변경한 edge package unit quick check로 `go test -count=1 ./apps/edge/internal/opsconsole`
- 사전 확인: `go test -run '^$' ./apps/node/internal/node ./apps/edge/internal/opsconsole` PASS
- fallback: 없음. 원격 field Edge, provider, bootstrap 검증은 이 console 렌더링 subtask에 필요하지 않다.

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

- `apps/edge/internal/opsconsole/events_test.go`는 background `delta` 출력과 complete 순서만 검증한다.
- `apps/edge/internal/opsconsole/console_test.go`는 `SendRun` metadata와 command formatting을 검증하지만 foreground `reasoning_delta` 렌더링은 검증하지 않는다.
- `reasoning_delta`가 `[node*-msg]` content에 섞이지 않고 별도 prefix로 보존되는지 검증이 없다.

### 심볼 참조

- renamed/removed symbols: none
- relevant references:
  - `apps/edge/internal/opsconsole/events.go` handles async background run events with `case "delta"` and default branch.
  - `apps/edge/internal/opsconsole/console.go` handles foreground run events with `case "delta"` and default branch.
  - OpenAI-compatible response paths already consume `reasoning_delta` separately, but this subtask does not change those files.

### 분할 판단

Split decision policy를 적용했다. 전체 bugfix는 Node와 Edge 도메인을 건드리므로 `reasoning_delta_log_preservation` task group 아래 독립 subtask로 분리한다.

- `01_node_stdout`: Node local stdout 보존. dependency 없음.
- `02_edge_opsconsole`: Edge foreground/background ops console 보존. dependency 없음.

이 subtask는 Edge opsconsole package 내부에서 완료 가능하며 Node subtask 완료를 기다리지 않는다.

### 범위 결정 근거

- 포함: Edge background `EventRouter.printAsyncLocked`, foreground `SendRun` event switch, Edge opsconsole tests.
- 제외: Node local stdout, OpenAI HTTP streaming/non-streaming surfaces, A2A task store, provider raw SSE chunk storage, request ledger schema.
- 이유: Edge ops console에서 `reasoning_delta`는 console 렌더링 문제이며 runtime wire나 OpenAI response mapping을 변경할 필요가 없다.

### 빌드 등급

- `local-G04`: bounded Edge package rendering bugfix다. 두 call site가 있지만 package-local tests로 deterministic하게 확인 가능하고 public schema 변경이 없다.

## 구현 체크리스트

- [ ] [BUG_EDGE-1] Edge opsconsole에 reasoning delta 전용 prefix/helper를 추가하고 background async 출력에서 `event.GetDelta()`를 보존한다.
- [ ] [BUG_EDGE-1] foreground `SendRun` 출력에서도 `reasoning_delta`를 같은 prefix/helper로 보존한다.
- [ ] [BUG_EDGE-1] background와 foreground/helper 동작을 검증하는 regression test를 추가한다.
- [ ] [BUG_EDGE-1] `go test -count=1 ./apps/edge/internal/opsconsole`를 실행해 PASS를 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [BUG_EDGE-1] Preserve Edge Ops Console Reasoning Delta

#### 문제

[events.go](/config/workspace/iop/apps/edge/internal/opsconsole/events.go:202)는 background run event에서 `"delta"`만 response stream으로 출력하고, [events.go](/config/workspace/iop/apps/edge/internal/opsconsole/events.go:216)의 default branch는 `reasoning_delta`를 `event.GetMessage()`로 출력한다. [console.go](/config/workspace/iop/apps/edge/internal/opsconsole/console.go:282) foreground loop도 같은 구조라 `reasoning_delta.Delta`가 `detail=""`로 사라진다.

Before:

```go
// apps/edge/internal/opsconsole/events.go:202
switch event.GetType() {
case "start":
	...
case "delta":
	r.responseStream(runID, label).Write(event.GetDelta())
case "complete":
	...
default:
	fmt.Fprintf(r.out, "%s %s run_id=%s detail=%q\n", nodeEventPrefix(label), event.GetType(), runID, event.GetMessage())
}
```

```go
// apps/edge/internal/opsconsole/console.go:282
switch event.GetType() {
case "start":
	...
case "delta":
	response.Write(event.GetDelta())
case "complete":
	...
default:
	fmt.Fprintf(out, "%s %s run_id=%s detail=%q\n", nodeEventPrefix(label), event.GetType(), dispatch.RunID, event.GetMessage())
}
```

#### 해결 방법

`reasoning_delta`를 response content와 분리해 `[<node-label>-reasoning]` prefix로 출력한다. Existing `ResponseStream` multiline prefix 동작을 재사용할 수 있도록 `nodeReasoningPrefix(label)`와 `writeReasoningDelta(out, label, delta)` helper를 package-private로 추가한다. Empty delta는 출력하지 않는다.

After:

```go
case "reasoning_delta":
	writeReasoningDelta(r.out, label, event.GetDelta())
case "delta":
	r.responseStream(runID, label).Write(event.GetDelta())
```

Foreground:

```go
case "reasoning_delta":
	writeReasoningDelta(out, label, event.GetDelta())
case "delta":
	response.Write(event.GetDelta())
```

Helper sketch:

```go
func nodeReasoningPrefix(label string) string {
	return fmt.Sprintf("[%s-reasoning] ", label)
}

func writeReasoningDelta(out io.Writer, label, delta string) {
	stream := NewResponseStream(out, nodeReasoningPrefix(label))
	stream.Write(delta)
	stream.FinishIfStarted()
}
```

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/opsconsole/events.go`: `reasoning_delta` async case 추가.
- [ ] `apps/edge/internal/opsconsole/events.go`: reasoning prefix/helper 추가.
- [ ] `apps/edge/internal/opsconsole/console.go`: foreground `reasoning_delta` case 추가.
- [ ] `apps/edge/internal/opsconsole/events_test.go`: async/background 또는 helper regression test 추가.
- [ ] `apps/edge/internal/opsconsole/console_test.go`: foreground path 또는 shared helper 사용을 보장하는 regression test 추가.

#### 테스트 작성

Regression tests required.

- path: `apps/edge/internal/opsconsole/events_test.go`
- test name: `TestEventRouterPrintsReasoningDeltaForAsyncRun`
- fixture: background `RunEvent{Type:"reasoning_delta", Delta:"사용자가 인사했습니다.\\n</think>\\n", Background:true}`.
- assertions: output contains `[unknown-reasoning] 사용자가 인사했습니다.` and `[unknown-reasoning] </think>`, does not contain `reasoning_delta run_id=` with `detail=""`, and does not create `[unknown-msg]` content.

- path: `apps/edge/internal/opsconsole/console_test.go`
- test name: `TestSendRunPrintsReasoningDeltaForForegroundRun` or a focused package-private render helper test if `SendRun` plumbing would add excessive fixture complexity.
- assertion goal: the foreground code path or the helper used by foreground code prints `[node0-reasoning] ...` from `event.GetDelta()` instead of `detail=""`.

#### 중간 검증

```bash
go test -run 'Test(EventRouterPrintsReasoningDeltaForAsyncRun|SendRunPrintsReasoningDeltaForForegroundRun|PrintReasoningDeltaPrefixesEachLine)$' -count=1 ./apps/edge/internal/opsconsole
```

Expected: PASS. If the implementer uses a different focused helper test name, record the replacement command and reason in `계획 대비 변경 사항`.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/opsconsole/events.go` | BUG_EDGE-1 |
| `apps/edge/internal/opsconsole/console.go` | BUG_EDGE-1 |
| `apps/edge/internal/opsconsole/events_test.go` | BUG_EDGE-1 |
| `apps/edge/internal/opsconsole/console_test.go` | BUG_EDGE-1 |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/opsconsole
```

Expected: PASS. Go test cache output is not acceptable; use `-count=1`.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
