<!-- /autoplan restore point: ~/.gstack/projects/isdp/master-autoplan-restore-20260602-160338.md -->
# ACP通信层错误处理机制改进

## 背景

**问题**：ACP通信层缺少错误处理机制
1. OpenCode进程启动后，因配置验证失败，没有向stdout写入任务内容
2. stderr只被消费不记录，导致无法诊断失败原因

**典型故障链路**：
```
进程启动 → 配置验证失败 → stderr写入错误 → stderr被丢弃 → 进程退出
→ stdout无输出 → SendRequest阻塞 → 超时返回"initialize failed"
→ 用户无法诊断真正原因
```

## 方案选择

**方案A：stderr缓冲 + 请求失败时返回**（已批准）
- 使用 `strings.Builder` 缓存 stderr 内容
- 在请求失败或进程异常退出时，将 stderr 内容附加到错误信息
- 正常执行完成后清空缓冲

## 修改范围

| 文件 | 改动类型 |
|------|----------|
| `internal/service/agent/plugins/acp/adapter_base.go` | 修改 |

## 详细实施步骤

### Step 1: 添加 stderr 缓冲结构
**时间**：3分钟

在 `acpSession` 结构体中添加 stderr 缓冲字段：

```go
type acpSession struct {
    // ... 现有字段
    stderrOutput strings.Builder  // 新增：stderr 输出缓冲
}
```

### Step 2: 修改 stderr 消费逻辑（ExecuteWithStream）
**时间**：5分钟

修改第144-150行的 stderr 消费 goroutine：

```go
wg.Add(1)
go func() {
    defer wg.Done()
    scanner := bufio.NewScanner(stderrPipe)
    for scanner.Scan() {
        line := scanner.Text()
        // 记录到日志
        LogInfo("ACP: stderr output", zap.String("line", line))
        // 缓存到 session
        session.mu.Lock()
        session.stderrOutput.WriteString(line)
        session.stderrOutput.WriteString("\n")
        session.mu.Unlock()
    }
    if err := scanner.Err(); err != nil {
        LogError("ACP: stderr scanner error", zap.Error(err))
    }
}()
```

**注意**：需要在 `cmd.Start()` 之后、创建 session 之前启动此 goroutine，因此需要调整代码顺序：
1. 先创建 session（不含 transport）
2. 启动 stderr goroutine（引用 session）
3. 创建 transport

### Step 3: 修改 stderr 消费逻辑（StartSession）
**时间**：5分钟

修改第339-343行的 stderr 消费 goroutine：

```go
go func() {
    scanner := bufio.NewScanner(stderrPipe)
    for scanner.Scan() {
        line := scanner.Text()
        LogInfo("ACP: stderr output (StartSession)",
            zap.String("sessionID", sessionID),
            zap.String("line", line))
        session.mu.Lock()
        session.stderrOutput.WriteString(line)
        session.stderrOutput.WriteString("\n")
        session.mu.Unlock()
    }
}()
```

### Step 4: 增强错误返回逻辑（ExecuteWithStream）
**时间**：5分钟

在以下失败点附加 stderr 内容：

1. **initialize 失败**（第184-188行）：
```go
if err != nil {
    session.mu.Lock()
    stderrContent := session.stderrOutput.String()
    session.mu.Unlock()
    a.cleanup(session)
    wg.Wait()
    enhancedErr := fmt.Errorf("ACP: initialize handshake failed: %w\nstderr: %s", err, stderrContent)
    return nil, enhancedErr
}
```

2. **session/new 失败**（第208-212行）：
```go
if err != nil {
    session.mu.Lock()
    stderrContent := session.stderrOutput.String()
    session.mu.Unlock()
    a.cleanup(session)
    wg.Wait()
    enhancedErr := fmt.Errorf("ACP: session/new failed: %w\nstderr: %s", err, stderrContent)
    return nil, enhancedErr
}
```

3. **session/prompt 失败**（第237-241行）：
```go
if err != nil {
    session.mu.Lock()
    stderrContent := session.stderrOutput.String()
    session.mu.Unlock()
    a.cleanup(session)
    wg.Wait()
    enhancedErr := fmt.Errorf("ACP: session/prompt failed: %w\nstderr: %s", err, stderrContent)
    return nil, enhancedErr
}
```

### Step 5: 增强错误返回逻辑（StartSession）
**时间**：3分钟

在以下失败点附加 stderr 内容：

1. **initialize 失败**（第365-369行）：
```go
if err != nil {
    session.mu.Lock()
    stderrContent := session.stderrOutput.String()
    session.mu.Unlock()
    transport.Close()
    cmd.Process.Kill()
    return fmt.Errorf("ACP: initialize handshake failed: %w\nstderr: %s", err, stderrContent)
}
```

2. **session/new 失败**（第380-384行）：
```go
if err != nil {
    session.mu.Lock()
    stderrContent := session.stderrOutput.String()
    session.mu.Unlock()
    transport.Close()
    cmd.Process.Kill()
    return fmt.Errorf("ACP: session/new failed: %w\nstderr: %s", err, stderrContent)
}
```

### Step 6: 添加进程状态检查
**时间**：5分钟

在 `cmd.Start()` 后添加进程状态检查，及早发现启动失败：

```go
// 给进程一小段时间稳定启动（50ms）
time.Sleep(50 * time.Millisecond)

// 检查进程是否仍在运行
if cmd.Process == nil {
    return nil, fmt.Errorf("ACP: process failed to start")
}

// 尝试读取进程状态（不阻塞）
select {
case <-done: // 进程已退出
    session.mu.Lock()
    stderrContent := session.stderrOutput.String()
    session.mu.Unlock()
    return nil, fmt.Errorf("ACP: process exited immediately after start\nstderr: %s", stderrContent)
default:
    // 进程仍在运行，继续
}
```

**注意**：需要调整 wg.Add(1) 在 cmd.Start() 之前，以便能检测进程退出。

### Step 7: 编写单元测试
**时间**：10分钟

创建测试文件 `internal/service/agent/plugins/acp/adapter_base_test.go`：

```go
// @feature F001 - Agent 对话核心
// @priority P1
// @id ACP-01-01

func TestStderrBuffering(t *testing.T) {
    // 测试 stderr 内容被缓冲
}

func TestStderrInError(t *testing.T) {
    // 测试失败时 stderr 内容附加到错误
}

func TestProcessImmediateExit(t *testing.T) {
    // 测试进程立即退出时能捕获 stderr
}
```

### Step 8: 手动验证
**时间**：5分钟

使用故意错误的配置启动 OpenCode，验证：
1. stderr 内容出现在日志中
2. 返回的错误包含 stderr 内容
3. 能看到配置验证失败的具体原因

### Step 9: 提交代码
**时间**：2分钟

```bash
git add internal/service/agent/plugins/acp/adapter_base.go
git add internal/service/agent/plugins/acp/adapter_base_test.go
git commit -m "feat(acp): add stderr buffering and error enhancement

- Add stderrOutput field to acpSession for buffering stderr content
- Log stderr lines in real-time for debugging visibility
- Append stderr content to error messages on request failures
- Add process state check after cmd.Start() to catch immediate exits

This enables diagnosis of CLI startup failures (e.g., config validation
errors) that were previously hidden due to stderr being discarded.

Co-Authored-By: Claude <noreply@anthropic.com>"
```

## 总预估时间

约 40 分钟（不含测试运行和验证迭代）

## 风险评估

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| stderr 内容过大 | 内存占用 | 正常执行完成后清空；考虑上限 |
| 并发读写 stderrOutput | 数据竞争 | 使用 session.mu 保护 |
| 进程状态检查引入延迟 | 性能影响 | 50ms 是经验值，可调整 |

## 验收标准

1. ✅ stderr 内容实时记录到日志
2. ✅ 请求失败时错误信息包含 stderr 内容
3. ✅ 进程立即退出时能捕获失败原因
4. ✅ 不影响正常执行的流程和性能
5. ✅ 单元测试覆盖核心逻辑

---

<!-- AUTONOMOUS DECISION LOG -->
## Decision Audit Trail (autoplan)

| # | Phase | Decision | Classification | Principle | Rationale | Rejected |
|---|-------|----------|-----------|-----------|----------|----------|
| 1 | CEO | 扩展scope到并发进程退出监控 | Mechanical | P1+P2 | 根本原因是缺少cmd.Wait goroutine，而非stderr缓冲 | - |
| 2 | CEO | 引用Claude Code adapter作为参考 | Mechanical | P4+P3 | Claude Code adapter已有成熟实现(adapter.go:240-260) | - |
| 3 | CEO | 添加ACPError结构化错误类型 | Taste | P5 | 长期架构改进，但这是scope扩展，需用户确认 | 暂时defer |
| 4 | CEO | 保留stderr缓冲方案 | Mechanical | P1 | stderr缓冲仍需要，但应结合cmd.Wait goroutine | - |
| 5 | Eng | 添加cmd.Wait goroutine模式 | Mechanical | P1+P2 | 必须添加并发进程退出监控，替换50ms sleep | - |
| 6 | Eng | 移除50ms sleep | Mechanical | P1 | sleep惩罚100%执行，使用select-based detection | Step 6 sleep方案 |
| 7 | Eng | 添加interruptible RPC wrapper | Taste | P5 | 创建SendRequestInterruptible函数，架构改动 | 暂时defer |
| 8 | Eng | 扩展测试到9个cases | Mechanical | P1 | 原计划3测试遗漏大量边界情况 | 原3测试 |
| 9 | Eng | 添加stderr截断(4KB)和token masking | Mechanical | P1 | stderr可能包含敏感信息 | 无截断 |
| 10 | Eng | 修复StartSession stderr goroutine leak | Mechanical | P1 | 添加wg.Add(1)和wg.Wait() | 无wg |

---

## 审查发现摘要

### CEO Review (Strategy)
- **Critical**: 计划误判根本原因 - 真正问题是缺少并发进程退出监控(cmd.Wait goroutine)
- Claude Code adapter已有正确实现模式(adapter.go:240-260)
- 50ms sleep是hack，惩罚100%执行

### Eng Review (Architecture)
- **Critical**: 缺少cmd.Wait goroutine导致进程退出时RPC阻塞
- **High**: 50ms sleep性能惩罚 unacceptable
- **High**: 测试覆盖不足(原3个→需9个)
- **Medium**: stderr内存无上限、可能含敏感信息
- **High**: StartSession stderr goroutine泄漏

### 架构方案推荐
参考 Claude Code adapter 模式：
1. stderr goroutine缓冲内容
2. cmd.Wait goroutine + cmdWaitDone channel
3. select-based interruptible RPC calls
4. 零延迟、覆盖所有退出时机

---

## Phase 4: Final Approval Gate (User Approved)

### User Decisions (Approved 2026-06-02)

| Decision | User Choice | Rationale |
|----------|-------------|-----------|
| **跳过Step 6 (50ms sleep)** | APPROVED: SKIP | sleep hack惩罚100%执行，应使用cmd.Wait goroutine替代 |
| **添加stderr缓冲上限** | APPROVED: 64KB | 防止OOM风险，stderr无限增长可能导致内存耗尽 |

### Final Plan Adjustments

**Based on user approval, the plan is adjusted to:**

1. **Step 1-5 保持不变** - stderr缓冲和错误返回逻辑
2. **Step 6 已移除** - 不使用50ms sleep hack
3. **Step 2/3 增强** - 添加64KB截断上限：
   ```go
   const maxStderrSize = 64 * 1024 // 64KB上限
   
   // 在写入stderr时检查上限
   if session.stderrOutput.Len() < maxStderrSize {
       session.stderrOutput.WriteString(line)
       session.stderrOutput.WriteString("\n")
   }
   ```
4. **Step 5 增强** - 添加scanner.Err()处理
5. **Step 7 测试扩展** - 增加边界情况测试

### Taste Decisions Deferred

| # | Decision | Reason for Deferral |
|---|----------|---------------------|
| 1 | 添加cmd.Wait goroutine | Requires larger refactor (interruptible RPC wrapper) |
| 2 | ACPError结构化错误类型 | Long-term architecture improvement |

---

## GSTACK REVIEW REPORT

| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | ISSUES_FOUND | 1 critical (root cause), 2 high |
| Eng Review | `/plan-eng-review` | Architecture & tests | 1 | ISSUES_FOUND | 1 critical (cmd.Wait), 3 high, 2 medium |
| Design Review | `/plan-design-review` | UI/UX gaps | 0 | SKIPPED | No UI scope |
| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | SKIPPED | No DX scope |

**UNRESOLVED**: 2 taste decisions deferred (cmd.Wait goroutine, ACPError type)
**VERDICT**: APPROVED WITH MODIFICATIONS - User approved skip Step 6 + 64KB cap

---

## Next Steps

@CoreDev全栈开发工程师 请执行TDD开发，实施本计划（注意：已移除Step 6，添加64KB上限）