evolcore 0.0.38 → 0.0.40

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 (125) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +31 -20
  3. package/bin/ec-safe-output.js +14 -2
  4. package/dist/agents/baseagent-availability.js +1 -0
  5. package/dist/agents/claude-runner.js +32 -46
  6. package/dist/agents/codex-runner.js +54 -30
  7. package/dist/agents/ecagent-runner.js +18 -7
  8. package/dist/agents/gemini-runner.js +1 -2
  9. package/dist/agents/runner-types.js +28 -6
  10. package/dist/aun/aid/agentmd.js +21 -0
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +23 -98
  13. package/dist/aun/aid/ownership-operation.js +35 -12
  14. package/dist/aun/group/invite-service.js +6 -0
  15. package/dist/aun/msg/p2p.js +8 -12
  16. package/dist/channels/aun.js +66 -48
  17. package/dist/channels/dingtalk.js +8 -2
  18. package/dist/channels/feishu.js +6 -1
  19. package/dist/channels/wecom.js +7 -3
  20. package/dist/cli/agent-create.js +30 -6
  21. package/dist/cli/agent.js +30 -37
  22. package/dist/cli/aid-bind-command.js +16 -2
  23. package/dist/cli/aun-commands.js +31 -23
  24. package/dist/cli/config.js +15 -18
  25. package/dist/cli/contact.js +9 -12
  26. package/dist/cli/ctl-command.js +3 -3
  27. package/dist/cli/daemon-commands.js +242 -17
  28. package/dist/cli/diag.js +7 -3
  29. package/dist/cli/fs-command.js +5 -4
  30. package/dist/cli/handoff-command.js +11 -11
  31. package/dist/cli/help.js +18 -0
  32. package/dist/cli/index.js +25 -13
  33. package/dist/cli/init-channel.js +2 -1
  34. package/dist/cli/memory-command.js +47 -27
  35. package/dist/cli/model.js +5 -4
  36. package/dist/cli/queue-command.js +3 -3
  37. package/dist/cli/response.js +5 -4
  38. package/dist/cli/restart-monitor.js +35 -13
  39. package/dist/cli/skill-command.js +13 -13
  40. package/dist/cli/task-command.js +3 -3
  41. package/dist/cli/task-context.js +1 -0
  42. package/dist/cli/thread-command.js +5 -3
  43. package/dist/cli/trigger-command.js +3 -3
  44. package/dist/cli/version.js +6 -2
  45. package/dist/config/config-field-policy.js +1 -0
  46. package/dist/contracts/ecweb-service-protocol.js +44 -0
  47. package/dist/core/agent-runtime.js +6 -0
  48. package/dist/core/auth/agent-delegation.js +7 -4
  49. package/dist/core/auth/auth-gateway.js +3 -1
  50. package/dist/core/auth/authorization-audit.js +5 -1
  51. package/dist/core/auth/debug-context.js +5 -1
  52. package/dist/core/auth/operation-catalog.js +17 -1
  53. package/dist/core/bootstrap-service.js +20 -12
  54. package/dist/core/command/cli-intent-parser.js +5 -2
  55. package/dist/core/command/command-handler.js +20 -5
  56. package/dist/core/command/menu-catalog.js +48 -12
  57. package/dist/core/command/menu-handler.js +74 -6
  58. package/dist/core/command/menu-protocol.js +20 -1
  59. package/dist/core/command/slash-gate.js +17 -3
  60. package/dist/core/command/slash-handler.js +90 -7
  61. package/dist/core/event-catalog.js +2 -0
  62. package/dist/core/interaction-router.js +2 -1
  63. package/dist/core/ipc-registration.js +26 -7
  64. package/dist/core/memory/batch-meta.js +2 -0
  65. package/dist/core/memory/consolidation-context.js +42 -51
  66. package/dist/core/memory/consolidation-ops-records.js +39 -7
  67. package/dist/core/memory/consolidation-trigger.js +71 -64
  68. package/dist/core/memory/consolidation.js +226 -20
  69. package/dist/core/memory/memory-writer.js +160 -34
  70. package/dist/core/memory/relation-idle-monitor.js +287 -0
  71. package/dist/core/memory/relation-sweep.js +206 -0
  72. package/dist/core/message/aun-ipc-sender.js +5 -0
  73. package/dist/core/message/message-bridge.js +64 -55
  74. package/dist/core/message/message-log.js +17 -0
  75. package/dist/core/message/message-queue.js +6 -0
  76. package/dist/core/message/outbound.js +70 -0
  77. package/dist/core/message/peer-mode.js +7 -2
  78. package/dist/core/message/response-engine.js +341 -73
  79. package/dist/core/ownership/owner-resolver.js +465 -140
  80. package/dist/core/permission/ec-command-parser.js +19 -3
  81. package/dist/core/permission/protected-paths.js +51 -0
  82. package/dist/core/permission/tool-policy.js +7 -7
  83. package/dist/core/role/runtime-policy.js +64 -0
  84. package/dist/core/session/session-manager.js +25 -5
  85. package/dist/core/system-session/memory.js +3 -18
  86. package/dist/ecweb/basic-settings-service.js +709 -0
  87. package/dist/ecweb/providers-service.js +172 -0
  88. package/dist/index.js +92 -4
  89. package/dist/ipc.js +78 -11
  90. package/dist/stats/db.js +33 -0
  91. package/dist/stats/index.js +2 -2
  92. package/dist/stats/query.js +246 -0
  93. package/dist/stats/writer.js +41 -1
  94. package/dist/trigger/application-service.js +12 -0
  95. package/dist/trigger/authorization.js +2 -2
  96. package/dist/trigger/history.js +36 -0
  97. package/dist/trigger/scheduler.js +3 -0
  98. package/dist/utils/ecweb-supervisor.js +31 -10
  99. package/dist/utils/error-utils.js +8 -4
  100. package/dist/utils/evolcore-version.js +3 -2
  101. package/dist/utils/host-info.js +52 -0
  102. package/dist/utils/welcome.js +18 -24
  103. package/ecagent/dist/harness/agent-harness.js +4 -1
  104. package/kits/docs/evolcore/INDEX.md +1 -1
  105. package/kits/docs/evolcore/agent.md +1 -1
  106. package/kits/docs/evolcore/aid.md +58 -8
  107. package/kits/docs/evolcore/config.md +9 -7
  108. package/kits/docs/evolcore/memory-architecture.md +3 -4
  109. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  110. package/kits/docs/evolcore/memory-tags.md +1 -1
  111. package/kits/docs/evolcore/memory.md +1 -1
  112. package/kits/docs/evolcore/menu-client.md +1 -1
  113. package/kits/docs/evolcore/menu-protocol.md +6 -0
  114. package/kits/docs/evolcore/msg.md +1 -1
  115. package/kits/eck_manifest.json +3 -3
  116. package/kits/rules/04-relation.md +3 -2
  117. package/kits/schemas/agent-config.schema.13.json +5 -1
  118. package/kits/templates/memory-consolidation/generalize.md +32 -4
  119. package/kits/templates/memory-consolidation/notes.md +2 -2
  120. package/kits/templates/memory-consolidation/promote.md +10 -2
  121. package/kits/templates/system-fragments/baseagent.md +5 -0
  122. package/kits/templates/system-fragments/memory.md +12 -3
  123. package/kits/templates/system-fragments/session.md +1 -1
  124. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  125. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -5,6 +5,70 @@
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.0.40 (2026-10-26)
9
+
10
+ ### Ownership 与绑定
11
+
12
+ - 改进 AUN `/bind` 发起前的状态检查,已绑定、已有待处理请求或状态未确认时避免重复提交。
13
+ - 升级 AUN SDK 最低兼容版本至 `0.5.25`,并使用 SDK 恢复和管理 Ownership 请求状态。
14
+ - 支持宿主 CLI 通过短连接查询、接受和拒绝 Ownership 邀约,并完善本机双身份绑定与共识校验。
15
+ - 修复 Agent 初始化期间 Owner 绑定竞态,并在初始化运行时补充 Owner 授权校验。
16
+ - Owner 状态暂时无法刷新时按暂态故障处理;绑定类敏感操作继续要求严格共识校验并提供刷新诊断。
17
+
18
+ ### 会话权限与交互
19
+
20
+ - 新增当前 EC 会话 Fullaccess 状态查询和开关,主会话与话题会话分别保存授权状态。
21
+ - 增加 Fullaccess 会话授权复核和审计,并支持通过 Menu 管理当前会话设置。
22
+ - 无人值守会话不再代替用户回答交互问题;阻塞式问题等待用户选择期间暂停回合超时计时。
23
+ - 交互卡片可展示模型建议选项,同时保留用户最终选择;拒绝空白回答并支持显式取消。
24
+ - 主动模式熔断后阻止新的工具调用,待当前工具调用收尾后再中断任务。
25
+
26
+ ### 记忆整理
27
+
28
+ - 记忆整理支持按关系空闲时触发,并通过兜底扫描推进待整理任务。
29
+ - 增加整理任务租约恢复和 SDK 超时任务恢复,减少异常中断后任务滞留。
30
+ - 按关系范围拆分记忆泛化任务,并完善整理过程状态与审计上下文。
31
+
32
+ ### 状态与运维
33
+
34
+ - 新增 `ec status --format json` 状态快照,汇总 daemon activity、Agent 用量、Trigger、实时队列和渠道状态。
35
+ - 补充主机、操作系统、硬件和 baseagent 信息;文本状态输出兼容 build ID,并修正 task activity 的 Agent AID 归属。
36
+ - 统一启动与重启流程的自动升级开关,使 `ec start`、`ec restart` 和 restart-monitor 遵循一致策略。
37
+
38
+ ### 错误诊断与安全
39
+
40
+ - 改进受管工具调用诊断和 Codex 命令参数提取,保留原始错误原因并限制递归读取。
41
+ - 修复受控输出过滤器对短选项连写的处理,并禁止 `grep -d` 递归读取。
42
+ - 保留 ecagent 中断失败的底层原因,避免 AUN 错误消息重复展示诊断。
43
+ - 修正 AUN 原始消息 payload 的话题路由传递。
44
+
45
+ ### AUN 与 EC Web
46
+
47
+ - 修正 AUN 私聊出站模式解析,并为托管 AUN 文本、链接、图片、视频、语音和文件消息写入当前任务 ID。
48
+ - EC Web 与 daemon 启动时协商服务协议,并改进 daemon 不可用或协议不兼容时的处理。
49
+ - 将 EC Web Provider 管理和基础设置改由 daemon IPC 服务处理。
50
+ - EC Web 状态页显示实时 Channels,并在受限时提供 EvolAgents 运行汇总。
51
+ - 统一 EC Web 项目路径编码与运行时规则,迁移旧会话项目选择,修复会话页可能显示空白的问题。
52
+ - 修复会话详情自动刷新时的滚动位置跳动。
53
+ - 用量探索器支持按 Agent 或对端筛选,并切换查看 Token 或费用趋势;模型筛选仅展示有数据的模型。
54
+ - 修正 EC Web 未就绪时的日志路径提示。
55
+ - 修复 ecagent 错误或中断时持久化无效 assistant 消息的问题,并增强 OpenAI assistant 数组消息转换。
56
+
57
+ ## 0.0.39 (2026-10-24)
58
+
59
+ ### Ownership 与 Agent 初始化
60
+
61
+ - 支持通过 `ec aid bind status` 查看经 SDK 验证的 Ownership 邀约,并按具体操作 ID 接受或拒绝。
62
+ - 按绑定任务上下文校验审批结果,避免处理不相关或已失效的邀约。
63
+ - Bootstrap 会验证本地 `agent.md`;已验签的名片不再重复发布,无效或未签名内容会阻止不安全的重发。
64
+ - 更新 AUN Owner 欢迎文案,更清晰地介绍 Agent 可提供的帮助与个性化设置。
65
+
66
+ ### 权限与兼容性
67
+
68
+ - 优化 Codex 在 Linux 上的文件权限规则,减少被父目录拒绝规则覆盖的冗余子规则,同时保留必要的读写例外。
69
+ - 将 AUN SDK 最低兼容版本提升至 `0.5.23`,以支持 Ownership 邀约审批。
70
+ - 改进 daemon IPC 连接失败诊断,相关命令现在会保留具体的连接错误信息。
71
+
8
72
  ## 0.0.38 (2026-10-24)
9
73
 
10
74
  ### npm 升级稳定性
package/README.md CHANGED
@@ -20,7 +20,7 @@ EvolCore 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
20
20
  - 🧩 **Agent 多实例**:一个 JSON 文件定义一个 Agent,多 Agent 并发运行,Agent 运行时隔离 + 热重载无需重启
21
21
  - ⏰ **Trigger 自主任务**:Agent 可设置延迟/定时/周期任务,cron 表达式支持,独立 silent session 执行,支持脚本模式和 prompt 模式
22
22
  - 🔧 **Daemon 模块**:系统级后台服务,管理升级检查、全局触发器等跨 Agent 任务
23
- - 🎯 **Agent 启动引导**:首次启动自动引导配置 AID、owners、channels,降低上手门槛
23
+ - 🎯 **Agent 启动引导**:首次启动自动引导配置 AID、owner、channels,降低上手门槛
24
24
  - 🎭 **主动模式策略**:proactive 模式下智能判断响应时机,群聊支持静默策略,避免无效打扰
25
25
  - 🎴 **交互卡片体系**:CommandCard + ActionInteraction,Feishu 与 AUN 统一支持
26
26
 
@@ -157,7 +157,7 @@ ec init aun
157
157
  - 渠道选择(飞书 / 微信 / 钉钉 / QQ 频道 / 企业微信 / AUN)并扫码登录
158
158
  - 默认项目路径
159
159
  - 模型选择(sonnet/opus/haiku)
160
- - owner 配置:默认走 **Evol App 扫码绑定**,若无 Evol App 可手工录入 AID 兜底。扫码流程只追加 owner;移除 owner 请直接编辑对应 JSON 配置。
160
+ - AUN 协议 Owner 绑定:使用统一命令 `ec aid bind <ownerAid> [--subject <subjectAid>]`;托管会话默认当前 Agent,独立 CLI 默认 daemon Control AID。Owner 通过其 SDK 明确审批,Evol App 绑定留待二期。协议 Owner 不从 daemon/Agent 配置读取或回写;非 AUN 渠道配置保持现状。
161
161
  - 自动写入 `EVOLCORE_HOME` 到 shell profile(Unix)或用户环境变量(Windows)
162
162
 
163
163
  配置文件生成在 `{EVOLCORE_HOME}/data/daemon.json`(默认 `~/.evolcore/data/daemon.json`)。
@@ -234,8 +234,11 @@ Claude、Codex 或 EC Agent 兼容网关需要额外请求头或查询参数时
234
234
  ### Baseagent 模型与备用链
235
235
 
236
236
  Session Renew 的辅助模型配置与主模型、推理强度放在同一个
237
- `baseagents.<baseagent>` 块中,与 `responseMode` 正交。主模型故障备用链
238
- `fallbackModels` 只在 `agents/defaults.json` 中配置:
237
+ `baseagents.<baseagent>` 块中,与 `responseMode` 正交。主模型故障备用链使用 `fallback`,
238
+ 只在 `agents/defaults.json` 中配置;每个候选可单独指定 Provider,省略时沿用主模型 Provider:
239
+
240
+ 示例中的 Provider ID 需先在 `providers.json` 定义;备用 Provider 必须提供与主模型相同的
241
+ `serviceType`,例如 `backup-codex` 提供 `codex` 服务、`backup-text` 提供 `text` 服务。
239
242
 
240
243
  ```jsonc
241
244
  {
@@ -245,18 +248,28 @@ Session Renew 的辅助模型配置与主模型、推理强度放在同一个
245
248
  "effort": "medium",
246
249
  "auxiliaryModel": "claude-haiku-4-5-20251001",
247
250
  "auxiliaryEffort": "low",
248
- "fallbackModels": [
251
+ "fallback": [
249
252
  { "model": "claude-sonnet-4-5", "effort": "medium" },
250
253
  { "model": "claude-haiku-4-5", "effort": "low" }
251
254
  ]
252
255
  },
253
256
  "codex": {
257
+ "provider": "company-codex",
254
258
  "model": "gpt-5.6",
255
259
  "effort": "high",
256
260
  "auxiliaryModel": "gpt-5.6-sol",
257
261
  "auxiliaryEffort": "medium",
258
- "fallbackModels": [
259
- { "model": "gpt-5.4", "effort": "medium" }
262
+ "fallback": [
263
+ { "provider": "backup-codex", "model": "gpt-5.4", "effort": "medium" }
264
+ ]
265
+ }
266
+ },
267
+ "Services": {
268
+ "auxiliary": {
269
+ "provider": "company-text",
270
+ "model": "text-primary",
271
+ "fallback": [
272
+ { "provider": "backup-text", "model": "text-backup" }
260
273
  ]
261
274
  }
262
275
  }
@@ -264,7 +277,10 @@ Session Renew 的辅助模型配置与主模型、推理强度放在同一个
264
277
  ```
265
278
 
266
279
  `auxiliaryEffort` 必须与同一 baseagent 块中的 `auxiliaryModel` 一起配置;未配置辅助模型时,
267
- Session Renew 按 `session_renew.fallback_action` 处理。`fallbackModels` 不会被辅助推理使用。
280
+ Session Renew 按 `session_renew.fallback_action` 处理。`baseagents.<baseagent>.fallback` 只用于
281
+ 该 Baseagent 的主模型,不会被 Session Renew 使用。`Services.<purpose>.fallback` 则只用于该用途;
282
+ 备用项可跨 Provider,但必须使用相同的 `serviceType`。例如 `Services.auxiliary` 使用 Provider
283
+ 的 `services.text` 模型目录,Provider 不配置 `services.auxiliary`。
268
284
 
269
285
  升级旧版本时,daemon 启动前会自动把模式桶中的辅助字段迁移到对应的 baseagent 块并创建备份。
270
286
  迁移逻辑集中在 `src/config/schema-migration.ts`,运行时不会回退读取旧字段。
@@ -413,21 +429,16 @@ evolcore/
413
429
 
414
430
  ### ⚠️ 进程级 menu 操作鉴权(v3.2 Breaking)
415
431
 
416
- 进程级 menu 操作(`/system restart/upgrade`、`/agent` agent 生命周期管理)的鉴权已迁移到
417
- `daemon.json` 顶层 `owners` 字段(v3.2 起,不再读 `agents/defaults.json`)。
418
- 升级后**必须**在 `daemon.json` 配置 `owners`,否则这些操作一律返回 `FORBIDDEN`(daemon 启动时也会 warn 提示)。
432
+ 进程级管理操作的 Owner 权限来自 Control AID 的 Ownership 2.1 新鲜共识,不使用 daemon 配置中的手工 `owner/owners[]`。
419
433
 
420
- ```json
421
- {
422
- "owners": ["eleans-2022.agentid.pub"]
423
- }
434
+ ```bash
435
+ ec aid bind <ownerAid> # 默认绑定 daemon Control AID
436
+ ec aid bind status # 核验 Owner 共识和运行态同步
437
+ ec aid bind <ownerAid> --subject <aid> # 指定本机 Agent AID
424
438
  ```
425
439
 
426
- `ec init` 交互流程会在生成控制 AID 后提示录入 owners(可跳过后手动编辑)。
427
-
428
- - **`owners`**:进程级管理者 AID 名单。可执行 `/system`(重启/升级)与 `/agent`
429
- (create / delete / enable / disable / list / show)。
430
- - 关系级的 `/trigger`(set / cancel / update / list)仍走 channel 角色(owner/admin)+ scoped 鉴权,**不**受 `owners` 影响。
440
+ 托管 Agent 会话默认以当前 `selfAid` 为绑定主体;每个 AID 的 Owner 独立核验,daemon 权限不自动继承给 Agent。`unavailable`、断线或过期共识不会授予 Owner 权限。Evol App 绑定与非 AUN 渠道的 owner 配置不在一期范围。
441
+ - 关系级的 `/trigger`(set / cancel / update / list)仍走 channel 角色(owner/admin)+ scoped 鉴权,**不**受 daemon `owner` 影响。
431
442
  - `/agent create` 为「受理即返回」:立即回 `{ accepted: true, aid }`,后台跑完整创建流程并把各
432
443
  环节写入 `agents/<aid>/create-status.json`;客户端用 `menu.query name=agent args={aid}` 轮询
433
444
  `createProgress.status` 直到 `ready` / `failed`。
@@ -36,7 +36,9 @@ const STDOUT_FILTER_SPECS = new Map([
36
36
  // `grep` takes either `-e`/`--regexp` or one leading pattern operand. With an
37
37
  // explicit pattern option the operand budget is zero, because a bare operand
38
38
  // would be a file to read. `-r`, `-R` and `-f` are absent, so recursion and
39
- // pattern files stay out.
39
+ // pattern files stay out. `-d recurse` is the same recursion as `-r`, so
40
+ // `-d`/`--directories` are absent too, along with `--include`/`--exclude`/
41
+ // `--exclude-dir`, which only act on recursion.
40
42
  ['grep', {
41
43
  flags: new Set([
42
44
  '-i', '--ignore-case', '-v', '--invert-match', '-c', '--count',
@@ -50,7 +52,7 @@ const STDOUT_FILTER_SPECS = new Map([
50
52
  valueOptions: new Set([
51
53
  '-m', '--max-count', '-A', '--after-context', '-B', '--before-context',
52
54
  '-C', '--context', '--color', '--colour', '--binary-files', '--label',
53
- '--group-separator', '-d', '--directories', '--include', '--exclude', '--exclude-dir',
55
+ '--group-separator',
54
56
  ]),
55
57
  patternValueOptions: new Set(['-e', '--regexp']),
56
58
  maxOperands: 1,
@@ -253,6 +255,16 @@ function resolveFilterArgv(filterArgv) {
253
255
  continue;
254
256
  }
255
257
  if (spec.numericShorthand && /^-\d+$/.test(arg)) continue;
258
+ // getopt reads the rest of a short option token as its value (`-A2`,
259
+ // `-n30`, `-ePATTERN`). Only the leading letter counts; `-iA2` is rejected.
260
+ if (!arg.startsWith('--') && arg.length > 2) {
261
+ const shortOption = arg.slice(0, 2);
262
+ if (spec.patternValueOptions?.has(shortOption)) {
263
+ sawPatternValue = true;
264
+ continue;
265
+ }
266
+ if (spec.valueOptions.has(shortOption)) continue;
267
+ }
256
268
  const option = arg.split('=', 1)[0];
257
269
  if (spec.patternValueOptions?.has(option)) {
258
270
  sawPatternValue = true;
@@ -119,6 +119,7 @@ export function buildBaseagentSnapshot(config, detections, loaded) {
119
119
  model: text(blocks.get(id)?.model),
120
120
  effort: text(blocks.get(id)?.effort ?? blocks.get(id)?.reasoning),
121
121
  version: probes.get(id)?.version ?? null,
122
+ detectionSource: probes.get(id)?.detectionSource ?? [],
122
123
  })),
123
124
  };
124
125
  }
@@ -941,19 +941,8 @@ export class AgentRunner {
941
941
  const questions = input.questions;
942
942
  // 没有交互上下文(无渠道适配器),回退到纯文本
943
943
  const permCtx = this.permissionContexts.get(sessionId);
944
- if (usesUnattendedInteractions(permCtx)) {
945
- logger.info(`[AgentRunner] interaction.auto_resolved kind=ask_user session=${sessionId}`);
946
- const unattendedAnswer = '当前为无人值守 Trigger,请基于已有上下文选择最安全合理的方案并继续执行。';
947
- const answers = {};
948
- for (const question of questions ?? []) {
949
- answers[question.question] = question.multiSelect ? [unattendedAnswer] : unattendedAnswer;
950
- }
951
- return {
952
- behavior: 'allow',
953
- updatedInput: { ...input, answers },
954
- decisionClassification: 'user_temporary',
955
- };
956
- }
944
+ if (usesUnattendedInteractions(permCtx))
945
+ throw new Error('无人值守任务不能自动回答用户问题');
957
946
  if (!permCtx?.adapter || !permCtx?.channelId) {
958
947
  return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
959
948
  }
@@ -990,11 +979,15 @@ export class AgentRunner {
990
979
  buttons: [
991
980
  { key: 'submit', label: '✅ 确认选择', style: 'primary' },
992
981
  ],
982
+ ...(typeof q.recommendedOption === 'string'
983
+ ? { recommendedOption: q.options.find(opt => opt.label === q.recommendedOption)?.label }
984
+ : {}),
993
985
  allowCustomInput: true,
994
986
  },
995
987
  channelId: permCtx.channelId,
996
988
  sessionId,
997
989
  initiatorId: permCtx.userId,
990
+ blocking: true,
998
991
  };
999
992
  }
1000
993
  else {
@@ -1018,11 +1011,15 @@ export class AgentRunner {
1018
1011
  label: opt.label,
1019
1012
  style: 'default',
1020
1013
  })),
1014
+ ...(typeof q.recommendedOption === 'string'
1015
+ ? { recommendedOption: q.options.find(opt => opt.label === q.recommendedOption)?.label }
1016
+ : {}),
1021
1017
  allowCustomInput: true,
1022
1018
  },
1023
1019
  channelId: permCtx.channelId,
1024
1020
  sessionId,
1025
1021
  initiatorId: permCtx.userId,
1022
+ blocking: true,
1026
1023
  };
1027
1024
  }
1028
1025
  let cardSent = false;
@@ -1048,14 +1045,7 @@ export class AgentRunner {
1048
1045
  logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
1049
1046
  }
1050
1047
  if (!cardSent) {
1051
- await permCtx.flushPending?.();
1052
- const firstLabel = q.options[0]?.label || '';
1053
- answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
1054
- if (sendPrompt) {
1055
- const optText = q.options.map((o, idx) => ` ${idx + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
1056
- await sendPrompt(`💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optText}\n → 自动选择:${firstLabel}`);
1057
- }
1058
- continue;
1048
+ throw new Error(`AskUserQuestion 卡片投递失败:${q.question}`);
1059
1049
  }
1060
1050
  try {
1061
1051
  await permCtx.turn?.onInteractionOpen?.({
@@ -1063,7 +1053,6 @@ export class AgentRunner {
1063
1053
  kind: 'ask_user',
1064
1054
  toolUseId: options.toolUseID,
1065
1055
  requestId: options.requestId,
1066
- expiresAt: Date.now() + 20 * 60 * 1000,
1067
1056
  });
1068
1057
  }
1069
1058
  catch (error) {
@@ -1091,7 +1080,10 @@ export class AgentRunner {
1091
1080
  }
1092
1081
  else if (action === '_custom_input') {
1093
1082
  const customText = values?.custom_text;
1094
- finish({ kind: 'answered', value: typeof customText === 'string' && customText.trim() ? customText.trim() : null });
1083
+ const trimmed = typeof customText === 'string' ? customText.trim() : '';
1084
+ if (!trimmed)
1085
+ return false;
1086
+ finish({ kind: 'answered', value: trimmed });
1095
1087
  }
1096
1088
  else if (action === 'submit' && q.multiSelect && values) {
1097
1089
  const selected = [];
@@ -1099,15 +1091,18 @@ export class AgentRunner {
1099
1091
  if (values[`opt_${idx}`] === true)
1100
1092
  selected.push(opt.label);
1101
1093
  });
1102
- finish({ kind: 'answered', value: selected.length > 0 ? selected : null });
1094
+ if (selected.length === 0)
1095
+ return false;
1096
+ finish({ kind: 'answered', value: selected });
1103
1097
  }
1104
1098
  else {
1105
1099
  finish({ kind: 'answered', value: action });
1106
1100
  }
1101
+ return true;
1107
1102
  }, {
1108
1103
  initiatorId: permCtx.userId,
1109
1104
  fallbackCommand: 'ask',
1110
- timeoutMs: 20 * 60 * 1000,
1105
+ // User questions wait until an explicit answer or cancellation.
1111
1106
  onCancel: async (reason) => {
1112
1107
  finish({ kind: 'cancelled', reason });
1113
1108
  await cancellationClosed;
@@ -1176,6 +1171,7 @@ export class AgentRunner {
1176
1171
  channelId: permCtx.channelId || '',
1177
1172
  sessionId,
1178
1173
  initiatorId: permCtx.userId,
1174
+ blocking: true,
1179
1175
  kind: {
1180
1176
  kind: 'action',
1181
1177
  title: `💬 ${q.question}`,
@@ -1198,7 +1194,6 @@ export class AgentRunner {
1198
1194
  kind: 'ask_user',
1199
1195
  toolUseId: options.toolUseID,
1200
1196
  requestId: options.requestId,
1201
- expiresAt: Date.now() + 20 * 60 * 1000,
1202
1197
  });
1203
1198
  }
1204
1199
  catch (error) {
@@ -1221,6 +1216,8 @@ export class AgentRunner {
1221
1216
  };
1222
1217
  permCtx.interactionRouter.register(requestId, sessionId, (action) => {
1223
1218
  const answer = action.trim();
1219
+ if (!answer)
1220
+ return false;
1224
1221
  const selectionParts = answer.split(/[,,]/).map(part => part.trim());
1225
1222
  const selectedIndexes = selectionParts.map(part => Number(part));
1226
1223
  const hasValidNumericSelection = selectionParts.length > 0
@@ -1240,10 +1237,10 @@ export class AgentRunner {
1240
1237
  else {
1241
1238
  finish({ kind: 'answered', value: answer });
1242
1239
  }
1240
+ return true;
1243
1241
  }, {
1244
1242
  initiatorId: permCtx.userId,
1245
1243
  fallbackCommand: 'ask',
1246
- timeoutMs: 20 * 60 * 1000,
1247
1244
  onCancel: async (reason) => {
1248
1245
  finish({ kind: 'cancelled', reason });
1249
1246
  await cancellationClosed;
@@ -1283,12 +1280,7 @@ export class AgentRunner {
1283
1280
  }
1284
1281
  }
1285
1282
  else {
1286
- const firstLabel = q.options[0]?.label || '';
1287
- answers[q.question] = firstLabel;
1288
- if (sendPrompt) {
1289
- const optText = q.options.map((o, i) => ` ${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
1290
- await sendPrompt(`💬 ${q.question}\n${optText}\n\n → 自动选择:${firstLabel}`);
1291
- }
1283
+ throw new Error(`AskUserQuestion 没有可用的交互路由:${q.question}`);
1292
1284
  }
1293
1285
  }
1294
1286
  }
@@ -1300,17 +1292,14 @@ export class AgentRunner {
1300
1292
  */
1301
1293
  async handleExitPlanMode(sessionId, input, options) {
1302
1294
  const permCtx = this.permissionContexts.get(sessionId);
1303
- if (usesUnattendedInteractions(permCtx)) {
1304
- logger.info(`[AgentRunner] interaction.auto_resolved kind=plan_approval session=${sessionId}`);
1305
- return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
1306
- }
1295
+ if (usesUnattendedInteractions(permCtx))
1296
+ throw new Error('无人值守任务不能自动批准计划');
1307
1297
  const sendPrompt = permCtx?.adapter && permCtx?.channelId
1308
1298
  ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
1309
1299
  : this.sendPromptFn;
1310
1300
  // 无任何交互能力,直接 allow
1311
- if (!permCtx?.channelId || !sendPrompt) {
1312
- return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
1313
- }
1301
+ if (!permCtx?.channelId || !sendPrompt)
1302
+ throw new Error('计划审批没有可用的交互路由');
1314
1303
  // 立即暂停 idle 监控,不等卡片发完再 register
1315
1304
  permCtx.interactionRouter?.markWaiting(sessionId);
1316
1305
  const waitForPlanDecision = async (requestId) => {
@@ -1321,7 +1310,6 @@ export class AgentRunner {
1321
1310
  kind: 'plan_approval',
1322
1311
  toolUseId: options.toolUseID,
1323
1312
  requestId: options.requestId,
1324
- expiresAt: Date.now() + 20 * 60 * 1000,
1325
1313
  });
1326
1314
  }
1327
1315
  catch (error) {
@@ -1369,7 +1357,6 @@ export class AgentRunner {
1369
1357
  }, {
1370
1358
  initiatorId: permCtx.userId,
1371
1359
  fallbackCommand: 'ask',
1372
- timeoutMs: 20 * 60 * 1000,
1373
1360
  onCancel: async (reason) => {
1374
1361
  finish({ kind: 'cancelled', reason });
1375
1362
  await cancellationClosed;
@@ -1446,6 +1433,7 @@ export class AgentRunner {
1446
1433
  channelId: permCtx.channelId,
1447
1434
  sessionId,
1448
1435
  initiatorId: permCtx.userId,
1436
+ blocking: true,
1449
1437
  fallback: {
1450
1438
  command: 'ask',
1451
1439
  buttonArgMap: { approve: '1', reject: '2' },
@@ -1482,6 +1470,7 @@ export class AgentRunner {
1482
1470
  channelId: permCtx.channelId || '',
1483
1471
  sessionId,
1484
1472
  initiatorId: permCtx.userId,
1473
+ blocking: true,
1485
1474
  kind: {
1486
1475
  kind: 'action',
1487
1476
  title: '📋 计划审批',
@@ -1501,11 +1490,8 @@ export class AgentRunner {
1501
1490
  permCtx.interactionRouter.unmarkWaiting(sessionId);
1502
1491
  return await waitForPlanDecision(fallbackRequestId);
1503
1492
  }
1504
- // 无交互能力,发提示后直接 allow
1505
1493
  permCtx?.interactionRouter?.unmarkWaiting(sessionId);
1506
- await permCtx.flushPending?.();
1507
- await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
1508
- return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
1494
+ throw new Error('计划审批没有可用的交互路由,已拒绝自动批准');
1509
1495
  }
1510
1496
  /**
1511
1497
  * SDK 原始事件 → 标准 AgentEvent 转换
@@ -6,7 +6,7 @@ import { requestDangerousCommandPermission } from '../core/permission/approval-g
6
6
  import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
7
7
  import { isShellToolName } from '../core/permission/tool-canonical.js';
8
8
  import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
9
- import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, getExistingRelationAttachmentDirs, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
9
+ import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, compactCodexLinuxFilesystemRules, getExistingRelationAttachmentDirs, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
10
10
  import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
11
11
  import { resolveOpenaiConfig, resolveCodexCliPath, resolveCodexPowerShellShimPath, } from './baseagent.js';
12
12
  import { logger } from '../utils/logger.js';
@@ -781,8 +781,9 @@ export class CodexRunner {
781
781
  ...(expectedCredentialToken !== undefined ? { expectedCredentialToken } : {}),
782
782
  };
783
783
  const classification = classifyEvolcoreShellCommand(command, parseOptions);
784
- if (classification.kind === 'bounded-output')
785
- return { argv: classification.command.argv };
784
+ if (classification.kind === 'bounded-output') {
785
+ return { argv: canonicalizeEvolcoreCommandArgv(classification.command.argv) };
786
+ }
786
787
  // Keep the approval/delegation parser aligned with the preflight parser:
787
788
  // the session-managed `$TMPDIR` token is a permitted EC path reference.
788
789
  const parsed = executable
@@ -913,7 +914,20 @@ export class CodexRunner {
913
914
  allowManagedTmpDir: true,
914
915
  ...(managedTempDir ? { managedTempDir } : {}),
915
916
  });
916
- return classification.kind === 'composite' ? { parseFailure: outer } : {};
917
+ if (classification.kind === 'bounded-output'
918
+ && normalizeEvolcoreCommandArgv(classification.command.argv)[0] === 'ec') {
919
+ const argv = classification.command.argv;
920
+ return containsLiteralManagedTmpDirOutsideSendContent(argv) || containsUnresolvedManagedTmpDirReference(argv)
921
+ ? { parseFailure: {
922
+ issue: 'unsafe-expansion',
923
+ offset: 0,
924
+ tokenIndex: 0,
925
+ dialect: 'posix',
926
+ inputLength: command.length,
927
+ } }
928
+ : { argv: canonicalizeEvolcoreCommandArgv(argv) };
929
+ }
930
+ return { parseFailure: outer };
917
931
  }
918
932
  return {};
919
933
  }
@@ -1428,7 +1442,7 @@ export class CodexRunner {
1428
1442
  onlyOtherAgents: true,
1429
1443
  }))
1430
1444
  relationAttachmentDenies[directory] = 'deny';
1431
- const filesystem = {
1445
+ const filesystem = compactCodexLinuxFilesystemRules({
1432
1446
  ...buildCodexProtectedFilesystemRules(resolveRoot(), {
1433
1447
  selfAid: this.resolvedConfig.agentAid,
1434
1448
  denyLClassRead: mode === 'readonly',
@@ -1455,7 +1469,7 @@ export class CodexRunner {
1455
1469
  [path.join(managedTempDir, 'locks')]: 'write',
1456
1470
  [path.join(managedTempDir, 'locks') + '/**']: 'write',
1457
1471
  } : {}),
1458
- };
1472
+ });
1459
1473
  // Codex resolves this complete profile before serializing the Windows
1460
1474
  // helper payload. Check after all runner-owned additions, not merely the
1461
1475
  // protected-path base projection, so Git and managed-temp rules cannot
@@ -1736,6 +1750,8 @@ export class CodexRunner {
1736
1750
  const policyCode = result.policyCode ?? 'session_policy_hook';
1737
1751
  const reason = result.reason ?? 'session policy denied this tool call';
1738
1752
  await recordPreToolUseDenial(policyCode, reason);
1753
+ if (policyCode === 'proactive_circuit_break')
1754
+ context.onProactiveCircuitBreak?.();
1739
1755
  return { ok: true, applicable: true, decision: 'deny', reason };
1740
1756
  }
1741
1757
  return { ok: true, applicable: true, decision: 'allow' };
@@ -3373,6 +3389,8 @@ export class CodexRunner {
3373
3389
  `reason=${policyResult.reason ?? 'policy denied'} executed=false`);
3374
3390
  const policyCode = policyResult.policyCode ?? 'session_policy_hook';
3375
3391
  const policyReason = policyResult.reason ?? 'session policy denied approval';
3392
+ if (policyCode === 'proactive_circuit_break')
3393
+ permissionContext?.onProactiveCircuitBreak?.();
3376
3394
  this.rememberToolDenial(request, params, {
3377
3395
  decision: 'deny',
3378
3396
  decisionSource: 'policy',
@@ -3857,25 +3875,9 @@ export class CodexRunner {
3857
3875
  }
3858
3876
  async collectUserInputAnswer(sessionKey, question) {
3859
3877
  const options = Array.isArray(question.options) ? question.options : [];
3860
- const fallback = options[0]?.label ? [String(options[0].label)] : [''];
3861
3878
  const context = this.permissionContexts.get(sessionKey);
3862
3879
  if (context?.approvalInteractionPolicy === 'deny') {
3863
- logger.info(`[CodexRunner] interaction.auto_resolved kind=ask_user session=${sessionKey}`);
3864
- const labels = options
3865
- .map((option) => typeof option?.label === 'string' ? option.label.trim() : '')
3866
- .filter(Boolean);
3867
- if (labels.length > 0) {
3868
- const prompt = `${String(question.header ?? '')} ${String(question.question ?? '')}`;
3869
- const isPlanApproval = /\bplan\b|计划|方案/i.test(prompt);
3870
- const affirmative = isPlanApproval
3871
- ? labels.find(label => {
3872
- const kind = this.classifyMcpApprovalLabel(label);
3873
- return kind === 'allow_once' || kind === 'allow_persistent';
3874
- })
3875
- : undefined;
3876
- return [affirmative ?? labels[0]];
3877
- }
3878
- return ['当前为无人值守 Trigger,请基于已有上下文选择最安全合理的方案并继续执行。'];
3880
+ throw new Error('无人值守任务不能自动回答用户问题');
3879
3881
  }
3880
3882
  const canFreeText = question.isOther !== false || options.length === 0;
3881
3883
  const sendPrompt = context?.adapter && context.channelId
@@ -3887,8 +3889,8 @@ export class CodexRunner {
3887
3889
  : this.sendPromptFn;
3888
3890
  if (!context?.interactionRouter || !sendPrompt) {
3889
3891
  if (sendPrompt)
3890
- await sendPrompt(this.formatUserInputFallback(question, fallback));
3891
- return fallback;
3892
+ await sendPrompt(this.formatUserInputFallback(question));
3893
+ throw new Error('用户问题没有可用的交互路由,已拒绝自动选择');
3892
3894
  }
3893
3895
  const requestId = `codex-ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3894
3896
  const buttonArgMap = {};
@@ -3914,6 +3916,9 @@ export class CodexRunner {
3914
3916
  title: String(question.header || '问题'),
3915
3917
  body: bodyLines.join('\n'),
3916
3918
  buttons,
3919
+ ...(typeof question.recommendedOption === 'string'
3920
+ ? { recommendedOption: buttons.find(button => button.label === question.recommendedOption)?.key }
3921
+ : {}),
3917
3922
  allowCustomInput: canFreeText,
3918
3923
  },
3919
3924
  fallback: {
@@ -3951,14 +3956,19 @@ export class CodexRunner {
3951
3956
  }
3952
3957
  if (!sent) {
3953
3958
  router.unmarkWaiting(sessionKey);
3954
- return fallback;
3959
+ throw new Error('用户问题卡片投递失败,已拒绝自动选择');
3955
3960
  }
3956
3961
  return new Promise((resolve) => {
3957
3962
  router.register(requestId, sessionKey, (action, values) => {
3958
- resolve(this.parseUserInputAction(action, values, options, fallback));
3963
+ const answer = this.parseUserInputAction(action, values, options, []);
3964
+ if (answer.length === 0 || answer.every(item => !item.trim()))
3965
+ return false;
3966
+ resolve(answer);
3967
+ return true;
3959
3968
  }, {
3960
3969
  initiatorId: context.userId,
3961
3970
  fallbackCommand: 'ask',
3971
+ retainOnRejected: true,
3962
3972
  });
3963
3973
  if (waitMarked) {
3964
3974
  router.unmarkWaiting(sessionKey);
@@ -3986,13 +3996,13 @@ export class CodexRunner {
3986
3996
  }
3987
3997
  return action.trim() ? [action.trim()] : fallback;
3988
3998
  }
3989
- formatUserInputFallback(question, fallback) {
3999
+ formatUserInputFallback(question) {
3990
4000
  const options = Array.isArray(question.options) ? question.options : [];
3991
4001
  const lines = [String(question.header || '问题'), String(question.question || '')].filter(Boolean);
3992
4002
  if (options.length > 0) {
3993
4003
  lines.push('', ...options.map((option, index) => `${index + 1}. ${String(option.label || `选项 ${index + 1}`)}${option.description ? ` — ${option.description}` : ''}`));
3994
4004
  }
3995
- lines.push('', `自动选择:${fallback.join(', ')}`);
4005
+ lines.push('', '等待用户选择后继续。');
3996
4006
  return lines.join('\n');
3997
4007
  }
3998
4008
  findSessionKeyByThread(threadId, conversationId) {
@@ -5402,10 +5412,24 @@ export class CodexRunner {
5402
5412
  const isError = exitCode !== undefined
5403
5413
  ? exitCode !== 0
5404
5414
  : item.status === 'failed' || item.status === 'declined';
5415
+ const rawOutput = item.aggregatedOutput ?? '';
5416
+ const commandStr = Array.isArray(item.command)
5417
+ ? item.command.join(' ')
5418
+ : typeof item.command === 'string' ? item.command : '';
5419
+ const isApplyPatchFailure = isError
5420
+ && /apply_patch/i.test(commandStr)
5421
+ && /The last line of the patch must be|Invalid patch/i.test(rawOutput);
5422
+ const isApplyPatchArgFailure = isError
5423
+ && /--codex-run-as-apply-patch requires a UTF-8 PATCH argument/i.test(rawOutput);
5424
+ const applyPatchHint = isApplyPatchFailure
5425
+ ? '\n[EvolCore 提示] apply_patch 在 Windows 上通过 .bat shim 调用时会截断多行参数,补丁末尾 *** End Patch 被丢弃。正确做法:改用 FileChange 工具直接写文件,或绕过 .bat 直接调用 codex.exe --codex-run-as-apply-patch。不要继续用 here-string 变体重试。'
5426
+ : isApplyPatchArgFailure
5427
+ ? '\n[EvolCore 提示] --codex-run-as-apply-patch 要求补丁内容通过命令行参数传入,不接受 PowerShell here-string 直接输出到 stdout 的方式。正确做法:改用 FileChange 工具直接写文件。'
5428
+ : '';
5405
5429
  yield {
5406
5430
  type: 'tool_result',
5407
5431
  name: 'Shell',
5408
- result: item.aggregatedOutput ?? '',
5432
+ result: rawOutput + applyPatchHint,
5409
5433
  ...(item.aggregatedOutput != null ? { outputKind: 'combined' } : {}),
5410
5434
  ...(blockedFields ?? {
5411
5435
  isError,