evolcore 0.0.35 → 0.0.37

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 +42 -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 +177 -85
  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 +115 -36
  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
@@ -9,10 +9,12 @@ import { isValidAid as validSendAid } from '../aun/aid/validation.js';
9
9
  import { AGENT_DELEGATION_TOKEN_ENV, readDelegationCredential } from '../core/auth/agent-delegation.js';
10
10
  import { isManagementRole } from '../config/builtin-roles.js';
11
11
  import { readBestTaskRuntimeContext } from './task-context.js';
12
- import { assertEcSingleLine } from './cli-argv.js';
12
+ import { assertEcSingleLine, parseTimeArg, TIME_ARG_FORMATS } from './cli-argv.js';
13
13
  import { canonicalizeEvolcoreCommandArgv } from '../core/permission/ec-command-parser.js';
14
14
  import { ManagedGroupOperationError } from '../aun/msg/managed-operation.js';
15
15
  import { parseDuration } from '../trigger/parser.js';
16
+ import { isTopicThreadId, safeFormatMainTopicId } from '../core/thread/topic-id.js';
17
+ import { parseJsonObjectArgument } from '../core/permission/ec-command-parser.js';
16
18
  // ==================== AID ====================
17
19
  function resolveAunPath(args) {
18
20
  const idx = args.indexOf('--aun-path');
@@ -194,9 +196,9 @@ async function runManagedAid(args, formatJson) {
194
196
  console.log(`✓ 头像已更新${response.result.avatar ? `: ${response.result.avatar}` : ''}${response.result.sha256 ? ` (sha256: ${response.result.sha256})` : ''}`);
195
197
  }
196
198
  }
197
- function failManagedGroup(formatJson, code, error) {
199
+ function failManagedGroup(formatJson, code, error, details = {}) {
198
200
  if (formatJson)
199
- console.log(JSON.stringify({ ok: false, code, error }));
201
+ console.log(JSON.stringify({ ok: false, code, error, ...details }));
200
202
  else
201
203
  console.error(`❌ ${error}`);
202
204
  process.exit(1);
@@ -465,6 +467,8 @@ Commands:
465
467
  agentmd get <aid> 下载 agent.md → 验签 → 本地持久化
466
468
  agentmd set <aid> <field> <value> 修改一个字段并签名上传
467
469
  avatar <aid> --file <path> | --url <https-url> 上传头像并更新 agent.md
470
+ bind <ownerAid> [--subject <subjectAid>] 发起 Ownership 2.1 绑定
471
+ bind status [--subject <subjectAid>] 核验协议 Owner、任务和同步状态
468
472
 
469
473
  Options:
470
474
  --format json 输出 JSON 格式
@@ -485,6 +489,11 @@ Options:
485
489
  ec aid agentmd set mybot.agentid.pub description "A helpful agent"`);
486
490
  return;
487
491
  }
492
+ if (sub === 'bind') {
493
+ const { cmdAidBind } = await import('./aid-bind-command.js');
494
+ await cmdAidBind(args.slice(1));
495
+ return;
496
+ }
488
497
  const isAgentmdHelp = sub === 'agentmd' && (!args[1] || isHelpFlag(args[1]));
489
498
  if (process.env.EVOLCORE_SESSION_ID && !wantsHelp(args) && !isAgentmdHelp) {
490
499
  await runManagedAid(args, formatJson);
@@ -617,11 +626,11 @@ Options:
617
626
  process.exit(1);
618
627
  }
619
628
  try {
620
- const result = await aidCreate(aid, { aunPath, force });
629
+ const result = await aidCreate(aid, { aunPath, force, type: 'service' });
621
630
  let agentmdUploaded = result.alreadyExisted;
622
631
  let agentmdError;
623
632
  if (!result.alreadyExisted) {
624
- const content = buildInitialAgentMd({ aid });
633
+ const content = buildInitialAgentMd({ aid, type: 'service' });
625
634
  try {
626
635
  await agentmdPut(content, { aid, aunPath });
627
636
  agentmdUploaded = true;
@@ -1318,8 +1327,10 @@ Options:
1318
1327
  *
1319
1328
  * `ec msg send --thread T2 --check-every 5m` 发送成功后自动创建。
1320
1329
  *
1321
- * 使用 `interval` 而非 `delay`:周期触发,只有显式
1322
- * `ec trigger cancel` 或 `ec trigger update --every <新时长>` 才改变其状态。
1330
+ * 使用 `interval` 而非 `delay`:周期触发,由显式
1331
+ * `ec trigger delete`(结束,不留残留)/ `ec trigger update --every <新时长>`(调整周期)改变状态。
1332
+ * `ec trigger cancel` 只是 disable,定义仍留在磁盘上,复查这种用完即弃的 Trigger
1333
+ * 不该走 cancel,否则会积压大量永不触发的死定义。
1323
1334
  * 一次性 Trigger 会静默消失,无法承担「一直跟进直到显式结束」的语义。
1324
1335
  */
1325
1336
  export function buildThreadCheckTrigger(input) {
@@ -1332,21 +1343,24 @@ export function buildThreadCheckTrigger(input) {
1332
1343
  enabled: true,
1333
1344
  createdAt: now,
1334
1345
  updatedAt: now,
1335
- // T10: labels 携带 kind + 业务上下文,渲染层按 kind=thread-check 路由到专属模板
1336
1346
  labels: {
1337
- kind: 'thread-check',
1347
+ kind: 'cross-thread-review',
1338
1348
  originThreadId: input.originThreadId,
1339
1349
  targetThreadId: input.targetThreadId,
1340
1350
  originPeer: input.originPeer,
1341
1351
  checkEvery: input.checkEvery,
1352
+ ...(input.originalText ? { originalText: input.originalText } : {}),
1353
+ // labels 只收字符串(validation.ts 校验);scheduler 读取时用 Number() 还原。
1354
+ ...(input.firstSentAt ? { firstSentAt: String(input.firstSentAt) } : {}),
1355
+ ...(input.originThreadName ? { originThreadName: input.originThreadName } : {}),
1356
+ ...(input.targetThreadName ? { targetThreadName: input.targetThreadName } : {}),
1342
1357
  },
1343
1358
  ...(input.origin ? { origin: input.origin } : {}),
1344
1359
  // 周期触发:每 everyMs 唤醒一次,直到显式取消/调整
1345
1360
  source: { type: 'interval', everyMs: input.everyMs },
1346
1361
  execution: {
1347
- type: 'trigger_session',
1362
+ type: 'target_session',
1348
1363
  prompt: input.prompt,
1349
- thread: 'by_trigger',
1350
1364
  onError: 'retry',
1351
1365
  noopSentinel: '[[NOOP]]',
1352
1366
  },
@@ -1355,7 +1369,8 @@ export function buildThreadCheckTrigger(input) {
1355
1369
  target: {
1356
1370
  channelKey: input.channelKey,
1357
1371
  channelId: input.targetChannelId,
1358
- session: 'thread',
1372
+ // 话题走 `thread`;主话题走 `main`(threadId 是主话题规范 id,仅作身份记录)。
1373
+ session: isTopicThreadId(input.targetThreadId) ? 'thread' : 'main',
1359
1374
  threadId: input.targetThreadId,
1360
1375
  chatType: input.feedbackChatType ?? 'private',
1361
1376
  },
@@ -1363,6 +1378,41 @@ export function buildThreadCheckTrigger(input) {
1363
1378
  reliability: { concurrency: 'forbid', missedPolicy: 'run_once', retry: { maxAttempts: 0, backoffMs: 30_000 } },
1364
1379
  };
1365
1380
  }
1381
+ /**
1382
+ * 构造跨话题复查的通知正文模板。
1383
+ *
1384
+ * 关键约束(见 docs/thread-chain/cross-thread-review-design.md §3.4):
1385
+ * - 只带**数据上下文**(话题名/ID、时间、次数、周期、Trigger id、原消息),
1386
+ * **不带任何操作命令**——命令与决策判据由 thread-ops skill 提供;
1387
+ * - T1 的 threadId 必须列出:回 T1 汇报或查空洞期都要用它做 `--thread` / `history` 参数;
1388
+ * - T2 的 threadId 不列:收到通知的会话本身就在 T2;
1389
+ * - `【跨话题复查】` 是信封头关键字,供 skill 的关键词触发使用;
1390
+ * - 原消息放最后:正文可能较长,前置字段保持可扫读。
1391
+ *
1392
+ * 模板用 `{{...}}` 占位,触发时由 `renderTemplate` 渲染(scheduler.ts 每次 fire
1393
+ * 都会重新渲染,因此"第几次/已过多久"必须在渲染期计算,不能写死)。
1394
+ */
1395
+ export function buildCrossThreadReviewPrompt(input) {
1396
+ const truncated = input.originalText && input.originalText.length > 500
1397
+ ? `${input.originalText.slice(0, 500)}…`
1398
+ : input.originalText;
1399
+ return [
1400
+ '【跨话题复查】第 {{run.number}} 次',
1401
+ '',
1402
+ `来源话题:${input.originThreadName}(对端 ${input.originPeerName})`,
1403
+ `来源话题 ID:${input.originThreadId}`,
1404
+ `当前话题:${input.targetThreadName}(对端 ${input.targetPeerName})`,
1405
+ `发送于:{{trigger.labels.firstSentAtTime}}`,
1406
+ `已过:{{run.elapsed}}`,
1407
+ `复查周期:${input.checkEvery}(每 ${input.checkEvery} 提醒一次,直到处理)`,
1408
+ '',
1409
+ `Trigger:${input.triggerId}`,
1410
+ ...(input.extra?.trim() ? ['', input.extra.trim()] : []),
1411
+ '',
1412
+ '原消息:',
1413
+ truncated || '(非文本消息)',
1414
+ ].join('\n');
1415
+ }
1366
1416
  /**
1367
1417
  * 解析并校验 `--check-every`。返回 `{ durationMs }` 或错误。
1368
1418
  * 私聊与群聊两条跨话题发送路径共用,保证错误码一致。
@@ -1388,19 +1438,54 @@ function parseCheckEveryArgs(checkEveryRaw, hasCheckEvery) {
1388
1438
  * 设计见 `docs/thread-chain/thread-context-mechanism.md` §十、`thread-cli-spec-v2.md` §3.4。
1389
1439
  * 任意一步失败都不影响消息已发出的事实:返回 `triggerId` 为 undefined 时调用方照常输出发送结果。
1390
1440
  */
1441
+ /** Trigger 名称里的话题片段:主话题 id 缩成 `main`(名称不承担身份)。 */
1442
+ function triggerTopicLabel(topicId) {
1443
+ if (!topicId)
1444
+ return 'unknown';
1445
+ return isTopicThreadId(topicId) ? topicId : 'main';
1446
+ }
1391
1447
  async function applyCrossThreadSendEffects(input) {
1392
1448
  const runtime = await readBestTaskRuntimeContext();
1393
- const originThreadId = runtime?.threadId || 'main';
1394
1449
  const originPeer = runtime?.channelId || input.targetChannelId;
1450
+ // 来源话题 id:话题用原 id;主话题用"本端+对端"拼出的规范 id。
1451
+ // 这里给的是**展示与持久化用**的身份,不会写进线上报文。
1452
+ const originThreadId = isTopicThreadId(runtime?.threadId)
1453
+ ? runtime.threadId
1454
+ : (safeFormatMainTopicId(input.selfAid, originPeer) ?? '');
1395
1455
  const originSessionId = runtime?.sessionId;
1396
1456
  if (!originSessionId)
1397
1457
  return { inboxWritten: false };
1458
+ // 话题名:从索引取,取不到时降级到 threadId 或 'main'。
1459
+ // 从 index.js 聚合入口引入:单文件实现路径会绕过测试里对 '../core/thread/index.js'
1460
+ // 的 vi.mock,导致 mock 未覆盖时整个模块被真实加载而报错。
1461
+ // 取值全部包在 try/catch 里:话题名只是通知里的展示信息,索引读失败不能影响
1462
+ // "消息已经发出去了"这个事实,也不能阻止复查创建。
1463
+ let originThreadName = originThreadId || originPeer;
1464
+ let originPeerName = originPeer;
1465
+ let targetThreadName = input.targetThreadId;
1466
+ let targetPeerName = input.targetChannelId;
1467
+ try {
1468
+ const { ThreadService } = await import('../core/thread/index.js');
1469
+ const originEntry = isTopicThreadId(originThreadId)
1470
+ ? ThreadService.findThreadById(input.selfAid, originThreadId)
1471
+ : ThreadService.findMainSessionEntry?.(input.selfAid, originPeer);
1472
+ const targetEntry = isTopicThreadId(input.targetThreadId)
1473
+ ? ThreadService.findThreadById(input.selfAid, input.targetThreadId)
1474
+ : ThreadService.findMainSessionEntry?.(input.selfAid, input.targetChannelId);
1475
+ originThreadName = originEntry?.name || originThreadName;
1476
+ originPeerName = originEntry?.peerName || originPeerName;
1477
+ targetThreadName = targetEntry?.name || targetThreadName;
1478
+ targetPeerName = targetEntry?.peerName || targetPeerName;
1479
+ }
1480
+ catch {
1481
+ // 索引不可用时保留 threadId / AID 作为降级展示值
1482
+ }
1398
1483
  // Trigger 的 id 与 name 是两个不同的东西(见 src/trigger/validation.ts:727):
1399
- // id = `trig_<ts>_<8位hex>` —— 唯一的持久化标识,`ec trigger show/update/cancel` 只认它
1484
+ // id = `trig_<ts>_<8位hex>` —— 唯一的持久化标识,`ec trigger show/update/delete` 只认它
1400
1485
  // name = `check-<目标threadId>-from-<来源threadId>-<唯一后缀>` —— 人读标签,不参与定位
1401
1486
  //
1402
1487
  // 这里**在 IPC 之前**就把 id 定下来,有三个理由:
1403
- // 1. prompt 里要写 `ec trigger cancel <id>`,而 prompt 是 definition 的一部分,
1488
+ // 1. prompt 里要写 `ec trigger delete <id>`,而 prompt 是 definition 的一部分,
1404
1489
  // create 之前就得拼完,拿不到 daemon 的返回值;
1405
1490
  // 2. definition 带上 id 后 daemon 直接采用(normalizeV3Phase2 认 `raw.id`),
1406
1491
  // 落盘 id 与 prompt 中的 id 天然一致,不存在"返回值和 prompt 两套值"的漂移;
@@ -1412,24 +1497,19 @@ async function applyCrossThreadSendEffects(input) {
1412
1497
  // (目标, 来源) 对第二次发送就撞 `trigger name already exists`(assertUnique 按 name 去重),
1413
1498
  // 结果消息照发、inbox 照写、Trigger 静默不建 —— 完全无感知。
1414
1499
  // 每次 `--check-every` 都该有自己的 new Trigger(要复用应走 update),所以追加唯一后缀。
1415
- const triggerName = `check-${input.targetThreadId}-from-${originThreadId}-${triggerSeed.split('_')[1]}`;
1416
- const userPromptPart = input.triggerPromptExtra
1417
- ? input.triggerPromptExtra
1418
- : `请检查该话题的最新进度,根据情况:继续等待、调整周期(ec trigger update ${triggerId} --every <时长>)或结束复查(ec trigger cancel ${triggerId})。`;
1419
- const prompt = [
1420
- `【Trigger 复查】话题 ${input.targetThreadId}(对端 ${input.targetChannelId})`,
1421
- `这是周期复查:每 ${input.checkEveryLabel} 唤醒一次,直到你显式结束它。`,
1422
- ``,
1423
- userPromptPart,
1424
- ``,
1425
- `参考命令:`,
1426
- ` ec thread history ${input.targetChannelId}/${input.targetThreadId} --limit 10`,
1427
- ` ec thread history ${originPeer}/${originThreadId} --after ${Date.now()}`,
1428
- ` ec trigger cancel ${triggerId}`,
1429
- ` ec trigger update ${triggerId} --every <时长>`,
1430
- `Trigger id:${triggerId}`,
1431
- `Trigger 名称:${triggerName}`,
1432
- ].join('\n');
1500
+ const triggerName = `check-${triggerTopicLabel(input.targetThreadId)}-from-${triggerTopicLabel(originThreadId)}-${triggerSeed.split('_')[1]}`;
1501
+ const prompt = buildCrossThreadReviewPrompt({
1502
+ triggerId,
1503
+ originThreadId,
1504
+ originThreadName,
1505
+ originPeerName,
1506
+ targetThreadName,
1507
+ targetPeerName,
1508
+ checkEvery: input.checkEveryLabel,
1509
+ firstSentAt: input.timestamp,
1510
+ originalText: input.contentSummary,
1511
+ extra: input.triggerPromptExtra,
1512
+ });
1433
1513
  let createdTriggerId;
1434
1514
  try {
1435
1515
  const { formatChannelKey } = await import('../core/channel-loader.js');
@@ -1446,6 +1526,10 @@ async function applyCrossThreadSendEffects(input) {
1446
1526
  originThreadId,
1447
1527
  originPeer,
1448
1528
  feedbackChatType: input.feedbackChatType,
1529
+ originalText: input.contentSummary,
1530
+ firstSentAt: input.timestamp,
1531
+ originThreadName,
1532
+ targetThreadName,
1449
1533
  });
1450
1534
  const triggerRes = await ipcQuery(resolvePaths().socket, {
1451
1535
  type: 'trigger.create',
@@ -1498,6 +1582,7 @@ Commands:
1498
1582
  send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
1499
1583
  send <from> <to> --link <url> [--title T] 发送链接卡片
1500
1584
  send <from> <to> --payload <json> 发送自定义 payload
1585
+ request get <self-aid> <request-id> 查询已登记请求状态
1501
1586
  pull <from> [--after-seq N] [--limit N] --app <name> 拉取收件箱(必须使用独立 slot)
1502
1587
  ack <from> <seq> [--app <name>] 确认已读
1503
1588
  recall <from> <message-id> [<message-id>...] 撤回消息
@@ -1514,6 +1599,9 @@ Options:
1514
1599
  --app <name> 指定应用 slot(独立消费通道,不影响 daemon)
1515
1600
  --aun-path <path> 指定 AUN keystore 根
1516
1601
  --format json 输出 JSON 格式
1602
+ --payload '<json>' 发送任意 JSON payload,不添加聊天字段
1603
+ --return none|required 是否等待关联响应(required 建立请求记录)
1604
+ --timeout <duration> required 的等待期限和 AUN volatile TTL,如 30s、60s、2m(默认 60s,最大 3600s)
1517
1605
  --encrypt 强制端到端加密(未指定时使用 daemon 默认值)
1518
1606
  --no-encrypt 强制明文发送(优先于 --encrypt)
1519
1607
  --thread <id> 指定目标话题 ID(话题必须存在于本端索引;from/to 可省略)
@@ -1525,10 +1613,11 @@ Options:
1525
1613
  --channel-name <name> 历史渠道实例名(非 AUN 渠道必须匹配 Agent 配置)
1526
1614
  --channel-key <key> 非 AUN 已落盘渠道的完整实例键(与 --peer-id 成对)
1527
1615
  --peer-id <id> 非 AUN 渠道的原生对端 ID(与 --channel-key 成对)
1528
- --check-every <duration> 跨话题发送后自动创建**周期**复查 Trigger(如 5m/30m/2h),跨话题时**必填**
1529
- 每 duration 唤醒一次,直到显式 ec trigger cancel 结束
1616
+ --check-every <duration> 跨话题发送后自动创建**跨话题复查**(如 5m/30m/2h),跨话题时**必填**
1617
+ 每 duration 向**目标话题**投递一条【跨话题复查】通知(不新建会话),
1618
+ 由目标话题侧的 agent 在自己的上下文里决定结束/调整周期/继续
1530
1619
  Trigger 名:check-<目标threadId>-from-<来源threadId>-<唯一后缀>
1531
- 取消/调整用返回的 trigger_id(trig_…),show/update/cancel 只认 id
1620
+ 结束/调整用返回的 trigger_id(trig_…),show/update/delete 只认 id
1532
1621
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
1533
1622
  --as <type> 附件类型:image|video|voice|file(仅 --file 模式)
1534
1623
  --text <说明> 附件说明文字(仅 --file 模式)
@@ -1545,6 +1634,8 @@ Options:
1545
1634
  ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
1546
1635
  ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
1547
1636
  ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
1637
+ ec msg send alice.agentid.pub bob.agentid.pub --payload '{"type":"query"}' --return required --timeout 60s --format json
1638
+ ec msg request get alice.agentid.pub request-123 --format json
1548
1639
  ec msg pull alice.agentid.pub --app my-bot
1549
1640
  ec msg ack alice.agentid.pub 42 --app my-bot
1550
1641
  ec msg recall alice.agentid.pub msg-uuid-1 msg-uuid-2
@@ -1584,6 +1675,41 @@ Options:
1584
1675
  }
1585
1676
  // P1b.6.2: 托管会话 send 时,from/to 可省略——从 runtime 和话题索引推断
1586
1677
  // history --thread 时同理:from/to 均可省略
1678
+ //
1679
+ // 本端来源:托管会话里恒取**当前会话的本端**(`runtime.selfAid`)——这正是
1680
+ // "主话题 id 隐含本端"成立的前提。显式 `--self` 只用于宿主终端(下方分支),
1681
+ // 托管会话里写了就必须与当前本端一致,不允许拿它去冒充别的本端。
1682
+ if (sub === 'request' && args[1] === 'get') {
1683
+ const aid = args[2];
1684
+ const requestId = args[3];
1685
+ if (!aid || !requestId) {
1686
+ console.error('usage: ec msg request get <self-aid> <request-id> [--format json]');
1687
+ process.exit(1);
1688
+ }
1689
+ const runtime = await readBestTaskRuntimeContext();
1690
+ if (runtime?.selfAid && runtime.selfAid.replace(/^@/, '') !== aid) {
1691
+ console.error(`request status is limited to the delegated self AID: ${runtime.selfAid}`);
1692
+ process.exit(1);
1693
+ }
1694
+ const result = await ipcQuery(resolvePaths().socket, {
1695
+ type: 'aun-msg-request-get', aid, requestId,
1696
+ sessionId: process.env.EVOLCORE_SESSION_ID,
1697
+ delegationToken: readDelegationCredential(),
1698
+ }, 10_000);
1699
+ if (!result) {
1700
+ console.error('unable to connect to the EvolCore daemon');
1701
+ process.exit(1);
1702
+ }
1703
+ if (formatJson)
1704
+ console.log(JSON.stringify(result));
1705
+ else if (!result.ok)
1706
+ console.error(`request query failed: ${result.error}`);
1707
+ else
1708
+ console.log(`request_id=${requestId} state=${result.request?.state ?? 'unknown'} message_id=${result.request?.outboundMessageId ?? '-'}`);
1709
+ if (!result.ok)
1710
+ process.exit(1);
1711
+ return;
1712
+ }
1587
1713
  if (process.env.EVOLCORE_SESSION_ID && (sub === 'send' || (sub === 'history' && getArgValue(args, '--thread')))) {
1588
1714
  const { isValidAid: checkAid } = await import('../aun/aid/index.js');
1589
1715
  const hasFrom = typeof args[1] === 'string' && checkAid(args[1]);
@@ -1592,13 +1718,36 @@ Options:
1592
1718
  const runtime = await readBestTaskRuntimeContext();
1593
1719
  if (runtime?.selfAid) {
1594
1720
  const inferredFrom = runtime.selfAid.replace(/^@/, '');
1721
+ const explicitSelf = getArgValue(args, '--self')?.replace(/^@/, '');
1722
+ if (explicitSelf && explicitSelf !== inferredFrom) {
1723
+ console.error(`❌ 当前会话的本端是 ${inferredFrom},--self 不能指定 ${explicitSelf};跨本端发送只能由该本端自己的会话发起`);
1724
+ process.exit(1);
1725
+ }
1595
1726
  let inferredTo;
1596
1727
  const threadArg = getArgValue(args, '--thread');
1597
1728
  if (threadArg) {
1598
- const { ThreadService } = await import('../core/thread/index.js');
1599
- inferredTo = ThreadService.findThreadById(inferredFrom, threadArg)?.peer;
1729
+ // 目标对端只能从话题引用里推导:主话题 id 自带另一端,话题查本端索引。
1730
+ // 解析失败必须直接失败——回落到"当前对端"会把一条本应发往别处的消息
1731
+ // 静默投进当前会话。
1732
+ const { resolveThreadRef } = await import('../core/thread/topic-id.js');
1733
+ const resolved = resolveThreadRef(threadArg, inferredFrom);
1734
+ if (resolved.kind === 'main') {
1735
+ inferredTo = resolved.peer;
1736
+ }
1737
+ else if (resolved.kind === 'thread') {
1738
+ const { ThreadService } = await import('../core/thread/index.js');
1739
+ inferredTo = resolved.peer ?? ThreadService.findThreadById(inferredFrom, resolved.threadId)?.peer;
1740
+ }
1741
+ else if (resolved.kind === 'invalid') {
1742
+ console.error(`❌ ${resolved.error}`);
1743
+ process.exit(1);
1744
+ }
1745
+ if (!inferredTo) {
1746
+ console.error(`❌ 无法从话题引用确定目标对端: ${threadArg}(话题必须存在于本端索引)`);
1747
+ process.exit(1);
1748
+ }
1600
1749
  }
1601
- if (!inferredTo) {
1750
+ else {
1602
1751
  inferredTo = runtime.channelId?.replace(/^@/, '');
1603
1752
  }
1604
1753
  if (!hasFrom && !hasTo && inferredTo) {
@@ -1613,6 +1762,45 @@ Options:
1613
1762
  }
1614
1763
  }
1615
1764
  }
1765
+ // 宿主终端的简化式:`ec msg send '<正文>' --self <selfAid> --thread <ref>`
1766
+ // 没有托管会话上下文,本端只能由 --self 给出;对端由话题引用推导。
1767
+ // 这和托管会话里的简化式是同一语义,只是本端来源不同。
1768
+ if (sub === 'send' && !process.env.EVOLCORE_SESSION_ID) {
1769
+ const { isValidAid: checkAid } = await import('../aun/aid/index.js');
1770
+ const selfArg = getArgValue(args, '--self')?.replace(/^@/, '');
1771
+ const threadArg = getArgValue(args, '--thread');
1772
+ const hasFrom = typeof args[1] === 'string' && checkAid(args[1]);
1773
+ const hasTo = typeof args[2] === 'string' && checkAid(args[2]);
1774
+ if (threadArg && (!hasFrom || !hasTo)) {
1775
+ if (!selfArg || !checkAid(selfArg)) {
1776
+ console.error('❌ 省略 <from> <to> 时必须用 --self <本端AID> 指定本端(对端由 --thread 推导)');
1777
+ process.exit(1);
1778
+ }
1779
+ const { resolveThreadRef } = await import('../core/thread/topic-id.js');
1780
+ const resolved = resolveThreadRef(threadArg, selfArg);
1781
+ if (resolved.kind === 'invalid' || resolved.kind === 'current') {
1782
+ console.error(`❌ ${resolved.kind === 'invalid' ? resolved.error : '无效的话题引用'}`);
1783
+ process.exit(1);
1784
+ }
1785
+ let peer;
1786
+ if (resolved.kind === 'main')
1787
+ peer = resolved.peer;
1788
+ else {
1789
+ const { ThreadService } = await import('../core/thread/index.js');
1790
+ peer = resolved.peer ?? ThreadService.findThreadById(selfArg, resolved.threadId)?.peer;
1791
+ }
1792
+ if (!peer) {
1793
+ console.error(`❌ 无法从话题引用确定目标对端: ${threadArg}(话题必须存在于本端索引)`);
1794
+ process.exit(1);
1795
+ }
1796
+ if (!hasFrom && !hasTo)
1797
+ args.splice(1, 0, selfArg, peer);
1798
+ else if (!hasFrom)
1799
+ args.splice(1, 0, selfArg);
1800
+ else
1801
+ args.splice(2, 0, peer);
1802
+ }
1803
+ }
1616
1804
  const from = args[1];
1617
1805
  if (!from) {
1618
1806
  console.error('❌ 缺少 <from-aid> 参数');
@@ -1621,6 +1809,9 @@ Options:
1621
1809
  const { isValidAid } = await import('../aun/aid/index.js');
1622
1810
  if (!isValidAid(from)) {
1623
1811
  console.error(`❌ 无效 AID 格式: ${from}`);
1812
+ if (sub === 'send') {
1813
+ console.error('提示:宿主终端可用 `ec msg send \'<正文>\' --self <本端AID> --thread <话题id> --check-every 5m`');
1814
+ }
1624
1815
  process.exit(1);
1625
1816
  }
1626
1817
  if (process.env.EVOLCORE_SESSION_ID && sub !== 'send') {
@@ -1649,7 +1840,7 @@ Options:
1649
1840
  if (sub === 'history') {
1650
1841
  const target = args[2] && !args[2].startsWith('--') ? args[2] : undefined;
1651
1842
  const historyValueFlags = new Set([
1652
- '--session', '--limit', '--before', '--after', '--direction', '--channel', '--channel-name', '--format', '--channel-key', '--peer-id',
1843
+ '--session', '--limit', '--before', '--after', '--direction', '--channel', '--channel-name', '--format', '--channel-key', '--peer-id', '--thread', '--self',
1653
1844
  ]);
1654
1845
  const seenHistoryFlags = new Set();
1655
1846
  for (let index = target ? 3 : 2; index < args.length; index++) {
@@ -1679,14 +1870,14 @@ Options:
1679
1870
  const limit = limitRaw === undefined ? undefined : Number(limitRaw);
1680
1871
  const beforeRaw = getArgValue(args, '--before');
1681
1872
  const afterRaw = getArgValue(args, '--after');
1682
- const before = beforeRaw === undefined ? undefined : parseMsgHistoryTime(beforeRaw);
1683
- const after = afterRaw === undefined ? undefined : parseMsgHistoryTime(afterRaw);
1873
+ const before = beforeRaw === undefined ? undefined : parseTimeArg(beforeRaw);
1874
+ const after = afterRaw === undefined ? undefined : parseTimeArg(afterRaw);
1684
1875
  if (beforeRaw !== undefined && before === undefined) {
1685
- console.error(`❌ --before 必须是 epoch 毫秒或有效时间: ${beforeRaw}`);
1876
+ console.error(`❌ --before 不是有效时间: ${beforeRaw}(支持 ${TIME_ARG_FORMATS})`);
1686
1877
  process.exit(1);
1687
1878
  }
1688
1879
  if (afterRaw !== undefined && after === undefined) {
1689
- console.error(`❌ --after 必须是 epoch 毫秒或有效时间: ${afterRaw}`);
1880
+ console.error(`❌ --after 不是有效时间: ${afterRaw}(支持 ${TIME_ARG_FORMATS})`);
1690
1881
  process.exit(1);
1691
1882
  }
1692
1883
  const channelKey = getArgValue(args, '--channel-key');
@@ -1725,6 +1916,18 @@ Options:
1725
1916
  process.exit(1);
1726
1917
  }
1727
1918
  }
1919
+ // `--thread` 接受 `ec thread list` 打印的 `<peer>/<id>`;前缀必须就是本次查询的对端
1920
+ let historyThreadId;
1921
+ const historyThreadRaw = getArgValue(args, '--thread');
1922
+ if (historyThreadRaw !== undefined) {
1923
+ const { normalizeThreadOptionValue } = await import('../core/thread/topic-id.js');
1924
+ const normalized = normalizeThreadOptionValue(historyThreadRaw, peer => peer === target?.replace(/^@/, ''));
1925
+ if (!normalized.ok) {
1926
+ console.error(`❌ ${normalized.error}`);
1927
+ process.exit(1);
1928
+ }
1929
+ historyThreadId = normalized.threadId;
1930
+ }
1728
1931
  const result = msgHistory({
1729
1932
  from,
1730
1933
  ...(usesSelector
@@ -1735,7 +1938,7 @@ Options:
1735
1938
  before,
1736
1939
  after,
1737
1940
  direction: (getArgValue(args, '--direction') ?? 'all'),
1738
- threadId: getArgValue(args, '--thread'),
1941
+ threadId: historyThreadId,
1739
1942
  });
1740
1943
  if (!result.ok) {
1741
1944
  if (format === 'json')
@@ -1824,7 +2027,7 @@ Options:
1824
2027
  else if (payloadVal) {
1825
2028
  let parsed;
1826
2029
  try {
1827
- parsed = JSON.parse(payloadVal);
2030
+ parsed = parseJsonObjectArgument(payloadVal);
1828
2031
  }
1829
2032
  catch (e) {
1830
2033
  console.error(`❌ --payload 解析失败: ${e.message}`);
@@ -1845,27 +2048,50 @@ Options:
1845
2048
  ? false
1846
2049
  : args.includes('--encrypt') ? true : undefined;
1847
2050
  const thread = getArgValue(args, '--thread');
1848
- // P1b.6.5: 话题存在性校验 — --thread 指定的话题必须在本端索引里存在
2051
+ // P1b.6.5: 话题存在性校验 — 引用必须能解析成本端已存在的话题
1849
2052
  let threadEntry;
2053
+ // 复查 Trigger 里记录的"目标话题"必须是规范 id:话题原样,主话题用
2054
+ // `main:<a>:<b>`(`--thread` 可能写的是裸对端 AID 或 `<peer>/<id>` 历史写法)。
2055
+ let threadTopicId = thread ?? '';
1850
2056
  if (thread) {
1851
2057
  const { ThreadService, normalizeThreadPeer } = await import('../core/thread/index.js');
1852
- threadEntry = ThreadService.findThreadById(from, thread);
1853
- if (!threadEntry || normalizeThreadPeer(threadEntry.peer) !== normalizeThreadPeer(to)) {
1854
- if (formatJson) {
1855
- console.log(JSON.stringify({ ok: false, code: 'TOPIC_NOT_FOUND', error: `话题不存在: ${thread}` }));
1856
- }
1857
- else {
1858
- console.error(`❌ 话题不存在: ${thread}`);
1859
- }
2058
+ const { resolveThreadRef, isMainSessionThreadId } = await import('../core/thread/topic-id.js');
2059
+ const resolved = resolveThreadRef(thread, from);
2060
+ const fail = (message) => {
2061
+ if (formatJson)
2062
+ console.log(JSON.stringify({ ok: false, code: 'TOPIC_NOT_FOUND', error: message }));
2063
+ else
2064
+ console.error(`❌ ${message}`);
1860
2065
  process.exit(1);
2066
+ };
2067
+ if (resolved.kind === 'invalid' || resolved.kind === 'current')
2068
+ fail(resolved.kind === 'invalid' ? resolved.error : '无效的话题引用');
2069
+ else if (resolved.kind === 'main') {
2070
+ if (normalizeThreadPeer(resolved.peer) !== normalizeThreadPeer(to))
2071
+ fail(`主话题 ${thread} 的对端不是 ${to}`);
2072
+ const main = await ThreadService.findMainSessionEntry(from, resolved.peer);
2073
+ if (!main)
2074
+ fail(`与 ${resolved.peer} 无主话题会话`);
2075
+ threadEntry = main;
2076
+ threadTopicId = resolved.topicId;
2077
+ }
2078
+ else {
2079
+ threadEntry = ThreadService.findThreadById(from, resolved.threadId);
2080
+ const peer = resolved.peer ?? threadEntry?.peer;
2081
+ if (!threadEntry || !peer || normalizeThreadPeer(threadEntry.peer) !== normalizeThreadPeer(to)) {
2082
+ fail(`话题不存在: ${thread}`);
2083
+ }
2084
+ threadTopicId = resolved.threadId;
1861
2085
  }
1862
2086
  }
1863
- // T8: 跨话题发送必须显式指定复查周期(发送前校验,避免消息已发出才报错)
2087
+ // T8: 跨话题发送必须显式指定复查周期(发送前校验,避免消息已发出才报错)。
2088
+ // 仅托管会话(有 runtime.sessionId)才会创建复查 Trigger,宿主终端直发不受此限制。
1864
2089
  const checkEveryRaw = getArgValue(args, '--check-every');
1865
2090
  const hasCheckEvery = args.includes('--check-every') || checkEveryRaw !== undefined;
1866
2091
  const triggerPromptExtra = getArgValue(args, '--trigger-prompt');
1867
2092
  let checkEveryDurationMs;
1868
- if (thread) {
2093
+ const runtime = await readBestTaskRuntimeContext();
2094
+ if (thread && runtime?.sessionId) {
1869
2095
  const parsedCheck = parseCheckEveryArgs(checkEveryRaw, hasCheckEvery);
1870
2096
  if ('error' in parsedCheck) {
1871
2097
  if (formatJson)
@@ -1876,16 +2102,35 @@ Options:
1876
2102
  }
1877
2103
  checkEveryDurationMs = parsedCheck.durationMs;
1878
2104
  }
1879
- // --return 参数已移除:跨会话 send 不再需要指定返回策略(内部默认 none)。
1880
2105
  // SDK 尚未提供字节进度回调,只展示上传开始和成功完成。
2106
+ const returnPolicyRaw = getArgValue(args, '--return');
2107
+ if (returnPolicyRaw !== undefined && returnPolicyRaw !== 'none' && returnPolicyRaw !== 'required') {
2108
+ console.error('invalid --return; expected none|required');
2109
+ process.exit(1);
2110
+ }
2111
+ const timeoutRaw = getArgValue(args, '--timeout');
2112
+ if (timeoutRaw !== undefined && returnPolicyRaw !== 'required') {
2113
+ console.error('--timeout requires --return required');
2114
+ process.exit(1);
2115
+ }
2116
+ let timeoutMs;
2117
+ if (timeoutRaw !== undefined) {
2118
+ const parsedTimeout = parseDuration(timeoutRaw);
2119
+ if (parsedTimeout === null || parsedTimeout <= 0) {
2120
+ console.error(`invalid --timeout: ${timeoutRaw}`);
2121
+ process.exit(1);
2122
+ }
2123
+ timeoutMs = parsedTimeout;
2124
+ }
1881
2125
  const onUploadProgress = formatJson ? undefined : (info) => {
1882
2126
  if (info.phase === 'upload')
1883
2127
  process.stderr.write(' ⏫ uploading...\n');
1884
2128
  else if (info.phase === 'done')
1885
2129
  process.stderr.write(' ✓ upload complete\n');
1886
2130
  };
1887
- const runtime = await readBestTaskRuntimeContext();
1888
- const result = await msgSend({ from, to, body, encrypt, thread, handoffContext: getArgValue(args, '--handoff-context'), onUploadProgress, ...commonOpts });
2131
+ // `--thread` 的原文只用于解析;发出的必须是规范 id(历史写法 `<peer>/<id>`
2132
+ // 若原样上线,对端会把它当成一个不存在的话题 id)。
2133
+ const result = await msgSend({ from, to, body, encrypt, thread: thread ? threadTopicId : undefined, returnPolicy: returnPolicyRaw, timeoutMs, handoffContext: getArgValue(args, '--handoff-context'), onUploadProgress, ...commonOpts });
1889
2134
  if (!result.ok) {
1890
2135
  if (formatJson) {
1891
2136
  console.log(JSON.stringify(result));
@@ -1901,7 +2146,7 @@ Options:
1901
2146
  if (thread && threadEntry && result.message_id && runtime?.sessionId) {
1902
2147
  const effects = await applyCrossThreadSendEffects({
1903
2148
  selfAid: from,
1904
- targetThreadId: thread,
2149
+ targetThreadId: threadTopicId,
1905
2150
  targetSessionId: threadEntry.sessionId,
1906
2151
  targetChannelId: to,
1907
2152
  feedbackChatType: 'private',
@@ -1920,7 +2165,10 @@ Options:
1920
2165
  // E4: 把复查 Trigger ID 回填到返回值,兑现 spec §3.4 的 `{ message_id, trigger_id }` 契约。
1921
2166
  if (createdTriggerId)
1922
2167
  result.trigger_id = createdTriggerId;
1923
- if (formatJson) {
2168
+ if (result.request_id && !formatJson) {
2169
+ console.log(`request_id=${result.request_id} state=${result.request_state ?? '-'} message_id=${result.message_id ?? '-'}`);
2170
+ }
2171
+ else if (formatJson) {
1924
2172
  console.log(JSON.stringify(result));
1925
2173
  }
1926
2174
  else {
@@ -2178,10 +2426,11 @@ Options:
2178
2426
  --encrypt 强制端到端加密(未指定时使用 daemon 默认值;仅 send)
2179
2427
  --no-encrypt 强制明文发送(优先于 --encrypt;仅 send)
2180
2428
  --thread <id> 指定话题 ID(仅 send;话题必须存在于本端索引)
2181
- --check-every <时长> 跨话题发送(带 --thread)时**必填**,创建周期复查 Trigger(如 5m/30m/2h)
2429
+ --check-every <时长> 跨话题发送(带 --thread)时**必填**,创建跨话题复查(如 5m/30m/2h)
2430
+ 到期向**目标话题**投递【跨话题复查】通知,不新建会话
2182
2431
  无值时默认 5m;Trigger 名:check-<目标threadId>-from-<来源threadId>-<唯一后缀>
2183
- 取消/调整用返回的 trigger_id(trig_…),show/update/cancel 只认 id
2184
- --trigger-prompt <text> 自定义复查 Trigger prompt 中的用户段(仅 --check-every)
2432
+ 结束/调整用返回的 trigger_id(trig_…),show/update/delete 只认 id
2433
+ --trigger-prompt <text> 追加到复查通知正文里的补充说明(仅 --check-every)
2185
2434
  --mention <...> 发送时 @ 某个成员。单引号包住、逗号分隔多个 aid
2186
2435
  (如 --mention 'alice.agentid.pub,bob.agentid.pub'),可多次传入
2187
2436
  --mention-all 发送时 @ 所有人
@@ -2514,7 +2763,7 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2514
2763
  else if (payloadVal) {
2515
2764
  let parsed;
2516
2765
  try {
2517
- parsed = JSON.parse(payloadVal);
2766
+ parsed = parseJsonObjectArgument(payloadVal);
2518
2767
  }
2519
2768
  catch (e) {
2520
2769
  console.error(`❌ --payload 解析失败: ${e.message}`);
@@ -2550,12 +2799,35 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2550
2799
  const groupTriggerPromptExtra = getSendArgValue('--trigger-prompt');
2551
2800
  let groupCheckEveryMs;
2552
2801
  let groupThreadEntry;
2802
+ // 与私聊路径同口径:复查 Trigger 记录规范 id,不记录调用方写的引用原文。
2803
+ let groupTopicId = thread ?? '';
2553
2804
  if (thread) {
2554
- // 话题必须存在于本端索引(与私聊路径一致,不自动创建)。
2805
+ // 引用必须解析成本端已存在的话题(与私聊路径一致,不自动创建)。
2555
2806
  const { ThreadService } = await import('../core/thread/index.js');
2556
- groupThreadEntry = ThreadService.findThreadById(from, thread);
2557
- if (!groupThreadEntry || normalizeGroupAid(groupThreadEntry.peer, from.slice(from.indexOf('.') + 1)) !== groupId) {
2558
- failManagedGroup(formatJson, 'TOPIC_NOT_FOUND', `话题不存在: ${thread}`);
2807
+ const { resolveThreadRef } = await import('../core/thread/topic-id.js');
2808
+ const resolved = resolveThreadRef(thread, from);
2809
+ if (resolved.kind === 'invalid' || resolved.kind === 'current') {
2810
+ failManagedGroup(formatJson, 'TOPIC_NOT_FOUND', resolved.kind === 'invalid' ? resolved.error : '无效的话题引用');
2811
+ }
2812
+ else if (resolved.kind === 'main') {
2813
+ if (normalizeGroupAid(resolved.peer, from.slice(from.indexOf('.') + 1)) !== groupId) {
2814
+ failManagedGroup(formatJson, 'TOPIC_NOT_FOUND', `主话题 ${thread} 的对端不是 ${groupId}`);
2815
+ }
2816
+ groupThreadEntry = ThreadService.findMainSessionEntry(from, resolved.peer);
2817
+ if (!groupThreadEntry)
2818
+ failManagedGroup(formatJson, 'TOPIC_NOT_FOUND', `与 ${resolved.peer} 无主话题会话`);
2819
+ groupTopicId = resolved.topicId;
2820
+ }
2821
+ else {
2822
+ const entry = ThreadService.findThreadById(from, resolved.threadId);
2823
+ const peer = resolved.peer ?? entry?.peer;
2824
+ if (!entry || !peer
2825
+ || normalizeGroupAid(entry.peer, from.slice(from.indexOf('.') + 1)) !== groupId
2826
+ || normalizeGroupAid(peer, from.slice(from.indexOf('.') + 1)) !== groupId) {
2827
+ failManagedGroup(formatJson, 'TOPIC_NOT_FOUND', `话题不存在: ${thread}`);
2828
+ }
2829
+ groupThreadEntry = entry;
2830
+ groupTopicId = resolved.threadId;
2559
2831
  }
2560
2832
  const parsedCheck = parseCheckEveryArgs(groupCheckEveryRaw, groupHasCheckEvery);
2561
2833
  if ('error' in parsedCheck) {
@@ -2569,7 +2841,7 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2569
2841
  body,
2570
2842
  mentions: mentions.length ? mentions : undefined,
2571
2843
  encrypt: encryptGroup,
2572
- thread,
2844
+ thread: thread ? groupTopicId : undefined,
2573
2845
  ...commonOpts,
2574
2846
  });
2575
2847
  // T2.6: 群聊跨话题发送成功后,同样写入本端 inbox(与私聊路径一致)
@@ -2579,7 +2851,7 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2579
2851
  if (messageId) {
2580
2852
  const effects = await applyCrossThreadSendEffects({
2581
2853
  selfAid: from,
2582
- targetThreadId: thread,
2854
+ targetThreadId: groupTopicId,
2583
2855
  targetSessionId: groupThreadEntry.sessionId,
2584
2856
  targetChannelId: groupId,
2585
2857
  feedbackChatType: 'group',
@@ -3123,13 +3395,27 @@ async function runManagedSend(argv, formatJson) {
3123
3395
  if (!result)
3124
3396
  failManagedGroup(formatJson, 'AUN_DAEMON_UNAVAILABLE', 'Unable to connect to the EvolCore daemon');
3125
3397
  if (!result.ok)
3126
- failManagedGroup(formatJson, result.code ?? 'AUN_SEND_FAILED', result.error ?? 'Send failed');
3398
+ failManagedGroup(formatJson, result.code ?? 'AUN_SEND_FAILED', `${result.error ?? 'Send failed'}${result.request_id ? ` request_id=${result.request_id}` : ''}`, {
3399
+ ...(typeof result.detail_code === 'string' ? { detail_code: result.detail_code } : {}),
3400
+ ...(typeof result.retryable === 'boolean' ? { retryable: result.retryable } : {}),
3401
+ ...(typeof result.sent === 'boolean' ? { sent: result.sent } : {}),
3402
+ });
3127
3403
  if (formatJson)
3128
3404
  console.log(JSON.stringify(result));
3129
3405
  else if (result.handoff_id)
3130
3406
  console.log(`✓ handoff ${result.handoff_id} ${result.status === 'delivered' ? '已投递' : '已排队'} ${result.message_id ?? ''}`.trimEnd());
3407
+ else if (result.request_id)
3408
+ console.log(`sent message_id=${result.message_id ?? '-'} request_id=${result.request_id} state=${result.request_state ?? '-'}`);
3131
3409
  else
3132
3410
  console.log(`✓ 已发送 message_id=${result.message_id ?? '-'} seq=${result.seq ?? result.message?.seq ?? '-'}`);
3411
+ // 复查 Trigger 结果必须可见:消息已发出所以不改退出码(避免调用方重发),
3412
+ // 但没建成要明说,别让调用方以为"N 分钟后会自动复查"。
3413
+ if (!formatJson) {
3414
+ if (result.trigger_id)
3415
+ console.log(`✓ 复查 Trigger ${result.trigger_id}`);
3416
+ else if (result.trigger_error)
3417
+ process.stderr.write(`⚠ 复查 Trigger 未创建: ${result.trigger_error}(消息已发出,需要复查请手动 ec trigger create)\n`);
3418
+ }
3133
3419
  }
3134
3420
  export function resolveManagedSendArgv(argv) {
3135
3421
  if (!['msg', 'group'].includes(argv[0]) || argv[1] !== 'send' || !validSendAid(argv[2]))
@@ -3143,7 +3429,7 @@ export function resolveManagedSendArgv(argv) {
3143
3429
  const booleans = new Set();
3144
3430
  const text = [];
3145
3431
  const mentions = [];
3146
- const valueFlags = new Set(['--format', '--file', '--payload', '--text-from-file', '--link', '--title', '--description', '--as', '--content-type', '--text', '--transcript', '--thread', '--check-every', '--trigger-prompt', '--return', '--handoff-context', '--mention']);
3432
+ const valueFlags = new Set(['--format', '--file', '--payload', '--text-from-file', '--link', '--title', '--description', '--as', '--content-type', '--text', '--transcript', '--thread', '--check-every', '--trigger-prompt', '--return', '--timeout', '--handoff-context', '--mention', '--self']);
3147
3433
  const booleanFlags = new Set(['--encrypt', '--no-encrypt', '--mention-all']);
3148
3434
  for (let index = 4; index < argv.length; index++) {
3149
3435
  const flag = argv[index];
@@ -3182,9 +3468,28 @@ export function resolveManagedSendArgv(argv) {
3182
3468
  const returnPolicy = values.get('--return');
3183
3469
  if (returnPolicy !== undefined && !['none', 'required'].includes(returnPolicy))
3184
3470
  throw new Error('Invalid return policy');
3471
+ if (scope === 'group' && returnPolicy === 'required')
3472
+ throw new Error('--return required is supported for private msg sends only');
3473
+ const timeoutRaw = values.get('--timeout');
3474
+ if (timeoutRaw !== undefined && returnPolicy !== 'required')
3475
+ throw new Error('--timeout requires --return required');
3476
+ let timeoutMs;
3477
+ if (timeoutRaw !== undefined) {
3478
+ const parsedTimeout = parseDuration(timeoutRaw);
3479
+ if (parsedTimeout === null || parsedTimeout <= 0)
3480
+ throw new Error('Invalid --timeout duration');
3481
+ if (returnPolicy === 'required' && parsedTimeout > 3_600_000)
3482
+ throw new Error('required request timeout cannot exceed 3600s for volatile AUN messages');
3483
+ timeoutMs = parsedTimeout;
3484
+ }
3185
3485
  const handoffContext = values.get('--handoff-context');
3186
3486
  if (handoffContext !== undefined && (scope !== 'msg' || !handoffContext.trim() || handoffContext.length > 65536))
3187
3487
  throw new Error('Invalid private handoff context');
3488
+ // `--self` 在托管发送里只作冗余声明:本端必须就是当前会话的本端。
3489
+ const declaredSelf = values.get('--self')?.replace(/^@/, '');
3490
+ if (declaredSelf !== undefined && declaredSelf !== aid) {
3491
+ throw new Error(`--self ${declaredSelf} 与当前会话本端 ${aid} 不一致`);
3492
+ }
3188
3493
  const thread = values.get('--thread');
3189
3494
  const checkEvery = values.get('--check-every');
3190
3495
  const triggerPromptExtra = values.get('--trigger-prompt');
@@ -3217,9 +3522,7 @@ export function resolveManagedSendArgv(argv) {
3217
3522
  file = { filePath: values.get('--file'), as, contentType: values.get('--content-type'), text: values.get('--text'), transcript: values.get('--transcript') };
3218
3523
  }
3219
3524
  else if (values.has('--payload')) {
3220
- payload = JSON.parse(values.get('--payload'));
3221
- if (!payload || typeof payload !== 'object' || Array.isArray(payload))
3222
- throw new Error('--payload must be a JSON object');
3525
+ payload = parseJsonObjectArgument(values.get('--payload'));
3223
3526
  }
3224
3527
  else if (values.has('--link')) {
3225
3528
  assertEcSingleLine(values.get('--title'), '--title');
@@ -3235,9 +3538,9 @@ export function resolveManagedSendArgv(argv) {
3235
3538
  throw new Error('Attachment options require --file');
3236
3539
  if (booleans.has('--mention-all'))
3237
3540
  mentions.push({ scope: 'all' });
3238
- return { aid, to, scope, payload, file, textFile: values.get('--text-from-file'),
3541
+ return { aid, to, scope, payload, rawPayload: values.has('--payload'), file, textFile: values.get('--text-from-file'),
3239
3542
  encrypt: booleans.has('--no-encrypt') ? false : booleans.has('--encrypt') ? true : undefined,
3240
- thread, checkEvery, checkEveryMs: checkEveryMs ?? undefined, triggerPromptExtra, returnPolicy: returnPolicy, handoffContext,
3543
+ thread, checkEvery, checkEveryMs: checkEveryMs ?? undefined, triggerPromptExtra, returnPolicy: returnPolicy, timeoutMs, handoffContext,
3241
3544
  mentions: mentions.length ? mentions : undefined };
3242
3545
  }
3243
3546
  /**
@@ -3259,7 +3562,8 @@ const VALUE_FLAGS = new Set([
3259
3562
  '--as', '--content-type', '--mention', '--visibility', '--join-mode',
3260
3563
  '--group-id', '--name', '--message', '--answer', '--page', '--size',
3261
3564
  '--member-type',
3262
- '--aun-path', '--thread', '--handoff-context',
3565
+ '--aun-path', '--thread', '--handoff-context', '--self',
3566
+ '--return', '--timeout',
3263
3567
  // 跨话题发送的复查参数:漏登记会让 collectPositional 把它们当正文(正文污染)
3264
3568
  '--check-every', '--trigger-prompt',
3265
3569
  ]);
@@ -3290,17 +3594,6 @@ function collectPositional(args, startIdx, extraValueFlags) {
3290
3594
  }
3291
3595
  return out;
3292
3596
  }
3293
- function parseMsgHistoryTime(value) {
3294
- const trimmed = value.trim();
3295
- if (!trimmed)
3296
- return undefined;
3297
- if (/^-?\d+$/.test(trimmed)) {
3298
- const timestamp = Number(trimmed);
3299
- return Number.isSafeInteger(timestamp) ? timestamp : undefined;
3300
- }
3301
- const timestamp = Date.parse(trimmed);
3302
- return Number.isFinite(timestamp) ? timestamp : undefined;
3303
- }
3304
3597
  /** Resolve a configured channel instance without exposing Agent session paths. */
3305
3598
  function resolveHistoryChannelName(agentAid, channel, requested) {
3306
3599
  if (channel === 'aun' && requested === undefined)