evolcore 0.0.35 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +134 -48
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +109 -34
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -8,6 +8,7 @@ import { isInternalInjectedUserMessage, isToolResultMessage } from './session-tr
8
8
  import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, compactJsonlTail, SESSION_META_COMPACT_POLICY, } from './session-fs-store.js';
9
9
  import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, normalizeTopicLabel, normalizeTopicStatus, } from './session-mapper.js';
10
10
  import { tryParseChannelKey } from '../channel-loader.js';
11
+ import { isMainSession, isMainSessionThreadId, isTopicSession, isTopicThreadId, mainTopicPeer, safeFormatMainTopicId, sessionLocalTopicId, sessionTopicThreadId, } from '../thread/topic-id.js';
11
12
  import { isSystemControlChannel } from '../system-channels.js';
12
13
  import path from 'path';
13
14
  import fs from 'fs';
@@ -507,11 +508,12 @@ export class SessionManager {
507
508
  /** 由 session 自身的 channelType/channelId/selfId/threadId 直接定位其 .jsonl 路径(不扫描目录)。 */
508
509
  metaPathForSession(session) {
509
510
  const dir = this.ensureChatDirForSession(session);
510
- const targetDir = session.threadId ? path.join(dir, '_threads') : dir;
511
+ // 主话题落在 chat 根目录;只有话题进 `_threads/`。
512
+ const targetDir = isTopicSession(session) ? path.join(dir, '_threads') : dir;
511
513
  return this.metaFilePath(targetDir, session.id);
512
514
  }
513
515
  forkHistoryPath(session) {
514
- if (!session.threadId)
516
+ if (!isTopicSession(session))
515
517
  throw new Error('fork history requires a topic session');
516
518
  return path.join(this.resolveChatDirFromSession(session), '_threads', `${session.id}.fork-history.json`);
517
519
  }
@@ -574,7 +576,7 @@ export class SessionManager {
574
576
  const loaded = this.loadSessionForUpdate(sessionId);
575
577
  const session = loaded?.current;
576
578
  const source = session?.metadata?.forkSource;
577
- if (!session?.threadId || !source)
579
+ if (!isTopicSession(session) || !source)
578
580
  throw new Error('fork topic is missing');
579
581
  if (source.historyVersion) {
580
582
  this.readForkHistory(session);
@@ -626,7 +628,7 @@ export class SessionManager {
626
628
  appendMeta(channel, channelId, session) {
627
629
  const file = sessionToFile(session);
628
630
  const metaPath = this.metaPathForSession(session);
629
- if (session.threadId)
631
+ if (isTopicSession(session))
630
632
  appendJsonlDurable(metaPath, file);
631
633
  else
632
634
  appendJsonl(metaPath, file);
@@ -671,7 +673,7 @@ export class SessionManager {
671
673
  // the newest backend fence unless this call is one of the two explicit
672
674
  // boundary writers. This is a last-line guard against a stale object
673
675
  // replaying an old agentSessionId into the JSONL history.
674
- if (session.threadId && !opts?.backendMutation) {
676
+ if (isTopicSession(session) && !opts?.backendMutation) {
675
677
  const latest = this.readMetaLatest(this.metaPathForSession(session));
676
678
  const latestId = normalizeBackendSessionId(latest?.agentSessionId);
677
679
  if (latest) {
@@ -714,7 +716,7 @@ export class SessionManager {
714
716
  }
715
717
  if (!opts?.forceWrite) {
716
718
  const metaPath = this.metaPathForSession(session);
717
- const lastMeta = session.threadId
719
+ const lastMeta = isTopicSession(session)
718
720
  ? readLastJsonlLineStrict(metaPath)
719
721
  : readLastJsonlLine(metaPath);
720
722
  if (lastMeta && this.sessionFilesEqual(lastMeta, sessionToFile(session))) {
@@ -776,7 +778,7 @@ export class SessionManager {
776
778
  return undefined;
777
779
  // active.json 不能参与 topic backend boundary/CAS;它可能落后于线程 JSONL。
778
780
  const active = this.readActive(fromJsonl.channel, fromJsonl.channelId);
779
- const base = (!fromJsonl.threadId && active && active.id === sessionId) ? active : fromJsonl;
781
+ const base = (isMainSession(fromJsonl) && active && active.id === sessionId) ? active : fromJsonl;
780
782
  const current = JSON.parse(JSON.stringify(base));
781
783
  return { current };
782
784
  }
@@ -789,8 +791,13 @@ export class SessionManager {
789
791
  logger.error(`[SessionManager] validateSessionFile: no adapter for baseagent=${session.baseagent}, session=${session.id}`);
790
792
  return agentSessionId;
791
793
  }
792
- if (adapter.checkExists(session.projectPath, agentSessionId, session.selfAID))
794
+ const status = adapter.checkStatus?.(session.projectPath, agentSessionId, session.selfAID)
795
+ ?? (adapter.checkExists(session.projectPath, agentSessionId, session.selfAID) ? 'exists' : 'missing');
796
+ if (status === 'exists' || status === 'unavailable') {
797
+ if (status === 'unavailable')
798
+ logger.info(`[SessionManager] backend unavailable; preserving ${session.baseagent} session binding: ${agentSessionId}`);
793
799
  return agentSessionId;
800
+ }
794
801
  logger.warn(`Session file not found for ${session.baseagent}: ${agentSessionId}, clearing session ID`);
795
802
  session.agentSessionId = undefined;
796
803
  this.persistSession(session, 'sync');
@@ -800,11 +807,15 @@ export class SessionManager {
800
807
  if (!session.agentSessionId)
801
808
  return false;
802
809
  const adapter = this.getFileAdapter(session.baseagent);
803
- return !!adapter?.checkExists(session.projectPath, session.agentSessionId, session.selfAID);
810
+ if (!adapter)
811
+ return false;
812
+ const status = adapter.checkStatus?.(session.projectPath, session.agentSessionId, session.selfAID)
813
+ ?? (adapter.checkExists(session.projectPath, session.agentSessionId, session.selfAID) ? 'exists' : 'missing');
814
+ return status !== 'missing';
804
815
  }
805
816
  getActiveChatType(channel, channelId) {
806
817
  const active = this.readActive(channel, channelId);
807
- if (active && !active.threadId)
818
+ if (active && isMainSession(active))
808
819
  return active.chatType || 'private';
809
820
  return 'private';
810
821
  }
@@ -968,7 +979,7 @@ export class SessionManager {
968
979
  }
969
980
  }
970
981
  assertTopicResumeMutationTarget(current, input) {
971
- if (!current.threadId || current.threadId !== input.expectedThreadId) {
982
+ if (sessionTopicThreadId(current) !== input.expectedThreadId) {
972
983
  const error = new Error('topic resumeAt threadId mismatch');
973
984
  error.code = 'THREAD_MISMATCH';
974
985
  throw error;
@@ -1035,7 +1046,7 @@ export class SessionManager {
1035
1046
  if (!input.expectedThreadId.trim())
1036
1047
  throw new Error('backend rotation requires a non-empty threadId');
1037
1048
  return this.mutateLatestTopicSession(input.sessionId, (current, found) => {
1038
- if (!current.threadId || current.threadId !== input.expectedThreadId) {
1049
+ if (sessionTopicThreadId(current) !== input.expectedThreadId) {
1039
1050
  const error = new Error('backend rotation threadId mismatch');
1040
1051
  error.code = 'THREAD_MISMATCH';
1041
1052
  throw error;
@@ -1084,7 +1095,7 @@ export class SessionManager {
1084
1095
  throw error;
1085
1096
  }
1086
1097
  return this.mutateLatestTopicSession(input.sessionId, (current, found) => {
1087
- if (!current.threadId)
1098
+ if (!isTopicSession(current))
1088
1099
  return 'stale';
1089
1100
  const active = current.metadata?.turnState?.active;
1090
1101
  if (!active
@@ -1223,7 +1234,8 @@ export class SessionManager {
1223
1234
  // 群聊场景下 conversationId 应为群 ID(AUN 群聊 channelId 即 groupId),私聊为 userId。
1224
1235
  // 用于 resolveIdentity fallback 正确命中群成员角色表(见 resolvePeerRoleDetail)。
1225
1236
  const identityConversationId = chatType === 'group' ? (metadata?.groupId || channelId) : userId;
1226
- if (threadId) {
1237
+ // 主话题的 id(`main:<aid>:<aid>`)不是话题会话目录键:它必须落到 chat 根目录。
1238
+ if (isTopicThreadId(threadId)) {
1227
1239
  const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus);
1228
1240
  session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
1229
1241
  return session;
@@ -1232,7 +1244,7 @@ export class SessionManager {
1232
1244
  const exactDir = this.resolveChatDirExact(channel, channelId, channelType, selfAID);
1233
1245
  const activeFile = readJsonFile(path.join(exactDir, 'active.json'));
1234
1246
  const active = activeFile ? fileToSession(activeFile) : undefined;
1235
- if (active && !active.threadId) {
1247
+ if (active && isMainSession(active)) {
1236
1248
  const validSessionId = this.validateSessionFile(active);
1237
1249
  const session = { ...active, agentSessionId: validSessionId };
1238
1250
  session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
@@ -1297,7 +1309,7 @@ export class SessionManager {
1297
1309
  const chatDir = this.resolveChatDir(channel, channelId, channelType, selfAID);
1298
1310
  const allSessions = this.findAllSessionsInChat(chatDir, false);
1299
1311
  const existing = allSessions
1300
- .filter(s => s.projectPath === defaultProjectPath && !s.threadId)
1312
+ .filter(s => s.projectPath === defaultProjectPath && isMainSession(s))
1301
1313
  .sort((a, b) => b.updatedAt - a.updatedAt)[0];
1302
1314
  if (existing) {
1303
1315
  const validSessionId = this.validateSessionFile(existing);
@@ -1342,7 +1354,7 @@ export class SessionManager {
1342
1354
  channelId,
1343
1355
  selfAID,
1344
1356
  projectPath: defaultProjectPath,
1345
- threadId: '',
1357
+ rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
1346
1358
  baseagent: resolvedBaseagent,
1347
1359
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
1348
1360
  chatType: chatType || 'private',
@@ -1378,7 +1390,7 @@ export class SessionManager {
1378
1390
  error.code = 'BASEAGENT_IMMUTABLE';
1379
1391
  throw error;
1380
1392
  }
1381
- if (current.threadId && Object.prototype.hasOwnProperty.call(updates, 'agentSessionId')) {
1393
+ if (isTopicSession(current) && Object.prototype.hasOwnProperty.call(updates, 'agentSessionId')) {
1382
1394
  const error = new Error('topic backend binding requires rotation or turn-lease activation');
1383
1395
  error.code = 'TOPIC_BACKEND_BINDING_PROTECTED';
1384
1396
  throw error;
@@ -1390,7 +1402,7 @@ export class SessionManager {
1390
1402
  if (updates.metadata !== undefined) {
1391
1403
  if (Object.hasOwn(updates.metadata, 'sessionLoop'))
1392
1404
  throw topicMetadataOwnershipError('sessionLoop');
1393
- if (current.threadId) {
1405
+ if (isTopicSession(current)) {
1394
1406
  for (const key of Object.keys(updates.metadata)) {
1395
1407
  if (TOPIC_METADATA_PROTECTED_KEYS.has(key))
1396
1408
  throw topicMetadataOwnershipError(key);
@@ -1421,7 +1433,7 @@ export class SessionManager {
1421
1433
  for (const key of Object.keys(patch)) {
1422
1434
  if (key === 'sessionLoop')
1423
1435
  throw topicMetadataOwnershipError(key);
1424
- if (current.threadId && TOPIC_METADATA_PROTECTED_KEYS.has(key)) {
1436
+ if (isTopicSession(current) && TOPIC_METADATA_PROTECTED_KEYS.has(key)) {
1425
1437
  throw topicMetadataOwnershipError(key);
1426
1438
  }
1427
1439
  }
@@ -1482,7 +1494,7 @@ export class SessionManager {
1482
1494
  // by this generic lookup; doing so would let the next run create a
1483
1495
  // backend without an explicit rotation. Repair/rotation decides how
1484
1496
  // to handle an unavailable bound backend.
1485
- const validSessionId = existing.threadId
1497
+ const validSessionId = isTopicSession(existing)
1486
1498
  ? normalizeBackendSessionId(existing.agentSessionId)
1487
1499
  : this.validateSessionFile(existing);
1488
1500
  let mutated = false;
@@ -1537,7 +1549,7 @@ export class SessionManager {
1537
1549
  channelId,
1538
1550
  selfAID: selfAID || '',
1539
1551
  projectPath,
1540
- threadId,
1552
+ rawThreadId: threadId,
1541
1553
  baseagent: inheritedBaseagent,
1542
1554
  sessionKey,
1543
1555
  chatType: effectiveChatType,
@@ -1632,7 +1644,7 @@ export class SessionManager {
1632
1644
  const chatDir = this.ensureResolvedChatDir(channel, channelId, identity.channelType, identity.selfAID);
1633
1645
  const allSessions = this.findAllSessionsInChat(chatDir, false);
1634
1646
  const target = allSessions
1635
- .filter(s => s.projectPath === newProjectPath && s.baseagent === baseagent && !s.threadId)
1647
+ .filter(s => s.projectPath === newProjectPath && s.baseagent === baseagent && isMainSession(s))
1636
1648
  .sort((a, b) => b.updatedAt - a.updatedAt)[0];
1637
1649
  if (target) {
1638
1650
  const validSessionId = this.validateSessionFile(target);
@@ -1651,7 +1663,7 @@ export class SessionManager {
1651
1663
  channelId,
1652
1664
  selfAID,
1653
1665
  projectPath: newProjectPath,
1654
- threadId: '',
1666
+ rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
1655
1667
  baseagent: baseagent,
1656
1668
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
1657
1669
  chatType: inheritedChatType,
@@ -1677,7 +1689,7 @@ export class SessionManager {
1677
1689
  const active = this.readActive(channel, channelId);
1678
1690
  if (!active)
1679
1691
  return;
1680
- if (active.threadId) {
1692
+ if (isTopicSession(active)) {
1681
1693
  // Topic backend binding is owned exclusively by rotation or an active
1682
1694
  // TurnLease activation. Keep this legacy channel-scoped API main-session
1683
1695
  // only so stale callers cannot resurrect a renewed topic backend.
@@ -1692,7 +1704,7 @@ export class SessionManager {
1692
1704
  if (!loaded)
1693
1705
  return 'stale';
1694
1706
  const { current } = loaded;
1695
- if (current.threadId) {
1707
+ if (isTopicSession(current)) {
1696
1708
  if (turnLease && normalizeBackendSessionId(agentSessionId)) {
1697
1709
  return this.activateTopicBackend({ sessionId, lease: turnLease, agentSessionId });
1698
1710
  }
@@ -1719,7 +1731,7 @@ export class SessionManager {
1719
1731
  if (!loaded)
1720
1732
  return 'missing';
1721
1733
  const { current } = loaded;
1722
- if (current.threadId)
1734
+ if (isTopicSession(current))
1723
1735
  return 'topic';
1724
1736
  const currentId = normalizeBackendSessionId(current.agentSessionId);
1725
1737
  if (!currentId)
@@ -1745,7 +1757,7 @@ export class SessionManager {
1745
1757
  const chatDir = this.ensureResolvedChatDir(channel, channelId, channelType, selfAID);
1746
1758
  const allSessions = this.findAllSessionsInChat(chatDir, false);
1747
1759
  const target = allSessions
1748
- .filter(s => s.projectPath === projectPath && s.baseagent === newBaseagent && !s.threadId)
1760
+ .filter(s => s.projectPath === projectPath && s.baseagent === newBaseagent && isMainSession(s))
1749
1761
  .sort((a, b) => b.updatedAt - a.updatedAt)[0];
1750
1762
  if (target) {
1751
1763
  const validSessionId = this.validateSessionFile(target);
@@ -1760,7 +1772,7 @@ export class SessionManager {
1760
1772
  channelId,
1761
1773
  selfAID,
1762
1774
  projectPath,
1763
- threadId: '',
1775
+ rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
1764
1776
  baseagent: newBaseagent,
1765
1777
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
1766
1778
  chatType: inheritedChatType,
@@ -1834,14 +1846,22 @@ export class SessionManager {
1834
1846
  hasMainSession(channel, channelId, channelType, selfAID) {
1835
1847
  const chatDir = this.resolveChatDirExact(channel, channelId, channelType, selfAID);
1836
1848
  const active = readJsonFile(path.join(chatDir, 'active.json'));
1837
- if (active && !active.threadId)
1849
+ if (active && !isTopicThreadId(active.threadId))
1838
1850
  return true;
1839
1851
  return scanMetaFiles(chatDir).some(metaFile => {
1840
1852
  const session = readLastJsonlLine(path.join(chatDir, metaFile));
1841
- return !!session && !session.threadId;
1853
+ return !!session && !isTopicThreadId(session.threadId);
1842
1854
  });
1843
1855
  }
1844
1856
  async getThreadSession(channel, channelId, threadId, selfAID) {
1857
+ // 主话题 id 不在 `_threads/index.json` 里,它对应 chat 根目录的 active session。
1858
+ // id 自带双方,必须校验"当前本端确实在两端之内",且另一侧就是 channelId。
1859
+ if (isMainSessionThreadId(threadId)) {
1860
+ const self = selfAID?.replace(/^@/, '') ?? '';
1861
+ if (selfAID && mainTopicPeer(threadId, self) !== channelId.replace(/^@/, ''))
1862
+ return undefined;
1863
+ return this.readActive(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
1864
+ }
1845
1865
  let chatDir;
1846
1866
  try {
1847
1867
  chatDir = this.resolveChatDirSafe(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
@@ -1873,7 +1893,8 @@ export class SessionManager {
1873
1893
  .filter(session => {
1874
1894
  if (archiveFilter === 'all')
1875
1895
  return true;
1876
- if (!session.threadId)
1896
+ // 主话题没有业务状态,永远算"活跃"
1897
+ if (!isTopicSession(session))
1877
1898
  return archiveFilter !== 'archived';
1878
1899
  const archived = session.topicStatus === 'archived';
1879
1900
  return archiveFilter === 'archived' ? archived : !archived;
@@ -1937,19 +1958,24 @@ export class SessionManager {
1937
1958
  current.metadata = metadata;
1938
1959
  this.persistSession(current, 'sync');
1939
1960
  }
1940
- async renameSession(sessionId, newName) {
1961
+ async renameSession(sessionId, newName, nameUpdatedAt) {
1941
1962
  const loaded = this.loadSessionForUpdate(sessionId);
1942
1963
  if (!loaded)
1943
1964
  return false;
1944
1965
  const { current } = loaded;
1945
1966
  current.name = newName;
1967
+ // 主话题的名称版本跟着会话走(话题在索引条目上)。远端同步来的改名必须
1968
+ // 带版本,否则后续无法判断"谁更新"。本地改名不传时保持不写版本。
1969
+ if (nameUpdatedAt !== undefined) {
1970
+ current.metadata = { ...(current.metadata ?? {}), nameUpdatedAt };
1971
+ }
1946
1972
  this.persistSession(current, 'sync');
1947
1973
  return true;
1948
1974
  }
1949
1975
  async setTopicLabel(sessionId, label) {
1950
1976
  const normalized = normalizeTopicLabel(label);
1951
1977
  const loaded = this.loadSessionForUpdate(sessionId);
1952
- if (!loaded?.current.threadId)
1978
+ if (!loaded || !isTopicSession(loaded.current))
1953
1979
  return false;
1954
1980
  loaded.current.label = normalized;
1955
1981
  this.persistSession(loaded.current, 'sync', { topicMutation: true });
@@ -1958,7 +1984,7 @@ export class SessionManager {
1958
1984
  async setTopicStatus(sessionId, status) {
1959
1985
  const normalized = normalizeTopicStatus(status);
1960
1986
  const loaded = this.loadSessionForUpdate(sessionId);
1961
- if (!loaded?.current.threadId)
1987
+ if (!loaded || !isTopicSession(loaded.current))
1962
1988
  return false;
1963
1989
  loaded.current.topicStatus = normalized;
1964
1990
  this.persistSession(loaded.current, 'sync', { topicMutation: true });
@@ -2115,7 +2141,7 @@ export class SessionManager {
2115
2141
  channelId,
2116
2142
  selfAID,
2117
2143
  projectPath,
2118
- threadId: '',
2144
+ rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
2119
2145
  baseagent,
2120
2146
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
2121
2147
  chatType: inheritedChatType,
@@ -2155,7 +2181,7 @@ export class SessionManager {
2155
2181
  }
2156
2182
  async createForkedSession(sourceSession, forkedAgentSessionId, name) {
2157
2183
  const channelType = sourceSession.channelType || sourceSession.channel;
2158
- const threadId = sourceSession.threadId || '';
2184
+ const sourceTopicId = sessionTopicThreadId(sourceSession);
2159
2185
  const session = {
2160
2186
  id: generateSessionId(),
2161
2187
  channel: sourceSession.channel,
@@ -2163,9 +2189,9 @@ export class SessionManager {
2163
2189
  channelId: sourceSession.channelId,
2164
2190
  selfAID: sourceSession.selfAID,
2165
2191
  projectPath: sourceSession.projectPath,
2166
- threadId,
2192
+ rawThreadId: sourceTopicId ?? sessionLocalTopicId(sourceSession) ?? '',
2167
2193
  baseagent: sourceSession.baseagent,
2168
- sessionKey: formatSessionKey(channelType, sourceSession.channelId, threadId || DEFAULT_THREAD_ID),
2194
+ sessionKey: formatSessionKey(channelType, sourceSession.channelId, sourceTopicId ?? DEFAULT_THREAD_ID),
2169
2195
  chatType: sourceSession.chatType || 'private',
2170
2196
  agentSessionId: forkedAgentSessionId,
2171
2197
  metadata: {},
@@ -2209,7 +2235,7 @@ export class SessionManager {
2209
2235
  replyContext: { threadId: targetThreadId },
2210
2236
  forkSource: {
2211
2237
  sessionId: sourceSession.id,
2212
- threadId: sourceSession.threadId || null,
2238
+ threadId: sessionTopicThreadId(sourceSession) ?? null,
2213
2239
  assistantMessageId: options.sourceAssistantMessageId,
2214
2240
  ...(options.requestedSourceAssistantMessageId
2215
2241
  ? { requestedAssistantMessageId: options.requestedSourceAssistantMessageId } : {}),
@@ -2229,7 +2255,7 @@ export class SessionManager {
2229
2255
  channelId: sourceSession.channelId,
2230
2256
  selfAID: sourceSession.selfAID,
2231
2257
  projectPath: sourceSession.projectPath,
2232
- threadId: targetThreadId,
2258
+ rawThreadId: targetThreadId,
2233
2259
  baseagent: sourceSession.baseagent,
2234
2260
  sessionKey,
2235
2261
  chatType: 'private',
@@ -2277,7 +2303,7 @@ export class SessionManager {
2277
2303
  projectPath: session.projectPath,
2278
2304
  name: session.name,
2279
2305
  chatType: session.chatType,
2280
- threadId: session.threadId,
2306
+ threadId: sessionTopicThreadId(session),
2281
2307
  baseagent: session.baseagent,
2282
2308
  timestamp: now,
2283
2309
  });
@@ -2301,7 +2327,8 @@ export class SessionManager {
2301
2327
  channelId: sourceSession.channelId,
2302
2328
  selfAID: sourceSession.selfAID,
2303
2329
  projectPath: sourceSession.projectPath,
2304
- threadId: '',
2330
+ // 续期沿用原会话的话题身份:主话题保持规范 id,话题保持原 id。
2331
+ rawThreadId: sessionLocalTopicId(sourceSession) ?? '',
2305
2332
  baseagent: context?.baseagent || sourceSession.baseagent,
2306
2333
  sessionKey: formatSessionKey(sourceSession.channelType || sourceSession.channel, sourceSession.channelId, DEFAULT_THREAD_ID),
2307
2334
  chatType: sourceSession.chatType || 'private',
@@ -2328,7 +2355,7 @@ export class SessionManager {
2328
2355
  sourceChannel: sourceSession.channelType || sourceSession.channel,
2329
2356
  sourceChannelId: sourceSession.channelId,
2330
2357
  sourceChatType: sourceSession.chatType,
2331
- sourceThreadId: sourceSession.threadId,
2358
+ sourceThreadId: sessionTopicThreadId(sourceSession),
2332
2359
  sourceSelfAid: sourceSession.selfAID,
2333
2360
  sourcePeerId: sourceSession.metadata?.peerId,
2334
2361
  timestamp: Date.now(),
@@ -2447,7 +2474,7 @@ export class SessionManager {
2447
2474
  channelId,
2448
2475
  selfAID,
2449
2476
  projectPath,
2450
- threadId: '',
2477
+ rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
2451
2478
  baseagent: resolvedBaseagent,
2452
2479
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
2453
2480
  chatType: inheritedChatType,
@@ -1,4 +1,5 @@
1
1
  import { formatTimestamp } from './session-fs-store.js';
2
+ import { classifyThreadId, sessionThreadKind } from '../thread/topic-id.js';
2
3
  /** Default topic component used by the stable session routing key. */
3
4
  export const DEFAULT_THREAD_ID = 'main';
4
5
  export function normalizeTopicLabel(value) {
@@ -98,7 +99,10 @@ export function sessionToFile(session) {
98
99
  }
99
100
  const now = session.updatedAt || Date.now();
100
101
  const channelType = session.channelType || session.channel;
101
- const threadId = session.threadId || DEFAULT_THREAD_ID;
102
+ // sessionKey 的第三段始终是渠道级占位 `main`:主话题的规范 id 会随关系变化,
103
+ // 而 sessionKey 是路由键,不能因为对方 AID 不同就换一个存储位置。
104
+ const kind = sessionThreadKind(session);
105
+ const threadId = kind.kind === 'topic' ? kind.threadId : DEFAULT_THREAD_ID;
102
106
  if (!session.baseagent) {
103
107
  throw new Error(`[session-mapper] sessionToFile: session.baseagent is empty for session ${session.id}`);
104
108
  }
@@ -110,13 +114,16 @@ export function sessionToFile(session) {
110
114
  sessionKey: formatSessionKey(channelType, session.channelId, threadId),
111
115
  selfAID: session.selfAID,
112
116
  baseagent: session.baseagent,
113
- threadId: session.threadId || '',
117
+ // 主话题落盘的是规范 id(`main:<aid>:<aid>`),不是空串——展示、持久化、
118
+ // 消费必须是同一个字符串。内部对端(bg:/trigger:/daemon:)与缺 selfAID 的
119
+ // 非 AUN 渠道拼不出 id,退回空串,读取端用 isMainSessionThreadId 统一识别。
120
+ threadId: kind.kind === 'topic' ? kind.threadId : (kind.topicId ?? ''),
114
121
  chatType: session.chatType || 'private',
115
122
  projectPath: session.projectPath,
116
123
  agentSessionId: session.agentSessionId ?? null,
117
124
  name: session.name ?? null,
118
125
  ...(session.label ? { label: session.label } : {}),
119
- ...(session.threadId ? { topicStatus: session.topicStatus ?? 'pending' } : {}),
126
+ ...(kind.kind === 'topic' ? { topicStatus: session.topicStatus ?? 'pending' } : {}),
120
127
  activeTask: session.processingState ?? null,
121
128
  metadata,
122
129
  createdAt: session.createdAt,
@@ -153,22 +160,26 @@ export function fileToSession(file) {
153
160
  if (v !== undefined)
154
161
  metadata[k] = v;
155
162
  }
163
+ // 主话题的 threadId 在旧文件里是空串(更早是 "main")。读入时统一补成规范 id,
164
+ // 让内存中的 Session 与 list/CLI 看到的是同一个字符串;写回时 sessionToFile 会落盘。
165
+ const kind = classifyThreadId(file.threadId, file.selfAID, file.channelId);
166
+ const isTopic = kind.kind === 'topic';
156
167
  return {
157
168
  id: file.id,
158
169
  channel: file.channel,
159
170
  channelType: file.channelType,
160
171
  channelId: file.channelId,
161
- sessionKey: file.sessionKey || formatSessionKey(file.channelType, file.channelId, file.threadId || DEFAULT_THREAD_ID),
172
+ sessionKey: file.sessionKey || formatSessionKey(file.channelType, file.channelId, isTopic ? kind.threadId : DEFAULT_THREAD_ID),
162
173
  selfAID: file.selfAID,
163
174
  baseagent,
164
- threadId: file.threadId,
175
+ rawThreadId: isTopic ? kind.threadId : (kind.topicId ?? ''),
165
176
  chatType: file.chatType,
166
177
  projectPath: file.projectPath,
167
178
  agentSessionId: file.agentSessionId ?? undefined,
168
179
  name: file.name ?? undefined,
169
- ...(file.threadId && (file.label || (file.lable && file.lable !== '归档'))
180
+ ...(isTopic && (file.label || (file.lable && file.lable !== '归档'))
170
181
  ? { label: file.label || file.lable } : {}),
171
- ...(file.threadId ? { topicStatus: file.topicStatus
182
+ ...(isTopic ? { topicStatus: file.topicStatus
172
183
  ? normalizeTopicStatus(file.topicStatus)
173
184
  : file.lable === '归档' ? 'archived' : 'pending' } : {}),
174
185
  processingState: file.activeTask ?? undefined,
@@ -6,6 +6,7 @@ import { logger } from '../../utils/logger.js';
6
6
  import { isTransientProtocolMessage, messageLogPath } from '../message/message-log.js';
7
7
  import { readAllJsonlLines } from './session-fs-store.js';
8
8
  import { getPackageRoot } from '../../paths.js';
9
+ import { isTopicSession, sessionTopicThreadId } from '../thread/topic-id.js';
9
10
  const DEFAULT_AFTER_HOURS = 24;
10
11
  const DEFAULT_FALLBACK = 'continue';
11
12
  const DEFAULT_NEW_PROBABILITY_THRESHOLD = 0.85;
@@ -159,7 +160,7 @@ export class SessionRenewService {
159
160
  return { session: request.session, renewed: false };
160
161
  // Thread / processing sessions are already an explicit routing decision.
161
162
  // Do not let a cached or active main session replace them before resolveLocked().
162
- if (request.session.threadId || request.session.processingState) {
163
+ if (isTopicSession(request.session) || request.session.processingState) {
163
164
  return { session: request.session, renewed: false };
164
165
  }
165
166
  const routeKey = this.routeKey(request);
@@ -220,11 +221,11 @@ export class SessionRenewService {
220
221
  let decision;
221
222
  let source = 'model';
222
223
  let judgeMetadata = {
223
- model: effectiveConfig.decision_model?.model,
224
+ model: effectiveConfig.Services?.decision ? effectiveConfig.Services.decision.model : effectiveConfig.decision_model?.model,
224
225
  threshold,
225
226
  };
226
227
  try {
227
- const model = effectiveConfig.decision_model?.model?.trim();
228
+ const model = (effectiveConfig.Services?.decision ? effectiveConfig.Services.decision.model : effectiveConfig.decision_model?.model)?.trim();
228
229
  if (!model)
229
230
  throw new SessionRenewJudgeError('decision_model_missing');
230
231
  const provider = this.inferenceProviderResolver.getDecisionInferenceProvider(request.selfAid);
@@ -355,7 +356,7 @@ export class SessionRenewService {
355
356
  }
356
357
  }
357
358
  routeKey(request) {
358
- return [request.channelType, request.selfAid || '', request.channelId, request.session.threadId || ''].join('#');
359
+ return [request.channelType, request.selfAid || '', request.channelId, sessionTopicThreadId(request.session) ?? ''].join('#');
359
360
  }
360
361
  async withLock(key, operation) {
361
362
  const previous = this.locks.get(key) ?? Promise.resolve();
@@ -1,3 +1,4 @@
1
+ import { isTopicThreadId } from '../thread/topic-id.js';
1
2
  const SYSTEM_PROMPT_MARKERS = [
2
3
  '--- [SYSTEM_PROMPT_END] ---',
3
4
  '<system-reminder>',
@@ -150,5 +151,5 @@ export function shouldAutoFillSessionTitle(title, threadId) {
150
151
  const normalized = sanitizeSessionTitle(title);
151
152
  if (normalized === '默认会话')
152
153
  return true;
153
- return !!threadId && threadId !== 'main' && normalized === '话题会话';
154
+ return isTopicThreadId(threadId) && normalized === '话题会话';
154
155
  }
@@ -107,7 +107,7 @@ export class SessionTurnCoordinator {
107
107
  await this.persist(session, next);
108
108
  return cloneTurnMetadata(next);
109
109
  }
110
- async completeInterrupt(session, reason) {
110
+ async completeInterrupt(session, reason, recoverable) {
111
111
  const state = this.hydrate(session);
112
112
  const active = state.active;
113
113
  if (!active) {
@@ -139,6 +139,7 @@ export class SessionTurnCoordinator {
139
139
  ? 'cancelled'
140
140
  : 'superseded',
141
141
  reason,
142
+ ...(recoverable !== undefined ? { recoverable } : {}),
142
143
  finishedAt: Date.now(),
143
144
  },
144
145
  };
@@ -213,7 +214,7 @@ export class SessionTurnCoordinator {
213
214
  await this.persist(session, next);
214
215
  return decision;
215
216
  }
216
- async finish(session, lease, status, reason) {
217
+ async finish(session, lease, status, reason, recoverable) {
217
218
  const state = this.hydrate(session);
218
219
  if (!this.isCurrent(lease))
219
220
  return false;
@@ -227,6 +228,7 @@ export class SessionTurnCoordinator {
227
228
  status,
228
229
  recoveryContext: terminalRecoveryContext(session, state.active?.recoveryContext),
229
230
  reason,
231
+ ...(recoverable !== undefined ? { recoverable } : {}),
230
232
  finishedAt: Date.now(),
231
233
  },
232
234
  };
@@ -35,25 +35,11 @@ function generateOneIndex(indexDir, filename, skills, label) {
35
35
  }
36
36
  return;
37
37
  }
38
- // 根目录从任一条目反推(同组内同源,父目录一致)
39
- const rootDir = path.dirname(skills[0].skillDir);
40
- // 示例用第一个技能的真实路径——模型照抄真路径比按规则推导可靠
41
- const exampleName = skills[0].name;
42
- const examplePath = path.join(skills[0].skillDir, 'SKILL.md');
43
- // 路径规则块(用 path.sep,示例路径用真实技能名)
44
- const sep = path.sep;
45
38
  const header = `[skills-${label}] 本 Agent 已启用 ${skills.length} 个${label === 'builtin' ? '内置' : '本机'}技能。
46
39
 
47
- 加载方式: EC 技能用 Read 工具读取上面对应的 SKILL.md 全文后按内容执行;
40
+ 加载方式:
41
+ EC 技能用 \`ec skill load <技能名>\` 加载,命令直接输出 SKILL.md 正文并附带技能目录的绝对路径。
48
42
  它们不是宿主工具自带的技能或斜杠命令,用 Skill/斜杠命令加载会报 Unknown skill/未知命令。
49
-
50
- 路径规则:
51
- 根目录: ${rootDir}
52
- 技能目录 = 根目录 + "${sep}" + 技能名
53
- SKILL.md = 技能目录 + "${sep}SKILL.md"
54
- 例: 技能名为 ${exampleName},其 SKILL.md 完整路径为
55
- ${examplePath}
56
- 执行脚本时必须写完整绝对路径。
57
43
  `;
58
44
  // 技能条目(name + description)
59
45
  const items = [];
@@ -1,5 +1,12 @@
1
1
  import { replyContextFromSession } from '../message/message-utils.js';
2
+ import { sessionTopicThreadId } from '../thread/topic-id.js';
2
3
  export const TASK_RECOVERY_PROMPT = '继续当前会话尚未完成的任务。先核对最新用户指令、已有结果和任务状态;若已完成、用户已停止或正在等待必要输入/审批,则不要重复执行。对可能已产生副作用的步骤先核实结果,再继续未完成的工作。';
4
+ /**
5
+ * Turn failure reason prefix: the agent run finished normally but delivering
6
+ * its final reply failed. Re-running the agent cannot fix a transport failure,
7
+ * so automatic recovery must not pick these turns up.
8
+ */
9
+ export const REPLY_DELIVERY_FAILED_REASON = 'reply_delivery_failed';
3
10
  function lastTask(state) {
4
11
  return state?.active ?? state?.lastTerminal;
5
12
  }
@@ -22,7 +29,11 @@ function canRecover(session, automatic, origin) {
22
29
  || (context?.agentSessionId && context.agentSessionId !== session.agentSessionId))
23
30
  return false;
24
31
  const reason = 'cancelReason' in task ? task.cancelReason : 'reason' in task ? task.reason : undefined;
25
- if (reason && /interaction|approval|permission|denied|recalled|new_message|empty_prompt/i.test(reason))
32
+ // Provider/model configuration is deterministic: an automatic retry resumes
33
+ // the same failed thread on the same missing model and only extends the loop.
34
+ if (reason && /interaction|approval|permission|denied|recalled|new_message|empty_prompt|invalid_model|model_not_found|model_unavailable/i.test(reason))
35
+ return false;
36
+ if (automatic && reason?.startsWith(REPLY_DELIVERY_FAILED_REASON))
26
37
  return false;
27
38
  if (task.status === 'cancelled' || task.status === 'superseded' || reason === 'stop' || reason === 'explicit_cancel') {
28
39
  return !automatic && (reason === 'stop' || reason === 'explicit_cancel');
@@ -31,6 +42,8 @@ function canRecover(session, automatic, origin) {
31
42
  return true;
32
43
  if (state?.active)
33
44
  return false;
45
+ if (state?.lastTerminal?.recoverable !== true)
46
+ return false;
34
47
  return !context?.originTriggerId || sameRecoveryRun(context.recovery, origin);
35
48
  }
36
49
  /** Check again at dequeue: a newer task, /stop, /renew or backend switch wins. */
@@ -150,7 +163,7 @@ export class TaskRecoveryService {
150
163
  } } : {}),
151
164
  channel: session.metadata?.channelKey || session.channel,
152
165
  channelType, channelId: session.channelId, selfAID: session.selfAID,
153
- baseagent: session.baseagent, threadId: session.threadId,
166
+ baseagent: session.baseagent, threadId: sessionTopicThreadId(session) ?? '',
154
167
  chatType: session.chatType === 'group' ? 'group' : 'private',
155
168
  groupId: session.chatType === 'group' ? session.metadata?.groupId || session.channelId : undefined,
156
169
  peerId, peerName, peerType,