evolcore 0.0.35 → 0.0.36

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 (224) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +134 -48
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +109 -34
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
package/dist/product.js CHANGED
@@ -7,6 +7,7 @@
7
7
  export const PRODUCT_DISPLAY_NAME = 'EvolCore';
8
8
  export const MAIN_PACKAGE_NAME = 'evolcore';
9
9
  export const MAIN_CLI_BIN = 'ec';
10
+ export const RESTART_REQUEST_ID_ENV = 'EVOLCORE_RESTART_REQUEST_ID';
10
11
  export const WEB_PACKAGE_NAME = 'ec-web';
11
12
  export const WEB_CLI_BIN = 'ec-web';
12
13
  export const WEB_PACKAGE_LATEST = `${WEB_PACKAGE_NAME}@latest`;
@@ -8,6 +8,7 @@
8
8
  * flow 是无状态单例:per-message 运行时状态存 ctx.state['proactive'],
9
9
  * 依赖全部由调用点显式传入。
10
10
  */
11
+ import { sessionTopicThreadId } from '../../../core/thread/topic-id.js';
11
12
  const STATE_KEY = 'proactive';
12
13
  const SNAPSHOT_KEY = 'proactive:continuation';
13
14
  const OWNER_KEY = 'proactive:owner';
@@ -248,7 +249,7 @@ function canRestoreSnapshot(ctx, state, snapshot) {
248
249
  || snapshot.sessionId !== ctx.session.id || snapshot.selfAid !== ctx.session.selfAID
249
250
  || snapshot.channel !== ctx.session.channel || snapshot.channelId !== ctx.session.channelId
250
251
  || snapshot.chatType !== ctx.message.chatType || snapshot.chatType !== ctx.session.chatType
251
- || snapshot.threadId !== normalizedThread(ctx.session.threadId)
252
+ || snapshot.threadId !== (sessionTopicThreadId(ctx.session) ?? '')
252
253
  || snapshot.threadId !== normalizedThread(continuation.threadId)
253
254
  || snapshot.baseagent !== ctx.session.baseagent || snapshot.agentSessionId !== ctx.session.agentSessionId
254
255
  || snapshot.firstSendRequired !== state.firstSendRequired
@@ -283,7 +284,7 @@ function saveContinuation(ctx) {
283
284
  channel: ctx.session.channel,
284
285
  channelId: ctx.session.channelId,
285
286
  chatType: state.chatType,
286
- threadId: normalizedThread(ctx.session.threadId),
287
+ threadId: sessionTopicThreadId(ctx.session) ?? '',
287
288
  baseagent: ctx.session.baseagent,
288
289
  agentSessionId: ctx.session.agentSessionId,
289
290
  turnId: terminal.turnId,
@@ -460,8 +461,14 @@ export const proactiveFlow = {
460
461
  && ctx.executionState !== 'blocked' && ctx.executionState !== 'failed'
461
462
  && ctx.decision !== 'deny' && ctx.executed !== false;
462
463
  if (isSend && succeeded) {
464
+ const cmd = typeof ctx.toolInput?.command === 'string' ? ctx.toolInput.command : '';
465
+ const returnMatch = cmd.match(/--return\s+(required|none)\b/);
466
+ const requiresBusinessReturn = returnMatch?.[1] === 'required';
467
+ const businessDone = requiresBusinessReturn
468
+ ? hasSuccessfulSendReceipt(ctx.result, ctx.isError, true)
469
+ : true;
463
470
  state.roundHasIntentSuccess = true;
464
- if (!state.firstToolDone)
471
+ if (!state.firstToolDone && businessDone)
465
472
  state.firstToolDone = true;
466
473
  state.violationCount = 0;
467
474
  }
package/dist/stats/db.js CHANGED
@@ -222,6 +222,8 @@ function _initTables(db) {
222
222
  peer_key TEXT NOT NULL,
223
223
  call_index INTEGER NOT NULL,
224
224
  model TEXT NOT NULL,
225
+ source TEXT NOT NULL DEFAULT 'request',
226
+ provider_thread_id TEXT,
225
227
  request_id TEXT,
226
228
  message_id TEXT,
227
229
  input_tokens INTEGER NOT NULL DEFAULT 0,
@@ -291,6 +293,12 @@ function _initTables(db) {
291
293
  // 使用上轮真实 context/max/compact window,避免按模型名猜窗口。
292
294
  try {
293
295
  const cols = db.prepare(`PRAGMA table_info(model_calls)`).all();
296
+ if (!cols.some(c => c.name === 'source')) {
297
+ db.exec(`ALTER TABLE model_calls ADD COLUMN source TEXT NOT NULL DEFAULT 'request'`);
298
+ }
299
+ if (!cols.some(c => c.name === 'provider_thread_id')) {
300
+ db.exec(`ALTER TABLE model_calls ADD COLUMN provider_thread_id TEXT`);
301
+ }
294
302
  if (!cols.some(c => c.name === 'context_tokens')) {
295
303
  db.exec(`ALTER TABLE model_calls ADD COLUMN context_tokens INTEGER`);
296
304
  }
@@ -2,9 +2,31 @@
2
2
  * writer.ts — 写入 usage_events 和 context_breakdown。
3
3
  */
4
4
  import { getDb, resetStatsDb, startDailyRollupMaintenance } from './db.js';
5
+ import { normalizeUsage } from './normalizer.js';
5
6
  import { resolvePrices } from './price-resolver.js';
6
7
  import { logger } from '../utils/logger.js';
7
- export function insertUsageEvent(evolcoreHome, event, gatewayPricing) {
8
+ export function insertUsageEvent(evolcoreHome, event, gatewayPricing, modelCalls) {
9
+ if (modelCalls?.some(call => call.source === 'subagent')) {
10
+ const events = modelCalls.map((call, index) => normalizeUsage({ ...call.tokenUsage,
11
+ cache_creation_input_tokens: call.tokenUsage.cache_creation_input_tokens ?? undefined,
12
+ cache_read_input_tokens: call.tokenUsage.cache_read_input_tokens ?? undefined,
13
+ }, {
14
+ ...event, model: call.model || 'unknown', turns: index === 0 ? event.turns : 0,
15
+ duration_ms: index === 0 ? event.duration_ms : undefined,
16
+ context_window_pct: index === 0 ? event.context_window_pct : undefined,
17
+ }));
18
+ for (const field of ['input_tokens', 'output_tokens', 'cache_creation_tokens', 'cache_read_tokens']) {
19
+ if (events.reduce((sum, entry) => sum + entry[field], 0) !== event[field]) {
20
+ throw new Error('Model usage breakdown does not match the turn total');
21
+ }
22
+ }
23
+ const prices = events.map(entry => insertUsageEvent(evolcoreHome, entry, gatewayPricing));
24
+ const sum = (kind) => prices.every(price => price[kind] !== null)
25
+ ? { usd: prices.reduce((total, price) => total + price[kind].usd, 0),
26
+ cny: prices.reduce((total, price) => total + price[kind].cny, 0) }
27
+ : null;
28
+ return { official: sum('official'), gateway: sum('gateway') };
29
+ }
8
30
  const db = getDb(evolcoreHome);
9
31
  // 写入时计算费用(官方价格 + 网关价格)。即使 DB 不可用也返回价格对,供调用方合入状态回执。
10
32
  const prices = resolvePrices(evolcoreHome, event, gatewayPricing);
@@ -155,13 +177,13 @@ export function insertModelCalls(evolcoreHome, rows) {
155
177
  const stmt = db.prepare(`
156
178
  INSERT INTO model_calls
157
179
  (ts, task_id, session_id, agent_session_id, agent_aid, peer_key, call_index, model,
158
- request_id, message_id, input_tokens, output_tokens, cache_creation_tokens,
180
+ source, provider_thread_id, request_id, message_id, input_tokens, output_tokens, cache_creation_tokens,
159
181
  cache_read_tokens, context_tokens, max_tokens, auto_compact_tokens, degraded)
160
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
182
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
161
183
  `);
162
184
  db.exec('BEGIN');
163
185
  for (const r of rows) {
164
- stmt.run(r.ts, r.task_id, r.session_id ?? null, r.agent_session_id ?? null, r.agent_aid, r.peer_key, r.call_index, r.model, r.request_id ?? null, r.message_id ?? null, r.input_tokens, r.output_tokens, r.cache_creation_tokens, r.cache_read_tokens, r.context_tokens ?? null, r.max_tokens ?? null, r.auto_compact_tokens ?? null, r.degraded);
186
+ stmt.run(r.ts, r.task_id, r.session_id ?? null, r.agent_session_id ?? null, r.agent_aid, r.peer_key, r.call_index, r.model, r.source ?? 'request', r.provider_thread_id ?? null, r.request_id ?? null, r.message_id ?? null, r.input_tokens, r.output_tokens, r.cache_creation_tokens, r.cache_read_tokens, r.context_tokens ?? null, r.max_tokens ?? null, r.auto_compact_tokens ?? null, r.degraded);
165
187
  }
166
188
  db.exec('COMMIT');
167
189
  }
@@ -2,10 +2,12 @@ import crypto from 'crypto';
2
2
  import { eventPatternIncludesInternal } from '../core/event-catalog.js';
3
3
  import { trustedExecutionIdentityInput } from '../core/auth/agent-delegation.js';
4
4
  import { authorizeOperation, buildAuthSubject } from '../core/auth/auth-gateway.js';
5
+ import { refreshOwnershipForAuthorization } from '../core/ownership/owner-resolver.js';
5
6
  import { isManagementRole } from '../config/builtin-roles.js';
6
7
  import { checkRoleAccess, resolvePeerRoleDetail, roleToSessionIdentity } from '../config/peer-role-resolver.js';
7
8
  import { validateModelSelectionForRole } from '../core/model/model-permission.js';
8
9
  import { isValidAunDeliveryForChannel } from '../core/message/outbound.js';
10
+ import { sessionTopicThreadId } from '../core/thread/topic-id.js';
9
11
  import { tryParseChannelKey } from '../core/channel-loader.js';
10
12
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from '../core/role/runtime-policy.js';
11
13
  import { resolveThreadRouteTarget } from '../core/thread/thread-route.js';
@@ -21,13 +23,16 @@ export function originFromActorSession(session, authenticatedPeerId, authenticat
21
23
  }
22
24
  const isGroup = authenticatedChatType === 'group' || session.chatType === 'group';
23
25
  const channelId = isGroup ? session.channelId : peerId;
24
- const sessionKind = sameConversation && session.threadId ? 'thread' : 'main';
26
+ // 只有话题会让 origin 变成 `thread`;主话题(含 `main:<a>:<b>` 规范 id)
27
+ // 在 Trigger 里仍用 `session: 'main'` 表达,主话题 id 不写进 origin.threadId。
28
+ const topicThreadId = sameConversation ? sessionTopicThreadId(session) : undefined;
29
+ const sessionKind = topicThreadId ? 'thread' : 'main';
25
30
  return {
26
31
  channelKey,
27
32
  channelType: session.channelType || tryParseChannelKey(channelKey)?.type || channelKey,
28
33
  channelId,
29
34
  session: sessionKind,
30
- threadId: sessionKind === 'thread' ? session.threadId : undefined,
35
+ threadId: topicThreadId,
31
36
  peerId,
32
37
  sessionKey: sameConversation ? session.sessionKey : undefined,
33
38
  delivery: isGroup ? { chatType: 'group', groupId: channelId } : { chatType: 'private' },
@@ -168,7 +173,46 @@ export function validateTriggerDefinitionForActor(definition, actor, dependencie
168
173
  }
169
174
  }
170
175
  }
171
- export function authorizeTriggerExecution(definition, dependencies) {
176
+ /**
177
+ * Trigger 的操作主体集合:谁能 cancel / update / show 这条 Trigger。
178
+ *
179
+ * origin 只记录"从哪个会话创建",feedback 目标才是"结果投给谁"。跨话题复查
180
+ * (origin = 来源话题对端 A,target = 目标话题对端 C)要求 C 侧也能操作,
181
+ * 否则结果回来之后在 C 那边完全看不见、也取消不掉。旧定义没有 operatorPeers
182
+ * 字段时按同样规则现算,不需要数据迁移。
183
+ */
184
+ export function triggerOperatorPeerIds(definition) {
185
+ const declared = (definition.operatorPeers ?? []).filter(peer => typeof peer === 'string' && peer.length > 0);
186
+ if (declared.length > 0)
187
+ return [...new Set(declared)];
188
+ const peers = [];
189
+ if (definition.origin)
190
+ peers.push(originConversationPeerId(definition.origin));
191
+ const target = definition.feedback.strategy === 'target' ? definition.feedback.target : undefined;
192
+ if (target?.channelId)
193
+ peers.push(target.channelId);
194
+ return [...new Set(peers.filter(peer => typeof peer === 'string' && peer.length > 0))];
195
+ }
196
+ /**
197
+ * 会话的对端身份:私聊是对方 AID,群聊是群 AID。
198
+ *
199
+ * 群会话里 `origin.peerId` 记的是**群内发言人**(`channels/aun.ts` 的
200
+ * `buildGroupReplyContext` 把 `replyContext.peerId` 设为 senderAid),
201
+ * 群本身的身份只在 `channelId`。协议里 `group_id` 与 `group_aid` 是同一个值
202
+ * (`10-Group-子协议.md`:`{"group_id":"g-abc.pub","group_aid":"g-abc.pub"}`),
203
+ * 所以群场景取 `channelId` 才是群 AID。若按 peerId 比对,群内每个发言人都会
204
+ * 得到不同身份,授权必然对不上。
205
+ */
206
+ export function originConversationPeerId(origin) {
207
+ return origin.delivery?.chatType === 'group' ? origin.channelId : (origin.peerId || origin.channelId);
208
+ }
209
+ export function isTriggerOperator(definition, actorConversationPeerId) {
210
+ if (!definition || !actorConversationPeerId)
211
+ return false;
212
+ return triggerOperatorPeerIds(definition).includes(actorConversationPeerId);
213
+ }
214
+ export async function authorizeTriggerExecution(definition, dependencies) {
215
+ await refreshOwnershipForAuthorization(definition.agentAid);
172
216
  if (definition.execution.permissionMode === 'fullaccess') {
173
217
  if (!definition.authorizedBy) {
174
218
  return { allowed: false, reason: 'fullaccess Trigger approval provenance is missing' };
@@ -342,6 +386,7 @@ export async function authenticatedTriggerActor(dependencies, agentAid, actorSes
342
386
  throw new Error('trigger delegation actor does not match session peer');
343
387
  }
344
388
  const actorChatType = triggerOriginChatType(actorOrigin);
389
+ await refreshOwnershipForAuthorization(agentAid);
345
390
  const actorConversationId = actorChatType === 'group' ? actorOrigin.channelId : actorOrigin.peerId;
346
391
  const roleDetail = resolvePeerRoleDetail({
347
392
  selfAid: agentAid,
@@ -2,7 +2,8 @@ import { buildEnvelope } from '../core/message/message-utils.js';
2
2
  import { executionTimeoutMs, renderTemplate, previewText, sha256 } from './validation.js';
3
3
  import { isDaemonEventSessionTrigger } from './event-source.js';
4
4
  import { TASK_RECOVERY_PROMPT } from '../core/task/task-recovery-service.js';
5
- import { ExecutionTimeoutError, OrphanedTargetSessionError } from '../utils/error-utils.js';
5
+ import { ExecutionTimeoutError, OrphanedTargetSessionError, isRetryableError } from '../utils/error-utils.js';
6
+ import { isTopicThreadId, sessionThreadKind, sessionTopicThreadId } from '../core/thread/topic-id.js';
6
7
  const FEEDBACK_DEADLINE_MS = 30_000;
7
8
  const TARGET_SESSION_CANCEL_GRACE_MS = 1_000;
8
9
  const TARGET_SESSION_OUTCOME_GRACE_MS = 5_000;
@@ -89,12 +90,15 @@ export class TriggerFeedbackDispatcher {
89
90
  if (channelTypeFromKey(channelKey) === 'aun' && typeof payload.encrypted !== 'boolean') {
90
91
  return this.failed(feedback, [], 'event_session_encryption_missing', 'AUN event source must preserve the original encryption mode');
91
92
  }
93
+ const eventSourceKind = sessionThreadKind(eventSession);
92
94
  unresolvedTarget = {
93
95
  channelKey,
94
96
  channelId: eventSession.channelId,
95
97
  chatType: eventSession.chatType,
96
- session: eventSession.threadId ? 'thread' : 'main',
97
- ...(eventSession.threadId ? { threadId: eventSession.threadId } : {}),
98
+ // 只有话题让目标变成 `thread`;主话题用 `session: 'main'` 表达,不写 threadId(同 originFromActorSession)。
99
+ ...(eventSourceKind.kind === 'topic'
100
+ ? { session: 'thread', threadId: eventSourceKind.threadId }
101
+ : { session: 'main' }),
98
102
  };
99
103
  }
100
104
  if (!unresolvedTarget)
@@ -104,6 +108,9 @@ export class TriggerFeedbackDispatcher {
104
108
  return this.failed(feedback, [], 'route_missing', 'AUN target_session has no explicit delivery route');
105
109
  }
106
110
  const isTaskErrorRecovery = !!eventSession && input.sourcePayload?.type === 'task:error';
111
+ if (isTaskErrorRecovery && input.sourcePayload?.recoverable !== true) {
112
+ return { status: 'skipped', reason: 'not_recoverable', feedback, effects: [], error: null };
113
+ }
107
114
  if (isTaskErrorRecovery && (typeof input.sourcePayload?.taskId !== 'string' || !input.sourcePayload.taskId
108
115
  || !Number.isSafeInteger(input.sourcePayload.generation))) {
109
116
  return this.failed(feedback, [], 'recovery_event_invalid', 'task:error must identify the original task and generation');
@@ -155,7 +162,7 @@ export class TriggerFeedbackDispatcher {
155
162
  let session;
156
163
  try {
157
164
  const existingLookup = eventSession ?? (target.session === 'thread'
158
- ? target.threadId && this.deps.sessionManager.getThreadSession
165
+ ? isTopicThreadId(target.threadId) && this.deps.sessionManager.getThreadSession
159
166
  ? this.deps.sessionManager.getThreadSession(target.channelKey, target.channelId, target.threadId)
160
167
  : undefined
161
168
  : this.deps.sessionManager.getActiveSession
@@ -168,7 +175,7 @@ export class TriggerFeedbackDispatcher {
168
175
  session = existing;
169
176
  }
170
177
  else if (target.session === 'thread') {
171
- if (!target.threadId) {
178
+ if (!isTopicThreadId(target.threadId)) {
172
179
  return this.failed(feedback, [], 'target_missing', 'target thread id is missing');
173
180
  }
174
181
  const mainLookup = this.deps.sessionManager.getActiveSession?.(target.channelKey, target.channelId);
@@ -240,7 +247,9 @@ export class TriggerFeedbackDispatcher {
240
247
  channelId: target.channelId,
241
248
  selfAID: targetAgentAid,
242
249
  baseagent: session.baseagent || binding.baseagent,
243
- threadId: session.threadId || '',
250
+ // Message.threadId 是入站路由字段:只有话题才非空,主会话为 ''(与入站消息、
251
+ // task-recovery 一致)。下游 resolveSession / message-queue scope 以它区分主会话。
252
+ threadId: sessionTopicThreadId(session) ?? '',
244
253
  chatType: messageChatType,
245
254
  peerId: `trigger:${input.trigger.id}`,
246
255
  peerName: input.trigger.name,
@@ -282,6 +291,11 @@ export class TriggerFeedbackDispatcher {
282
291
  noopSentinel: input.trigger.execution.noopSentinel,
283
292
  authorizedBy: input.trigger.authorizedBy,
284
293
  labels: input.trigger.labels,
294
+ // 跨话题复查:标记本次投递的类型,执行侧据此走 T2 会话自身身份,
295
+ // 而不是 originPeerId(来源话题的对端)。
296
+ ...(input.trigger.labels?.kind === 'cross-thread-review'
297
+ ? { reviewKind: 'cross-thread-review' }
298
+ : {}),
285
299
  },
286
300
  };
287
301
  outcome = this.waitForTriggerOutcome(input.runId, input.attemptId, delivery.signal);
@@ -324,7 +338,7 @@ export class TriggerFeedbackDispatcher {
324
338
  const event = observed.outcome === 'completed'
325
339
  ? { ...base, type: 'trigger:completed', durationMs: Date.now() - startedAt }
326
340
  : observed.outcome === 'failed'
327
- ? { ...base, type: 'trigger:failed', error: observed.reason ?? 'target_session_failed', phase: 'execute' }
341
+ ? { ...base, type: 'trigger:failed', error: observed.reason ?? 'target_session_failed', recoverable: observed.recoverable, phase: 'execute' }
328
342
  : { ...base, type: 'trigger:skipped', reason: observed.reason ?? 'stale_generation',
329
343
  decisionSource: 'infrastructure', executionState: 'interrupted' };
330
344
  outcome?.settle(event);
@@ -423,7 +437,7 @@ export class TriggerFeedbackDispatcher {
423
437
  reason: event.phase,
424
438
  feedback,
425
439
  effects: [this.inboundEffect('success', target, session.id, startedAt)],
426
- error: { code: event.phase || 'target_session_failed', message: event.error },
440
+ error: { code: event.phase || 'target_session_failed', message: event.error, recoverable: event.recoverable },
427
441
  };
428
442
  }
429
443
  if (event?.type === 'trigger:skipped') {
@@ -473,7 +487,7 @@ export class TriggerFeedbackDispatcher {
473
487
  reason: err instanceof OrphanedTargetSessionError ? err.reason : 'enqueue_error',
474
488
  feedback,
475
489
  effects: [this.inboundEffect('failed', target, session.id, startedAt, undefined, err?.message || String(err))],
476
- error: { code: err instanceof OrphanedTargetSessionError ? err.reason : 'enqueue_error', message: err?.message || String(err) },
490
+ error: { code: err instanceof OrphanedTargetSessionError ? err.reason : 'enqueue_error', message: err?.message || String(err), recoverable: isRetryableError(err) },
477
491
  };
478
492
  }
479
493
  }
@@ -848,6 +862,7 @@ function errorFromReply(reply) {
848
862
  return {
849
863
  code: reply.error?.reason ?? reply.outcome,
850
864
  message: reply.error?.text ?? reply.outcome,
865
+ ...(reply.error?.recoverable !== undefined ? { recoverable: reply.error.recoverable } : {}),
851
866
  };
852
867
  }
853
868
  function originAsTarget(origin) {
@@ -864,8 +879,11 @@ function replyContextFromTarget(target) {
864
879
  const delivery = targetDeliveryFor(target);
865
880
  if (delivery)
866
881
  ctx.delivery = delivery;
867
- if (target.session === 'thread' && target.threadId)
882
+ // 话题 id 上线;主话题 id 只进本地日志(`localTopicId`),不进报文。
883
+ if (isTopicThreadId(target.threadId))
868
884
  ctx.threadId = target.threadId;
885
+ else if (target.threadId)
886
+ ctx.localTopicId = target.threadId;
869
887
  return Object.keys(ctx).length > 0 ? ctx : undefined;
870
888
  }
871
889
  function targetChatType(target) {
@@ -1,7 +1,7 @@
1
1
  import { getEventCatalog } from '../core/event-catalog.js';
2
- import { authorizeOperation, canApprovePersistentFullAccessTrigger, isCrossAgentTriggerOperationAllowed, } from '../core/auth/auth-gateway.js';
2
+ import { authorizeFreshOperation, canApprovePersistentFullAccessTrigger, isCrossAgentTriggerOperationAllowed, } from '../core/auth/auth-gateway.js';
3
3
  import { auditFullAccessEvent } from '../core/auth/authorization-audit.js';
4
- import { assertTriggerLocalFileAuthority, daemonPrivilegedTriggerReason, } from './authorization.js';
4
+ import { assertTriggerLocalFileAuthority, daemonPrivilegedTriggerReason, isTriggerOperator, originConversationPeerId, triggerOperatorPeerIds, } from './authorization.js';
5
5
  import { decodeTriggerScriptFile } from './local-files.js';
6
6
  import { applyTriggerPatch } from './patch.js';
7
7
  import { definitionRevision, normalizeTriggerDefinition, splitScriptCommand } from './validation.js';
@@ -106,6 +106,63 @@ export function createTriggerIpcExecutor(dependencies) {
106
106
  if (approvedActor && (agentAid !== approvedAgentAid || operation !== approvedOperation)) {
107
107
  throw new Error('Trigger operation does not match its approved argv');
108
108
  }
109
+ // Allow a trigger session to cancel, delete, or disable its own trigger.
110
+ // The trigger session's channelId is "trigger:<triggerId>", which fails the normal
111
+ // delegation peer check because it isn't a real AID. For these de-escalation
112
+ // operations we synthesise an actor directly from the trigger definition's origin,
113
+ // which already passed vetting when the trigger was created.
114
+ const isSelfDeescalation = !approvedActor
115
+ && triggerId
116
+ && (operation === 'trigger.cancel' || operation === 'trigger.delete' || operation === 'trigger.setEnabled')
117
+ && typeof cmd.actorSessionId === 'string';
118
+ if (isSelfDeescalation) {
119
+ const actorSession = await dependencies.getSessionById(cmd.actorSessionId);
120
+ const sessionChannelId = actorSession?.channelId;
121
+ if (sessionChannelId === `trigger:${triggerId}`) {
122
+ const definition = schedulerFor(agentAid).list({ all: true }).find(t => t.id === triggerId);
123
+ if (!definition?.origin?.peerId)
124
+ throw new Error('trigger not found or access denied');
125
+ const { buildAuthSubject } = await import('../core/auth/auth-gateway.js');
126
+ const { resolvePeerRoleDetail, roleToSessionIdentity } = await import('../config/peer-role-resolver.js');
127
+ const { tryParseChannelKey } = await import('../core/channel-loader.js');
128
+ const origin = definition.origin;
129
+ const channelType = origin.channelType || tryParseChannelKey(origin.channelKey)?.type || origin.channelKey;
130
+ const chatType = origin.delivery?.chatType ?? 'private';
131
+ const conversationId = chatType === 'group' ? origin.channelId : origin.peerId;
132
+ const roleDetail = resolvePeerRoleDetail({
133
+ selfAid: agentAid,
134
+ channelKey: origin.channelKey,
135
+ channelType,
136
+ chatType,
137
+ actorId: origin.peerId,
138
+ conversationId: conversationId,
139
+ });
140
+ const subject = buildAuthSubject({
141
+ selfAid: agentAid,
142
+ actorId: origin.peerId,
143
+ channel: origin.channelKey,
144
+ channelType,
145
+ channelId: origin.channelId,
146
+ chatType,
147
+ conversationId,
148
+ roleDetail,
149
+ identity: roleToSessionIdentity(roleDetail.effectiveRole),
150
+ processOwners: [],
151
+ fromControlChannel: false,
152
+ });
153
+ return {
154
+ origin,
155
+ management: false,
156
+ daemonOwner: false,
157
+ daemonPrivileged: false,
158
+ fullAccess: false,
159
+ selfAid: agentAid,
160
+ control: false,
161
+ role: roleDetail.effectiveRole || 'none',
162
+ subject,
163
+ };
164
+ }
165
+ }
109
166
  const actor = approvedActor ?? await dependencies.authenticateActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.delegationCommandHash, cmd.controlToken, contextOnly);
110
167
  if (actor.permissionMode === 'readonly' && !actor.fullAccess
111
168
  && !['trigger.list', 'trigger.show', 'trigger.history', 'trigger.eventCatalog'].includes(operation)) {
@@ -124,7 +181,7 @@ export function createTriggerIpcExecutor(dependencies) {
124
181
  throw new Error('cross-agent Trigger operations require DaemonOwner');
125
182
  throw new Error('trigger agent does not match authenticated task agent');
126
183
  }
127
- const operationDecision = authorizeOperation({
184
+ const operationDecision = await authorizeFreshOperation({
128
185
  source: actor.control ? 'control' : 'agent-tool',
129
186
  subject: actor.subject,
130
187
  intent: {
@@ -143,6 +200,10 @@ export function createTriggerIpcExecutor(dependencies) {
143
200
  });
144
201
  if (!operationDecision.allow)
145
202
  throw new Error(operationDecision.reason);
203
+ actor.daemonOwner = actor.subject.isDaemonOwner;
204
+ actor.daemonPrivileged = actor.control || actor.daemonOwner || actor.fullAccess;
205
+ actor.role = actor.subject.role;
206
+ actor.management = ['owner', 'admin'].includes(actor.role) || actor.daemonPrivileged;
146
207
  if (!triggerId)
147
208
  return actor;
148
209
  const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
@@ -157,9 +218,7 @@ export function createTriggerIpcExecutor(dependencies) {
157
218
  }
158
219
  if (actor.management)
159
220
  return actor;
160
- if (!definition
161
- || definition.origin?.peerId !== actor.origin?.peerId
162
- || definition.origin?.channelKey !== actor.origin?.channelKey) {
221
+ if (!isTriggerOperator(definition, actor.origin ? originConversationPeerId(actor.origin) : undefined)) {
163
222
  throw new Error('trigger not found or access denied');
164
223
  }
165
224
  return actor;
@@ -213,8 +272,7 @@ export function createTriggerIpcExecutor(dependencies) {
213
272
  const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => ((actor.daemonPrivileged
214
273
  || !daemonPrivilegedTriggerReason(trigger)
215
274
  || daemonPrivilegedTriggerReason(trigger) === 'persistent fullaccess Trigger')
216
- && (actor.management
217
- || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey))));
275
+ && (actor.management || isTriggerOperator(trigger, actor.origin ? originConversationPeerId(actor.origin) : undefined))));
218
276
  return { ok: true, triggers: scheduler.listItems(definitions) };
219
277
  }
220
278
  case 'trigger.show': {
@@ -269,7 +327,15 @@ export function createTriggerIpcExecutor(dependencies) {
269
327
  }
270
328
  const agentAid = requireAgent(rawDefinition.agentAid);
271
329
  const actor = await authorizeTrigger(agentAid, 'trigger.create');
272
- const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid, dependencies);
330
+ // 操作主体在创建时固定下来:origin 对端 + feedback 目标对端。跨话题复查的
331
+ // feedback 目标(C)因此也拿得到 cancel/update/show 权限,与 thread-ops
332
+ // 技能"在目标话题里结束复查"的流程一致。
333
+ const operatorPeers = triggerOperatorPeerIds({
334
+ origin: actor.origin,
335
+ feedback: rawDefinition.feedback,
336
+ operatorPeers: undefined,
337
+ });
338
+ const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin, operatorPeers }, agentAid, dependencies);
273
339
  let definition = normalizeTriggerDefinition(materialized);
274
340
  rejectUnapprovedFullAccessConfiguration(definition, actor);
275
341
  definition = withFullAccessConfigurationProvenance(definition, actor);
@@ -319,6 +385,8 @@ export function createTriggerIpcExecutor(dependencies) {
319
385
  }
320
386
  decodeTriggerScriptFile(scriptFileBase64);
321
387
  }
388
+ // 操作主体之间不做字段限制:operatorPeers 是创建者对"谁能管理这条 Trigger"的授权,
389
+ // 被授权方与 origin 侧拥有同样的修改权限。
322
390
  const currentRevision = definitionRevision(existing);
323
391
  if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
324
392
  throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
@@ -328,7 +396,15 @@ export function createTriggerIpcExecutor(dependencies) {
328
396
  : existing;
329
397
  rejectUnapprovedFullAccessConfiguration(definition, actor);
330
398
  definition = withFullAccessConfigurationProvenance(definition, actor);
331
- dependencies.validateTriggerDefinitionForActor(definition, actor);
399
+ // update 时只有 feedback 被修改才重新校验目标限制;
400
+ // 未改 feedback 的 patch(如只改 prompt)对已合法存在的 trigger 不应再执行创建时的
401
+ // feedbackTargetAllowedForNonManagement 检查,否则 operator peers 侧的 member
402
+ // 更新 prompt 会被拒。其他校验(permissionMode/model/effort)照常执行。
403
+ const feedbackChanged = 'feedback' in definitionPatch;
404
+ const actorForValidation = feedbackChanged || actor.management || actor.daemonPrivileged
405
+ ? actor
406
+ : { ...actor, management: true };
407
+ dependencies.validateTriggerDefinitionForActor(definition, actorForValidation);
332
408
  dependencies.validateTriggerFeedbackChannels(definition);
333
409
  const files = scriptFileBase64 !== undefined && existing.execution.script
334
410
  ? [{