<!-- task=reasoning_delta_log_preservation/01_node_stdout plan=0 tag=BUG_NODE -->

# Plan - BUG_NODE Node Reasoning Delta Stdout Preservation

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

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

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

## 배경

OneXPlayer Node 로그에서 `reasoning_delta` 이벤트는 남지만 `detail=""`로 출력되어 실제 reasoning delta 본문을 사후 확인할 수 없었다. Node는 이미 `RuntimeEvent.Delta`를 proto `RunEvent.Delta`로 전송하지만, 로컬 stdout 출력기만 `reasoning_delta`의 `Delta`를 쓰지 않는다. 이 subtask는 Node 로컬 stdout에서 reasoning delta를 응답 content와 분리된 prefix로 보존하는 버그픽스다.

## 사용자 리뷰 요청 흐름

선택된 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 `request-execution-log-usage-ledger-foundation` SDD의 raw capture, retention, ledger schema 결정은 범위 밖이며 `Roadmap Targets`를 두지 않는다.

### 테스트 환경 규칙

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

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

- 현재 `apps/node/internal/node/sink_test.go`는 `delta` 즉시 출력과 complete 출력만 검증한다.
- `reasoning_delta`가 `RunEvent.Delta`로 전송되면서 stdout에도 보존되는지 검증하는 테스트가 없다.

### 심볼 참조

- renamed/removed symbols: none
- relevant references: `RuntimeEvent.Delta`는 `sessionSink.Emit`에서 proto `RunEvent.Delta`로 보존된다. stdout 출력은 `sessionSink.printEvent`의 `EventTypeDelta` case와 default branch가 전부다.

### 분할 판단

Split decision policy를 적용했다. 변경은 Node local stdout과 Edge ops console 두 도메인을 모두 포함하므로 `reasoning_delta_log_preservation` task group 아래 독립 subtask로 분리한다.

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

이 subtask는 Node 도메인 내부 파일과 Node package test만 수정한다. Edge subtask와 순서 의존성은 없다.

### 범위 결정 근거

- 포함: Node local stdout에 `reasoning_delta.Delta`를 분리 prefix로 출력, Node sink regression test.
- 제외: Edge ops console 출력, OpenAI-compatible HTTP `reasoning_content`, provider raw SSE chunk 저장, request ledger, retention/redaction 정책.
- 이유: `RunEvent.Delta` 전송은 이미 보존되고 있으며, 문제는 Node stdout 렌더러가 default branch에서 `Message`만 찍는 구현 결함이다.

### 빌드 등급

- `local-G04`: 단일 Node package 내부 버그픽스이며 locally deterministic unit test로 검증 가능하다. Runtime wire schema나 provider 실행 경로는 변경하지 않는다.

## 구현 체크리스트

- [x] [BUG_NODE-1] Node `sessionSink.printEvent`가 `reasoning_delta.Delta`를 `[node-reasoning]` prefix로 출력하고 빈 delta는 무시하도록 구현한다.
- [x] [BUG_NODE-1] `apps/node/internal/node/sink_test.go`에 reasoning delta stdout 보존 regression test를 추가한다.
- [x] [BUG_NODE-1] `go test -count=1 ./apps/node/internal/node`를 실행해 PASS를 확인한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [BUG_NODE-1] Preserve Node Reasoning Delta Stdout

#### 문제

[node.go](/config/workspace/iop/apps/node/internal/node/node.go:816)의 `EventTypeDelta` case만 `event.Delta`를 stdout에 쓴다. [node.go](/config/workspace/iop/apps/node/internal/node/node.go:847)의 default branch는 `reasoning_delta`를 포함한 나머지 이벤트를 `event.Message`로만 출력하므로, `RuntimeEvent{Type: EventTypeReasoningDelta, Delta: "..."}`가 `[node-event] reasoning_delta ... detail=""`로 남는다.

Before:

```go
// apps/node/internal/node/node.go:816
case runtime.EventTypeDelta:
	if event.Delta == "" {
		return
	}
	if !s.streaming {
		fmt.Fprint(s.out, "[node-message] ")
		s.streaming = true
	}
	fmt.Fprint(s.out, event.Delta)
	s.lineEnded = strings.HasSuffix(event.Delta, "\n")
case runtime.EventTypeComplete:
	...
default:
	fmt.Fprintf(s.out, "[node-event] %s run_id=%s detail=%q\n", event.Type, event.RunID, event.Message)
}
```

#### 해결 방법

`EventTypeDelta` 다음에 `EventTypeReasoningDelta` case를 추가한다. Reasoning은 assistant content가 아니므로 `[node-message]`에 섞지 말고 `[node-reasoning]` prefix로 출력한다. 직전 content stream이 줄 끝에서 끝나지 않았으면 먼저 줄바꿈해 message와 reasoning을 분리한다. multiline reasoning은 구현 단순성을 위해 helper를 추가해 각 줄에 prefix가 붙게 한다.

After:

```go
// apps/node/internal/node/node.go:826
case runtime.EventTypeReasoningDelta:
	if event.Delta == "" {
		return
	}
	if s.streaming && !s.lineEnded {
		fmt.Fprintln(s.out)
	}
	s.streaming = false
	s.lineEnded = true
	printPrefixedLines(s.out, "[node-reasoning] ", event.Delta)
case runtime.EventTypeComplete:
	...
```

권장 helper:

```go
func printPrefixedLines(out io.Writer, prefix, text string) {
	text = strings.TrimRight(text, "\n")
	if text == "" {
		return
	}
	for _, line := range strings.Split(text, "\n") {
		fmt.Fprintf(out, "%s%s\n", prefix, line)
	}
}
```

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

- [x] `apps/node/internal/node/node.go`: `EventTypeReasoningDelta` stdout case 추가.
- [x] `apps/node/internal/node/node.go`: multiline delta prefix helper 추가 또는 동일 동작을 기존 함수로 구현.
- [x] `apps/node/internal/node/sink_test.go`: `TestSessionSinkPrintEventPreservesReasoningDelta` 추가.

#### 테스트 작성

Regression test required.

- path: `apps/node/internal/node/sink_test.go`
- test name: `TestSessionSinkPrintEventPreservesReasoningDelta`
- fixture: `sessionSink` with `bytes.Buffer`, then emit start, `EventTypeReasoningDelta` with `Delta: "사용자가 인사했습니다.\\n</think>\\n"`, complete.
- assertions:
  - proto event for reasoning keeps `Type == "reasoning_delta"` and `Delta` text.
  - stdout contains `[node-reasoning] 사용자가 인사했습니다.` and `[node-reasoning] </think>`.
  - stdout does not contain `reasoning_delta run_id=` with `detail=""`.
  - `[node-message]` count remains `0` for reasoning-only output.

#### 중간 검증

```bash
go test -run '^TestSessionSinkPrintEventPreservesReasoningDelta$' -count=1 ./apps/node/internal/node
```

Expected: PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/node.go` | BUG_NODE-1 |
| `apps/node/internal/node/sink_test.go` | BUG_NODE-1 |

## 최종 검증

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

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

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