<!-- task=m-workflow-core/04+03_retry_timeout plan=0 tag=WFC-RETRY -->

# Workflow Core Retry Timeout Plan

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

이 계획은 `03+01_failure_policy` PASS 후 진행한다. 구현 완료는 active `CODE_REVIEW-cloud-G07.md` 작성까지다. 사용자 결정이나 외부 환경 준비가 필요한 blocker만 `사용자 리뷰 요청`으로 기록한다.

## 배경

River worker는 `MaxAttempts: 3`을 사용하고 lifecycle은 failed task를 다시 running으로 시작할 수 있지만, retry 정책이 코드/metadata/docs에서 한 흐름으로 읽히지 않는다. Timeout은 `WORKFLOW_TASK_TIMEOUT_SEC`로 설정되지만 실패 사유와 retry 가능성의 관계도 명확하지 않다. 이 작업은 최소 retry/timeout 정책을 상수, metadata, 문서, tests로 고정한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 USER_REVIEW 작성 여부를 판단한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- Task ids:
  - `retry-timeout`: retry와 timeout의 최소 정책
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/workflow-core/PHASE.md`
- `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- `services/core/internal/workflow/model.go`
- `services/core/internal/workflow/lifecycle.go`
- `services/core/internal/workflow/service_test.go`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/internal/scheduler/river.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `services/core/README.md`

### 테스트 환경 규칙

- test_env: `local`
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/core-smoke.md`
- 적용 명령: `cd services/core && go test ./...`
- focused 명령: `cd services/core && go test -count=1 ./internal/workflow ./internal/scheduler ./internal/config`

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

- Config timeout default/override tests exist.
- Worker timeout failure test exists but only checks fail message.
- River `MaxAttempts` is hard-coded at [services/core/internal/scheduler/river.go](/config/workspace/nomadcode/services/core/internal/scheduler/river.go:43) and not covered by a direct test.

### 심볼 참조

- planned new symbols: `DefaultTaskMaxAttempts`, optional metadata keys `retryable`, `max_attempts`.
- existing references: `RunTimeout` in scheduler worker and config; `WorkflowTaskTimeoutSec` in config and server wiring.

### 분할 판단

- task group: `m-workflow-core`
- this subtask: `04+03_retry_timeout`
- predecessor: `03+01_failure_policy`; active plan exists, `complete.log` pending.
- split reason: retry/timeout policy depends on structured failure metadata but should be reviewed separately from failure recording.

### 범위 결정 근거

- No custom River backoff policy unless needed; minimum policy is max attempts + canonical metadata/docs.
- No external provider retry/projection retry implementation; projection retry is later integration work.
- No user-facing UI changes.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`
- 근거: retry semantics affect scheduler behavior and failure observability.

## 구현 체크리스트

- [ ] [WFC-RETRY-1] retry policy constants and metadata/doc hooks를 추가한다.
- [ ] [WFC-RETRY-2] timeout policy tests and docs를 failure metadata와 연결한다.
- [ ] 중간 및 최종 검증 명령을 실행하고 출력 원문을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `03+01_failure_policy` complete.log가 필요하다.
- Runtime dependency source: directory name `04+03_retry_timeout`.

## [WFC-RETRY-1] Minimum Retry Policy

### 문제

- [services/core/internal/scheduler/river.go](/config/workspace/nomadcode/services/core/internal/scheduler/river.go:43)는 `MaxAttempts: 3`을 inline으로 둔다.
- [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:27)는 failed -> running retry를 허용하지만 max attempts나 retryable 판단은 보이지 않는다.

### 해결 방법

Before [services/core/internal/scheduler/river.go](/config/workspace/nomadcode/services/core/internal/scheduler/river.go:37):

```go
riverClient, err := river.NewClient[pgx.Tx](driver, &river.Config{
	Logger: logger,
	Queues: map[string]river.QueueConfig{
		river.QueueDefault: {MaxWorkers: 2},
	},
	Workers:     workers,
	MaxAttempts: 3,
})
```

After:

```go
const DefaultTaskMaxAttempts = 3

riverClient, err := river.NewClient[pgx.Tx](driver, &river.Config{
	Logger: logger,
	Queues: map[string]river.QueueConfig{
		river.QueueDefault: {MaxWorkers: 2},
	},
	Workers:     workers,
	MaxAttempts: DefaultTaskMaxAttempts,
})
```

Add metadata updates on start/failure so each attempt records `attempt`, and failure records `retryable` based on `attempt < DefaultTaskMaxAttempts` when scheduler has enough context. If River job attempt metadata is not exposed in the current API, document that canonical retry is inferred by lifecycle `attempt` and River max attempts, and leave `retryable` out rather than inventing unreliable data.

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

- [ ] `services/core/internal/scheduler/river.go`: introduce `DefaultTaskMaxAttempts`.
- [ ] `services/core/internal/scheduler/jobs.go`: use existing lifecycle attempt metadata; add retryable metadata only if reliable.
- [ ] `services/core/internal/workflow/model.go`: add metadata keys only if actually written.
- [ ] `services/core/README.md`: document minimum retry policy.

### 테스트 작성

- Required:
  - `TestWorkRetriesAfterFailureState` should assert retry path remains valid.
  - Add a scheduler/config test or README-only assertion if `DefaultTaskMaxAttempts` cannot be directly observed without River internals.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow
```

Expected: retry tests pass.

## [WFC-RETRY-2] Timeout Policy

### 문제

- [services/core/internal/config/config.go](/config/workspace/nomadcode/services/core/internal/config/config.go:57) has `WORKFLOW_TASK_TIMEOUT_SEC` default 300.
- [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:64) applies `RunTimeout`, but docs do not connect timeout to canonical failed state and retry policy.

### 해결 방법

Update README to state:

- Timeout wraps a single worker execution attempt.
- Timeout is recorded as `failed` with `failure_type=timeout` and `error=timeout`.
- River may retry failed attempts up to `DefaultTaskMaxAttempts`.
- A task can be re-enqueued from `failed` while terminal `completed/canceled` cannot restart.

Tests should verify timeout path still returns error, marks failed, and carries timeout classification from predecessor failure policy.

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

- [ ] `services/core/internal/scheduler/jobs_test.go`: strengthen timeout assertions.
- [ ] `services/core/internal/config/config_test.go`: keep timeout default/override tests passing.
- [ ] `services/core/README.md`: add retry/timeout section near execution configuration.

### 테스트 작성

- Required: update `TestWorkMarksTimeoutFailure`.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/scheduler ./internal/config
```

Expected: timeout and config tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/scheduler/river.go` | WFC-RETRY-1 |
| `services/core/internal/scheduler/jobs.go` | WFC-RETRY-1, WFC-RETRY-2 |
| `services/core/internal/scheduler/jobs_test.go` | WFC-RETRY-1, WFC-RETRY-2 |
| `services/core/internal/workflow/model.go` | WFC-RETRY-1 |
| `services/core/internal/config/config_test.go` | WFC-RETRY-2 |
| `services/core/README.md` | WFC-RETRY-1, WFC-RETRY-2 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/workflow ./internal/scheduler ./internal/config
cd services/core && go test ./...
```

Expected: focused and full tests pass. Fresh `-count=1` is required for focused packages; final full run may use cache.

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