evolcore 0.0.31 → 0.0.33

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 (179) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/install-codex-managed-hooks.mjs +124 -37
  3. package/dist/agents/claude-runner.js +29 -2
  4. package/dist/agents/codex/app-server-client.js +3 -0
  5. package/dist/agents/codex/diagnostics.js +76 -44
  6. package/dist/agents/codex-runner.js +51 -24
  7. package/dist/agents/ecagent-runner.js +9 -13
  8. package/dist/agents/gemini-runner.js +4 -0
  9. package/dist/aun/aid/store.js +0 -81
  10. package/dist/aun/group/fs-download.js +1 -10
  11. package/dist/aun/group/operations.js +0 -1
  12. package/dist/aun/msg/history.js +1 -0
  13. package/dist/aun/msg/managed-operation.js +130 -32
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/aun/service-proxy.js +12 -18
  17. package/dist/aun/storage/index.js +29 -0
  18. package/dist/channels/aun.js +38 -137
  19. package/dist/channels/daemon.js +257 -2
  20. package/dist/cli/agent-command.js +64 -31
  21. package/dist/cli/agent-create.js +43 -24
  22. package/dist/cli/agent.js +16 -70
  23. package/dist/cli/aun-commands.js +512 -32
  24. package/dist/cli/contact.js +8 -2
  25. package/dist/cli/ctl-command.js +1 -2
  26. package/dist/cli/daemon-commands.js +82 -13
  27. package/dist/cli/debug-mode.js +154 -0
  28. package/dist/cli/diag.js +2 -2
  29. package/dist/cli/handoff-command.js +18 -5
  30. package/dist/cli/index.js +81 -64
  31. package/dist/cli/init.js +2 -2
  32. package/dist/cli/managed-command-guard.js +5 -0
  33. package/dist/cli/memory-command.js +93 -12
  34. package/dist/cli/model.js +14 -138
  35. package/dist/cli/queue-command.js +51 -0
  36. package/dist/cli/response.js +5 -54
  37. package/dist/cli/session-command.js +194 -0
  38. package/dist/cli/skill-command.js +85 -3
  39. package/dist/cli/stats.js +10 -4
  40. package/dist/cli/task-command.js +1 -0
  41. package/dist/cli/task-context.js +16 -0
  42. package/dist/cli/thread-command.js +620 -0
  43. package/dist/cli/trigger-command.js +76 -50
  44. package/dist/config/builtin-roles.js +9 -7
  45. package/dist/config/config-field-policy.js +2 -3
  46. package/dist/config/config-manager.js +7 -2
  47. package/dist/config/resolved-config-op.js +3 -0
  48. package/dist/config-store.js +15 -0
  49. package/dist/core/agent-ipc-registration.js +65 -22
  50. package/dist/core/auth/agent-delegation.js +42 -2
  51. package/dist/core/auth/debug-context.js +139 -0
  52. package/dist/core/auth/delegation-authorization.js +1 -0
  53. package/dist/core/auth/operation-authorizer.js +24 -1
  54. package/dist/core/auth/operation-catalog.js +44 -3
  55. package/dist/core/background-session/session-cleanup.js +212 -0
  56. package/dist/core/background-session/types.js +10 -0
  57. package/dist/core/bootstrap-messages.js +1 -1
  58. package/dist/core/bootstrap-service.js +6 -3
  59. package/dist/core/command/command-handler.js +224 -45
  60. package/dist/core/command/diag-service.js +5 -1
  61. package/dist/core/command/memory-slash.js +6 -2
  62. package/dist/core/command/menu-catalog.js +48 -1
  63. package/dist/core/command/menu-handler.js +92 -16
  64. package/dist/core/command/menu-protocol.js +24 -1
  65. package/dist/core/command/skill-slash.js +322 -0
  66. package/dist/core/command/slash-gate.js +6 -4
  67. package/dist/core/command/slash-handler.js +193 -13
  68. package/dist/core/command/topic-slash.js +528 -0
  69. package/dist/core/evolagent-registry.js +20 -5
  70. package/dist/core/handoff/ipc-registration.js +38 -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 +210 -9
  74. package/dist/core/memory/consolidation-context.js +49 -16
  75. package/dist/core/memory/consolidation-ops-records.js +35 -0
  76. package/dist/core/memory/consolidation-queue-format.js +21 -0
  77. package/dist/core/memory/consolidation.js +174 -180
  78. package/dist/core/message/aun-ipc-sender.js +207 -8
  79. package/dist/core/message/im-renderer.js +149 -6
  80. package/dist/core/message/message-bridge.js +13 -3
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +131 -3
  83. package/dist/core/message/peer-mode.js +6 -1
  84. package/dist/core/message/response-engine.js +379 -69
  85. package/dist/core/message/subtask-tool-projection.js +140 -0
  86. package/dist/core/model/catalog-service.js +17 -12
  87. package/dist/core/permission/ec-command-parser.js +126 -7
  88. package/dist/core/permission/sandbox-runtime.js +14 -1
  89. package/dist/core/session/session-idle-monitor.js +249 -22
  90. package/dist/core/session/session-manager.js +54 -9
  91. package/dist/core/skill/command-parser.js +13 -0
  92. package/dist/core/skill/index-generator.js +4 -1
  93. package/dist/core/skill/startup.js +41 -0
  94. package/dist/core/skill/switch-resolver.js +55 -29
  95. package/dist/core/task/task-recovery-service.js +2 -0
  96. package/dist/core/task/task-resume-operation.js +11 -0
  97. package/dist/core/thread/command-parser.js +125 -0
  98. package/dist/core/thread/index.js +1 -0
  99. package/dist/core/thread/ipc-operation.js +95 -0
  100. package/dist/core/thread/thread-context-storage.js +131 -0
  101. package/dist/core/thread/thread-operation.js +258 -0
  102. package/dist/core/thread/thread-route.js +33 -0
  103. package/dist/core/thread/thread-service.js +777 -0
  104. package/dist/core/thread/types.js +1 -0
  105. package/dist/eck/message-renderer.js +35 -0
  106. package/dist/index.js +10 -7
  107. package/dist/ipc.js +234 -17
  108. package/dist/paths.js +23 -0
  109. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  110. package/dist/trigger/authorization.js +59 -29
  111. package/dist/trigger/feedback.js +7 -1
  112. package/dist/trigger/ipc-service.js +49 -35
  113. package/dist/trigger/parser.js +63 -1
  114. package/dist/trigger/script-sandbox.js +58 -77
  115. package/dist/trigger/validation.js +14 -0
  116. package/dist/utils/error-utils.js +22 -1
  117. package/kits/docs/AUTHORING.md +1 -1
  118. package/kits/docs/channels/aun.md +8 -0
  119. package/kits/docs/context-assembly.md +1 -0
  120. package/kits/docs/evolcore/INDEX.md +3 -2
  121. package/kits/docs/evolcore/agent-create.md +39 -28
  122. package/kits/docs/evolcore/agent.md +19 -4
  123. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  124. package/kits/docs/evolcore/group-collaboration.md +3 -5
  125. package/kits/docs/evolcore/group.md +3 -4
  126. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  127. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  128. package/kits/docs/evolcore/menu-protocol.md +7 -0
  129. package/kits/docs/evolcore/msg.md +5 -8
  130. package/kits/docs/evolcore/task.md +2 -0
  131. package/kits/docs/evolcore/thread-chain.md +267 -0
  132. package/kits/docs/evolcore/trigger.md +2 -2
  133. package/kits/docs/prompt-loading-architecture.md +1 -0
  134. package/kits/eck_manifest.json +61 -26
  135. package/kits/eck_message_manifest.json +169 -10
  136. package/kits/rules/01-overview.md +0 -9
  137. package/kits/rules/05-venue.md +1 -1
  138. package/kits/rules/06-channel.md +7 -1
  139. package/kits/rules/07-memory-and-thread.md +202 -0
  140. package/kits/schemas/relation-config.schema.9.json +18 -3
  141. package/kits/schemas/role-config.schema.1.json +6 -6
  142. package/kits/schemas/single-session.schema.1.json +6 -6
  143. package/kits/schemas/single-session.schema.2.json +6 -6
  144. package/kits/schemas/single-session.schema.3.json +6 -6
  145. package/kits/skills/.switches.json +12 -1
  146. package/kits/skills/ec-group/SKILL.md +234 -0
  147. package/kits/skills/ec-skill/SKILL.md +10 -1
  148. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  149. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  150. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  151. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  152. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  153. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  154. package/kits/skills/thread-ops/SKILL.md +492 -0
  155. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  156. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  157. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  158. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  159. package/kits/templates/bootstrap-welcome.md +7 -7
  160. package/kits/templates/memory-consolidation/forget.md +5 -0
  161. package/kits/templates/memory-consolidation/generalize.md +5 -0
  162. package/kits/templates/memory-consolidation/profile.md +5 -0
  163. package/kits/templates/memory-consolidation/promote.md +5 -0
  164. package/kits/templates/message-fragments/item.md +2 -1
  165. package/kits/templates/message-fragments/thread-context.md +6 -0
  166. package/kits/templates/message-fragments/trigger-session.md +2 -0
  167. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  168. package/kits/templates/roles/member.json +2 -0
  169. package/kits/templates/roles/visitor.json +2 -0
  170. package/kits/templates/system-fragments/bootstrap.md +66 -72
  171. package/kits/templates/system-fragments/channel.md +3 -6
  172. package/kits/templates/system-fragments/commands.md +2 -1
  173. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  174. package/kits/templates/system-fragments/reply.md +3 -2
  175. package/kits/templates/system-fragments/session.md +5 -14
  176. package/package.json +3 -1
  177. package/kits/docs/evolcore/group-fs.md +0 -17
  178. package/kits/docs/venues/aun-group.md +0 -45
  179. package/kits/docs/venues/group.md +0 -43
@@ -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) {
@@ -8,6 +8,7 @@ import { validateModelSelectionForRole } from '../core/model/model-permission.js
8
8
  import { isValidAunDeliveryForChannel } from '../core/message/outbound.js';
9
9
  import { tryParseChannelKey } from '../core/channel-loader.js';
10
10
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from '../core/role/runtime-policy.js';
11
+ import { resolveThreadRouteTarget } from '../core/thread/thread-route.js';
11
12
  export function originFromActorSession(session, authenticatedPeerId, authenticatedChannelKey, authenticatedChatType) {
12
13
  const peerId = authenticatedPeerId || session.metadata?.peerId;
13
14
  if (!peerId)
@@ -15,16 +16,21 @@ export function originFromActorSession(session, authenticatedPeerId, authenticat
15
16
  const sessionChannelKey = session.metadata?.channelKey || session.channel;
16
17
  const channelKey = authenticatedChannelKey || sessionChannelKey;
17
18
  const sameConversation = channelKey === sessionChannelKey;
19
+ if (authenticatedChatType && (authenticatedChatType === 'group') !== (session.chatType === 'group')) {
20
+ return undefined;
21
+ }
18
22
  const isGroup = authenticatedChatType === 'group' || session.chatType === 'group';
23
+ const channelId = isGroup ? session.channelId : peerId;
24
+ const sessionKind = sameConversation && session.threadId ? 'thread' : 'main';
19
25
  return {
20
26
  channelKey,
21
27
  channelType: session.channelType || tryParseChannelKey(channelKey)?.type || channelKey,
22
- channelId: peerId,
23
- session: sameConversation && !isGroup && session.threadId ? 'thread' : 'main',
24
- threadId: sameConversation && !isGroup ? (session.threadId || undefined) : undefined,
28
+ channelId,
29
+ session: sessionKind,
30
+ threadId: sessionKind === 'thread' ? session.threadId : undefined,
25
31
  peerId,
26
- sessionKey: sameConversation && !isGroup ? session.sessionKey : undefined,
27
- delivery: { chatType: 'private' },
32
+ sessionKey: sameConversation ? session.sessionKey : undefined,
33
+ delivery: isGroup ? { chatType: 'group', groupId: channelId } : { chatType: 'private' },
28
34
  };
29
35
  }
30
36
  export function daemonPrivilegedTriggerReason(definition) {
@@ -76,6 +82,36 @@ export function validateTriggerFeedbackChannels(definition, getTriggerChannel) {
76
82
  export function hasDaemonTriggerAuthority(subject) {
77
83
  return subject.isDaemonOwner || subject.processRole === 'daemon-service' || subject.dataScope === 'daemon';
78
84
  }
85
+ function triggerOriginChatType(origin) {
86
+ return origin.delivery?.chatType ?? 'private';
87
+ }
88
+ function feedbackTargetMatchesOrigin(target, origin) {
89
+ return target.channelKey === origin.channelKey
90
+ && target.channelId === origin.channelId
91
+ && target.session === origin.session
92
+ && (target.threadId ?? '') === (origin.threadId ?? '')
93
+ && (target.chatType === undefined || target.chatType === triggerOriginChatType(origin));
94
+ }
95
+ function feedbackTargetIsSamePeerThread(definition, target, origin) {
96
+ if (!target || !origin || target.session !== 'thread' || !target.threadId)
97
+ return false;
98
+ if (target.channelKey !== origin.channelKey || target.channelId !== origin.channelId)
99
+ return false;
100
+ const originChatType = triggerOriginChatType(origin);
101
+ if (target.chatType !== originChatType)
102
+ return false;
103
+ return !!resolveThreadRouteTarget(definition.agentAid, target.threadId, target.chatType === 'group' ? 'group' : 'msg', target.channelId);
104
+ }
105
+ function feedbackTargetAllowedForNonManagement(definition, origin) {
106
+ const target = definition.feedback.target;
107
+ if (!target || !origin)
108
+ return false;
109
+ if (feedbackTargetMatchesOrigin(target, origin))
110
+ return true;
111
+ if (target.chatType === 'group')
112
+ return false;
113
+ return feedbackTargetIsSamePeerThread(definition, target, origin);
114
+ }
79
115
  export function validateTriggerDefinitionForActor(definition, actor, dependencies) {
80
116
  if (definition.execution.permissionMode === 'fullaccess') {
81
117
  if (!actor.daemonOwner && !actor.fullAccess) {
@@ -127,14 +163,8 @@ export function validateTriggerDefinitionForActor(definition, actor, dependencie
127
163
  throw new Error('event-session feedback requires owner/admin');
128
164
  }
129
165
  if (!actor.management && definition.feedback.strategy === 'target') {
130
- const target = definition.feedback.target;
131
- const origin = actor.origin;
132
- if (!target || !origin
133
- || target.channelKey !== origin.channelKey
134
- || target.channelId !== origin.channelId
135
- || target.session !== origin.session
136
- || (target.threadId ?? '') !== (origin.threadId ?? '')) {
137
- throw new Error('non-management trigger target must be the creator private conversation');
166
+ if (!feedbackTargetAllowedForNonManagement(definition, actor.origin)) {
167
+ throw new Error('non-management trigger target must be the creator conversation or an existing same-peer private thread');
138
168
  }
139
169
  }
140
170
  }
@@ -187,13 +217,15 @@ export function authorizeTriggerExecution(definition, dependencies) {
187
217
  const channelType = origin.channelType || tryParseChannelKey(origin.channelKey)?.type;
188
218
  if (!channelType)
189
219
  return { allowed: false, reason: 'trigger origin channel type is missing' };
220
+ const chatType = triggerOriginChatType(origin);
221
+ const conversationId = chatType === 'group' ? origin.channelId : origin.peerId;
190
222
  const roleDetail = resolvePeerRoleDetail({
191
223
  selfAid: definition.agentAid,
192
224
  channelKey: origin.channelKey,
193
225
  channelType,
194
- chatType: 'private',
226
+ chatType,
195
227
  actorId: origin.peerId,
196
- conversationId: origin.peerId,
228
+ conversationId,
197
229
  });
198
230
  const role = roleDetail.effectiveRole;
199
231
  if (!checkRoleAccess(role, definition.agentAid)) {
@@ -205,8 +237,8 @@ export function authorizeTriggerExecution(definition, dependencies) {
205
237
  channel: origin.channelKey,
206
238
  channelType,
207
239
  channelId: origin.channelId,
208
- chatType: 'private',
209
- conversationId: origin.peerId,
240
+ chatType,
241
+ conversationId,
210
242
  roleDetail,
211
243
  identity: roleToSessionIdentity(role),
212
244
  processOwners: dependencies.getDaemonOwners(),
@@ -236,18 +268,13 @@ export function authorizeTriggerExecution(definition, dependencies) {
236
268
  return { allowed: false, reason: 'event-session feedback requires current owner/admin role' };
237
269
  }
238
270
  if (definition.feedback.strategy === 'target' && !isManagementRole(role)) {
239
- const target = definition.feedback.target;
240
- if (!target
241
- || target.channelKey !== origin.channelKey
242
- || target.channelId !== origin.channelId
243
- || target.session !== origin.session
244
- || (target.threadId ?? '') !== (origin.threadId ?? '')) {
245
- return { allowed: false, reason: `cross-conversation target requires current owner/admin role (role=${role ?? 'none'})` };
271
+ if (!feedbackTargetAllowedForNonManagement(definition, origin)) {
272
+ return { allowed: false, reason: `cross-conversation target requires current owner/admin role or an existing same-peer private thread (role=${role ?? 'none'})` };
246
273
  }
247
274
  }
248
275
  const permissionDecision = constrainRuntimePermissionMode({
249
276
  selfAid: definition.agentAid,
250
- role,
277
+ role: typeof role === 'string' ? role : undefined,
251
278
  requestedValue: definition.execution.permissionMode,
252
279
  });
253
280
  if (definition.execution.permissionMode && permissionDecision.constrained) {
@@ -260,6 +287,7 @@ export function authorizeTriggerExecution(definition, dependencies) {
260
287
  allowed: true,
261
288
  scope: privilegedReason && hasDaemonTriggerAuthority(subject) ? 'daemon' : 'agent',
262
289
  permissionMode: permissionDecision.effectiveValue,
290
+ role: role ?? undefined,
263
291
  };
264
292
  }
265
293
  export async function authenticatedTriggerActor(dependencies, agentAid, actorSessionId, delegationToken, delegationCommandHash, controlToken, contextOnly = false) {
@@ -313,13 +341,15 @@ export async function authenticatedTriggerActor(dependencies, agentAid, actorSes
313
341
  if (validation.grant.actorId !== actorOrigin.peerId) {
314
342
  throw new Error('trigger delegation actor does not match session peer');
315
343
  }
344
+ const actorChatType = triggerOriginChatType(actorOrigin);
345
+ const actorConversationId = actorChatType === 'group' ? actorOrigin.channelId : actorOrigin.peerId;
316
346
  const roleDetail = resolvePeerRoleDetail({
317
347
  selfAid: agentAid,
318
348
  channelKey: validation.grant.channel,
319
349
  channelType: validation.grant.channelType,
320
- chatType: 'private',
350
+ chatType: actorChatType,
321
351
  actorId: validation.grant.actorId,
322
- conversationId: validation.grant.actorId,
352
+ conversationId: actorConversationId,
323
353
  });
324
354
  const currentRole = roleDetail.effectiveRole;
325
355
  const subject = buildAuthSubject({
@@ -328,8 +358,8 @@ export async function authenticatedTriggerActor(dependencies, agentAid, actorSes
328
358
  channel: validation.grant.channel,
329
359
  channelType: validation.grant.channelType,
330
360
  channelId: actorOrigin.channelId,
331
- chatType: 'private',
332
- conversationId: validation.grant.actorId,
361
+ chatType: actorChatType,
362
+ conversationId: actorConversationId,
333
363
  roleDetail,
334
364
  identity: roleToSessionIdentity(currentRole),
335
365
  processOwners: dependencies.getDaemonOwners(),
@@ -272,10 +272,16 @@ export class TriggerFeedbackDispatcher {
272
272
  originChannelKey: eventSession ? target.channelKey : input.trigger.origin?.channelKey,
273
273
  originChannelType: eventSession ? channelType : input.trigger.origin?.channelType
274
274
  ?? (input.trigger.origin?.channelKey ? channelTypeFromKey(input.trigger.origin.channelKey) : undefined),
275
- ...(eventSession ? { originChatType: messageChatType, originConversationId: target.channelId } : {}),
275
+ originChatType: eventSession ? messageChatType : input.trigger.origin?.delivery?.chatType,
276
+ originConversationId: eventSession
277
+ ? target.channelId
278
+ : input.trigger.origin?.delivery?.chatType === 'group'
279
+ ? input.trigger.origin.channelId
280
+ : input.trigger.origin?.peerId,
276
281
  originControl: !eventSession && input.trigger.origin?.channelKey === 'daemon',
277
282
  noopSentinel: input.trigger.execution.noopSentinel,
278
283
  authorizedBy: input.trigger.authorizedBy,
284
+ labels: input.trigger.labels,
279
285
  },
280
286
  };
281
287
  outcome = this.waitForTriggerOutcome(input.runId, input.attemptId, delivery.signal);