evolcore 0.0.32 → 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 (150) hide show
  1. package/CHANGELOG.md +24 -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 +23 -19
  7. package/dist/agents/ecagent-runner.js +5 -13
  8. package/dist/aun/group/operations.js +0 -1
  9. package/dist/aun/msg/history.js +1 -0
  10. package/dist/aun/msg/p2p.js +0 -1
  11. package/dist/aun/outbox.js +50 -1
  12. package/dist/channels/aun.js +28 -2
  13. package/dist/channels/daemon.js +257 -2
  14. package/dist/cli/agent-command.js +58 -24
  15. package/dist/cli/agent.js +1 -1
  16. package/dist/cli/aun-commands.js +365 -29
  17. package/dist/cli/contact.js +8 -2
  18. package/dist/cli/daemon-commands.js +81 -12
  19. package/dist/cli/debug-mode.js +154 -0
  20. package/dist/cli/handoff-command.js +3 -0
  21. package/dist/cli/index.js +81 -64
  22. package/dist/cli/init.js +2 -2
  23. package/dist/cli/managed-command-guard.js +5 -0
  24. package/dist/cli/memory-command.js +8 -1
  25. package/dist/cli/queue-command.js +9 -4
  26. package/dist/cli/session-command.js +194 -0
  27. package/dist/cli/skill-command.js +78 -2
  28. package/dist/cli/task-context.js +1 -0
  29. package/dist/cli/thread-command.js +620 -0
  30. package/dist/cli/trigger-command.js +4 -2
  31. package/dist/config/builtin-roles.js +1 -0
  32. package/dist/config/config-field-policy.js +2 -3
  33. package/dist/config/config-manager.js +7 -2
  34. package/dist/config/resolved-config-op.js +3 -0
  35. package/dist/config-store.js +15 -0
  36. package/dist/core/auth/agent-delegation.js +25 -1
  37. package/dist/core/auth/debug-context.js +139 -0
  38. package/dist/core/auth/operation-authorizer.js +24 -1
  39. package/dist/core/auth/operation-catalog.js +33 -0
  40. package/dist/core/background-session/session-cleanup.js +212 -0
  41. package/dist/core/background-session/types.js +10 -0
  42. package/dist/core/bootstrap-messages.js +1 -1
  43. package/dist/core/bootstrap-service.js +6 -3
  44. package/dist/core/command/command-handler.js +60 -24
  45. package/dist/core/command/memory-slash.js +6 -2
  46. package/dist/core/command/menu-catalog.js +48 -1
  47. package/dist/core/command/menu-handler.js +92 -16
  48. package/dist/core/command/menu-protocol.js +24 -1
  49. package/dist/core/command/skill-slash.js +322 -0
  50. package/dist/core/command/slash-gate.js +6 -4
  51. package/dist/core/command/slash-handler.js +193 -13
  52. package/dist/core/command/topic-slash.js +528 -0
  53. package/dist/core/evolagent-registry.js +20 -5
  54. package/dist/core/handoff/runtime.js +5 -11
  55. package/dist/core/handoff/store.js +5 -0
  56. package/dist/core/ipc-registration.js +139 -2
  57. package/dist/core/memory/consolidation-context.js +49 -16
  58. package/dist/core/memory/consolidation-ops-records.js +35 -0
  59. package/dist/core/memory/consolidation-queue-format.js +21 -0
  60. package/dist/core/memory/consolidation.js +174 -180
  61. package/dist/core/message/aun-ipc-sender.js +161 -6
  62. package/dist/core/message/im-renderer.js +149 -6
  63. package/dist/core/message/message-bridge.js +13 -3
  64. package/dist/core/message/message-log.js +1 -0
  65. package/dist/core/message/peer-mode.js +6 -1
  66. package/dist/core/message/response-engine.js +349 -67
  67. package/dist/core/message/subtask-tool-projection.js +140 -0
  68. package/dist/core/model/catalog-service.js +17 -12
  69. package/dist/core/permission/ec-command-parser.js +126 -7
  70. package/dist/core/session/session-idle-monitor.js +249 -22
  71. package/dist/core/session/session-manager.js +54 -9
  72. package/dist/core/skill/command-parser.js +13 -0
  73. package/dist/core/skill/index-generator.js +4 -1
  74. package/dist/core/skill/startup.js +41 -0
  75. package/dist/core/skill/switch-resolver.js +55 -29
  76. package/dist/core/thread/command-parser.js +125 -0
  77. package/dist/core/thread/index.js +1 -0
  78. package/dist/core/thread/ipc-operation.js +95 -0
  79. package/dist/core/thread/thread-context-storage.js +131 -0
  80. package/dist/core/thread/thread-operation.js +258 -0
  81. package/dist/core/thread/thread-route.js +33 -0
  82. package/dist/core/thread/thread-service.js +777 -0
  83. package/dist/core/thread/types.js +1 -0
  84. package/dist/eck/message-renderer.js +35 -0
  85. package/dist/index.js +8 -5
  86. package/dist/ipc.js +89 -10
  87. package/dist/paths.js +23 -0
  88. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  89. package/dist/trigger/authorization.js +57 -28
  90. package/dist/trigger/feedback.js +7 -1
  91. package/dist/trigger/parser.js +63 -1
  92. package/dist/trigger/validation.js +14 -0
  93. package/dist/utils/error-utils.js +22 -1
  94. package/kits/docs/context-assembly.md +1 -0
  95. package/kits/docs/evolcore/INDEX.md +1 -0
  96. package/kits/docs/evolcore/agent-create.md +39 -28
  97. package/kits/docs/evolcore/agent.md +19 -4
  98. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  99. package/kits/docs/evolcore/group-collaboration.md +3 -5
  100. package/kits/docs/evolcore/group.md +3 -4
  101. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  102. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  103. package/kits/docs/evolcore/menu-protocol.md +7 -0
  104. package/kits/docs/evolcore/msg.md +5 -8
  105. package/kits/docs/evolcore/thread-chain.md +267 -0
  106. package/kits/docs/prompt-loading-architecture.md +1 -0
  107. package/kits/eck_manifest.json +61 -26
  108. package/kits/eck_message_manifest.json +169 -10
  109. package/kits/rules/05-venue.md +1 -1
  110. package/kits/rules/07-memory-and-thread.md +202 -0
  111. package/kits/schemas/relation-config.schema.9.json +18 -3
  112. package/kits/schemas/role-config.schema.1.json +6 -6
  113. package/kits/schemas/single-session.schema.1.json +6 -6
  114. package/kits/schemas/single-session.schema.2.json +6 -6
  115. package/kits/schemas/single-session.schema.3.json +6 -6
  116. package/kits/skills/.switches.json +12 -1
  117. package/kits/skills/ec-group/SKILL.md +234 -0
  118. package/kits/skills/ec-skill/SKILL.md +10 -1
  119. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  120. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  121. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  122. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  123. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  124. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  125. package/kits/skills/thread-ops/SKILL.md +492 -0
  126. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  127. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  128. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  129. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  130. package/kits/templates/bootstrap-welcome.md +7 -7
  131. package/kits/templates/memory-consolidation/forget.md +5 -0
  132. package/kits/templates/memory-consolidation/generalize.md +5 -0
  133. package/kits/templates/memory-consolidation/profile.md +5 -0
  134. package/kits/templates/memory-consolidation/promote.md +5 -0
  135. package/kits/templates/message-fragments/item.md +2 -1
  136. package/kits/templates/message-fragments/thread-context.md +6 -0
  137. package/kits/templates/message-fragments/trigger-session.md +2 -0
  138. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  139. package/kits/templates/roles/member.json +2 -0
  140. package/kits/templates/roles/visitor.json +2 -0
  141. package/kits/templates/system-fragments/bootstrap.md +66 -72
  142. package/kits/templates/system-fragments/channel.md +3 -6
  143. package/kits/templates/system-fragments/commands.md +1 -0
  144. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  145. package/kits/templates/system-fragments/reply.md +3 -2
  146. package/kits/templates/system-fragments/session.md +5 -14
  147. package/package.json +3 -1
  148. package/kits/docs/evolcore/group-fs.md +0 -17
  149. package/kits/docs/venues/aun-group.md +0 -45
  150. 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,13 +268,8 @@ 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({
@@ -314,13 +341,15 @@ export async function authenticatedTriggerActor(dependencies, agentAid, actorSes
314
341
  if (validation.grant.actorId !== actorOrigin.peerId) {
315
342
  throw new Error('trigger delegation actor does not match session peer');
316
343
  }
344
+ const actorChatType = triggerOriginChatType(actorOrigin);
345
+ const actorConversationId = actorChatType === 'group' ? actorOrigin.channelId : actorOrigin.peerId;
317
346
  const roleDetail = resolvePeerRoleDetail({
318
347
  selfAid: agentAid,
319
348
  channelKey: validation.grant.channel,
320
349
  channelType: validation.grant.channelType,
321
- chatType: 'private',
350
+ chatType: actorChatType,
322
351
  actorId: validation.grant.actorId,
323
- conversationId: validation.grant.actorId,
352
+ conversationId: actorConversationId,
324
353
  });
325
354
  const currentRole = roleDetail.effectiveRole;
326
355
  const subject = buildAuthSubject({
@@ -329,8 +358,8 @@ export async function authenticatedTriggerActor(dependencies, agentAid, actorSes
329
358
  channel: validation.grant.channel,
330
359
  channelType: validation.grant.channelType,
331
360
  channelId: actorOrigin.channelId,
332
- chatType: 'private',
333
- conversationId: validation.grant.actorId,
361
+ chatType: actorChatType,
362
+ conversationId: actorConversationId,
334
363
  roleDetail,
335
364
  identity: roleToSessionIdentity(currentRole),
336
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);
@@ -11,7 +11,7 @@ const TRIGGER_UPDATE_FLAGS = new Set([
11
11
  ]);
12
12
  const TRIGGER_CREATE_FLAGS = new Set([
13
13
  'once', 'delay', 'at', 'cron', 'every', 'event', 'event-filter', 'tz',
14
- 'exec', 'prompt', 'name', 'agent',
14
+ 'exec', 'prompt', 'name', 'agent', 'label',
15
15
  'script-path', 'script-runtime', 'script-args', 'script-timeout', 'execution-timeout',
16
16
  'feedback', 'target-channel', 'target-channel-id', 'target-chat-type', 'target-session', 'target-thread-id',
17
17
  'trigger-thread', 'baseagent',
@@ -68,6 +68,60 @@ export function parseDuration(s) {
68
68
  const multiplier = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : 1_000;
69
69
  return amount * multiplier;
70
70
  }
71
+ /**
72
+ * T10: 从原始 args 里提取 `--label key=value`(支持多次出现,值内可用逗号分隔多对)。
73
+ * 约定:labels 若存在则必须含 `kind`(渲染模板路由 key)。
74
+ */
75
+ export function extractLabels(args) {
76
+ const tokens = [];
77
+ if (Array.isArray(args)) {
78
+ for (let i = 0; i < args.length; i++) {
79
+ const a = args[i];
80
+ if (a === '--label') {
81
+ const next = args[i + 1];
82
+ if (next === undefined || next.startsWith('--'))
83
+ return { error: '--label 不能为空(格式:--label key=value)' };
84
+ tokens.push(next);
85
+ i += 1;
86
+ }
87
+ else if (a.startsWith('--label=')) {
88
+ tokens.push(a.slice('--label='.length));
89
+ }
90
+ }
91
+ }
92
+ else {
93
+ const re = /--label(?:=(\S+)|(?:\s+(?!--)(\S+)))?/g;
94
+ let m;
95
+ while ((m = re.exec(args)) !== null) {
96
+ const v = m[1] ?? m[2];
97
+ if (v === undefined)
98
+ return { error: '--label 不能为空(格式:--label key=value)' };
99
+ tokens.push(v.replace(/^["']|["']$/g, ''));
100
+ }
101
+ }
102
+ if (tokens.length === 0)
103
+ return { labels: {} };
104
+ const labels = {};
105
+ for (const token of tokens) {
106
+ for (const pair of token.split(',')) {
107
+ const trimmed = pair.trim();
108
+ if (!trimmed)
109
+ continue;
110
+ const eq = trimmed.indexOf('=');
111
+ if (eq <= 0)
112
+ return { error: `--label 格式无效: "${trimmed}"(应为 key=value)` };
113
+ const key = trimmed.slice(0, eq).trim();
114
+ const value = trimmed.slice(eq + 1);
115
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
116
+ return { error: `--label key 名无效: "${key}"(只允许字母、数字、下划线,且不以数字开头)` };
117
+ }
118
+ labels[key] = value;
119
+ }
120
+ }
121
+ if (!('kind' in labels))
122
+ return { error: '--label 必须包含 kind(如 --label kind=default)' };
123
+ return { labels };
124
+ }
71
125
  export function parseIsoDate(s) {
72
126
  const d = new Date(s);
73
127
  return isNaN(d.getTime()) ? null : d.getTime();
@@ -496,6 +550,9 @@ export function parseTriggerSet(args) {
496
550
  if (!parsed.ok)
497
551
  return { ok: false, error: withPromptFileHint(parsed.error, args) };
498
552
  const value = parsed.value;
553
+ const labelsResult = extractLabels(args);
554
+ if ('error' in labelsResult)
555
+ return { ok: false, error: labelsResult.error };
499
556
  return {
500
557
  ok: true,
501
558
  value: {
@@ -525,6 +582,7 @@ export function parseTriggerSet(args) {
525
582
  model: value.model,
526
583
  effort: value.effort,
527
584
  permissionMode: value.permissionMode,
585
+ ...(Object.keys(labelsResult.labels).length > 0 ? { labels: labelsResult.labels } : {}),
528
586
  },
529
587
  };
530
588
  }
@@ -538,6 +596,9 @@ export function parseTriggerSetArgv(args, opts = {}) {
538
596
  if (!parsed.ok)
539
597
  return { ok: false, error: withPromptFileHint(parsed.error, args, opts) };
540
598
  const value = parsed.value;
599
+ const labelsResult = extractLabels(args);
600
+ if ('error' in labelsResult)
601
+ return { ok: false, error: labelsResult.error };
541
602
  return {
542
603
  ok: true,
543
604
  value: {
@@ -567,6 +628,7 @@ export function parseTriggerSetArgv(args, opts = {}) {
567
628
  model: value.model,
568
629
  effort: value.effort,
569
630
  permissionMode: value.permissionMode,
631
+ ...(Object.keys(labelsResult.labels).length > 0 ? { labels: labelsResult.labels } : {}),
570
632
  },
571
633
  };
572
634
  }
@@ -252,6 +252,20 @@ function normalizeV3Phase2(raw, opts) {
252
252
  reliability: normalizeReliability(raw.reliability),
253
253
  limits: normalizeLimits(raw.limits),
254
254
  };
255
+ // T10: labels 字典原样保留,只做类型校验(如果存在必须是 Record<string, string>)
256
+ const rawLabels = raw.labels;
257
+ if (rawLabels !== undefined && rawLabels !== null) {
258
+ if (typeof rawLabels !== 'object' || Array.isArray(rawLabels)) {
259
+ throw new Error('labels must be a plain object');
260
+ }
261
+ const labels = {};
262
+ for (const [k, v] of Object.entries(rawLabels)) {
263
+ if (typeof v !== 'string')
264
+ throw new Error(`labels.${k} must be a string`);
265
+ labels[k] = v;
266
+ }
267
+ definition.labels = labels;
268
+ }
255
269
  validateTriggerSemantics(definition);
256
270
  return definition;
257
271
  }