@konglx/rotom 2.21.0

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 (189) hide show
  1. package/README.md +417 -0
  2. package/bin/mesh-master.sh +439 -0
  3. package/bin/rotom +29 -0
  4. package/bin/rotom-link.sh +136 -0
  5. package/bin/rotom-send-with-status +57 -0
  6. package/bin/rotom-up.sh +428 -0
  7. package/dist/cli/ask.js +62 -0
  8. package/dist/cli/common.js +321 -0
  9. package/dist/cli/config.js +65 -0
  10. package/dist/cli/directory.js +17 -0
  11. package/dist/cli/executor.js +58 -0
  12. package/dist/cli/fed.js +91 -0
  13. package/dist/cli/group.js +273 -0
  14. package/dist/cli/identity.js +62 -0
  15. package/dist/cli/init.js +268 -0
  16. package/dist/cli/issue.js +202 -0
  17. package/dist/cli/join.js +170 -0
  18. package/dist/cli/link.js +47 -0
  19. package/dist/cli/master.js +51 -0
  20. package/dist/cli/memory.js +307 -0
  21. package/dist/cli/note.js +68 -0
  22. package/dist/cli/repo.js +77 -0
  23. package/dist/cli/rotom.js +277 -0
  24. package/dist/cli/routes.js +118 -0
  25. package/dist/cli/run.js +45 -0
  26. package/dist/cli/schedule.js +237 -0
  27. package/dist/cli/skill.js +173 -0
  28. package/dist/cli/team.js +106 -0
  29. package/dist/executor/claude-code-hook.cjs +80 -0
  30. package/dist/executor/cli-executor.js +8 -0
  31. package/dist/executor/executors/claude-code.js +780 -0
  32. package/dist/executor/executors/codex.js +719 -0
  33. package/dist/executor/executors/hermes-cli.js +855 -0
  34. package/dist/executor/executors/openclaw.js +467 -0
  35. package/dist/executor/executors/pi.js +514 -0
  36. package/dist/executor/index.js +269 -0
  37. package/dist/executor/jsonrpc-transport.js +125 -0
  38. package/dist/executor/process-runner.js +101 -0
  39. package/dist/executor/reasoning-status.js +83 -0
  40. package/dist/executor/repo-cache.js +502 -0
  41. package/dist/executor/session-store.js +188 -0
  42. package/dist/executor/worker-chat.js +257 -0
  43. package/dist/executor/worker-connection.js +89 -0
  44. package/dist/executor/worker-issue.js +264 -0
  45. package/dist/executor/worker.js +877 -0
  46. package/dist/link/pending-requests.js +72 -0
  47. package/dist/link/server.js +233 -0
  48. package/dist/link/visibility-store.js +58 -0
  49. package/dist/master/api/agents.js +333 -0
  50. package/dist/master/api/artifacts.js +271 -0
  51. package/dist/master/api/domains.js +64 -0
  52. package/dist/master/api/groups.js +635 -0
  53. package/dist/master/api/guidance-templates.js +147 -0
  54. package/dist/master/api/index.js +89 -0
  55. package/dist/master/api/issues-patrol.js +172 -0
  56. package/dist/master/api/issues.js +663 -0
  57. package/dist/master/api/links-patrol.js +168 -0
  58. package/dist/master/api/links.js +114 -0
  59. package/dist/master/api/memory.js +259 -0
  60. package/dist/master/api/messages.js +157 -0
  61. package/dist/master/api/notes.js +77 -0
  62. package/dist/master/api/schedule-patterns.js +133 -0
  63. package/dist/master/api/schedules.js +272 -0
  64. package/dist/master/api/sessions.js +158 -0
  65. package/dist/master/api/share.js +269 -0
  66. package/dist/master/api/skills.js +190 -0
  67. package/dist/master/api/teams.js +122 -0
  68. package/dist/master/api/uploads.js +245 -0
  69. package/dist/master/auth.js +134 -0
  70. package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
  71. package/dist/master/dashboard/animations/calico-error.apng +0 -0
  72. package/dist/master/dashboard/animations/calico-happy.apng +0 -0
  73. package/dist/master/dashboard/animations/calico-notification.apng +0 -0
  74. package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
  75. package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
  76. package/dist/master/dashboard/animations/calico-waking.apng +0 -0
  77. package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
  78. package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
  79. package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
  80. package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
  81. package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
  82. package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
  83. package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
  84. package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
  85. package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
  86. package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
  87. package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
  88. package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
  89. package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
  90. package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
  91. package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
  92. package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
  93. package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
  94. package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
  95. package/dist/master/dashboard/debug-auth.html +197 -0
  96. package/dist/master/dashboard/favicon.ico +0 -0
  97. package/dist/master/dashboard/index.html +20 -0
  98. package/dist/master/dashboard/rotom-avatar.png +0 -0
  99. package/dist/master/db/agent-sessions.js +60 -0
  100. package/dist/master/db/agent-visibility.js +64 -0
  101. package/dist/master/db/agents.js +119 -0
  102. package/dist/master/db/ask-bridges.js +157 -0
  103. package/dist/master/db/build-update.js +59 -0
  104. package/dist/master/db/core.js +82 -0
  105. package/dist/master/db/domains.js +80 -0
  106. package/dist/master/db/groups.js +316 -0
  107. package/dist/master/db/guidance-templates.js +58 -0
  108. package/dist/master/db/index.js +12 -0
  109. package/dist/master/db/internal.js +45 -0
  110. package/dist/master/db/issues-patrol.js +81 -0
  111. package/dist/master/db/issues.js +373 -0
  112. package/dist/master/db/links.js +221 -0
  113. package/dist/master/db/master-node.js +43 -0
  114. package/dist/master/db/memory.js +272 -0
  115. package/dist/master/db/messages.js +210 -0
  116. package/dist/master/db/notes.js +55 -0
  117. package/dist/master/db/schedule-patterns.js +56 -0
  118. package/dist/master/db/schedules.js +135 -0
  119. package/dist/master/db/skills.js +144 -0
  120. package/dist/master/db/team.js +88 -0
  121. package/dist/master/db/types.js +10 -0
  122. package/dist/master/db.js +12 -0
  123. package/dist/master/embedded.js +133 -0
  124. package/dist/master/federation/client.js +283 -0
  125. package/dist/master/federation/identity.js +133 -0
  126. package/dist/master/federation/manager.js +267 -0
  127. package/dist/master/federation/publisher.js +87 -0
  128. package/dist/master/federation/self-publisher.js +69 -0
  129. package/dist/master/federation/server.js +487 -0
  130. package/dist/master/group-paths.js +208 -0
  131. package/dist/master/offline-queue.js +38 -0
  132. package/dist/master/opc-bootstrap.js +245 -0
  133. package/dist/master/patrol-terminal.js +275 -0
  134. package/dist/master/repo-scan.js +188 -0
  135. package/dist/master/router.js +214 -0
  136. package/dist/master/scheduler-handlers.js +510 -0
  137. package/dist/master/scheduler.js +201 -0
  138. package/dist/master/server.js +203 -0
  139. package/dist/master/services/link-collector.js +82 -0
  140. package/dist/master/services/link-patrol-bootstrap.js +50 -0
  141. package/dist/master/services/memory-extract-prompt.js +34 -0
  142. package/dist/master/services/patrol-bootstrap.js +63 -0
  143. package/dist/master/share-tokens.js +56 -0
  144. package/dist/master/terminal-hub.js +300 -0
  145. package/dist/master/uploads.js +108 -0
  146. package/dist/master/util/fs.js +100 -0
  147. package/dist/master/util/paths.js +50 -0
  148. package/dist/master/util/persona.js +10 -0
  149. package/dist/master/ws-hub/connection.js +928 -0
  150. package/dist/master/ws-hub/conversation.js +290 -0
  151. package/dist/master/ws-hub/directory.js +70 -0
  152. package/dist/master/ws-hub/dispatch-enrich.js +34 -0
  153. package/dist/master/ws-hub/hub.js +136 -0
  154. package/dist/master/ws-hub/index.js +9 -0
  155. package/dist/master/ws-hub/internal.js +35 -0
  156. package/dist/master/ws-hub/routing.js +295 -0
  157. package/dist/master/ws-hub/sessions.js +130 -0
  158. package/dist/master/ws-hub.js +11 -0
  159. package/dist/shared/agent-profile.js +44 -0
  160. package/dist/shared/constants.js +55 -0
  161. package/dist/shared/dedup.js +33 -0
  162. package/dist/shared/group-context.js +62 -0
  163. package/dist/shared/json-codec.js +33 -0
  164. package/dist/shared/logger.js +136 -0
  165. package/dist/shared/mention.js +22 -0
  166. package/dist/shared/network.js +40 -0
  167. package/dist/shared/parse.js +18 -0
  168. package/dist/shared/prompt-composer.js +171 -0
  169. package/dist/shared/protocol/client-messages.js +8 -0
  170. package/dist/shared/protocol/enums.js +6 -0
  171. package/dist/shared/protocol/federation.js +62 -0
  172. package/dist/shared/protocol/guards.js +87 -0
  173. package/dist/shared/protocol/server-messages.js +8 -0
  174. package/dist/shared/protocol/types.js +8 -0
  175. package/dist/shared/protocol.js +19 -0
  176. package/dist/shared/readonly-allowlist.js +122 -0
  177. package/dist/shared/rotom-cli-prompt.js +23 -0
  178. package/dist/shared/skill-context.js +19 -0
  179. package/dist/shared/skill-md.js +43 -0
  180. package/dist/shared/slash-commands.js +50 -0
  181. package/dist/shared/time.js +80 -0
  182. package/dist/shared/title.js +46 -0
  183. package/dist/shared/url-extractor.js +99 -0
  184. package/migrations/001-schema.sql +942 -0
  185. package/package.json +68 -0
  186. package/scripts/fix-node-pty-perms.mjs +46 -0
  187. package/skill/rotom-a2a-communicate/SKILL.md +257 -0
  188. package/skill/rotom-bus-host/SKILL.md +78 -0
  189. package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Conversation enrichment + HTTP-side send + ask-bridge helpers.
3
+ *
4
+ * `enrichGroupConversation` attaches workingDir / activeIssues / memoryCounts /
5
+ * skillCount / guidancePrompt to outgoing conversation payloads so agents and
6
+ * dashboards see what the group is doing. Called from the connection handler's
7
+ * a2a_send / a2a_reply / a2a_reply_end paths.
8
+ *
9
+ * `sendAsAgent` is the HTTP-side mirror of `a2a_send` — used by REST handlers
10
+ * (CLI, dashboard API) to send messages on behalf of an agent.
11
+ *
12
+ * `autoCreateBridgeOnMention` / `checkAndCancelBridgesForMessage` drive the
13
+ * implicit ask-bridge timer when agents @ each other with #reply.
14
+ *
15
+ * Methods attach via Object.assign.
16
+ */
17
+ import { randomUUID } from "node:crypto";
18
+ import { safeJsonParse } from "../../shared/parse.js";
19
+ import { extractMentions } from "../../shared/mention.js";
20
+ import { enrichWorkerDispatch } from "./dispatch-enrich.js";
21
+ import { TIMER_PERSONA_NAME } from "../util/persona.js";
22
+ import { collectLinksFromText } from "../services/link-collector.js";
23
+ export const conversationMethods = {
24
+ /**
25
+ * Attach workingDir / activeIssues / memoryCounts / skillCount /
26
+ * guidancePrompt to a conversation payload.
27
+ *
28
+ * `targetAgentName` 让 workingDir 走成员级 override 优先:dashboard 在
29
+ * MemberListModal 里设的 per-(group, agent) working_dir 写到了
30
+ * group_member_settings,必须在这里查出来才能透传给前端展示。
31
+ *
32
+ * 注意:`workingDir` 仅用于 dashboard 展示/解析,**不会**被 executor 当作
33
+ * spawn cwd —— 后者必须走本机 `resolveIssueCwd` 派生(见 worker.ts
34
+ * "跨机器部署安全"注释),否则多机部署下会把 master 本地路径推给别的机器。
35
+ */
36
+ enrichGroupConversation(conversation, targetAgentName) {
37
+ if (!conversation || !conversation.groupId)
38
+ return conversation;
39
+ // workingDir 优先级:成员级 override > 群级默认。仅作为元数据透传给前端,
40
+ // executor 不会消费(避免跨机器把 master 本地路径推到非 master executor)。
41
+ const memberOverride = targetAgentName
42
+ ? this.db.getGroupMemberSetting(conversation.groupId, targetAgentName)
43
+ : null;
44
+ const group = this.db.getGroupById(conversation.groupId);
45
+ const workingDir = memberOverride || group?.working_dir || undefined;
46
+ if (conversation.type !== "group") {
47
+ // DM / single chat — only workingDir enrichment is meaningful.
48
+ return workingDir ? { ...conversation, workingDir } : conversation;
49
+ }
50
+ // Active task issues for the group — used by agents to decide whether
51
+ // file writes are permitted. Cap at 8 to keep prompt small.
52
+ const openIssues = this.db
53
+ .listIssuesByGroup(conversation.groupId, "open", "task")
54
+ .slice(0, 8);
55
+ const inProgressIssues = this.db
56
+ .listIssuesByGroup(conversation.groupId, "in_progress", "task")
57
+ .slice(0, 8);
58
+ const activeIssues = [...inProgressIssues, ...openIssues].slice(0, 8).map((it) => ({
59
+ id: it.id,
60
+ title: it.title,
61
+ status: it.status,
62
+ assignedTo: it.assigned_to || undefined,
63
+ priority: it.priority || undefined,
64
+ }));
65
+ // 记忆计数(极简指针注入用,只算 agent_visible=1 且 active 且非 pending)
66
+ const memoryCounts = {
67
+ group: this.db.countMemory("group", conversation.groupId),
68
+ global: this.db.countMemory("global"),
69
+ };
70
+ // skill 计数(该 agent 在该群绑定的 active skill 数,per-agent)
71
+ const skillCount = targetAgentName
72
+ ? this.db.countSkillsForAgent(conversation.groupId, targetAgentName)
73
+ : 0;
74
+ return { ...conversation, activeIssues, workingDir, memoryCounts, skillCount, guidancePrompt: group?.guidance_prompt || undefined };
75
+ },
76
+ /**
77
+ * 隐式 bridge 创建:群消息落库后调。若 sender @ 了某 agent B,自动建 bridge
78
+ * (asker=sender, target=B) + 20s interval scheduled_task。A 不用调 rotom ask,
79
+ * 直接 @ B 即可,系统透明管 timer。
80
+ *
81
+ * 跳过条件:
82
+ * - B == sender(自 @)
83
+ * - B 是真人(category=真人,真人不参与 bridge)
84
+ * - sender 是某 pending bridge 的 target(说明 sender 在回复别人问题,不是在提问)
85
+ * - (sender, B) 已有 pending bridge(防重)
86
+ */
87
+ autoCreateBridgeOnMention(groupId, sender, mentions, msgId) {
88
+ if (mentions.length === 0 || sender === "system")
89
+ return;
90
+ // 只有消息含 #reply 标记才建 bridge——普通 @ 不需要回复,不建 timer
91
+ const content = this.db.getGroupMessageContent(msgId) || "";
92
+ if (!content.includes("#reply"))
93
+ return;
94
+ const senderAgent = this.db.getAgentByName(sender);
95
+ if (!senderAgent)
96
+ return;
97
+ for (const targetName of mentions) {
98
+ if (targetName === sender)
99
+ continue;
100
+ const targetAgent = this.db.getAgentByName(targetName);
101
+ if (!targetAgent)
102
+ continue;
103
+ // 跳过真人 target
104
+ const targetProfile = safeJsonParse(targetAgent.profile, {});
105
+ if (targetProfile.category === "真人")
106
+ continue;
107
+ // 跳过:sender 是某 pending bridge 的 target(在回复,不在提问)
108
+ const asTarget = this.db.findPendingBridge(groupId, targetName, sender);
109
+ if (asTarget)
110
+ continue;
111
+ // 跳过:已有 pending bridge (sender → targetName)
112
+ const existing = this.db.findPendingBridge(groupId, sender, targetName);
113
+ if (existing)
114
+ continue;
115
+ // 建 bridge
116
+ const bridgeId = randomUUID();
117
+ this.db.createAskBridge({
118
+ id: bridgeId,
119
+ groupId,
120
+ asker: sender,
121
+ target: targetName,
122
+ questionMsgId: msgId,
123
+ escalateTo: null,
124
+ timeoutMs: 5 * 60_000,
125
+ });
126
+ const task = this.db.createScheduledTask({
127
+ name: `${TIMER_PERSONA_NAME} · 等待 ${targetName} 回复`,
128
+ groupId,
129
+ mode: "message",
130
+ scheduleKind: "interval",
131
+ intervalSec: 20,
132
+ prompt: `${TIMER_PERSONA_NAME} 每 20s 检查一次 ${targetName} 有没有回复 ${sender} 的问题;有回复就复述给 ${sender},5 分钟没回复就升级 Issue。`,
133
+ handlerKey: "ask-bridge-check",
134
+ handlerPayload: JSON.stringify({ bridgeId, asker: sender, target: targetName }),
135
+ });
136
+ this.logger.info(`[mesh] bridge auto-created: ${bridgeId} (${sender}→${targetName}) msg=${msgId} (#reply), schedule task #${task.id}`);
137
+ }
138
+ },
139
+ /**
140
+ * 事件式 bridge 检测:群消息落库后调。若该消息"答中"了某 pending bridge
141
+ * (sender = bridge.target AND mentions 含 bridge.asker),mark answered + disable
142
+ * scheduled_task + **注入 system @ 消息给 asker**(带"汇报给原始提问者"上下文)。
143
+ *
144
+ * 不直接 WS 推 raw @ —— 那样 A 的 LLM 会回复给 B 而非汇报给原始提问者。
145
+ * 改用 postSystemToGroup 注入带上下文的 system 消息,A 的 LLM 知道该汇报给谁。
146
+ */
147
+ checkAndCancelBridgesForMessage(groupId, sender, mentions, msgId) {
148
+ if (mentions.length === 0)
149
+ return;
150
+ const bridges = this.db.findBridgesAnsweredByMessage(groupId, sender, mentions);
151
+ if (bridges.length === 0)
152
+ return;
153
+ // B @ A 回复:A 通过 a2a_message 广播已收到(session 复用,有上下文)。
154
+ // 这里只 cancel bridge + delete timer,不注入 system 消息——避免 A 收到两条消息(raw @ + system)顺序不确定。
155
+ // system 复述只在 handler 路径(非@回复,20s poll 检测)走,A 没被 @ 触发才需要 system 唤醒。
156
+ for (const bridge of bridges) {
157
+ this.db.markBridgeAnswered(bridge.id, msgId);
158
+ const task = this.db.findAskBridgeScheduledTask(bridge.id);
159
+ if (task && task.enabled) {
160
+ this.db.disableScheduledTask(task.id);
161
+ }
162
+ this.logger.info(`[mesh] bridge ${bridge.id} auto-answered: ${sender} @ ${bridge.asker} (msg ${msgId}), timer task #${task?.id ?? "?"} cancelled`);
163
+ }
164
+ },
165
+ /**
166
+ * Send a message on behalf of an agent, mirroring the WS `a2a_send` path
167
+ * for HTTP/CLI callers. If `groupId` is provided the message is treated as
168
+ * a group message: routed to `target` and recorded in group history.
169
+ */
170
+ sendAsAgent(opts) {
171
+ const fromAgent = this.db.getAgentByName(opts.fromName);
172
+ if (!fromAgent)
173
+ return { requestId: "", delivered: false, queued: false, error: `Sender agent "${opts.fromName}" not found` };
174
+ const targetAgent = this.db.getAgentByName(opts.target);
175
+ if (!targetAgent)
176
+ return { requestId: "", delivered: false, queued: false, error: `Target agent "${opts.target}" not found` };
177
+ const requestId = `cli_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
178
+ const conversation = opts.groupId
179
+ ? { type: "group", groupId: opts.groupId, groupName: opts.groupName }
180
+ : undefined;
181
+ // needReply: 自动补 @target 到正文开头(若没有),确保 worker 的 @-mention 检查命中。
182
+ let messageBody = opts.message;
183
+ const mentionTag = `@${opts.target}`;
184
+ if (opts.needReply && !messageBody.startsWith(mentionTag)) {
185
+ messageBody = `${mentionTag} ${messageBody}`;
186
+ }
187
+ const result = this.router.route(fromAgent.id, {
188
+ requestId,
189
+ target: opts.target,
190
+ payload: { message: messageBody },
191
+ ...(conversation ? { conversation } : {}),
192
+ ...(opts.needReply ? { qaMode: true } : {}),
193
+ });
194
+ if (result.error)
195
+ return { requestId, delivered: false, queued: false, error: result.error };
196
+ // needReply: 登记 requestId → asker,master 收到 reply 时据此硬剥 @<asker>
197
+ if (opts.needReply) {
198
+ this.qaModeAskers.set(requestId, opts.fromName);
199
+ }
200
+ let delivered = false;
201
+ let queued = false;
202
+ let messageId;
203
+ if (result.targetAgentId) {
204
+ const enrichedConversation = this.enrichGroupConversation(conversation);
205
+ const wireMsg = enrichWorkerDispatch(this, {
206
+ type: "a2a_message",
207
+ requestId,
208
+ from: { name: opts.fromName, domain: fromAgent.domain || undefined, status: "online" },
209
+ payload: { message: messageBody },
210
+ routeType: "exact",
211
+ conversation: enrichedConversation,
212
+ ...(opts.needReply ? { qaMode: true } : {}),
213
+ }, result.targetName, enrichedConversation?.groupId);
214
+ // noDispatch:不直接推给 target 的 WS,也不入 offline_queue。
215
+ // 消息仍会广播给其他群成员 + 入库,target 只能通过 group history 看到。
216
+ if (!opts.noDispatch) {
217
+ delivered = this.sendToAgent(result.targetAgentId, wireMsg);
218
+ }
219
+ if (opts.groupId) {
220
+ // 兜底:发信人不在 group_members 时自动 addMembers(防"自激丢消息" +
221
+ // "多 tab 真人看不到自己的消息")。INSERT OR IGNORE 幂等。
222
+ const groupMembers = this.db.getGroupMembers(opts.groupId);
223
+ if (!groupMembers.some((m) => m.agent_name === opts.fromName)) {
224
+ this.db.addGroupMembers(opts.groupId, [opts.fromName]);
225
+ this.logger.info(`[mesh] sendAsAgent group: auto-joined sender "${opts.fromName}" as group member`);
226
+ }
227
+ // 群消息:除打给 target 外广播给全群(对齐 a2a_reply L462-465)。
228
+ // 排除列表含 target 防重复推送。同时排除所有 @mentioned agent 防
229
+ // Dashboard 多次发送 a2a_send 导致的广播重复投递。
230
+ //
231
+ // qaMode 不能漏给非 target 群成员:target 已通过 sendToAgent 收到
232
+ // qaMode=true 副本以 bypass @-mention 检查,但 broadcast 给其他成员
233
+ // 时若还带 qaMode=true,他们也会 bypass,导致群里全员被唤醒回复
234
+ // (典型症状:--need-reply 后群里非 @ 对象也冒泡接话)。
235
+ // 剥一份广播专用副本。
236
+ //
237
+ // 单播群(unicast, type=a2a_direct)默认静默:不广播、也不投递给非
238
+ // target 成员。消息只入库,asker 通过 group history / new-messages
239
+ // 拉,reply 通过 a2a_reply 路径同样静默(target sendToAgent 仅对
240
+ // asker 那条连接,其他成员 worker 不会被消息自动唤醒)。
241
+ const group = this.db.getGroupById(opts.groupId);
242
+ const a2aDirect = group?.type === "a2a_direct";
243
+ const sendAsMentions = extractMentions(messageBody);
244
+ if (!a2aDirect) {
245
+ const sendAsMentionAgentIds = sendAsMentions
246
+ .map((name) => this.db.getAgentByName(name)?.id)
247
+ .filter((id) => !!id);
248
+ const broadcastWire = (opts.needReply
249
+ ? { ...wireMsg, qaMode: undefined }
250
+ : wireMsg);
251
+ this.broadcastToGroup(opts.groupId, broadcastWire, [fromAgent.id, result.targetAgentId, ...sendAsMentionAgentIds]);
252
+ }
253
+ else if (opts.needReply) {
254
+ this.logger.info(`[mesh] sendAsAgent a2a_direct group: qaMode target engaged (no broadcast)`);
255
+ }
256
+ else {
257
+ this.logger.info(`[mesh] sendAsAgent a2a_direct group: pure store, no broadcast, no target dispatch`);
258
+ }
259
+ const mentions = sendAsMentions;
260
+ messageId = this.db.addGroupMessage(opts.groupId, opts.fromName, messageBody, mentions);
261
+ this.autoCreateBridgeOnMention(opts.groupId, opts.fromName, mentions, messageId);
262
+ this.checkAndCancelBridgesForMessage(opts.groupId, opts.fromName, mentions, messageId);
263
+ // 链接采集(inline hook,失败不影响主路径)
264
+ collectLinksFromText(messageBody, {
265
+ sourceType: "group_message",
266
+ sourceId: String(messageId),
267
+ sourceGroupId: opts.groupId,
268
+ sourceSender: opts.fromName,
269
+ }, this.db);
270
+ }
271
+ if (!opts.noDispatch && !delivered) {
272
+ queued = this.offlineQueue.enqueue(result.targetAgentId, opts.fromName, fromAgent.domain || undefined, { message: opts.message }, "exact");
273
+ }
274
+ }
275
+ this.db.logMessage({
276
+ requestId,
277
+ fromName: opts.fromName,
278
+ fromDomain: fromAgent.domain || undefined,
279
+ toName: result.targetName,
280
+ toDomain: result.targetAgentId ? this.db.getAgentById(result.targetAgentId)?.domain ?? undefined : undefined,
281
+ routeType: "exact",
282
+ direction: "send",
283
+ payload: JSON.stringify({ message: opts.message }),
284
+ status: queued ? "queued" : delivered ? "routed" : "no_target",
285
+ groupId: opts.groupId,
286
+ source: "cli",
287
+ });
288
+ return { requestId, delivered, queued, messageId };
289
+ },
290
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Directory — agent roster broadcast + per-agent config push.
3
+ *
4
+ * The directory is a denormalized view of the agents table that every
5
+ * connected agent holds in memory. Whenever an agent joins, leaves, or
6
+ * updates its description/profile, master fans out a `directory_update`
7
+ * to all connected agents so their local copies stay fresh.
8
+ *
9
+ * Methods attach via Object.assign. `parseProfile` is shared with hub.ts.
10
+ */
11
+ import { parseProfile } from "./hub.js";
12
+ export const directoryMethods = {
13
+ getDirectory() {
14
+ return this.db.listAgents().map((a) => ({
15
+ name: a.name,
16
+ domain: a.domain || undefined,
17
+ description: a.description || undefined,
18
+ status: a.status,
19
+ enabled: a.enabled !== 0,
20
+ profile: parseProfile(a.profile),
21
+ }));
22
+ },
23
+ /** Push a config_update to a specific connected Agent (called by API layer). */
24
+ pushConfigUpdate(agentId, config) {
25
+ const conn = this.connections.get(agentId);
26
+ if (!conn || conn.ws.readyState !== WebSocket.OPEN)
27
+ return false;
28
+ // Update in-memory connection state
29
+ if (config.domain !== undefined)
30
+ conn.domain = config.domain;
31
+ this.send(conn.ws, { type: "config_update", ...config });
32
+ this.logger.info(`[mesh] Pushed config_update to ${conn.name}: ${JSON.stringify(config)}`);
33
+ return true;
34
+ },
35
+ /** Read agent from DB and broadcast directory_update to all connected agents. */
36
+ broadcastAgentUpdate(agentId) {
37
+ const agent = this.db.getAgentById(agentId);
38
+ if (!agent)
39
+ return;
40
+ this.broadcastDirectory("update", {
41
+ name: agent.name,
42
+ domain: agent.domain || undefined,
43
+ description: agent.description || undefined,
44
+ status: agent.status,
45
+ enabled: agent.enabled !== 0,
46
+ profile: parseProfile(agent.profile),
47
+ });
48
+ },
49
+ broadcastDirectory(event, agent) {
50
+ const msg = { type: "directory_update", event, agent };
51
+ for (const conn of this.connections.values()) {
52
+ this.send(conn.ws, msg);
53
+ }
54
+ },
55
+ /**
56
+ * 返回所有 online agent 的 cliTool 映射(name → cliTool)。
57
+ * 给 API /agents 列表用 —— DB 不存 cliTool(那是 worker 的运行时属性),
58
+ * 但 dashboard 想展示"这个 agent 用什么 CLI 跑"。
59
+ * Offline agent 不在 map 里(没有连接)。
60
+ */
61
+ onlineCliTools() {
62
+ const out = new Map();
63
+ for (const conn of this.connections.values()) {
64
+ if (conn.cliTool) {
65
+ out.set(conn.name, conn.cliTool);
66
+ }
67
+ }
68
+ return out;
69
+ },
70
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * dispatch-enrich —— master 在向 worker 发送 WS 消息前,把 `agentProfile` 与
3
+ * `cwd` 注入 msg 的统一入口。
4
+ *
5
+ * 注入字段:
6
+ * - `agentProfile`: 从 `agents.profile` JSON 解析;若 groupId 存在,再用
7
+ * `group_member_settings.profile` 覆盖(群级别字段优先 merge 到 agent 全局
8
+ * profile 上)。worker 收到后更新本地缓存,供 prompt-composer 渲染
9
+ * `[Agent 角色]` 层。Dashboard 编辑后下一条 dispatch 即时生效,无需重启 executor。
10
+ * - `cwd`: 由 `resolveGroupAgentWorkingDir` 派生,worker 收到后若本机存在该路径
11
+ * 则覆盖本地派生(跨机器部署时本机无该路径则静默回落)。
12
+ *
13
+ * 设计为纯函数,只依赖 `db`,ws-hub method bag 与 api 层都能直接调。
14
+ */
15
+ import { parseAgentProfile, mergeGroupProfile } from "../../shared/agent-profile.js";
16
+ import { resolveGroupAgentWorkingDir } from "../group-paths.js";
17
+ /** 调用方仅需提供 db(ws-hub method bag 传 this,api 层传 hub 实例)。
18
+ * agentName / groupId 任一缺失时跳过对应字段的注入(返回 undefined)。 */
19
+ export function enrichWorkerDispatch(self, msg, agentName, groupId) {
20
+ const baseProfile = agentName
21
+ ? (() => {
22
+ const agent = self.db.getAgentByName(agentName);
23
+ return agent?.profile ? parseAgentProfile(agent.profile) ?? undefined : undefined;
24
+ })()
25
+ : undefined;
26
+ const groupProfile = agentName && groupId
27
+ ? parseAgentProfile(self.db.getGroupMemberProfile(groupId, agentName))
28
+ : null;
29
+ const profile = mergeGroupProfile(baseProfile, groupProfile);
30
+ const cwd = agentName && groupId
31
+ ? resolveGroupAgentWorkingDir(self.db, groupId, agentName)
32
+ : undefined;
33
+ return { ...msg, agentProfile: profile, cwd };
34
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * WSHubCore — constructor, lifecycle, heartbeat / cleanup timers, shared types.
3
+ *
4
+ * The class shell here owns the persistent in-memory state (connection map,
5
+ * pending session requests, snapshot cache) and the wss instance. Domain
6
+ * modules (./connection.ts, ./routing.ts, ./conversation.ts, ...) attach
7
+ * their methods to the WSHub class via `Object.assign(this, ...)` in the
8
+ * composition root (./internal.ts). The `this` inside each method is typed
9
+ * as `WSHubSelf` — a structural shape that exposes the cross-module surface
10
+ * (db, auth, router, offlineQueue, logger, connections, send, etc.).
11
+ *
12
+ * handleConnection is a single 715-line method that owns its own closure
13
+ * state (authenticated flag, agentId, generation). It's deliberately kept
14
+ * intact in ./connection.ts for now — a follow-up PR can split it by
15
+ * msg.type once we have confidence the dispatch + helper boundaries hold.
16
+ */
17
+ import { WebSocketServer, WebSocket } from "ws";
18
+ import { HEARTBEAT_CHECK_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, CLEANUP_INTERVAL_MS, PENDING_REQUEST_TTL_MS, WS_MAX_PAYLOAD, } from "../../shared/constants.js";
19
+ export class WSHubCore {
20
+ db;
21
+ auth;
22
+ router;
23
+ offlineQueue;
24
+ logger;
25
+ wss;
26
+ connections = new Map(); // agentId → conn
27
+ sendTimestamps = new Map(); // requestId → send timestamp (for latency)
28
+ heartbeatTimer = null;
29
+ cleanupTimer = null;
30
+ generation = 0; // global generation counter — assigned on each successful auth
31
+ /**
32
+ * In-flight session management requests awaiting a worker response. Keyed
33
+ * by requestId; each entry holds a resolver + a setTimeout that fires
34
+ * routeToExecutor's rejection with a timeout error. Cleaned up in the
35
+ * session response handler (first response wins, except for broadcasts
36
+ * which collect all responses until timeout).
37
+ */
38
+ pendingSessionRequests = new Map();
39
+ /**
40
+ * In-memory cache of each worker's SessionStore. Workers push a
41
+ * `session_snapshot` after auth and after every mutation; we replace the
42
+ * entry on receipt. Powers the dashboard's `GET /sessions?groupId=X`
43
+ * without WS round-trips — fast list, no broadcast.
44
+ *
45
+ * Key is `ConnectedAgent.id` (workerAgentId). On disconnect we drop the
46
+ * entry so offline workers don't surface stale sessions; the next reconnect
47
+ * re-pushes a snapshot.
48
+ */
49
+ sessionSnapshots = new Map();
50
+ /**
51
+ * issueId → agentId Set。dashboard 客户端 subscribe_issue_detail 后加入。
52
+ * issue_usage_progress 只转发给订阅者(不广播、不落 DB)。disconnect 时
53
+ * 整个 agentId 条目从所有 Set 里清掉。
54
+ */
55
+ issueSubscriptions = new Map();
56
+ qaModeAskers = new Map();
57
+ constructor(httpServer, db, auth, router, offlineQueue, logger) {
58
+ this.db = db;
59
+ this.auth = auth;
60
+ this.router = router;
61
+ this.offlineQueue = offlineQueue;
62
+ this.logger = logger;
63
+ this.wss = new WebSocketServer({
64
+ server: httpServer,
65
+ path: "/ws",
66
+ maxPayload: WS_MAX_PAYLOAD,
67
+ });
68
+ }
69
+ start() {
70
+ const self = this;
71
+ this.wss.on("connection", (ws, req) => self.handleConnection(ws, req));
72
+ // Periodic heartbeat check
73
+ this.heartbeatTimer = setInterval(() => {
74
+ const now = Date.now();
75
+ for (const [agentId, conn] of this.connections) {
76
+ if (conn.ws.readyState !== WebSocket.OPEN || now - conn.lastHeartbeat > HEARTBEAT_TIMEOUT_MS) {
77
+ const reason = conn.ws.readyState !== WebSocket.OPEN ? "ws_closed" : "heartbeat_timeout";
78
+ self.handleDisconnect(agentId, conn.generation, reason);
79
+ }
80
+ }
81
+ }, HEARTBEAT_CHECK_INTERVAL_MS);
82
+ // Periodic cleanup of sendTimestamps (prevents memory leak)
83
+ this.cleanupTimer = setInterval(() => {
84
+ const now = Date.now();
85
+ for (const [id, ts] of this.sendTimestamps) {
86
+ if (now - ts > PENDING_REQUEST_TTL_MS) {
87
+ this.sendTimestamps.delete(id);
88
+ }
89
+ }
90
+ // Also clean up old logs periodically (every cycle)
91
+ try {
92
+ this.db.cleanupOldLogs();
93
+ }
94
+ catch { /* non-fatal */ }
95
+ }, CLEANUP_INTERVAL_MS);
96
+ this.logger.info("[mesh-master] WebSocket Hub started");
97
+ }
98
+ getOnlineCount() {
99
+ return this.connections.size;
100
+ }
101
+ stop() {
102
+ if (this.heartbeatTimer)
103
+ clearInterval(this.heartbeatTimer);
104
+ if (this.cleanupTimer)
105
+ clearInterval(this.cleanupTimer);
106
+ // Close all connections
107
+ for (const conn of this.connections.values()) {
108
+ if (conn.ws.readyState === WebSocket.OPEN) {
109
+ conn.ws.close(1001, "Server shutting down");
110
+ }
111
+ }
112
+ this.connections.clear();
113
+ this.wss.close();
114
+ }
115
+ }
116
+ // ────────────────────────────────────────────────────────────────────────────
117
+ // Helpers shared across modules
118
+ // ────────────────────────────────────────────────────────────────────────────
119
+ /** Parse a stored agent profile JSON string. Returns undefined on missing/malformed input. */
120
+ export function parseProfile(raw) {
121
+ if (!raw)
122
+ return undefined;
123
+ try {
124
+ return JSON.parse(raw);
125
+ }
126
+ catch {
127
+ return undefined;
128
+ }
129
+ }
130
+ /**
131
+ * 把 DB 里宽松的 string 收敛成协议枚举。脏数据/空值统一回落到 'r_allow',
132
+ * 避免 worker 端把未知值当成 bypass。
133
+ */
134
+ export function normalizeApprovalPolicy(raw) {
135
+ return raw === "rw_allow" ? "rw_allow" : "r_allow";
136
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * WS Hub public surface.
3
+ *
4
+ * The full implementation lives in `./internal.ts` while handler-by-handler
5
+ * extraction lands in follow-up PRs (planned: `connection.ts` for the
6
+ * message-dispatch if-chain, `routing.ts` for broadcasts/issue routing,
7
+ * `conversation.ts` for enrichment + ask-bridge).
8
+ */
9
+ export { WSHub } from "./internal.js";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * WSHub — composition root that wires each domain module's methods onto a
3
+ * single instance. Public API surface (method names) is unchanged from the
4
+ * pre-split monolithic version, so the 29 call sites across
5
+ * `src/master/*.ts` and `tests/*.ts` need no edits.
6
+ *
7
+ * Domain modules (./connection.ts, ./routing.ts, ./directory.ts,
8
+ * ./sessions.ts, ./conversation.ts) export method bags whose `this` is
9
+ * typed as `WSHubSelf` (declared in ./hub.ts) — a structural shape with the
10
+ * cross-module surface (db, auth, router, offlineQueue, logger, connections,
11
+ * send, broadcast, etc.) so methods in one bag can call methods in another.
12
+ *
13
+ * The `declare` lines below are purely type annotations: they tell the
14
+ * TypeScript compiler that the runtime instance (built via Object.assign in
15
+ * the constructor) has every domain method, without forcing us to duplicate
16
+ * signatures. Implementations live in the domain modules.
17
+ */
18
+ import { WSHubCore } from "./hub.js";
19
+ import { connectionMethods } from "./connection.js";
20
+ import { routingMethods } from "./routing.js";
21
+ import { directoryMethods } from "./directory.js";
22
+ import { sessionsMethods } from "./sessions.js";
23
+ import { conversationMethods } from "./conversation.js";
24
+ export class WSHub extends WSHubCore {
25
+ constructor(httpServer, db, auth, router, offlineQueue, logger) {
26
+ super(httpServer, db, auth, router, offlineQueue, logger);
27
+ // Each method bag's `this` resolves to this instance at call time.
28
+ // The `declare` lines above ensure TypeScript sees these as members.
29
+ Object.assign(this, connectionMethods);
30
+ Object.assign(this, routingMethods);
31
+ Object.assign(this, directoryMethods);
32
+ Object.assign(this, sessionsMethods);
33
+ Object.assign(this, conversationMethods);
34
+ }
35
+ }