Skip to content

Reject malformed numeric CLI options - #17

Open
xy200303 wants to merge 4 commits into
stepfun-ai:mainfrom
xy200303:fix/strict-cli-option-parsing
Open

Reject malformed numeric CLI options#17
xy200303 wants to merge 4 commits into
stepfun-ai:mainfrom
xy200303:fix/strict-cli-option-parsing

Conversation

@xy200303

@xy200303 xy200303 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Reject malformed numeric CLI inputs instead of accepting partial parseInt / parseFloat parses.
  • Reuse the shared non-negative integer parser for step artifacts list --limit.
  • Keep the branch current with main and make the newly merged Windows-sensitive tests pass under normal Windows permissions.

Verification

  • pnpm exec vitest run src/commands/option-parsers.test.ts
  • pnpm exec vitest run packages/utils/src/path.test.ts packages/utils/src/shell.test.ts
  • pnpm exec prettier --check src/commands/option-parsers.ts src/commands/artifacts-command.ts src/commands/option-parsers.test.ts
  • pnpm exec tsc --noEmit --pretty false
  • pnpm check

Notes

@xy200303
xy200303 force-pushed the fix/strict-cli-option-parsing branch from 236395e to fadf4e3 Compare June 12, 2026 05:32
@ZouR-Ma

ZouR-Ma commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the contribution!

To get this ready to merge:

  • Reference an issue in the description with Closes #<number> (our link-check CI requires it; open one first if needed).
  • Make sure pnpm check passes locally (lint, type-check, dependency/dead-code guards, formatting, and tests — also enforced in CI).

Full conventions:
https://github.com/stepfun-ai/Step-Realtime-CLI/blob/main/CONTRIBUTING.md

@github-actions github-actions Bot added area/cli src/main.ts, src/cli.tsx, src/i18n.ts, src/version.ts, src/utils area/utils packages/utils labels Jun 18, 2026
@xy200303

Copy link
Copy Markdown
Contributor Author

Thanks! I've addressed the feedback on this PR:

  • Opened issue Reject malformed numeric CLI options #66 and linked it from the PR description with Closes #66.
  • Updated the PR description with the current verification steps.
  • Confirmed pnpm check passes locally and all PR checks are green.

This should be ready for review/merge when you have a chance.

@ZouR-Ma

ZouR-Ma commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this PR — plugging the partial-parse hole (previously --limit 12abc was silently accepted as 12) and consolidating the duplicate parser in artifacts-command.ts into the shared module are both exactly the right calls. Two things before this can land:

  1. (Must) Take packages/utils/src/path.test.ts and shell.test.ts out of this PR. Those two files address Windows local-test compatibility, which is unrelated to this PR's topic — and #75 is already handling exactly these two files, using the vi.skipIf pattern that CONTRIBUTING.md prescribes (the process.platform === "win32" ? it.skip : it approach here is the hardcoded platform check the guideline advises against). Dropping them won't break CI — those failures only occur on local Windows without Developer Mode; the GitHub Windows lane is green on main. Acceptance criterion: the file list shrinks to option-parsers, artifacts-command, and their tests.
  2. (Must) Add tests for the new behavior. The existing cases in option-parsers.test.ts only cover fully non-numeric input ("abc"), which the old code rejected too. The rejections this PR actually introduces — "12abc", "3.14xyz", "1e3" as an integer, values beyond the safe-integer range — currently have zero coverage. Please add cases for these. Acceptance criterion: new test cases cover the partial-parse scenarios and pass.

Ping us after you push — once slimmed down, this can land quickly.

@ZouR-Ma

ZouR-Ma commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this PR — the new theme.test.ts deserves a special mention: a proper WCAG relative-luminance contrast check with well-chosen thresholds (7:1 for body text, 4.5:1 for muted text, 3:1 for structural lines), which gives every future theme change a regression guard. The brightened muted / line values across the five builtin themes and the improved /theme recovery copy are also the right calls. Two things before this lands:

  1. (Must) Rebase onto the latest main and drop the changes to packages/utils/src/path.test.ts and shell.test.ts. Windows test compatibility has been handled by #75, which landed on main using the vi.skipIf pattern that docs/TESTING.md prescribes. The equivalent changes in this branch will conflict on rebase — resolve by keeping main's version and discarding this branch's. Acceptance criterion: the file list shrinks to the three files under src/tui/ (the one-line docs fix is fine to keep).

  2. (Must) Keep the role colors — errors must stay red. The current change renders all badge text in theme.foreground and all rails in the line color. Contrast passes, but the transcript loses its color coding: user/assistant/tool entries become indistinguishable by color and error entries are no longer red, which is not what we want. Please restore per-tone colors in resolveTranscriptBadgeStyle, picking contrast-passing brighter variants per theme — danger stays red, warning stays yellow, success stays green; only brighten the values that are too dark on the badge backgrounds. Also add contrast assertions for these tone colors against the badge backgrounds to theme.test.ts (we'd suggest ≥ 4.5:1), so "role colors and legibility" is guaranteed by tests. Acceptance criteria: across all five themes the danger badge and rail read visibly red, and the new tone-color contrast assertions pass.

Ping us after you push — and we'd suggest running pnpm tui:dev and cycling through the five themes with /theme to sanity-check the look.

li-xiu-qi added a commit to li-xiu-qi/Step-Realtime-CLI that referenced this pull request Aug 7, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末
  persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时
  扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重),
  两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。
  App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。
- 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充
  改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker
  加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行
  chrome 余量),小终端弹层不再越线。
li-xiu-qi added a commit to li-xiu-qi/Step-Realtime-CLI that referenced this pull request Aug 7, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末
  persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时
  扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重),
  两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。
  App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。
- 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充
  改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker
  加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行
  chrome 余量),小终端弹层不再越线。
li-xiu-qi added a commit to li-xiu-qi/Step-Realtime-CLI that referenced this pull request Aug 20, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末
 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时
 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重),
 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。
 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。
- 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充
 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker
 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行
 chrome 余量),小终端弹层不再越线。
li-xiu-qi added a commit to li-xiu-qi/Step-Realtime-CLI that referenced this pull request Aug 21, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末
 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时
 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重),
 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。
 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。
- 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充
 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker
 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行
 chrome 余量),小终端弹层不再越线。
ZouR-Ma added a commit that referenced this pull request Aug 21, 2026
* feat: step doctor config 显示 media_keep_recent 当前生效值

**改动**:doctor 输出加一行 info 提示 media_keep_recent 的当前值:
- 已配置:`info: media_keep_recent = 5(媒体降级时保留最近 5 张图,0 = 全部换占位)`
- 未配置:`info: media_keep_recent 未配置,缺省 3(媒体降级时保留最近 3 张图)`

**价值**:用户可能不知道这个键的存在,doctor 输出明示当前生效值,避免「配了但不知道有没有生效」。

**测试**:configDoctor.test.ts 9 例全绿,防漂移测试通过。

* fix: tool_result 裸对象 content 防御(cc-switch #6170 边缘 case)

**问题**:tool_result 的 content 字段如果是裸对象(非 string、非数组),
for...of 会抛 TypeError: c is not iterable,整个请求崩溃。

**真实场景**:MCP 工具可能输出裸 image/text 对象作为 tool_result content
(多厂商代理层实测,cc-switch #6170)。严格网关(部分国产端点)会因此 400。

**修复**:toolResultContent 加裸对象分支,按 type 分发:
- 裸 image 对象 → image_url(data URI)
- 裸 text 对象 → string
- 无法识别的裸对象 → 空字符串(丢弃,不崩溃)

**测试**:+3 例(裸 image / 裸 text / 无法识别对象),9 例全绿。
全量 2348 测试通过。

* feat: 媒体降级推广到全通道 + 方言识别 + 默认保留 10 张

- degrader.isReprojectableError:400 不再一刀切,只在文案命中媒体方言
 (stepfun/Anthropic/Gemini/vLLM/OpenAI 兼容,全部来自公开 issue 与实测)
 且非上下文溢出时可重投影;413 语义唯一保持直通。裸 400 参数错误不再被
 降级掩盖。
- 降级占位文本保留「原图因 API 限制被移除」语义(IMAGE_DEGRADED_TEXT),
 与主动降级的 image omitted 区分——防模型失忆(公开 issue 实录教训)。
- 新增 withMediaDegradation wrapper:把 StepfunAdapter.send 的错误驱动
 重投影抽成通道无关装饰器,包装任意 ChatProvider 的 stream.finalMessage,
 每档每请求一次熔断(防 400 无限循环)。
- factory 全通道接入:openai/openai_responses/anthropic 均包 wrapper;
 stepfun 沿用 adapter(其 send 已有等价逻辑)。
- media_keep_recent 缺省 3→10(step-3.7 实测 60 张上限的 1/6 安全值);
 [models.*] 下可按别名覆盖(resolveModelEntry 合并)。
- 单测:degrader 方言识别 6 例(含裸 400 反向例)、mediaDegradation
 wrapper 7 例;adapter/degrader 旧断言同步新文案。全量测试绿。
- 端到端:step37-plan 与 k3 读图正常,wrapper 不破坏正常请求。

实测(2026-08-06 直连 API):step-3.7-flash 图片上限 60 张/请求、
单张最长边 8000px、60×1024px 组合载荷 27MB 通过。

* feat: read_media 加 probe 模式 + region 超限自动建议,消除分块猜测试错

读大图/长图时 agent 不知道图片实际尺寸,只能猜 region 参数,猜错就报
「region 超出图片范围」,再猜再错——30000px 长截图的真实痛点(一次
分块阅读要试 3 次才能算准末块边界)。

- probe:true:只回元数据(格式/尺寸/字节数)+ 建议分块方案,不交付图片。
 建议 region 按交付长边上限(1568px)切块,长图按高度方向、宽图按宽度
 方向,末块用 min(剩余, 块高) 自动收窄——调用方照抄即可不超界。
 短边图直接说「无需分块」。
- region 超限错误:自动给出 clamp 后的可重试 region(起点收进图内、
 跨度按剩余收窄),并提示可用 probe:true 拿完整方案。
- 工具 description 补 probe 用法提示。

单测 5 例(probe 小图/长图/宽图、region 建议两种超界形态),其中长图
用例逐个回读 20 个建议 region 验证照抄不超界。全量 2362 测试绿。
端到端:508×30173 真实长图 probe 返回 20 块方案,末块 381px 正确收窄。

* docs: 媒体降级全通道与 read_media probe 补进配置/工具文档(中英)+ CHANGELOG

- configuration.md(zh/en):顶层键表与别名表补 media_keep_recent;
 新增「媒体降级 / Media degradation」节(降级档位、方言触发识别、
 默认值 10 的实测依据、全通道生效、prompt cache 边界)。
- tools.md(zh/en):read_media 参数表补 probe;行为边界补
 「超限自动建议 clamp region」与「读大/长图先 probe」。
- CHANGELOG Unreleased:媒体降级全通道与 probe 两条进 Added。

* fix: streamBuffer delta 按到达顺序消费 + logo 左肩斜切

- streamBuffer flush 从固定 text→thinking 顺序改为按到达顺序队列消费,
 修复 thinking 泄露到正文下方的问题
- WelcomeBox logo 左肩加斜切角(▐ 右半格),削出左肩斜面

* feat: 斜杠命令参数补全(/goal /permission /provider /lang /plugin)

- 各命令注册 getArgumentCompletions,返回子命令候选 + 描述
- /provider 补全自定义渠道 id + 内置预设名
- /plugin 补全磁盘发现的插件 id
- 补全上下文注入 providers/providerPresets/pluginIds

* docs: CHANGELOG 0.1.1 版本挂牌(2026-08-06)

- [Unreleased] 转 [0.1.1] - 2026-08-06
- 新增空 [Unreleased] 段
- 本次 PR 包含:命令参数补全、贴图双轨制、媒体降级全通道、read_media probe、模型切换写回、streamBuffer 修复等

* docs: 文档与 licenses 对齐更新

- README/README_CN:补媒体降级全通道与贴图路径回读的一句话描述
- docs/zh+en/interactive.md:补 Shift+Enter 仅本会话切换模型说明
- ModelPicker onSelect 回调加 sessionOnly 参数,Enter 传 key.shift 控制是否写回 config
- modelPicker 测试断言同步双参数签名
- licenses/NOTICE.md:删除一段专有软件说明,补两条开源协议登记
- licenses/ 新增两个开源协议文件

* fix: CI 环境两处测试失败修复

- firstFrameSmoke:CI 无 API key 时进程直接退出,跳过而非失败(环境限制非代码 bug)
- bash.outputcap:close 事件可能在 stderr data 之前触发,漏掉尾部错误标记;
 改为等 stdout/stderr end 与 close 三者齐了再 snapshot

* fix: bash.outputcap 测试改用 console.error 确保 stderr flush

CI 管道环境下 process.stderr.write 可能不触发 data 事件,
导致 BOOM-MARKER-42 未到达收集器。console.error 会强制 flush。

* fix: bash.outputcap 测试用 process.exit(3) 替代 exitCode=3

process.exitCode=3 时进程自然退出,CI 管道环境下 stderr 的 data 事件
可能在 close 之后才触发或根本不触发。process.exit(3) 强制 flush stdio。

* fix: 模型别名展开后保留原始别名指针(modelAlias)

多别名指向同一真实 id 时(step37/step37-plan 同为 step-3.7-flash),
App 侧用真实 id 反查别名会命中第一个(step37),导致:
- 状态栏显示 Step 3.7 Flash 而非 Step 3.7 Flash(Plan)
- currentModelAliasRef 为 null,/reload 时 provider 重建策略错误
- defaultModelPointerRef 为真实 id,/model 切换时误写真实 id 到 config

修复:StepCodeConfig 新增 modelAlias 字段,loadConfig 展开别名时保留
原始别名指针。App 初始化 currentModelAliasRef/modelLabel/
defaultModelPointerRef 时优先使用 config.modelAlias。

* docs: 修正 Release 状态说明 + 首次运行 API key 配置引导

文档修正(Release 尚未发布):
- quickstart.md / installation.md / SKILL.md 标注单文件可执行与
 Release tarball 为「尚未发布」,避免用户访问空 Releases 页面
- 推荐路径收敛为 npm 预构建分支 / npm 源码分支 / 源码安装三种

首次运行引导(对齐主流 CLI 的引导体验):
- 交互模式启动时检测无 API key,不直接退出,渲染 FirstRunSetup 组件
- 提供「粘贴 key 自动写入配置」与「查看文档手动配置」两个选项
- 粘贴后自动写入 ~/.step-code/config.toml 顶层 api_key 并继续启动
- 非交互模式(-p / --reflect)保持原有报错退出行为

* chore: bump version to 0.1.1

* feat(first-run): 引导向导支持选择接入方式 + 可编辑输入框

- 选择 StepFun Plan / API 按量 / 自定义 base_url,写入对应渠道 section
- 输入框复用 promptEdit 原语 + usePaste,支持光标移动/删除/bracketed paste
- 新增 saveProviderKey 写 [providers.<name>] section 字段
- 修正 version.ts 与 package.json 同步(0.1.1),修两处版本测试

* fix(first-run): 粘贴 key/base_url 剥除换行符,防止写入非法 TOML 毁掉 config

终端粘贴的长 key 常被折行,换行符若进 TOML 字符串即成非法控制字符,
导致整个 config.toml 解析失败、无法启动。两层修复:
- EditableInput 插入前 toSingleLine 剥掉所有 \r/\n(粘贴与逐字符两路径)
- saveProviderKey 写入前再剥一次(兜底,防其他调用方传入脏值)

* feat(first-run): 坏 TOML 配置也能进引导修复,不再直接退出

此前 config.toml 语法错误时 loadConfig 在启动早期直接 exit(1),
恰恰是 config 已坏、最需要修复的用户被挡在引导门外。

- 新增 TomlParseError 类型,cli 用 instanceof 区分坏 TOML 与其他配置错误
- 坏 TOML + 交互模式:先把坏文件改名备份(config.toml.broken-<ts>),
 再进引导写入全新配置、重载生效;备份而非删除,供用户抢救其他配置
- 非交互模式(-p/--reflect)照旧报错退出,不阻塞脚本
- 改名失败(占用/权限)退守复制备份后继续

* feat(first-run): 引导新增选择默认模型步骤,绑定渠道与模型

修复真实设计缺陷:此前引导只写 [providers.X],但启动用顶层 model 别名
所属渠道,用户配了 stepfun-plan、顶层 model 却指向 stepfun 别名,启动仍缺 key。

改为配完渠道后让用户选一个模型,该模型显式绑定刚配的渠道并设为默认
(model -> models.X.provider -> providers.X 三层硬绑定,配完即可用)。

- PROVIDER_OPTIONS 每个渠道带预设模型列表(Plan: router/step37;API: step37/step35)
- custom 渠道用 EditableInput 输入自定义模型 id
- 新增 saveModelAlias 写 [models.<别名>] 段,复用 section 级写入风格
- 选定后 saveDefaultModel 把顶层 model 指向新别名,下次启动即用
- confirm 页显示默认模型;Esc 返回链路相应调整

* feat(logo): TUI banner 换用 space-invader 造型 + 头部左端斜切角

沿用经典 space-invader 三行造型为基底,头部最左端叠加 ▖→▗ 两级
对角块构成左低右高的斜向上角。logo 由 3 行变 4 行,右侧标题块
改 justifyContent=center 垂直居中对齐主体三行。

顺带修复:引导 API 按量渠道 baseUrl 补 /v1(原 https://api.stepfun.com
少了 /v1,会导致该渠道请求 404)。

最终稿与选型记录见产品设计文档「step-code-logo设计.md」。

* fix(cli): 引导 Esc 取消退出不再触发 unsettled top-level await 警告

main.ts 顶层 await import('./cli.js') 时,cli 模块内的 process.exit
(如引导里按 Esc 取消)发生在模块执行中途,进程退出时 main 的顶层
await 仍未 settle,Node 24 打 unsettled 警告。

改为 import().catch() 形式:main 模块立即执行完、进程靠 cli 的事件
循环存活,警告消除,cli 内 process.exit 的退出码不受影响;加载失败
由 catch 打印并以码 1 退出。env.test.ts 的「先设 NODE_ENV 后加载」
断言相应放宽为匹配 import('./cli.js')(不绑定是否顶层 await)。

* fix(tui): thinking 落成瞬间不定稿,避免在途 assistant 双渲染

thinking 落成定稿条目且恰在 items 末尾时,countSettledItems 会把 settled
推到 items.length,仍在流式增长的 assistant 一并冻结进 <Static>(append-only),
下一帧 text 又新开条目进动态区——Static 旧副本 + 动态新副本 = 正文重复,
表现为 thinking 残句碎片混排在正文开头。

修复:countSettledItems 在有效末尾为 thinking 时把 settled 停在其下标,
thinking 留动态区随其后正文一起定稿。间歇性来源是 StreamBuffer 50ms 合帧
时序(thinking_end 与 text 是否同窗)。

补两个针对测试(thinking 是末尾 / thinking 后仅跟透明 note)。
根因登记见产品设计文档「20260807-thinking泄漏到正文-Static定稿时序」。

* fix(tui): text 事件到来时 thinking 与正文原子落定,消除合帧时序竞争

继 4750afc(countSettledItems 止血)之后,从事件处理顺序上根治:
thinking 落成与首个 text 追加改为在同一次 setItems 里原子完成,
thinking 不再单独成为 items 末尾的瞬间,countSettledItems 看到的末尾
恒为 assistant。这一层提供「正文前先落定思考」的强顺序保证,
独立于 StreamBuffer 50ms 合帧时序。

4750afc 的 countSettledItems 守卫保留作防御纵深(两层冗余但安全)。

* fix(dynamic-workflow): 沙箱销毁后在途 spawn 回调短路,修复 UseAfterFree 进程崩溃

Esc 中断/超时触发 sandbox.dispose() 后,仍在运行的子 agent 完成时
其 .then 回调调 deferred.resolve() 指向已销毁的 QuickJS context,
导致 QuickJSUseAfterFree + JS_FreeRuntime gc_obj_list assert,
整个 Node 进程崩溃。

修复:sandbox 暴露 isDisposed 只读标志,__agent 的 resolve/reject
回调开头检查该标志,沙箱已销毁时直接返回不再触碰 ctx。

同 commit 修复关联问题:恢复空会话(崩溃留下的空壳)时无提示,
用户无法区分「恢复成功但历史为空」与「全新会话」。cli.tsx 新增
resumeHit 标志,空会话恢复时 stderr + TUI 双层提示。

* feat(agent): thinking 预算耗尽自动降档重试(thinking_downgrade)

- runTurn 检测 stop_reason=max_tokens + 正文零输出 + 档位可降(非 low/off/未重试过)时,
 自动降到 low 档重发请求 1 次;成功后恢复原档位,用户零操作。
- 降级过程透出 thinking_downgrade 事件(fromLevel/toLevel),TUI 落一条
 boundary note 告知自动恢复发生;降级后仍耗尽退到原提示路径(兜底)。
- 修复 i18n 英文缺键(typecheck 阻断)。
- 补 4 个针对测试:high 降 low 成功 / low 降无可降 / off 不触发 / 重试仍耗尽只试 1 次。
 全量 2368 全绿。

恒思考模型(不可降档)的 max_tokens 翻倍恢复路径见产品设计文档调研汇总,
本期不做(需模型能力注册表先行)。

* feat(agent): thinking 流死循环检测与诱导跳出(thinking_loop)

step-3.7 恒思考(reasoning_effort 最低 low 档,不可关),thinking 死循环时
预算耗尽降档(thinking_downgrade)只在终点介入,且注入预填对恒思考模型无效。
本 commit 在流式中途检测循环并主动跳出:

- 新增 agent/thinkingLoop.ts:纯函数检测器。短周期逐字复读(周期 1-12 字符
 精确循环)+ 长重复段落级(末尾连续窗口在更早内容中重复 ≥3 次)双路径;
 MIN_CHARS=400 门槛防早期误判;fired 一次性防重复触发。只扫流末尾,
 天然避开代码/列表在中部的合理重复(误报控制)。
- runTurn 流式循环内联检测:命中即中止当前流(已吐正文时不中止,守
 「正文不进重试」铁律),构造「诱导跳出」提示追加为新 user 消息重试 1 次。
 注入会污染上下文(reasoning leakage,arXiv:2510.11713),故用
 「终止+新请求」而非同流续写。
- 透出 thinking_loop 事件(sample/repeats/retried),TUI 落 boundary note。
- 测试 9 例:检测器 7 例(复读/段落重复/正常文本/列表不误报/fired 一次性/
 门槛/reset)+ 端到端 2 例(注入重试成功 / 二次循环不重复注入)。
 全量 2377 全绿。

设计文档与调研汇总见产品设计仓。

* feat: thinking 统一回灌 + /resume 懒加载 + AgentGroup 限高折叠

三个子 agent 并发执行的改动汇总:

1. thinking 统一回灌(openaiCommon/openaiChat/factory):
 Chat 通道不再无条件剥离 thinking,按 capability.reasoning 决定。
 reasoning=true 时保留并序列化为 reasoning_content,false 时剥离。
 修复 Step/Kimi/DeepSeek 经 Chat 通道的兼容性问题。

2. /resume 懒加载(session/store):
 索引缓存方案(_index.json),list() 从 O(N IO) 降为 O(1 IO)。
 save/delete/rename 时同步更新索引,崩溃/过期自动重建。

3. AgentGroup 限高折叠(tui/AgentGroup):
 MAX_VISIBLE_AGENTS=8,超出折叠提示「还有 N 个,/tasks 查看全部」。
 排序:running 优先 + startedAt 降序。agentGroupRows 与渲染侧一致。

测试:2394/2394 全部通过(含新增 148 个测试)。

* feat(tui): busy 时 chrome 面板让位——流式正文优先

busy(流式输出)期间 AgentGroup/TodoPanel/QueuePreview 压缩:
- AgentGroup 从列表面板压缩为单行摘要(⠶ N 个子 agent,/tasks 查看)
- TodoPanel / QueuePreview 隐藏
- thinking preview 压缩到 ≤2 行
- idle 时恢复完整显示

实测效果:30 个子 agent busy 时 liveMaxRows 从 ~6 行提升到 ~termRows-10。

测试:2402/2402 全部通过(新增 11 条 busy 相关测试)。

* feat(agent): think-only 自动恢复——降档仍耗尽时落盘 thinking 并注入「直接回答」

降档重试(thinking_downgrade)后仍 max_tokens 零正文时,不再直接走提示路径:
把原始耗尽轮次的 thinking 落盘为独立 assistant 消息(注入提示「基于已有的
分析」所指的分析主体),追加「停止推理直接作答」user 消息后用同一份历史
发新请求,最多 1 次。

- 落盘语义:thinking 单独一条 assistant,恢复正文另起一条(保留「前轮只思考、
 后轮直接作答」的轨迹分界,供 resume 与排查);注入的 user 消息同样落盘。
- 恢复轮次仍耗尽:thinking/注入已落盘,恢复轮的空 thinking 不落盘(与
 「空响应不落盘」一致),走原提示路径,不二次注入。
- 中断:恢复流中途 abort 时,thinking 与注入消息已落定,正文未成不落盘。
- skipFinalPush 标记让恢复路径与循环外统一 push 互斥,避免重复落盘。
- 透出 thinking_recover 事件,TUI 落 boundary note 告知自动恢复发生。
- 7 个针对测试(成功/仍耗尽/abort/注入文案/落盘轨迹/off 不触发/不可恢复)。
- 顺带:openaiChat 测试注释去除竞品名(措辞合规)。

全量 2409 全绿。

* fix(tui): resume 恢复会话后状态栏/别名指针显示错误的模型名

根因:resume 会话的 session.model 存的是别名(如 step37-plan),cli.tsx 用它对 provider 重建是正确的;
但 App 启动时 modelLabel 与 currentModelAliasRef 都优先读了全局 config.modelAlias(如 router),导致
状态栏显示 step-router-v1,实际请求是 step37-plan,显示与实际分叉。

修法:新增纯函数 resolveStartupModelAlias(sessionModel, config),按 session.model 是否命中
config.models 返回命中的别名;modelLabel 与 currentModelAliasRef 初始化优先取 resume 会话别名,
未命中(新建会话 session.model 是展开后的真实 id / 旧格式存真实 id / 空串)回退到原
config.modelAlias / 真实 id 反查路径。defaultModelPointerRef 保持 config.modelAlias 不变
(默认模型指针语义,不应被 resume 污染)。

新增 4 个单元测试钉住别名命中 / 旧格式真实 id / 空串 / undefined 四种输入。
typecheck 0 错,全量 2413 全绿,措辞合规通过。

* feat(tui): banner logo 换为字母 S(Small 字体),红色改蓝色

- 沿用 space-invader 多年的像素小生物 logo 换为字母 S(Step 首字母意象),
 用 FIGlet Small 字体(4 行),与右侧 2 行标题高度协调。
- 渲染颜色由红改蓝。
- 选型过程与备选(Cards / 渐变阴影 / Delta Corps / Z 系列)记录于产品设计
 文档 step-code-logo设计.md;字符画生成器 TAAG 已收录知识库。
- 顺带清理两个 logo 探索临时脚本(未跟踪,直接删除)。

* docs: 收敛子 agent 统计字段的注释表述

tool_uses/duration_ms 相关注释与测试描述,改为只陈述字段含义与设计理由。
功能行为不变(纯措辞)。

* test(session): list 倒序用假时钟错开 updatedAt,消除同毫秒 flaky

* docs: CHANGELOG 补 0.1.1 挂牌后合入的条目;英文安装页标注单文件与 tarball 尚未发布

* docs: CHANGELOG 补登 0.1.1 挂牌后合入的引导重做、思考恢复链、resume 懒加载等条目

* fix: 子会话锁加 stale 检测(pid 判活回收)+ 只读白名单补 5 个纯查询工具

- 子 agent 会话锁:进程被强杀后锁文件残留导致子会话永远无法 resume/delete。
 锁文件写入 pid,acquireLock/delete 发现锁存在时判活,死锁回收后继续。
 旧格式锁(无 pid)按 stale 处理,向后兼容。
- plan 模式只读白名单补 tool_search/task_list/task_output/get_goal/cron_list,
 消除纯查询操作的确认弹窗。

* fix: /new 与 /fork 换绑 BackgroundManager 的 tasksDir

此前仅 /resume 换绑,切换会话后新会话的后台任务仍落盘到旧会话目录,
任务归属错乱。/new /fork 补上同样的换绑;旧管理器直接丢弃不做对账
(旧会话的任务由它自己 resume 时的 reconcile 处理),与 /resume 语义对齐。

* test(tui): SessionPicker 真实 ANSI 流捕获测试——无清屏、帧重写有界

ink-testing-library 剥离 ANSI 只存整帧,测不到「清屏/整页闪换」。
用真实 ink render 写 fake TTY stdout 捕获原始字节流:
连续滚动/搜索收缩/极小终端下断言无 \x1b[2J、\x1b[3J(清 scrollback
是滚动跳顶根因),帧重写行数稳定不整屏。覆盖待办 #26 人工清单中
可自动化的部分(清单 2/5/8 的客观断言)。

* feat: 斜杠命令参数补全落地——/goal /permission /provider /lang /mcp /plugin /history

此前 CHANGELOG 已登记该功能但代码未随提交落盘(commit message 与内容
脱节),本次真正实现:7 个命令注册 getArgumentCompletions,静态子命令
+ 动态渠道/插件 id(CompletionContext 扩展 providers/pluginIds,
App 经 completionCtx 注入,cli 传入已发现的 plugin id 列表)。

* fix: edit_file diff 截断的「Ctrl+O 展开」假承诺改为完整 diff 落盘

截断发生在工具侧,被隐藏的改动从未进 content,Ctrl+O 展开无从恢复。
现在超限时完整 diff 落盘 .step-code/tool-output/edit-diff-*.log(保留
20 个),提示行换成真实可用的路径;落盘失败退回原提示文案。

* fix: 子 agent 会话区改滑动窗口——第 6 条起不再无法选中

此前子区 slice(0,5) 硬截断,游标最多走到第 5 条子会话。现在游标走
全程,5 行显示窗口居中锚定跟随;区头附窗口区间指示(并入区头行,
不额外占行,帧预算口径不变)。

* docs: CHANGELOG 补登锁 stale 检测、tasksDir 换绑、白名单补全等五项修复

* fix: 锁判活拦截 pid<=0 非法数据 + stale 测试改用真实死进程 pid

POSIX 下 kill(-1, 0) 是「发信号给全部进程」的特殊语义会误判存活,
Linux/macOS CI 上 stale 回收测试假失败(Windows 本地绿)。实现侧把
pid<=0 视为非法锁按 stale 回收;测试改用真实启动后立即退出的子进程
pid,覆盖「pid 合法但进程已死」的真实产品场景。

* feat: /think /provider 切换写回配置 + [tools.web] 缓存三阈值接入 config.toml

- /think 写回 [thinking].default_level(off 仅会话级不落盘),/provider
 写回顶层 provider;幂等判断(值相同不写文件),失败只提示不阻断。
 App.tsx 调用点在下一笔(与通知修复同文件一并提交)。
- web 缓存三阈值(max_size/max_bytes/max_entry_bytes)从硬编码默认值
 改为可配置:resolveWebCacheConfig 解析 + WebResultCache.configure
 运行时重配 + 启动与 /reload 时应用 + doctor config 显示生效值。
- docs 中英配置参考同步补 [tools.web] 段,updateConfig skill schema 同步。

* fix: 通知 delivered 事件与消息本体同刻落盘 + picker 视口感知

- 待办 #17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末
 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时
 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重),
 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。
 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。
- 待办 #24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充
 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker
 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行
 chrome 余量),小终端弹层不再越线。

* perf: 子 agent 会话列表加 _index.json 索引缓存

SubagentStore.list 此前对每个快照逐个读盘+全量 JSON 解析(快照含完整
messages,226 条实测环境下每次调用都全量扫)。照搬主会话 store 的索引
模式:list 优先读索引,缺失/过期(目录内任何文件 mtime 晚于 rebuiltAt)
自动重建,create/save/delete 同步更新;重建只收录 .json 快照,.lock 与
.jsonl 不计入。

* feat: 全局 /resume 只列主会话,新增 /agents 查看当前会话的子 agent

子 agent 会话混在全局切换列表里语义错位(只能只读下钻、不能切换过去
对话)、占行挤掉主会话、且其全量解析是 /resume 卡顿主因。现在:
- /resume 只列主会话(走索引缓存,不再触碰子会话目录)
- /agents 列出当前会话派生的子 agent(parentId 过滤),选中下钻只读
 回看历史;选择器复用 SessionPicker(新模式下不提供删除/重命名,
 标题文案区分)
- SessionPicker 加 titleKey 可选入参

* docs: CHANGELOG 补登 /agents 命令、写回配置、picker 视口与通知时序修复

* docs: 0.1.2 版本挂牌——CHANGELOG 重组 + /agents 命令文档(中英)+ 版本号

CHANGELOG:0.1.1 段只保留 tag 内内容,tag 后修复与变更归入新 [0.1.2] 段
(参数补全条目代码实际落在本周期,一并归入)。docs:interactive 命令表
加 /agents(中英),sessions 的子 agent 会话节改为 /agents 入口,
agents.md 工具说明里的下钻引用同步修正。

* docs: 致谢与许可证收录措辞中性化 + 代码注释自述化

- README 中英致谢段、licenses/NOTICE.md 开头段:去掉「研究/参考/受启发
 于 X」类表述,只陈述「源码自行编写、无隶属」与许可证收录的合规事实。
- 5 处代码注释删除对第三方实现的对照性描述(含源码坐标引用),改为
 只陈述我们自身的机制与设计理由。
- docs 中英 sessions.md 明确标注 /agents 下钻为只读、不能在子会话中
 继续对话。

* test: 两处测试标题同步自述化(规则表扩词后检出)

* test: 子会话索引测试用假时钟错开 updatedAt,消除同毫秒倒序 flaky(Linux CI 检出)

* build(release): 安装链路收敛为 latest 永久链接,dist-npm 预构建分支退役

- Release 资产新增固定名 step-code.tgz 副本,latest/download 永久链接可装最新版
- 删除 CI dist-branch job 与 make-dist-branch.mjs(分支已在远程删除)
- 安装文档/快速上手/README(中英)与 step-code-install skill 对齐为四种安装方式
- 单文件可执行与 tarball 段落启用真实下载链接,去除「未发布」标注

* feat(tui): 任务清单面板按状态优先级裁剪,折叠行带状态分布

- 新增 selectVisibleTodos:进行中全保留,最新一条已完成做进度上下文,
 剩余名额按原顺序填待办,待办不足时从最近回补已完成
- 替换 slice(0,5) 硬切:已完成在清单前部堆积时不再把进行中/待办挤出可视区
- 折叠行从 +N more 改为带隐藏条目状态分布(如 +3(1 已完成 · 2 待办))
- i18n 新增 todo.status.* 三个 key(中英),todo.more 改含 detail 占位
- 测试补 8 个用例:裁剪优先级 6 个 + 渲染 2 个

* fix(goal): 工具描述写明预算默认不设上限,纠正模型顺手设预算的倾向

* feat(team): /team 多 agent 团队模式——worktree 物理隔离 + 任务范围互斥 + 依赖硬化门控 + 六道门审阅收编

- 模式与工具:team_init/plan/spawn/merge/teardown/send/inbox/status 八工具 + /team 命令(init/status/exit/teardown),session 级开关,快照随会话落盘恢复
- 隔离:每任务独立 git worktree(.teams/worktrees/,写 .git/info/exclude 不动 .gitignore);写文件 per-worker 硬拦(runner 层 wrapWriteGuard);协调者在模式活跃时不能写代码
- 门控:build 任务范围两两互斥;deps 未全 merged 系统拒绝启动(代码强制,不靠自觉);依赖满足自动解锁
- 收编六道门:门〇空 diff 拒绝(防 worker 直提主仓绕过流程)/ 已审阅 / tip 未移动 / 依赖全并 / 无范围外文件 / --no-ff;冲突自动 merge --abort 救回;成功后自动清理干净 worktree(dirty 保留并提示)
- 生命周期:closedAt 落盘标记(resume 不复活,init 重进清标记保留状态);teardown quit_only 应急强退;lost 联动(BackgroundManager onLost → 仅 active 任务标 blocked,可 respawn 重派);completed 可 rework 返工
- 可指定基准:--repo 指挥其他仓、--base 指定基准分支(缺省当前分支,收编校验 checkout 防合错)
- 引导:内置 team skill(参数组合/反面教材/工作期用法/收编后验证/dist 错位运维知识)+ drift 锚点测试强制同步;init 报错意图识别(dir 误指 git 仓时引导改用 repo)
- TUI:状态栏 team 徽标、/team 忙时即时执行;subagent 请求支持 cwd/writeAllowRoot
- 测试:team 系列 30+ 用例(真实临时 git 仓跑 worktree/merge/门控/信箱/teardown)

* fix(cli): resume 恢复会话后 session.model 同步为真实模型 ID——恢复路径 provider 用别名解析重建成功但漏了同步,别名原样发给服务端导致 404 model_invalid;状态栏正常是因 displayName 走别名查表的另一路径

* feat(tui): 子 agent 可见性 + 队列自动发送保留输入框草稿——spawn 卡片显示角色与任务描述;/tasks 运行中优先、同级新触发在前;状态栏 bg 徽章带最近任务名;submit 加 fromQueue 守卫,队列 drain 与空闲通知注入不再误清用户正在编辑的草稿

* feat(agent): 跨回合零进展检测——回合指纹 + 两段式处置(连续 3 轮警告注入 / 第 4 轮硬停)

模型在 tool_use 分支陷入「看到相同结果 → 做出相同反应」的稳态时(每轮
assistant 消息与工具结果逐字节相同、零信息增量),此前只能烧到
maxIterations=500 大断路器才被拦下。本提交在 loop.ts 的 tool_use 分支
挂上确定性检测:

- 新增 src/agent/roundLoop.ts(纯函数,无 IO):fingerprintRound 取
 messages 尾部 assistant + tool_result 对拼稳定指纹(排除每轮必变的
 调用 id 与消息 ts;工具结果参与指纹,合法轮询结果在变则不触发);
 createRoundLoopDetector 维护 streak,连续相同第 3 轮 warn、第 4 轮 stop
- loop.ts 接线:warn 注入 user 消息 + yield notice(不打断,给一次机会);
 stop 走 notice + turn_done 收尾(模型行为问题不是系统故障,不走 error)
- i18n 新增 loop.roundLoop.{warn,inject,stop} 中英双语
- tests/agent/roundLoop.test.ts 穷举:指纹各成分参与度、streak 清零、
 合法轮询不误伤、is_error 参与、warn/stop 的 runAgent 级集成断言
- subagent.test.ts 压缩估算用例的工具参数逐轮区分:完全同构序列正是
 本检测要拦的形态,会在压缩触发前被硬停,测不到压缩路径
- CHANGELOG 补条目

完整设计见内部产品设计文档(跨回合零进展检测设计)。

* fix(session): 索引过期判定改 >= 修同毫秒竞态——rebuiltAt 是毫秒精度墙钟,写索引与同毫秒内的快照直改 mtime 相等时 > 漏判索引过期(fork Windows CI 的 cleanup ttl_days 随机失败根因,upstream 同内容全绿佐证 flake);测试用 utimesSync 把直改 mtime 拨到未来消除时序依赖;主 SessionStore 同源同修

* feat(agent): 死循环防线加固——round-loop 改滑动窗口 + 单轮步数分级预警

真实事故驱动:一个会话在同一条 bash grep 上原地复读 497 轮,直到撞 500 轮硬上限
才被中止,期间用户侧没有任何提示。回放该会话(1070 条消息 / 518 个回合对)确认
指纹算法本身有效(最大连续相同 streak 达 497,远超阈值),但暴露两个缺口:

一、round-loop 只认「与上一轮相同」。单槽 lastFingerprint 一旦不等就清零 streak,
于是周期≥2 的交替循环(A→B→A→B)每步都与上一步不同,streak 永远停在 1,检测一次
都不会触发;而「读 A 发现要看 B,读 B 又回头看 A」这种两态摆动在工具链路里比原地
复读更常见。改为大小 8 的滑动窗口 + 窗口内出现次数计数:出现 4 次及以上判 stop,
恰好 3 次判 warn。周期 1 的行为与原实现逐条一致,原有用例全部保留守住回归。

二、500 轮硬上限是唯一真正生效过的防线,且中间完全静默、撞线才硬停。现在在
maxIterations 的 50% / 80% 两档各注入一次自查提醒(notice + injection,照 roundLoop
的现成范式),阈值按比例计算而非硬编码,每档一轮交互只触发一次。提醒挂在循环顶部
而非 tool_use 分支——挂分支内会被 roundLoop.stop 的提前 return 吞掉。

i18n 同步:roundLoop 文案措辞从「连续」改为窗口语义,新增 turnWarning.mid / late 双语。

* feat(tools): bash 写操作静态守卫——三档判定纯函数(尚未接线)

bash 工具此前对写操作零检查:write_file / edit_file 受 runner.ts 的 wrapWriteGuard
限制在 allowRoot 内,而 bash 的重定向、mv、rm、sed -i 一律直通,安全边界完全寄托在
模型自觉上(AGENTS.md 与 team skill 都只是把这一点写成告知)。

新增 checkBashWrite(command, cwd, allowRoot) 纯函数,引号感知地拆段后三档判定:

- A 档,目标可解析且越界 → 拒绝。相对与绝对路径统一 resolve 后再判定,路径遍历
 由 resolve 自身处理,不用正则去理解 ..;边界判定避开兄弟目录前缀陷阱。
- B 档,有写入迹象但目标不可解析 → 拒绝,并要求改写成 allowRoot 内的显式路径。
 覆盖变量、命令替换、eval、python -c / node -e / perl -e 内联写入、以及命令内
 cd 越界后的相对路径。这里不做 fail-open:放行等于没拦,而模型收到明确反馈会改写。
- C 档,无写入迹象 → 放行。动态路径判定附加「该段确有写入迹象」门槛,否则
 ls $HOME、grep $PAT f 这类只读命令会被误拦,误拦比漏拦更伤可用性。

生效边界:allowRoot 缺省或为空时一律放行。主 agent 没有 allowRoot,必须保持能往
仓库外写文件(既定行为);只有 team worker 场景才受约束。

本 commit 只落纯函数与单测(93 例,含误报防线;Windows 语义的路径断言按平台跳过),尚未接入
bash.ts 的 execute()。接线、ctx 传参与用户可见文案另做。

已知残留缺口:>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法不覆盖。

* fix(tui): busy 摘要行补 Ctrl+B 转后台提示——提示原先只在它没用的场景显示

AgentGroup 的 backgroundHint 只挂在非 busy 的完整渲染路径(AgentGroup.tsx:215),
而 agentGroupRows 在 busy 时 return 1、面板压缩成单行摘要。前台派生子 agent 时主 agent
必然 busy,于是这条提示在真正需要它的场景永远不显示;只有后台派生且主 agent 空闲时
才出现——那时任务早已在后台,提示没有意义。

不是回归:Ctrl+B 的按键处理(App.tsx:1150)与文案 key 一直都在,缺的是 busy 摘要行
上的入口提示。

修法是在 busy 摘要行按 running > 0 追加一条紧凑提示,风格与已有的「· /tasks 查看全部」
一致,因此新增 agentGroup.busyBackgroundHint 而不复用带括号的 backgroundHint。
摘要行本身是 wrap="truncate",追加文本不会增加行数,agentGroupRows 仍返回 1——
AgentGroup.tsx:101 的注释记着一次「加 backgroundHint 导致帧高超预算越过 rows-1 红线」
的事故,这次刻意避开同一个坑,并在测试里断言行数不变。

补两条回归守卫:有 running 时含提示且行数不变、全部 done 时不含提示。

* test(tui): tasksViewer 预览切换改用确定性 startedAt——真因是列表排序不确定

现象:「↓/j 选择下一条,tail 预览跟着切换」这条用例在 CI 上间歇失败,断言期望预览显示(ta),实际是(tb)。

先前把它归因为「等待不够」并把固定 delay 换成 vi.waitFor,归因错了——换完之后 windows CI 仍挂在同一条断言。真因是列表顺序本身不确定:TasksViewer 的排序是「运行中优先,同级按 startedAt 倒序」(TasksViewer.tsx:165-170),用例里两个任务同为 running,谁排在前完全由 startedAt 决定;而 makeTask 的默认 startedAt 是各自现取 new Date(),于是顺序取决于「两次调用是否落在同一毫秒」。落在同一毫秒时稳定排序保持声明顺序(ta 在前,凑巧满足断言),一旦跨过时钟 tick,tb 变新排到最前,初始选中项就成了 tb。

修法是显式给两个任务 startedAt 并让 ta 比 tb 新(偏移 50ms,两条仍算刚启动,时长显示不受影响)。诊断与修复都用注入法验证过:把 makeTask 的默认值改成「tb 晚 1 秒」以模拟 CI 条件,修复前这条用例失败、其余 19 条全过(与 CI 表现一致),修复后同一模拟下 20 条全过——说明对时钟的依赖被真正消除,不是碰巧通过。

vi.waitFor 保留:预览区确实要等一次 ink re-render 才出现,固定 20ms 在 CI 负载高时不够。它解决的是「等得够不够」,与排序不确定是两个独立问题,注释里已写清以免下次再误判。

* fix(first-run): 恢复被三重失效吞掉的 [4] 文档出口,并清理同源死文案

设计文档「首次运行引导设计」第 20-21 行要求 select 步骤有四个选项,第四项是「查看文档,稍后手动配置」,i18n 的 firstRun.optionDocs 中英文案齐备,但该出口在实现上被三处叠加吞掉,实际彻底不可达(用户想跳过配置只剩 Esc):渲染只 map PROVIDER_OPTIONS(3 项 provider),第四项从未画出;方向键取模用 PROVIDER_OPTIONS.length,光标最大到索引 2,画出来也选不中;数字键路由里 idx === 3 分支写在 const option = PROVIDER_OPTIONS[idx]! 与 if (!option) return 之后,PROVIDER_OPTIONS[3] 恒为 undefined,按 4 被守卫提前 return,分支是死代码。修法对应三处:追加文档出口渲染行、取模改用 SELECT_OPTION_COUNT(渠道数 + 1)、把出口判断提到取值守卫之前。

选项 label 原为硬编码中文字面量('StepFun Plan 订阅' 等),i18n 里配套的 firstRun.optionPlan/optionApi/optionCustom 三条从未被用——后果不是提示缺失,而是英文用户在向导里看到中文。改为在 ProviderOption 存 labelKey、渲染时 t():PROVIDER_OPTIONS 是模块级常量,若在其中直接调 t() 会在 import 时求值把语言固化,运行时 /lang 切换不生效。硬编码的配置文档链接提示一并接线为新增的 firstRun.docsNotice。

顺带清理普查出的 26 条零引用文案(zh/en 双表对称删除,tsc 保证键集一致):app.goal/permission/provider/lang/plugin.arg.* 与 app.skill.list/none 共 22 条系重构遗留,实际替代者是 commands.ts 的 cmd.<命令>.sub.<子命令>;agentGroup.status.running/queued 在状态改用彩点后失效(done/error 仍在用);firstRun.optionPaste 属旧流程遗留、optionManual 与 optionDocs 语义重复。app.image.unsupported 同为零引用但保留并加注释:全仓没有任何 supportsImage/vision/multimodal 能力判断,该文案是「贴图给非多模态模型静默无提示」这个缺口的唯一线索。

* feat(agent): 注入当前时间快照与跨天提醒——模型不再靠猜「今天几号」

此前 system prompt 里没有任何时间信息,模型只能拿训练截止日期当「现在」,后果是搜索时用错年份、写文档与 commit 时写错日期、判断「最新版本」基于过时认知,且它意识不到自己在猜。

为什么是「启动快照 + 诚实标注」而不是逐轮刷新真实时间:system 整块打 cache_control(provider/prepare.ts 的 buildSystemBlocks),逐轮改写会让 system 断点连同其后的 tools 与历史断点一起失效,等于每轮按全上下文重算 input token。所以这里放弃「保持准确」,改为明说它是启动快照、可能过时数小时、真要准确时间就去跑 date——换来 system 永久静态,精度反而可以给到分钟而不必压到天。

跨天是唯一不可容忍的过时(日期错一天会让写文档、判断时效系统性出错),用一条 injection 消息修正,复用 loop 既有注入通道(与 turnWarning、后台通知同形)。baseline 取「最后一条消息的本地日期」而非局部状态变量:runLoop 每个 prompt 回合重新调用,局部变量在回合边界就重置了,而跨天几乎总发生在回合之间——用局部变量等于永远检测不到。注入的提醒自身成为最后一条消息、ts 即今天,故天然只注入一次,无需 warned 标记;resume 后同样成立。

时间一律用本地时区并显式标注 UTC 偏移与 IANA 名。直接给 toISOString() 会错日期:实测 new Date(2026,7,9,7,30).toISOString() 在东八区得到 2026-08-08T23:30:00Z,每天 00:00 到 08:00 报给模型的日期都比本地早一天,而模型无法自行修正(它不知道用户在哪个时区)。crossedLocalMidnight 另加坏时间戳防御:Invalid Date 的日期键是 NaN-NaN-NaN,与今天必然不等,不拦掉会导致每轮都注入一次「日期已变更」。

三处注入点:主 agent(systemPrompt.ts,now 可注入以保证测试不随真实日期漂移)、子 agent(runner.ts,联网调查最依赖当前日期)、--print 纯净模式。25 个新用例:nowContext 20 个覆盖时区换算、半小时时区、跨午夜判定与坏 ts(时区靠运行时改 process.env.TZ 切换,其中 UTC 对照组兼作方法自验——本机是 UTC+8,若切换在 vitest 里不生效该组必然失败);loopDateChange 5 个覆盖注入接线本身,判定函数写对而接线接错的情形纯函数测不出来,并用「临时把注入条件短路」反向验证过——禁用后期望注入的 3 条失败、期望不注入的 2 条仍过。

* feat(team): bash 写守卫接线——worker 的越界写不再只靠合并时的事后 diff

bashWriteGuard 的三档判定纯函数与 93 个单测早已落地,但没有任何调用方:wrapWriteGuard 只拦 write_file/edit_file,bash 直接透传给 base。后果是 worker 一句重定向或 cp 就能写到工作间外,范围互斥只剩 team_merge 合并时的 diff 检查兜着——已经发生过踩穿。

接线点选在 wrapWriteGuard 而非原计划的 bash.ts execute():前者本就是 per-worker 写隔离的位置、已单测、已在 runner 里接到子 agent 的 hooks 上,改动只是加一个分支;接在 execute() 则要把 allowRoot 一路穿进 ToolContext,而这对非 team 场景没有意义。hook 层拦截还有个好处是能把守卫给出的 reason 直接回给模型,比工具层抛错更容易让它自己改对。

拦 bash 曾被担心会拦死 git(worker 要在工作间里提交),实测不成立:守卫只看命令行里的显式写入语法(重定向、cp/mv/rm/tee/sed -i/dd/truncate),git 子命令与 npm/npx 一律判为无写入迹象放行。接线前用 21 条 worker 典型命令量过误报面,唯一被误拦的是 ls -la > /dev/null——/dev/null 会被解析成绝对路径、天然落在 allowRoot 外,而这是最常见的丢弃输出写法,拦下去会卡死正常命令。故本 commit 一并加丢弃型设备白名单(/dev/null、/dev/stdout、/dev/stderr、/dev/tty、NUL、/dev/fd/N、/proc/self/fd/N),并留一条用例锁住「白名单不是所有 /dev/*」——dd of=/dev/sda 仍按越界拦。

测试:守卫层加 6 例(99 例)覆盖特殊设备与块设备对照;接线层加 5 例覆盖两端——git 提交/跑测试/丢弃输出必须放行,越界重定向/拷贝/删除与动态路径必须拦。两侧都做了反向验证:把 bash 分支短路后,期望拦截的 2 例失败、期望放行的 3 例仍过,确认用例测的是接线本身而不是恒真。

已知边界(未变):tee FILE < input 形态下取错 token 而漏判(tee FILE 与 cmd | tee -a FILE 能正确拦);>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法仍不覆盖。这些形态的越界写仍由 team_merge 的事后 diff 兜住。

* test(agent): 补跨天提醒在 resume 路径的保护——ts 被持久化重写会让判定永久失效

缺口:loopDateChange.test.ts 覆盖的是 runAgent 层接线,但它直接构造内存里的 StoredMessage,绕过了持久化。而 resume 恰恰是跨天最典型的场景(昨天的会话今天接着聊),这条链路多了落盘与加载两个环节。只要其中任一步把 ts 重写成写入时刻或加载时刻,跨天判定就永久失效——baseline 恒为今天,接线测试与纯函数单测都发现不了。

三个用例:save → load 后 ts 保真;用加载回来的昨日历史跑 runAgent 仍注入提醒;同一天内保存又恢复不注入(避免把 resume 本身误判成跨天)。

测试有效性用注入法验证过,不靠「跑绿了」当证据:在 SessionStore.save 里临时植入「把所有消息 ts 重写为当前时刻」的 bug,前两个用例立即失败(报 expected 2026-08-10T03:11 to be 2026-08-09T01:11,正是被重写的证据),第三个仍通过——它测的是反向行为,本来就抓不到该 bug,符合预期。移除注入后三条全过,store.ts 无残留改动。

时间一律用「26 小时前」构造:往前推 26 小时必然落在不同的本地日期(一天只有 24 小时),因此无论几点跑都成立,不需要等到跨过真实午夜才能验证。

* test(tools): readMedia 长图 probe 用例放宽 per-test timeout 到 60s

该用例单跑基线 11–15s(编码 508×30173 PNG + 20 次 region 全解码裁剪),全量并发下放大到 27.8s,撞全局 20s 阈值。2026-08-10 三轮全量测试里两轮挂在它上面。

不动全局 testTimeout:20s 是明确设计过的(vitest.config.ts 注释写明是给 16 核并发下 3 倍放大预留、防假阳性训练人忽略红灯),全局放宽会把其它测试的死锁假阴性窗口一起放大。只给这一条用例单独放宽到 60s,约 2.2 倍并发余量。

per-test timeout 对象形态经实验验证生效:构造 2s 睡眠 + 3s per-test timeout 的临时用例,全局 20s 下通过,说明 vitest 4 支持 it(name, {timeout}, fn) 三参签名。

* docs(readme): 前置非官方社区探索声明

* feat(memory): 记忆观察池——agent 自主沉淀的观察层,默认关闭、回顾确认后才生效

定位是观察池而非直接生效的记忆层:agent 用既有文件工具把观察写进两级 markdown 目录(全局 ~/.step-code/memory/ 与项目 .step-code/memory/,同主题项目层优先),system 尾部注入目录说明与索引并显式标注「未经确认,与规范冲突时以规范为准」;定期回顾经用户确认后才晋升进用户自维护的规范层。确认权不旁落是这个设计的核心约束。

存储为 markdown 正文 + HTML 注释藏 JSON 字段(version 单调递增、occurrences 记「第 N 次出现」、updated_at、source),人可读可手改、机器可解析;解析失败进 /memory 待修复列表而不阻塞。写侧刻意不做专用工具,复用 write_file / edit_file。

开关 [memory] enabled 默认 false:关闭时不注入、不建目录、已有文件原样保留。/memory 无参列出两层条目(含索引字符用量、坏文件、超阈值回顾提示),on/off 写回 config 并即时生效。中途开启(off→on)时经 injection 通道注入一条回看引导,agent 补沉淀本次会话的遗留观察——与跨天提醒同一机制。子 agent 只读注入(有索引无写入引导),并行写冲突与临时上下文噪声由此规避。

实现顺手修了两处既有问题:saveSectionKey 不支持 boolean(字符串 'true' 会带引号落盘,布尔解析恒 false——本功能第一个使用者就会踩);drift 保险要求的 doctor 顶层键清单与 update-config skill 内嵌表同步加 [memory] 段。21 个新用例覆盖解析/两层合并/索引预算/坏文件容错/只读变体/boolean 裸值回写。

* feat(reflect): 产出同步注入会话流——「记住第 N 条」的两阶段搭配不再断链

此前 /reflect 的清单只 pushItem 到界面、不进会话消息流,用户看到清单后说「记住第 2 条」时 agent 上下文里并没有这份清单,反思到沉淀的衔接是断的。现在非占位产出会以 injection 消息同步进 history(与跨天提醒、memory 开启引导同一通道,落盘、resume 后可见),注入文案说明来源并约束 agent 不主动写、等用户挑选。reflect 与 memory 保持松耦合:两者互不知晓,搭配发生在会话流里。

两个占位文案(空历史/未提炼出经验)提为导出常量,App 用 === 判断跳过注入;新增用例锁引用相等——若有人把返回改成同内容的新字面量,占位判断会静默失效,占位文本就会被写进会话流。

* docs: CHANGELOG 补第四批与 memory/reflect 条目;README 功能列表加 memory;interactive 的 /reflect 行补会话流注入(中英)

* feat(session): 第一轮回答后 AI 自动生成会话标题——语义化 /resume 列表

- title.ts:generateSessionTitle(复用主会话 provider,输入截断 300 字/条)+
 cleanGeneratedTitle(去 think 块/引号/截断 50 字)+ canOverwriteTitle 判定
- 覆盖纪律:rename 过(name 非空)永不覆盖;title 非派生结果不动;每会话只尝试一次
- store.updateTitle:直写文件不刷新 updatedAt,不把会话顶到列表最前(同 rename 模式)
- App:submit 收尾 persist 后 fire-and-forget 触发;写回前重新 load 再判一次防竞态

* fix(background): 后台任务防失控三件套——output.log 滚动截断 + stdin 立即 EOF + Windows 进程树终止

2026-08-10 事故:失控 python REPL 死循环 3 天写 287 GB output.log,C 盘归零
致 pagefile 无法扩展、V8 FatalOOM 闪退;任务标 killed 后孙进程逃逸又写 24 GB。

- output.log 磁盘滚动截断:超 maxOutputFileBytes(默认 32MB)保留尾部一半重写,
 单 chunk 超保留量时自身先截尾;截断标记行进文件,outputTruncated/outputTotalBytes 落 meta
- spawn stdio ['ignore','pipe','pipe']:stdin 立即 EOF。agent 本无 stdin 通道,
 pipe 永不关闭只会让读 stdin 的命令挂起或 REPL 死循环(前后台统一)
- terminateProcTree:Windows taskkill /T /F 杀整树,POSIX detached 建组 + kill(-pid);
 stop/超时/前台中断/前台超时四处 kill 路径统一改走

* feat(team): worktree 挂载主仓 node_modules 目录联接——worker 可在工作间自验测试

worktree 只 checkout 版本控制文件,node_modules 被 gitignore,worker 一跑 vitest/tsc
就因缺依赖卡到超时被杀,只能提交未验证代码(实际发生过两次)。用目录联接共享
主仓依赖而非复制(数百 MB 且 pnpm 链接农场不宜复制)。失败静默跳过不阻塞启动;
依赖共享的副作用(worker 装依赖会写主仓)靠任务提示约束。

* fix(session): session.model 统一存别名,providerModel 单独承载真实 id

问题:新建会话 / resume 后 session.model 被写成真实模型 id,
当真实 id 恰好是某个别名的 key 时(如 qwen-for-coding),
resolveStartupModelAlias 反查会命中 [models.qwen-for-coding]
而非用户实际选择的 [models.song],导致 currentModelAliasRef
指向错误渠道,状态栏丢失别名 displayName。

修复:
- cli.tsx 新建会话 / resume 回退时 session.model 存别名
 (config.modelAlias ?? config.model)
- cli.tsx resume 命中别名时 session.model 保持别名,
 新增 providerModel 变量承载真实 id 给 provider.stream
- cli.tsx 非交互模式 runAgent 与 App 传参改用 providerModel
- App.tsx /new 时 store.create 用 currentModelAliasRef.current ?? model
 (与 persist 口径一致)

更新 tests/cli/resumeModelAlias.test.ts 匹配新逻辑。

* feat(tui): 统一编辑器 TextEditField 替换 8 处输入框——一处修行为处处生效;ModelPicker 渠道 tab 视口跟随(选中项滚出可视区时自动跟随);补 unified-text-editor 设计文档(中英)

* feat(config): 图片输入上限按别名声明——[models.*] image_max_edge_px 覆盖全局保守值,read_media 降采样阈值与轻量刷新按当前别名解析

* feat(tui): 子 agent 工具调用嵌套可见性——spawn_agent 条目下实时回传子工具事件,运行中滚动窗口保留最近 3 条、完成折叠计数,Ctrl+O 看全过程;runner onEvent 回传链 + tool_end 事件;连带:events billedDelta 注释口径修正、runner 图片上限透传(配置本体在上一笔)

* fix(compaction): 压缩摘要压到最低思考档(按模型能力门控)+ billedTokens 计费口径修正——Anthropic input_tokens 本身已排除缓存命中,不再重复减 cache_read

* feat(headless): stream-json 协议 v3——新增 session.not_found 与 result 事件、-p 支持 --output-format json;stdin 防护与别名解析修复;连带:cli 接线图片上限别名配置(配置本体见前笔)

* revert(skill): 撤除 skill 激活次数上限——误抄嵌套深度语义成总次数,正常多 skill 协作被误拦;防循环由 maxIterations 与 token 预算兜底;保留「同一 skill 多次激活不受限」回归用例

* feat(tui): 聊天区排版第二轮——用户消息淡色背景块提升可扫描性;超宽 URL 独占行整体溢出不再字符级拆断;列表悬挂缩进修「续行顶格像突然换行」;连带第一批 OSC 8 超链接渲染

* fix(tui): 正文排版三修——折行超宽溢出字符顶格、列表项粗体星号裸露、段落软断行句中硬断

- 前缀行布局(● / › / 列表 marker)的内容盒 flexShrink 改显式定宽:
 Ink 在 flexShrink 下按父行整宽折行再套缩进,续行超宽被终端硬折行、
 溢出字符顶到第 0 列(实录:2025/中AI/Pexo 顶格碎片)
- renderInline/extractCellSegments 的 text 分支递归子 tokens:
 marked 给列表项的是块级 text 容器,直接输出原文导致 **粗体** 裸露
- 段落软断行软化:CJK 边界直连、拉丁边界补空格、域名不断
 (thepaper.\ncn 类模型断碎可复原),测量路径同步
- 回归契约:渲染行永不超终端宽(四档宽度)+ 列表悬挂缩进 + 星号不裸露

* fix(image): Alt+V 贴图在 OpenAI 系通道被静默丢弃 + Windows 读图多路径与自诊断

- 协议翻译层:messagesToOpenAi / messagesToResponsesInput 处理 user 消息时
 此前只拼接文本块(blocksToText),image 块被直接丢弃——走 openai/
 openai_responses 通道的渠道(Kimi Coding 等)贴图界面显示已附加,实际请求
 里没有图片,模型完全收不到,无任何报错。有图片时 content 升级为多模态
 parts(image_url / input_image data URI),无图片保持原 string 路径
- Windows 剪贴板读取:单次 PowerShell 多路径尝试——WinForms GetImage
 → PNG 格式 → FileDrop 图片文件;全部失败输出剪贴板格式清单,UI 三级
 诊断(缺工具 / 格式清单 / 通用无图),不再一句「没有图片」兜底
- 回归:user 图片 parts 形态、纯文本不回归、混合消息顺序、parseWinOutput
 解析契约

* fix(compaction): 手动压缩假成功提示 + 复述闸门密度误判

- 复述判定从「出现即拒」改密度口径(≥1 处/千字符才拦):高保真交接笔记
 零星引用 [image …] 等序列化标记被误判为照抄历史,导致压缩三连败后放弃
 (真实会话回放:9.4 万字符合格摘要被拒,随后两次重试退化为百字符短摘要)
- 复述被拒后追加反复述提示原样重试,不再 dropOldest(丢消息不治输出行为)
- 重试循环全程记日志:API 失败 status/原因、闸门拦截候选长度/原因、放弃总结
- UI:fullCompact 同引用返回(未压缩)时明确提示「未能压缩」,不再打印
 「已压缩:X → X」假成功

* fix(degrader): 识别「端点只收 text part」的 400 为媒体问题并自动降级

智谱 BigModel 实测(2026-08-13):含图历史切到只收纯文本 content 的端点
(messages.content.type 参数非法,取值范围 ['text'])每条请求都 400。
该报错等价于「不收图片」——我们发出的非 text part 只有图片。登记进媒体
方言表后沿降级链把图片换占位文本重试,会话可继续。

* docs: 标注 step-code-pi 为 pi-tui 实验仓(定位与边界声明)

* feat(tui-pi): M1 主循环——pi-tui 前端跑通输入/流式/审批/持久化

平行目录接线(--pi 开关),Ink 版原样保留可对照:
- Transcript/ItemBlock:DisplayItem → 行数组,块级渲染缓存
- StatusLine/ActivityLine:两行式状态栏 + spinner/思考预览
- ChatEditor:Esc/Ctrl+C 交回控制器,补全打开时让位编辑器
- PiChat:runAgent 消费、Esc 三态、Ctrl+C 三态、发送队列、persist 顺序不变量
- InlineApproval:y/a/n 最简审批(完整三桥在 M2)

实测推翻了设计文档的裁剪方案:任何裁剪都必然触发全屏重绘 + CSI 3J 清 scrollback,
而不裁剪的成本仅 3.47ms/帧(12000 行历史),故默认不裁剪,maxTurns 降级为安全阀。
测试:23 个渲染/交互用例 + 1 个进程级首帧冒烟;上游未导出 VirtualTerminal,改用自写 FakeTerminal

* feat(tui-pi): M2 审批三桥——工具审批/计划确认/向用户提问

ChoiceBlock 抽出三桥共用的交互(↑↓ 选择 + Enter + 直选键 + Esc + 内联反馈输入):
Ink 版三个组件各写一遍同构状态机,命令式重写下抽基类更自然。

- InlineApproval:四选项(y/a/n/f)+ bash 危险模式警告 + diff/写入预览 Ctrl+E 展开
- PlanApproval:exit_plan_mode 确认,计划正文走 markdown,拒绝可附修订意见
- QuestionPrompt:ask_user 多题逐题问,空格多选、←→ 切题、自由输入项、Esc 回空字典
- PiChat:plan 模式守卫 + exit_plan_mode 确认链路 + ctx.askUser 接线 + /plan 命令
- 弹层激活时暂停 spinner(用户在读弹层,动画只是噪声与无谓重绘)

弹层互斥由「overlayHost 同时只放一个块」保证,替掉 Ink 版 9 个 useInput 早退分支。
测试:18 个三桥交互用例,真实模型跑通 ask_user 全链路(提问弹出 → 选择 → 答案回灌)

* M3:pi-tui 侧三个选择器(模型 / 会话 / 思考深度)

`src/tui-pi/pickers.ts`
- `PickerOverlay`:SelectList + 标题行(含过滤串)+ 提示行的覆盖层组件。
 可打印字符进过滤串、退格删字、Enter 结算、Esc 取消,全部走
 `handleInput`,不用全局钩子(理由同 M1 的 Esc:语义依赖当前状态)。
- `modelItems`:按渠道分组排序,label 用 displayName,description 给
 「渠道 · 真实模型 id · 上下文窗口」,当前别名前面标 ●。
- `sessionItems`:展示口径 name ?? title,description 给相对时间 +
 消息数 + 短 id。`relativeTime` 按秒/分/小时/天分档,超 30 天给日期。
- `thinkItems`:high/medium/low/off + 跟随配置默认五档。

`src/tui-pi/PiChat.ts`
- `/model`、`/resume`、`/think` 路由到对应选择器;选中后分别调
 `applyModel` / `applySession` / `applyThink`。
- `session` 字段从 readonly 改为可变(恢复会话要换实例)。

`tests/tui-pi/pickers.test.ts`:候选项构造 + 按键序列结算,9 个用例。

实测(`dist/main.js --pi`,见提交说明外的实测记录):27 个模型别名
分组显示正确,输入 step35 过滤到 2 项,Enter 切换后状态栏与提示条
同步更新;/resume 列出 3 个历史会话;/think 五档当前项标记正确。

全量 2913 个用例绿,tsc --noEmit 干净。

* M4:命令注册表接线 + 19 条命令 + wire 事件落盘

分发改成走注册表,不再手写 switch 匹配命令名:
`parseSlash` / `busyRoute` / `helpText` 直接复用 `src/tui/commands.ts`
(纯逻辑、不 import react),两版共用同一张 SLASH_COMMANDS 表,命令集与
别名不会漂移。回合进行中的分流也由 `busyRoute` 决定:只读命令即时执行,
改动 turn 前提的排队到回合边界;双态命令(/model /think /resume)无参
只读即时、带参变更排队。

新实现的命令:/permission /yolo /auto /lang /mcp /usage /tasks /memory
/restore /compact /export-debug-zip /fork,以及重写的 /new(原来只清历史,
没换会话,也没换绑后台任务目录)。加上 M1-M3 的 /help /model /think
/resume /plan /clear /exit,pi 版现有 19 条命令。

wire 事件落盘(这是 M4 的前提,不是附带项):
`appendWire` + runAgent 的 `onWireEvent`,权限模式/plan/思考深度/压缩应用
四类状态变更各落一条。M4 之前 pi 版只写 appendFull 快照,事件日志是空的,
`/usage` 与 `/export-debug-zip` 拿不到任何数据。

队列排空改走 `drainQueue`:队列里可能混着排队的斜杠命令,原来无条件
`runTurn(next)` 会把命令当普通消息发给模型。

`src/tui-pi/commandText.ts`:`formatTaskList` / `formatMemoryList` /
`NOT_WIRED`。文本生成从控制器里拆出来是为了能直接断言,Ink 版这些输出与
setState 混在 case 分支里,只能起整棵 React 树才能验证。

未接线的 10 条(/goal /team /loop /history /reflect /agents /skill
/provider /reload /plugin)显式列在 NOT_WIRED,提示「pi 版尚未接线」而不是
落到「未知命令」——命令是存在的,提示要能区分打错了和这版还没有。

顺带修了 Ink 版两处误导文案(pi 版已改,主仓待回写):
1. 历史短于 keepRecent 时 fullCompact 原样返回,与摘要失败同一出口,
 Ink 版打「多次尝试均未产出可用摘要」,而实际一次请求都没发。
2. 摘要可能比被替换的原文更长(实测 1346 → 1535),Ink 版无条件打
 「已压缩:X → Y」,Y 比 X 大会被读成程序算错。

测试:`tests/tui-pi/commands.test.ts` 13 个用例(注册表分流语义 +
文本生成)。全量 2926 个用例绿,tsc --noEmit 干净。

实测(`dist/main.js --pi`,逐条发命令抓 stdout):
19 条命令逐条验证输出正确;一轮真实对话后 /usage 报出 18.7k input(证明
wire 事件真的落盘了);4 轮对话后 /compact 实跑 10s,摘要生成成功,历史
替换、context 用量从 19.2k 回落。

* M4b:输入补全接线(/命令 · 命令参数 · @文件)

`src/tui-pi/completion.ts`:把既有补全引擎适配到 pi-tui Editor 的
`AutocompleteProvider` 接口,候选仍由 `computeCompletions` 算,菜单渲染与
键位交给 Editor。

没用 pi-tui 自带的 `CombinedAutocompleteProvider`,两个原因:
1. 它自己扫盘做文件补全,而本项目的 @ 补全用 `scanFileIndex` 的索引
 (排除 node_modules/dist 与点目录、有条数上限)。换掉等于悄悄改了补全
 范围,可能把 node_modules 的路径提上来。
2. 命令名匹配的既有语义是「前缀优先 + 两字符查询才启用子序列回退」,
 有测试钉住;换成它的模糊匹配会改变命中集。

文件索引在 `start()` 里后台扫,不阻塞首帧;扫完前 @ 补全为空,扫描失败降级
为空索引,命令补全不受影响。

`tests/tui-pi/completion.test.ts` 9 个用例,重点在 applyCompletion 的文本
替换(写坏了表现为补全后输入框留…
li-xiu-qi added a commit to li-xiu-qi/Step-Realtime-CLI that referenced this pull request Sep 4, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末
 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时
 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重),
 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。
 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。
- 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充
 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker
 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行
 chrome 余量),小终端弹层不再越线。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli src/main.ts, src/cli.tsx, src/i18n.ts, src/version.ts, src/utils area/utils packages/utils

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reject malformed numeric CLI options

2 participants