refactor(token-meter): make context occupancy durable projection state

Replace the transient `session/model-request` mux frame with ordinary durable
session state. Occupancy now rides two last-wins projection fields instead of a
non-replayable frame that needed removal tombstones and cross-stream fencing.

The frame was the only non-replayable class on the mux stream. Because host and
mux are independent SSE streams with no cross-stream order, a request emitted
before a removal could arrive after `host/session-removed`, and a legitimate
request for a new lifecycle reusing the same id could be fenced by a late
removal. Fixing that needed a lifecycle generation on every frame; the frame
itself was the problem.

Removed: the `session/model-request` frame and schema, the `agent/model-request`
core event, the ApiProxy measurement point, the client-side telemetry map and
removal tombstone, and the synthetic `cancelled` open error used to signal
reconnect through the error channel.

Added: `request/context`, a log-only session event recording the
registration-bound capacity of the route a request resolved to, appended beside
`request/header` from the lookup that already prepared the call and skipped when
the route is unchanged. Capacity stays out of `EpochHeader` because it is
adapter metadata about a route, not an input the request was built from, so it
must not join request reconstruction or header equality.

The `contextPressure` projection pairs the newest provider-reported prompt size
with the newest recorded capacity. The two are deliberately not one atomic
request observation: switching models can pair a fresh capacity with the prior
route's pressure until the next request reports usage. The figure is a
user-facing reference, and this matches how the TUI status line has always
computed occupancy.
This commit is contained in:
Hypatia May
2026-07-30 13:53:08 +08:00
parent fdccc58cef
commit 4819210142
62 changed files with 382 additions and 1362 deletions

View File

@@ -22,10 +22,6 @@ Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.cr
`host.openPath` 会用操作系统的默认应用打开一个文件系统路径(macOS 为 `open`,Windows 为 `Invoke-Item`,Linux 为 `xdg-open`)。打开器可在测试中注入。浏览器载体对其施加与 `host.pickDirectory` 相同的回环、同源限制。
`session.history` 按消息边界分页。其尾页(不带 `beforeSeq`)只携带已注册单元的通用 `projections` 基线;较早页面则省略该基线。当 token-meter 与投影注册表组合时,完整日志中的提供方计费用量会通过普通 `tokenUsage` 键承载。系统按 `(turn, step)` 对其用量分片和最终消息去重,缓存读取与写入则仍是相互独立的计数项。ApiProxy 不拥有任何 token 专用的历史字段、mux 帧、投影器、修订计数器或刷新队列。
请求上下文使用独立的临时 `session/model-request` mux 帧。外层流调用为一次已观测的请求尝试返回句柄后,系统会根据 Agent 通知发出该帧,并收容通知失败。这个边界不能证明提供方 I/O 已开始。在同一同步事件边界内,ApiProxy 会可选地读取一次 `tokenMeter.measure(session).totalTokens`,并将结果与该次准备完成调用的容量合并。这个原子帧携带轮次、步骤、最终提供方/模型与可选的 `contextTokens`/`contextWindow`,且只发送给当时已经打开的 mux 连接。测量失败时只省略分子。`session.history`、mux 订阅基线、重连和会话恢复绝不会查询或回放更早的快照;后续帧中缺失的字段会清除对应的先前值,而不是继承它。
`command.*` 与 `skill.*` 领域向客户端暴露宿主命令注册表和技能目录。每个方法都通过 `sessionId` 寻址一个会话的 Agent(被服务的会话必有 Agent;`command.*` 经由与 `session.*` 相同的路径恢复冷会话,而 `skill.list` 从会话头解析项目根目录,不触碰 Agent 注册表)。`command.execute` 在宿主侧运行一条斜杠命令行,语义为纯准入:响应报告该行是否解析到处理器,并在解析到时回带铸造的生命周期 `commandId`(将本次确认与流节点关联);结局经由持久落账并在 mux 流广播的 `command/run`/`command/done` 生命周期事件对承载;载体的请求信号可取消正在运行的处理器。`host/commands-changed` 是目录失效帧:客户端重新拉取 `command.list` 而不是做差分。
## 载体层(`/client` + 根路径)