iop/agent-task/unbounded_cli_sessions/plan_1.log
toki 96f79fcd08 feat: unbounded CLI sessions support and related improvements
- Add unbounded CLI sessions agent-task with plan and code review logs
- Add edge console with tests
- Add node run manager for session lifecycle management
- Add router and store tests
- Update transport session handling
- Add runtime proto definitions
- Update configurations and packages
2026-05-03 20:07:09 +09:00

221 lines
8.9 KiB
Text

<!-- task=unbounded_cli_sessions plan=1 tag=REVIEW_REFACTOR -->
# Follow-up Plan - Unbounded CLI Sessions Review Fixes
## 이 파일을 읽는 구현 에이전트에게
이 계획은 `plan_0.log` / `code_review_0.log` 리뷰에서 발견된 Required 이슈만 해결하기 위한 follow-up입니다. 각 체크리스트를 완료하고, 중간/최종 검증을 실행한 뒤 `agent-task/unbounded_cli_sessions/CODE_REVIEW.md`의 모든 섹션에 실제 구현 메모와 명령 출력을 채우세요.
구현 에이전트는 `CODE_REVIEW.md`의 "이 파일을 읽는 리뷰 에이전트에게" 섹션에 적힌 아카이브 지시를 실행하면 안 됩니다. `CODE_REVIEW.md` -> `code_review_*.log`, `PLAN.md` -> `plan_*.log`, `complete.log` 작성은 code-review skill 전용 작업입니다.
## 배경
첫 구현은 unbounded CLI logical session의 큰 구조를 도입했지만, foreground adapter error 반환 계약과 기존 SQLite DB migration 안전성에서 Required 이슈가 남았습니다. 이 follow-up은 새 구조를 유지하면서 호출자 error 관측과 기존 workspace DB 호환성을 회복합니다.
## 의존 관계 및 구현 순서
1. `REVIEW_REFACTOR-1`에서 node foreground error 반환 계약을 복구한다.
2. `REVIEW_REFACTOR-2`에서 store migration을 idempotent하게 추가한다.
3. 두 항목의 중간 검증 후 최종 `go test ./...`를 실행한다.
### [REVIEW_REFACTOR-1] Restore Foreground OnRunRequest Error Return
#### 문제
`OnRunRequest`의 foreground 경로가 adapter failure를 호출자에게 반환하지 않습니다.
현재 코드:
```go
// apps/node/internal/node/node.go:120
run := func() {
defer cancel()
defer n.runs.deregister(spec.RunID)
defer close(h.done)
execErr := adapter.Execute(execCtx, spec, sink)
n.completeRun(spec, execErr)
}
if spec.Background {
go run()
return nil
}
run()
return nil
```
이전 계약과 `plan_0.log`의 pseudocode는 synchronous run에서 `adapter.Execute` error를 반환해야 합니다. 지금 상태에서는 store에는 failed/cancelled가 기록되더라도 transport handler caller는 실패를 알 수 없습니다.
#### 해결 방법
`run` closure가 `error`를 반환하도록 바꾸고, foreground 경로에서는 그 error를 그대로 반환한다. background 경로는 기존처럼 goroutine dispatch 후 `nil`을 반환한다.
계획 코드:
```go
// apps/node/internal/node/node.go:120
run := func() error {
defer cancel()
defer n.runs.deregister(spec.RunID)
defer close(h.done)
execErr := adapter.Execute(execCtx, spec, sink)
n.completeRun(spec, execErr)
return execErr
}
if spec.Background {
go func() { _ = run() }()
return nil
}
return run()
```
`runtime.ErrRunCancelled`도 adapter error이므로 foreground caller에 반환한다. store status는 기존 `completeRun` 로직대로 `cancelled`를 유지한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/node/node.go`: `run` closure return type을 `error`로 변경.
- [ ] `apps/node/internal/node/node.go`: foreground path를 `return run()`으로 변경.
- [ ] `apps/node/internal/node/node.go`: background path는 `go func() { _ = run() }()`로 error를 소비하되 store update는 유지.
- [ ] `apps/node/internal/node/node_test.go`: adapter failure를 반환하는 test double 추가.
- [ ] `apps/node/internal/node/node_test.go`: foreground adapter failure가 `OnRunRequest` return error와 store `failed` status에 모두 반영되는 regression test 추가.
- [ ] `apps/node/internal/node/node_test.go`: foreground `runtime.ErrRunCancelled` 반환 시 `OnRunRequest`가 해당 sentinel error를 반환하고 store status가 `cancelled`가 되는지 필요하면 추가 test로 고정.
#### 테스트 작성
작성한다.
- `apps/node/internal/node/node_test.go`
- `TestOnRunRequest_ForegroundAdapterErrorReturned`: fake adapter가 `errors.New("adapter boom")`을 반환하게 하고 `OnRunRequest`가 non-nil error를 반환하는지, `st.GetRun(...).Status == "failed"`인지, `Error`에 `"adapter boom"`이 들어가는지 검증한다.
- `TestOnRunRequest_ForegroundCancelReturnedAndStored`: blocking adapter 또는 sentinel adapter를 사용해 foreground path에서 `runtime.ErrRunCancelled`가 반환되고 store status가 `cancelled`인지 검증한다. 구현이 복잡하면 첫 번째 test를 필수로 두고 cancelled path는 existing background cancel test로 충분한지 CODE_REVIEW에 근거를 남긴다.
#### 중간 검증
```bash
go test ./apps/node/internal/node
```
예상 결과: node package tests가 통과하고 새 regression test가 실패 계약을 방지한다.
### [REVIEW_REFACTOR-2] Add Idempotent Store Migration For Existing Runs Table
#### 문제
store schema는 새 table 생성에는 `session_id`, `background` 컬럼을 포함하지만, 기존 DB에 이미 `runs` table이 있으면 `CREATE TABLE IF NOT EXISTS`는 아무 것도 바꾸지 않습니다.
현재 코드:
```go
// apps/node/internal/store/store.go:47
func New(dsn string, logger *zap.Logger) (*Store, error) {
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("store: open %s: %w", dsn, err)
}
db.SetMaxOpenConns(1) // SQLite is single-writer
if _, err := db.Exec(schema); err != nil {
return nil, fmt.Errorf("store: migrate: %w", err)
}
logger.Info("store ready", zap.String("dsn", dsn))
return &Store{db: db, logger: logger}, nil
}
```
그런데 `InsertRun`과 `GetRun`은 항상 새 컬럼을 사용합니다.
```go
// apps/node/internal/store/store.go:77
`INSERT INTO runs (run_id, adapter, model, session_id, background, status, created_at) VALUES (?,?,?,?,?,?,?)`
// apps/node/internal/store/store.go:95
`SELECT run_id, adapter, model, session_id, background, status, created_at, completed_at, error FROM runs WHERE run_id=?`
```
기존 workspace DB에서는 insert/select가 `no such column: session_id` 계열 오류로 깨집니다.
#### 해결 방법
`schema` 실행 후 idempotent migration을 실행한다. SQLite는 `ADD COLUMN IF NOT EXISTS` 지원이 환경에 따라 애매할 수 있으므로, `PRAGMA table_info(runs)`로 컬럼 존재 여부를 확인한 뒤 없는 컬럼만 `ALTER TABLE` 한다.
계획 코드:
```go
// apps/node/internal/store/store.go:54
if _, err := db.Exec(schema); err != nil {
return nil, fmt.Errorf("store: migrate: %w", err)
}
if err := migrateRunsTable(db); err != nil {
_ = db.Close()
return nil, fmt.Errorf("store: migrate runs: %w", err)
}
```
```go
// apps/node/internal/store/store.go:57
func migrateRunsTable(db *sql.DB) error {
cols, err := tableColumns(db, "runs")
if err != nil {
return err
}
if !cols["session_id"] {
if _, err := db.Exec(`ALTER TABLE runs ADD COLUMN session_id TEXT NOT NULL DEFAULT 'default'`); err != nil {
return err
}
}
if !cols["background"] {
if _, err := db.Exec(`ALTER TABLE runs ADD COLUMN background INTEGER NOT NULL DEFAULT 0`); err != nil {
return err
}
}
return nil
}
```
`tableColumns`는 `PRAGMA table_info(runs)`의 `name` column을 읽어 `map[string]bool`로 반환한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/store/store.go`: `migrateRunsTable` 추가.
- [ ] `apps/node/internal/store/store.go`: `tableColumns` helper 추가.
- [ ] `apps/node/internal/store/store.go`: migration 실패 시 DB close 후 error 반환.
- [ ] `apps/node/internal/store/store_test.go`: old schema fixture 생성 helper 추가.
- [ ] `apps/node/internal/store/store_test.go`: old schema DB를 `Store.New`로 열고 `InsertRun`/`GetRun`이 새 columns와 함께 동작하는 regression test 추가.
#### 테스트 작성
작성한다.
- `apps/node/internal/store/store_test.go`
- `TestStore_MigratesExistingRunsTable`: temp sqlite file에 구 schema(`run_id`, `adapter`, `model`, `status`, `created_at`, `completed_at`, `error`)만 가진 `runs` table을 먼저 만든다. 이후 `store.New(dsn, ...)`를 호출하고 `InsertRun` with `SessionID: "session-a", Background: true`가 성공하는지, `GetRun`이 해당 값을 읽는지 검증한다.
- `TestStore_MigrationIsIdempotent`: 같은 DB에 `store.New`를 두 번 호출해 두 번째 호출도 실패하지 않는지 검증한다.
#### 중간 검증
```bash
go test ./apps/node/internal/store
```
예상 결과: store package tests가 통과하고 구 schema DB migration regression test가 추가된다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/node.go` | REVIEW_REFACTOR-1 |
| `apps/node/internal/node/node_test.go` | REVIEW_REFACTOR-1 |
| `apps/node/internal/store/store.go` | REVIEW_REFACTOR-2 |
| `apps/node/internal/store/store_test.go` | REVIEW_REFACTOR-2 |
| `agent-task/unbounded_cli_sessions/CODE_REVIEW.md` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2 |
## 최종 검증
```bash
gofmt -w apps/node/internal/node/node.go apps/node/internal/node/node_test.go apps/node/internal/store/store.go apps/node/internal/store/store_test.go
go test ./apps/node/internal/node
go test ./apps/node/internal/store
go test ./...
```
예상 결과: 모든 명령이 성공한다.