evolcore 0.0.32 → 0.0.34

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 (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
@@ -1,25 +1,142 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { ConfigTarget, read as readConfig } from '../../config/config-manager.js';
3
+ import { ConfigTarget, mutateConfig, read as readConfig } from '../../config/config-manager.js';
4
4
  import { formatPeerKey } from '../relation/peer-identity.js';
5
+ import { isSystemControlChannel } from '../system-channels.js';
5
6
  import { logger } from '../../utils/logger.js';
6
7
  import { atomicWriteJson } from './session-fs-store.js';
7
8
  import { createRootCausation } from '../causation/context.js';
8
- export const MIN_SESSION_IDLE_SECONDS = 30;
9
- export function readRelationSessionIdleSeconds(session) {
10
- if (!session.selfAID || !session.channelId)
9
+ export const DEFAULT_SESSION_LOOP_SECONDS = 300;
10
+ export const MIN_SESSION_LOOP_SECONDS = 30;
11
+ export const MAX_SESSION_LOOP_SECONDS = 86400;
12
+ export function normalizeSessionLoopConfig(value) {
13
+ if (!value || typeof value !== 'object' || Array.isArray(value))
11
14
  return undefined;
12
- try {
13
- const relation = readConfig(ConfigTarget.Relation, {
14
- self: session.selfAID,
15
- peerKey: formatPeerKey(session.channelType || session.channel, session.channelId),
16
- }, { cache: true, expand: true });
17
- return relation?.sessionIdleSec;
18
- }
19
- catch (error) {
20
- logger.warn(`[SessionIdleMonitor] Failed to read relation config for ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
15
+ const raw = value;
16
+ if (Object.keys(raw).some(key => key !== 'enabled' && key !== 'idleSeconds'))
17
+ return undefined;
18
+ if (raw.enabled === false && Object.keys(raw).every(key => key === 'enabled'))
19
+ return { enabled: false };
20
+ if (raw.enabled !== true || !isValidIdleSeconds(raw.idleSeconds))
21
+ return undefined;
22
+ return { enabled: true, idleSeconds: raw.idleSeconds };
23
+ }
24
+ export function validateIdleSeconds(value) {
25
+ return isValidIdleSeconds(value) ? value : undefined;
26
+ }
27
+ export function resolveSessionLoopTarget(session) {
28
+ if (!session || session.deletedAt || session.topicStatus === 'archived'
29
+ || session.metadata?.sessionType || isSystemControlChannel(session.channel)
30
+ || (session.chatType !== 'private' && session.chatType !== 'group'))
31
+ return undefined;
32
+ const peerId = session.chatType === 'group'
33
+ ? session.metadata?.groupId || session.channelId
34
+ : session.channelId;
35
+ if (!session.selfAID || !peerId)
21
36
  return undefined;
37
+ const peerKey = formatPeerKey(session.channelType || session.channel, peerId);
38
+ return { level: session.threadId ? 'topic-session' : 'relation-main', session, peerKey };
39
+ }
40
+ export function readSessionLoopConfig(target) {
41
+ let value;
42
+ if (target.level === 'topic-session')
43
+ value = target.session.metadata?.sessionLoop;
44
+ else {
45
+ try {
46
+ const relation = readConfig(ConfigTarget.Relation, {
47
+ self: target.session.selfAID,
48
+ peerKey: target.peerKey,
49
+ }, { cache: true, expand: true });
50
+ value = relation?.sessionLoop;
51
+ }
52
+ catch (error) {
53
+ logger.warn(`[SessionLoop] Failed to read configuration for ${target.session.id}; disabled: ${error instanceof Error ? error.message : String(error)}`);
54
+ return undefined;
55
+ }
56
+ }
57
+ const config = normalizeSessionLoopConfig(value);
58
+ if (value !== undefined && !config)
59
+ logger.warn(`[SessionLoop] Invalid configuration for ${target.session.id}; disabled`);
60
+ return config;
61
+ }
62
+ export function readSessionLoopConfigForSession(session) {
63
+ const target = resolveSessionLoopTarget(session);
64
+ return target ? readSessionLoopConfig(target) : undefined;
65
+ }
66
+ export function updateRelationSessionLoop(session, config) {
67
+ const target = resolveSessionLoopTarget(session);
68
+ if (!target || target.level !== 'relation-main')
69
+ throw sessionLoopError('INVALID_TARGET', 'relation main session is required');
70
+ const selector = { self: session.selfAID, peerKey: target.peerKey };
71
+ const normalized = normalizeSessionLoopConfig(config);
72
+ if (!normalized)
73
+ throw sessionLoopError('INVALID_ARGUMENT', 'invalid sessionLoop configuration');
74
+ mutateConfig(ConfigTarget.Relation, selector, current => {
75
+ current.sessionLoop = normalized;
76
+ });
77
+ }
78
+ export async function resolveCurrentSessionLoopTarget(store, route) {
79
+ let session;
80
+ try {
81
+ session = route.threadId
82
+ ? await store.getThreadSession(route.channel, route.channelId, route.threadId)
83
+ : await store.getActiveSession(route.channel, route.channelId);
84
+ }
85
+ catch {
86
+ throw sessionLoopError('SESSION_LOOKUP_FAILED', '无法读取当前会话,拒绝回退到其他会话');
87
+ }
88
+ if (!session || session.deletedAt)
89
+ throw sessionLoopError(route.threadId ? 'NOT_FOUND' : 'NO_ACTIVE_SESSION', '当前会话不存在');
90
+ const target = resolveSessionLoopTarget(session);
91
+ if (!target || !route.selfAid || session.selfAID !== route.selfAid
92
+ || session.channel !== route.channel || session.channelId !== route.channelId
93
+ || (session.threadId || '') !== (route.threadId || '')) {
94
+ throw sessionLoopError('INVALID_CONTEXT', '当前会话路由或归属不匹配');
95
+ }
96
+ return target;
97
+ }
98
+ export async function refreshSessionLoopTarget(store, route, expected) {
99
+ const current = await resolveCurrentSessionLoopTarget(store, route);
100
+ if (current.session.id !== expected.session.id || current.peerKey !== expected.peerKey || current.level !== expected.level) {
101
+ throw sessionLoopError('CONFLICT', '当前会话已发生变化,请重新查询');
22
102
  }
103
+ return current;
104
+ }
105
+ export function writeSessionLoopConfig(store, target, config) {
106
+ if (target.level === 'topic-session') {
107
+ const session = store.updateSessionLoop(target.session.id, config);
108
+ return { ...target, session };
109
+ }
110
+ const active = store.getActiveSessionSync(target.session.channel, target.session.channelId);
111
+ if (!active || active.deletedAt || active.id !== target.session.id || active.threadId) {
112
+ throw sessionLoopError('CONFLICT', '当前主会话已发生变化,请重新查询');
113
+ }
114
+ updateRelationSessionLoop(active, config);
115
+ return { ...target, session: active };
116
+ }
117
+ export function sessionLoopPayload(target) {
118
+ const config = readSessionLoopConfig(target);
119
+ return {
120
+ target: { level: target.level, sessionId: target.session.id },
121
+ sessionId: target.session.id,
122
+ value: config?.enabled === true,
123
+ ...(config?.enabled ? { idleSeconds: config.idleSeconds } : {}),
124
+ };
125
+ }
126
+ export function sessionLoopError(code, message) {
127
+ return Object.assign(new Error(message), { code });
128
+ }
129
+ function isValidIdleSeconds(value) {
130
+ return typeof value === 'number'
131
+ && Number.isFinite(value)
132
+ && Number.isSafeInteger(value)
133
+ && value >= MIN_SESSION_LOOP_SECONDS
134
+ && value <= MAX_SESSION_LOOP_SECONDS;
135
+ }
136
+ export const MIN_SESSION_IDLE_SECONDS = MIN_SESSION_LOOP_SECONDS;
137
+ export function readSessionLoopIdleSeconds(session) {
138
+ const config = readSessionLoopConfigForSession(session);
139
+ return config?.enabled ? config.idleSeconds : undefined;
23
140
  }
24
141
  /**
25
142
  * Publishes one session:idle event after a runner task ends, using the
@@ -71,7 +188,17 @@ export class SessionIdleMonitor {
71
188
  if (event.type !== 'session:switched')
72
189
  return;
73
190
  this.clearSession(event.fromSessionId);
74
- this.clearSession(event.toSessionId);
191
+ void this.activateMainSession(event.toSessionId);
192
+ };
193
+ onSessionCreated = (event) => {
194
+ if (event.type !== 'session:created' || event.threadId)
195
+ return;
196
+ for (const sessionId of this.sessions.keys())
197
+ void this.pruneInactiveMainSession(sessionId);
198
+ };
199
+ onSessionLoopChanged = (event) => {
200
+ if (event.type === 'session:loop-changed')
201
+ void this.refreshSession(event.sessionId, event.timestamp);
75
202
  };
76
203
  constructor(options) {
77
204
  this.options = options;
@@ -88,6 +215,8 @@ export class SessionIdleMonitor {
88
215
  this.options.eventBus.subscribe('task:started', this.onTaskStarted);
89
216
  this.options.eventBus.subscribe('session:deleted', this.onSessionDeleted);
90
217
  this.options.eventBus.subscribe('session:switched', this.onSessionSwitched);
218
+ this.options.eventBus.subscribe('session:created', this.onSessionCreated);
219
+ this.options.eventBus.subscribe('session:loop-changed', this.onSessionLoopChanged);
91
220
  }
92
221
  /** Arm restored timers only after event-trigger subscriptions are registered. */
93
222
  activate() {
@@ -108,6 +237,47 @@ export class SessionIdleMonitor {
108
237
  this.scheduleSession(sessionId);
109
238
  }
110
239
  }
240
+ async refreshSession(sessionId, changedAt = this.now()) {
241
+ const state = this.sessions.get(sessionId);
242
+ if (!state || !this.active)
243
+ return;
244
+ const generation = this.generations.get(sessionId) ?? 0;
245
+ try {
246
+ const session = await this.options.getSessionById(sessionId);
247
+ if (!session || !this.isEligibleSession(session))
248
+ return;
249
+ const seconds = await this.readIdleSeconds(session);
250
+ if (this.sessions.get(sessionId) !== state || (this.generations.get(sessionId) ?? 0) !== generation)
251
+ return;
252
+ if (state.idleSeconds === seconds)
253
+ return;
254
+ this.clearTimers(sessionId);
255
+ this.generations.set(sessionId, generation + 1);
256
+ state.idleSeconds = seconds;
257
+ state.armedAt = changedAt;
258
+ state.emitted = false;
259
+ this.persistState();
260
+ if (seconds !== undefined && !await this.options.isSessionBusy(sessionId))
261
+ this.scheduleSession(sessionId);
262
+ }
263
+ catch (error) {
264
+ this.warn(`Failed to refresh loop configuration for ${sessionId}`, error);
265
+ }
266
+ }
267
+ async refreshRelation(selfAid, peerKey, changedAt = this.now()) {
268
+ for (const sessionId of this.sessions.keys()) {
269
+ try {
270
+ const session = await this.options.getSessionById(sessionId);
271
+ const target = resolveSessionLoopTarget(session);
272
+ if (target?.level === 'relation-main' && target.peerKey === peerKey && session?.selfAID === selfAid) {
273
+ await this.refreshSession(sessionId, changedAt);
274
+ }
275
+ }
276
+ catch (error) {
277
+ this.warn(`Failed to refresh relation loop configuration for ${sessionId}`, error);
278
+ }
279
+ }
280
+ }
111
281
  stop() {
112
282
  this.active = false;
113
283
  this.clearAllTimers();
@@ -120,6 +290,8 @@ export class SessionIdleMonitor {
120
290
  this.options.eventBus.unsubscribe('task:started', this.onTaskStarted);
121
291
  this.options.eventBus.unsubscribe('session:deleted', this.onSessionDeleted);
122
292
  this.options.eventBus.unsubscribe('session:switched', this.onSessionSwitched);
293
+ this.options.eventBus.unsubscribe('session:created', this.onSessionCreated);
294
+ this.options.eventBus.unsubscribe('session:loop-changed', this.onSessionLoopChanged);
123
295
  }
124
296
  now() {
125
297
  return this.options.now?.() ?? Date.now();
@@ -172,15 +344,21 @@ export class SessionIdleMonitor {
172
344
  this.clearSession(sessionId);
173
345
  return;
174
346
  }
175
- if (!isSupportedSession(session))
347
+ if (!this.isEligibleSession(session)) {
348
+ this.clearTimers(sessionId);
176
349
  return;
350
+ }
177
351
  const idleSeconds = await this.readIdleSeconds(session);
178
352
  if (!this.isCurrentCycle(sessionId, state.lastEndedAt, generation))
179
353
  return;
354
+ if (this.timers.has(sessionId))
355
+ return;
180
356
  const idleMs = toIdleMs(idleSeconds);
357
+ state.idleSeconds = idleSeconds;
358
+ this.persistState();
181
359
  if (idleMs === undefined)
182
360
  return;
183
- const delay = Math.max(0, state.lastEndedAt + idleMs - this.now());
361
+ const delay = Math.max(0, (state.armedAt ?? state.lastEndedAt) + idleMs - this.now());
184
362
  const timer = setTimeout(() => {
185
363
  this.timers.delete(sessionId);
186
364
  void this.emitIfIdle(sessionId, state.lastEndedAt, generation);
@@ -209,14 +387,14 @@ export class SessionIdleMonitor {
209
387
  this.clearSession(sessionId);
210
388
  return;
211
389
  }
212
- if (!isSupportedSession(session))
390
+ if (!this.isEligibleSession(session))
213
391
  return;
214
392
  const idleMs = toIdleMs(await this.readIdleSeconds(session));
215
393
  if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
216
394
  return;
217
395
  if (idleMs === undefined)
218
396
  return;
219
- const remaining = lastEndedAt + idleMs - this.now();
397
+ const remaining = (this.sessions.get(sessionId)?.armedAt ?? lastEndedAt) + idleMs - this.now();
220
398
  if (remaining > 0) {
221
399
  this.scheduleSession(sessionId);
222
400
  return;
@@ -232,6 +410,8 @@ export class SessionIdleMonitor {
232
410
  }
233
411
  if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
234
412
  return;
413
+ if (!this.isEligibleSession(session))
414
+ return;
235
415
  if (busy) {
236
416
  // An active/queued task is not idle. Disarm this cycle and let its next
237
417
  // terminal event start a fresh one; repeatedly rearming here would poll
@@ -248,7 +428,8 @@ export class SessionIdleMonitor {
248
428
  if (!state || state.emitted)
249
429
  return;
250
430
  state.emitted = true;
251
- this.persistState();
431
+ if (!this.persistState())
432
+ return;
252
433
  this.options.eventBus.publish({
253
434
  type: 'session:idle',
254
435
  sessionId,
@@ -308,6 +489,49 @@ export class SessionIdleMonitor {
308
489
  return undefined;
309
490
  }
310
491
  }
492
+ isEligibleSession(session) {
493
+ return !!resolveSessionLoopTarget(session)
494
+ && (!!session.threadId || !this.options.isCurrentMainSession || this.options.isCurrentMainSession(session));
495
+ }
496
+ async disarmMainSession(sessionId) {
497
+ try {
498
+ const session = await this.options.getSessionById(sessionId);
499
+ if (!session || session.threadId)
500
+ return;
501
+ this.clearTimers(sessionId);
502
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
503
+ }
504
+ catch (error) {
505
+ this.warn(`Failed to disarm main session ${sessionId}`, error);
506
+ }
507
+ }
508
+ async activateMainSession(sessionId) {
509
+ try {
510
+ const session = await this.options.getSessionById(sessionId);
511
+ const state = this.sessions.get(sessionId);
512
+ if (!session || session.threadId || !state || !this.isEligibleSession(session))
513
+ return;
514
+ this.clearTimers(sessionId);
515
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
516
+ state.armedAt = this.now();
517
+ state.emitted = false;
518
+ this.persistState();
519
+ this.scheduleSession(sessionId);
520
+ }
521
+ catch (error) {
522
+ this.warn(`Failed to activate main session ${sessionId}`, error);
523
+ }
524
+ }
525
+ async pruneInactiveMainSession(sessionId) {
526
+ try {
527
+ const session = await this.options.getSessionById(sessionId);
528
+ if (session && !session.threadId && !this.isEligibleSession(session))
529
+ await this.disarmMainSession(sessionId);
530
+ }
531
+ catch (error) {
532
+ this.warn(`Failed to inspect main session ${sessionId}`, error);
533
+ }
534
+ }
311
535
  loadState() {
312
536
  try {
313
537
  if (!fs.existsSync(this.options.statePath))
@@ -330,6 +554,8 @@ export class SessionIdleMonitor {
330
554
  lastEndedAt,
331
555
  emitted,
332
556
  ...(typeof rawState.encrypted === 'boolean' ? { encrypted: rawState.encrypted } : {}),
557
+ ...(typeof rawState.armedAt === 'number' && Number.isFinite(rawState.armedAt) ? { armedAt: rawState.armedAt } : {}),
558
+ ...(typeof rawState.idleSeconds === 'number' ? { idleSeconds: rawState.idleSeconds } : {}),
333
559
  });
334
560
  }
335
561
  }
@@ -344,23 +570,24 @@ export class SessionIdleMonitor {
344
570
  lastEndedAt: state.lastEndedAt,
345
571
  emitted: state.emitted,
346
572
  ...(typeof state.encrypted === 'boolean' ? { encrypted: state.encrypted } : {}),
573
+ ...(state.armedAt !== undefined ? { armedAt: state.armedAt } : {}),
574
+ ...(state.idleSeconds !== undefined ? { idleSeconds: state.idleSeconds } : {}),
347
575
  };
348
576
  }
349
577
  try {
350
578
  fs.mkdirSync(path.dirname(this.options.statePath), { recursive: true });
351
579
  atomicWriteJson(this.options.statePath, { version: 2, sessions });
580
+ return true;
352
581
  }
353
582
  catch (error) {
354
583
  this.warn(`Failed to persist session idle state at ${this.options.statePath}`, error);
584
+ return false;
355
585
  }
356
586
  }
357
587
  warn(message, error) {
358
588
  logger.warn(`[SessionIdleMonitor] ${message}${error === undefined ? '' : `: ${error instanceof Error ? error.message : String(error)}`}`);
359
589
  }
360
590
  }
361
- function isSupportedSession(session) {
362
- return session.chatType === 'group' || session.chatType === 'private';
363
- }
364
591
  function toIdleMs(seconds) {
365
592
  if (seconds === undefined || !Number.isFinite(seconds) || seconds <= 0)
366
593
  return undefined;
@@ -1,10 +1,11 @@
1
+ import { normalizeSessionLoopConfig, resolveSessionLoopTarget, sessionLoopError } from './session-idle-monitor.js';
1
2
  import { ensureDir } from '../../utils/atomic-write.js';
2
3
  import { resolvePaths } from '../../paths.js';
3
4
  import { logger } from '../../utils/logger.js';
4
5
  import { encodePath } from '../../utils/cross-platform.js';
5
6
  import { readRunnerEventSummary } from './runner-event-summary.js';
6
7
  import { isInternalInjectedUserMessage, isToolResultMessage } from './session-transcript.js';
7
- import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
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';
8
9
  import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, normalizeTopicLabel, normalizeTopicStatus, } from './session-mapper.js';
9
10
  import { tryParseChannelKey } from '../channel-loader.js';
10
11
  import { isSystemControlChannel } from '../system-channels.js';
@@ -225,7 +226,7 @@ export function pageForkHistory(snapshot, before = snapshot.items.length, limit
225
226
  };
226
227
  }
227
228
  /** Metadata keys owned by ordinary topic/session routing writers. */
228
- const TOPIC_METADATA_PROTECTED_KEYS = new Set(['turnState', 'resumeAt', 'agentSessions']);
229
+ const TOPIC_METADATA_PROTECTED_KEYS = new Set(['turnState', 'resumeAt', 'agentSessions', 'sessionLoop']);
229
230
  const TOPIC_BACKEND_SCOPED_KEYS = ['resumeAt', 'agentSessions'];
230
231
  function topicMetadataOwnershipError(key) {
231
232
  const error = new Error(`topic metadata field is owned by a dedicated updater: ${key}`);
@@ -260,6 +261,9 @@ export class SessionManager {
260
261
  identityResolver;
261
262
  fileAdapters = new Map();
262
263
  sessionEncryptState = new Map();
264
+ /** findSessionFileById 的查找缓存:命中前一律 stat 校验,不作为真相源。 */
265
+ sessionFileIndex = new Map();
266
+ knownChatDirs = null;
263
267
  constructor(sessionsDir, eventBus, identityResolver) {
264
268
  ensureDir(sessionsDir);
265
269
  this.sessionsDir = sessionsDir;
@@ -626,6 +630,13 @@ export class SessionManager {
626
630
  appendJsonlDurable(metaPath, file);
627
631
  else
628
632
  appendJsonl(metaPath, file);
633
+ // 本次快照已落盘;压缩只回收历史行,失败不影响写入结果,原文件保持不变。
634
+ try {
635
+ compactJsonlTail(metaPath, SESSION_META_COMPACT_POLICY);
636
+ }
637
+ catch (error) {
638
+ logger.warn(`[SessionManager] meta compaction skipped for ${metaPath}: ${error instanceof Error ? error.message : String(error)}`);
639
+ }
629
640
  }
630
641
  /**
631
642
  * Session 持久化的唯一事务原语。所有 session 写操作都应经此,业务层不直接调
@@ -646,6 +657,15 @@ export class SessionManager {
646
657
  * @returns 是否真的写入了 .jsonl(去重命中时返回 false)
647
658
  */
648
659
  persistSession(session, intent, opts) {
660
+ if (!opts?.sessionLoopMutation) {
661
+ const latest = this.readMetaLatest(this.metaPathForSession(session));
662
+ const metadata = { ...(session.metadata ?? {}) };
663
+ if (latest?.metadata?.sessionLoop !== undefined)
664
+ metadata.sessionLoop = latest.metadata.sessionLoop;
665
+ else
666
+ delete metadata.sessionLoop;
667
+ session.metadata = metadata;
668
+ }
649
669
  // Topic session snapshots can be written by synchronous legacy paths
650
670
  // (mark/clear processing) after an async /renew has committed. Preserve
651
671
  // the newest backend fence unless this call is one of the two explicit
@@ -807,24 +827,84 @@ export class SessionManager {
807
827
  }
808
828
  return results;
809
829
  }
830
+ /**
831
+ * 按 sessionId 定位 meta 文件。逐个 id 全量 scanChatDirs 在几百个 chat 目录下
832
+ * 单次约 50ms,启动时批量查询(SessionIdleMonitor 恢复 170+ 会话)会同步阻塞数秒,
833
+ * 所以首次查询时按 readdir 建一份 id→路径索引,之后:
834
+ * - 命中:stat 校验仍在才返回(文件被移进 _trash/目录被迁移时自然失效);
835
+ * - 未命中:先在已知目录里按原规则 stat 探测(覆盖索引之后新建的会话),
836
+ * 再重扫目录列表、只探测新出现的目录。不存在的 id 开销与原实现相当。
837
+ * 目录顺序与「同目录先主文件后 _threads」的优先级保持原实现语义。
838
+ */
810
839
  findSessionFileById(sessionId) {
811
- const chatDirs = scanChatDirs(this.sessionsDir);
812
- for (const { dirPath } of chatDirs) {
840
+ const cached = this.sessionFileIndex.get(sessionId);
841
+ if (cached) {
842
+ try {
843
+ fs.statSync(cached.metaPath);
844
+ return cached;
845
+ }
846
+ catch {
847
+ this.sessionFileIndex.delete(sessionId);
848
+ }
849
+ }
850
+ if (!this.knownChatDirs) {
851
+ this.rebuildSessionFileIndex();
852
+ const indexed = this.sessionFileIndex.get(sessionId);
853
+ if (indexed)
854
+ return indexed;
855
+ }
856
+ const probed = this.probeSessionFile(sessionId, this.knownChatDirs);
857
+ if (probed)
858
+ return probed;
859
+ const previous = new Set(this.knownChatDirs);
860
+ this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
861
+ return this.probeSessionFile(sessionId, this.knownChatDirs.filter(dir => !previous.has(dir)));
862
+ }
863
+ probeSessionFile(sessionId, chatDirs) {
864
+ for (const dirPath of chatDirs) {
813
865
  const mainPath = path.join(dirPath, `${sessionId}.jsonl`);
814
866
  try {
815
867
  fs.statSync(mainPath);
816
- return { chatDir: dirPath, metaPath: mainPath, isThread: false };
868
+ return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: mainPath, isThread: false });
817
869
  }
818
870
  catch { }
819
871
  const threadPath = path.join(dirPath, '_threads', `${sessionId}.jsonl`);
820
872
  try {
821
873
  fs.statSync(threadPath);
822
- return { chatDir: dirPath, metaPath: threadPath, isThread: true };
874
+ return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: threadPath, isThread: true });
823
875
  }
824
876
  catch { }
825
877
  }
826
878
  return undefined;
827
879
  }
880
+ rememberSessionFile(sessionId, found) {
881
+ this.sessionFileIndex.set(sessionId, found);
882
+ return found;
883
+ }
884
+ rebuildSessionFileIndex() {
885
+ this.sessionFileIndex.clear();
886
+ this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
887
+ for (const dirPath of this.knownChatDirs) {
888
+ for (const [dir, isThread] of [[dirPath, false], [path.join(dirPath, '_threads'), true]]) {
889
+ let names;
890
+ try {
891
+ names = fs.readdirSync(dir);
892
+ }
893
+ catch {
894
+ continue;
895
+ }
896
+ for (const name of names) {
897
+ if (!name.endsWith('.jsonl'))
898
+ continue;
899
+ const id = name.slice(0, -'.jsonl'.length);
900
+ // 先到先得:与逐目录探测时的优先级一致。
901
+ if (!this.sessionFileIndex.has(id)) {
902
+ this.sessionFileIndex.set(id, { chatDir: dirPath, metaPath: path.join(dir, name), isThread });
903
+ }
904
+ }
905
+ }
906
+ }
907
+ }
828
908
  // ─── Public API ───
829
909
  getKnownThreadIds(channel) {
830
910
  const chatDirs = scanChatDirs(this.sessionsDir);
@@ -1308,6 +1388,8 @@ export class SessionManager {
1308
1388
  if (updates.name !== undefined)
1309
1389
  current.name = updates.name;
1310
1390
  if (updates.metadata !== undefined) {
1391
+ if (Object.hasOwn(updates.metadata, 'sessionLoop'))
1392
+ throw topicMetadataOwnershipError('sessionLoop');
1311
1393
  if (current.threadId) {
1312
1394
  for (const key of Object.keys(updates.metadata)) {
1313
1395
  if (TOPIC_METADATA_PROTECTED_KEYS.has(key))
@@ -1337,6 +1419,8 @@ export class SessionManager {
1337
1419
  return;
1338
1420
  const { current } = loaded;
1339
1421
  for (const key of Object.keys(patch)) {
1422
+ if (key === 'sessionLoop')
1423
+ throw topicMetadataOwnershipError(key);
1340
1424
  if (current.threadId && TOPIC_METADATA_PROTECTED_KEYS.has(key)) {
1341
1425
  throw topicMetadataOwnershipError(key);
1342
1426
  }
@@ -1351,6 +1435,21 @@ export class SessionManager {
1351
1435
  current.metadata = metadata;
1352
1436
  this.persistSession(current, 'sync');
1353
1437
  }
1438
+ updateSessionLoop(sessionId, config) {
1439
+ const normalized = normalizeSessionLoopConfig(config);
1440
+ if (!normalized)
1441
+ throw sessionLoopError('INVALID_ARGUMENT', 'invalid sessionLoop configuration');
1442
+ return this.mutateLatestTopicSession(sessionId, current => {
1443
+ const target = resolveSessionLoopTarget(current);
1444
+ if (!target || target.level !== 'topic-session')
1445
+ throw sessionLoopError('INVALID_CONTEXT', '话题会话不存在或已不可用');
1446
+ current.metadata = { ...(current.metadata ?? {}), sessionLoop: normalized };
1447
+ const changed = this.persistSession(current, 'none', { sessionLoopMutation: true });
1448
+ if (changed)
1449
+ this.eventBus.publish({ type: 'session:loop-changed', sessionId, timestamp: Date.now() });
1450
+ return current;
1451
+ });
1452
+ }
1354
1453
  /** Persist only the turn coordinator-owned metadata field. */
1355
1454
  async updateTurnState(sessionId, turnState) {
1356
1455
  const loaded = this.loadSessionForUpdate(sessionId);
@@ -1366,6 +1465,8 @@ export class SessionManager {
1366
1465
  this.persistSession(current, 'sync', { turnStateMutation: true });
1367
1466
  }
1368
1467
  getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus) {
1468
+ const topicMetadata = { ...(metadata ?? {}) };
1469
+ delete topicMetadata.sessionLoop;
1369
1470
  // 使用精确路径(channelType + selfAID)
1370
1471
  const chatDir = (channelType && selfAID)
1371
1472
  ? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
@@ -1387,7 +1488,7 @@ export class SessionManager {
1387
1488
  let mutated = false;
1388
1489
  if (metadata) {
1389
1490
  const creatorPeerId = existing.metadata?.peerId;
1390
- existing.metadata = { ...(existing.metadata || {}), ...metadata };
1491
+ existing.metadata = { ...(existing.metadata || {}), ...topicMetadata };
1391
1492
  if (creatorPeerId && existing.metadata)
1392
1493
  existing.metadata.peerId = creatorPeerId;
1393
1494
  mutated = true;
@@ -1411,7 +1512,21 @@ export class SessionManager {
1411
1512
  const mainActive = readJsonFile(path.join(chatDir, 'active.json'));
1412
1513
  // 兼容旧字段名
1413
1514
  const mainBaseagent = mainActive?.baseagent || mainActive?.agentType;
1414
- const inheritedBaseagent = mainBaseagent || baseagent;
1515
+ // 最终兜底:从 agent config.json 读 active_baseagent(宿主终端下 active.json 可能不存在)
1516
+ const configBaseagent = (() => {
1517
+ if (mainBaseagent || baseagent)
1518
+ return undefined;
1519
+ if (!selfAID)
1520
+ return undefined;
1521
+ try {
1522
+ const cfg = readJsonFile(path.join(resolvePaths().agentsDir, selfAID, 'config.json'));
1523
+ return cfg?.active_baseagent || undefined;
1524
+ }
1525
+ catch {
1526
+ return undefined;
1527
+ }
1528
+ })();
1529
+ const inheritedBaseagent = mainBaseagent || baseagent || configBaseagent;
1415
1530
  if (!inheritedBaseagent) {
1416
1531
  throw new Error('[SessionManager] getOrCreateThreadSession: baseagent is empty');
1417
1532
  }
@@ -1426,7 +1541,7 @@ export class SessionManager {
1426
1541
  baseagent: inheritedBaseagent,
1427
1542
  sessionKey,
1428
1543
  chatType: effectiveChatType,
1429
- metadata,
1544
+ metadata: topicMetadata,
1430
1545
  name: name || '话题会话',
1431
1546
  label: normalizeTopicLabel(label),
1432
1547
  topicStatus: topicStatus ? normalizeTopicStatus(topicStatus) : 'pending',
@@ -1671,10 +1786,10 @@ export class SessionManager {
1671
1786
  return !!session && !session.threadId;
1672
1787
  });
1673
1788
  }
1674
- async getThreadSession(channel, channelId, threadId) {
1789
+ async getThreadSession(channel, channelId, threadId, selfAID) {
1675
1790
  let chatDir;
1676
1791
  try {
1677
- chatDir = this.resolveChatDirSafe(channel, channelId);
1792
+ chatDir = this.resolveChatDirSafe(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
1678
1793
  }
1679
1794
  catch {
1680
1795
  return undefined;
@@ -1687,9 +1802,6 @@ export class SessionManager {
1687
1802
  const session = this.readMetaLatest(metaPath);
1688
1803
  if (!session)
1689
1804
  return undefined;
1690
- // Do not consult provider-file existence to derive topic binding state;
1691
- // the topic JSONL boundary is authoritative and may intentionally point
1692
- // at a backend that needs an explicit /renew or repair operation.
1693
1805
  const validSessionId = normalizeBackendSessionId(session.agentSessionId);
1694
1806
  return { ...session, agentSessionId: validSessionId };
1695
1807
  }
@@ -1761,6 +1873,8 @@ export class SessionManager {
1761
1873
  return target;
1762
1874
  }
1763
1875
  updateMetadata(sessionId, metadata) {
1876
+ if (Object.hasOwn(metadata, 'sessionLoop'))
1877
+ throw topicMetadataOwnershipError('sessionLoop');
1764
1878
  const loaded = this.loadSessionForUpdate(sessionId);
1765
1879
  if (!loaded)
1766
1880
  return;
@@ -2123,6 +2237,7 @@ export class SessionManager {
2123
2237
  delete metadata.agentSessions;
2124
2238
  delete metadata.resumeAt;
2125
2239
  delete metadata.replyContext;
2240
+ delete metadata.sessionLoop;
2126
2241
  metadata.__sessionRenewedAt = Date.now();
2127
2242
  const session = {
2128
2243
  id: generateSessionId(),
@@ -0,0 +1,13 @@
1
+ import { isValidAid } from '../../aun/aid/validation.js';
2
+ export function resolveSkillRefreshArgv(argv) {
3
+ if (argv[0] !== 'skill' || argv[1] !== 'refresh' || argv.length < 3 || argv.length > 4
4
+ || argv.some(value => typeof value !== 'string'))
5
+ throw new Error('Invalid skill refresh argv');
6
+ const positional = argv.slice(2).filter(value => value !== '--json');
7
+ if (positional.length !== 1)
8
+ throw new Error('Specify one Agent AID');
9
+ const aid = positional[0].replace(/^@/, '');
10
+ if (!isValidAid(aid))
11
+ throw new Error('Invalid Agent AID');
12
+ return aid;
13
+ }