evolcore 0.0.23 → 0.0.25

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 (245) hide show
  1. package/CHANGELOG.md +583 -516
  2. package/LICENSE +21 -21
  3. package/README.md +455 -453
  4. package/bin/codex-managed-acl.ps1 +160 -155
  5. package/bin/codex-managed-hook.mjs +209 -92
  6. package/bin/ec-safe-output.js +327 -245
  7. package/bin/ec.js +29 -29
  8. package/bin/install-codex-managed-hooks.mjs +555 -544
  9. package/dist/agents/baseagent-availability.js +4 -4
  10. package/dist/agents/baseagent.js +29 -1
  11. package/dist/agents/claude-runner.js +152 -14
  12. package/dist/agents/codex/app-server-client.js +18 -8
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +215 -0
  15. package/dist/agents/codex-runner.js +445 -63
  16. package/dist/agents/ecagent-runner.js +31 -16
  17. package/dist/agents/gemini-runner.js +82 -4
  18. package/dist/agents/runner-types.js +1 -1
  19. package/dist/aun/aid/agentmd.js +56 -4
  20. package/dist/aun/aid/index.js +1 -0
  21. package/dist/aun/aid/managed-operation.js +80 -6
  22. package/dist/aun/aid/store.js +41 -0
  23. package/dist/aun/msg/managed-operation.js +6 -1
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +4 -5
  26. package/dist/aun/storage/client.js +4 -1
  27. package/dist/aun/storage/index.js +1 -1
  28. package/dist/channels/aun.js +147 -76
  29. package/dist/channels/daemon.js +7 -1
  30. package/dist/channels/dingtalk.js +1 -0
  31. package/dist/channels/feishu.js +10 -4
  32. package/dist/channels/wecom.js +2 -1
  33. package/dist/cli/agent-command.js +214 -89
  34. package/dist/cli/agent-create.js +576 -0
  35. package/dist/cli/agent.js +129 -676
  36. package/dist/cli/aun-commands.js +419 -313
  37. package/dist/cli/bench.js +20 -20
  38. package/dist/cli/config.js +20 -20
  39. package/dist/cli/contact.js +10 -10
  40. package/dist/cli/ctl-command.js +22 -20
  41. package/dist/cli/daemon-commands.js +163 -32
  42. package/dist/cli/fs-command.js +67 -67
  43. package/dist/cli/handoff-command.js +7 -7
  44. package/dist/cli/index.js +79 -79
  45. package/dist/cli/link-rules.js +21 -21
  46. package/dist/cli/managed-command-guard.js +3 -0
  47. package/dist/cli/menu-client.js +11 -11
  48. package/dist/cli/net-check.js +22 -22
  49. package/dist/cli/queue-command.js +29 -29
  50. package/dist/cli/response.js +30 -30
  51. package/dist/cli/restart-monitor.js +462 -38
  52. package/dist/cli/stats.js +161 -51
  53. package/dist/cli/trigger-command.js +56 -56
  54. package/dist/config/builtin-roles.js +3 -0
  55. package/dist/config/config-manager.js +15 -0
  56. package/dist/config/config-operation-service.js +20 -3
  57. package/dist/config/resolved-config-op.js +39 -1
  58. package/dist/core/auth/auth-gateway.js +3 -2
  59. package/dist/core/auth/authorization-audit.js +72 -19
  60. package/dist/core/auth/operation-authorizer.js +125 -7
  61. package/dist/core/auth/operation-catalog.js +26 -2
  62. package/dist/core/auth/remote-access-store.js +3 -2
  63. package/dist/core/baseagent-loader.js +4 -4
  64. package/dist/core/bootstrap-messages.js +39 -18
  65. package/dist/core/bootstrap-service.js +32 -26
  66. package/dist/core/capability/capability-manager.js +8 -4
  67. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  68. package/dist/core/command/agent-control.js +60 -26
  69. package/dist/core/command/cli-intent-parser.js +18 -0
  70. package/dist/core/command/command-handler.js +173 -36
  71. package/dist/core/command/menu-catalog.js +4 -1
  72. package/dist/core/command/menu-handler.js +315 -75
  73. package/dist/core/command/menu-protocol.js +7 -2
  74. package/dist/core/command/slash-gate.js +1 -3
  75. package/dist/core/command/slash-handler.js +166 -57
  76. package/dist/core/data-migration.js +5 -15
  77. package/dist/core/event-catalog.js +8 -0
  78. package/dist/core/evolagent.js +3 -3
  79. package/dist/core/handoff/runtime.js +3 -1
  80. package/dist/core/handoff/store.js +7 -1
  81. package/dist/core/message/im-renderer.js +13 -6
  82. package/dist/core/message/message-bridge.js +5 -36
  83. package/dist/core/message/message-utils.js +6 -3
  84. package/dist/core/message/response-engine.js +85 -27
  85. package/dist/core/model/catalog-service.js +3 -1
  86. package/dist/core/model/model-catalog.js +10 -0
  87. package/dist/core/model/model-permission.js +4 -1
  88. package/dist/core/permission/approval-gateway.js +169 -33
  89. package/dist/core/permission/tool-policy.js +56 -10
  90. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  91. package/dist/core/session/session-manager.js +49 -13
  92. package/dist/core/session/session-renew.js +1 -0
  93. package/dist/index.js +327 -134
  94. package/dist/ipc.js +68 -21
  95. package/dist/paths.js +23 -1
  96. package/dist/stats/billing.js +18 -4
  97. package/dist/stats/db.js +159 -159
  98. package/dist/stats/eck-vars.js +9 -9
  99. package/dist/stats/query.js +139 -139
  100. package/dist/stats/role-budget.js +6 -6
  101. package/dist/stats/writer.js +51 -51
  102. package/dist/utils/atomic-write.js +26 -2
  103. package/dist/utils/avatar-upload.js +282 -23
  104. package/dist/utils/cross-platform.js +436 -1
  105. package/dist/utils/error-dict.json +160 -160
  106. package/dist/utils/error-utils.js +11 -4
  107. package/dist/utils/macos-autostart.js +22 -22
  108. package/dist/utils/model-prices.jsonl +23 -20
  109. package/dist/utils/npm-ops.js +24 -1
  110. package/dist/utils/welcome.js +22 -22
  111. package/dist/utils/windows-autostart.js +1 -1
  112. package/ecagent/LICENSE +21 -21
  113. package/ecagent/NOTICE +11 -11
  114. package/ecagent/README.md +61 -61
  115. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  116. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  117. package/ecagent/dist/harness/messages.js +7 -7
  118. package/kits/docs/GUIDE.md +20 -20
  119. package/kits/docs/INDEX.md +66 -66
  120. package/kits/docs/aun/CHEATSHEET.md +19 -19
  121. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  122. package/kits/docs/channels/aun.md +56 -56
  123. package/kits/docs/channels/feishu.md +56 -56
  124. package/kits/docs/context-assembly.md +365 -364
  125. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  126. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  127. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  128. package/kits/docs/eck_templates/runtime.template.md +19 -19
  129. package/kits/docs/evolcore/INDEX.md +71 -69
  130. package/kits/docs/evolcore/agent-create.md +170 -156
  131. package/kits/docs/evolcore/agent.md +83 -73
  132. package/kits/docs/evolcore/aid.md +64 -62
  133. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  134. package/kits/docs/evolcore/config.md +195 -195
  135. package/kits/docs/evolcore/contact.md +68 -68
  136. package/kits/docs/evolcore/ctl.md +46 -46
  137. package/kits/docs/evolcore/event.md +216 -216
  138. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  139. package/kits/docs/evolcore/fs.md +110 -110
  140. package/kits/docs/evolcore/group-collaboration.md +243 -243
  141. package/kits/docs/evolcore/group-fs.md +17 -17
  142. package/kits/docs/evolcore/group-rules.md +251 -251
  143. package/kits/docs/evolcore/group.md +152 -152
  144. package/kits/docs/evolcore/menu-client.md +34 -34
  145. package/kits/docs/evolcore/model.md +50 -50
  146. package/kits/docs/evolcore/msg.md +165 -165
  147. package/kits/docs/evolcore/response.md +75 -75
  148. package/kits/docs/evolcore/rpc.md +37 -37
  149. package/kits/docs/evolcore/self-summary.md +29 -29
  150. package/kits/docs/evolcore/stats.md +83 -83
  151. package/kits/docs/evolcore/storage.md +50 -50
  152. package/kits/docs/evolcore/trigger.md +575 -575
  153. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  154. package/kits/docs/identity/PATH_OPS.md +16 -16
  155. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  156. package/kits/docs/identity/identity-tools.md +26 -26
  157. package/kits/docs/path-registry.md +62 -62
  158. package/kits/docs/prompt-loading-architecture.md +270 -269
  159. package/kits/docs/venues/aun-group.md +45 -45
  160. package/kits/docs/venues/aun-private.md +10 -10
  161. package/kits/docs/venues/client-desktop.md +10 -10
  162. package/kits/docs/venues/client-mobile.md +10 -10
  163. package/kits/docs/venues/feishu-group.md +13 -13
  164. package/kits/docs/venues/feishu-private.md +9 -9
  165. package/kits/docs/venues/group.md +25 -25
  166. package/kits/docs/venues/private.md +10 -10
  167. package/kits/eck_manifest.auxiliary.json +43 -43
  168. package/kits/eck_manifest.json +195 -183
  169. package/kits/eck_message_manifest.json +74 -74
  170. package/kits/migrations/README-role-config-v4.md +32 -32
  171. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  172. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  173. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  174. package/kits/migrations/rename-config-file.mjs +99 -99
  175. package/kits/rules/01-overview.md +162 -162
  176. package/kits/rules/02-navigation.md +96 -96
  177. package/kits/rules/03-identity.md +39 -37
  178. package/kits/rules/04-relation.md +75 -75
  179. package/kits/rules/05-venue.md +60 -60
  180. package/kits/rules/06-channel.md +63 -63
  181. package/kits/schemas/_meta.json +53 -53
  182. package/kits/schemas/agent-config.schema.1.json +177 -177
  183. package/kits/schemas/agent-config.schema.10.json +395 -395
  184. package/kits/schemas/agent-config.schema.11.json +421 -421
  185. package/kits/schemas/agent-config.schema.12.json +427 -427
  186. package/kits/schemas/agent-config.schema.2.json +239 -239
  187. package/kits/schemas/agent-config.schema.3.json +119 -119
  188. package/kits/schemas/agent-config.schema.4.json +208 -208
  189. package/kits/schemas/agent-config.schema.5.json +326 -326
  190. package/kits/schemas/agent-config.schema.6.json +322 -322
  191. package/kits/schemas/agent-config.schema.7.json +303 -303
  192. package/kits/schemas/agent-config.schema.8.json +304 -304
  193. package/kits/schemas/agent-config.schema.9.json +364 -364
  194. package/kits/schemas/contact-book.schema.1.json +36 -36
  195. package/kits/schemas/contact-book.schema.2.json +43 -43
  196. package/kits/schemas/contact-book.schema.3.json +67 -67
  197. package/kits/schemas/daemon.schema.1.json +91 -91
  198. package/kits/schemas/daemon.schema.2.json +101 -101
  199. package/kits/schemas/daemon.schema.3.json +123 -123
  200. package/kits/schemas/daemon.schema.4.json +136 -136
  201. package/kits/schemas/daemon.schema.5.json +135 -135
  202. package/kits/schemas/daemon.schema.6.json +131 -131
  203. package/kits/schemas/defaults.schema.1.json +81 -81
  204. package/kits/schemas/defaults.schema.2.json +85 -85
  205. package/kits/schemas/defaults.schema.3.json +73 -73
  206. package/kits/schemas/defaults.schema.4.json +107 -107
  207. package/kits/schemas/defaults.schema.5.json +119 -119
  208. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  209. package/kits/schemas/migrations/README.md +30 -30
  210. package/kits/schemas/relation-config.schema.1.json +158 -158
  211. package/kits/schemas/relation-config.schema.2.json +73 -73
  212. package/kits/schemas/relation-config.schema.3.json +50 -50
  213. package/kits/schemas/relation-config.schema.4.json +47 -47
  214. package/kits/schemas/relation-config.schema.5.json +47 -47
  215. package/kits/schemas/relation-config.schema.6.json +46 -46
  216. package/kits/schemas/relation-config.schema.7.json +59 -59
  217. package/kits/schemas/relation-config.schema.8.json +103 -103
  218. package/kits/schemas/role-config.schema.1.json +201 -201
  219. package/kits/schemas/role-registry.schema.1.json +35 -35
  220. package/kits/schemas/single-session.schema.1.json +31 -31
  221. package/kits/schemas/single-session.schema.2.json +57 -57
  222. package/kits/schemas/single-session.schema.3.json +32 -32
  223. package/kits/templates/bootstrap-welcome.md +17 -17
  224. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  225. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  226. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  227. package/kits/templates/message-fragments/inject-default.md +2 -2
  228. package/kits/templates/message-fragments/item.md +2 -2
  229. package/kits/templates/roles/admin.json +96 -82
  230. package/kits/templates/roles/member.json +97 -96
  231. package/kits/templates/roles/owner.json +9 -9
  232. package/kits/templates/roles/visitor.json +86 -85
  233. package/kits/templates/system-fragments/baseagent.md +14 -14
  234. package/kits/templates/system-fragments/bootstrap.md +38 -29
  235. package/kits/templates/system-fragments/channel.md +64 -64
  236. package/kits/templates/system-fragments/commands.md +28 -28
  237. package/kits/templates/system-fragments/identity.md +13 -13
  238. package/kits/templates/system-fragments/relation.md +19 -19
  239. package/kits/templates/system-fragments/session.md +59 -59
  240. package/kits/templates/system-fragments/venue.md +31 -31
  241. package/package.json +98 -94
  242. package/AGENTS.md +0 -62
  243. package/dist/utils/codex-cli.js +0 -43
  244. package/skills/eclink/SKILL.md +0 -84
  245. package/skills/eclink/agents/openai.yaml +0 -4
@@ -25,8 +25,11 @@ export function buildMenuClientProtectedHeaders(input) {
25
25
  ...(input.remoteAccessToken || input.ecwebToken ? { auth_method: 'remote-access-token', remote_access_token: input.remoteAccessToken ?? input.ecwebToken } : {}),
26
26
  };
27
27
  }
28
+ const appClientVersion = input.clientVersion?.trim();
28
29
  return {
30
+ ...(clientName === 'Evol App' ? { client_name: clientName } : {}),
29
31
  ...(input.evolVersion ? { evol_version: input.evolVersion } : {}),
32
+ ...(clientName === 'Evol App' && appClientVersion ? { client_version: appClientVersion } : {}),
30
33
  ...(input.remoteAccessToken || input.ecwebToken ? { auth_method: 'remote-access-token', remote_access_token: input.remoteAccessToken ?? input.ecwebToken } : {}),
31
34
  };
32
35
  }
@@ -40,7 +43,7 @@ export function parseMenuClientProtectedHeaders(value) {
40
43
  normalized[key.trim().toLowerCase()] = raw;
41
44
  }
42
45
  const result = {};
43
- if (normalized.client_name === 'evol' || normalized.client_name === 'ec-menu-client')
46
+ if (normalized.client_name === 'evol' || normalized.client_name === 'Evol App' || normalized.client_name === 'ec-menu-client')
44
47
  result.client_name = normalized.client_name;
45
48
  else if (normalized.client_name !== undefined)
46
49
  result.client_name = normalized.client_name;
@@ -71,7 +74,7 @@ export function evaluateMenuClientCompatibility(input) {
71
74
  }
72
75
  return null;
73
76
  }
74
- if (clientName !== 'evol') {
77
+ if (clientName !== 'evol' && clientName !== 'Evol App') {
75
78
  return { code: 'INVALID_CLIENT', message: `Unsupported Menu client: ${String(clientName)}` };
76
79
  }
77
80
  return evaluateEvolMenuVersionGate(input);
@@ -104,6 +107,7 @@ export const MENU_NAME_COMMANDS = Object.freeze({
104
107
  session: '/session',
105
108
  topic: '/topic',
106
109
  baseagent: '/baseagent',
110
+ 'baseagent-models': '/baseagent-models',
107
111
  model: '/model',
108
112
  effort: '/effort',
109
113
  chatmode: '/chatmode',
@@ -116,6 +120,7 @@ export const MENU_NAME_COMMANDS = Object.freeze({
116
120
  system: '/system',
117
121
  cli: '/cli',
118
122
  agent: '/agent',
123
+ aid: '/aid',
119
124
  trigger: '/trigger',
120
125
  file: '/file',
121
126
  gateway: '/gateway',
@@ -119,10 +119,8 @@ export function guardThreadCommand(content, threadId) {
119
119
  const threadBlocked = ['/new', '/slist', '/s', '/session', '/fork', '/del', '/repair'];
120
120
  const hasCommandArgument = (command) => new RegExp(`^${command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`).test(content);
121
121
  const isBlocked = threadBlocked.some(hasCommandArgument);
122
- const isBaseagentSwitch = /^\/baseagent\s/.test(content);
123
- const isBaseAliasSwitch = /^\/base\s/.test(content);
124
122
  const isRewindMutation = /^\/rewind\s/.test(content);
125
- if (!isBlocked && !isBaseagentSwitch && !isBaseAliasSwitch && !isRewindMutation)
123
+ if (!isBlocked && !isRewindMutation)
126
124
  return undefined;
127
125
  return { kind: 'command.error', text: '⚠️ 话题中不支持此命令' };
128
126
  }
@@ -25,7 +25,8 @@ import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-m
25
25
  import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
26
26
  import { isManagementRole } from '../../config/builtin-roles.js';
27
27
  import { isSystemControlChannel } from '../system-channels.js';
28
- import { spawnDetachedNode } from '../../utils/cross-platform.js';
28
+ import { launchMonitorAndConfirm, resolveMonitorLaunch, cleanupMonitorStage } from '../../utils/cross-platform.js';
29
+ import { scanInstances } from '../../utils/instance-registry.js';
29
30
  import { inspectDataMigrationRequirement } from '../data-migration.js';
30
31
  import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
31
32
  const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
@@ -1063,11 +1064,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1063
1064
  const activeBaseagent = owningAgent?.baseagent || this.parseDefaultBaseagent();
1064
1065
  // 尝试发送 CommandCard 卡片
1065
1066
  if (this.interactionRouter && available.length > 1) {
1067
+ const baseagentSession = await getExistingSessionForCommand();
1066
1068
  const interaction = {
1067
1069
  type: 'interaction',
1068
1070
  id: `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1069
1071
  channelId,
1070
- sessionId: activeSession?.id || `agent-${Date.now()}`,
1072
+ sessionId: baseagentSession?.id || `agent-${Date.now()}`,
1071
1073
  initiatorId: userId,
1072
1074
  kind: {
1073
1075
  kind: 'command-card',
@@ -1080,7 +1082,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1080
1082
  })),
1081
1083
  },
1082
1084
  };
1083
- const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1085
+ const replyCtx = baseagentSession ? this.getReplyContext(baseagentSession) : replyContext;
1084
1086
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1085
1087
  if (cardResult === null)
1086
1088
  return null;
@@ -1100,29 +1102,79 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1100
1102
  if (!owningAgent) {
1101
1103
  return { kind: 'command.error', text: '❌ 当前 channel 无绑定 agent,无法设置 active_baseagent' };
1102
1104
  }
1103
- await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
1104
- const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
1105
+ const currentSession = await getExistingSessionForCommand();
1106
+ const previousBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
1107
+ if (currentSession?.threadId) {
1108
+ const rotation = await withIdleTopicBackend(currentSession.id, currentSession.threadId, async (current) => {
1109
+ try {
1110
+ owningAgent.setActiveBaseagent(args);
1111
+ }
1112
+ catch (error) {
1113
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1114
+ }
1115
+ try {
1116
+ const result = await this.sessionManager.rotateTopicBackend({
1117
+ sessionId: current.id,
1118
+ expectedThreadId: current.threadId,
1119
+ baseagent: args,
1120
+ });
1121
+ return { ok: true, result };
1122
+ }
1123
+ catch (error) {
1124
+ try {
1125
+ owningAgent.setActiveBaseagent(previousBaseagent);
1126
+ }
1127
+ catch { }
1128
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1129
+ }
1130
+ }, currentSession);
1131
+ if (!rotation.ok) {
1132
+ if (rotation.reason === 'busy')
1133
+ return { kind: 'command.error', text: '⚠️ 当前话题会话忙碌,无法切换 baseagent,请稍后再试' };
1134
+ if (rotation.reason === 'unknown')
1135
+ return { kind: 'command.error', text: '⚠️ 当前话题会话状态未知,无法安全切换 baseagent,请稍后再试' };
1136
+ return { kind: 'command.error', text: `❌ baseagent 切换失败:${rotation.reason === 'missing' ? '当前话题会话不存在' : '状态未知'}` };
1137
+ }
1138
+ if (!rotation.value.ok)
1139
+ return { kind: 'command.error', text: `❌ baseagent 切换失败:${rotation.value.error}` };
1140
+ this.eventBus.publish({ type: 'agent:baseagent-changed', aid: owningAgent.aid, baseagent: args, previousBaseagent, scope: 'agent', timestamp: Date.now() });
1141
+ return {
1142
+ kind: 'system.notice',
1143
+ text: `✓ 当前话题已切换 Baseagent: ${previousBaseagent} → ${args}\n 话题会话 ID 保持不变: ${currentSession.id}`,
1144
+ subtype: 'session.created',
1145
+ };
1146
+ }
1147
+ await interruptPausedSessionBeforeReplacement(currentSession);
1105
1148
  try {
1106
1149
  owningAgent.setActiveBaseagent(args);
1107
1150
  }
1108
1151
  catch (error) {
1109
1152
  return { kind: 'command.error', text: `❌ ${error instanceof Error ? error.message : String(error)}` };
1110
1153
  }
1111
- this.eventBus.publish({
1112
- type: 'agent:baseagent-changed',
1113
- aid: owningAgent.aid,
1114
- baseagent: args,
1115
- previousBaseagent: previousDefaultBaseagent,
1116
- scope: 'agent',
1117
- timestamp: Date.now(),
1118
- });
1119
- const projectName = this.getProjectName(owningAgent.projectPath);
1120
- const agentSwitchResponse = [
1121
- `✓ 已设置 Baseagent: ${args}`,
1122
- ` Agent: ${owningAgent.name}`,
1123
- ` 项目: ${projectName}`,
1124
- ].join('\n');
1125
- return { kind: 'command.result', text: agentSwitchResponse };
1154
+ let newSession;
1155
+ try {
1156
+ newSession = await this.sessionManager.createNewSession(channel, channelId, owningAgent.projectPath, undefined, args, currentSession?.metadata, { previousBaseagent, baseagentChanged: previousBaseagent !== args });
1157
+ }
1158
+ catch (error) {
1159
+ try {
1160
+ owningAgent.setActiveBaseagent(previousBaseagent);
1161
+ }
1162
+ catch { }
1163
+ return { kind: 'command.error', text: `❌ 创建新会话失败:${error instanceof Error ? error.message : String(error)}` };
1164
+ }
1165
+ if (currentSession) {
1166
+ const previousAgent = getActiveAgentIfAvailable();
1167
+ if (previousAgent) {
1168
+ await previousAgent.clearSession(currentSession.id, currentSession.agentSessionId || '', currentSession.projectPath);
1169
+ await previousAgent.closeSession(currentSession.id);
1170
+ }
1171
+ }
1172
+ this.eventBus.publish({ type: 'agent:baseagent-changed', aid: owningAgent.aid, baseagent: args, previousBaseagent, scope: 'agent', timestamp: Date.now() });
1173
+ return {
1174
+ kind: 'system.notice',
1175
+ text: `✓ 已切换 Baseagent: ${previousBaseagent} → ${args}\n 已创建新的主会话: ${newSession.id}\n 之前的对话历史已保留,可通过 /s 查看`,
1176
+ subtype: 'session.created',
1177
+ };
1126
1178
  }
1127
1179
  // /model 命令:查看或切换模型/推理强度
1128
1180
  if (normalizedContent.startsWith('/model')) {
@@ -1317,7 +1369,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1317
1369
  }),
1318
1370
  },
1319
1371
  };
1320
- const replyCtx = modelSession ? this.getReplyContext(modelSession) : undefined;
1372
+ const replyCtx = modelSession ? this.getReplyContext(modelSession) : replyContext;
1321
1373
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1322
1374
  if (cardResult === null)
1323
1375
  return null;
@@ -1626,7 +1678,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1626
1678
  })),
1627
1679
  },
1628
1680
  };
1629
- const replyCtx = effortSession ? this.getReplyContext(effortSession) : undefined;
1681
+ const replyCtx = effortSession ? this.getReplyContext(effortSession) : replyContext;
1630
1682
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1631
1683
  if (cardResult === null)
1632
1684
  return null;
@@ -1879,7 +1931,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1879
1931
  })),
1880
1932
  },
1881
1933
  };
1882
- const replyCtx = replyContext ?? (activitySession ? this.getReplyContext(activitySession) : undefined);
1934
+ // A topic session's persisted route is authoritative; an inbound
1935
+ // replyContext may be a stale/main-session snapshot (notably for card
1936
+ // callbacks), so prefer the resolved session context whenever present.
1937
+ const replyCtx = activitySession ? this.getReplyContext(activitySession) : replyContext;
1883
1938
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1884
1939
  if (cardResult === null)
1885
1940
  return null;
@@ -1947,7 +2002,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1947
2002
  })),
1948
2003
  },
1949
2004
  };
1950
- const replyCtx = existingChatmodeSession ? this.getReplyContext(existingChatmodeSession) : undefined;
2005
+ const replyCtx = existingChatmodeSession ? this.getReplyContext(existingChatmodeSession) : replyContext;
1951
2006
  const cardResult = await this.sendCommandCard({
1952
2007
  channel,
1953
2008
  channelId,
@@ -2253,7 +2308,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2253
2308
  }
2254
2309
  return {
2255
2310
  kind: 'system.notice',
2256
- text: `✅ backend 已切换${outcome.wasBound ? '' : '(当前未绑定 backend)'};下一条消息将初始化新 backend(阶段一不携带历史摘要)`,
2311
+ text: '已切换到新会话。下一条消息将在新会话中处理。',
2257
2312
  subtype: 'session.renewed',
2258
2313
  };
2259
2314
  }
@@ -2290,7 +2345,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2290
2345
  // The runner instance's model is only the final fallback; without this
2291
2346
  // override Claude's /compact command could resume with a stale agent
2292
2347
  // default (for example glm-5.3) while the turn itself used gpt-5.6-sol.
2293
- const compactBaseagentRaw = session.baseagent || currentSessionAgent.name;
2348
+ const compactBaseagentRaw = session.baseagent;
2349
+ if (!compactBaseagentRaw) {
2350
+ return { kind: 'command.error', text: '❌ 当前会话缺少 baseagent 绑定,拒绝隐式切换' };
2351
+ }
2294
2352
  const compactBaseagentNormalized = normalizeBaseagent(compactBaseagentRaw);
2295
2353
  const compactBaseagent = compactBaseagentNormalized.canonical === 'unknown'
2296
2354
  ? compactBaseagentRaw
@@ -2610,7 +2668,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2610
2668
  groups.set(type, []);
2611
2669
  groups.get(type).push({ name, status });
2612
2670
  }
2613
- if (!isAdmin && !isDaemonOwner) {
2671
+ // ECWeb 的系统控制面需要结构化健康数据(尤其是 daemon.aun.connected)。
2672
+ // 这里不能把全局 system 通道降级成仅文本摘要,否则前端会因缺少
2673
+ // daemonStatusProvider 返回值而把已连接的控制 AID 显示为 disconnect。
2674
+ if (!isAdmin && !isDaemonOwner && !isGlobalCheckChannel) {
2614
2675
  // visitor/member: 仅显示实例通道摘要
2615
2676
  const total = [...groups.values()].flat().length;
2616
2677
  const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
@@ -2763,6 +2824,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2763
2824
  // 进程级操作:必须是 daemon owner(daemon.json.owners),与 menu 协议 /system restart 一致。
2764
2825
  // agent-channel 的 owner/admin 角色不足以重启整个 daemon。
2765
2826
  const restartSelfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
2827
+ const commandSession = await getExistingSessionForCommand();
2766
2828
  const authDenied = await authorizeIntent({
2767
2829
  intent: {
2768
2830
  operation: 'system.restart',
@@ -2772,7 +2834,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2772
2834
  dangerous: true,
2773
2835
  },
2774
2836
  identity,
2775
- session: activeSession,
2837
+ session: commandSession ?? activeSession,
2776
2838
  channel,
2777
2839
  channelId,
2778
2840
  userId,
@@ -2784,7 +2846,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2784
2846
  const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
2785
2847
  // 排除当前会话(如果存在)。如果 activeSession 不存在,说明还没建立会话,
2786
2848
  // 此时检查所有任务;如果有其他会话在处理,仍然阻塞重启。
2787
- const excludeKey = activeSession?.id;
2849
+ // A topic command owns its topic session even when the channel's active
2850
+ // session remains the main conversation. Exclude that exact queue key
2851
+ // from the busy check so the current topic cannot block its own restart.
2852
+ const excludeKey = commandSession?.id ?? activeSession?.id;
2788
2853
  const busyInfo = getAgentBusyInfo(this, selfAid, excludeKey);
2789
2854
  if (busyInfo && busyInfo.count > 0) {
2790
2855
  // busyInfo.count > 0 说明【除当前会话外】还有其他会话的任务在执行
@@ -2809,10 +2874,14 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2809
2874
  const suppressRealRestart = shouldSuppressRealRestart();
2810
2875
  let restartReplyContext = replyContext;
2811
2876
  try {
2812
- if (!restartReplyContext?.delivery && threadId) {
2877
+ // A topic's persisted session context is authoritative for card
2878
+ // callbacks and for inbound messages that only carry a partial route.
2879
+ // Do not let a stale/main-session replyContext win merely because it
2880
+ // happens to contain a delivery target.
2881
+ if (threadId) {
2813
2882
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2814
2883
  if (threadSession)
2815
- restartReplyContext = this.getReplyContext(threadSession);
2884
+ restartReplyContext = { ...(replyContext ?? {}), ...(this.getReplyContext(threadSession) ?? {}), threadId };
2816
2885
  }
2817
2886
  else if (!restartReplyContext?.delivery && activeSession) {
2818
2887
  restartReplyContext = this.getReplyContext(activeSession);
@@ -2854,16 +2923,39 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2854
2923
  channel,
2855
2924
  channelId,
2856
2925
  timestamp: Date.now(),
2926
+ ...(restartReplyContext?.threadId ? { threadId: restartReplyContext.threadId } : {}),
2857
2927
  ...(restartReplyContext?.delivery ? { delivery: restartReplyContext.delivery } : {}),
2858
2928
  ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
2859
2929
  };
2860
2930
  if (!suppressRealRestart) {
2931
+ // 这条路径上 daemon 会在下面 kill 掉自己,所以必须先确认 monitor 真的活起来:
2932
+ // 只查文件存在不够 —— 开发仓下 monitor 走 tsx,源码语法错误会让它加载即退出;
2933
+ // 安装包下 monitor 先复制到 stage 再启动,避免升级替换掉自己脚下的文件。
2934
+ const monitorEntry = resolveMonitorLaunch(getPackageRoot(), path.join(resolvePaths().root, 'data', 'monitor-stage'));
2935
+ if (monitorEntry.note)
2936
+ logger.info(`[System] ${monitorEntry.note}`);
2937
+ if (!fs.existsSync(monitorEntry.entry)) {
2938
+ logger.error(`[System] Refusing restart: restart-monitor entry not found: ${monitorEntry.entry}`);
2939
+ return { code: 'RESTART_ENTRY_MISSING', entry: monitorEntry.entry };
2940
+ }
2861
2941
  const controlDir = daemonControlDir();
2862
2942
  fs.mkdirSync(controlDir, { recursive: true });
2863
2943
  fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
2864
- spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2865
- EVOLCORE_HOME: resolvePaths().root,
2866
- });
2944
+ const confirmation = await launchMonitorAndConfirm([...monitorEntry.args, 'restart-monitor'], { EVOLCORE_HOME: resolvePaths().root, ...monitorEntry.env }, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid === pid), pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid));
2945
+ if (!confirmation.ok) {
2946
+ // 没有看门狗就不要停服务:撤掉回执文件,让本进程继续跑。
2947
+ if (monitorEntry.stageDir)
2948
+ cleanupMonitorStage(monitorEntry.stageDir);
2949
+ try {
2950
+ fs.rmSync(path.join(controlDir, 'restart-pending.json'), { force: true });
2951
+ }
2952
+ catch { }
2953
+ logger.error(`[System] Refusing restart: restart-monitor did not come up (${confirmation.reason})`);
2954
+ for (const line of (confirmation.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
2955
+ logger.error(`[System] | ${line}`);
2956
+ }
2957
+ return { code: 'RESTART_MONITOR_DEAD', reason: confirmation.reason ?? 'unknown' };
2958
+ }
2867
2959
  }
2868
2960
  else {
2869
2961
  logger.info('[System] Suppressed real restart in test runtime');
@@ -2894,15 +2986,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2894
2986
  logger.error('[System] Failed to send restart notification:', err);
2895
2987
  }
2896
2988
  }
2897
- // 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
2898
- // 正常关闭所有 channel(包括 Feishu WebSocket close frame),
2899
- // 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
2900
- if (!suppressRealRestart) {
2901
- setTimeout(() => {
2902
- logger.info('[System] Restarting by user command...');
2903
- process.kill(process.pid, 'SIGTERM');
2904
- }, 1000);
2905
- }
2989
+ // restart-monitor owns the old daemon shutdown and the replacement
2990
+ // startup. Do not kill this process here: the monitor may have exited
2991
+ // after a read-only migration check, in which case the current service
2992
+ // must remain available.
2906
2993
  return true;
2907
2994
  };
2908
2995
  // 文本确认流程
@@ -2931,7 +3018,21 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2931
3018
  if (typeof restarted === 'object' && restarted.code === 'DATA_MIGRATION_REQUIRED') {
2932
3019
  return {
2933
3020
  kind: 'command.error',
2934
- text: `❌ 待处理的用户数据迁移(${restarted.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3021
+ text: `❌ 待处理的用户数据迁移(${restarted.operationCount} 项)阻止重启;本次仅检查,未停止当前服务。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3022
+ reason: restarted.code,
3023
+ };
3024
+ }
3025
+ if (typeof restarted === 'object' && restarted.code === 'RESTART_ENTRY_MISSING') {
3026
+ return {
3027
+ kind: 'command.error',
3028
+ text: `❌ 重启已中止:找不到 restart-monitor 入口 ${restarted.entry}。服务未重启,当前进程继续运行。请先执行 npm run build 重建产物。`,
3029
+ reason: restarted.code,
3030
+ };
3031
+ }
3032
+ if (typeof restarted === 'object' && restarted.code === 'RESTART_MONITOR_DEAD') {
3033
+ return {
3034
+ kind: 'command.error',
3035
+ text: `❌ 重启已中止:看门狗进程未能启动(${restarted.reason})。服务未停止,仍在运行当前版本。开发仓下这通常是源码语法错误导致 tsx 无法加载,请执行 npx tsc --noEmit 检查。`,
2935
3036
  reason: restarted.code,
2936
3037
  };
2937
3038
  }
@@ -3099,11 +3200,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3099
3200
  ? { delivery: { chatType: 'private' } }
3100
3201
  : sendSession
3101
3202
  ? this.getReplyContext(sendSession)
3102
- : chatType === 'group'
3203
+ : replyContext ?? (chatType === 'group'
3103
3204
  ? { delivery: { chatType: 'group', groupId: channelId } }
3104
3205
  : chatType === 'private'
3105
3206
  ? { delivery: { chatType: 'private' } }
3106
- : undefined;
3207
+ : undefined);
3107
3208
  await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
3108
3209
  const sizeStr = stat.size < 1024 ? `${stat.size} B`
3109
3210
  : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
@@ -3122,10 +3223,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3122
3223
  // /slist cli — 仅 CLI 会话(未导入的)
3123
3224
  if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
3124
3225
  if (!session) {
3125
- return { kind: 'command.error', text: `❌ 当前没有活跃会话
3126
-
3127
- 请先执行以下操作之一:
3128
- 1. 发送任意消息 - 自动创建新会话
3226
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
3227
+
3228
+ 请先执行以下操作之一:
3229
+ 1. 发送任意消息 - 自动创建新会话
3129
3230
  2. /new [名称] - 创建命名会话` };
3130
3231
  }
3131
3232
  const showCliOnly = normalizedContent === '/slist cli';
@@ -3329,7 +3430,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3329
3430
  projectPaths.unshift(session.projectPath);
3330
3431
  }
3331
3432
  for (const projectPath of projectPaths) {
3332
- const currentBaseagent = session?.baseagent || this.primaryRunnerKey;
3433
+ const currentBaseagent = session?.baseagent
3434
+ ?? this.agentRegistry?.resolveByChannel(channel)?.baseagent
3435
+ ?? (this.agentMap?.size === 1 ? this.parseDefaultBaseagent() : undefined);
3436
+ if (!currentBaseagent) {
3437
+ return { kind: 'command.error', text: '❌ 无法确定当前会话的 baseagent,拒绝导入 CLI 会话' };
3438
+ }
3333
3439
  const cliSessions = currentBaseagent === 'ecagent'
3334
3440
  ? await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent, selfAID ?? session?.selfAID)
3335
3441
  : await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent);
@@ -3391,11 +3497,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3391
3497
  if (!newName)
3392
3498
  return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
3393
3499
  if (!session) {
3394
- return { kind: 'command.error', text: `❌ 当前没有活跃会话
3395
-
3396
- 请先执行以下操作之一:
3397
- 1. 发送任意消息 - 自动创建新会话
3398
- 2. /new [名称] - 创建命名会话
3500
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
3501
+
3502
+ 请先执行以下操作之一:
3503
+ 1. 发送任意消息 - 自动创建新会话
3504
+ 2. /new [名称] - 创建命名会话
3399
3505
  3. /session <名称> - 切换到已有会话` };
3400
3506
  }
3401
3507
  const existing = await this.sessionManager.getSessionByName(channel, channelId, newName);
@@ -3504,7 +3610,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3504
3610
  const session = await getExistingSessionForCommand();
3505
3611
  if (!session)
3506
3612
  return { kind: 'command.error', text: '❌ 当前没有活跃会话' };
3507
- const rewindBaseagent = session.baseagent ?? session.agentId;
3613
+ const rewindBaseagent = session.baseagent;
3614
+ if (!rewindBaseagent) {
3615
+ return { kind: 'command.error', text: '❌ 当前会话缺少 baseagent 绑定,拒绝隐式切换' };
3616
+ }
3508
3617
  const rewindAgent = this.getAgent(channel, rewindBaseagent);
3509
3618
  if (!session.agentSessionId) {
3510
3619
  return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
@@ -752,21 +752,11 @@ export function planDataMigration(root) {
752
752
  }
753
753
  }
754
754
  catch { }
755
- const legacyOutbox = path.join(root, 'data', 'outbox');
756
- try {
757
- for (const entry of fs.readdirSync(legacyOutbox, { withFileTypes: true })) {
758
- if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
759
- continue;
760
- const aid = entry.name.slice(0, -'.jsonl'.length);
761
- if (!aids.has(aid)) {
762
- warnings.push(`Unresolved outbox owner retained: ${entry.name}`);
763
- continue;
764
- }
765
- const operationId = nextId('outbox');
766
- addCopy(operations, operationId, path.join(legacyOutbox, entry.name), path.join(root, 'agents', aid, 'data', 'outbox.jsonl'), stageFor(operationId));
767
- }
768
- }
769
- catch { }
755
+ // Outbox layouts are imported lazily by src/aun/outbox.ts when a queue is
756
+ // first read or written. They are append-only delivery state, so including
757
+ // them in the maintenance migration would unnecessarily block startup and
758
+ // could race a live connector. Keep the explicit migration reserved for
759
+ // data that needs operator review or an exclusive maintenance window.
770
760
  const contactAudit = path.join(root, 'data', 'contact-book-audit.jsonl');
771
761
  // The legacy audit file may be provisioned as an empty placeholder (for
772
762
  // example by a protected-path sandbox). An empty source has nothing to
@@ -89,6 +89,9 @@ const CATALOG = [
89
89
  { path: 'name', type: 'string', optional: true },
90
90
  { path: 'chatType', type: 'string', optional: true },
91
91
  { path: 'threadId', type: 'string', optional: true },
92
+ { path: 'baseagent', type: 'string', optional: true },
93
+ { path: 'previousBaseagent', type: 'string', optional: true },
94
+ { path: 'baseagentChanged', type: 'boolean', optional: true },
92
95
  { path: 'cause', type: 'string', optional: true },
93
96
  { path: 'sourceSessionId', type: 'string', optional: true },
94
97
  { path: 'renewSource', type: 'string', optional: true },
@@ -793,6 +796,11 @@ const CATALOG = [
793
796
  fields: [
794
797
  { path: 'aid', type: 'string' },
795
798
  { path: 'avatar', type: 'string' },
799
+ { path: 'sha256', type: 'string', optional: true },
800
+ { path: 'mime', type: 'string', optional: true },
801
+ { path: 'width', type: 'number', optional: true },
802
+ { path: 'height', type: 'number', optional: true },
803
+ { path: 'aspectRatio', type: 'number', optional: true },
796
804
  { path: 'timestamp', type: 'number', optional: true },
797
805
  ],
798
806
  },
@@ -275,9 +275,9 @@ export class EvolAgent {
275
275
  return `agent-files:${this.aid}`;
276
276
  }
277
277
  /**
278
- * 读取 personal/persona.md 内容。走 fileCache(mtime 门控):persona 没有任何
279
- * 写入命令、由 agent 自己带外改写,与 working memory 同样改了即应生效,故每次读
280
- * stat 比对、变了自动重读。文件不存在返回 null。
278
+ * 读取 personal/persona.md 内容。走 fileCache(mtime 门控):persona 可由
279
+ * `ec agent persona` 或 agent 自己更新,与 working memory 一样改了即应生效,
280
+ * 故每次读 stat 比对、变了自动重读。文件不存在返回 null。
281
281
  */
282
282
  getPersona() {
283
283
  const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
@@ -118,6 +118,7 @@ export class HandoffRuntime {
118
118
  targetSessionId: target.id,
119
119
  payload: input.payload,
120
120
  encrypt: input.encrypt,
121
+ contextOnly: input.contextOnly,
121
122
  delivery: targetChatType === 'group'
122
123
  ? { chatType: 'group', groupId: input.to }
123
124
  : { chatType: 'private' },
@@ -131,7 +132,8 @@ export class HandoffRuntime {
131
132
  status: 'started',
132
133
  refs: { handoffId: handoff.handoff_id, sessionId: origin.id, messageId: input.originMessageId },
133
134
  });
134
- await this.dispatcher.drain(input.selfAid, target.id);
135
+ if (!input.contextOnly)
136
+ await this.dispatcher.drain(input.selfAid, target.id);
135
137
  const dispatched = this.store.get(input.selfAid, handoff.handoff_id) ?? handoff;
136
138
  const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
137
139
  if (dispatched.state === 'discarded' || replacedSessionId) {
@@ -122,6 +122,7 @@ export class HandoffStore {
122
122
  request: {
123
123
  payload: clonePayload(input.payload),
124
124
  encrypt: input.encrypt,
125
+ ...(input.contextOnly ? { contextOnly: true } : {}),
125
126
  ...(input.delivery ? { delivery: input.delivery } : {}),
126
127
  },
127
128
  ...(input.originAuthorization
@@ -129,7 +130,12 @@ export class HandoffStore {
129
130
  : {}),
130
131
  causation: normalizeCausation(input.causation),
131
132
  return_policy: input.returnPolicy ?? 'required',
132
- state: 'queued',
133
+ // Local-only requests are already available for the next target-session
134
+ // turn and must never enter the outbound dispatch queue. Keep them in
135
+ // target_sent while waiting for the target turn to bind the context;
136
+ // target_context_pending is reserved for a context-only handoff after
137
+ // it has been consumed by a returnPolicy=none target turn.
138
+ state: input.contextOnly ? 'target_sent' : 'queued',
133
139
  target_message_id: null,
134
140
  response_message_id: null,
135
141
  consumed_at: null,
@@ -227,25 +227,32 @@ export class IMRenderer {
227
227
  durationMs: opts.durationMs,
228
228
  });
229
229
  }
230
- /** 发送不进入 activity/thought 历史的非终态 warning status。 */
231
- async sendWarningStatus(message, subtype, details) {
230
+ /** 发送不进入 activity/thought 历史、也不代表执行活跃的任务级 notice。 */
231
+ async sendStatusNotice(message, severity = 'info', subtype, details) {
232
232
  const payload = {
233
- kind: 'status.progress',
233
+ kind: 'status.notice',
234
234
  metadata: {
235
- activityType: 'progress',
236
235
  message,
237
- severity: 'warn',
236
+ severity,
238
237
  ...(subtype !== undefined && { subtype }),
239
238
  ...(details?.missingItems !== undefined && { missingItems: details.missingItems }),
239
+ ...(details?.idleSec !== undefined && { idleSec: details.idleSec }),
240
+ ...(details?.totalEvents !== undefined && { totalEvents: details.totalEvents }),
241
+ ...(details?.totalToolCalls !== undefined && { totalToolCalls: details.totalToolCalls }),
242
+ ...(details?.lastToolName !== undefined && { lastToolName: details.lastToolName }),
240
243
  },
241
244
  };
242
245
  try {
243
246
  await this.opts.send(payload);
244
247
  }
245
248
  catch (error) {
246
- logger.warn(`[IMRenderer] warning status send failed: ${error instanceof Error ? error.message : String(error)}`);
249
+ logger.warn(`[IMRenderer] status notice send failed: ${error instanceof Error ? error.message : String(error)}`);
247
250
  }
248
251
  }
252
+ /** 兼容旧调用方:发送 warning 级别的任务 notice。 */
253
+ async sendWarningStatus(message, subtype, details) {
254
+ return this.sendStatusNotice(message, 'warn', subtype, details);
255
+ }
249
256
  /** 添加任务生命周期边界事件,复用 activity.batch 投影路径。 */
250
257
  addLifecycle(phase, metadata, text) {
251
258
  const item = {