evolclaw 3.3.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 (44) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +7 -3
  3. package/dist/agents/claude-runner.js +23 -27
  4. package/dist/agents/codex-runner.js +90 -6
  5. package/dist/agents/runner-types.js +30 -0
  6. package/dist/aun/outbox.js +14 -2
  7. package/dist/channels/aun.js +506 -108
  8. package/dist/channels/feishu.js +29 -5
  9. package/dist/cli/agent-command.js +591 -0
  10. package/dist/cli/agent.js +15 -3
  11. package/dist/cli/aun-commands.js +1444 -0
  12. package/dist/cli/ctl-command.js +78 -0
  13. package/dist/cli/daemon-commands.js +2707 -0
  14. package/dist/cli/index.js +12 -5027
  15. package/dist/cli/restart-monitor.js +539 -0
  16. package/dist/cli/watch-logs.js +33 -0
  17. package/dist/core/channel-loader.js +4 -1
  18. package/dist/core/command/command-handler.js +1189 -0
  19. package/dist/core/command/menu-handler.js +1478 -0
  20. package/dist/core/command/slash-gate.js +142 -0
  21. package/dist/core/command/slash-handler.js +2090 -0
  22. package/dist/core/evolagent-registry.js +81 -0
  23. package/dist/core/evolagent.js +16 -0
  24. package/dist/core/message/im-renderer.js +67 -49
  25. package/dist/core/message/message-bridge.js +30 -9
  26. package/dist/core/message/message-processor.js +200 -122
  27. package/dist/core/message/message-queue.js +68 -0
  28. package/dist/core/permission.js +16 -0
  29. package/dist/core/session/session-manager.js +59 -13
  30. package/dist/core/stats/db.js +20 -0
  31. package/dist/core/stats/writer.js +3 -3
  32. package/dist/data/error-dict.json +7 -0
  33. package/dist/index.js +49 -6
  34. package/dist/ipc.js +99 -0
  35. package/dist/utils/cross-platform.js +35 -0
  36. package/dist/utils/ecweb-launch.js +49 -0
  37. package/dist/utils/error-utils.js +18 -5
  38. package/dist/utils/npm-ops.js +38 -8
  39. package/dist/utils/stats.js +63 -6
  40. package/kits/eck_manifest.json +0 -12
  41. package/package.json +2 -3
  42. package/dist/core/command-handler.js +0 -4235
  43. package/dist/core/message/response-depth.js +0 -56
  44. package/kits/templates/system-fragments/response-depth.md +0 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
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
+
3
39
  ## v3.3.0 (2026-06-10)
4
40
 
5
41
  ### 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 # 卡片交互回调路由
@@ -295,7 +299,7 @@ v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.
295
299
  ## 技术栈
296
300
 
297
301
  - **运行时**:Node.js >= 22 + TypeScript(ES modules)
298
- - **AI 后端**:@anthropic-ai/claude-agent-sdk >= 0.2.100、Codex CLI app-server、Gemini CLI
302
+ - **AI 后端**:@anthropic-ai/claude-agent-sdk >= 0.3.170、Codex CLI app-server、Gemini CLI
299
303
  - **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
300
304
  - **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
301
305
  - **测试框架**:Vitest
@@ -10,7 +10,7 @@ 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 } from './runner-types.js';
13
+ import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
14
14
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
15
15
  // ── 模型别名解析 ──
16
16
  // SDK 内置的别名表可能落后于代理实际可用的最新模型,
@@ -99,8 +99,6 @@ function resolveModelAlias(model, baseUrl) {
99
99
  // 回退静态表
100
100
  return STATIC_MODEL_ALIASES[model] || model;
101
101
  }
102
- /** 支持 1M 上下文窗口的模型 ID 前缀(SDK 通过 `[1m]` 后缀启用)。 */
103
- const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
104
102
  /**
105
103
  * 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
106
104
  * 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
@@ -108,26 +106,10 @@ const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
108
106
  function applyContextWindow(modelId) {
109
107
  if (/\[1m\]$/.test(modelId))
110
108
  return modelId; // 已带后缀
111
- if (ONE_M_CONTEXT_PREFIXES.some(p => modelId === p))
109
+ if (isOneMillionContextModel(modelId))
112
110
  return `${modelId}[1m]`;
113
111
  return modelId;
114
112
  }
115
- /** 真实上下文窗口大小:1M 模型 = 1000000,否则 200000 */
116
- function realContextWindowFor(sdkModel) {
117
- if (/\[1m\]$/.test(sdkModel))
118
- return 1000000;
119
- if (/deepseek-v4/i.test(sdkModel))
120
- return 1000000; // deepseek-v4 系列原生 1M 窗口(无 [1m] 后缀)
121
- return 200000;
122
- }
123
- /** autoCompact 触发阈值:1M 模型 = 900000(留 ~100k buffer),否则 200000 */
124
- function autoCompactWindowFor(sdkModel) {
125
- if (/\[1m\]$/.test(sdkModel))
126
- return 900000;
127
- if (/deepseek-v4/i.test(sdkModel))
128
- return 900000;
129
- return 200000;
130
- }
131
113
  /** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
132
114
  function resolveSdkModel(model, baseUrl) {
133
115
  return applyContextWindow(resolveModelAlias(model, baseUrl));
@@ -850,10 +832,10 @@ export class AgentRunner {
850
832
  // input_tokens 本身就是完整的上下文输入量。
851
833
  const u = event.usage;
852
834
  const effectiveModel = callModel ?? this.model;
853
- const isClaudeModel = effectiveModel?.startsWith('claude');
835
+ const isClaudeModel = isClaudeContextUsageModel(effectiveModel);
854
836
  const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
855
- const contextWindowTokens = sdkModel ? realContextWindowFor(sdkModel) : 200000;
856
- const autoCompactTokens = sdkModel ? autoCompactWindowFor(sdkModel) : 200000;
837
+ const contextWindowTokens = realContextWindowForModel(sdkModel);
838
+ const autoCompactTokens = autoCompactWindowForModel(sdkModel);
857
839
  const contextUsage = totalTokens > 0 ? {
858
840
  totalTokens,
859
841
  maxTokens: contextWindowTokens,
@@ -877,21 +859,35 @@ export class AgentRunner {
877
859
  } : undefined,
878
860
  };
879
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
+ };
880
873
  // 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
881
874
  const callModel_ = callModel ?? this.model;
882
875
  let modelCalls;
883
876
  const iterArr = Array.isArray(u?.iterations) && u.iterations.length > 0 ? u.iterations : null;
884
877
  if (iterArr) {
885
878
  modelCalls = iterArr.map((it, i) => ({
886
- call_index: i, model: callModel_, tokenUsage: it,
879
+ call_index: i, model: callModel_, tokenUsage: it, contextUsage: contextUsageForCall(it),
887
880
  }));
888
881
  }
889
882
  else if (collectedCalls.length > 0) {
890
- modelCalls = collectedCalls;
883
+ modelCalls = collectedCalls.map(call => ({
884
+ ...call,
885
+ contextUsage: contextUsageForCall(call.tokenUsage),
886
+ }));
891
887
  }
892
888
  else if (u) {
893
889
  // 降级:无逐次数据,写一条累计行
894
- modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, degraded: true }];
890
+ modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
895
891
  }
896
892
  yield {
897
893
  type: 'complete',
@@ -1122,7 +1118,7 @@ export class AgentRunner {
1122
1118
  model: sdkModel,
1123
1119
  ...(callEffort ? { effort: callEffort } : {}),
1124
1120
  ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
1125
- autoCompactWindow: autoCompactWindowFor(sdkModel),
1121
+ autoCompactWindow: autoCompactWindowForModel(sdkModel),
1126
1122
  advisorModel: 'haiku',
1127
1123
  canUseTool: canUseToolCallback,
1128
1124
  permissionMode: sdkPermissionMode,
@@ -307,6 +307,7 @@ export class CodexRunner {
307
307
  streamedAgentMessageIds: new Set(),
308
308
  agentMessageDeltaText: new Map(),
309
309
  completedItemIds: new Set(),
310
+ emittedEditCallIds: new Set(),
310
311
  completedTurnIds: new Set(),
311
312
  };
312
313
  const unsubscribe = appServer.onNotification(notification => {
@@ -1073,7 +1074,7 @@ export class CodexRunner {
1073
1074
  break;
1074
1075
  }
1075
1076
  case 'item/started': {
1076
- yield* this.mapAppServerItemStarted(params.item);
1077
+ yield* this.mapAppServerItemStarted(params.item, state);
1077
1078
  break;
1078
1079
  }
1079
1080
  case 'item/agentMessage/delta': {
@@ -1092,6 +1093,10 @@ export class CodexRunner {
1092
1093
  yield* this.mapAppServerItemCompleted(item, state);
1093
1094
  break;
1094
1095
  }
1096
+ case 'item/fileChange/patchUpdated': {
1097
+ yield* this.mapAppServerFileChangePatchUpdated(params, state);
1098
+ break;
1099
+ }
1095
1100
  case 'turn/plan/updated': {
1096
1101
  const plan = Array.isArray(params.plan) ? params.plan : [];
1097
1102
  const completed = plan.filter((step) => step?.status === 'completed').length;
@@ -1123,12 +1128,17 @@ export class CodexRunner {
1123
1128
  break;
1124
1129
  }
1125
1130
  case 'error': {
1126
- yield { type: 'error', error: params.message || 'Codex app-server error', errorType: 'unknown' };
1131
+ if (!params.message) {
1132
+ // SSE idle timeout reconnect — not a real task error, suppress
1133
+ logger.debug(`[CodexRunner] app-server SSE reconnect (no message)`);
1134
+ break;
1135
+ }
1136
+ yield { type: 'error', error: params.message, errorType: 'unknown' };
1127
1137
  break;
1128
1138
  }
1129
1139
  }
1130
1140
  }
1131
- *mapAppServerItemStarted(item) {
1141
+ *mapAppServerItemStarted(item, state) {
1132
1142
  if (!item)
1133
1143
  return;
1134
1144
  switch (item.type) {
@@ -1142,8 +1152,12 @@ export class CodexRunner {
1142
1152
  yield { type: 'tool_use', name: item.namespace ? `${item.namespace}:${item.tool}` : item.tool, input: item.arguments, callId: item.id };
1143
1153
  break;
1144
1154
  case 'fileChange': {
1145
- const desc = this.normalizeFileChanges(item.changes).map((change) => this.describeFileChange(change)).join(', ');
1146
- yield { type: 'tool_use', name: 'FileChange', input: { description: desc }, callId: item.id };
1155
+ const editEvent = this.buildCodexEditEvent(item.id, item.changes);
1156
+ if (editEvent) {
1157
+ if (item.id)
1158
+ state?.emittedEditCallIds.add(item.id);
1159
+ yield editEvent;
1160
+ }
1147
1161
  break;
1148
1162
  }
1149
1163
  case 'webSearch':
@@ -1197,10 +1211,80 @@ export class CodexRunner {
1197
1211
  };
1198
1212
  break;
1199
1213
  case 'fileChange':
1200
- yield { type: 'tool_result', name: 'FileChange', result: item.changes, isError: item.status === 'failed', callId: item.id };
1214
+ if (this.fileChangesHaveProtocolDiff(item.changes)) {
1215
+ if (item.id && !state.emittedEditCallIds.has(item.id)) {
1216
+ const editEvent = this.buildCodexEditEvent(item.id, item.changes);
1217
+ if (editEvent) {
1218
+ state.emittedEditCallIds.add(item.id);
1219
+ yield editEvent;
1220
+ }
1221
+ }
1222
+ yield { type: 'tool_result', name: 'Edit', result: item.changes, isError: item.status === 'failed', callId: item.id };
1223
+ }
1224
+ else {
1225
+ const desc = this.normalizeFileChanges(item.changes).map((change) => this.describeFileChange(change)).join(', ');
1226
+ yield { type: 'tool_use', name: 'FileChange', input: { description: desc }, callId: item.id };
1227
+ yield { type: 'tool_result', name: 'FileChange', result: item.changes, isError: item.status === 'failed', callId: item.id };
1228
+ }
1201
1229
  break;
1202
1230
  }
1203
1231
  }
1232
+ *mapAppServerFileChangePatchUpdated(params, state) {
1233
+ const itemId = typeof params.itemId === 'string' ? params.itemId : undefined;
1234
+ if (!itemId || state.emittedEditCallIds.has(itemId))
1235
+ return;
1236
+ const editEvent = this.buildCodexEditEvent(itemId, params.changes);
1237
+ if (!editEvent)
1238
+ return;
1239
+ state.emittedEditCallIds.add(itemId);
1240
+ yield editEvent;
1241
+ }
1242
+ buildCodexEditEvent(callId, changes) {
1243
+ const editInput = this.buildCodexEditInput(changes);
1244
+ if (!editInput)
1245
+ return null;
1246
+ return { type: 'tool_use', name: 'Edit', input: editInput, callId };
1247
+ }
1248
+ buildCodexEditInput(changes) {
1249
+ const normalized = this.normalizeFileChanges(changes)
1250
+ .map((change) => this.normalizeCodexProtocolDiffChange(change))
1251
+ .filter((change) => !!change);
1252
+ if (normalized.length === 0)
1253
+ return null;
1254
+ const first = normalized[0];
1255
+ return {
1256
+ file_path: first.path,
1257
+ unified_diff: normalized.map(change => this.formatCodexUnifiedDiff(change)).join('\n'),
1258
+ codex_file_changes: normalized,
1259
+ };
1260
+ }
1261
+ normalizeCodexProtocolDiffChange(change) {
1262
+ const filePath = typeof change?.path === 'string' ? change.path : '';
1263
+ const diff = typeof change?.diff === 'string' ? change.diff
1264
+ : typeof change?.unified_diff === 'string' ? change.unified_diff
1265
+ : typeof change?.unifiedDiff === 'string' ? change.unifiedDiff
1266
+ : '';
1267
+ if (!filePath || !diff)
1268
+ return null;
1269
+ return {
1270
+ path: filePath,
1271
+ diff,
1272
+ kind: this.normalizeFileChangeKind(change.kind ?? change.type),
1273
+ };
1274
+ }
1275
+ fileChangesHaveProtocolDiff(changes) {
1276
+ return this.buildCodexEditInput(changes) !== null;
1277
+ }
1278
+ formatCodexUnifiedDiff(change) {
1279
+ const pathLabel = change.path.replace(/\\/g, '/');
1280
+ const diffPath = pathLabel.replace(/^\/+/, '');
1281
+ const header = change.diff.startsWith('diff ')
1282
+ || change.diff.startsWith('--- ')
1283
+ || change.diff.startsWith('+++ ')
1284
+ ? ''
1285
+ : `--- a/${diffPath}\n+++ b/${diffPath}\n`;
1286
+ return `${header}${change.diff.trimEnd()}`;
1287
+ }
1204
1288
  pickNumber(...values) {
1205
1289
  for (const value of values) {
1206
1290
  if (typeof value === 'number' && Number.isFinite(value))
@@ -26,3 +26,33 @@ export function usageForContext(usage) {
26
26
  const lastIteration = iterations?.slice().reverse().find(it => contextTokensForUsage(it, true) > 0);
27
27
  return lastIteration ?? usage;
28
28
  }
29
+ /** Models whose base ID uses a 1M context window when sent to the SDK with [1m]. */
30
+ export const ONE_M_CONTEXT_MODEL_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
31
+ const ONE_M_CONTEXT_MODEL_ALIASES = ['opus', 'sonnet'];
32
+ const CLAUDE_CONTEXT_MODEL_ALIASES = ['opus', 'sonnet', 'haiku'];
33
+ export function isClaudeContextUsageModel(model) {
34
+ if (!model)
35
+ return false;
36
+ const baseModel = model.replace(/\[1m\]$/i, '');
37
+ return /^claude-/i.test(baseModel) || CLAUDE_CONTEXT_MODEL_ALIASES.includes(baseModel);
38
+ }
39
+ export function isOneMillionContextModel(model) {
40
+ if (!model)
41
+ return false;
42
+ if (ONE_M_CONTEXT_MODEL_ALIASES.includes(model))
43
+ return true;
44
+ if (/\[1m\]$/i.test(model))
45
+ return true;
46
+ if (/deepseek-v4/i.test(model))
47
+ return true;
48
+ const baseModel = model.replace(/\[1m\]$/i, '');
49
+ return ONE_M_CONTEXT_MODEL_PREFIXES.some(prefix => baseModel === prefix || baseModel.startsWith(`${prefix}-`));
50
+ }
51
+ /** Real context window size: 1M models = 1000000, otherwise 200000. */
52
+ export function realContextWindowForModel(model) {
53
+ return isOneMillionContextModel(model) ? 1000000 : 200000;
54
+ }
55
+ /** autoCompact trigger threshold: 1M models = 900000, otherwise 200000. */
56
+ export function autoCompactWindowForModel(model) {
57
+ return isOneMillionContextModel(model) ? 900000 : 200000;
58
+ }
@@ -59,10 +59,14 @@ export function enqueue(aid, opts) {
59
59
  aid,
60
60
  channelId: opts.channelId,
61
61
  type: opts.type,
62
+ contentKind: opts.contentKind,
63
+ payload: opts.payload,
62
64
  text: opts.text,
63
65
  filePath: opts.filePath,
66
+ logText: opts.logText,
64
67
  context: opts.context,
65
68
  ttl: opts.ttl ?? DEFAULT_TTL,
69
+ postSend: opts.postSend,
66
70
  };
67
71
  const dir = outboxDir();
68
72
  fs.mkdirSync(dir, { recursive: true });
@@ -97,6 +101,7 @@ export async function drain(aid, sender) {
97
101
  const entries = readEntries(aid);
98
102
  if (entries.length === 0)
99
103
  return { sent: 0, expired: 0, failed: 0 };
104
+ const drainedIds = new Set(entries.map(e => e.id));
100
105
  let sent = 0;
101
106
  let expired = 0;
102
107
  let failed = 0;
@@ -107,21 +112,28 @@ export async function drain(aid, sender) {
107
112
  continue;
108
113
  }
109
114
  try {
115
+ entry.attempts = (entry.attempts ?? 0) + 1;
110
116
  const ok = await sender(entry);
111
117
  if (ok) {
112
118
  sent++;
113
119
  }
114
120
  else {
115
121
  failed++;
122
+ entry.lastError = 'sender returned false';
116
123
  remaining.push(entry);
117
124
  }
118
125
  }
119
- catch {
126
+ catch (e) {
120
127
  failed++;
128
+ entry.lastError = e instanceof Error ? e.message : String(e);
121
129
  remaining.push(entry);
122
130
  }
123
131
  }
124
- writeEntries(aid, remaining);
132
+ const current = readEntries(aid);
133
+ const currentIds = new Set(current.map(e => e.id));
134
+ const retainedNewEntries = current.filter(e => !drainedIds.has(e.id));
135
+ const retainedFailedEntries = remaining.filter(e => currentIds.has(e.id));
136
+ writeEntries(aid, [...retainedFailedEntries, ...retainedNewEntries]);
125
137
  return { sent, expired, failed };
126
138
  }
127
139
  export function hasPending(aid) {