Inbox、Steering 与 Continuation
运行中输入如何被排队、认领、注入或定向到子 Agent
结论:Inbox 是可回放的调度协议,不是聊天输入框的临时数组
DeepSeek Harness 用两个有序 Inbox 列表表达输入意图:next-turn 保存“以后各开一个 Turn”的工作,next-step 保存“在最近可用 Step 边界一起进入”的 steering 或 context。followup、steer 与 inject 只是对目标列表与 wake 行为的三种固定组合;真正决定消息何时可见的是 claim、pre-step admission、Turn 关闭检查与取消收敛。
其关键优点不是支持“运行中再发一句”,而是把待处理输入本身写进 Session Event Log:稳定 MessageId、插入、编辑、取消、认领、恢复和 UI 分类都能围绕同一事实工作。代价也很明确:已接受只表示 Inbox 已接收,不表示已执行、已产生回复,甚至不必然表示已经 fsync;running 表示整个 driver 活动,也不是某一条消息的完成句柄。
1. 先拆开六个容易混名的机制
| 名称 | 进入哪里 | 是否 wake | 边界语义 |
|---|---|---|---|
followup | next-turn | 是 | 普通后续输入;成功路径中每条各占一个后续 Turn |
steer | next-step | 是 | 尽量进入当前 Turn 的下一个 Step;空闲时会自己打开 Turn |
inject | next-step | 否 | 安静地放入模型上下文;空闲时一直停放到别的 wake 到来 |
| Tool continuation | 工具结果已在日志;additional context 进入 next-step | 由当前 driver 继续 | 同一 Turn 的后续 Step,不是新的 follow-up |
| Continuable child follow-up | 子 Agent 的 next-turn | 是 | 永远是子 Agent 的后续 FIFO Turn,不能 redirect 当前 Turn |
| Child report / settlement notice | 父 Agent 的 next-turn 或 next-step | 按策略 | 子→父消息;与父→子的 follow-up 完全不是同一方向 |
2. 队列类别不是 Message role:身份、来源和调度位置彼此正交
进入 Inbox 的对象仍是统一的不可变 UserMessage:有稳定 id、role: user、精确 content blocks 和必填 source。MessageSourceMap 可由插件扩展;freezeMessage 会 structured-clone 后深冻结,创建函数则分配新的 UUID。INBOX-MESSAGE-IDENTITY
| 维度 | 回答的问题 | 例子 |
|---|---|---|
| Message role | 怎样进入 provider-neutral conversation | user |
| Source | 谁产生它、UI 应怎样解释 | user、plugin notice、coordinator relay、subagent report |
| Inbox target | 在哪个调度边界被 claim | next-turn / next-step |
| Web placement | pending 状态显示在哪里 | queued / steering / context |
因此,“steering message”不是第三种消息结构。它是 user-source 消息先从 next-step 被认领这一历史事实;同一个 role 的非用户 source 则会被 UI 解释为 context。
3. 两个列表由同一种 durable splice 重建;提交先于 live mutation
agent/inbox/spliced {
target: next-turn | next-step,
start,
removedCount?,
inserted: full UserMessage[],
outcome?: canceled
}
插入、编辑、删除、claim 和 clear 都归一化成同一种 splice;事件携带完整插入消息,删除只有在用户/API/取消路径上才标 outcome: canceled,loop claim 的纯删除不标取消。INBOX-EVENT-SCHEMA
Inbox.mutate() 先验证坐标与跨两列表的 MessageId 唯一性,再调用 session.append,成功后才修改内存数组并发 live notification。append 抛错时,Inbox 保持原样;同步 Session observer 看到的是 splice 之前的列表,可用事件坐标恢复被移除对象。INBOX-COMMIT-IDENTITY
这也是为什么 Web projection 不需要自己维护一套命令队列:它读取 Agent 当前列表,再把正在发布的 splice 应用一次,就得到权威 post-state。
4. Claim 有严格优先级:全部 next-step,然后至多一个 next-turn
每个提议 Step 的 claim(target)
1. 删除并取得全部 next-step(按 FIFO)
2. 若 target = next-turn,再删除 next-turn 头部一条
3. 按上述顺序逐条发 claimed(message, turn)
Turn 第一个 Step:target = next-turn
同 Turn 后续 Step:target = next-step
构造器只回放 seedLength 之后的 Inbox splice;claim 总是把当前所有 next-step 组成一个 batch,并只附带一条普通队列消息。它不会中断已经发出的模型请求,只能在下一个 pre-step 边界生效。INBOX-CLAIM-REPLAY
这给出一个常被忽略的组合:Turn 的第一个请求可以同时包含早已停放的注入上下文、steering,以及该 Turn 自己的普通 prompt;顺序固定为 next-step 批次在前、next-turn 头部在后。
5. Follow-up 保证普通输入的 FIFO Turn 映射,但 status 不会逐 Turn 闪烁
followup(message) 等价于 send(message, next-turn, true)。空闲时 wake 会同步把 phase 预留为 running,再异步启动 driver;运行中只需排队,因为活着的 driver 在当前 Turn 关闭后会检查 hasPending,换新 AbortController 并继续下一 Turn。INBOX-SEND-MAP
同步 burst、逐条等待以及两者混合三种调度都验证:失败自由路径中 N 条 follow-up 产生严格递增的 N 个 Turn,每个 Turn 恰好一条普通消息且不重排;整个 burst 的状态轨迹仍只有 running → idle。INBOX-FIFO-PROPERTY
6. Steer 是“最近 Step + wake”,不是对正在飞行请求的流内追加
steer(message) 写入 next-step 并请求 wake。若 Agent 正在正常运行,wakeDriver() 不另开 driver:当前 driver 应在下一 Step 边界 claim 它。若 Agent 空闲,steer 自己打开一个 Turn;因此底层 API 并不要求 running,空闲 steer 最终仍会成为该新 Turn 的第一个输入,只是 replay 分类保留为 steering。INBOX-DRIVER-WAKE
这里的 steering 是 boundary steering,而不是 provider 正在生成 token 时修改同一 HTTP request。它能改变下一次模型调用看到的日志前缀,却不能撤回已生成的 chunk,也不会自动取消当前 tool call。
Web composer 只在 ordinary session 的 running 状态提供 steer 选择,减少“空闲 steer 其实新开 Turn”的意外;但 Host 的直接 session.prompt(mode: steer) 仍接受该底层语义。
7. Inject 是“最近 Step但不 wake”;是否同 Turn完全由落点决定
| 注入时刻 | 结果 |
|---|---|
| Agent 空闲 | 停放在 next-step,直到 follow-up/steer 或其他 waking delivery |
| 当前 pre-step 尚未 claim | 进入本次 batch |
| pre-step 已 claim、正在 assemble/listener/model/tool | 错过本次 request,进入下一 Step |
agent/turn-stopping listener 内 | 结束检查之后重读列表,因此可再开同 Turn Step |
| 取消后的 driver 正在收敛 | 没有 wake latch;停放等待未来 wake |
测试在第一个 pre-step listener 暂停后依次调用 inject 与 steer;两者都留在 next-step,第一请求完全看不到,第二请求按插入顺序看到。若当前 batch 被 reject,后来放入的 next-step 输入保留,但也需要新的 wake 才会运行。INBOX-PRESTEP-CUTOFF-TEST
8. Pre-step 先消费、后准入:reject 不会自动把原消息退回
turn/start
→ inbox.claim(target) // durable pure deletion
→ assemble system prompt
→ agent/pre-step waterfall
├─ enter(messages) → step/start → user/message* → model
└─ reject → turn/end(blocked),无 Step
loop 在 listener 之前已经从 Inbox 删除完整 batch。listener 可以保留、替换或清空 messages;reject 将 Turn 记为 blocked,原 batch 不会被标 canceled、不会形成 user/message、也不会回到队列。首次 enter 的空数组则产生 completed 的无 Step Turn。INBOX-TURN-MACHINE
如果 assemble 或 listener 抛错,Turn 以 error 关闭;同一时刻尚未 claim 的 next-turn 与后来到达的 next-step 仍在队列,但 driver 停止。这里选择的是“input claim 是提交点”,而不是“模型调用成功才消费”。
9. Tool continuation 与 turn-stopping 都复用 next-step,却拥有不同生产者
模型给出 tool calls 后,结果按模型顺序写入日志;每个 finalized result 的 additionalContexts 随后依次 splice 到 next-step。工具未声明 concludesTurn 时,loop 在同一 Turn 开下一个 Step;声明 concluded 也不会越过已经存在的 next-step 输入。INBOX-TOOL-CONTEXTINBOX-TOOL-CONTINUATION
当模型看起来可以结束且 next-step 暂空,loop await agent/turn-stopping,然后再次读取 next-step。listener 中的 steer 已由回归测试证明会强制第二个 Step;若 listener 注入的 batch 被 pre-step 重写为空,则不会多做模型调用。INBOX-TURN-STOPPING-TEST
10. running 属于 whole driver;maintenance 对外仍显示 idle
| 观察项 | 真实含义 | 不保证 |
|---|---|---|
status: running | driver 已预留,可能在 pre-step、模型、工具、Turn 关闭或连续 Turn 之间 | 某条消息正在执行;当前 Turn 一定仍接受 steer |
status: idle | 没有 turn driver;也可能正在运行 maintenance | Inbox 为空 |
whenIdle() | 等待当前活动及其在退休前替换出的活动稳定结束 | 某个 MessageId 的结果 |
runMaintenance() | 从真正 idle 独占非 Turn 活动;waking input 被 latch | 向模型暴露 maintenance 自身 |
公共类型明确把 running 定义为完整 drain 区间,把 maintenance 保持为外部 idle;whenIdle 通过比较 activity Promise 跟随替换工作,而不是绑定某次 send。INBOX-ACTIVITY-CONTRACT
11. Cancel 要分“信号”“待处理输入”“已认领输入”三层看
| 调用 | 当前活动 | 未 claim Inbox | 已 claim batch |
|---|---|---|---|
cancel(cause) | 若非 idle,abort | 先清 next-step、再清 next-turn,写 canceled splice;idle 时也会清安静停放的输入 | 不恢复 |
cancel(cause,{keepInbox:true}) | 若非 idle,abort | 保留,但 pre-abort 工作不会因此自动 wake | 不恢复 |
Web session.cancel | 调用上式 keepInbox | 保留普通队列与 steering | 不恢复 |
| Subagent interrupt | 调用上式 keepInbox,立即返回 | 保留,等待下一次 waking send | 不恢复 |
默认 cancel 无条件调用 Inbox.clear,并只在非 idle phase 上 abort;keepInbox 跳过 clear。测试证明 waking send 可能在调用 cancel 前已经被 claim,此时 keepInbox 没有东西可保留;默认 cancel 则会丢弃 mid-step queued tail 和 steering。INBOX-CANCEL-CODEINBOX-CANCEL-BEHAVIOR-TESTINBOX-CANCEL-DROP-TEST
12. Abort-to-idle wake latch 修复了取消收敛窗口,但仍保留一个更窄的退休缺口
活跃 Turn 已 abort,但尚未 idle
├─ 此前已排队 + keepInbox → 保留但停放(没有 latch)
├─ 此后新的 waking send → 强制 next-turn + wakeRequested=true
├─ 默认 cancel → 清 Inbox + 清 latch
└─ disposed → 永不 latch
旧 driver finally:idle → 若 latch 且仍有 pending → 新 driver
send 在插入前读取 abort 状态;post-abort 的 steer 也会改投 next-turn,避免加入一个已经注定结束的 Turn。收敛后的 replay 保证旧 turn/end 先于新 turn/start,移除被 latch 的消息会抑制空 replay。回归测试覆盖同 tick、慢收敛、maintenance、默认 cancel 与 disposed。INBOX-CANCEL-LATCH-TEST
13. Turn 记录本身不足以回答“这份工作去了哪里”
一个无 Step 的 Turn 可能是空 claim、listener 清空、pre-step reject、取消或错误;只看 turn/end 无法区分“什么都没做”和“消费了但没有进入模型”。foldConsumedWork 因而联合读取 step、纯删除 claim 与 canceled splice。INBOX-CONSUMED-WORK
| 历史形状 | 归账 |
|---|---|
| 进入过 Step | 该 Turn 计为 consumed |
| claim 后 blocked / aborted / error | 该 Turn 计为 consumed |
| claim 后 completed 且无 Step | 视为 listener 清空,不把该 Turn当作完成工作 |
| Inbox canceled 且无 replacement | droppedUnrun: true |
| 一次 splice 用新消息替换旧消息 | 仍 pending,不算 dropped |
专门测试覆盖 failed claim、stopped claim、blocked claim、empty rewrite、turn 外 claim、unrun drop、replacement 与后续 Turn 吸收旧 drop。INBOX-CONSUMED-WORK-TEST
14. “已写日志”“已发 Web 快照”“已落盘”是三个不同提交点
Host prompt accepted
→ Inbox splice 已同步 append 到 live Session
→ API 可返回 accepted
→ JSONL write-behind batch(默认固定窗口)
首个 pre-step checkpoint
→ flush enqueue + turn/start + claim
模型 adapter dispatch 前
→ flush step/start + user/message + request prefix
checkpoint policy 在 pre-step 与 llm/stream 边界 flush;后者延迟构造下游 stream,flush 失败就不调用模型。顶层 tool dispatch 也采用同样策略。INBOX-CHECKPOINT-POLICY
E2E 子进程在模型 dispatch 时被硬杀,恢复日志仍包含 insertion、turn/start、claim、step、user message 与 request prefix,并合成 interrupted closers;本次交付也重新运行该文件的 2 个用例并全部通过。INBOX-CRASH-RECOVERY
JSONL backend 把 live events 固定窗口合批,flush/teardown 会立即 drain;每批 append 与 fsync,失败回滚到原字节长度。由此可知 prompt 的同步 accepted 是 Session-level admission,不是独立的 fsync receipt;如果产品需要“回复前必须断电安全”,还需要显式等待 checkpoint。INBOX-JSONL-DURABILITY
15. Resume 会重建 pending,但不会仅因重建而自动开跑;fork seed 也不会复制祖先待办
恢复路径先从 persistence prepare Session,再走与新建相同的 prepare/setup/publish;新的 ReactLoopAgent 构造器从日志恢复最后 Turn 编号和 Inbox。INBOX-AGENT-RESUME
Inbox 只回放 session.events.slice(seedLength)。fork seed 中来自父 Session 的历史 splice 是孩子的上下文历史,不会重新变成孩子的 pending 工作;孩子自己在 seed 之后接受但未 claim 的消息则会恢复。INBOX-SEED-BOUNDARY
构造与 publish 本身不调用 wake。普通 Host 冷恢复通常紧接着处理一条 waking prompt;continuable child 冷恢复也紧接着调用 followup,所以旧 pending 会按两队列 claim 规则与新 wake 一起继续。单独恢复一个 Agent 并不承诺自动消费停放输入。
16. Ordinary Host API 返回 admission,不返回 Turn 或回复
session.prompt 接受 mode: queue | steer,处理时区、附件耐久化和 message source 后,分别调用 agent.followup 或 agent.steer;同步入队成功即返回 {accepted:true}。它没有等待 pre-step、模型结果或 idle。INBOX-HOST-PROMPT
公开 contract 将 queue/steer 直接映射到两种 Agent 操作;updateQueue 支持 edit/remove/steer,而 session.cancel 明确保留 pending work。INBOX-HOST-CONTRACT
Host 会为 cold ordinary session 先解析/恢复 Agent;但直接 mode: steer 不额外检查 running,因此它仍服从底层的“空闲时由 steer 打开一个 Turn”。这与 UI 的保守 submission policy 是不同层的约束。
17. Queue edit/remove/strict-steer 以 MessageId 定址;claim 赢掉竞态
| 动作 | 允许条件 | 实现 |
|---|---|---|
| Edit | 仍 pending;仅 text blocks | 深冻结 replacement,保留同一 MessageId |
| Remove | 仍 pending | canceled splice 删除 |
| Strict steer | 项目仍在 next-turn 且 Agent 当前 running | 先 remove,再用同一消息调用 steer |
Host 在两个列表中重新定位消息;若 loop 已先 claim,操作返回 queue-item-not-found。strict steer 的 running 检查比自由 mode: steer 更严格,但它不是绑定某个 Turn 的 capability token,仍受最终退休窄窗口约束。Subagent-owned Session 禁止这些 ordinary queue 操作。INBOX-HOST-QUEUE-MUTATION
18. Web 使用完整快照做 live mirror,再用 durable splice 恢复历史身份
Session splice(observer 看到 pre-state)
→ Host 对当前列表应用本次 splice
→ session/queue { complete items[] }
next-turn → queued
next-step + user → steering
next-step + non-user → context
Host 显式投影两个列表,并用 source.kind 将 next-step 分成 steering/context;事件 observer 把当前发出的 splice 应用到 pre-state,避免等待 live Inbox mutation。INBOX-HOST-PROJECTION
mux 先发 session/subscribed,仅在有 pending 时再发 queue baseline。客户端在 subscribed 上清空旧 generation,所以“没有后续 queue frame”正确表示空队列,不会把断线前的项目留成幽灵。INBOX-RECONNECT-HOSTINBOX-RECONNECT-CLIENT
Queue mirror 以完整 snapshot last-wins;pending steering 一旦出现相同 MessageId 的 durable user/message 就从瞬态镜像退休。历史 replay 则读取 next-step splice 的非 canceled removal,再检查 user source,把同一个 user/message 重建为 steering。INBOX-CLIENT-MIRRORINBOX-HISTORY-CLASSIFICATION
19. UI 把 queued 与 steering 放在两个位置,并把“快捷发送”定义成策略而非真相
QueueDock 只显示 placement=queued,提供编辑、删除和逐条 steer;steer 按钮只在 running 时启用。pending steering 则以用户气泡样式显示在 conversation tail,等 durable message 进入历史后完成 handoff。INBOX-QUEUE-DOCKINBOX-PENDING-STEERING-UI
非 running 或不支持 steering 的 transport 一律 queue;busy ordinary session 依据保存的 Queue/Steer 偏好选择,Cmd/Ctrl+Enter 取相反模式。空 draft 的加速 Enter 可把全部 queued rows 逐条 strict-steer;held Enter 被抑制,避免连续发送。INBOX-SUBMISSION-POLICYINBOX-INPUT-GESTURES
20. Continuable child 的 Activation 是 residency epoch,不是一次请求或一个结果
startContinuable 建立 durable child;后续 followup 隐藏 resident 与 cold-resume 差异,并把每条输入交给孩子自己的唯一 Inbox。成功边界是孩子接受 message id,不等待 Turn 开始,也不返回孩子回复。INBOX-CONTINUATION-SEAM
| Activation state | 推导条件 |
|---|---|
| running | Child Agent running,或 manager 已登记 waking message 但同步 send 尚未完成 |
| waiting | Child Agent quiescent,但仍拥有尚未释放的后代 Activation |
| settled | Agent quiescent且没有 owned children;可以 dispose handle |
一个 Activation 可以执行许多 FIFO Turn,也会因后代仍运行而在自身 idle 后继续驻留;manager 只拥有 residency/ownership,Turn 顺序仍完全由 Agent loop 与 Inbox 所有。INBOX-ACTIVATION-MODELINBOX-ACTIVATION-STATE
21. 父→子 delivery 永远是 follow-up;锁把 delivery 与 disposal 线性化
对 resident child,manager 在 child-id 锁内提交消息;若 disposal 已开始,就等待旧 Activation 释放后 cold-resume。对 absent child,它检查持久化 Session、先按 durable parent lineage 授权、只折叠 seed 之后的 continuable descriptor,再重建 Activation。INBOX-CONTINUATION-DELIVERYINBOX-COLD-RESUME
最终 admission 是无 await 的同步段:检查 caller signal、manager drain、Activation disposal 和 direct-parent authority;先把 MessageId 放进 accepted set,再调用 child Agent.followup,成功后标记 announced。每条父→子消息因此只进入 next-turn,不会 steer 孩子当前 Turn。INBOX-CONTINUATION-ADMISSION
interrupt 只在 live Activation 上授权并调用 cancel(...,{keepInbox:true}),立即返回;pending 与后代保留,claimed work 不恢复,absent target 是 accepted no-op。下一条 waking follow-up 才重新启动停放队列。INBOX-CHILD-INTERRUPT
22. 子→父有两种独立消息:主动 report 与自动 settlement account
| 消息 | 来源 | 父调度 | 是否结束 child |
|---|---|---|---|
| Explicit report | subagent-report relay | wakeup → followup;quiet → inject | 否 |
| Settlement notice | subagent-settled notice | parent closing → inject;idle → followup;busy → steer | 是 manager 对已结束 residency 的说明,不是 child 自述 |
只有 exact resident child Agent 可以 report,收件人从 durable direct parent 推导而不能由 caller 任意指定;wakeup 与 quiet 是父调度策略,报告本身不改变孩子 Turn 或 Activation。INBOX-CHILD-REPORT
manager 在 child handle 已释放、但父 ownership 尚未释放时投递 settlement,避免父 watcher 先把自己销毁。busy 父使用 steer,使同时完成的多个孩子在同一 next-step batch 合并;通知失败只记录并丢弃,最终 flush 也是 best-effort。INBOX-SETTLEMENT-NOTICE
23. Child 的 Web 与模型控制面有意比 ordinary Session 更窄
| 表面 | Follow-up | Steer | Stop |
|---|---|---|---|
| Web continuable child | 要求 live direct parent;只接 text;返回 MessageId | 无,客户端忽略 mode 并走 subagent.prompt | 可在 parent offline 时按 durable address interrupt |
| Web one-shot child | 只读 | 无 | 无 |
模型 send_message | exact live direct parent 发给 child 的下一 FIFO Turn | 无 | interrupt_agent 可由 live ancestor 中断 descendant |
Wire contract 明确把 prompt receipt 定义为 Inbox MessageId,把 interrupt receipt 定义为信号被接受而非目标 quiescent;Host 的 child prompt 要求 live parent,interrupt 则刻意不查询 parent、catalog 或 persistence。INBOX-SUBAGENT-WIREINBOX-SUBAGENT-HOST
客户端把 one-shot 设为只读;continuable child 即使 running 也保留 Send 并单列 Stop,使新消息排为后续 Turn。parent offline 时输入禁用但仍允许停止 live child;模型工具同样说明 send_message 不返回回复、不能 redirect 当前工作。INBOX-CHILD-CLIENTINBOX-CHILD-COMPOSERINBOX-CHILD-OFFLINE-UIINBOX-CONTROL-TOOLS
24. 固定基线存在两处可复现的文档漂移
| 相邻文档/注释 | 写了什么 | 当前生产代码 |
|---|---|---|
api/events.ts | pending message 到 claim 才 durable;pending work 没有 durable Session event | agent/inbox/spliced 在 live mutation 前写入,插入事件携带完整消息 |
| ApiProxy README | pending next-step steering 不进入 Web projection | queueItems() 投影两个列表;user-origin next-step 明确标 steering,ChatView 显示 pending bubble |
Wire 注释与当前 durable event schema/Inbox 实现直接矛盾;README 与当前 Host projection/UI 直接矛盾。这些判断来自同一固定 commit 的源码对照,不是对运行时的猜测。INBOX-WIRE-DOC-DRIFTINBOX-README-DRIFT
25. 保证矩阵与设计评价
| 命题 | 保证 | 不保证 |
|---|---|---|
| 普通 follow-up | 单一 Inbox 中 FIFO;成功 drain 时每条一个 Turn | 每条独立结果 Promise;遇到 reject/error 后自动继续 |
| Steer | 最近尚未越过的 Step boundary;空闲时可 wake | 修改飞行中的 request;绝对无退休竞态 |
| Inject | 下一次 claim 时作为 model-facing context | 自己 wake;一定属于当前 Turn |
| Cancel keepInbox | 保留尚未 claim 的输入 | 恢复已 claim 工作;自动 wake pre-abort 队列 |
| Prompt acceptance | 同步 Inbox/Session admission 与 MessageId(child) | 模型已调用、回复已产生、物理 fsync 已完成 |
| Continuable child | 跨 residency cold resume;父→子永远下一 FIFO Turn | send_message 同步返回 child result;current-turn steer |
最强的设计点是没有为 UI、插件与 subagent 各造一套待办队列:两列表、一个 splice 词汇、稳定身份和同一 claim 算法贯穿内核、恢复和产品面。最昂贵的复杂度则集中在“接受不等于执行”的边界:pre-step 先 claim、cancel 不回滚、driver status 跨 Turn、Web snapshot 与物理持久化又各有自己的提交点。系统已经用 consumed-work fold、checkpoint 和 wake latch补齐大部分可观测性,但公开文档仍需纠正,若要更强的 no-stranding 或 crash-safe acknowledgement,还需新的显式协议而不是更乐观的文案。
本章除逐分支追踪固定 commit 的生产源码、wire contract、README 和测试外,还实际运行了 13 个 Agent/Inbox/Host/client/subagent 定向 Vitest 文件(379 tests passed),并单独以 E2E 配置运行 hard-crash checkpoint 文件(2 tests passed):合计 14 files、381 tests 全部通过。
我的学习体会
内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。