# Codex Runtime Instructions Prompt Split

Date: 2026-04-12
Status: design-draft
Scope: 将 `codex` runtime 的 system prompt 从 `turn/start` user input 迁移到 `thread/start` 的 `baseInstructions` / `developerInstructions` 字段。`codex` 的实现是 Codex app-server JSON-RPC process。
Related: [`specs/daemon/prompt-compiler-architecture.md`](prompt-compiler-architecture.md), [`docs/codex-app-server-native-protocol.md`](../../docs/codex-app-server-native-protocol.md), [`desktop/internal/dispatcher/prompt_compiler.go`](../../desktop/internal/dispatcher/prompt_compiler.go), [`desktop/internal/promptcompiler/compile.go`](../../desktop/internal/promptcompiler/compile.go), [`desktop/internal/agent/driver_codex.go`](../../desktop/internal/agent/driver_codex.go)

Implementation note: 本文的设计目标已经通过 prompt compiler 架构落地。当前实现不再依赖 `prompt.go` / `prependIdentityNotice()` 直接拼装 `codex` prompt，而是由 `desktop/internal/promptcompiler/*` 编译 thread contract / turn context，再由 `desktop/internal/dispatcher/prompt_compiler.go` 把 compiler 输出映射到 `StartConfig.{BaseInstructions,DeveloperInstructions,FreshPrompt,ResumePrompt}`。下面的“builder / prompt.go”表述应理解为设计动机；实际 source of truth 以 prompt compiler 边界为准。`codex app-server` sandbox 配置由 agent `permissionMode` 决定：`full_access` 使用 `sandbox_mode="danger-full-access"`、thread `sandbox: "danger-full-access"`、turn `sandboxPolicy: { type: "dangerFullAccess" }`；`agent_workspace_write` 使用 `sandbox_mode="workspace-write"`、thread `sandbox: "workspace-write"`、turn `sandboxPolicy: { type: "workspaceWrite", writableRoots: [workDir] }`。不再发送 `permissionProfile`。

## 1. 背景

### 1.1 设计立项时现状

当 AgentTeam 启动一个 `codex` agent 时，所有内容——包括系统指令（身份声明、行为规范、memory 管理、skill 文档等）和用户消息——都被拼接成一个字符串，作为第一个 `turn/start` 请求的 `input[0].text` 发送给 Codex app-server。

设计立项时的旧流程：

```
buildStartPrompt()           →  StartConfig.Prompt
prependIdentityNotice()      →  (prepend to Prompt)
startThread(ctx, cfg)        →  thread/start { cwd, sandbox, approvalPolicy, model }
startTurnParams(cfg.Prompt)  →  turn/start { threadId, input: [{ type: "text", text: <entire prompt> }] }
```

这意味着：

1. **system prompt 被当成 user message**：模型将完整的系统指令（数千 token）视为第一条用户消息，而非系统级指令
2. **占用 turn 历史**：system prompt 作为 turn #0 存在于 `thread.turns` 中
3. **compaction 不安全**：当上下文压缩发生时，system prompt 可能被摘要化，导致关键行为指令丢失
4. **rollback 不安全**：`thread/rollback` 如果回退范围覆盖到 turn #0，会丢失系统指令

### 1.2 上游能力

Codex app-server 的 `ThreadStartParams` 和 `ThreadResumeParams` schema 均支持两个 instruction 字段（基于 `codex-cli 0.120.0` 验证）：

```json
{
  "baseInstructions": { "type": ["string", "null"] },
  "developerInstructions": { "type": ["string", "null"] }
}
```

`thread/resume` 的 schema 同样支持这两个字段，但本地对 `codex-cli 0.120.0` 的实际探测显示：existing thread 在 `thread/resume` 后并不会可靠地切换到新的 instructions。`thread/start` 会生效；`thread/resume` 对 instruction 字段的接受应视为 best-effort，而不是可依赖的热更新语义。

设计立项时，这两个字段在 AgentTeam 实现中完全未使用。当前实现已经通过 prompt compiler + dispatcher/driver split 使用它们。

## 2. 目标

将 `buildStartPrompt()` 生成的完整 prompt 拆分为三块：

- **instructions（核心不变量）**：丢失后 agent 会立刻违反核心行为约束的内容，通过 `thread/start` 和 `thread/resume` 的 instruction 字段传递
- **turn history reference（参考手册）**：协议示例、格式细则、工具目录等查阅性内容，作为首次 `turn/start` 的 user input 传递，可被 compaction 摘要化
- **dynamic user input（动态内容）**：用户消息、wake prompt 等每次不同的内容，作为 `turn/start` 的 user input 传递

## 3. 拆分方案

### 3.1 拆分维度：核心不变量 vs 参考手册

拆分的判断标准不是"静态 vs 动态"或"长 vs 短"，而是：

> **如果这段内容在 compaction / rollback / resume 之后丢掉，agent 会不会立刻违反核心行为约束？**

- **会** → 进 instructions（核心不变量），永久 pin 在 context window 中
- **不会，只是降低效率** → 留在 turn history（参考手册），可被 compaction 压缩

这个标准直接决定了 context window 的使用效率：instructions 永久占用空间，所以只有真正"丢不得"的内容才应该放进去。

### 3.2 当前 prompt 各段落的归类

#### `buildStartPrompt()` 的段落（`prompt.go`）

| # | 段落 | 归类 | 理由 |
|---|------|------|------|
| 1 | 身份声明 `You are "name", an AI agent in AgentTeam.` | **instructions** | 必须始终知道自己是谁 |
| 2 | `## Who you are` — persistent agent 行为定义 | **instructions** | 丢了会变成 one-shot assistant |
| 3 | Skill 文档（`skillDoc`） | **需要拆分**（见 §3.3） | 包含核心规则和参考手册两种内容 |
| 4 | `## Workspace & Memory` — MEMORY.md 规范、compaction safety | **instructions** | compaction safety 规则本身不能被 compact 掉 |
| 5 | `## Capabilities` — 通用能力描述 | **instructions** | 很短（两行），是核心行为定义 |
| 6 | `## Agent instructions` — AGENTS.md | **instructions** | 用户显式指令，不按大小降级（见 §3.4） |
| 7 | `## Initial role` — agent description | **instructions** | 通常很短，是 agent 的角色定义 |
| 8 | `## Current message` — 用户实际消息 | **dynamic input** | 每次不同 |

#### Resume 路径的内容（`buildResumePrompt()`）

| 内容 | 归类 | 理由 |
|------|------|------|
| Wake prompt | **dynamic input** | 每次恢复时不同 |
| Unread prompt | **dynamic input** | 每次恢复时不同 |
| Idle prompt | **dynamic input** | 每次恢复时不同 |
| Identity notice | **dynamic input** | 一次性通知 |
| Memory resume reminder | **dynamic input** | 恢复提示 |

### 3.3 `skillDoc` 的拆分

`skilldoc.go`（以 AgentTeam channel 为例）是 prompt 中最大的一块，包含 MCP 工具说明、messaging protocol、task board 用法等。它需要按"核心不变量 vs 参考手册"拆成两部分。

#### 进 instructions 的（核心不变量）

这些是硬规则，丢了 agent 会立刻行为错误：

```
## Communication — MCP tools ONLY（标题 + 核心约束）

CRITICAL RULES:
- Do NOT output text directly. ALL communication goes through message_send.
- Do NOT explore the filesystem looking for messaging scripts.
- Do NOT use bash/curl/sqlite to send or receive messages.
- IMPORTANT: After completing any requested work, you MUST send a reply message
  via message_send — even if it is just a brief confirmation.

## Startup sequence（核心流程）
1. ... send early acknowledgment ...
2. Read MEMORY.md ...
3. Follow instructions in MEMORY.md ...
4. If no message, stop ...
5. Process and reply ...

## Task boards — 核心规则
- CRITICAL: You MUST claim a task before starting work on it.
- IMPORTANT: When you finish a task, set status to in_review.
- IMPORTANT: After approval, move to done.

## Communication style（核心行为约束）
- Keep user informed
- Acknowledge tasks, send progress updates, summarize results

## Conversation etiquette（核心行为约束）
- Don't interrupt ongoing conversations
- Only the person doing the work should report on it
- Claim before you start

## Message Delivery — 核心规则
- AgentTeam/runtime 在安全 input boundary 自动递交新消息；agent 不轮询本地 inbox
- 收到 concrete incoming message 后先处理它，再继续旧工作

## Local workspace memory
- Read `MEMORY.md` and then the referenced `state/*`, `kb/*`, or `archive/*` files when you need durable local context.
- Do not expect any shared or semantic memory service.
```

#### 留在 turn history 的（参考手册）

这些是协议细节和示例，agent 用了几轮后已经学会，被 compaction 摘要化是可接受的：

```
## Communication — 工具列表
1. message_send — ...
2. message_history_read — ...
...
10. message_attachment_view — ...

## Messaging — 消息格式示例
[target=#general msg=a1b2c3d4 ...] @richard: hello everyone
...（header fields 解释）

## Sending messages — 语法示例
- Reply to the current delivery: message_send(target="#channel-name", content="...")
- Proactively post elsewhere: message_send(target="#other-channel", content="...")
- Reply to a DM: ...
- Reply in a thread: ...
- Reply inside an owner-scoped private-agent execution context: reuse the received parent channel/thread/task target exactly. Hidden execution context is session metadata; do not construct or expose legacy private-context target aliases or hidden execution IDs.
- Start a NEW DM: ...
- Send images: ...
- IMPORTANT: reuse exact target ...

## Threads — 详细说明
...（thread target 格式、how to start/reply、how to read history）

## Discovering people and channel scopes
...

## Channel awareness
...

## Reading history — 语法示例
...

## Task boards — 详细协议
...（status flow、tool syntax examples、splitting tasks for parallel execution）

## @Mentions — 详细说明
...

## Broadcast and task-notify protocol
...

## Formatting — No HTML
...

## Formatting — URLs in non-English text
...
```

### 3.4 AGENTS.md 的处理

AGENTS.md 是用户通过配置文件显式提供的 agent 指令。无论长短，它都代表用户的明确意图，不应因为体积大就降级为普通 user input。

**全部放入 `developerInstructions`**。

如果后续发现某些 AGENTS.md 过长导致 context window 压力，正确的做法是引导用户精简 AGENTS.md 内容，或者在 AGENTS.md 内部约定"critical directives vs reference"的分区格式，而不是在 driver 层按大小做启发式判断。

## 4. 实现设计

### 4.1 核心约束：拆分在 compiler / dispatcher 层，不在 driver 层

**不要在 driver 层临时判断"这段 prompt 算 core 还是 reference"。**

拆分边界必须在 prompt compiler / dispatcher 层明确确定。当前实现里：

- `desktop/internal/promptcompiler/compile.go` 负责把 source materialize 成 `RuntimeInputEnvelope`
- `desktop/internal/dispatcher/prompt_compiler.go` 负责为 `codex` app-server implementation 生成 `StartConfig.{BaseInstructions,DeveloperInstructions,FreshPrompt,ResumePrompt}`
- `desktop/internal/agent/driver_codex.go` 只做 thread/turn transport 透传

也就是说：
- `SkillDocSplit()`、workspace docs、agent config、notice、event context 都先进入 compiler
- driver 不拥有 prompt 分类、notice prepend、reference 选择或 fallback prompt policy

### 4.2 `StartConfig` 扩展

在 `desktop/internal/agent/driver.go` 的 `StartConfig` 中新增字段：

```go
type StartConfig struct {
    AgentID                  string
    Prompt                   string            // 现有字段，非 split runtime 继续使用
    FreshPrompt              string            // 新增：fresh session 的首次 turn input（reference + user message）
    ResumePrompt             string            // 新增：resume session 的 turn input（wake/unread/idle prompt）
    BaseInstructions         string            // 新增：平台级核心不变量（始终填充）
    DeveloperInstructions    string            // 新增：agent 级自定义指令（始终填充，可为空）
    MCP                      types.MCPConfig
    SessionID                string
    WorkDir                  string
    Binary                   string
    Model                    string
    ReasoningEffort          string
    Env                      map[string]string
    InitialDeliveryMessageID string
}
```

#### 为什么用 `FreshPrompt` + `ResumePrompt` 而非单个 `Prompt`

当前 `prepareInitialThread()`（`driver_codex.go:722-745`）的实际控制流：

```
SessionID 为空 → startThread() → 使用 FreshPrompt
SessionID 非空 → resumeThread()
  ├─ 成功 → resolveTurnRequestForDirectDelivery()
  │    ├─ 成功 → 使用 ResumePrompt
  │    └─ 失败 → startFreshThreadForInitialTurn() → 使用 FreshPrompt  ← fallback
  └─ 失败 → startFreshThreadForInitialTurn() → 使用 FreshPrompt      ← fallback
```

关键点：**resume 可能在两个地方 fallback 到 fresh thread**。如果 builder 在 dispatcher 层按 `binding.serverSessionID` 提前决定了走 resume prompt 路径，但 driver 内部 fallback 到 fresh thread，新 thread 的首次 turn 只会拿到 resume prompt（缺少参考手册）。

同样，dispatcher 层的 `validateSessionResume()`（`dispatcher.go:894-895`）也可能清空 SessionID，导致最终走 fresh 路径。

因此 dispatcher 必须**同时准备好两套 Prompt**，由 driver 根据实际 session 结果选择。

#### 为什么 instructions 用 `string` 而非 `*string` tri-state

之前的设计用 `*string` 表达三种状态（设置 / 清空 / 不传），但存在一个关键问题：**resume fallback 到 fresh thread 时，`startThread()` 和 `resumeThread()` 共享同一个 `cfg`**。如果 legacy thread 将 `*string` 设为 `nil`（不传），fallback 创建的新 thread 也不会带 instructions——新 thread 无法升级为 post-migration thread。

改用直接填充的 `string` 字段即可：

- `BaseInstructions` / `DeveloperInstructions`（`string`）：**始终填充**为当前最新的 instructions 内容。`startThread()` 无条件使用它们，确保所有新创建的 thread（包括 fallback）都带 instructions。
- legacy thread 不再走“resume 时省略 instructions”的分支；本地 runtime 验证表明这不能可靠解决 stale instruction 问题，正确做法是直接 cut over 到 fresh thread。

这样 resume fallback 路径的行为变成：

```
legacy thread / changed instructions
  → dispatcher policy: 清空 SessionID，放弃 resume
  → startFreshThreadForInitialTurn()
    → startThread(): 传 instructions ✓ (新 thread 升级为 post-migration)
```

instructions 清空语义（AGENTS.md 被删除）通过 `DeveloperInstructions = ""`（空字符串）表达。需要验证上游对空字符串的处理（见 §5.3）。

### 4.3 `skilldoc.go` 拆分

以 AgentTeam channel 的 `skilldoc.go` 为例，将 `SkillDoc()` 拆为两个函数：

```go
// SkillDocCore 返回 skillDoc 中的核心不变量——丢失后 agent 会违反行为约束的硬规则。
// 用于 thread/start 的 baseInstructions。
func (h *mcpHandler) SkillDocCore(toolName types.ToolNamer) string

// SkillDocReference 返回 skillDoc 中的参考手册——工具目录、协议示例、格式细则等。
// 用于 turn/start 的 user input（turn history reference）。
func (h *mcpHandler) SkillDocReference(toolName types.ToolNamer) string
```

`MCPHandler` 接口改为返回结构化结果，避免三份独立文案（`SkillDoc` / `SkillDocCore` / `SkillDocReference`）长期漂移：

```go
// SkillDocParts 包含 skillDoc 的结构化拆分结果。
type SkillDocParts struct {
    Core      string // 核心不变量，用于 instructions
    Reference string // 参考手册，用于 turn history
}

type MCPHandler interface {
    // ... existing methods ...
    SkillDoc(toolName types.ToolNamer) string              // 保留，供非 split runtime 使用
    SkillDocSplit(toolName types.ToolNamer) SkillDocParts   // 新增，返回结构化拆分
}
```

`SkillDoc()` 的实现可以改为基于 `SkillDocSplit()` 拼接，保证单一来源：

```go
func (h *mcpHandler) SkillDoc(toolName types.ToolNamer) string {
    parts := h.SkillDocSplit(toolName)
    return parts.Core + "\n\n" + parts.Reference
}
```

Future channel implementations with `skilldoc.go` should use the same split.

### 4.4 Prompt Compiler Mapping

当前实现不再通过 `prompt.go` 暴露四个直接字符串 builder，而是通过 prompt compiler 做等价映射：

- `CompileThreadContract(...)`
  - 输出 `RuntimeInputEnvelope{EventKind=thread_contract}`
  - `SlotBase` block materialize 到 `baseInstructions`
  - `SlotDeveloper` block materialize 到 `developerInstructions`
- `CompileTurnContext(...)`
  - 输出 `RuntimeInputEnvelope{EventKind=turn_context}`
  - `SlotReference` + `SlotTurn` block materialize 到 `codex` 的 turn input
- source of truth
  - 平台 / 事件 / notice / channel wording：`desktop/prompts/*`
  - workspace docs：`IDENTITY.md`
  - developer docs：`AGENTS.md`
  - agent description：generated `agent_config` source
  - channel reference / daemon reference：reference blocks，由 compiler 统一排序；它们进入 turn prompt，但不进入跨 daemon provider-session instruction fingerprint

对 `codex` app-server implementation 而言，语义上的四块仍然存在，但拥有者已经变成 compiler：

- `baseInstructions`
  - 平台身份、persistent behavior、channel core rules、workspace memory rules、capabilities、runtime guidance、`IDENTITY.md`
- `developerInstructions`
  - `AGENTS.md`、agent initial role / description
- `FreshPrompt`
  - 当前 lifecycle event 需要的 turn context；当这是一个 fresh-thread first turn 时，包含被允许进入 turn history 的 reference material
- `ResumePrompt`
  - 当前 lifecycle event 的 lean direct-resume input；不重新注入可 compact 的 reference blocks

### 4.5 `dispatcher.go` 调用侧：同时准备 fresh 和 resume 两套 Prompt

**关键约束：dispatcher 必须同时构建 FreshPrompt 和 ResumePrompt，由 driver 根据实际 session 结果选择。**

原因：dispatcher 层的 `binding.serverSessionID` 不是最终的 fresh/resume 决策点。以下两个环节可能推翻 dispatcher 的初始判断：

1. `validateSessionResume()` 只在当前 runtime policy 或 semantic instructions fingerprint 不匹配时清空 SessionID；daemon URL、MCP 接线和 Codex `SessionResumeSafe` 不属于 provider-session guard
2. `prepareInitialThread()` 中 runtime authority 对 `thread/resume` 或首次 direct delivery 明确返回 invalid/not-found 时，最多一次 fallback 到 `startFreshThreadForInitialTurn()`；transport-ambiguous 或 outcome unknown 必须 fail closed

如果 builder 提前只构建了 resume prompt，fallback 到 fresh thread 时新 thread 的首次 turn 就会缺少参考手册。

当前实现的等价逻辑在 `desktop/internal/dispatcher/prompt_compiler.go`：

- thread contract：始终编译一次，并渲染成 `BaseInstructions` / `DeveloperInstructions`
- `FreshPrompt`
  - 使用完整 turn-context input 编译
  - 对 `fresh_start`、`wake`、`resume`、`deferred_continuation` 都保留 compiler 选中的 reference blocks
- `ResumePrompt`
  - 仅在已有 `SessionID` 时编译
  - 通过 `withoutTurnContextReferences(...)` 去掉 `SkillDocReference`、`DaemonReference`
  - 保留 notice + dynamic wake/unread/idle/deferred text，不重复注入 compactable reference

这样可以满足两个同时存在的约束：

- direct `thread/resume` / idle `turn/start` / queued refresh 路径尽量 lean
- 一旦 `thread/resume` 或首次 direct delivery 失败并 cut over 到 fresh thread，driver 改用 `FreshPrompt` 时仍然带着 compiler-owned reference material，而不会把新 thread 启动成“只有动态消息、没有 reference”的半残状态

legacy / changed-instruction session 的 cutover 在 dispatcher policy 完成；driver 只需处理“确实要 resume 的 thread”。如果 runtime authority 明确报告 invalid/not-found，driver 最多一次 fallback 到 `startThread()`，instructions 字段始终可用——新 thread 自动成为 post-migration thread。未知 transport outcome 不得用 fresh thread 盲目重放输入。

#### 参考手册的补注入问题

参考手册只在 fresh thread 首次 turn 注入一次。以下场景需要考虑是否补注入：

- **resume 后 compaction 把参考手册压缩掉了**：这是预期行为——参考手册本来就被归类为"可压缩"。agent 可以通过学习恢复。不需要补注入。
- **legacy thread（变更上线前创建的）resume**：旧 thread 的 turn #0 中已经包含了完整 prompt（含参考手册），不需要补注入。
- **新创建 thread 的后续 resume**：参考手册在 turn history 中，instructions 通过 `thread/resume` 刷新。不需要补注入。
- **resume fallback 到 fresh thread**：driver 使用 `FreshPrompt`（含 compiler 选中的 reference blocks），不需要额外处理。

结论：**参考手册不需要在 resume 时补注入。**

### 4.6 `driver_codex.go` 修改

#### `startThread()` — 传递 instructions

```go
func (d *CodexAppServerDriver) startThread(ctx context.Context, cfg StartConfig) (string, error) {
    sandboxMode, err := codexAppServerSandboxMode(cfg)
    if err != nil {
        return "", err
    }
    approvalPolicy, err := appServerApprovalPolicy(cfg)
    if err != nil {
        return "", err
    }
    params := map[string]any{
        "cwd":            cfg.WorkDir,
        "sandbox":        sandboxMode,
        "approvalPolicy": approvalPolicy,
    }
    if strings.TrimSpace(cfg.Model) != "" {
        params["model"] = cfg.Model
    }
    // instructions 始终填充，无条件传递
    if strings.TrimSpace(cfg.BaseInstructions) != "" {
        params["baseInstructions"] = cfg.BaseInstructions
    }
    if strings.TrimSpace(cfg.DeveloperInstructions) != "" {
        params["developerInstructions"] = cfg.DeveloperInstructions
    }

    result, err := d.request(ctx, "thread/start", params)
    // ...
}
```

`startThread()` 始终传递非空 instructions。这确保了所有新创建的 thread（包括 resume fallback）都带 instructions。

Driver 只做透传，不做任何内容判断。

#### `resumeThread()` — 传递当前 instructions

```go
func (d *CodexAppServerDriver) resumeThread(ctx context.Context, cfg StartConfig, threadID string) error {
    sandboxMode, err := codexAppServerSandboxMode(cfg)
    if err != nil {
        return err
    }
    approvalPolicy, err := appServerApprovalPolicy(cfg)
    if err != nil {
        return err
    }
    params := map[string]any{
        "threadId":       threadID,
        "sandbox":        sandboxMode,
        "approvalPolicy": approvalPolicy,
    }
    if strings.TrimSpace(cfg.WorkDir) != "" {
        params["cwd"] = cfg.WorkDir
    }
    if strings.TrimSpace(cfg.Model) != "" {
        params["model"] = cfg.Model
    }
    params["baseInstructions"] = cfg.BaseInstructions
    params["developerInstructions"] = cfg.DeveloperInstructions

    result, err := d.request(ctx, "thread/resume", params)
    // ...
}
```

Instructions 清空语义（AGENTS.md 被删除）：`DeveloperInstructions = ""`。AgentTeam 在真实 resume 路径中始终传递当前 strings；但由于 upstream 目前不会可靠应用这些更新，任何 instruction 变化仍然必须由 fingerprint policy 触发 fresh-thread cutover（见 §5.3 / §6.3）。

#### `prepareInitialThread()` — 使用 FreshPrompt / ResumePrompt

```go
func (d *CodexAppServerDriver) prepareInitialThread(ctx context.Context, cfg StartConfig) (string, string, map[string]any, error) {
    threadID := strings.TrimSpace(cfg.SessionID)
    if threadID == "" {
        // 无 session → fresh thread，使用 FreshPrompt
        newThreadID, err := d.startThread(ctx, cfg)
        if err != nil {
            return "", "", nil, err
        }
        return newThreadID, "turn/start", d.startTurnParams(cfg.FreshPrompt, newThreadID), nil
    }

    if err := d.resumeThread(ctx, cfg, threadID); err != nil {
        // resume 失败 → fallback 到 fresh thread，使用 FreshPrompt
        return d.startFreshThreadForInitialTurn(ctx, cfg)
    }

    // resume 成功 → 使用 ResumePrompt
    method, params, err := d.resolveTurnRequestForDirectDelivery(ctx, cfg.ResumePrompt, threadID)
    if err == nil {
        return threadID, method, params, nil
    }

    // direct delivery 失败 → fallback 到 fresh thread，使用 FreshPrompt
    return d.startFreshThreadForInitialTurn(ctx, cfg)
}

func (d *CodexAppServerDriver) startFreshThreadForInitialTurn(ctx context.Context, cfg StartConfig) (string, string, map[string]any, error) {
    threadID, err := d.startThread(ctx, cfg)
    if err != nil {
        return "", "", nil, err
    }
    // fallback 时使用 FreshPrompt，确保新 thread 包含参考手册
    return threadID, "turn/start", d.startTurnParams(cfg.FreshPrompt, threadID), nil
}
```

关键变化：
- `prepareInitialThread()` 不再使用 `cfg.Prompt`，而是根据实际路径选择 `cfg.FreshPrompt` 或 `cfg.ResumePrompt`
- 所有 fallback 到 fresh thread 的路径都使用 `FreshPrompt`（含参考手册）
- resume 成功时使用 `ResumePrompt`（仅 wake/unread/idle prompt）

#### `EncodeStdinMessage()` — 继续编码精确正文

`EncodeStdinMessage()` 处理后续消息的精确正文投递，与 instructions 和初始 prompt 选择无关。active turn 上的 ack-aware transport 不可用或投递失败时，daemon 把原始正文保留在当前 live Process 的内存 continuation inbox 中，并在下一个安全边界自动重试；不再发送不含正文的通用 queue notification nudge。

### 4.7 Prompt 与 turn 发送行为

当前 `startTurnParams()` 在以下情况返回 `nil`（跳过 initial turn）：
- `threadID` 为空
- `text` 为空

拆分后各路径的行为：

- **FreshPrompt**：始终非空（参考手册 + 用户消息），fresh session 总是由用户消息触发 → 正常发 turn
- **ResumePrompt**：
  - 有新消息时（wake prompt）：非空 → 正常发 turn
  - 有未读摘要时（unread prompt）：非空 → 正常发 turn
  - 无新消息时（idle prompt）：**也是非空**（`buildResumePrompt()` 的 idle 分支返回 `"No new messages while you were away. Nothing to do — just stop. ..."`）→ 正常发 turn

注意：当前实现中 idle resume 会发送一个包含 idle prompt 的 turn，agent 会收到"无新消息，停止"的指令。这是现有行为，本变更不改变。`startTurnParams()` 的空字符串跳过逻辑仅在 `text` 真正为空时触发，不会被 idle prompt 触发。

## 5. 待验证事项

### 5.1 Compaction 行为

本地验证（2026-04-12，`codex-cli 0.120.0`）结果：

- `thread/start` 创建、带 `baseInstructions` 的 probe thread，在手动 `thread/compact/start` 之后仍然按原 instructions 响应后续 turn
- 这表明 start-time instructions 至少不会因为一次显式 compaction 立即失效

仍建议后续继续观察更长线程和自动 compaction 场景，但当前实现可以把 compaction 视为对 start-time instructions 安全。

验证方法：
1. 使用 `thread/start` 创建一个带 instructions 的 thread
2. 发送多个 turn 直到触发 compaction（或手动调用 `thread/compact/start`）
3. 用 `thread/read(includeTurns=true)` 检查 compaction 后 instructions 是否仍然完整

如果 compaction 不保留 instructions，核心不变量仍然比大块参考手册更值得 pin 住——因为核心规则更短且更关键。参考手册被压缩后 agent 可以通过学习恢复，但核心行为约束丢失会导致立即的行为错误。

### 5.2 `baseInstructions` vs `developerInstructions` 的语义差异

需要确认上游对这两个字段是否有不同的处理逻辑：
- 在发送给模型时的位置/优先级是否不同？
- compaction 时是否有不同的保留策略？
- 是否有 token 限制差异？

如果没有差异，也可以简化为只使用 `baseInstructions`。但建议保持分开传递，因为语义上的区分（平台框架 vs 用户定制）本身是有价值的。

### 5.3 `thread/resume` 对空字符串的处理

本地验证（2026-04-12，`codex-cli 0.120.0`）结果：

- `thread/resume` 接受 `developerInstructions: ""`，但后续 turn 仍继续遵循旧 developer instruction
- `thread/resume` 接受新的 `baseInstructions`，但 existing thread 的后续 turn 也没有切换到新的 base rule
- fresh `thread/start` + 新 instructions 可以立即生效

因此当前 upstream 语义应按“resume-time instruction refresh 不可靠”处理。AgentTeam 不能依赖 `thread/resume` 完成 instruction 清空或热更新；一旦 instructions 可能变化，必须 cut over 到 fresh thread。

## 6. 兼容性

### 6.1 非 split runtime

此变更只影响 `codex` runtime 的 app-server implementation。其他 runtime（`claude`、`opencode`）仍然使用 `StartConfig.Prompt` 包含完整 prompt 的现有路径，`BaseInstructions` 和 `DeveloperInstructions` 字段被忽略。

### 6.2 向后兼容

如果 `thread/start` 请求中不包含 `baseInstructions` / `developerInstructions` 字段（例如 `DeveloperInstructions` 为空字符串时被省略），Codex app-server 的行为应与当前（不传这些字段）一致。因此可以通过功能开关或配置逐步灰度。

### 6.3 Legacy thread 的 resume 与 migration

对于此变更上线前创建的 thread（instructions 在 turn #0 的 user input 中），resume 时注入 instructions 字段会产生两份指令来源：

- **旧来源**：turn #0 中的完整 prompt（含系统指令 + 参考手册）
- **新来源**：`thread/resume` 注入的 `baseInstructions` / `developerInstructions`

**如果 instructions 内容未变化**，两份指令基本一致，模型会看到冗余但不矛盾的信息。后续 compaction 可能会摘要化 turn #0 中的冗余部分。

**如果 instructions 内容已变化**（AGENTS.md 修改、agent description 更新、平台级指令迭代），两份指令会矛盾——旧 turn #0 包含过时的指令，新 `thread/resume` 注入的是最新指令。模型需要在两份矛盾的指令中做选择，行为不可预测。

#### Legacy thread cutover 策略

为避免矛盾指令导致不可预测行为，建议基于 instruction 内容变化检测来决定是否 cutover 到新 thread：

1. **计算 semantic instruction fingerprint**：只对 `baseInstructions + developerInstructions` 计算 hash，作为 thread 的 daemon 元数据记录；不得把 MCP URL、command、args、CWD、tool surface、daemon URL 或 provider 接线信息混入
2. **resume 时比较**：如果当前 semantic fingerprint 与 thread 记录不同，说明 instructions 已变化
3. **变化时 cutover**：放弃 resume 旧 thread，改为创建新 thread（`thread/start`），将最新 instructions 和参考手册注入新 thread

这样可以保证：
- instructions 未变化时，旧 thread 正常 resume，对话历史保留
- instructions 已变化时，干净地 cutover 到新 thread，避免矛盾指令

Fingerprint 可以持久化在 daemon 的 session 元数据中（例如 session store），不需要存储在 Codex app-server 的 thread 内。没有 fingerprint 记录的 session 视为 legacy thread。

#### 简化方案

本地运行时验证表明 `thread/resume` 不能可靠刷新 instructions，因此 legacy thread 需要明确的兼容策略：

- 变更上线后的所有新 thread 使用 instructions 字段，并记录 semantic fingerprint
- 旧 thread（session 元数据中无 fingerprint）在当前 instructions 无法证明一致时放弃 resume，改为 fresh `thread/start`
- 已记录 fingerprint 的 thread，只有 semantic fingerprint 保持不变时才允许 resume；真实 instructions 变化必须 fresh
- 旧 daemon 可能把 MCP surface 追加为 `:mcp:<hash>`，兼容比较先去掉该已知后缀，再比较 instruction hash；MCP endpoint/tool 变化本身仍允许 resume
- `SessionResumeSafe`、epoch、version 继续服务 delivery ownership/reconciliation，但不因 unsafe 值清空 provider thread；active/inflight input 仍禁止 blind replay
- 这样可以避免 legacy thread 永久保留 pre-split prompt 语义，也不把易变 daemon/provider 接线误判为 instructions 变化

## 7. 状态与后续

### P0

- 验证 `codex` app-server implementation 对 `baseInstructions` / `developerInstructions` 的 compaction 行为（§5.1）
- 验证 `baseInstructions` vs `developerInstructions` 的实际语义差异（§5.2）
- 验证 `thread/resume` 对空字符串和字段缺失的处理语义（§5.3）

### 已落地的主里程碑

- `skilldoc.go` 已收敛到 `SkillDocSplit()` / `SkillDocParts{Core, Reference}`，并继续由 compiler 消费
- `codex` prompt split 已不再依赖 `prompt.go` builder，而是走 `promptcompiler` 的 thread contract / turn context 编译
- `StartConfig` 已承载 `BaseInstructions` / `DeveloperInstructions` / `FreshPrompt` / `ResumePrompt`
- dispatcher 已按 fresh-thread fallback 与 direct resume 分别准备 reference-bearing `FreshPrompt` 和 lean `ResumePrompt`
- `driver_codex.go` 已透传 instruction 字段，并仅在 runtime authority 明确 invalid/not-found 的 resume 或首次 direct-delivery 失败时最多一次回退到 `FreshPrompt`
- legacy thread migration 已由 instruction fingerprint / cutover policy 落地

### P2

- 监控 instructions 的实际 token 占用，评估对 context window 利用率的影响
- 考虑在 AGENTS.md 内部约定分区格式（critical directives vs reference），进一步优化 `developerInstructions` 的精简度
