evolclaw 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -4
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -31
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1152 -140
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +58 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/outbox.js +14 -2
  11. package/dist/aun/storage/download.js +1 -1
  12. package/dist/aun/storage/upload.js +13 -1
  13. package/dist/channels/aun.js +869 -358
  14. package/dist/channels/dingtalk.js +77 -140
  15. package/dist/channels/feishu.js +125 -154
  16. package/dist/channels/qqbot.js +75 -138
  17. package/dist/channels/wechat.js +75 -136
  18. package/dist/channels/wecom.js +75 -138
  19. package/dist/cli/agent-command.js +591 -0
  20. package/dist/cli/agent.js +23 -8
  21. package/dist/cli/aun-commands.js +1444 -0
  22. package/dist/cli/ctl-command.js +78 -0
  23. package/dist/cli/daemon-commands.js +2707 -0
  24. package/dist/cli/index.js +23 -4905
  25. package/dist/cli/init.js +33 -6
  26. package/dist/cli/model.js +1 -1
  27. package/dist/cli/restart-monitor.js +539 -0
  28. package/dist/cli/stats.js +558 -0
  29. package/dist/cli/version.js +87 -0
  30. package/dist/cli/watch-logs.js +33 -0
  31. package/dist/cli/watch-msg.js +5 -2
  32. package/dist/config-store.js +12 -6
  33. package/dist/core/channel-loader.js +88 -83
  34. package/dist/core/command/command-handler.js +1189 -0
  35. package/dist/core/command/menu-handler.js +1478 -0
  36. package/dist/core/command/slash-gate.js +142 -0
  37. package/dist/core/command/slash-handler.js +2090 -0
  38. package/dist/core/evolagent-registry.js +82 -0
  39. package/dist/core/evolagent.js +17 -1
  40. package/dist/core/interaction-router.js +8 -0
  41. package/dist/core/message/command-handler-agent-control.js +63 -1
  42. package/dist/core/message/im-renderer.js +91 -51
  43. package/dist/core/message/items-formatter.js +9 -1
  44. package/dist/core/message/message-bridge.js +73 -24
  45. package/dist/core/message/message-log.js +1 -0
  46. package/dist/core/message/message-processor.js +432 -94
  47. package/dist/core/message/message-queue.js +70 -2
  48. package/dist/core/message/pending-hints.js +232 -0
  49. package/dist/core/model/model-catalog.js +1 -1
  50. package/dist/core/model/model-scope.js +2 -2
  51. package/dist/core/permission.js +25 -12
  52. package/dist/core/relation/peer-identity.js +16 -1
  53. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  54. package/dist/core/session/session-manager.js +86 -26
  55. package/dist/core/session/session-title.js +26 -0
  56. package/dist/core/stats/billing.js +151 -0
  57. package/dist/core/stats/budget.js +93 -0
  58. package/dist/core/stats/db.js +334 -0
  59. package/dist/core/stats/eck-vars.js +84 -0
  60. package/dist/core/stats/index.js +10 -0
  61. package/dist/core/stats/normalizer.js +78 -0
  62. package/dist/core/stats/query.js +760 -0
  63. package/dist/core/stats/writer.js +115 -0
  64. package/dist/core/trigger/manager.js +34 -0
  65. package/dist/core/trigger/parser.js +9 -3
  66. package/dist/core/trigger/scheduler.js +20 -17
  67. package/dist/data/error-dict.json +7 -0
  68. package/dist/{agents → eck}/manifest-engine.js +20 -1
  69. package/dist/{agents → eck}/message-renderer.js +24 -1
  70. package/dist/index.js +174 -9
  71. package/dist/ipc.js +116 -1
  72. package/dist/utils/cross-platform.js +58 -5
  73. package/dist/utils/ecweb-launch.js +49 -0
  74. package/dist/utils/ecweb-pair.js +20 -0
  75. package/dist/utils/error-utils.js +18 -5
  76. package/dist/utils/npm-ops.js +38 -8
  77. package/dist/utils/stats.js +77 -6
  78. package/kits/docs/evolclaw/INDEX.md +3 -1
  79. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  80. package/kits/docs/evolclaw/fs.md +131 -0
  81. package/kits/docs/evolclaw/group-fs.md +209 -0
  82. package/kits/docs/evolclaw/stats.md +70 -0
  83. package/kits/docs/venues/aun-group.md +29 -6
  84. package/kits/docs/venues/group.md +5 -4
  85. package/kits/eck_message_manifest.json +30 -3
  86. package/kits/rules/05-venue.md +1 -1
  87. package/kits/templates/message-fragments/inject-default.md +2 -0
  88. package/package.json +5 -6
  89. package/dist/agents/baseagent-normalize.js +0 -19
  90. package/dist/core/command-handler.js +0 -3876
  91. package/dist/core/relation/peer-key.js +0 -16
  92. package/dist/evolclaw-config.js +0 -11
  93. package/dist/utils/channel-helpers.js +0 -46
  94. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  95. /package/dist/{agents → eck}/kit-renderer.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.4.0 (2026-06-12)
4
+
5
+ ### New Features
6
+
7
+ - **CLI 模块化** — 将4640行 `command-handler.ts` 和5131行 `cli/index.ts` 拆分为8个专注模块(`command/`、`cli/`子目录),claude-agent-sdk 升级至 ^0.3.170,净减约10000行
8
+ - **ECWeb Monitor 视图** — 新增实时监控页面:进程级 CPU/内存采样(1s 后台循环)、全局统计、per-agent 摘要;IPC 新增 `monitor-snapshot` 处理器
9
+ - **Agent 运行时控制** — ECWeb 支持对每个 agent 执行 start/stop/mute/unmute/queue-clear,stop 中断进行中的模型调用,mute 暂停队列消费同时保留入队消息
10
+ - **`ec watch logs` 多选** — 新增日志类型多选菜单,支持按类型(session/tool/error等)筛选实时聚合日志;`watch.logTypes` 配置默认可选集
11
+ - **AUN 结构化出站 payload** — task status 改走 `notify`(`event/app.task.status`)不入消息历史;activity 逐条结构化(`type:'activity'` + `item`);notice/error 结构化;`ref_message_id`/`initiator`/`thread_id` 统一透传
12
+ - **Context-aware auto-compact** — 根据 DB 中上次 model call 的实际 context token 记录决策压缩时机,在下一任务开始前执行;DB 新增 `context_tokens`/`max_tokens`/`auto_compact_tokens` 字段
13
+ - **Codex Edit events 统一 diff** — fileChange 映射为 `Edit` tool_use 事件并附带 unified diff,permission 层直接渲染,不重新计算
14
+ - **AUN 群命令 mention 过滤** — broadcast 指令强制要求 @ 触发;`action_card_reply` 归属由消息上下文精确判定
15
+ - **群话题创建权限** — 由 AUN `group.get_admins` 实时查询,仅 owner/admin 可建话题,fail-closed;无权时静默丢弃避免 agent 互相拒绝循环
16
+ - **`/baseagent scope` 参数** — 支持 `session`/`default`/`both` 三档控制切换范围
17
+ - **Session topic rename** — 支持 `/rename` 重命名当前话题会话
18
+ - **evolclaw-web 自动升级** — ECWeb 启动时检测并自动升级新版本,对标 fastaun 升级机制
19
+ - **Agent displayName 解析** — 从 agent.md 本地缓存 + 异步网络拉取 displayName,ECWeb 展示更友好
20
+
21
+ ### Improvements
22
+
23
+ - **用户中断归类** — 新消息/`/stop`/撤回触发的流中断独立为 `task:interrupted` 事件,不计入 `task:error`
24
+ - **统一出站响应投递** — AUN 渠道所有出站路径收敛到 `adapter.send`,消除渠道间重复分支
25
+ - **Feishu Pin→CheckMark 两阶段 ack** — 收到消息先加 Pin(排队中),runner 开始时升级为 CheckMark 并移除 Pin,视觉无空窗
26
+ - **dispatchModeOverride 分离** — 动态覆盖与持久化 `dispatchMode` 解耦,避免一次性覆盖污染会话配置
27
+ - **ECWeb token TTL 延长至30天** — 支持滑动续期;端口被占时杀旧进程而非漂移到 port+1
28
+ - **`evolclaw status` 展示 ECWeb** — 状态命令新增 ECWeb 进程与 HTTP 就绪状态
29
+
30
+ ### Bug Fixes
31
+
32
+ - **话题回复上下文丢失** — `ctl send/file` 在 Feishu 话题内未透传 `replyToMessageId`/`replyInThread`,回复落到主会话气泡
33
+ - **文件标记提前暴露** — 定时 flush(非最终)触发时 `[SEND_FILE:...]` 未过滤,原样发给用户
34
+ - **AUN inbound replyContext 缺字段** — 入站消息未填充 `peerId`/`replyToMessageId`,导致 task.status `initiator` 为空
35
+ - **Codex SSE idle 重连误报错** — SSE 超时重连的空 error 事件被当作任务错误处理
36
+ - **JSON parse error 自动重试** — API 返回 JSON 解析异常时触发指数退避重试
37
+ - **ECWeb 启动就绪检测** — HTTP 探测根路径判断就绪,避免进程存活但 HTTP 未就绪的误判
38
+
39
+ ## v3.3.0 (2026-06-10)
40
+
41
+ ### New Features
42
+
43
+ - **Thread 会话继承 baseagent** — 创建 thread 会话时自动继承父会话的 `agentId` 及 baseagent 配置,多线程场景无需重新切换
44
+ - **Codex runner 增强** — 新增 CLI 版本检测、streaming delta 支持、server 可用性检查;codex-app-server-client 补充类型定义
45
+ - **Menu 话题管理** — command-handler 新增话题菜单的权限判定(`canReadTopics`/`canDeleteTopic`)与格式化(`buildTopicMenuItem`/`resolveMenuChatType`)
46
+
47
+ ### Improvements
48
+
49
+ - **Channel plugin 接口统一** — `ChannelPlugin` 从 `isEnabled/createChannel/createChannels` 收敛为单一 `createInstance(inst, ctx)` 单实例模型,新增 `ChannelBuildContext` 与 `showActivities` 共享策略;六个渠道(aun/feishu/wechat/dingtalk/qqbot/wecom)同步迁移
50
+ - **ECWeb 控制台重构** — control source 拆分为 `system`(evolclaw/fastaun/evolclaw-web 三包版本与健康检查)和 `triggers`(定时任务管理),前端联动更新
51
+ - **Trigger 失败统计** — Trigger 新增 `failCount`/`lastResult` 字段,scheduler/manager/parser 同步
52
+ - **Runner 类型模块化** — 抽出 `runner-types.ts` 统一共享类型,消除各 runner 重复声明
53
+ - **Agent AID 展示** — `AgentInfo` 新增 `aid` 字段,`ec agent` 命令展示 agent AID
54
+ - **缓存模块更名** — `read-cache` 重命名为 `daemon-file-cache`,语义更清晰
55
+
3
56
  ## v3.2.0 (2026-06-05)
4
57
 
5
58
  ### New Features
package/README.md CHANGED
@@ -42,7 +42,7 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
42
42
 
43
43
  1. **消息渠道层** (`src/channels/`) - Feishu + WeChat + DingTalk + QQBot + WeCom + AUN 网络
44
44
  2. **消息队列层** (`src/core/message/message-queue.ts`) - 会话级串行处理 + 中断支持
45
- 3. **命令处理层** (`src/core/command-handler.ts`) - 斜杠命令处理(CommandHandler 类)
45
+ 3. **命令处理层** (`src/core/command/`) - 斜杠命令处理(slash-handler / menu-handler / command-handler)
46
46
  4. **消息处理层** (`src/core/message/message-processor.ts`) - 统一事件处理引擎
47
47
  5. **会话管理层** (`src/core/session/session-manager.ts`) - 多项目会话管理
48
48
  6. **交互路由层** (`src/core/interaction-router.ts`) - 卡片交互回调注册与路由
@@ -190,6 +190,11 @@ evolclaw/
190
190
  │ │ └── gemini-runner.ts # Gemini CLI 封装
191
191
  │ ├── aun/ # AUN 协议工具
192
192
  │ ├── core/
193
+ │ │ ├── command/
194
+ │ │ │ ├── command-handler.ts # 命令派发入口
195
+ │ │ │ ├── slash-handler.ts # 斜杠命令实现
196
+ │ │ │ ├── menu-handler.ts # Menu 协议处理
197
+ │ │ │ └── slash-gate.ts # 权限前置拦截
193
198
  │ │ ├── message/
194
199
  │ │ │ ├── message-bridge.ts # 渠道 ↔ 核心消息桥
195
200
  │ │ │ ├── message-processor.ts # 统一消息处理引擎
@@ -202,7 +207,6 @@ evolclaw/
202
207
  │ │ │ ├── session-fs-store.ts # 文件系统存储原语
203
208
  │ │ │ └── session-manager.ts # 会话管理(多项目支持)
204
209
  │ │ ├── trigger/ # 触发器引擎
205
- │ │ ├── command-handler.ts # 斜杠命令处理
206
210
  │ │ ├── evolagent.ts # EvolAgent 实体
207
211
  │ │ ├── evolagent-registry.ts # Agent 注册表(扫描/路由/热重载)
208
212
  │ │ ├── interaction-router.ts # 卡片交互回调路由
@@ -250,7 +254,6 @@ evolclaw/
250
254
  - `/perm [mode]` - 查看或切换权限模式(auto / edit / default / readonly)
251
255
 
252
256
  **系统管理**:
253
- - `/clear` - 清空对话历史
254
257
  - `/compact` - 压缩会话上下文
255
258
  - `/rewind <turn>` - 回退会话到指定轮次
256
259
  - `/stop` - 中断当前任务
@@ -296,7 +299,7 @@ v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.
296
299
  ## 技术栈
297
300
 
298
301
  - **运行时**:Node.js >= 22 + TypeScript(ES modules)
299
- - **AI SDK**:@anthropic-ai/claude-agent-sdk >= 0.2.100、@openai/codex-sdk、Gemini CLI
302
+ - **AI 后端**:@anthropic-ai/claude-agent-sdk >= 0.3.170、Codex CLI app-server、Gemini CLI
300
303
  - **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
301
304
  - **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
302
305
  - **测试框架**:Vitest
@@ -1,14 +1,37 @@
1
1
  /**
2
- * Baseagent credential resolvers.
2
+ * Baseagent identity + credential resolution.
3
3
  *
4
- * 输入是 Config 形态(`config.agents.<baseagent>` + override)。启动期由 index.ts
5
- * primaryAgent.config.baseagents 构造一个 syntheticConfig 喂入;各 plugin
6
- * createAgent 也各自构造 syntheticConfig。
4
+ * 两部分:
5
+ * 1. normalizeBaseagent —— 把用户输入的各种别名(cc / claude-code / gemini cli …)
6
+ * 归一到 canonical 标识 + 展示名。
7
+ * 2. resolve*Config —— 各后端的凭证解析。输入是 Config 形态
8
+ * (`config.agents.<baseagent>` + override)。启动期由 index.ts 从
9
+ * primaryAgent.config.baseagents 构造一个 syntheticConfig 喂入;各 plugin 的
10
+ * createAgent 也各自构造 syntheticConfig。
7
11
  */
8
12
  import fs from 'fs';
9
13
  import path from 'path';
10
14
  import os from 'os';
11
15
  import { commandExists } from '../utils/cross-platform.js';
16
+ const BASEAGENT_ALIASES = {
17
+ claude: { canonical: 'claude', displayName: 'Claude Code' },
18
+ cc: { canonical: 'claude', displayName: 'Claude Code' },
19
+ 'claude-code': { canonical: 'claude', displayName: 'Claude Code' },
20
+ 'claude code': { canonical: 'claude', displayName: 'Claude Code' },
21
+ claudecode: { canonical: 'claude', displayName: 'Claude Code' },
22
+ codex: { canonical: 'codex', displayName: 'Codex' },
23
+ 'codex-cli': { canonical: 'codex', displayName: 'Codex' },
24
+ 'codex cli': { canonical: 'codex', displayName: 'Codex' },
25
+ gemini: { canonical: 'gemini', displayName: 'Gemini CLI' },
26
+ 'gemini-cli': { canonical: 'gemini', displayName: 'Gemini CLI' },
27
+ 'gemini cli': { canonical: 'gemini', displayName: 'Gemini CLI' },
28
+ geminicli: { canonical: 'gemini', displayName: 'Gemini CLI' },
29
+ hermes: { canonical: 'hermes', displayName: 'Hermes' },
30
+ };
31
+ export function normalizeBaseagent(input) {
32
+ const key = String(input || '').trim().toLowerCase().replace(/_/g, '-');
33
+ return BASEAGENT_ALIASES[key] || { canonical: 'unknown', displayName: input ? String(input) : 'Unknown' };
34
+ }
12
35
  function loadClaudeSettings() {
13
36
  try {
14
37
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
@@ -109,7 +132,13 @@ export function resolveOpenaiConfig(config, override) {
109
132
  || config.agents?.codex?.effort
110
133
  || config.agents?.codex?.reasoning
111
134
  || undefined;
112
- return { apiKey, baseUrl, model, effort };
135
+ const enableRequestUserInput = override?.enableRequestUserInput
136
+ ?? config.agents?.codex?.enableRequestUserInput
137
+ ?? true;
138
+ const approvalsReviewer = override?.approvalsReviewer
139
+ ?? config.agents?.codex?.approvalsReviewer
140
+ ?? undefined;
141
+ return { apiKey, baseUrl, model, effort, enableRequestUserInput, approvalsReviewer };
113
142
  }
114
143
  export function resolveGoogleConfig(config, override) {
115
144
  const googleCfg = config.agents?.gemini;
@@ -1,6 +1,6 @@
1
1
  import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { ensureDir } from '../utils/atomic-write.js';
3
- import { resolveAnthropicConfig } from './resolve.js';
3
+ import { resolveAnthropicConfig } from './baseagent.js';
4
4
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
5
5
  import { renderActionAsText } from '../core/interaction-router.js';
6
6
  import { buildEnvelope, sendInteractionPayload } from '../core/message/message-processor.js';
@@ -10,6 +10,8 @@ import os from 'os';
10
10
  import { logger } from '../utils/logger.js';
11
11
  import { checkBlacklist, checkReadonly, summarizeToolInput } from '../core/permission.js';
12
12
  import { encodePath } from '../utils/cross-platform.js';
13
+ import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
14
+ export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
13
15
  // ── 模型别名解析 ──
14
16
  // SDK 内置的别名表可能落后于代理实际可用的最新模型,
15
17
  // 因此优先从 {baseUrl}/models 动态获取各系列最新版本,失败则回退静态表。
@@ -97,8 +99,6 @@ function resolveModelAlias(model, baseUrl) {
97
99
  // 回退静态表
98
100
  return STATIC_MODEL_ALIASES[model] || model;
99
101
  }
100
- /** 支持 1M 上下文窗口的模型 ID 前缀(SDK 通过 `[1m]` 后缀启用)。 */
101
- const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
102
102
  /**
103
103
  * 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
104
104
  * 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
@@ -106,14 +106,10 @@ const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
106
106
  function applyContextWindow(modelId) {
107
107
  if (/\[1m\]$/.test(modelId))
108
108
  return modelId; // 已带后缀
109
- if (ONE_M_CONTEXT_PREFIXES.some(p => modelId === p))
109
+ if (isOneMillionContextModel(modelId))
110
110
  return `${modelId}[1m]`;
111
111
  return modelId;
112
112
  }
113
- /** 根据 SDK model 串(含 [1m] 后缀)返回合适的 autoCompactWindow 值。 */
114
- function contextWindowFor(sdkModel) {
115
- return /\[1m\]$/.test(sdkModel) ? 900000 : 200000;
116
- }
117
113
  /** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
118
114
  function resolveSdkModel(model, baseUrl) {
119
115
  return applyContextWindow(resolveModelAlias(model, baseUrl));
@@ -168,19 +164,9 @@ class MessageStream {
168
164
  }
169
165
  }
170
166
  }
171
- // ── 类型守卫 ──
172
- export function hasModelSwitcher(agent) {
173
- return typeof agent.setModel === 'function' && typeof agent.listModels === 'function';
174
- }
175
- export function hasPermissionController(agent) {
176
- return typeof agent.setMode === 'function' && typeof agent.listModes === 'function';
177
- }
178
- export function hasCompact(agent) {
179
- return typeof agent.compact === 'function';
180
- }
181
167
  export class AgentRunner {
182
168
  name = 'claude';
183
- capabilities = { clear: true, compact: true, fork: true };
169
+ capabilities = { clear: true, compact: true, fork: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
184
170
  apiKey;
185
171
  model;
186
172
  effort;
@@ -383,7 +369,6 @@ export class AgentRunner {
383
369
  },
384
370
  channelId: permCtx.channelId,
385
371
  sessionId,
386
- expiresAt: Date.now() + 5 * 60 * 1000,
387
372
  };
388
373
  }
389
374
  else {
@@ -411,11 +396,11 @@ export class AgentRunner {
411
396
  },
412
397
  channelId: permCtx.channelId,
413
398
  sessionId,
414
- expiresAt: Date.now() + 5 * 60 * 1000,
415
399
  };
416
400
  }
417
401
  let cardSent = false;
418
402
  try {
403
+ await permCtx.flushPending?.();
419
404
  const envelope = buildEnvelope({
420
405
  taskId: permCtx.taskId,
421
406
  channel: permCtx.channel ?? permCtx.adapter.channelName,
@@ -433,6 +418,7 @@ export class AgentRunner {
433
418
  logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
434
419
  }
435
420
  if (!cardSent) {
421
+ await permCtx.flushPending?.();
436
422
  const firstLabel = q.options[0]?.label || '';
437
423
  answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
438
424
  if (sendPrompt) {
@@ -530,7 +516,7 @@ export class AgentRunner {
530
516
  else {
531
517
  resolve(action.trim());
532
518
  }
533
- }, { timeoutMs: 120_000, onTimeout: () => resolve(q.options[0]?.label || ''), initiatorId: permCtx.userId, fallbackCommand: 'ask' });
519
+ }, { initiatorId: permCtx.userId, fallbackCommand: 'ask' });
534
520
  });
535
521
  answers[q.question] = answer;
536
522
  }
@@ -606,6 +592,7 @@ export class AgentRunner {
606
592
  },
607
593
  };
608
594
  try {
595
+ await permCtx.flushPending?.();
609
596
  const envelope = buildEnvelope({
610
597
  taskId: permCtx.taskId,
611
598
  channel: permCtx.channel ?? permCtx.adapter.channelName,
@@ -663,6 +650,7 @@ export class AgentRunner {
663
650
  buttonArgMap: { approve: '1', reject: '2' },
664
651
  },
665
652
  };
653
+ await permCtx.flushPending?.();
666
654
  await sendPrompt(renderActionAsText(fallbackInteraction));
667
655
  permCtx.interactionRouter.unmarkWaiting(sessionId);
668
656
  return new Promise((resolve) => {
@@ -674,11 +662,12 @@ export class AgentRunner {
674
662
  else {
675
663
  resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
676
664
  }
677
- }, { timeoutMs: 300_000, onTimeout: () => resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' }), initiatorId: permCtx.userId, fallbackCommand: 'ask' });
665
+ }, { initiatorId: permCtx.userId, fallbackCommand: 'ask' });
678
666
  });
679
667
  }
680
668
  // 无交互能力,发提示后直接 allow
681
669
  permCtx?.interactionRouter?.unmarkWaiting(sessionId);
670
+ await permCtx.flushPending?.();
682
671
  await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
683
672
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
684
673
  }
@@ -692,6 +681,9 @@ export class AgentRunner {
692
681
  const toolUseNames = new Map();
693
682
  let turnCount = 0;
694
683
  const seenMessageIds = new Set();
684
+ let lastModelCall;
685
+ // 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
686
+ const collectedCalls = [];
695
687
  try {
696
688
  for await (const event of sdkStream) {
697
689
  // 提取 session_id(任意 SDK 事件都可能携带)
@@ -700,6 +692,38 @@ export class AgentRunner {
700
692
  this.updateSessionId(sessionId, event.session_id);
701
693
  yield { type: 'session_id', sessionId: event.session_id };
702
694
  }
695
+ if (event.type === 'stream_event') {
696
+ const streamEvent = event.event;
697
+ if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
698
+ lastModelCall = {
699
+ uuid: event.uuid,
700
+ model: streamEvent.message.model,
701
+ tokenUsage: streamEvent.message.usage,
702
+ };
703
+ // 流式收集:每个 message_start = 一次新的大模型调用
704
+ collectedCalls.push({
705
+ call_index: collectedCalls.length,
706
+ model: streamEvent.message.model ?? callModel ?? this.model,
707
+ request_id: event.request_id,
708
+ tokenUsage: { ...streamEvent.message.usage },
709
+ });
710
+ }
711
+ else if (streamEvent?.type === 'message_delta' && streamEvent.usage) {
712
+ lastModelCall = {
713
+ ...lastModelCall,
714
+ uuid: lastModelCall?.uuid ?? event.uuid,
715
+ tokenUsage: {
716
+ ...(lastModelCall?.tokenUsage ?? {}),
717
+ ...streamEvent.usage,
718
+ },
719
+ };
720
+ // 将 message_delta 的 usage 合并进当前(最后一次)收集的调用
721
+ const last = collectedCalls[collectedCalls.length - 1];
722
+ if (last)
723
+ last.tokenUsage = { ...last.tokenUsage, ...streamEvent.usage };
724
+ }
725
+ continue;
726
+ }
703
727
  // system: compact_boundary → compact
704
728
  if (event.type === 'system' && event.subtype === 'compact_boundary') {
705
729
  yield {
@@ -730,6 +754,18 @@ export class AgentRunner {
730
754
  seenMessageIds.add(msgId);
731
755
  turnCount++;
732
756
  }
757
+ if (event.message.usage) {
758
+ lastModelCall = {
759
+ ...lastModelCall,
760
+ messageId: event.message.id,
761
+ requestId: event.request_id,
762
+ model: event.message.model,
763
+ tokenUsage: {
764
+ ...event.message.usage,
765
+ ...(lastModelCall?.tokenUsage ?? {}),
766
+ },
767
+ };
768
+ }
733
769
  // 统计本轮 base agent 全部输出字符数(text + tool_use input)
734
770
  let turnOutputChars = 0;
735
771
  for (const content of event.message.content) {
@@ -790,19 +826,69 @@ export class AgentRunner {
790
826
  const cleanResult = typeof event.result === 'string'
791
827
  ? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
792
828
  : event.result;
793
- // 从 usage 三项求和得到当前上下文占用(与 claude-hud getTotalTokens 相同算法)
829
+ // 从 usage 求当前上下文占用。
830
+ // Claude:input_tokens 是净输入(不含 cache),三项求和 = 实际上下文长度。
831
+ // 非 Claude(DeepSeek/OpenAI 兼容):cache_read 是服务端 KV cache 不占上下文窗口,
832
+ // input_tokens 本身就是完整的上下文输入量。
794
833
  const u = event.usage;
795
- const totalTokens = u
796
- ? (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
797
- : 0;
798
- const maxTokens = sdkModel ? contextWindowFor(sdkModel) : 200000;
834
+ const effectiveModel = callModel ?? this.model;
835
+ const isClaudeModel = isClaudeContextUsageModel(effectiveModel);
836
+ const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
837
+ const contextWindowTokens = realContextWindowForModel(sdkModel);
838
+ const autoCompactTokens = autoCompactWindowForModel(sdkModel);
799
839
  const contextUsage = totalTokens > 0 ? {
800
840
  totalTokens,
801
- maxTokens,
802
- percentage: Math.round((totalTokens / maxTokens) * 100),
841
+ maxTokens: contextWindowTokens,
842
+ percentage: Math.round((totalTokens / contextWindowTokens) * 100),
843
+ autoCompactTokens,
803
844
  model: callModel ?? this.model,
804
845
  effort: callEffort ?? this.effort,
805
846
  } : undefined;
847
+ if (lastModelCall?.tokenUsage) {
848
+ const lastUsageForContext = usageForContext(lastModelCall.tokenUsage);
849
+ const lastTotalTokens = contextTokensForUsage(lastUsageForContext, !!isClaudeModel);
850
+ lastModelCall = {
851
+ ...lastModelCall,
852
+ contextUsage: lastTotalTokens > 0 ? {
853
+ totalTokens: lastTotalTokens,
854
+ maxTokens: contextWindowTokens,
855
+ percentage: Math.round((lastTotalTokens / contextWindowTokens) * 100),
856
+ autoCompactTokens,
857
+ model: callModel ?? this.model,
858
+ effort: callEffort ?? this.effort,
859
+ } : undefined,
860
+ };
861
+ }
862
+ const contextUsageForCall = (usage) => {
863
+ const callTotalTokens = contextTokensForUsage(usageForContext(usage), !!isClaudeModel);
864
+ return callTotalTokens > 0 ? {
865
+ totalTokens: callTotalTokens,
866
+ maxTokens: contextWindowTokens,
867
+ percentage: Math.round((callTotalTokens / contextWindowTokens) * 100),
868
+ autoCompactTokens,
869
+ model: callModel ?? this.model,
870
+ effort: callEffort ?? this.effort,
871
+ } : undefined;
872
+ };
873
+ // 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
874
+ const callModel_ = callModel ?? this.model;
875
+ let modelCalls;
876
+ const iterArr = Array.isArray(u?.iterations) && u.iterations.length > 0 ? u.iterations : null;
877
+ if (iterArr) {
878
+ modelCalls = iterArr.map((it, i) => ({
879
+ call_index: i, model: callModel_, tokenUsage: it, contextUsage: contextUsageForCall(it),
880
+ }));
881
+ }
882
+ else if (collectedCalls.length > 0) {
883
+ modelCalls = collectedCalls.map(call => ({
884
+ ...call,
885
+ contextUsage: contextUsageForCall(call.tokenUsage),
886
+ }));
887
+ }
888
+ else if (u) {
889
+ // 降级:无逐次数据,写一条累计行
890
+ modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
891
+ }
806
892
  yield {
807
893
  type: 'complete',
808
894
  result: cleanResult,
@@ -817,6 +903,8 @@ export class AgentRunner {
817
903
  numTurns: event.num_turns,
818
904
  tokenUsage: event.usage,
819
905
  contextUsage,
906
+ lastModelCall,
907
+ modelCalls,
820
908
  };
821
909
  // result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
822
910
  return;
@@ -1030,11 +1118,12 @@ export class AgentRunner {
1030
1118
  model: sdkModel,
1031
1119
  ...(callEffort ? { effort: callEffort } : {}),
1032
1120
  ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
1033
- autoCompactWindow: contextWindowFor(sdkModel),
1121
+ autoCompactWindow: autoCompactWindowForModel(sdkModel),
1034
1122
  advisorModel: 'haiku',
1035
1123
  canUseTool: canUseToolCallback,
1036
1124
  permissionMode: sdkPermissionMode,
1037
1125
  persistSession: true,
1126
+ includePartialMessages: true,
1038
1127
  enableFileCheckpointing: true,
1039
1128
  hooks: {
1040
1129
  PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],