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
@@ -5,6 +5,8 @@ import { agentSessionsDir, agentDir, agentThreadIndexPath, agentThreadSummariesD
5
5
  import { atomicWriteJson, atomicReadJson } from '../../utils/atomic-write.js';
6
6
  import { appendJsonl, appendJsonlDurable, readAllJsonlLines, readLastJsonlLine, readLastJsonlLineStrict, chatDirPath, scanChatDirs, tryParseChannelSessionKey } from '../session/session-fs-store.js';
7
7
  import { sanitizeSessionTitle, SESSION_TITLE_MAX_LEN, SESSION_TITLE_STORED_MAX_LEN, SESSION_TITLE_ELLIPSIS } from '../session/session-title.js';
8
+ import { isMainSessionThreadId, isTopicThreadId, mainTopicPeer, safeFormatMainTopicId, sessionTopicThreadId } from './topic-id.js';
9
+ import { PeerIdentityCache } from '../relation/peer-identity.js';
8
10
  import { ThreadContextStorage } from './thread-context-storage.js';
9
11
  const THREAD_ID_RE = /^thread-[0-9]+-[0-9a-f]{4}-[0-9a-f]{4}$/;
10
12
  const RESERVED_THREAD_IDS = new Set(['current', 'main', 'all']);
@@ -144,7 +146,79 @@ export class ThreadService {
144
146
  findInIndex(selfAid, peer, threadId) {
145
147
  const want = normalizeThreadPeer(peer);
146
148
  const index = this.readThreadIndex(selfAid);
147
- return index.threads.find(t => normalizeThreadPeer(t.peer) === want && t.threadId === threadId);
149
+ const indexed = index.threads.find(t => normalizeThreadPeer(t.peer) === want && t.threadId === threadId);
150
+ if (indexed)
151
+ return indexed;
152
+ // Older agents can still have a valid topic session under the per-chat
153
+ // `_threads/` directory while the consolidated agent index only contains
154
+ // newer topics. Summary reads depend on the consolidated index, so repair
155
+ // the missing entry lazily when a concrete topic is requested.
156
+ const recovered = this.recoverThreadFromSessionFiles(selfAid, want, threadId);
157
+ if (!recovered)
158
+ return undefined;
159
+ const next = {
160
+ version: 1,
161
+ threads: [...index.threads, recovered],
162
+ };
163
+ try {
164
+ this.writeThreadIndex(selfAid, next);
165
+ }
166
+ catch {
167
+ // An unwritable derived index must not hide a readable topic. Retry the
168
+ // repair on its next lookup, as with the legacy index-path migration.
169
+ }
170
+ return recovered;
171
+ }
172
+ recoverThreadFromSessionFiles(selfAid, peer, threadId) {
173
+ if (!peer || !threadId || RESERVED_THREAD_IDS.has(threadId))
174
+ return undefined;
175
+ const sessionsDir = agentSessionsDir(selfAid);
176
+ if (!fs.existsSync(sessionsDir))
177
+ return undefined;
178
+ for (const dirEntry of fs.readdirSync(sessionsDir, { withFileTypes: true })) {
179
+ if (!dirEntry.isDirectory())
180
+ continue;
181
+ const parsedDir = tryParseChannelSessionKey(dirEntry.name);
182
+ if (!parsedDir || normalizeThreadPeer(parsedDir.channelId) !== peer)
183
+ continue;
184
+ const chatDir = path.join(sessionsDir, dirEntry.name);
185
+ const threadsDir = path.join(chatDir, '_threads');
186
+ if (!fs.existsSync(threadsDir))
187
+ continue;
188
+ for (const file of fs.readdirSync(threadsDir, { withFileTypes: true })) {
189
+ if (!file.isFile() || !file.name.endsWith('.jsonl'))
190
+ continue;
191
+ const sessionId = file.name.slice(0, -'.jsonl'.length);
192
+ try {
193
+ const last = readLastJsonlLine(path.join(threadsDir, file.name));
194
+ if (!last || last.threadId !== threadId)
195
+ continue;
196
+ if (normalizeThreadPeer(last.channelId) !== peer)
197
+ continue;
198
+ if (last.selfAID && last.selfAID !== selfAid)
199
+ continue;
200
+ if (last.id && last.id !== sessionId)
201
+ continue;
202
+ return {
203
+ threadId,
204
+ sessionId,
205
+ peer,
206
+ channelType: parsedDir.channelType,
207
+ peerKind: last.chatType === 'group' ? 'group' : 'private',
208
+ name: last.name ?? threadId,
209
+ label: last.label ?? null,
210
+ topicStatus: last.topicStatus ?? 'pending',
211
+ createdAt: last.createdAt ?? 0,
212
+ lastActiveAt: last.updatedAt ?? 0,
213
+ ...(last.nameUpdatedAt !== undefined ? { nameUpdatedAt: last.nameUpdatedAt } : {}),
214
+ };
215
+ }
216
+ catch {
217
+ // Ignore an unrelated or corrupt historical session file.
218
+ }
219
+ }
220
+ }
221
+ return undefined;
148
222
  }
149
223
  /**
150
224
  * 该条目对应的会话目录。
@@ -221,19 +295,25 @@ export class ThreadService {
221
295
  if (!sanitized)
222
296
  throw new ThreadServiceError('话题名称不能为空', 'INVALID_NAME');
223
297
  assertThreadNameLength(name, sanitized);
224
- // 主会话(threadId 为空)从不进话题索引,它由会话文件产生。
298
+ // 主话题(threadId 为空 / `main` / 规范 id)从不进话题索引,它由会话文件产生。
225
299
  // 改名直接落到 active session 上,不走索引与快照。
226
300
  // 群里主会话名只有 admin/owner 能改,且在授权层已经拦过;这里照常落本地。
227
- if (!threadId) {
301
+ if (isMainSessionThreadId(threadId)) {
228
302
  const main = await this.showMainSession(selfAid, peer);
229
303
  if (!main)
230
304
  throw new ThreadServiceError(`与 ${peer} 无活跃主会话`, 'TOPIC_NOT_FOUND');
231
305
  if ((main.name ?? '') === sanitized)
232
306
  return { changed: false, name: sanitized };
233
- const renamed = await this.sessionManager.renameSession(main.sessionId, sanitized);
234
- if (!renamed)
235
- throw new ThreadServiceError(`主会话改名失败: ${main.sessionId}`, 'RENAME_FAILED');
236
- return { changed: true, name: sanitized, nameUpdatedAt: Date.now() };
307
+ const nameUpdatedAt = Math.max(Date.now(), Number(main.nameUpdatedAt ?? 0) + 1);
308
+ // sessionId 为空说明目录存在但 active.json 还没生成(旧数据从未经过 daemon 持久化)。
309
+ // 此时本地存储里没有 session 记录可以改,跳过 renameSession 直接返回成功——
310
+ // 改名通知会发出去,对端能看到;本端下次 daemon 首次处理该会话时会写 active.json。
311
+ if (main.sessionId) {
312
+ const renamed = await this.sessionManager.renameSession(main.sessionId, sanitized, nameUpdatedAt);
313
+ if (!renamed)
314
+ throw new ThreadServiceError(`主会话改名失败: ${main.sessionId}`, 'RENAME_FAILED');
315
+ }
316
+ return { changed: true, name: sanitized, nameUpdatedAt, peerKind: main.peerKind };
237
317
  }
238
318
  const index = this.readThreadIndex(selfAid);
239
319
  const entry = index.threads.find(t => t.peer === peer && t.threadId === threadId);
@@ -252,10 +332,10 @@ export class ThreadService {
252
332
  if (session) {
253
333
  await this.sessionManager.renameSession(session.id, sanitized);
254
334
  }
255
- return { changed: true, name: sanitized, nameUpdatedAt };
335
+ return { changed: true, name: sanitized, nameUpdatedAt, peerKind: entry.peerKind };
256
336
  }
257
337
  /**
258
- * 应用对端发来的改名通知(`topic-renamed`)。
338
+ * 应用对端发来的改名通知(`event/topic.renamed`)。
259
339
  *
260
340
  * 判据是最新更新时间先到先赢:通知里的 `nameUpdatedAt` 只有**严格大于**本地
261
341
  * 记录时才生效。对端尚未建档时按通知内容建条目(与会话自动建档一致),
@@ -263,9 +343,25 @@ export class ThreadService {
263
343
  */
264
344
  async applyRemoteRename(input) {
265
345
  const sanitized = sanitizeSessionTitle(input.name);
266
- if (!sanitized || !input.threadId)
346
+ if (!sanitized || !isMainSessionThreadId(input.threadId) && !isTopicThreadId(input.threadId)) {
267
347
  return { applied: false, reason: 'invalid' };
348
+ }
268
349
  const peer = normalizeThreadPeer(input.peer) ?? input.peer;
350
+ // 主话题:与话题同一套改名语义,只是落到主会话上——主话题从不进话题索引,
351
+ // 这里绝不能新建条目(会凭空多出一条不存在的话题)。id 自带双方,
352
+ // 先校验另一端就是本次发送方,避免拿别人的主话题 id 改错关系。
353
+ if (isMainSessionThreadId(input.threadId)) {
354
+ if (mainTopicPeer(input.threadId, input.selfAid) !== peer) {
355
+ return { applied: false, reason: 'invalid' };
356
+ }
357
+ const main = await this.showMainSession(input.selfAid, peer);
358
+ if (!main)
359
+ return { applied: false, reason: 'invalid' };
360
+ if (input.nameUpdatedAt <= Number(main.nameUpdatedAt ?? 0))
361
+ return { applied: false, reason: 'stale' };
362
+ const renamed = await this.sessionManager.renameSession(main.sessionId, sanitized, input.nameUpdatedAt);
363
+ return renamed ? { applied: true } : { applied: false, reason: 'invalid' };
364
+ }
269
365
  const index = this.readThreadIndex(input.selfAid);
270
366
  const entry = index.threads.find(t => t.peer === peer && t.threadId === input.threadId);
271
367
  const current = entry?.nameUpdatedAt ?? 0;
@@ -367,7 +463,7 @@ export class ThreadService {
367
463
  }
368
464
  }
369
465
  // 索引之外的兜底:磁盘上真实存在、且有消息或会话存档的 chat 目录。
370
- // 只加索引里没有的对端——索引已经有了的,子话题数已算过,不再重复计主会话。
466
+ // 只加索引里没有的对端——索引已经有了的,话题数已算过,不再重复计主会话。
371
467
  // 从目录名解析 peer 与 channelType,避免信赖可能过期的索引字段。
372
468
  try {
373
469
  const agentsRoot = path.dirname(agentDir(selfAid));
@@ -420,7 +516,9 @@ export class ThreadService {
420
516
  catch {
421
517
  // 消息日志缺失/损坏时保持索引值
422
518
  }
423
- return { peer, peerKind: v.peerKind, threadCount: v.threadCount, lastActiveAt };
519
+ const cached = PeerIdentityCache.get('aun', peer, agentDir(selfAid));
520
+ const peerName = cached?.name ?? (typeof cached?.metadata?.['name'] === 'string' ? cached.metadata['name'] : undefined);
521
+ return { peer, peerName, peerKind: v.peerKind, threadCount: v.threadCount, lastActiveAt };
424
522
  })
425
523
  .sort((a, b) => b.lastActiveAt - a.lastActiveAt);
426
524
  }
@@ -441,12 +539,11 @@ export class ThreadService {
441
539
  return false;
442
540
  return true;
443
541
  });
444
- // 主会话是 threadId 为 `''` 的话题:它由会话文件而非 createThread 产生,
445
- // 所以从不进话题索引。只列子话题会让「和某对端聊过什么」在列表里看不到主线,
446
- // 而 `history <peer>` 不传 threadId 读的恰恰是主会话消息。指定对端时补一条
447
- // 合成记录,让 list 与 history 的口径一致。
542
+ // 主话题由会话文件产生、从不进话题索引。只列话题会让「和某对端聊过什么」
543
+ // 在列表里看不到主线,而 `history <peer>` 不传话题读的恰恰是主话题消息。
544
+ // 指定对端时补一条合成记录,让 list 与 history 的口径一致。
448
545
  if (peer) {
449
- const hasMain = threads.some(t => !t.threadId);
546
+ const hasMain = threads.some(t => isMainSessionThreadId(t.threadId));
450
547
  if (!hasMain) {
451
548
  const chatDir = this.chatDirFor(selfAid, peer);
452
549
  const logPath = path.join(chatDir, 'messages.jsonl');
@@ -454,7 +551,7 @@ export class ThreadService {
454
551
  if (fs.existsSync(logPath)) {
455
552
  try {
456
553
  for (const m of readAllJsonlLines(logPath)) {
457
- if (!m.threadId && typeof m.ts === 'number' && m.ts > lastTs)
554
+ if (isMainSessionThreadId(m.threadId) && typeof m.ts === 'number' && m.ts > lastTs)
458
555
  lastTs = m.ts;
459
556
  }
460
557
  }
@@ -465,14 +562,15 @@ export class ThreadService {
465
562
  const active = this.sessionManager.getActiveSessionSync('aun', peer, 'aun', selfAid);
466
563
  const peerKind = active?.chatType === 'group' ? 'group' : 'private';
467
564
  const synthetic = {
468
- threadId: '',
565
+ // 主话题的 id 与话题并列,同样可直接作为 --thread 的值。
566
+ threadId: safeFormatMainTopicId(selfAid, peer) ?? '',
469
567
  sessionId: active?.id ?? '',
470
568
  peer,
471
569
  channelType: 'aun',
472
570
  peerKind,
473
571
  // 主会话有自己的名字(创建时写 `默认会话`,可经 ec thread rename 修改)。
474
572
  // 读真实值而不是写死 `主会话`:否则列表里的名字和会话存档对不上,
475
- // 用户也没法从列表确认改名是否生效。`[主会话]` 前缀由 CLI 渲染。
573
+ // 用户也没法从列表确认改名是否生效。`[主话题]` 前缀由 CLI 渲染。
476
574
  name: active?.name || '主会话',
477
575
  label: null,
478
576
  topicStatus: 'pending',
@@ -493,6 +591,10 @@ export class ThreadService {
493
591
  // 先读消息日志并派生真实活跃时间,再排序、再分页——
494
592
  // 否则 recent 排序用的是索引里的管理动作时间,与实际展示的活跃时间不一致。
495
593
  const ts24h = Date.now() - 86400000;
594
+ // 主话题在消息日志里可能是历史空值/`main`,也可能是现行规范 id;两者都算同一条主线。
595
+ const msgBelongsTo = (msgThreadId, topicThreadId) => isMainSessionThreadId(topicThreadId)
596
+ ? isMainSessionThreadId(msgThreadId)
597
+ : (msgThreadId ?? '') === (topicThreadId ?? '');
496
598
  // 按会话目录分组,每个目录只读一次 messages.jsonl。
497
599
  // 键用 channelType + peer,避免不同命名空间下同名对端串数据。
498
600
  const peerMessages = new Map();
@@ -526,12 +628,10 @@ export class ThreadService {
526
628
  const lastMsgOf = new Map();
527
629
  for (const t of threads) {
528
630
  const msgs = (peerMessages.get(`${entryChannelType(t)}\u0000${t.peer}`) ?? [])
529
- .filter(m => (m.threadId ?? '') === (t.threadId ?? ''));
631
+ .filter(m => msgBelongsTo(m.threadId, t.threadId));
530
632
  const lastMsg = msgs.reduce((acc, m) => (!acc || m.ts > acc.ts) ? m : acc, null);
531
- // 合成的主会话条目用 `main` 作 threadId,日志里主会话记的是 null。
532
- const key = t.threadId ?? '';
533
633
  if (lastMsg)
534
- lastMsgOf.set(key, lastMsg);
634
+ lastMsgOf.set(t.threadId ?? '', lastMsg);
535
635
  }
536
636
  if (sort === 'created') {
537
637
  threads.sort((a, b) => b.createdAt - a.createdAt);
@@ -545,7 +645,7 @@ export class ThreadService {
545
645
  threads = threads.slice(offset, offset + limit);
546
646
  threads = threads.map(t => {
547
647
  const msgs = (peerMessages.get(`${entryChannelType(t)}\u0000${t.peer}`) ?? [])
548
- .filter(m => (m.threadId ?? '') === (t.threadId ?? ''));
648
+ .filter(m => msgBelongsTo(m.threadId, t.threadId));
549
649
  const msgTotal = msgs.length;
550
650
  const msg24h = msgs.filter(m => m.ts >= ts24h).length;
551
651
  const lastMsg = lastMsgOf.get(t.threadId ?? '');
@@ -598,18 +698,20 @@ export class ThreadService {
598
698
  const peer = normalizeThreadPeer(peerInput) ?? peerInput;
599
699
  // 不检查索引——允许读取已删除话题的历史(session 文件已移入 _trash,但 messages.jsonl 仍在)
600
700
  // 索引里能查到条目时用条目的命名空间(内部话题在 daemon 下),查不到时退回 aun。
601
- const indexed = this.readThreadIndex(selfAid).threads.find(t => t.threadId === threadId && normalizeThreadPeer(t.peer) === peer);
701
+ const indexed = isMainSessionThreadId(threadId)
702
+ ? undefined
703
+ : this.readThreadIndex(selfAid).threads.find(t => t.threadId === threadId && normalizeThreadPeer(t.peer) === peer);
602
704
  const chatDir = this.chatDirFor(selfAid, indexed?.peer ?? peer, indexed?.channelType);
603
705
  const logPath = path.join(chatDir, 'messages.jsonl');
604
706
  if (!fs.existsSync(logPath))
605
707
  return { peer, messages: [] };
606
708
  const messages = readAllJsonlLines(logPath)
607
709
  .filter((m) => {
608
- // 不传 threadId 时读主会话消息:主会话的行 threadId 为 ''(历史残留 null
609
- // 也按主会话处理),否则按 threadId 精确匹配子话题消息。
610
- if (threadId)
611
- return m.threadId === threadId;
612
- return !m.threadId;
710
+ // 主话题的消息行在历史里是 '' 或 null,新写入的是规范 id;三种都算主话题。
711
+ // 话题按 id 精确匹配。
712
+ if (isMainSessionThreadId(threadId))
713
+ return isMainSessionThreadId(m.threadId);
714
+ return m.threadId === threadId;
613
715
  })
614
716
  .map((m) => {
615
717
  // 消息体优先取 payloadSummary.text(加密消息的明文摘要),
@@ -651,7 +753,10 @@ export class ThreadService {
651
753
  count: Math.min(filtered.length, limit),
652
754
  total: filtered.length,
653
755
  messages: shown,
654
- threadId: threadId ?? '',
756
+ // 主话题返回规范 id,让调用方拿到的东西可以直接再当 `--thread` 用。
757
+ threadId: isMainSessionThreadId(threadId)
758
+ ? (safeFormatMainTopicId(selfAid, peer) ?? '')
759
+ : (threadId ?? ''),
655
760
  peer,
656
761
  };
657
762
  }
@@ -748,20 +853,42 @@ export class ThreadService {
748
853
  const normalized = normalizeThreadPeer(peer) ?? peer;
749
854
  const channelKey = `aun#${selfAid}#main`;
750
855
  const session = await this.sessionManager.getActiveSession(channelKey, normalized);
751
- if (!session)
856
+ if (session) {
857
+ return {
858
+ // 主话题返回规范 id(按调用方给的 selfAid/peer 拼),话题返回话题 id。
859
+ threadId: sessionTopicThreadId(session)
860
+ ?? (safeFormatMainTopicId(selfAid, normalized) ?? ''),
861
+ sessionId: session.id,
862
+ peer: normalized,
863
+ channelType: 'aun',
864
+ peerKind: session.chatType === 'group' ? 'group' : 'private',
865
+ name: session.name ?? peer,
866
+ nameUpdatedAt: typeof session.metadata?.nameUpdatedAt === 'number' ? session.metadata.nameUpdatedAt : undefined,
867
+ label: session.label ?? null,
868
+ topicStatus: session.topicStatus ?? 'pending',
869
+ createdAt: session.createdAt ?? 0,
870
+ lastActiveAt: session.updatedAt ?? 0,
871
+ running: session.processingState?.startsWith('processing') ?? false,
872
+ };
873
+ }
874
+ // active.json 不存在时(旧数据,未经 daemon 持久化),回退到目录推断。
875
+ // findMainSessionEntry 用目录存在性 + messages.jsonl 第一行推断 chatType。
876
+ const entry = ThreadService.findMainSessionEntry(selfAid, normalized);
877
+ if (!entry)
752
878
  return undefined;
753
879
  return {
754
- threadId: session.threadId ?? '',
755
- sessionId: session.id,
880
+ threadId: entry.threadId,
881
+ sessionId: entry.sessionId,
756
882
  peer: normalized,
757
883
  channelType: 'aun',
758
- peerKind: session.chatType === 'group' ? 'group' : 'private',
759
- name: session.name ?? peer,
760
- label: session.label ?? null,
761
- topicStatus: session.topicStatus ?? 'pending',
762
- createdAt: session.createdAt ?? 0,
763
- lastActiveAt: session.updatedAt ?? 0,
764
- running: session.processingState?.startsWith('processing') ?? false,
884
+ peerKind: entry.peerKind,
885
+ name: entry.name,
886
+ nameUpdatedAt: undefined,
887
+ label: null,
888
+ topicStatus: 'pending',
889
+ createdAt: entry.createdAt,
890
+ lastActiveAt: entry.lastActiveAt,
891
+ running: false,
765
892
  };
766
893
  }
767
894
  // ── touch (update lastActiveAt) ──
@@ -783,7 +910,7 @@ export class ThreadService {
783
910
  * 禁止删除:busy(processingState 有值)的话题,以及 threadId 为空的主会话。
784
911
  */
785
912
  async deleteThread(selfAid, peer, threadId) {
786
- if (!threadId)
913
+ if (isMainSessionThreadId(threadId))
787
914
  throw new ThreadServiceError('不能删除主会话', 'NOT_A_TOPIC');
788
915
  const want = normalizeThreadPeer(peer);
789
916
  const indexPath = agentThreadIndexPath(selfAid);
@@ -865,7 +992,71 @@ export class ThreadService {
865
992
  const index = readIndex(agentThreadIndexPath(selfAid));
866
993
  return index.threads.find(t => t.threadId === threadId);
867
994
  }
868
- // ── Thread Context inbox(跨会话上下文的提醒缓存) ──
995
+ /**
996
+ * 同步读取主会话(threadId=null/空/"main")的等价 ThreadIndexEntry。
997
+ * 主会话不写入 threads 索引,需直读 active.json 构造。
998
+ * 供 resolveThreadRouteTarget 跨话题路由校验时使用。
999
+ */
1000
+ static findMainSessionEntry(selfAid, peer) {
1001
+ const normalizedPeer = normalizeThreadPeer(peer) ?? peer;
1002
+ const channelKey = `aun#${selfAid}#main`;
1003
+ // chatDirPath 期望的是 `<root>/agents` 这一级,它会自己拼 `<aid>/sessions/...`;
1004
+ // 传 agentSessionsDir 会得到 `agents/<aid>/sessions/<aid>/sessions/...` 的双层路径。
1005
+ const agentsRoot = path.dirname(agentDir(selfAid));
1006
+ const dir = chatDirPath(agentsRoot, 'aun', normalizedPeer, selfAid, channelKey);
1007
+ // 先尝试 active.json(热缓存,有则最准确)。
1008
+ let raw;
1009
+ try {
1010
+ raw = JSON.parse(fs.readFileSync(`${dir}/active.json`, 'utf8'));
1011
+ // active.json 里 threadId 是话题 id 说明该目录放的不是主会话,丢弃。
1012
+ if (isTopicThreadId(raw['threadId']))
1013
+ return undefined;
1014
+ }
1015
+ catch {
1016
+ raw = undefined;
1017
+ }
1018
+ // active.json 不存在(旧数据或未触发持久化)时,用目录本身推断:
1019
+ // 目录存在说明主会话存在,从 messages.jsonl 第一行读 chatType 补齐 peerKind。
1020
+ if (!raw) {
1021
+ if (!fs.existsSync(dir))
1022
+ return undefined;
1023
+ let chatType;
1024
+ try {
1025
+ const firstLine = fs.readFileSync(`${dir}/messages.jsonl`, 'utf8').split('\n')[0];
1026
+ if (firstLine?.trim()) {
1027
+ const msg = JSON.parse(firstLine);
1028
+ chatType = typeof msg['chatType'] === 'string' ? msg['chatType'] : undefined;
1029
+ }
1030
+ }
1031
+ catch { /* 读取失败时留 undefined,下面用默认值 */ }
1032
+ const peerKind = chatType === 'group' ? 'group' : 'private';
1033
+ return {
1034
+ threadId: safeFormatMainTopicId(selfAid, normalizedPeer) ?? '',
1035
+ sessionId: '',
1036
+ peer: normalizedPeer,
1037
+ channelType: 'aun',
1038
+ peerKind,
1039
+ name: normalizedPeer,
1040
+ topicStatus: 'pending',
1041
+ createdAt: 0,
1042
+ lastActiveAt: 0,
1043
+ };
1044
+ }
1045
+ const chatType = raw['chatType'];
1046
+ const peerKind = chatType === 'group' ? 'group' : 'private';
1047
+ return {
1048
+ threadId: safeFormatMainTopicId(selfAid, normalizedPeer) ?? '',
1049
+ sessionId: String(raw['id'] ?? raw['agentSessionId'] ?? ''),
1050
+ peer: normalizedPeer,
1051
+ channelType: 'aun',
1052
+ peerKind,
1053
+ name: String(raw['name'] ?? normalizedPeer),
1054
+ topicStatus: raw['topicStatus'] ?? 'pending',
1055
+ createdAt: Number(raw['createdAt'] ?? 0),
1056
+ lastActiveAt: Number(raw['updatedAt'] ?? 0),
1057
+ };
1058
+ }
1059
+ // ── Thread Context inbox(跨话题上下文的提醒缓存) ──
869
1060
  /** 跨话题发送成功后写入本端 inbox。 */
870
1061
  static appendInbox(selfAid, entry) {
871
1062
  new ThreadContextStorage().append(selfAid, entry);
@@ -929,7 +1120,7 @@ export class ThreadService {
929
1120
  if (!session)
930
1121
  return undefined;
931
1122
  peer = session.channelId;
932
- threadId = session.threadId ?? '';
1123
+ threadId = sessionTopicThreadId(session) ?? '';
933
1124
  }
934
1125
  if (!peer)
935
1126
  return undefined;