evolcore 0.0.32 → 0.0.34

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 (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
package/dist/paths.js CHANGED
@@ -208,6 +208,26 @@ export function channelStatePath(channelKey, ...parts) {
208
208
  export function agentTriggersDir(aid) {
209
209
  return path.join(agentDir(aid), 'triggers');
210
210
  }
211
+ /** 话题数据的统一根目录:索引、摘要、续读锚点、跨会话收件箱都收在这里。 */
212
+ export function agentThreadsDir(aid) {
213
+ return path.join(agentDataDir(aid), 'threads');
214
+ }
215
+ export function agentThreadIndexPath(aid) {
216
+ return path.join(agentThreadsDir(aid), 'index.json');
217
+ }
218
+ export function agentThreadSummariesDir(aid) {
219
+ return path.join(agentThreadsDir(aid), 'summaries');
220
+ }
221
+ export function agentThreadSummaryPath(aid, sessionId) {
222
+ return path.join(agentThreadSummariesDir(aid), `${sessionId}.summary.jsonl`);
223
+ }
224
+ /** Thread Context 收件箱:跨话题发送后在目标会话侧留下的注入提醒。 */
225
+ export function agentThreadInboxDir(aid) {
226
+ return path.join(agentThreadsDir(aid), 'inbox');
227
+ }
228
+ export function agentThreadInboxPath(aid, targetSessionId) {
229
+ return path.join(agentThreadInboxDir(aid), `${targetSessionId}.jsonl`);
230
+ }
211
231
  function stripTrailingWindowsSeparators(value) {
212
232
  const stripped = value.replace(/[\\/]+$/g, '');
213
233
  // Keep a drive-root separator. `C:` is a drive-relative path on Windows.
@@ -414,8 +434,81 @@ export function kitsSchemasDir() {
414
434
  export function eckDebugDir() {
415
435
  return path.join(resolveRoot(), 'data', 'eck-debug');
416
436
  }
437
+ export function debugSessionsDir() {
438
+ return path.join(resolveRoot(), 'data', 'debug-sessions');
439
+ }
417
440
  export function getPackageRoot() {
418
441
  // import.meta.dirname is available in Node.js 21.2+ and always returns
419
442
  // the correct OS-native path, regardless of Git Bash or MSYS2 environment.
420
443
  return path.resolve(import.meta.dirname, '..');
421
444
  }
445
+ // ── FileIntegrityStore 路径 ──
446
+ export function integrityDir() {
447
+ return path.join(resolveRoot(), 'data', 'integrity');
448
+ }
449
+ export function integrityDbPath() {
450
+ return path.join(integrityDir(), 'file-integrity.db');
451
+ }
452
+ export function integritySnapshotDir() {
453
+ return path.join(integrityDir(), 'snapshots');
454
+ }
455
+ /**
456
+ * 判断当前是否为开发仓运行模式。
457
+ * 与 ec start 显示的安装类型保持口径一致:包路径包含 node_modules 则为安装模式,否则为开发仓。
458
+ * 调用方据此决定是否跳过 FileIntegrityStore 的 verify 调用。
459
+ *
460
+ * 唯一覆盖来源是 `ec start --as-mode` 写下的一次性文件(见 asModeOverridePath),
461
+ * 由 daemon 启动时读取并立即删除。**不读环境变量**——环境变量会被 restart 之类的
462
+ * 路径静默继承,导致模式在操作者不知情的情况下改变。
463
+ */
464
+ export function isDevMode() {
465
+ if (!_asModeResolved) {
466
+ _asModeResolved = true;
467
+ _asModeOverride = consumeAsModeOverride();
468
+ }
469
+ if (_asModeOverride)
470
+ return _asModeOverride === 'dev';
471
+ return !getPackageRoot().includes('node_modules');
472
+ }
473
+ /**
474
+ * `ec start --as-mode <mode>` 的一次性覆盖文件。
475
+ *
476
+ * 走文件而不是环境变量:环境变量会被 `ec restart`(全量继承 process.env)
477
+ * 静默带进新 daemon,操作者不重启终端就撤不掉。文件由 CLI 在启动前写入,
478
+ * daemon 首次调用 isDevMode() 时读走并删除,只影响这一次启动。
479
+ */
480
+ export function asModeOverridePath() {
481
+ return path.join(resolveRoot(), 'data', 'daemon', 'as-mode.override');
482
+ }
483
+ /**
484
+ * 本次进程的覆盖值。**必须缓存**:文件读一次就删,若每次都去读,
485
+ * 第二次调用起就会悄悄退回按包路径判断——模式在进程运行中途改变是最坏的情况。
486
+ */
487
+ let _asModeResolved = false;
488
+ let _asModeOverride = null;
489
+ /** 仅供测试:清掉缓存,让下一次 isDevMode() 重新读覆盖文件。 */
490
+ export function _resetAsModeCache() {
491
+ _asModeResolved = false;
492
+ _asModeOverride = null;
493
+ }
494
+ /**
495
+ * 读出并删除覆盖文件(一次性)。返回 null 表示没有覆盖。
496
+ * 只应由 daemon 启动流程(或 isDevMode 的首次求值)调用一次。
497
+ */
498
+ export function consumeAsModeOverride() {
499
+ const file = asModeOverridePath();
500
+ if (!fs.existsSync(file))
501
+ return null;
502
+ let value = null;
503
+ try {
504
+ value = fs.readFileSync(file, 'utf-8').trim();
505
+ }
506
+ catch {
507
+ value = null;
508
+ }
509
+ try {
510
+ fs.rmSync(file, { force: true });
511
+ }
512
+ catch { /* ignore */ }
513
+ return value === 'dev' || value === 'install' ? value : null;
514
+ }
@@ -12,20 +12,18 @@ const STATE_KEY = 'proactive';
12
12
  const SNAPSHOT_KEY = 'proactive:continuation';
13
13
  const OWNER_KEY = 'proactive:owner';
14
14
  const SNAPSHOT_MAX_AGE_MS = 30 * 60 * 1_000;
15
- const TOOL_REPORT_INTERVAL = 20;
15
+ /** 计数器初值与提示档位:计数 = 该值时放行并提示,> 该值时只放行意图表达 */
16
+ export const INTENT_GATE_INTERVAL = 15;
17
+ const TOOL_REPORT_INTERVAL = INTENT_GATE_INTERVAL;
18
+ /** 队列未读提醒长度跨轮次持久化的 sessionState key(问题3修复) */
19
+ const QUEUE_REMINDER_SESSION_KEY = 'proactive:lastQueueReminderLen';
20
+ /** 熔断已触发标记,存 sessionState 供 afterProcess 发渠道消息(F5修复) */
21
+ const CIRCUIT_BREAK_SESSION_KEY = 'proactive:circuitBreak';
22
+ /** 单轮内允许的最大违规次数;超过后触发熔断而非继续注入提醒(问题1/2修复) */
23
+ export const VIOLATION_CIRCUIT_BREAK_THRESHOLD = 2;
16
24
  const SEND_RECEIPT_RE = /["']?(?:message[_ -]?id|handoff[_ -]?id)["']?\s*[:=]\s*["']?([A-Za-z0-9][A-Za-z0-9._:-]*)/gi;
17
25
  const HANDOFF_TEXT_RECEIPT_RE = /\bhandoff\s+(h-[A-Za-z0-9._-]+)/i;
18
26
  const RETURN_CONTENT_RE = /["']?(?:return[_ -]?content|returnContent)["']?\s*[:=]\s*["']?([^}\n,]*)/i;
19
- function hasRequiredReturnFlag(toolInput) {
20
- if (!toolInput || typeof toolInput !== 'object')
21
- return false;
22
- const input = toolInput;
23
- const commandValues = ['command', 'cmd', 'script'].flatMap(key => {
24
- const value = input[key];
25
- return typeof value === 'string' ? [value] : [];
26
- });
27
- return commandValues.some(command => /(?:^|\s)--return(?:=|\s+)['"]?required(?:['"]?)(?=\s|$)/i.test(command));
28
- }
29
27
  function hasSuccessfulSendReceipt(result, isError, requiresBusinessReturn = false) {
30
28
  if (isError === true)
31
29
  return false;
@@ -137,6 +135,57 @@ function hasSuccessfulSendReceipt(result, isError, requiresBusinessReturn = fals
137
135
  return true;
138
136
  return false;
139
137
  }
138
+ function sendHint(state) {
139
+ return state.chatType === 'group'
140
+ ? { cmdHint: 'ec group send', target: '群里' }
141
+ : { cmdHint: 'ec msg send', target: '对方' };
142
+ }
143
+ /**
144
+ * 结算当前轮:本轮有意图表达成功 → 清零(不再 +1);否则 +1。
145
+ * 被拒绝的调用不会产生成功回执,因此不能触发清零。
146
+ */
147
+ function settleRound(state) {
148
+ if (!state.roundOpen)
149
+ return;
150
+ state.roundOpen = false;
151
+ state.roundPending = 0;
152
+ state.roundCounter = state.roundHasIntentSuccess ? 0 : state.roundCounter + 1;
153
+ }
154
+ /**
155
+ * 进入一轮。turn 为 runner 提供的模型返回序号:
156
+ * - 有 turn:同一 turn 视为同一轮;turn 变化时先结算上一轮
157
+ * - 无 turn(runner 不支持):已有未结算的轮则沿用,结算由飞行数归零触发
158
+ */
159
+ function beginRound(state, turn) {
160
+ if (typeof turn === 'number' && Number.isFinite(turn)) {
161
+ state.turnAware = true;
162
+ // 先由拿不到 turn 的调用开了轮:认领这个 turn,而不是当成新轮结算
163
+ if (state.roundOpen && state.roundTurn === undefined) {
164
+ state.roundTurn = turn;
165
+ return;
166
+ }
167
+ if (state.roundOpen && state.roundTurn === turn)
168
+ return;
169
+ settleRound(state);
170
+ }
171
+ else if (state.roundOpen) {
172
+ return;
173
+ }
174
+ state.roundOpen = true;
175
+ state.roundTurn = typeof turn === 'number' ? turn : undefined;
176
+ state.roundStartCounter = state.roundCounter;
177
+ state.roundPending = 0;
178
+ state.roundHasIntentAttempt = false;
179
+ state.roundHasIntentSuccess = false;
180
+ state.roundWarned = false;
181
+ }
182
+ /** 新输入(新消息、插入消息、队列合并、续跑、任务恢复)到来:计数重置为初值 */
183
+ export function resetIntentCounter(state) {
184
+ state.roundOpen = false;
185
+ state.roundPending = 0;
186
+ state.roundTurn = undefined;
187
+ state.roundCounter = INTENT_GATE_INTERVAL;
188
+ }
140
189
  function isHumanFacingMessage(message) {
141
190
  if (message.source === 'trigger')
142
191
  return false;
@@ -161,7 +210,17 @@ function buildState(message, cfg) {
161
210
  firstSendRequired: preTool1stMsgChk && humanFacing,
162
211
  toolReportRequired: toolUseReminder && humanFacing,
163
212
  toolReportInterval: TOOL_REPORT_INTERVAL,
164
- toolReportPending: false,
213
+ violationCount: 0,
214
+ intentGateRequired: (preTool1stMsgChk || toolUseReminder) && humanFacing,
215
+ roundCounter: INTENT_GATE_INTERVAL,
216
+ roundOpen: false,
217
+ roundStartCounter: INTENT_GATE_INTERVAL,
218
+ roundTurn: undefined,
219
+ turnAware: false,
220
+ roundPending: 0,
221
+ roundHasIntentAttempt: false,
222
+ roundHasIntentSuccess: false,
223
+ roundWarned: false,
165
224
  };
166
225
  }
167
226
  function normalizedThread(threadId) {
@@ -169,7 +228,10 @@ function normalizedThread(threadId) {
169
228
  }
170
229
  function isResumableTerminal(terminal) {
171
230
  return terminal?.status === 'interrupted' && terminal.reason === 'daemon_restart'
172
- || terminal?.status === 'cancelled' && (terminal.reason === 'stop' || terminal.reason === 'explicit_cancel');
231
+ || terminal?.status === 'cancelled' && (terminal.reason === 'stop'
232
+ || terminal.reason === 'explicit_cancel'
233
+ // 熔断中断走 agent.interrupt() 默认路径,reason 为 explicit_cancel,此处已覆盖
234
+ );
173
235
  }
174
236
  function canRestoreSnapshot(ctx, state, snapshot) {
175
237
  const previous = ctx.previousTurnState;
@@ -182,7 +244,7 @@ function canRestoreSnapshot(ctx, state, snapshot) {
182
244
  const age = Date.now() - snapshot.finishedAt;
183
245
  if (!Number.isFinite(age) || age < 0 || age > SNAPSHOT_MAX_AGE_MS
184
246
  || !Number.isSafeInteger(snapshot.toolCount) || snapshot.toolCount < 0
185
- || typeof snapshot.firstToolDone !== 'boolean' || typeof snapshot.toolReportPending !== 'boolean'
247
+ || typeof snapshot.firstToolDone !== 'boolean'
186
248
  || snapshot.sessionId !== ctx.session.id || snapshot.selfAid !== ctx.session.selfAID
187
249
  || snapshot.channel !== ctx.session.channel || snapshot.channelId !== ctx.session.channelId
188
250
  || snapshot.chatType !== ctx.message.chatType || snapshot.chatType !== ctx.session.chatType
@@ -230,10 +292,10 @@ function saveContinuation(ctx) {
230
292
  finishedAt: terminal.finishedAt,
231
293
  firstToolDone: state.firstToolDone,
232
294
  toolCount: state.toolCount,
233
- toolReportPending: state.toolReportPending,
234
295
  firstSendRequired: state.firstSendRequired,
235
296
  toolReportRequired: state.toolReportRequired,
236
297
  toolReportInterval: state.toolReportInterval,
298
+ violationCount: state.violationCount, // F4修复:跨续跑保留违规计数
237
299
  };
238
300
  ctx.sessionState.set(SNAPSHOT_KEY, snapshot);
239
301
  }
@@ -265,10 +327,25 @@ export const proactiveFlow = {
265
327
  const state = buildState(ctx.message, ctx.modeConfig ?? {});
266
328
  const snapshot = ctx.sessionState?.get(SNAPSHOT_KEY);
267
329
  ctx.sessionState?.delete(SNAPSHOT_KEY);
268
- if (snapshot && canRestoreSnapshot(ctx, state, snapshot)) {
330
+ const snapshotRestored = snapshot && canRestoreSnapshot(ctx, state, snapshot);
331
+ if (snapshotRestored) {
269
332
  state.firstToolDone = snapshot.firstToolDone;
270
333
  state.toolCount = snapshot.toolCount;
271
- state.toolReportPending = state.toolReportRequired && snapshot.toolReportPending;
334
+ // F4修复:跨续跑恢复违规计数,防止每次续跑都重置熔断配额
335
+ if (typeof snapshot.violationCount === 'number')
336
+ state.violationCount = snapshot.violationCount;
337
+ // 轮次计数不恢复:续跑/任务恢复属于新输入,buildState 已置为初值
338
+ }
339
+ else if (snapshot) {
340
+ // F8修复:快照存在但恢复失败(channel/chatType 变化等),
341
+ // 状态已不连续,清除过期的队列提醒长度,避免错误抑制新轮次的提醒
342
+ ctx.sessionState?.delete(QUEUE_REMINDER_SESSION_KEY);
343
+ }
344
+ // 问题3修复:从 sessionState 恢复跨轮次的队列提醒长度,
345
+ // 避免每轮重置为 0 后队列 ≥ 3 就反复触发未读提醒。
346
+ const persistedQueueLen = ctx.sessionState?.get(QUEUE_REMINDER_SESSION_KEY);
347
+ if (typeof persistedQueueLen === 'number' && persistedQueueLen > 0) {
348
+ state.lastQueueReminderLen = persistedQueueLen;
272
349
  }
273
350
  ctx.state.set(STATE_KEY, state);
274
351
  ctx.sessionState?.set(OWNER_KEY, state);
@@ -279,109 +356,121 @@ export const proactiveFlow = {
279
356
  if (!state)
280
357
  return undefined;
281
358
  return {
282
- policyHook: (toolName, toolInput) => {
359
+ policyHook: (toolName, toolInput, meta) => {
283
360
  // Codex only invokes this hook for local tool paths. Hosted tools stay
284
- // enabled and do not satisfy or clear the pending first-send gate.
361
+ // enabled and do not satisfy the intent gate.
285
362
  const isSendCommand = ctx.isSendCommand(toolName, toolInput);
286
- if (!isSendCommand && ((state.firstSendRequired && !state.firstToolDone) || state.toolReportPending)) {
287
- const sendFailure = ctx.describeSendCommandFailure?.(toolName, toolInput);
288
- if (sendFailure)
289
- return { block: true, policyCode: 'proactive_send_invalid', reason: sendFailure };
363
+ const { cmdHint, target } = sendHint(state);
364
+ beginRound(state, meta?.turn);
365
+ // 意图表达工具始终放行;是否清零看执行结果(onToolResult)
366
+ if (isSendCommand) {
367
+ state.roundHasIntentAttempt = true;
368
+ return undefined;
290
369
  }
291
- if (state.firstSendRequired && !state.firstToolDone) {
292
- if (!isSendCommand) {
293
- const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
294
- const target = state.chatType === 'group' ? '群里' : '对方';
295
- return {
296
- block: true,
297
- policyCode: 'proactive_first_send_required',
298
- reason: `请先用 ${cmdHint} 向${target}说明你的意图,再执行其他工具`,
299
- };
370
+ // 非 send 命令:先检查格式是否"像 send 但解析失败"(F2修复:计入违规)
371
+ const sendFailure = ctx.describeSendCommandFailure?.(toolName, toolInput);
372
+ if (sendFailure) {
373
+ state.violationCount++;
374
+ if (state.violationCount >= VIOLATION_CIRCUIT_BREAK_THRESHOLD) {
375
+ return { block: true, policyCode: 'proactive_circuit_break', reason: `连续违规 ${state.violationCount} 次,任务已中断,请重新描述需求` };
300
376
  }
377
+ return { block: true, policyCode: 'proactive_send_invalid', reason: sendFailure };
301
378
  }
302
- if (state.toolReportPending) {
303
- if (!isSendCommand) {
304
- const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
305
- const target = state.chatType === 'group' ? '群里' : '对方';
306
- return {
307
- block: true,
308
- policyCode: 'proactive_tool_report_required',
309
- reason: `工具调用已达到 ${state.toolCount} 次,请先用 ${cmdHint} 向${target}汇报当前情况和下一步意图,再执行其他工具`,
310
- };
379
+ if (!state.intentGateRequired)
380
+ return undefined;
381
+ // 计数 < 15:本条消息内已有意图表达,放行
382
+ if (state.roundStartCounter < INTENT_GATE_INTERVAL)
383
+ return undefined;
384
+ // 计数 = 15:全部放行,提示下一轮先做意图表达(本轮已有发送则不提示)
385
+ if (state.roundStartCounter === INTENT_GATE_INTERVAL) {
386
+ if (!state.roundHasIntentAttempt && !state.roundWarned) {
387
+ state.roundWarned = true;
388
+ state.pendingRoundReminder = `⚠️ 本轮未包含意图表达,下一轮必须先用 ${cmdHint} 向${target}说明当前情况和下一步意图,否则其他工具会被拒绝。`;
311
389
  }
390
+ return undefined;
391
+ }
392
+ // 计数 ≥ 16:只放行意图表达,其他调用全部拒绝
393
+ state.violationCount++;
394
+ if (state.violationCount >= VIOLATION_CIRCUIT_BREAK_THRESHOLD) {
395
+ return { block: true, policyCode: 'proactive_circuit_break', reason: `连续违规 ${state.violationCount} 次,任务已中断,请重新描述需求` };
312
396
  }
313
- return undefined;
397
+ return {
398
+ block: true,
399
+ policyCode: 'proactive_intent_required',
400
+ reason: `上一轮未包含意图表达,本轮只允许 ${cmdHint};请先向${target}说明当前情况和下一步意图,再执行其他工具`,
401
+ };
314
402
  },
315
403
  };
316
404
  },
317
- // ─── onToolUse:队列未读提醒 + 工具汇报提醒 ───
405
+ // ─── onToolUse:轮次计数 + 队列未读提醒 + 工具汇报提醒 ───
318
406
  onToolUse(ctx) {
319
407
  const state = ctx.state.get(STATE_KEY);
320
408
  if (!state)
321
409
  return;
322
- // PreToolUse 和 tool_use 都发生在命令执行前,不能推进发送门禁。
323
- // 只有成功的 tool_result 携带明确回执时才提交状态。
410
+ // 轮次追踪:按模型返回序号进轮,每次工具调用开始时 pending+1
411
+ beginRound(state, ctx.modelTurn);
412
+ state.roundPending++;
413
+ // 15 档提示由 policyHook 写入,这里注入给模型
414
+ if (state.pendingRoundReminder) {
415
+ ctx.injectToModel(state.pendingRoundReminder);
416
+ ctx.recordReminder?.({ kind: 'tool_report', text: state.pendingRoundReminder, toolCount: state.roundStartCounter });
417
+ state.pendingRoundReminder = undefined;
418
+ }
324
419
  const isAllowedTool = ctx.isSendCommand(ctx.toolName, ctx.toolInput);
325
420
  if (!state.toolUseReminder)
326
421
  return;
327
- // 工具计数(排除表态命令白名单)
422
+ // 非发送工具才计入 toolCount(仅审计)
328
423
  if (state.toolReportRequired && !isAllowedTool)
329
424
  state.toolCount++;
330
- // 队列未读变化提醒:首次达到 3 条立即提醒,后续复用工具汇报的计数间隔,
331
- // 避免连续工具调用时每次队列数量变化都重复注入。
425
+ // 队列未读变化提醒
332
426
  const queueLen = ctx.getQueueLength();
333
427
  const queueChanged = queueLen !== state.lastQueueReminderLen;
334
- const toolReportDue = state.toolReportRequired
335
- && !isAllowedTool
336
- && state.toolCount > 0
337
- && state.toolCount % state.toolReportInterval === 0;
338
428
  let queueReminder;
339
429
  const queueReminderDue = state.lastQueueReminderLen === 0
340
430
  ? queueLen >= 3
341
- : toolReportDue;
431
+ : false; // 汇报间隔改由轮次控制
342
432
  if (queueLen === 0) {
343
433
  state.lastQueueReminderLen = 0;
434
+ ctx.sessionState?.delete(QUEUE_REMINDER_SESSION_KEY);
344
435
  }
345
436
  else if (queueChanged && queueReminderDue) {
346
437
  state.lastQueueReminderLen = queueLen;
438
+ ctx.sessionState?.set(QUEUE_REMINDER_SESSION_KEY, queueLen);
347
439
  queueReminder = queueLen >= 5
348
440
  ? `⚠️ 有 ${queueLen} 条消息未读,请尽快完成当前任务,或使用 ec ctl queue 读取后同步处理。`
349
441
  : `⚠️ 有 ${queueLen} 条消息未读,可使用 ec ctl queue 读取。`;
350
442
  ctx.recordReminder?.({ kind: 'queue_unread', text: queueReminder, queueLength: queueLen });
351
- }
352
- if (queueReminder && !toolReportDue) {
353
443
  ctx.injectToModel(queueReminder);
354
444
  }
355
- // 每 N 次工具调用警告
356
- // 只有计数推进到新阈值时才提醒;发送工具不能重新打开已解除的汇报门禁。
357
- if (toolReportDue) {
358
- state.toolReportPending = true;
359
- const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
360
- const target = state.chatType === 'group' ? '群里' : '对方';
361
- const reportMsg = `⚠️ 工具调用已达到 ${state.toolCount} 次,请立即用 ${cmdHint} 向${target}汇报当前情况和下一步意图。`;
362
- ctx.recordReminder?.({ kind: 'tool_report', text: reportMsg, toolCount: state.toolCount });
363
- ctx.injectToModel(queueReminder ? `${queueReminder}\n${reportMsg}` : reportMsg);
364
- }
365
445
  },
366
- // ─── onToolResult:成功发送回执后解除门禁 ───
446
+ // ─── onToolResult:轮次推进 + 成功发送回执后更新标志位 ───
367
447
  onToolResult(ctx) {
368
448
  const state = ctx.state.get(STATE_KEY);
369
449
  if (!state)
370
450
  return;
371
- if (!ctx.isSendCommand(ctx.toolName, ctx.toolInput))
372
- return;
373
- const requiresBusinessReturn = hasRequiredReturnFlag(ctx.toolInput)
374
- && (typeof ctx.result === 'string'
375
- ? HANDOFF_TEXT_RECEIPT_RE.test(ctx.result)
376
- : !!ctx.result && typeof ctx.result === 'object'
377
- && ('handoff_id' in ctx.result || 'handoffId' in ctx.result));
378
- HANDOFF_TEXT_RECEIPT_RE.lastIndex = 0;
379
- if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error, requiresBusinessReturn))
380
- return;
381
- if (state.firstSendRequired && !state.firstToolDone)
382
- state.firstToolDone = true;
383
- if (state.toolReportPending)
384
- state.toolReportPending = false;
451
+ const isSend = ctx.isSendCommand(ctx.toolName, ctx.toolInput);
452
+ // tool_use 事件可能先于 PreToolUse 到达,15 档提示在这里补注入
453
+ if (state.pendingRoundReminder) {
454
+ ctx.injectToModel(state.pendingRoundReminder);
455
+ ctx.recordReminder?.({ kind: 'tool_report', text: state.pendingRoundReminder, toolCount: state.roundStartCounter });
456
+ state.pendingRoundReminder = undefined;
457
+ }
458
+ // 先记成功,再结算:被拒绝/失败的调用不能触发清零
459
+ const succeeded = !ctx.isError && !ctx.error
460
+ && ctx.executionState !== 'blocked' && ctx.executionState !== 'failed'
461
+ && ctx.decision !== 'deny' && ctx.executed !== false;
462
+ if (isSend && succeeded) {
463
+ state.roundHasIntentSuccess = true;
464
+ if (!state.firstToolDone)
465
+ state.firstToolDone = true;
466
+ state.violationCount = 0;
467
+ }
468
+ // 轮次追踪:pending-1。拿得到模型返回序号时由下一轮 beginRound 结算;
469
+ // 拿不到时(runner 不提供 turn)退回飞行数归零结算
470
+ if (state.roundPending > 0)
471
+ state.roundPending--;
472
+ if (state.roundPending === 0 && !state.turnAware)
473
+ settleRound(state);
385
474
  },
386
475
  // ─── onComplete:标志位检查 ───
387
476
  async onComplete(ctx) {
@@ -180,24 +180,36 @@ function loadSpecs(evolcoreHome) {
180
180
  _specCacheTs = now;
181
181
  return _specCache;
182
182
  }
183
- /** 取模型在 ts 时刻的能力参数。找不到时返回默认值。
184
- * 带窗口后缀的 ID 复用基础模型的参数行,但窗口由后缀决定(网关的权威声明)。 */
185
- export function resolveModelSpec(evolcoreHome, model, ts) {
183
+ export function findModelSpec(evolcoreHome, model, ts = Date.now()) {
186
184
  const specs = loadSpecs(evolcoreHome);
187
- const t = ts ?? Date.now();
188
185
  const suffixWindow = parseModelId(model).contextWindow;
189
186
  for (const key of modelLookupKeys(evolcoreHome, model)) {
190
- const candidates = specs.filter(s => s.model === key && s.effective_from <= t);
187
+ const candidates = specs.filter(spec => spec.model === key && spec.effective_from <= ts);
191
188
  if (!candidates.length)
192
189
  continue;
193
- const row = candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
190
+ const row = candidates.reduce((a, b) => a.effective_from > b.effective_from ? a : b);
194
191
  if (suffixWindow === undefined || suffixWindow === row.context_window)
195
192
  return row;
196
193
  // 后缀声明的窗口覆盖基础行,输入上限按原行的预留比例等比缩放。
197
194
  const ratio = row.context_window > 0 ? row.max_input_tokens / row.context_window : 0.9;
198
195
  return { ...row, model, context_window: suffixWindow, max_input_tokens: Math.floor(suffixWindow * ratio) };
199
196
  }
197
+ return undefined;
198
+ }
199
+ /** 取模型在 ts 时刻的能力参数。找不到时返回 fallback 或默认值。
200
+ * 带窗口后缀的 ID 复用基础模型的参数行,但窗口由后缀决定(网关的权威声明)。 */
201
+ export function resolveModelSpec(evolcoreHome, model, ts, fallback) {
202
+ const spec = findModelSpec(evolcoreHome, model, ts);
203
+ if (spec)
204
+ return spec;
205
+ const suffixWindow = parseModelId(model).contextWindow;
200
206
  // 默认值
201
- const contextWindow = suffixWindow ?? 200000;
202
- return { model, effective_from: 0, context_window: contextWindow, max_input_tokens: Math.floor(contextWindow * 0.9), max_output_tokens: 8192 };
207
+ const contextWindow = suffixWindow ?? fallback?.context_window ?? 200000;
208
+ return {
209
+ model,
210
+ effective_from: 0,
211
+ context_window: contextWindow,
212
+ max_input_tokens: Math.floor(contextWindow * 0.9),
213
+ max_output_tokens: fallback?.max_output_tokens ?? 8192,
214
+ };
203
215
  }