涵盖 QueryGuard 状态机、四个核心函数的职责划分、isActive 语义、dispatching 存在的必要性
一、QueryGuard 状态机
1.1 状态定义
arduino
idle → 空闲,无查询执行
dispatching → 已预约槽位,异步处理输入中(图片 resize、附件提取、bash/slash 命令执行)
running → 查询正在执行中(API 调用 + 工具循环)
1.2 状态转换全集
scss
idle ────────────────── tryStart() ─────────────────► running (直接提交,无队列)
idle ────────────────── reserve() ──────────────────► dispatching (经队列处理器)
dispatching ─────────── tryStart() ─────────────────► running (输入处理完,确认要调 API)
dispatching ─────────── cancelReservation() ────────► idle (本地命令,不需要调 API)
dispatching ─────────── forceEnd() ─────────────────► idle (用户取消)
running ─────────────── end(gen) ───────────────────► idle (查询正常完成)
running ─────────────── forceEnd() ─────────────────► idle (用户取消,递增加法)
running ─────────────── tryStart() ─────────────────► null (并发,拒绝)
1.3 各场景的时序
场景一:正常提交------队列空闲时用户按回车
scss
onSubmit → handlePromptSubmit → executeUserInput
reserve() idle → dispatching
processUserInput() [await 图片resize、附件...]
└─ 此间隙 isActive=true,并发提交入队
onQuery()
tryStart() dispatching → running
onQueryImpl()
for await...of query() [await 流式响应]
end(gen) running → idle
finally → cancelReservation() 空操作(已是 idle)
场景二:本地斜杠命令(/model、/theme)
scss
onSubmit → executeUserInput
reserve() idle → dispatching
processSlashCommand()
[await] 模型列表、UI 渲染...
返回 shouldQuery=false, 无消息
cancelReservation() dispatching → idle
setToolJSX / clearToolJSX
场景三:用户取消
scss
onCancel()
forceEnd() running → idle
++generation ← 废弃当前查询
resetLoadingState()
── 被取消的 onQuery finally ──
end(staleGeneration) ← 返回 false(代数不匹配),跳过清理
场景四:并发提交
scss
用户第一次提交 → reserve() → dispatching
↓
用户第二次提交 → isActive=true → enqueue(),返回
↓
第一次提交 → tryStart() → running → end() → idle
↓
useQueueProcessor → isActive=false → dequeue → 第二次提交执行
二、四个核心函数的职责划分
2.1 handlePromptSubmit --- 编排器
文件 : utils/handlePromptSubmit.ts
makefile
输入: 用户原始字符串 + 元数据(mode、pastedContents...)
输出: 无(副作用)
本质: 编排层,不做消息转换也不做 API 调用
职责顺序:
scss
1. 路径选择:
├─ queuedCommands 已存在 → 直接 executeUserInput
└─ 直接输入:
a. 过滤已删除引用的图片
b. 检查空输入 / exit / quit
c. 展开 [Pasted text #N]
d. 检查 immediate 命令 → 执行 JSX
e. 检查 isActive → 入队
f. 构造 QueuedCommand → executeUserInput
2. executeUserInput(内部函数):
a. queryGuard.reserve()
b. AsyncLocalStorage 上下文(workload 传播)
c. forEach command → processUserInput()
d. 文件历史快照
e. 有消息 → onQuery();无消息 → cancelReservation()
f. 处理 nextInput 链式调用
3. finally:cancelReservation() + 清除 placeholder
2.2 processUserInput --- 转换器
文件 : utils/processUserInput/processUserInput.ts
makefile
输入: 已展开的字符串 + 上下文
输出: { messages: Message[], shouldQuery, allowedTools, ... }
本质: 用户输入 → Message[] 的转换
职责顺序:
scss
1. 显示 placeholder
2. processUserInputBase:
a. 图片处理(content block 的图片→resize,粘贴的图片→并行 resize)
b. bridgeOrigin 安全命令检查
c. Ultraplan 关键词检测
d. 附件提取(IDE 选择、todos、diff)
e. 模式分发:
├─ bash → processBashCommand()
├─ /xxx → processSlashCommand()
└─ 纯文本 → processTextPrompt()
f. 返回消息数组 + 控制参数
3. UserPromptSubmit hooks:
for await...of executeUserPromptSubmitHooks()
├─ blockingError → 阻止查询
├─ preventContinuation → 阻止查询
├─ additionalContexts → 追加附件
└─ hook_success → 追加结果消息
2.3 onQuery --- 控制器
文件 : REPL.tsx:2855
makefile
输入: Message[](已转换好的消息)
输出: 无(副作用)
本质: 并发安全外壳 + 生命周期管理
职责:
scss
1. queryGuard.tryStart()
├─ 成功 → 继续
└─ 失败(并发)→ 入队,返回
2. try:
a. 重置计时器、追加消息、清空流式状态
b. mrOnBeforeQuery() → onBeforeQuery()
c. onQueryImpl()
3. finally(代数匹配时):
a. resetLoadingState()
b. mrOnTurnComplete()
c. 通知 bridge 客户端
d. 捕获 API 指标 → turn 耗时消息
e. 自动恢复(取消后无有意义响应时回滚)
2.4 query --- 引擎
文件 : query.ts:219(async generator)
makefile
输入: 完整消息数组 + 系统提示 + 上下文
输出: AsyncGenerator<StreamEvent | Message | ..., Terminal>
本质: API 调用 + 工具执行循环
职责 (queryLoop 的每次迭代):
scss
1. yield stream_request_start
2. 上下文压缩链:
a. applyToolResultBudget()
b. snipCompactIfNeeded()
c. microcompact()
d. contextCollapse 投影
e. autoCompact()
3. API 调用:
a. fetchStream() → SSE
b. yield 每个事件
4. 工具循环:
a. collectToolResults()
b. runTools() → 执行工具
c. continue → 回到步骤 1
d. terminal → return
5. 反采样 hooks
6. 继续判定 → return Terminal
2.5 对比总结
| 维度 | handlePromptSubmit |
processUserInput |
onQuery |
query |
|---|---|---|---|---|
| 本质 | 编排器 | 转换器 | 控制器 | 引擎 |
| 是否操作 DOM/UI | 是(清输入框、弹通知) | 是(placeholder) | 是(setMessages、spinner) | 否 |
| 是否调 API | 否 | 否(hook 除外) | 否 | 是 |
| 有无 await | 有(processUserInput) | 有(resize、bash) | 有(onQueryImpl) | 有(SSE、工具) |
| 异常处理 | finally → cancelReservation | 错误阻断返回 | generation 隔离 + finally | error → retry / return |
| 产出 | 副作用 | Message[] |
void(副作用) |
StreamEvent[] |
三、isActive 语义详解
3.1 定义
ts
get isActive(): boolean {
return this._status !== 'idle'
}
3.2 真值表
_status |
isActive |
含义 |
|---|---|---|
idle |
false | 可安全出队执行新查询 |
dispatching |
true | 正在处理输入(异步),不要并发 |
running |
true | 正在执行查询,不要并发 |
3.3 所有读取点
| 位置 | 判断目的 | 期望值 |
|---|---|---|
handlePromptSubmit.ts:251 |
并发守卫:已有查询时入队 | true→入队 |
handlePromptSubmit.ts:313 |
同上(下游路径) | true→入队 |
REPL.tsx:904 |
useSyncExternalStore 订阅驱动 UI |
--- |
REPL.tsx:3010 |
取消后自动恢复 | 必须是 false |
REPL.tsx:3184 |
是否将命令视为 immediate |
true 才允许 |
REPL.tsx:3999 |
回调守卫 | true→跳过 |
REPL.tsx:4868,4873 |
Ultraplan 延迟写入 | false→写入 |
useQueueProcessor.ts:49 |
队列等待 | false→出队 |
3.4 在 UI 层的体现
REPL.tsx:904 通过 useSyncExternalStore 订阅:
tsx
const isQueryActive = React.useSyncExternalStore(
queryGuard.subscribe,
queryGuard.getSnapshot,
);
const isLoading = isQueryActive || isExternalLoading;
isLoading 直接驱动 <SpinnerWithVerb /> 的显示/隐藏。
四、为什么需要 dispatching 状态?
4.1 核心矛盾
processUserInput 内部有 await 调用 (图片 resize、bash 执行、附件提取),且执行完后不确定是否真的需要调 API (本地命令 /model 就不需要)。
4.2 只有 idle + running 的替代方案及其缺陷
方案 A:不保留,processUserInput 之后才 tryStart()
vbnet
executeUserInput:
processUserInput() // [await] 没有任何并发保护!
└─ 图片resize期间用户又提交一次
→ 第二个 processUserInput 并发执行!
缺陷 : processUserInput 内的所有 await 都是裸奔的,并发提交会导致消息乱序或状态错乱。
方案 B:processUserInput 之前就 tryStart()
csharp
executeUserInput:
tryStart() // idle → running, gen++
processUserInput() // 可能返回 shouldQuery=false
发现是 /model
end() // running → idle
缺陷 1 --- 语义混淆 :running 意味着"执行查询中",/model 只是本地 UI 从未调 API,两者不应共享同一状态。
缺陷 2 --- 代数污染 :tryStart() 递增 _generation,end(gen) 依赖代数匹配做取消安全:
scss
gen=1 正常查询 A → tryStart() → running → [处理中]
gen=2 /model → tryStart() → running → end(2) → idle
gen=3 正常查询 B → tryStart() → running → [用户取消]
gen=4 用户重新提交 C → tryStart() → running
查询 B 的 stale finally → end(3) → 代数不匹配 → 安全跳过 ✓
每次 /model /theme 都跑一遍 tryStart→end,代数被快速消耗,竞态保护的信号噪声比下降。极端情况:一小时内运行 50 次本地命令 → gen 跳到 52,但实际真正需要保护的查询只有几次。
缺陷 3 --- forceEnd 副作用 :forceEnd() 必须 ++generation。如果本地命令走了 end() 正常释放路径还好,但如果有取消操作必须 forceEnd,每次都会污染代数。
4.3 dispatching 方案的优势
scss
reserve() tryStart()
idle ──────────────► dispatching ──────────────► running ──end()──► idle
▲ │ ↑
│ cancelReservation │ 语义: "在输入处理中" │
│ ◄─────────────────┘ │
│ 不涉及代数 │
│ 没有副作用 │
│ │
│ forceEnd() ──── 涉及代数递增 ────────────────┘
| 特性 | reserve() / cancelReservation() |
tryStart() / end() |
|---|---|---|
| 涉及 generation | 否 | 是 |
可以被 forceEnd() 清除 |
是 | 是 |
| 清除后是否需要代数匹配 | 否 | 是 |
| 语义 | "我在处理输入" | "我在执行查询" |
| 典型路径 | /model、/theme、/compact | 正常对话、tool 循环 |
dispatching 是一个轻量级预约状态:
- 只负责在异步输入处理期间挡住并发
- 不需要代数保护
- 可以无副作用地通过
cancelReservation()释放 - 使用成本极低
running 是重量级执行状态:
- 需要代数机制处理取消+重新提交的竞态
end()通过代数匹配确保只有当前查询能执行清理forceEnd()通过递增代数"废弃"旧查询的 pending finally 块
两者职责不同,分开设计的核心原因就是:"我在处理输入"和"我在执行查询"是两件完全不同的事,不应该共享同一个状态。