@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,295 @@
1
+ /**
2
+ * Routing — send/broadcast primitives and issue-coordination pushes.
3
+ *
4
+ * Lower-level transport helpers (send / sendToAgent / broadcastToGroup) plus
5
+ * the issue-system notifications (assignment / approval / chat cancel /
6
+ * continue / append / new issue / change). All of these are pure routing —
7
+ * no message-handler logic lives here. Methods attach via Object.assign.
8
+ */
9
+ import { WebSocket } from "ws";
10
+ import { extractMentions } from "../../shared/mention.js";
11
+ import { normalizeApprovalPolicy } from "./hub.js";
12
+ import { enrichWorkerDispatch } from "./dispatch-enrich.js";
13
+ import { resolveIssueRepoCtxLocalOnly } from "../group-paths.js";
14
+ export const routingMethods = {
15
+ // ─────────────────────────────────────────────────────────────────────────
16
+ // Low-level transport
17
+ // ─────────────────────────────────────────────────────────────────────────
18
+ /** Send a message to a connected agent. Returns false if not connected. */
19
+ sendToAgent(agentId, msg) {
20
+ const conn = this.connections.get(agentId);
21
+ if (!conn || conn.ws.readyState !== WebSocket.OPEN)
22
+ return false;
23
+ this.send(conn.ws, msg);
24
+ return true;
25
+ },
26
+ /** Lowest-level transport: serialize + ws.send. Public since connection,
27
+ * directory, and conversation modules all need to push directly. */
28
+ send(ws, msg) {
29
+ if (ws.readyState === WebSocket.OPEN) {
30
+ ws.send(JSON.stringify(msg));
31
+ }
32
+ },
33
+ /**
34
+ * Broadcast a message to all group members EXCEPT those in excludeAgentIds.
35
+ * Used for group message visibility — messages/replies are broadcast so all
36
+ * group members see them in real-time.
37
+ */
38
+ broadcastToGroup(groupId, msg, excludeAgentIds = []) {
39
+ const members = this.db.getGroupMembers(groupId);
40
+ const delivered = [];
41
+ for (const member of members) {
42
+ const memberAgent = this.db.getAgentByName(member.agent_name);
43
+ if (!memberAgent)
44
+ continue;
45
+ if (excludeAgentIds.includes(memberAgent.id))
46
+ continue;
47
+ const ok = this.sendToAgent(memberAgent.id, msg);
48
+ delivered.push({ name: member.agent_name, sent: ok });
49
+ }
50
+ // 流式 chunk 广播会每 chunk 调一次,记日志只会刷屏,跳过;其他类型
51
+ // (a2a_send 等)的广播日志保留,方便排查投递问题。
52
+ const isStreamingChunk = msg.type === "a2a_stream_chunk";
53
+ if (!isStreamingChunk) {
54
+ this.logger.info(`[mesh] broadcastToGroup ${groupId}: ${delivered.length} members, results=${JSON.stringify(delivered)}`);
55
+ }
56
+ },
57
+ /**
58
+ * Public entry point for broadcasting a group message from outside the hub
59
+ * (e.g. REST handlers in api/groups.ts). Thin wrapper over the private
60
+ * broadcastToGroup — keeps the internal helper encapsulated.
61
+ */
62
+ broadcastToGroupPublic(groupId, msg, excludeAgentIds = []) {
63
+ this.broadcastToGroup(groupId, msg, excludeAgentIds);
64
+ },
65
+ /**
66
+ * 发一条 sender=system 的群消息:入库 + 实时广播给在线群成员。
67
+ * 用于协作流转类消息(启动 / 进入下一轮 / 结束),让群里所有人同步看到状态。
68
+ * - excludeAgentNames:不往这些成员的 WS 推,但消息仍然入库。用于避免 @ 的对象被双触发。
69
+ * - ensureRecipientNames:保证这些 agent 能收到(即便它不在群成员里)。
70
+ * 用于 mention 了非群成员(如协作 firstParticipant 不在群里)的场景。
71
+ */
72
+ postSystemToGroup(groupId, content, excludeAgentNames = [], ensureRecipientNames = []) {
73
+ this.logger.info(`[mesh] postSystemToGroup groupId=${groupId} exclude=${JSON.stringify(excludeAgentNames)} ensure=${JSON.stringify(ensureRecipientNames)}`);
74
+ const mentions = extractMentions(content);
75
+ this.db.addGroupMessage(groupId, "system", content, mentions);
76
+ const excludeAgentIds = excludeAgentNames
77
+ .map((name) => this.db.getAgentByName(name)?.id)
78
+ .filter((id) => !!id);
79
+ const wireMsg = {
80
+ type: "a2a_message",
81
+ requestId: `sys-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
82
+ from: { name: "system", status: "online" },
83
+ payload: { message: content },
84
+ routeType: "exact",
85
+ conversation: { type: "group", groupId },
86
+ };
87
+ // 1) 推给群成员
88
+ this.broadcastToGroup(groupId, wireMsg, excludeAgentIds);
89
+ // 2) 保证这些 recipient 收到(即便它不在群里);与 broadcast 去重
90
+ const memberNames = new Set(this.db.getGroupMembers(groupId).map((m) => m.agent_name));
91
+ for (const name of ensureRecipientNames) {
92
+ if (excludeAgentNames.includes(name))
93
+ continue;
94
+ if (memberNames.has(name))
95
+ continue; // already covered by broadcastToGroup
96
+ const agent = this.db.getAgentByName(name);
97
+ if (!agent) {
98
+ this.logger.warn(`[mesh] postSystemToGroup: ensure recipient "${name}" not registered`);
99
+ continue;
100
+ }
101
+ const ok = this.sendToAgent(agent.id, wireMsg);
102
+ this.logger.info(`[mesh] postSystemToGroup ensure delivery → ${name}: sent=${ok}`);
103
+ }
104
+ },
105
+ // ─────────────────────────────────────────────────────────────────────────
106
+ // Issue system (task coordination)
107
+ // ─────────────────────────────────────────────────────────────────────────
108
+ /** Push issue assignment notification to a specific executor agent. */
109
+ pushIssueAssignment(issueId, agentName) {
110
+ const issue = this.db.getIssueById(issueId);
111
+ if (!issue)
112
+ return false;
113
+ const agent = this.db.getAgentByName(agentName);
114
+ if (!agent)
115
+ return false;
116
+ const repo = resolveIssueRepoCtxLocalOnly(this.db, issue);
117
+ return this.sendToAgent(agent.id, enrichWorkerDispatch(this, {
118
+ type: "issue_assigned",
119
+ issueId: issue.id,
120
+ groupId: issue.group_id,
121
+ title: issue.title,
122
+ description: issue.description,
123
+ workingDir: issue.working_dir || undefined,
124
+ slashCommand: issue.slash_command || undefined,
125
+ approvalPolicy: normalizeApprovalPolicy(issue.approval_policy),
126
+ ...(repo ? { repoUrl: repo.repoUrl, repoBranch: repo.repoBranch, extraRepos: repo.extraRepos } : {}),
127
+ }, agentName, issue.group_id));
128
+ },
129
+ /**
130
+ * Push the user's approval decision to the worker that owns the parked
131
+ * codex JSON-RPC request. Returns false when the issue has no assignee or
132
+ * the assignee is offline (REST layer should still record the decision so
133
+ * it sticks once the agent reconnects).
134
+ */
135
+ pushApprovalResponse(issueId, approvalId, decision, feedback) {
136
+ const issue = this.db.getIssueById(issueId);
137
+ if (!issue?.assigned_to)
138
+ return false;
139
+ const agent = this.db.getAgentByName(issue.assigned_to);
140
+ if (!agent)
141
+ return false;
142
+ return this.sendToAgent(agent.id, {
143
+ type: "issue_approval_response",
144
+ issueId,
145
+ approvalId,
146
+ decision,
147
+ ...(decision === "deny" && feedback ? { feedback } : {}),
148
+ });
149
+ },
150
+ /**
151
+ * Push a chat-stream cancellation to the responder worker. The responder
152
+ * is the agent currently generating a reply (the dashboard knows its name
153
+ * from the streaming bubble's `from` field). Returns false when the agent
154
+ * is unknown or offline — in that case the stream is already broken (WS
155
+ * disconnect killed the subprocess via existing cleanup paths), so the
156
+ * HTTP caller can no-op.
157
+ *
158
+ * Worker-side: looks up `activeTasks["chat:" + requestId]`, flips aborted,
159
+ * and calls controller.abort() so the CLI executor kills its subprocess.
160
+ * If the task already completed naturally before this arrives, the worker
161
+ * logs "no active task" and returns — idempotent.
162
+ */
163
+ pushChatCancel(agentName, requestId, reason) {
164
+ const agent = this.db.getAgentByName(agentName);
165
+ if (!agent)
166
+ return false;
167
+ return this.sendToAgent(agent.id, {
168
+ type: "chat_cancelled",
169
+ requestId,
170
+ agentName,
171
+ ...(reason ? { reason } : {}),
172
+ });
173
+ },
174
+ /**
175
+ * Push a user-supplied follow-up prompt to the assigned worker so it can
176
+ * spawn its CLI with `--resume <sessionId>` (or start fresh when sessionId
177
+ * is missing) and continue the conversation. Returns false when the issue
178
+ * has no assignee or the assignee is offline.
179
+ */
180
+ pushIssueContinue(issueId, prompt) {
181
+ const issue = this.db.getIssueById(issueId);
182
+ if (!issue?.assigned_to)
183
+ return false;
184
+ const agent = this.db.getAgentByName(issue.assigned_to);
185
+ if (!agent)
186
+ return false;
187
+ const repo = resolveIssueRepoCtxLocalOnly(this.db, issue);
188
+ return this.sendToAgent(agent.id, enrichWorkerDispatch(this, {
189
+ type: "issue_continue",
190
+ issueId,
191
+ groupId: issue.group_id,
192
+ title: issue.title,
193
+ prompt,
194
+ sessionId: issue.session_id || undefined,
195
+ workingDir: issue.working_dir || undefined,
196
+ slashCommand: issue.slash_command || undefined,
197
+ approvalPolicy: normalizeApprovalPolicy(issue.approval_policy),
198
+ ...(repo ? { repoUrl: repo.repoUrl, repoBranch: repo.repoBranch, extraRepos: repo.extraRepos } : {}),
199
+ }, issue.assigned_to, issue.group_id));
200
+ },
201
+ /**
202
+ * Push an append-while-active prompt. Worker queues it onto the running
203
+ * task and consumes the queue when the current CLI invocation finishes
204
+ * (continuing with --resume <sessionId> if one is available). Distinct
205
+ * from pushIssueContinue, which the master only fires AFTER the issue
206
+ * has reached completed/failed.
207
+ */
208
+ pushIssueAppend(issueId, prompt) {
209
+ const issue = this.db.getIssueById(issueId);
210
+ if (!issue?.assigned_to)
211
+ return false;
212
+ const agent = this.db.getAgentByName(issue.assigned_to);
213
+ if (!agent)
214
+ return false;
215
+ const repo = resolveIssueRepoCtxLocalOnly(this.db, issue);
216
+ return this.sendToAgent(agent.id, enrichWorkerDispatch(this, {
217
+ type: "issue_append",
218
+ issueId,
219
+ groupId: issue.group_id,
220
+ title: issue.title,
221
+ prompt,
222
+ sessionId: issue.session_id || undefined,
223
+ workingDir: issue.working_dir || undefined,
224
+ slashCommand: issue.slash_command || undefined,
225
+ approvalPolicy: normalizeApprovalPolicy(issue.approval_policy),
226
+ ...(repo ? { repoUrl: repo.repoUrl, repoBranch: repo.repoBranch, extraRepos: repo.extraRepos } : {}),
227
+ }, issue.assigned_to, issue.group_id));
228
+ },
229
+ /** Broadcast to all connected agents that a new issue is available. */
230
+ notifyNewIssue(issueId, groupId, title, createdBy) {
231
+ const msg = {
232
+ type: "issue_created",
233
+ issueId, groupId, title, createdBy,
234
+ };
235
+ for (const conn of this.connections.values()) {
236
+ this.send(conn.ws, msg);
237
+ }
238
+ },
239
+ /**
240
+ * Notify the issue's group of a change so dashboards can refresh without
241
+ * polling. Safe to call after any DB write touching the issue.
242
+ */
243
+ notifyIssueChanged(issueId, groupId, kind) {
244
+ if (!groupId)
245
+ return;
246
+ this.broadcastToGroup(groupId, { type: "issue_changed", issueId, groupId, kind });
247
+ },
248
+ /**
249
+ * Add this agentId to the issue's subscriber set. Idempotent — re-subscribe
250
+ * on reconnect is safe. Used by dashboard clients to opt into
251
+ * issue_usage_progress pushes for the issue they're viewing.
252
+ */
253
+ subscribeIssue(issueId, agentId) {
254
+ let set = this.issueSubscriptions.get(issueId);
255
+ if (!set) {
256
+ set = new Set();
257
+ this.issueSubscriptions.set(issueId, set);
258
+ }
259
+ set.add(agentId);
260
+ },
261
+ /** Remove this agentId from the issue's subscriber set (no-op if absent). */
262
+ unsubscribeIssue(issueId, agentId) {
263
+ const set = this.issueSubscriptions.get(issueId);
264
+ if (!set)
265
+ return;
266
+ set.delete(agentId);
267
+ if (set.size === 0)
268
+ this.issueSubscriptions.delete(issueId);
269
+ },
270
+ /**
271
+ * Remove this agentId from ALL issue subscriptions. Called on disconnect /
272
+ * "Replaced by new connection" to prevent pushes to dead sockets. Iterating
273
+ * a snapshot via Array.from avoids mutating-during-iteration if cleanup
274
+ * deletes the Set (which would also delete the Map entry).
275
+ */
276
+ unsubscribeAllIssues(agentId) {
277
+ for (const issueId of Array.from(this.issueSubscriptions.keys())) {
278
+ this.unsubscribeIssue(issueId, agentId);
279
+ }
280
+ },
281
+ /**
282
+ * Push a ServerMessage to every agent currently subscribed to the issue.
283
+ * Used for issue_usage_progress — explicitly NOT a broadcast, since usage
284
+ * pushes are high-frequency (1Hz during execution) and only the client
285
+ * viewing that issue's detail cares.
286
+ */
287
+ sendToIssueSubscribers(issueId, msg) {
288
+ const set = this.issueSubscriptions.get(issueId);
289
+ if (!set || set.size === 0)
290
+ return;
291
+ for (const agentId of set) {
292
+ this.sendToAgent(agentId, msg);
293
+ }
294
+ },
295
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Session management — Master ↔ Executor session routing.
3
+ *
4
+ * Dashboard 的 `/sessions` 端点现在直接读 master DB 的 `agent_sessions` 表
5
+ * (替代了之前的 in-memory `sessionSnapshots` 缓存)。worker 仍然推
6
+ * `session_snapshot`,master 在 connection.ts 里 upsert 到 DB + 更新内存
7
+ * 缓存(缓存保留给 online 判定 + routeToExecutor 用)。
8
+ *
9
+ * `routeToExecutor` 是反向:master 把 view/delete 请求转发给匹配的 worker,
10
+ * 取第一个响应。
11
+ *
12
+ * Methods attach via Object.assign.
13
+ */
14
+ export const sessionsMethods = {
15
+ /**
16
+ * 列出该群所有 session(包括已失效的),按最近使用倒序。数据源是 DB。
17
+ * online 字段由 connections 内存表 join 算出:该 session 的 (agentName,
18
+ * cliTool) 对应的 worker 当前是否 WS 连着。
19
+ *
20
+ * `GET /sessions?groupId=X` 的快路径。
21
+ */
22
+ listSessionsByGroup(groupId) {
23
+ const rows = this.db.listAgentSessionsByGroup(groupId);
24
+ // 算 online:agent_name + cli_tool 同时匹配某个 connected worker
25
+ const onlineKeys = new Set();
26
+ for (const conn of this.connections.values()) {
27
+ if (conn.ws.readyState !== WebSocket.OPEN)
28
+ continue;
29
+ if (!conn.cliTool)
30
+ continue;
31
+ onlineKeys.add(`${conn.name}:${conn.cliTool}`);
32
+ }
33
+ return rows.map(r => {
34
+ const online = onlineKeys.has(`${r.agent_name}:${r.cli_tool}`);
35
+ const entry = {
36
+ cliTool: r.cli_tool,
37
+ groupId: r.group_id,
38
+ sessionId: r.session_id,
39
+ agentName: r.agent_name,
40
+ usage: (r.input_tokens == null && r.output_tokens == null && r.total_cost_usd == null)
41
+ ? null
42
+ : {
43
+ inputTokens: r.input_tokens ?? undefined,
44
+ outputTokens: r.output_tokens ?? undefined,
45
+ cacheReadTokens: r.cache_read_tokens ?? undefined,
46
+ cacheCreationTokens: r.cache_creation_tokens ?? undefined,
47
+ totalCostUsd: r.total_cost_usd ?? undefined,
48
+ },
49
+ model: r.model ?? null,
50
+ cumulativeCostUsd: r.cumulative_cost_usd,
51
+ cumulativeInputTokens: r.cumulative_input_tokens,
52
+ cumulativeOutputTokens: r.cumulative_output_tokens,
53
+ cumulativeCacheReadTokens: r.cumulative_cache_read_tokens,
54
+ cumulativeCacheCreationTokens: r.cumulative_cache_creation_tokens,
55
+ online,
56
+ invalidatedAt: r.invalidated_at,
57
+ };
58
+ return entry;
59
+ });
60
+ },
61
+ /**
62
+ * 反查单条 session 的 usage/model/cumulative/online/invalidatedAt。
63
+ * `GET /sessions/:.../usage` 用。直接读 DB,不再依赖 worker 在线。
64
+ */
65
+ findSessionEntry(sessionId) {
66
+ const r = this.db.findAgentSession(sessionId);
67
+ if (!r)
68
+ return undefined;
69
+ let online = false;
70
+ for (const conn of this.connections.values()) {
71
+ if (conn.ws.readyState !== WebSocket.OPEN)
72
+ continue;
73
+ if (conn.name === r.agent_name && conn.cliTool === r.cli_tool) {
74
+ online = true;
75
+ break;
76
+ }
77
+ }
78
+ return {
79
+ cliTool: r.cli_tool,
80
+ groupId: r.group_id,
81
+ sessionId: r.session_id,
82
+ agentName: r.agent_name,
83
+ usage: (r.input_tokens == null && r.output_tokens == null && r.total_cost_usd == null)
84
+ ? null
85
+ : {
86
+ inputTokens: r.input_tokens ?? undefined,
87
+ outputTokens: r.output_tokens ?? undefined,
88
+ cacheReadTokens: r.cache_read_tokens ?? undefined,
89
+ cacheCreationTokens: r.cache_creation_tokens ?? undefined,
90
+ totalCostUsd: r.total_cost_usd ?? undefined,
91
+ },
92
+ model: r.model ?? null,
93
+ cumulativeCostUsd: r.cumulative_cost_usd,
94
+ cumulativeInputTokens: r.cumulative_input_tokens,
95
+ cumulativeOutputTokens: r.cumulative_output_tokens,
96
+ cumulativeCacheReadTokens: r.cumulative_cache_read_tokens,
97
+ cumulativeCacheCreationTokens: r.cumulative_cache_creation_tokens,
98
+ online,
99
+ invalidatedAt: r.invalidated_at,
100
+ };
101
+ },
102
+ /**
103
+ * Send a request to one or more online workers matching `predicate` and
104
+ * return the **first** response received within `timeoutMs`. Other responses
105
+ * (including late ones) are dropped.
106
+ *
107
+ * Used by /sessions endpoints:
108
+ * - view: predicate = cliTool match, timeoutMs = 5s
109
+ * - delete: predicate = cliTool match, timeoutMs = 5s
110
+ *
111
+ * Rejects with a TimeoutError if no worker answers in time. The HTTP layer
112
+ * maps that to a 504.
113
+ */
114
+ routeToExecutor(predicate, payload, timeoutMs = 5_000) {
115
+ const targets = [...this.connections.values()].filter((c) => c.ws.readyState === WebSocket.OPEN && predicate(c));
116
+ if (targets.length === 0) {
117
+ return Promise.reject(new Error("no matching executor online"));
118
+ }
119
+ return new Promise((resolve, reject) => {
120
+ const timer = setTimeout(() => {
121
+ this.pendingSessionRequests.delete(payload.requestId);
122
+ reject(new Error(`executor did not respond within ${timeoutMs}ms`));
123
+ }, timeoutMs);
124
+ this.pendingSessionRequests.set(payload.requestId, { resolve, reject, timer });
125
+ for (const conn of targets) {
126
+ this.send(conn.ws, payload);
127
+ }
128
+ });
129
+ },
130
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Backwards-compatible facade for the WebSocket hub.
3
+ *
4
+ * Implementation has moved into `./ws-hub/`:
5
+ * • `./ws-hub/internal.ts` — WSHub runtime class (still monolithic; handler
6
+ * extraction lands in follow-up PRs)
7
+ * • `./ws-hub/index.ts` — Public surface
8
+ *
9
+ * Existing callers (`import { WSHub } from "./ws-hub.js"`) continue to work.
10
+ */
11
+ export * from "./ws-hub/index.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Agent 档案 —— `agents.profile` 列存的 JSON 字符串的解析。
3
+ *
4
+ * 字段定义见 `src/shared/protocol.ts:AgentProfile`(本文件 re-export 保持旧 import path 兼容)。
5
+ * 本文件只做"JSON 字符串 → 强类型"还原,容忍 null/损坏输入(返回 null 而不是抛错,
6
+ * 因为运行时 prompt 渲染可以接受缺角色)。
7
+ */
8
+ export function parseAgentProfile(json) {
9
+ if (!json)
10
+ return null;
11
+ try {
12
+ const obj = JSON.parse(json);
13
+ if (!obj || typeof obj !== "object")
14
+ return null;
15
+ const out = {};
16
+ if (typeof obj.category === "string")
17
+ out.category = obj.category;
18
+ if (typeof obj.position === "string")
19
+ out.position = obj.position;
20
+ if (typeof obj.bio === "string")
21
+ out.bio = obj.bio;
22
+ return out;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /**
29
+ * 群级别 profile 覆盖 merge 到 agent 全局 profile 上,群级别非 undefined 字段胜出。
30
+ * 供 dispatch-enrich(WS 推送 self profile)与 GET /groups/:id(返回群成员花名册)共用,
31
+ * 避免两份 merge 逻辑漂移。
32
+ */
33
+ export function mergeGroupProfile(base, group) {
34
+ if (!group)
35
+ return base ?? undefined;
36
+ const merged = { ...(base ?? {}) };
37
+ if (typeof group.category === "string")
38
+ merged.category = group.category;
39
+ if (typeof group.position === "string")
40
+ merged.position = group.position;
41
+ if (typeof group.bio === "string")
42
+ merged.bio = group.bio;
43
+ return merged;
44
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Digital Employee Mesh — Constants
3
+ */
4
+ /** Protocol version — bump when wire format changes (v2: field ownership + config_update) */
5
+ export const PROTOCOL_VERSION = 2;
6
+ /** Default Master WebSocket port */
7
+ export const DEFAULT_MASTER_PORT = 28800;
8
+ /** Default Master bind host */
9
+ export const DEFAULT_MASTER_HOST = "0.0.0.0";
10
+ /** Agent heartbeat interval (ms) */
11
+ export const HEARTBEAT_INTERVAL_MS = 10_000;
12
+ /** Master heartbeat timeout — disconnect if no heartbeat for this long (ms) */
13
+ export const HEARTBEAT_TIMEOUT_MS = 90_000;
14
+ /** Master heartbeat check interval (ms) */
15
+ export const HEARTBEAT_CHECK_INTERVAL_MS = 30_000;
16
+ /** Auth timeout — Agent must authenticate within this time (ms) */
17
+ export const AUTH_TIMEOUT_MS = 10_000;
18
+ /** JWT expiration */
19
+ export const JWT_EXPIRY = "7d";
20
+ /** JWT algorithm */
21
+ export const JWT_ALGORITHM = "HS256";
22
+ /** Offline message TTL (24 hours) */
23
+ export const OFFLINE_MESSAGE_TTL_HOURS = 24;
24
+ /** Offline message per-agent limit */
25
+ export const OFFLINE_MESSAGE_LIMIT = 100;
26
+ /** Message dedup TTL (5 minutes) */
27
+ export const DEDUP_TTL_MS = 5 * 60 * 1000;
28
+ /** Pending request TTL — reply correlation expires after this (5 minutes) */
29
+ export const PENDING_REQUEST_TTL_MS = 5 * 60 * 1000;
30
+ /** Cleanup interval for dedup and pending requests (60 seconds) */
31
+ export const CLEANUP_INTERVAL_MS = 60_000;
32
+ /** Agent reconnect base delay (ms) */
33
+ export const RECONNECT_BASE_DELAY_MS = 1_000;
34
+ /** Agent reconnect max delay (ms) */
35
+ export const RECONNECT_MAX_DELAY_MS = 30_000;
36
+ /** WebSocket max payload size (1 MB) — prevents OOM from oversized messages */
37
+ export const WS_MAX_PAYLOAD = 1_048_576;
38
+ /** Per-agent rate limit: max messages per window */
39
+ export const RATE_LIMIT_MAX = 60;
40
+ /** Rate limit sliding window (ms) */
41
+ export const RATE_LIMIT_WINDOW_MS = 60_000;
42
+ /** Audit / message log retention (days) */
43
+ export const LOG_RETENTION_DAYS = 30;
44
+ /** Max concurrent inbound dispatches per agent */
45
+ export const MAX_CONCURRENT_DISPATCHES = 10;
46
+ /** WebSocket close codes */
47
+ export const WS_CLOSE = {
48
+ AUTH_TIMEOUT: 4001,
49
+ AUTH_FAILED: 4002,
50
+ INVALID_JSON: 4400,
51
+ NOT_AUTHENTICATED: 4401,
52
+ RATE_LIMITED: 4429,
53
+ };
54
+ /** 合法的 issue 状态(对应 issues.status 列,见 migrations/008-issues.sql) */
55
+ export const ISSUE_STATUSES = ["open", "in_progress", "completed", "failed", "cancelled"];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Message deduplication — in-memory requestId → timestamp map with TTL cleanup.
3
+ */
4
+ export class MessageDedup {
5
+ seen = new Map();
6
+ ttlMs;
7
+ constructor(ttlMs = 5 * 60 * 1000) {
8
+ this.ttlMs = ttlMs;
9
+ }
10
+ /** Returns true if this requestId was seen recently (within TTL). */
11
+ isDuplicate(requestId) {
12
+ const ts = this.seen.get(requestId);
13
+ if (!ts)
14
+ return false;
15
+ if (Date.now() - ts > this.ttlMs) {
16
+ this.seen.delete(requestId);
17
+ return false;
18
+ }
19
+ return true;
20
+ }
21
+ /** Mark a requestId as seen. */
22
+ mark(requestId) {
23
+ this.seen.set(requestId, Date.now());
24
+ }
25
+ /** Evict entries older than TTL. Call periodically (e.g. every 60s). */
26
+ cleanup() {
27
+ const now = Date.now();
28
+ for (const [id, ts] of this.seen) {
29
+ if (now - ts > this.ttlMs)
30
+ this.seen.delete(id);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Group context prompt injection — shared by InboundDispatcher and ExecutorWorker.
3
+ *
4
+ * Prepends group metadata (groupId, groupName, selfName) to the prompt so that
5
+ * any CLI-backed agent knows which group it is responding in.
6
+ */
7
+ function renderActiveIssues(issues) {
8
+ if (!issues || issues.length === 0) {
9
+ return (`[当前群活跃 issue]\n` +
10
+ `无\n` +
11
+ `提示:本群当前没有进行中的 issue。如需修改文件,请先 \`rotom issue create\` 建任务承载,否则只允许 Read/Grep/Glob。\n`);
12
+ }
13
+ const lines = issues.map((it) => {
14
+ const id = it.id.slice(0, 8);
15
+ const owner = it.assignedTo ? ` by ${it.assignedTo}` : " 未认领";
16
+ const prio = it.priority ? ` [${it.priority}]` : "";
17
+ return `- #${id} ${it.status}${prio} "${it.title}"${owner}`;
18
+ });
19
+ return (`[当前群活跃 issue]\n` +
20
+ lines.join("\n") + "\n" +
21
+ `提示:涉及文件改动请关联以上某个 issue;若无匹配的,先 \`rotom issue create\` 新建,确认 in_progress 后再写盘。\n`);
22
+ }
23
+ /**
24
+ * If `conversation` represents a group chat, return the prompt wrapped with
25
+ * group-context metadata; otherwise return the prompt unchanged.
26
+ */
27
+ export function injectGroupContext(prompt, conversation, selfName) {
28
+ const isGroup = conversation?.type === "group" && !!conversation.groupId;
29
+ if (!isGroup)
30
+ return prompt;
31
+ const groupId = conversation.groupId;
32
+ const groupName = conversation.groupName || groupId;
33
+ const header = `[群消息 context: groupId=${groupId}, groupName="${groupName}", ` +
34
+ `你自己是="${selfName}"。` +
35
+ `重要:如果 @ 的是你自己("${selfName}"),那就是在叫你回答,直接回答即可,` +
36
+ `不要再调用发送消息给自己。]\n`;
37
+ const issuesBlock = renderActiveIssues(conversation.activeIssues);
38
+ return `${header}${issuesBlock}\n${prompt}`;
39
+ }
40
+ /**
41
+ * Prefix the prompt with the working directory so CLI agents resolve paths
42
+ * (`src/foo.ts` etc.) consistently with the cwd we pass to spawn(). The
43
+ * spawn cwd alone is invisible to the model — it only sees the prompt text.
44
+ *
45
+ * No-op when cwd is empty (e.g. fallback to executor default).
46
+ *
47
+ * Read-only semantics: the working directory is a **read-only** mount for the
48
+ * agent. The agent may Read / Grep / Glob / Bash (read-only commands) but
49
+ * must NOT call Write / Edit or any other disk-mutating tool on paths under
50
+ * this directory. Cross-machine deployments enforce this so each executor
51
+ * machine's local FS doesn't diverge.
52
+ */
53
+ export function prependWorkingDir(prompt, cwd) {
54
+ if (!cwd)
55
+ return prompt;
56
+ return (`[artifacts目录] ${cwd}\n` +
57
+ `所有相对路径基于此目录解析;spawn 的子进程 cwd 已设置在这里,` +
58
+ `Read/Grep/Glob 直接用相对路径即可,不要用 \`cd\` 切换到其他目录。\n` +
59
+ `**重要:此目录为只读,agent 仅可 Read/Grep/Glob/Bash(只读命令),不得调用 Write/Edit 等写盘工具。**\n` +
60
+ `需要持久化的产出请通过 issue 评论 / artifact 工具回传 master,或用 Bash 写到非 workingDir 的沙箱目录。\n\n` +
61
+ prompt);
62
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * JSON frame encode/decode helpers shared across the three transport sites
3
+ * that previously each hand-rolled `JSON.stringify` / `JSON.parse`:
4
+ * - executor/jsonrpc-transport.ts (stdio newline-delimited JSON-RPC)
5
+ * - executor/worker-connection.ts (agent-protocol WebSocket)
6
+ * - master/terminal-hub.ts (browser xterm WebSocket)
7
+ *
8
+ * `encodeJsonLine` adds the trailing newline that the stdio JSON-RPC
9
+ * transport needs; WS callers use plain `JSON.stringify` since the socket
10
+ * is message-framed.
11
+ *
12
+ * `decodeJson` returns `undefined` on parse failure — callers decide whether
13
+ * to silently drop (WS hubs) or log a warning (JSON-RPC transport with its
14
+ * own labelled warn). Passing the raw Buffer through keeps a `toString()`
15
+ * allocation out of the WS hot path.
16
+ */
17
+ export function encodeJsonLine(msg) {
18
+ return JSON.stringify(msg) + "\n";
19
+ }
20
+ export function decodeJson(data) {
21
+ try {
22
+ if (typeof data === "string")
23
+ return JSON.parse(data);
24
+ if (Array.isArray(data))
25
+ return JSON.parse(Buffer.concat(data).toString());
26
+ if (data instanceof ArrayBuffer)
27
+ return JSON.parse(Buffer.from(data).toString());
28
+ return JSON.parse(data.toString());
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }