diff --git a/.planning/2026-08-16-goal-bubble-rendering/findings.md b/.planning/2026-08-16-goal-bubble-rendering/findings.md
index 2a7f12e..df4ed93 100644
--- a/.planning/2026-08-16-goal-bubble-rendering/findings.md
+++ b/.planning/2026-08-16-goal-bubble-rendering/findings.md
@@ -71,3 +71,40 @@
- 完成后 `Goal active + 完整目标` 被渲染为居中、接近消息区宽度的大块磨砂面板。
- 长正文包含多行约束与反引号,居中排版和纯文本渲染明显降低可读性。
- 目标视觉应为右侧普通用户气泡;状态反馈应保持短小,不重复正文。
+- 用户在真实桌面界面确认 Goal 已显示为普通用户气泡,但缺少明确的“目标模式”身份标签。
+- `/goal` 由服务端 Slash/RPC 旁路处理,不会先进入模型回合,因此不能依赖模型主动调用 `ccweb_set_title`。
+- 当前会话中直接调用 `ccweb_set_title` 成功,证明 MCP 本身可用;缺口位于 `/goal` 旁路没有标题联动,而不是标题 MCP 故障。
+- 本轮开始时 `public/app.js`、`public/style.css` 与 `server.js` 没有未提交差异;工作树中的 Codex App 重试改动属于其他任务,必须避开。
+- `.planning/.active_plan` 指向另一任务,本轮继续显式维护 `.planning/2026-08-16-goal-bubble-rendering/`,不改全局 active plan。
+- 主题架构要求视觉修复只命中用户指出的消息层级;Goal 标签应使用专用 selector,不能扩大到普通用户气泡或其他卡片。
+- CSS 接入需注意共享暗色 `:is()` 的 specificity,并在最终验收中核对真实 computed style;自动化结构断言不能替代浏览器视觉确认。
+- 修改静态 CSS/JS 后需检查 cache bust 与服务实际加载版本;本次不新增主题资产,也不改 welcome 双路径。
+- Trellis 当前没有 `.trellis/.current-task`,现有任务列表也没有 Goal 标记/标题修复任务;进入代码阶段前需建立任务资料并提供给实现与检查代理。
+- Trellis 要求 Codex 的实现与质量检查由独立子代理执行;当前运行时没有名为 `trellis-implement`/`trellis-check` 的自定义角色,因此后续使用通用 worker,并在任务提示中显式要求读取 Trellis PRD、上下文清单和相关规范。
+- `home-cc-web` 索引为 `ready`,当前 6444 个节点、14582 条边,可直接用于符号级定位。
+- 精确命中 `createMsgElement()`、`buildMsgElement()`、`persistCodexAppGoalDisplayMessage()`、`handleCodexAppGoalSlashCommand()`、`isTitleLockedByUser()` 与 `setCurrentConversationTitle()`。
+- `setCurrentConversationTitle()` 已具备标题历史、持久化、`session_renamed` 推送和 `broadcastSessionList()`,但它将来源写为 `llm`;Goal 自动派生标题需要复用相同广播契约并保留 `derived` 来源语义。
+- 手动重命名路径会设置 `titleSource: 'manual'`,已有 `isTitleLockedByUser()` 可作为禁止 Goal 覆盖用户标题的第一道保护。
+- `buildMsgElement()` 会把完整消息对象作为 `meta` 传给 `createMsgElement()`;因此在 `createMsgElement()` 读取 `meta.ccwebGoalCommand.action === 'set'`,即可让实时与历史 Goal 消息共用同一标签渲染,无需额外历史迁移。
+- `persistCodexAppGoalDisplayMessage()` 当前在消息入栈后统一 `saveSession()`;最小写入方案是在同一保存前尝试派生默认标题,并把 `titleChanged` 返回给 Slash handler 负责发送 `session_renamed` 与列表广播。
+- 普通消息自动标题只在标题严格等于 `New Chat`/`Untitled` 时生效,取用户正文前 60 字并把换行替为空格;Goal 应沿用同一默认标题判定和长度上限,同时压平连续空白,避免多行 objective 生成杂乱标题。
+- `normalizeTitleHistory()` 只接受 `source: 'llm'`;现有普通消息的 `derived` 标题也不写 title history。因此 Goal 自动标题应保持 `titleSource: 'derived'` 并不伪造 LLM 标题历史事件,只推送 `session_renamed` 元数据和会话列表。
+- Trellis 相关上下文应覆盖前端组件/质量规范、后端质量规范和跨层契约指南;本次改动同时涉及 DOM/CSS、WebSocket 事件、会话持久化与集成回归。
+- 暗金荒野主题的真实 ID 是 `wasteland`,不是 `gilded-wasteland`;Goal 标签的主题适配必须限定 `html[data-theme='wasteland']`。
+- `index.html` 的 `app.js` 使用动态 `__CC_WEB_FRONTEND_ASSET_VERSION__`,JS 无需手工版本号;`style.css` 仍使用固定 `20260805-usage-loading-inline`,新增标签样式后应推进样式 cache bust,并同步既有回归契约中的精确版本断言。
+- 现有 Goal 集成回归集中在 `scripts/regression.js` 约 7135 行,已覆盖 display-only 元数据、稳定 request ID、重复请求和刷新持久化,适合在同一场景追加标题持久化/广播断言;前端 DOM 标签需要增加静态契约测试。
+- Goal thread 的 MCP 注入链路已确认:`ensureCodexAppGoalThread()` 调用 `codexAppThreadParams()`,后者通过 `codexAppThreadConfig()` 和 `listRuntimeMcpServerConfigs()` 写入 `mcp_servers.ccweb`;内置配置携带当前 `sourceSessionId`。
+- 用户怀疑“Goal 未注册 ccweb MCP”的根因不是注册缺失:`thread/goal/set` 是 app-server RPC,不是模型 turn/MCP 调用。工具可用性不会强制模型调用 `ccweb_set_title`,所以可靠标题仍应由服务端旁路直接派生。
+- 第一轮独立计划审查发现:仅广播 `session_list` 只会刷新侧栏,当前聊天顶部标题依赖 `session_renamed`。计划已补成“先向 viewers 发送 `session_renamed`,再广播列表”,回归也必须分别断言。
+- 第二轮独立计划复审已通过,无实施阻断项;建议将默认标题判定收敛为 helper,并把自动标题严格限定在 `/goal set` 专用路径。
+- 现有 Goal 集成场景复用了已经发送过多条普通消息的 `codexAppSession`,其标题早已不是默认值;自动标题正向回归需要另建一个尚为 `New Chat` 的 Codex App 会话,不能在原场景上做错误断言。
+- 当前 `wasteland` 用户气泡最大宽度为 `min(42%, 480px)`;标签必须是内容流内的紧凑元素,正文继续依赖现有 `word-break`/`pre-wrap`,不能增加固定宽度或绝对定位造成窄屏溢出。
+- 当前目标文件中只有 `scripts/regression.js` 带并发 Codex App 重试差异;实现必须只在 Goal/缓存断言附近追加,保留约 2446 和 main target 注册处的现有重试代码。
+- 本机可用 `/usr/bin/firefox`,虽未发现 Playwright/Puppeteer;可在服务重启条件满足后用 Firefox headless 做基础桌面/窄屏截图验收。
+- 当前 `127.0.0.1:8002` 返回 200 且 `Cache-Control: no-store`;在线 HTML 仍引用 `style.css?v=20260805-usage-loading-inline` 与动态 app.js 版本,CSS cache bust 更新后需重新检查实际响应。
+- 已完整读取主题验收清单;本次不新增主题/素材,因此资产、主题注册和 welcome 双路径条目不适用,但仍需执行消息 DOM 隔离、`wasteland` computed style、桌面/窄屏、缓存 URL、JS 回归与真实浏览器记录。
+- `sendSessionEventToViewers()` 原样转发 payload,不自动补 `sessionId`;Goal 自动标题事件必须显式携带 `type`、`sessionId`、`title` 和 `publicTitleMetadata()`。
+- `normalizeSession()` 已接受 `titleSource: 'derived'`;`VALID_TITLE_SOURCES` 包含 derived,因此 Goal 旁路可安全复用该来源而不修改清洗逻辑。
+- `truncateTextValue()` 超长时会追加持久化截断标记,不适合标题;Goal 标题应使用独立的 60 个 Unicode code point 截取,避免标题中出现换行标记。
+- 现有 mock 在 `thread/goal/set` 后会发 Goal 后台 turn;包含 `dynamic` 的普通 turn 会执行 `completeMcpToolTurn()`,其结果带 `hasCcwebMcpConfig`,可作为 Goal thread 注册 ccweb MCP 的集成证据。
+- 现有 Codex App 回归会话在发送多条普通消息后才进入旧 Goal 场景;新增默认标题测试应在同一服务器内创建独立的 `codexapp-goal-title` 会话,避免旧会话标题已被普通消息派生。
diff --git a/.planning/2026-08-16-goal-bubble-rendering/progress.md b/.planning/2026-08-16-goal-bubble-rendering/progress.md
index c2bbbac..6ccbeaa 100644
--- a/.planning/2026-08-16-goal-bubble-rendering/progress.md
+++ b/.planning/2026-08-16-goal-bubble-rendering/progress.md
@@ -101,6 +101,48 @@
|-----------|-------|---------|------------|
| 2026-08-16 | 仓库内 `todo-list-csv` 技能短路径不存在 | 1 | 使用技能清单中的实际安装路径 |
+## Session: 2026-08-18
+
+### 阶段 7:执行真实浏览器桌面与窄屏验收
+
+- **Status:** complete(剩余验收已转移至阶段 14)
+- Actions taken:
+ - 用户在真实桌面界面确认 Goal 正文已经显示为普通用户气泡。
+ - 用户指出气泡缺少“目标模式”标记,并确认 Goal 会话标题没有自动更新。
+ - 将新增缺陷纳入同一任务,最终窄屏和主题验收统一放到阶段 14。
+
+### 阶段 9:定位 Goal 标记与自动标题链路
+
+- **Status:** complete
+- Actions taken:
+ - 执行 session catchup 并恢复上一轮 Goal 实施与验证记录。
+ - 核对当前工作树,识别并隔离另一项 Codex App 重试修复的未提交改动。
+ - 直接调用当前会话的 `ccweb_set_title` 成功,确认标题 MCP 可用;`/goal` 缺少的是服务端旁路联动。
+ - 完整读取主题架构与经验教训,确认只为 Goal 用户消息增加专用标签样式,不扩散到普通气泡。
+ - 完整读取 Trellis 工作流并确认当前没有活跃 Trellis task;将在实现前补齐任务资料与独立实现/检查环节。
+ - 使用 codebase-memory 确认索引就绪,并定位 Goal 渲染、Slash 处理、标题锁定和标题广播函数。
+ - 读取精确函数源码,确认 Goal 元数据可在统一消息渲染入口生成标签,标题可在 display-only 消息保存周期内派生并实时广播。
+ - 交叉核对 CSS、缓存版本和现有 Goal 回归位置,确认主题 ID 为 `wasteland`,样式版本需随 CSS 修改推进。
+ - 确认 Goal thread 已注入 `mcp_servers.ccweb`;标题未变化源于 `thread/goal/set` 不会自动触发工具,而非 MCP 注册缺失。
+
+### 阶段 10:审查增量实施计划并修正缺口
+
+- **Status:** complete
+- Actions taken:
+ - 第一轮独立审查指出计划缺少 `session_renamed` 实时事件要求,仅广播列表会让顶部标题保持陈旧。
+ - 已补充 `session_renamed` + `session_list` 双事件及其回归断言;第二轮完整复审通过。
+
+### 阶段 11:实现目标模式气泡标记
+
+- **Status:** in_progress
+- Actions taken:
+ - Trellis 任务 `08-18-goal-mode-title` 已建立、启动并通过上下文校验。
+ - 实施前重新核对目标文件差异,只有 `scripts/regression.js` 含另一任务改动,已记录不可覆盖区块。
+ - 检查本地运行服务与浏览器能力:服务在线且 no-store,本机有 Firefox 可做后续 headless 验收。
+ - 完整读取 `create-cc-web-theme` 验收清单,已将与本次消息层级相关的浏览器、缓存、响应式和隔离项纳入收尾门禁。
+ - 进一步核对标题事件与清洗实现:事件 payload 需显式带 sessionId;derived 来源已被会话规范接受;标题截取不能复用带截断标记的持久化 helper。
+ - 确认 mock 可验证 Goal thread 的 ccweb MCP 配置,并规划在现有 Codex App 回归中使用独立默认标题会话。
+
## 5-Question Reboot Check
| Question | Answer |
diff --git a/.planning/2026-08-16-goal-bubble-rendering/task_plan.md b/.planning/2026-08-16-goal-bubble-rendering/task_plan.md
index 0eb2ad5..15cd60b 100644
--- a/.planning/2026-08-16-goal-bubble-rendering/task_plan.md
+++ b/.planning/2026-08-16-goal-bubble-rendering/task_plan.md
@@ -6,7 +6,7 @@
## Current Phase
-阶段 7:执行真实浏览器桌面与窄屏验收
+阶段 11:实现目标模式气泡标记
## Phases
@@ -57,10 +57,10 @@
### 阶段 7:执行真实浏览器桌面与窄屏验收
-- [ ] 在桌面视口验证长 Goal 为用户气泡
-- [ ] 在窄屏验证换行、溢出和系统提示尺寸
-- [ ] 确认暗金荒野主题未再生成大块 Goal 系统面板
-- **Status:** in_progress
+- [x] 用户在桌面真实界面确认长 Goal 已回到普通用户气泡
+- [x] 基于真实反馈记录新缺陷:缺少“目标模式”标记、标题未变化
+- [x] 将窄屏、主题和最终视觉验收转移至阶段 14
+- **Status:** complete
### 阶段 8:审查差异并清理临时任务记录
@@ -68,11 +68,50 @@
- [ ] 完成计划记录并删除本任务 TODO CSV
- **Status:** pending
+### 阶段 9:定位 Goal 标记与自动标题链路
+
+- [x] 使用代码索引确认 Goal 消息渲染、标题派生、保存和广播入口
+- [x] 用源码与当前差异交叉验证修改边界,避开其他任务改动
+- [x] 确认 Goal thread 已注入 `mcp_servers.ccweb`,但 `thread/goal/set` 不会自动触发工具调用
+- **Status:** complete
+
+### 阶段 10:审查增量实施计划并修正缺口
+
+- [x] 由独立计划审查员检查标记、自动标题与回归范围
+- [x] 修正会导致实现偏差或阻塞的计划问题并完成复审
+- **Status:** complete
+
+### 阶段 11:实现目标模式气泡标记
+
+- [ ] 复用 `ccwebGoalCommand` 元数据给 Goal 用户消息增加专用类名和标签
+- [ ] 增加基础样式及暗金荒野主题适配,不改变普通用户气泡
+- **Status:** in_progress
+
+### 阶段 12:实现 Goal 新会话自动标题
+
+- [ ] 仅在会话仍为默认标题时从 Goal objective 派生简短标题
+- [ ] 持久化派生标题并向当前 viewers 发送 `session_renamed`,再广播会话列表,不覆盖手动标题
+- **Status:** pending
+
+### 阶段 13:补充标记与标题回归测试
+
+- [ ] 覆盖 Goal 标签 DOM、普通消息隔离、默认标题派生和手动标题保护
+- [ ] 同时覆盖标题磁盘持久化、`session_renamed` 与 `session_list` 实时更新契约
+- **Status:** pending
+
+### 阶段 14:运行语法、全量回归与视觉验收
+
+- [ ] 执行语法检查、全量回归和差异格式检查
+- [ ] 检查桌面、窄屏及暗金荒野主题下的 Goal 标记视觉
+- [ ] 按验收清单记录真实浏览器能力与剩余限制
+- **Status:** pending
+
## Key Questions
1. 设置型 `/goal` 气泡如何持久化而不进入普通模型对话链路?
2. 如何只改变 Goal 命令,不改变其他 Slash 命令的既有行为?
3. 长 Goal 在桌面与窄屏下是否仍保持普通用户气泡布局?
+4. `/goal` 绕过模型时,如何可靠更新默认标题且不覆盖用户手动命名?
## Decisions Made
@@ -84,6 +123,10 @@
| Goal 气泡使用独立展示消息路径 | 写入 `session.messages` 并发出 `session_message`,但不调用普通消息运行时,兼顾刷新恢复与防止重复模型执行 |
| `clientMessageId/requestId` 是命令与消息去重键 | 传输重放时可识别同一次设置,避免重复气泡与重复 Goal RPC |
| 校验前失败恢复草稿;RPC 已启动后失败保留气泡且不恢复草稿 | 保持即时提交记录,同时避免用户重试产生两条相同气泡 |
+| Goal 标签复用 `ccwebGoalCommand` 元数据 | 历史消息与实时消息共用同一渲染入口,无需迁移持久化数据 |
+| `/goal` 自动标题由服务端旁路直接派生 | Goal RPC 不经过模型,不能依赖模型主动调用 `ccweb_set_title` MCP |
+| 只更新默认标题且保留 `titleSource: derived` | 避免覆盖手动标题,并允许后续模型/MCP继续优化标题 |
+| Goal 标题变化同时发送 `session_renamed` 和 `session_list` | 前者更新当前聊天顶部标题,后者更新侧栏;缺一都会产生局部陈旧 UI |
## Errors Encountered
diff --git a/.planning/repackage-and-push/progress.md b/.planning/repackage-and-push/progress.md
index f0e7b66..f0b979a 100644
--- a/.planning/repackage-and-push/progress.md
+++ b/.planning/repackage-and-push/progress.md
@@ -14,3 +14,15 @@
- 已创建主提交 `2efb9d2`(`feat: improve conversation timeline and agent plans`);阶段 5 完成,准备推送 `main`。
- 同步进度后 amend 得到最终主提交 `33c9783`,已成功推送到 `origin/main`(`4d97446..33c9783`)。
- 六个阶段全部完成,准备删除本次临时 TODO CSV,并用收尾提交同步规划状态。
+
+## 2026-08-18
+
+- 用户要求再次执行 CentOS 7 baseline 单文件打包,并提交、推送当前工作树全部修改。
+- 按用户要求停止额外视觉/代码审计,仅执行发布技能规定的构建与轻量验证。
+- 当前分支为 `main`,远端为 `origin`;工作树中的已跟踪与未跟踪修改均在本次提交范围内。
+- PATH 中没有 Bun;已找到 `/tmp/ccweb-bun.vyBprA/node_modules/@oven/bun-linux-x64-baseline/bin/bun`,进入重新打包阶段。
+- 已用 baseline Bun 1.3.14 完成 `npm run build:single-exe`,重新生成发布目录与 tar.gz,进入发布包验证阶段。
+- 六项 JavaScript 语法检查通过;发布二进制 MCP `initialize` 返回有效 JSON-RPC;tar 清单可读。
+- 新发布包 SHA-256:`f14c108687b84699d6c4f2301363a36235702db51227da719492a4cdd156d48b`。
+- 已确认本次提交范围覆盖当前工作区全部已跟踪与未跟踪修改,进入统一暂存与提交阶段。
+- 已创建包含全部修改的主提交,进入 `origin/main` 推送阶段。
diff --git a/.planning/repackage-and-push/task_plan.md b/.planning/repackage-and-push/task_plan.md
index 114e588..ad80d32 100644
--- a/.planning/repackage-and-push/task_plan.md
+++ b/.planning/repackage-and-push/task_plan.md
@@ -13,7 +13,7 @@
| 3. 执行发布包与语法验证 | complete | 技能规定的语法、MCP smoke、tar 清单检查通过 |
| 4. 暂存当前工作区全部修改 | complete | `git add -A` 后无未暂存或未跟踪修改 |
| 5. 创建包含全部修改的提交 | complete | 当前全部修改进入一个新提交 |
-| 6. 推送当前分支并确认远端 | complete | 推送成功,当前分支与上游一致 |
+| 6. 推送当前分支并确认远端 | in_progress | 推送成功,当前分支与上游一致 |
## 约束
@@ -28,3 +28,9 @@
| 错误 | 尝试 | 处理 |
|---|---|---|
| `bun` 不在 PATH | 1 | 按发布技能查找本机已有 baseline Bun |
+
+## 当前执行
+
+- 日期:2026-08-18
+- 当前阶段:6. 推送当前分支并确认远端
+- 用户要求:再次打包;提交并推送当前全部修改;不做额外审计。
diff --git a/.trellis/spec/backend/error-handling.md b/.trellis/spec/backend/error-handling.md
index bcd5533..847c98b 100644
--- a/.trellis/spec/backend/error-handling.md
+++ b/.trellis/spec/backend/error-handling.md
@@ -32,7 +32,12 @@ Questions to answer:
-(To be filled by the team)
+### Codex App 过程错误与终态错误
+
+- app-server 的 JSON-RPC 方法名为 `error` 时,不能仅凭方法名判定当前 turn 已结束;必须结合消息语义和后续终态事件处理。
+- 纯 `Reconnecting... N/M` 表示 provider 内部重连进度:应向客户端展示,但不得写入最终错误、清理 active turn 或启动新的 cc-web 业务重试。
+- `503 Service Unavailable`、`No available providers`、`service_unavailable_error` 等终态错误必须保留完整错误信息,再交给服务端重试分类。
+- 自动续接必须携带并校验原 `expectedThreadId`;恢复到不同 thread 时应停止,不能用新 thread 掩盖上下文丢失。
---
@@ -48,4 +53,5 @@ Questions to answer:
-(To be filled by the team)
+- 把每个 app-server `error` 通知都返回为 `done: true`,会在 provider 自身仍重连时提前关闭 turn,并与 cc-web 重试状态机形成交错重试。
+- 使用非锚定正则识别 `Reconnecting... N/M`,可能误吞同时包含最终 503 的组合错误;过程通知识别应匹配整条消息。
diff --git a/.trellis/tasks/08-18-codexapp-retry-resume/check.jsonl b/.trellis/tasks/08-18-codexapp-retry-resume/check.jsonl
new file mode 100644
index 0000000..563d0a0
--- /dev/null
+++ b/.trellis/tasks/08-18-codexapp-retry-resume/check.jsonl
@@ -0,0 +1,5 @@
+{"file":".trellis/spec/backend/error-handling.md","reason":"核验错误分类、重试上限与最终失败行为"}
+{"file":".trellis/spec/backend/quality-guidelines.md","reason":"核验后端测试与代码质量"}
+{"file":".trellis/spec/frontend/state-management.md","reason":"核验重连状态、计数和任务终态一致性"}
+{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"核验前端测试与代码质量"}
+{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"核验跨层事件契约和恢复后的数据流"}
diff --git a/.trellis/tasks/08-18-codexapp-retry-resume/implement.jsonl b/.trellis/tasks/08-18-codexapp-retry-resume/implement.jsonl
new file mode 100644
index 0000000..e2b2795
--- /dev/null
+++ b/.trellis/tasks/08-18-codexapp-retry-resume/implement.jsonl
@@ -0,0 +1,5 @@
+{"file":".trellis/spec/backend/error-handling.md","reason":"约束临时服务错误、终态错误和重试边界的处理方式"}
+{"file":".trellis/spec/backend/quality-guidelines.md","reason":"约束后端实现与回归测试质量"}
+{"file":".trellis/spec/frontend/state-management.md","reason":"约束重连计数和运行终态的前端状态管理"}
+{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"约束前端实现与测试质量"}
+{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"重试链路跨越 app-server、SSE/WebSocket 与前端状态机,需要校验边界契约"}
diff --git a/.trellis/tasks/08-18-codexapp-retry-resume/prd.md b/.trellis/tasks/08-18-codexapp-retry-resume/prd.md
new file mode 100644
index 0000000..a37899f
--- /dev/null
+++ b/.trellis/tasks/08-18-codexapp-retry-resume/prd.md
@@ -0,0 +1,27 @@
+# 修复 Codex App 重试中断
+
+## 问题
+
+Codex App 遇到临时 503 或网络错误时,界面虽然连续显示自动重试与“从中断处继续”,但当前任务仍被终止。第五次附近还会先展示终端 503,再把重连计数重置为 `1/5`,造成状态与真实执行不一致。
+
+## 需求
+
+1. 可恢复错误进入统一的续跑状态,不提前关闭当前任务。
+2. 同一恢复周期的尝试次数单调递增,不因终端事件或传输重连错误重置。
+3. 每次自动重试必须真正续接原线程/任务,而不只是显示通知。
+4. 恢复成功后清理重试状态并继续流式输出。
+5. 达到重试上限后才进入明确失败终态,并保留可理解的错误原因。
+
+## 验收标准
+
+- 自动化测试复现“连续临时失败后仍续跑”的场景,并在修复前失败、修复后通过。
+- 自动化测试验证计数不会在同一恢复周期中从 `5/5` 错误回到 `1/5`。
+- 自动化测试验证成功恢复与耗尽重试两种终态。
+- 现有 Codex App 相关回归测试和静态检查通过。
+
+## 非目标
+
+- 不修改上游 provider 的可用性或调度策略。
+- 不把不可恢复的协议、鉴权或用户输入错误纳入无限重试。
+- 不重做现有视觉主题。
+
diff --git a/.trellis/tasks/08-18-codexapp-retry-resume/task.json b/.trellis/tasks/08-18-codexapp-retry-resume/task.json
new file mode 100644
index 0000000..0c0427f
--- /dev/null
+++ b/.trellis/tasks/08-18-codexapp-retry-resume/task.json
@@ -0,0 +1,31 @@
+{
+ "id": "codexapp-retry-resume",
+ "name": "codexapp-retry-resume",
+ "title": "修复 Codex App 重试中断",
+ "description": "",
+ "status": "completed",
+ "dev_type": null,
+ "scope": null,
+ "package": null,
+ "priority": "P2",
+ "creator": "shiyue",
+ "assignee": "shiyue",
+ "createdAt": "2026-08-18",
+ "completedAt": "2026-08-18",
+ "branch": null,
+ "base_branch": "main",
+ "worktree_path": null,
+ "commit": null,
+ "pr_url": null,
+ "subtasks": [],
+ "children": [],
+ "parent": null,
+ "relatedFiles": [
+ "lib/codex-app-runtime.js",
+ "scripts/mock-codex-app-server.js",
+ "scripts/regression.js",
+ ".trellis/spec/backend/error-handling.md"
+ ],
+ "notes": "区分 Codex App 重连过程通知与终端错误;定向及完整回归通过。",
+ "meta": {}
+}
diff --git a/.trellis/tasks/08-18-goal-mode-title/check.jsonl b/.trellis/tasks/08-18-goal-mode-title/check.jsonl
new file mode 100644
index 0000000..2db10c7
--- /dev/null
+++ b/.trellis/tasks/08-18-goal-mode-title/check.jsonl
@@ -0,0 +1,7 @@
+{"file":".trellis/spec/frontend/index.md","reason":"核验前端检查清单和组件边界"}
+{"file":".trellis/spec/frontend/component-guidelines.md","reason":"核验标签 DOM、可访问性和普通消息隔离"}
+{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"核验缓存版本、前端回归和视觉限制说明"}
+{"file":".trellis/spec/backend/index.md","reason":"核验后端会话处理检查清单"}
+{"file":".trellis/spec/backend/quality-guidelines.md","reason":"核验标题持久化、广播和回归覆盖"}
+{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"核验 DOM、WebSocket、持久化和会话列表的一致契约"}
+{"file":".trellis/tasks/08-18-goal-mode-title/research/goal-message-title-chain.md","reason":"核验实现是否符合已确认链路与并行工作边界"}
diff --git a/.trellis/tasks/08-18-goal-mode-title/implement.jsonl b/.trellis/tasks/08-18-goal-mode-title/implement.jsonl
new file mode 100644
index 0000000..4605711
--- /dev/null
+++ b/.trellis/tasks/08-18-goal-mode-title/implement.jsonl
@@ -0,0 +1,7 @@
+{"file":".trellis/spec/frontend/index.md","reason":"加载前端开发检查清单与组件规范入口"}
+{"file":".trellis/spec/frontend/component-guidelines.md","reason":"约束 Goal 标签 DOM、可访问性与样式隔离"}
+{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"约束前端静态契约、回归和缓存版本质量"}
+{"file":".trellis/spec/backend/index.md","reason":"加载后端开发检查清单与会话处理规范入口"}
+{"file":".trellis/spec/backend/quality-guidelines.md","reason":"约束会话持久化、标题保护与回归质量"}
+{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"Goal 改动跨越 DOM、WebSocket 事件、会话持久化和列表广播"}
+{"file":".trellis/tasks/08-18-goal-mode-title/research/goal-message-title-chain.md","reason":"提供已验证的 Goal 消息、标题、主题和并行修改边界"}
diff --git a/.trellis/tasks/08-18-goal-mode-title/prd.md b/.trellis/tasks/08-18-goal-mode-title/prd.md
new file mode 100644
index 0000000..22c01c7
--- /dev/null
+++ b/.trellis/tasks/08-18-goal-mode-title/prd.md
@@ -0,0 +1,32 @@
+# 修复 Goal 模式标记与自动标题
+
+## 问题
+
+设置型 `/goal <目标>` 已经以普通用户气泡持久化和展示,但气泡与普通消息完全一致,用户无法识别这是 Goal。与此同时,`/goal` 由服务端 Slash/RPC 旁路处理,不会先创建模型回合,因此模型没有机会主动调用 `ccweb_set_title`,默认标题会一直保留为 `New Chat` 或 `Untitled`。
+
+## 需求
+
+1. 仅对 `role: user` 且 `ccwebGoalCommand.action === 'set'` 的消息增加 Goal 专用 class,并在正文上方显示固定标签“目标模式”。
+2. 实时 `session_message` 与刷新后的历史消息必须走同一渲染入口;普通用户消息、控制型 `/goal` 命令和其他 display-only 消息不得出现该标签。
+3. 标签保持紧凑、可读,不把用户气泡重新变成大系统面板;提供基础样式和 `wasteland` 暗金荒野主题适配,并保证长正文和窄屏不溢出。
+4. 设置型 `/goal` 持久化展示消息时,仅在当前标题仍为 `New Chat`/`Untitled` 且 `titleSource` 不是 `manual` 时,从 objective 派生标题。
+5. 派生标题压平连续空白、截取最多 60 个字符,写入 `session.title` 与 `titleSource: 'derived'`;不得伪造只接受 `source: 'llm'` 的标题历史事件。
+6. 标题改变后立即保存,并向查看该会话的客户端发送 `session_renamed`,再广播最新 `session_list`;手动标题和任何非默认标题必须保持不变。
+7. 保持 Goal RPC、稳定请求 ID 去重、display-only 持久化、失败草稿语义和控制型 `/goal` 行为不变。
+8. 修改 `style.css` 后推进样式 cache-busting 版本,并同步相关回归断言。
+
+## 验收标准
+
+- 回归证明 Goal 消息生成专用 class 与“目标模式”标签,普通用户消息不会误命中。
+- 回归证明默认标题会从多行 Goal objective 派生、持久化为 `derived`,并实时发出 `session_renamed`/`session_list`。
+- 回归证明手动标题和已有非默认标题不会被 Goal 覆盖。
+- 重复同一 Goal 命令仍只产生一条展示消息、一次 Goal RPC,且不会重复改标题。
+- `node --check`、相关前端契约测试、`timeout 60s npm run regression` 与 `git diff --check` 通过。
+- 在可用的真实浏览器中检查桌面、窄屏和 `wasteland` 主题;若本机仍无浏览器能力,必须如实记录限制,不得以自动化替代视觉结论。
+
+## 非目标
+
+- 不让 `/goal` 额外启动普通模型 turn,也不为了改标题调用内部 HTTP/MCP 回路。
+- 不覆盖用户手动标题,不把 Goal 自动标题记作 LLM 标题历史。
+- 不重做普通用户气泡、系统消息、其他主题或 welcome 页面。
+- 不修改当前工作树中 Codex App 重试任务的文件和区块。
diff --git a/.trellis/tasks/08-18-goal-mode-title/research/goal-message-title-chain.md b/.trellis/tasks/08-18-goal-mode-title/research/goal-message-title-chain.md
new file mode 100644
index 0000000..f073dc5
--- /dev/null
+++ b/.trellis/tasks/08-18-goal-mode-title/research/goal-message-title-chain.md
@@ -0,0 +1,22 @@
+# Goal 消息与标题链路
+
+## 已确认入口
+
+- `public/app.js`:`buildMsgElement()` 把完整消息对象作为 `meta` 传给 `createMsgElement()`,适合直接读取 `ccwebGoalCommand`,同时覆盖实时和历史消息。
+- `server.js`:`persistCodexAppGoalDisplayMessage()` 创建 `role: user`、`ccwebDisplayOnly: true`、带 `ccwebGoalCommand.action: 'set'` 的持久消息,并统一保存会话。
+- `server.js`:`handleCodexAppGoalSlashCommand()` 在普通消息运行时之前处理 `/goal`,因此不会触发模型主动调用 `ccweb_set_title`。
+- `server.js`:`ensureCodexAppGoalThread()` 复用 `codexAppThreadParams()`;`codexAppThreadConfig()` 会从 runtime MCP 配置写入 `mcp_servers.ccweb`。因此底层 Goal thread 已注册 ccweb MCP,缺口是 `thread/goal/set` 本身不是模型工具调用。
+- `server.js`:普通消息只在标题为 `New Chat`/`Untitled` 时取正文前 60 字并设置 `titleSource: 'derived'`。
+- `server.js`:`setCurrentConversationTitle()` 展示了 `session_renamed`、`publicTitleMetadata()` 和 `broadcastSessionList()` 的实时通知契约;该 MCP 路径使用 `titleSource: 'llm'`,不能直接复用其来源语义。
+- `server.js`:`handleRenameSession()` 将用户标题写为 `titleSource: 'manual'`;`isTitleLockedByUser()` 可阻止自动覆盖。
+- `server.js`:`normalizeTitleHistory()` 只保留 `source: 'llm'`,普通 `derived` 标题不进入标题历史。
+
+## 视觉与缓存边界
+
+- 暗金荒野主题 ID 为 `wasteland`。
+- Goal 标签只应命中专用 Goal user message selector,不能扩大到普通 `.msg.user`。
+- `app.js` 使用动态 `__CC_WEB_FRONTEND_ASSET_VERSION__`;`style.css` 使用固定 query,CSS 变化后需要推进版本并同步回归断言。
+
+## 并行工作边界
+
+当前工作树另有 Codex App 重试修复,涉及 `lib/codex-app-runtime.js`、`scripts/mock-codex-app-server.js`、`scripts/regression.js` 及 Trellis 规范文件。实现可在 `scripts/regression.js` 的 Goal 场景追加断言,但必须保留并发改动,不得重排或还原整文件。
diff --git a/.trellis/tasks/08-18-goal-mode-title/task.json b/.trellis/tasks/08-18-goal-mode-title/task.json
new file mode 100644
index 0000000..1f6a0f4
--- /dev/null
+++ b/.trellis/tasks/08-18-goal-mode-title/task.json
@@ -0,0 +1,32 @@
+{
+ "id": "goal-mode-title",
+ "name": "goal-mode-title",
+ "title": "修复 Goal 模式标记与自动标题",
+ "description": "",
+ "status": "in_progress",
+ "dev_type": null,
+ "scope": null,
+ "package": null,
+ "priority": "P2",
+ "creator": "shiyue",
+ "assignee": "shiyue",
+ "createdAt": "2026-08-18",
+ "completedAt": null,
+ "branch": null,
+ "base_branch": "main",
+ "worktree_path": null,
+ "commit": null,
+ "pr_url": null,
+ "subtasks": [],
+ "children": [],
+ "parent": null,
+ "relatedFiles": [
+ "public/app.js",
+ "public/style.css",
+ "public/index.html",
+ "server.js",
+ "scripts/regression.js"
+ ],
+ "notes": "Goal 用户气泡增加模式标记;默认标题由服务端旁路派生,手动标题受保护。",
+ "meta": {}
+}
diff --git a/.trellis/workspace/shiyue/index.md b/.trellis/workspace/shiyue/index.md
index 0d06f07..fa5413b 100644
--- a/.trellis/workspace/shiyue/index.md
+++ b/.trellis/workspace/shiyue/index.md
@@ -8,8 +8,8 @@
- **Active File**: `journal-1.md`
-- **Total Sessions**: 0
-- **Last Active**: -
+- **Total Sessions**: 1
+- **Last Active**: 2026-08-18
---
@@ -19,7 +19,7 @@
| File | Lines | Status |
|------|-------|--------|
-| `journal-1.md` | ~0 | Active |
+| `journal-1.md` | ~37 | Active |
---
@@ -29,6 +29,7 @@
| # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------|
+| 1 | 2026-08-18 | 修复 Codex App 重试中断 | - | `main` |
---
@@ -37,4 +38,4 @@
- Sessions are appended to journal files
- New journal file created when current exceeds 2000 lines
-- Use `add_session.py` to record sessions
+- Use `add_session.py` to record sessions
\ No newline at end of file
diff --git a/.trellis/workspace/shiyue/journal-1.md b/.trellis/workspace/shiyue/journal-1.md
index f73ca2f..dd30c63 100644
--- a/.trellis/workspace/shiyue/journal-1.md
+++ b/.trellis/workspace/shiyue/journal-1.md
@@ -4,3 +4,34 @@
> Started: 2026-04-24
---
+
+
+## Session 1: 修复 Codex App 重试中断
+
+**Date**: 2026-08-18
+**Task**: 修复 Codex App 重试中断
+**Branch**: `main`
+
+### Summary
+
+区分 Reconnecting N/M 过程通知与终端 503,保留原 thread 续接重试;定向和完整回归通过。
+
+### Main Changes
+
+(Add details)
+
+### Git Commits
+
+(No commits - planning session)
+
+### Testing
+
+- [OK] (Add test results)
+
+### Status
+
+[OK] **Completed**
+
+### Next Steps
+
+- None - task complete
diff --git a/Goal 气泡显示 TO DO list.csv b/Goal 气泡显示 TO DO list.csv
index 7c5954a..9869b6f 100644
--- a/Goal 气泡显示 TO DO list.csv
+++ b/Goal 气泡显示 TO DO list.csv
@@ -5,5 +5,11 @@ id,item,status,done_at,notes
4,将 Goal 同步和成功反馈改为短暂精简提示,DONE,2026-08-16T21:36:08+08:00,同步与成功反馈 transient 且成功文案不回显目标
5,补充 Goal 气泡及消息协议回归测试,DONE,2026-08-16T21:36:08+08:00,覆盖消息协议、元数据、去重与背景 turn 负向断言
6,运行语法、专项和全量回归,DONE,2026-08-16T21:36:08+08:00,node --check 与 npm run regression 均通过
-7,执行真实浏览器桌面与窄屏验收,IN_PROGRESS,,
+7,执行真实浏览器桌面与窄屏验收,DONE,2026-08-18T18:25:00+08:00,用户已确认普通气泡生效;新增缺陷与剩余视觉验收转移至步骤 9-14
8,审查差异并清理临时任务记录,TODO,,
+9,定位 Goal 标记与自动标题链路,DONE,2026-08-18T18:42:00+08:00,已确认渲染/标题/MCP 注入链路及并行修改边界
+10,审查增量实施计划并修正缺口,DONE,2026-08-18T18:49:00+08:00,补齐 session_renamed 后第二轮独立复审通过
+11,实现目标模式气泡标记,IN_PROGRESS,,
+12,实现 Goal 新会话自动标题,TODO,,
+13,补充标记与标题回归测试,TODO,,
+14,运行语法、全量回归与视觉验收,TODO,,
diff --git a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz
index de77473..2635ee5 100644
Binary files a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz and b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz differ
diff --git a/lib/codex-app-runtime.js b/lib/codex-app-runtime.js
index a784863..eb616c2 100644
--- a/lib/codex-app-runtime.js
+++ b/lib/codex-app-runtime.js
@@ -429,6 +429,10 @@ function createCodexAppRuntime(deps = {}) {
return safeStringifyPreview(value, 2000, { maxDepth: 3, maxArray: 10, maxKeys: 20 });
}
+ function isCodexAppReconnectNotice(value) {
+ return /^reconnecting(?:\.\.\.)?\s*\d+\s*\/\s*\d+\s*[.!]?$/i.test(codexAppErrorMessage(value).trim());
+ }
+
function sendRuntime(entry, sessionId, payload) {
wsSend(entry.ws, { ...payload, sessionId });
}
@@ -1020,10 +1024,12 @@ function createCodexAppRuntime(deps = {}) {
? codexAppErrorMessage(params)
: (params.message || params.title || '');
if (message) {
- if (method === 'error') entry.lastError = message;
+ const reconnectNotice = method === 'error' && isCodexAppReconnectNotice(params);
+ if (method === 'error' && !reconnectNotice) entry.lastError = message;
if (method === 'error' || shouldShowRuntimeNotice(method, message)) {
sendRuntime(entry, sessionId, { type: 'system_message', message });
}
+ if (reconnectNotice) return { done: false, transient: true };
}
return { done: method === 'error' };
}
diff --git a/public/app.js b/public/app.js
index 986a1c8..563cf9b 100644
--- a/public/app.js
+++ b/public/app.js
@@ -7979,11 +7979,13 @@
const isCrossConversation = !!meta.crossConversation;
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
const canCollapseCrossConversationReply = role === 'assistant' && isCrossConversationReply;
+ const isGoalMessage = role === 'user' && meta?.ccwebGoalCommand?.action === 'set';
const resolvedMessageId = meta?.messageId || meta?.id || createLocalId('user');
- div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}`;
+ div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}${isGoalMessage ? ' goal-message' : ''}`;
if (role === 'user') {
div.id = `hapi-message-${resolvedMessageId}`;
div.dataset.messageId = resolvedMessageId;
+ if (isGoalMessage) div.dataset.goalMode = 'true';
}
if (role === 'system') {
@@ -8109,6 +8111,13 @@
}
if (role === 'user') {
+ if (isGoalMessage) {
+ const goalLabel = document.createElement('div');
+ goalLabel.className = 'goal-message-label';
+ goalLabel.textContent = '目标模式';
+ goalLabel.setAttribute('aria-label', '目标模式');
+ bubble.appendChild(goalLabel);
+ }
if (content) {
const textNode = document.createElement('div');
textNode.className = 'msg-text';
diff --git a/public/index.html b/public/index.html
index 7d5b443..3a23a18 100644
--- a/public/index.html
+++ b/public/index.html
@@ -24,7 +24,7 @@
document.documentElement.dataset.dividerTime = dividerTime;
})();
-
+
diff --git a/public/style.css b/public/style.css
index d28d8d1..61fd990 100644
--- a/public/style.css
+++ b/public/style.css
@@ -2991,6 +2991,38 @@ html[data-theme='wasteland'] .advanced-search-panel {
border-bottom-right-radius: 4px;
padding-right: 42px;
}
+.msg.user.goal-message .msg-bubble {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+.goal-message-label {
+ display: inline-flex;
+ align-items: center;
+ align-self: flex-start;
+ gap: 5px;
+ max-width: 100%;
+ margin: -2px 0 7px;
+ padding: 2px 7px;
+ border: 1px solid rgba(255, 255, 255, 0.24);
+ border-radius: 5px;
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.86);
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1.35;
+ letter-spacing: 0;
+ white-space: nowrap;
+}
+.goal-message-label::before {
+ width: 6px;
+ height: 6px;
+ flex: 0 0 6px;
+ border-radius: 50%;
+ background: currentColor;
+ content: '';
+ opacity: 0.82;
+}
.msg-mentions {
display: flex;
flex-wrap: wrap;
@@ -9508,6 +9540,12 @@ html[data-theme='wasteland'] .msg.user .msg-bubble {
box-shadow: inset 0 1px rgba(255, 230, 180, 0.03);
}
+html[data-theme='wasteland'] .msg.user.goal-message .goal-message-label {
+ border-color: rgba(196, 154, 90, 0.42);
+ background: rgba(185, 139, 75, 0.14);
+ color: #e0bd7a;
+}
+
html[data-theme='wasteland'] :is(.msg.assistant, .msg.cross-conversation, .msg.cross-conversation-reply, .msg.system) .msg-bubble {
background: rgba(5, 7, 7, 0.1);
border-color: rgba(185, 139, 75, 0.16);
diff --git a/scripts/mock-codex-app-server.js b/scripts/mock-codex-app-server.js
index d5fe99d..4f0e79e 100755
--- a/scripts/mock-codex-app-server.js
+++ b/scripts/mock-codex-app-server.js
@@ -917,7 +917,7 @@ function startTurn(params) {
message: 'Reconnecting... 1/5',
},
});
- thread.activeTurnId = null;
+ emitCapacityError(thread, turnId);
return { turn: { id: turnId, status: 'running', items: [] } };
}
}
diff --git a/scripts/regression.js b/scripts/regression.js
index 0c15b48..10cdc6a 100644
--- a/scripts/regression.js
+++ b/scripts/regression.js
@@ -671,7 +671,7 @@ function assertFrontendSidebarCollapseContract() {
'Rich themes should provide isolated rail treatments on top of the shared semantic fallback'
);
assert(
- indexSource.includes('style.css?v=20260805-usage-loading-inline')
+ indexSource.includes('style.css?v=20260818-goal-mode-label')
&& indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'),
'Sidebar interaction assets should share the reviewed cache-busting version'
);
@@ -1143,7 +1143,7 @@ function assertPlanListProgressContract() {
assert(extractorSource.includes('references/source-assets/wasteland-icon-sheet.webp'), 'Plan progress extractor should read the archived source sheet');
assert(!extractorSource.includes('sessions/_attachments'), 'Plan progress extractor should not depend on temporary session attachments');
- assert(indexSource.includes('style.css?v=20260805-usage-loading-inline'), 'Plan progress CSS should use the current cache-busted URL');
+ assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Plan progress CSS should use the current cache-busted URL');
assert(indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'), 'Plan progress frontend logic should use the dynamic cache-busted URL');
}
@@ -1259,7 +1259,7 @@ function assertFrontendGildedThemeContract() {
assert(contrast('#655446', '#fff7ea') >= 4.5, 'Gilded muted text should remain readable on ivory panels');
assert(contrast('#fff7ea', '#7a3f20') >= 7, 'Gilded primary action text should reach AAA contrast on copper');
assert(themeStyle.includes('@media (prefers-reduced-motion: reduce)'), 'Gilded theme motion should respect reduced-motion preferences');
- assert(indexSource.includes('style.css?v=20260805-usage-loading-inline'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
+ assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
assert(indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'), 'Theme bundle app script should use the dynamic cache-busted asset URL');
}
@@ -1513,7 +1513,7 @@ function assertFrontendWastelandThemeContract() {
assert(contrast('#c9bda6', backgroundColor) >= 4.5, `Wasteland muted text should reach AA contrast on ${backgroundColor}`);
});
- assert(indexSource.includes('style.css?v=20260805-usage-loading-inline'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
+ assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
assert(indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'), 'Wasteland registration should share the dynamic cache-busted theme bundle URL');
}
@@ -2446,6 +2446,71 @@ function assertCodexAppRuntimeSubAgentActivityContract() {
assert(!Object.prototype.hasOwnProperty.call(reasoningEnd, 'name'), 'Runtime non-subAgentActivity reasoning tool_end should not gain name');
}
+function assertCodexAppTransientReconnectContract() {
+ const { createCodexAppRuntime } = require(path.join(REPO_DIR, 'lib', 'codex-app-runtime'));
+ const sent = [];
+ const runtime = createCodexAppRuntime({
+ wsSend: (_ws, payload) => sent.push(payload),
+ loadSession: () => null,
+ saveSession: () => {},
+ });
+ const entry = { ws: {}, toolCalls: [], fullText: '' };
+ const sessionId = 'codexapp-transient-reconnect-session';
+
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
+ const result = runtime.processCodexAppNotification(entry, {
+ method: 'error',
+ params: { message: `Reconnecting... ${attempt}/5` },
+ }, sessionId);
+ assert(result?.done === false, 'Codex App reconnect progress must not terminate the active turn');
+ assert(!entry.lastError, 'Codex App reconnect progress must not be persisted as the final error');
+ }
+
+ const finalResult = runtime.processCodexAppNotification(entry, {
+ method: 'error',
+ params: {
+ error: {
+ type: 'service_unavailable_error',
+ code: 'server_is_overloaded',
+ message: 'No available providers',
+ },
+ },
+ }, sessionId);
+ assert(finalResult?.done === true, 'Codex App service-unavailable error must terminate the failed turn');
+ assert(/No available providers/.test(entry.lastError || ''), 'Codex App terminal error should remain available for retry classification');
+
+ const combinedEntry = { ws: {}, toolCalls: [], fullText: '' };
+ const combinedResult = runtime.processCodexAppNotification(combinedEntry, {
+ method: 'error',
+ params: { message: 'Reconnecting... 5/5: unexpected status 503 Service Unavailable' },
+ }, sessionId);
+ assert(combinedResult?.done === true, 'Reconnect text combined with a terminal 503 must still terminate the failed turn');
+ assert(/503 Service Unavailable/.test(combinedEntry.lastError || ''), 'Combined terminal errors must remain available for retry classification');
+ assert(sent.filter((message) => message.type === 'system_message').length === 7, 'Reconnect progress and terminal errors should each be forwarded as system messages');
+}
+
+function assertGoalModeTitleContract() {
+ const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
+ const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8');
+ const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8');
+ const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
+
+ assert(frontendSource.includes("meta?.ccwebGoalCommand?.action === 'set'"), 'Goal messages should be identified from dedicated metadata');
+ assert(frontendSource.includes("isGoalMessage ? ' goal-message' : ''"), 'Goal messages should receive a dedicated DOM class');
+ assert(frontendSource.includes("goalLabel.textContent = '目标模式'"), 'Goal messages should render the visible mode label');
+ assert(frontendSource.includes("div.dataset.goalMode = 'true'"), 'Goal messages should expose a stable goal-mode data attribute');
+ assert(styleSource.includes('.msg.user.goal-message .msg-bubble'), 'Goal label styling should remain scoped to user Goal bubbles');
+ assert(styleSource.includes('.goal-message-label'), 'Goal mode label should have a dedicated style');
+ assert(styleSource.includes("html[data-theme='wasteland'] .msg.user.goal-message .goal-message-label"), 'Wasteland should provide a dedicated Goal label treatment');
+ assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Goal label CSS should use the current cache-busted URL');
+ assert(serverSource.includes('GOAL_DERIVED_TITLE_MAX_CHARS = 60'), 'Goal-derived titles should keep the 60-character limit');
+ assert(serverSource.includes('function isDefaultConversationTitle(session)'), 'Goal title updates should use a default-title guard');
+ assert(serverSource.includes('function applyDerivedGoalConversationTitle(session, objective)'), 'Goal title updates should stay in the /goal-specific server path');
+ assert(serverSource.includes("type: 'session_renamed'"), 'Goal title updates should notify current viewers');
+ assert(serverSource.includes('broadcastSessionList();'), 'Goal title updates should refresh the session list');
+ assert(serverSource.includes('mcp_servers.${item.server}'), 'Codex App thread config should continue to inject runtime MCP servers');
+}
+
function assertFrontendPrimaryCodexAppUiContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8');
@@ -4902,7 +4967,7 @@ function assertAdvancedSessionSearchContract() {
'Existing sidebar search clear control contract should remain unchanged');
assert(indexSource.includes('id="advanced-search-panel"') && indexSource.includes('id="advanced-search-results"'),
'Advanced search workspace should expose stable panel and result hooks');
- assert(indexSource.includes('style.css?v=20260805-usage-loading-inline')
+ assert(indexSource.includes('style.css?v=20260818-goal-mode-label')
&& indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'),
'Advanced search CSS and frontend script should share the reviewed cache-bust');
@@ -5019,7 +5084,7 @@ function assertUsageStatisticsContract() {
'Usage dashboard should be a chat-main-local workspace');
assert(indexSource.includes('class="usage-dashboard-open"') && !indexSource.includes('class="settings-btn usage-dashboard-open"'),
'Usage entry must use its own class so theme-specific settings pseudo-elements cannot leak');
- assert(indexSource.includes('style.css?v=20260805-usage-loading-inline')
+ assert(indexSource.includes('style.css?v=20260818-goal-mode-label')
&& indexSource.includes('vendor/echarts.min.js?v=5.6.0')
&& indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'),
'Usage dashboard CSS, local ECharts runtime, and frontend script should share the current asset contract');
@@ -5878,6 +5943,16 @@ async function main() {
console.log('Sub-agent card routing regression checks passed.');
return;
}
+ if (regressionTarget === 'codexapp-retry-runtime') {
+ assertCodexAppTransientReconnectContract();
+ console.log('Codex App retry runtime regression checks passed.');
+ return;
+ }
+ if (regressionTarget === 'goal-mode-title') {
+ assertGoalModeTitleContract();
+ console.log('Goal mode/title regression checks passed.');
+ return;
+ }
throw new Error(`Unknown regression target: ${regressionTarget}`);
}
@@ -5897,6 +5972,8 @@ async function main() {
assertPlanListProgressContract();
assertFrontendSubagentCardMetadataContract();
assertCodexAppRuntimeSubAgentActivityContract();
+ assertCodexAppTransientReconnectContract();
+ assertGoalModeTitleContract();
assertFrontendPrimaryCodexAppUiContract();
assertSetTitleMcpContract();
assertSessionItemTooltipContract();
@@ -6954,6 +7031,87 @@ async function main() {
const codexAppSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.cwd === codexAppCwd);
assert(codexAppSession.model === 'gpt-5.5(max)', 'Codex App new_session should preserve the max default Codex model');
+ const codexAppGoalTitleCwd = path.join(tempRoot, 'codexapp-goal-title');
+ mkdirp(codexAppGoalTitleCwd);
+ ws.send(JSON.stringify({ type: 'new_session', agent: 'codexapp', cwd: codexAppGoalTitleCwd, mode: 'yolo' }));
+ const codexAppGoalTitleSession = await nextMessage(messages, ws, (msg) => (
+ msg.type === 'session_info' && msg.agent === 'codexapp' && msg.cwd === codexAppGoalTitleCwd
+ ));
+ assert(codexAppGoalTitleSession.title === 'New Chat', 'A new Goal title fixture should start with the default title');
+ const goalTitleObjective = ' 修复 Goal 自动标题\n并保留 ccweb MCP 注册以及窄屏标签 ';
+ const goalTitleExpected = goalTitleObjective.replace(/\s+/g, ' ').trim().slice(0, 60);
+ ws.send(JSON.stringify({
+ type: 'message',
+ text: `/goal${goalTitleObjective}`,
+ sessionId: codexAppGoalTitleSession.sessionId,
+ mode: 'yolo',
+ agent: 'codexapp',
+ requestId: 'regression-goal-title-request',
+ clientMessageId: 'regression-goal-title-message',
+ }));
+ const goalTitleBubble = await nextMessage(messages, ws, (msg) => (
+ msg.type === 'session_message' &&
+ msg.sessionId === codexAppGoalTitleSession.sessionId &&
+ msg.message?.id === 'regression-goal-title-message'
+ ));
+ assert(goalTitleBubble.message.ccwebGoalCommand?.action === 'set', 'Goal title fixture should persist dedicated Goal metadata');
+ const goalTitleRenamed = await nextMessage(messages, ws, (msg) => (
+ msg.type === 'session_renamed' &&
+ msg.sessionId === codexAppGoalTitleSession.sessionId &&
+ msg.title === goalTitleExpected
+ ));
+ assert(goalTitleRenamed.titleSource === 'derived', 'Goal default title should be marked as derived');
+ const goalTitleList = await nextMessage(messages, ws, (msg) => (
+ msg.type === 'session_list' &&
+ msg.sessions.some((item) => (
+ item.id === codexAppGoalTitleSession.sessionId &&
+ item.title === goalTitleExpected &&
+ item.titleSource === 'derived'
+ ))
+ ));
+ assert(goalTitleList.sessions.some((item) => item.id === codexAppGoalTitleSession.sessionId && item.title === goalTitleExpected), 'Goal derived title should refresh the session list');
+ await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppGoalTitleSession.sessionId && /正在同步 Goal/.test(msg.message || ''));
+ await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppGoalTitleSession.sessionId && /Goal active/.test(msg.message || ''));
+ await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppGoalTitleSession.sessionId && /Goal background output/.test(msg.text || ''));
+ await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppGoalTitleSession.sessionId);
+ const storedGoalTitleSession = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppGoalTitleSession.sessionId}.json`), 'utf8'));
+ assert(storedGoalTitleSession.title === goalTitleExpected, 'Goal derived title should persist to the session file');
+ assert(storedGoalTitleSession.titleSource === 'derived', 'Persisted Goal title should retain derived source');
+ assert(!storedGoalTitleSession.titleHistory?.some((event) => event.source === 'derived'), 'Goal derived title should not fabricate an LLM title history event');
+
+ ws.send(JSON.stringify({ type: 'message', text: 'codexapp dynamic goal mcp config probe', sessionId: codexAppGoalTitleSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
+ const goalMcpConfigProbe = await nextMessage(messages, ws, (msg) => (
+ msg.type === 'tool_end' &&
+ msg.sessionId === codexAppGoalTitleSession.sessionId &&
+ msg.toolUseId === 'mcp-ccweb-list'
+ ));
+ assert(/"hasCcwebMcpConfig": true/.test(goalMcpConfigProbe.result || ''), 'Goal thread should retain ccweb MCP configuration after thread/goal/set');
+ await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppGoalTitleSession.sessionId);
+
+ ws.send(JSON.stringify({ type: 'rename_session', sessionId: codexAppGoalTitleSession.sessionId, title: '手动 Goal 标题' }));
+ await nextMessage(messages, ws, (msg) => (
+ msg.type === 'session_renamed' &&
+ msg.sessionId === codexAppGoalTitleSession.sessionId &&
+ msg.title === '手动 Goal 标题'
+ ));
+ ws.send(JSON.stringify({
+ type: 'message',
+ text: '/goal second objective must not overwrite manual title',
+ sessionId: codexAppGoalTitleSession.sessionId,
+ mode: 'yolo',
+ agent: 'codexapp',
+ requestId: 'regression-goal-manual-request',
+ clientMessageId: 'regression-goal-manual-message',
+ }));
+ await nextMessage(messages, ws, (msg) => msg.type === 'session_message' && msg.message?.id === 'regression-goal-manual-message');
+ await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppGoalTitleSession.sessionId && /正在同步 Goal/.test(msg.message || ''));
+ await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppGoalTitleSession.sessionId && /Goal active/.test(msg.message || ''));
+ await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppGoalTitleSession.sessionId && /Goal background output/.test(msg.text || ''));
+ await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppGoalTitleSession.sessionId);
+ const storedManualGoalTitleSession = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppGoalTitleSession.sessionId}.json`), 'utf8'));
+ assert(storedManualGoalTitleSession.title === '手动 Goal 标题', 'Goal must not overwrite a manually renamed title');
+ assert(storedManualGoalTitleSession.titleSource === 'manual', 'Manual Goal title protection should retain manual source');
+
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-skill', trigger: '$', query: 'reg', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
const codexAppSkillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-skill');
assert(codexAppSkillComposer.items.some((item) => item.kind === 'skill' && item.name === 'regression-skill'), 'Codex App composer skill suggestions should include local Codex skill');
diff --git a/server.js b/server.js
index c04302d..c70244b 100644
--- a/server.js
+++ b/server.js
@@ -819,6 +819,8 @@ const VALID_AGENTS = new Set(['claude', 'codex', 'codexapp']);
const VALID_PERMISSION_MODES = new Set(['default', 'plan', 'yolo']);
const VALID_TITLE_SOURCES = new Set(['derived', 'llm', 'manual', 'system']);
const MCP_CONVERSATION_TITLE_MAX_CHARS = 120;
+const GOAL_DERIVED_TITLE_MAX_CHARS = 60;
+const DEFAULT_CONVERSATION_TITLE_VALUES = new Set(['new chat', 'untitled']);
const MCP_PROMPT_TITLE_MAX_CHARS = 160;
const MCP_PROMPT_DESCRIPTION_MAX_CHARS = 2000;
const MCP_PROMPT_QUESTION_MAX_COUNT = 10;
@@ -8102,9 +8104,10 @@ function persistCodexAppGoalDisplayMessage(session, command, source = {}) {
},
};
session.messages.push(message);
+ const titleUpdate = applyDerivedGoalConversationTitle(session, command.objective);
session.updated = timestamp;
saveSession(session);
- return { message, created: true, duplicate: false };
+ return { message, created: true, duplicate: false, titleUpdate };
}
async function ensureCodexAppGoalThread(session) {
@@ -8214,6 +8217,15 @@ async function handleCodexAppGoalSlashCommand(ws, text, session, source = {}) {
sessionId: session.id,
message: goalDisplayMessage,
});
+ if (persisted.titleUpdate?.changed) {
+ sendSessionEventToViewers(session.id, {
+ type: 'session_renamed',
+ sessionId: session.id,
+ title: session.title,
+ ...publicTitleMetadata(session),
+ });
+ broadcastSessionList();
+ }
} catch (err) {
sendGoalSystemMessage(ws, `Goal failed: ${err?.message || err}`, { sessionId: session.id }, { preserveComposerDraft: true });
return;
@@ -8552,6 +8564,31 @@ function isTitleLockedByUser(session) {
return String(session?.titleSource || '').trim().toLowerCase() === 'manual';
}
+function isDefaultConversationTitle(session) {
+ if (!session || isTitleLockedByUser(session)) return false;
+ const normalized = String(session.title || '').replace(/\s+/g, ' ').trim().toLowerCase();
+ return !normalized || DEFAULT_CONVERSATION_TITLE_VALUES.has(normalized);
+}
+
+function deriveGoalConversationTitle(objective) {
+ const normalized = String(objective || '').replace(/\s+/g, ' ').trim();
+ return [...normalized].slice(0, GOAL_DERIVED_TITLE_MAX_CHARS).join('');
+}
+
+function applyDerivedGoalConversationTitle(session, objective) {
+ if (!isDefaultConversationTitle(session)) {
+ return { changed: false, title: session?.title || 'Untitled' };
+ }
+ const title = deriveGoalConversationTitle(objective);
+ if (!title) return { changed: false, title: session?.title || 'Untitled' };
+ const previousTitle = session.title || 'Untitled';
+ const changed = previousTitle !== title
+ || String(session.titleSource || '').trim().toLowerCase() !== 'derived';
+ session.title = title;
+ session.titleSource = 'derived';
+ return { changed, title, previousTitle };
+}
+
function publicTitleMetadata(session) {
return {
titleSource: String(session?.titleSource || '').trim().toLowerCase() || null,
diff --git a/重新打包并提交推送 TO DO list.csv b/重新打包并提交推送 TO DO list.csv
new file mode 100644
index 0000000..08c3963
--- /dev/null
+++ b/重新打包并提交推送 TO DO list.csv
@@ -0,0 +1,7 @@
+id,item,status,done_at,notes
+1,确认 Bun baseline 构建器与 Git 推送目标,DONE,2026-08-18T21:26:00+08:00,main 分支;origin 远端;已找到本地 baseline Bun
+2,重新生成 CentOS 7 baseline 发布包,DONE,2026-08-18T21:30:00+08:00,baseline Bun 1.3.14 构建成功
+3,执行发布包与语法验证,DONE,2026-08-18T21:32:00+08:00,语法检查、MCP initialize、tar 清单均通过;SHA-256 f14c1086...
+4,暂存当前工作区全部修改,DONE,2026-08-18T21:35:00+08:00,全部工作区修改统一纳入暂存范围
+5,创建包含全部修改的提交,DONE,2026-08-18T21:37:00+08:00,全部 27 个变更路径已进入统一提交
+6,推送当前分支并确认远端,IN_PROGRESS,,