@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,275 @@
1
+ /**
2
+ * ๅทกๆฃ€ issue ็ปˆๆ€ๅค„็† โ€”โ€” ๅทกๆฃ€ๅ‘˜ๅฎŒๆˆ patrol issue ๅŽ,่งฃๆž result JSON ่ฝๅบ“ใ€‚
3
+ *
4
+ * ็”ฑ server.ts ็š„ _onIssueTerminal hook ๅœจ issue ่ฟ›ๅ…ฅ terminal ็Šถๆ€ๆ—ถ่ฐƒ็”จใ€‚
5
+ * ่ทŸ e2ed/sync.js ๅŒ็บง,่Œ่ดฃๅ•ไธ€:่งฃๆž result โ†’ ๅ†™ issue_patrol_logs + finish runใ€‚
6
+ *
7
+ * ๅ…ฅๅฃๅˆ†ไธคๆก่ทฏๅพ„:
8
+ * - handleIssuePatrolTerminal: issue-patrol ๅทกๆฃ€ๅ‘˜ โ†’ ๅ†™ issue_patrol_logs
9
+ * - handleLinkPatrolIssueTerminal: link-patrol ๅทกๆฃ€ๅ‘˜ โ†’ UPDATE links + ๅ†™ link_patrol_logs + memory
10
+ *
11
+ * ็ปŸไธ€ๅ…ฅๅฃ dispatchPatrolTerminal(issue) ๅ…ˆๆŒ‰ issueId ๅๆŸฅ link_patrol_runs,
12
+ * ๅ‘ฝไธญ่ตฐ link ๆต็จ‹,ๅฆๅˆ™ fallback ๅˆฐ issue ๆต็จ‹ใ€‚
13
+ */
14
+ import { randomUUID } from "node:crypto";
15
+ import { createLogger } from "../shared/logger.js";
16
+ const log = createLogger("mesh-patrol-terminal");
17
+ /**
18
+ * ็ปŸไธ€ๅ…ฅๅฃ:issue ็ปˆๆ€ๆ—ถ server.ts ่ฐƒ่ฟ™ไธชใ€‚
19
+ * ไผ˜ๅ…ˆๅๆŸฅ link_patrol_runs.patrol_issue_id โ€”โ€” ๅ‘ฝไธญ่ตฐ link ๅˆ†็ฑปๆต็จ‹,
20
+ * ๅฆๅˆ™ fallback ๅˆฐ issue ๅทกๆฃ€ๆต็จ‹ใ€‚
21
+ */
22
+ export function dispatchPatrolTerminal(db, issue) {
23
+ const linkRun = db.getLinkPatrolRunByIssueId(issue.id);
24
+ if (linkRun) {
25
+ handleLinkPatrolIssueTerminal(db, issue);
26
+ return;
27
+ }
28
+ handleIssuePatrolTerminal(db, issue);
29
+ }
30
+ /**
31
+ * ่งฃๆž patrol issue ็š„ result(JSON ๆ•ฐ็ป„),้€ๆกๅ†™ issue_patrol_logs,
32
+ * ๅนถ finish ๅฏนๅบ”็š„ runใ€‚่งฃๆžๅคฑ่ดฅ โ†’ run status='error'ใ€‚
33
+ */
34
+ export function handleIssuePatrolTerminal(db, issue) {
35
+ const run = db.getPatrolRunByIssueId(issue.id);
36
+ if (!run) {
37
+ log.warn(`patrol issue ${issue.id} terminal, but no run found (orphan?)`);
38
+ return;
39
+ }
40
+ // ๅทกๆฃ€ๅ‘˜ๆŠŠ JSON ๆ”พๅœจ issue.result ้‡Œใ€‚result ๅฏ่ƒฝๆ˜ฏ่ฃธ JSON,ไนŸๅฏ่ƒฝ่ขซๅŒ…ๅœจ markdown ไปฃ็ ๅ—้‡Œใ€‚
41
+ const raw = (issue.result ?? "").trim();
42
+ if (!raw) {
43
+ db.finishPatrolRun(run.run_id, "error", { note: "empty result" });
44
+ log.warn(`patrol run ${run.run_id}: empty result`);
45
+ return;
46
+ }
47
+ const parsed = parseVerdicts(raw);
48
+ if (!parsed) {
49
+ db.finishPatrolRun(run.run_id, "error", { note: `bad result JSON: ${raw.slice(0, 200)}` });
50
+ log.error(`patrol run ${run.run_id}: bad result JSON`);
51
+ return;
52
+ }
53
+ let readyCount = 0;
54
+ for (const v of parsed) {
55
+ const verdict = normalizeVerdict(v.verdict);
56
+ if (verdict === "ready")
57
+ readyCount++;
58
+ db.insertPatrolLog({
59
+ id: randomUUID(),
60
+ runId: run.run_id,
61
+ patrolGroupId: run.patrol_group_id,
62
+ issueId: v.issue_id ?? null,
63
+ candidateGroupId: resolveCandidateGroupId(db, v.issue_id),
64
+ verdict,
65
+ ruleMatched: v.rule_matched ?? null,
66
+ rationale: v.rationale ?? null,
67
+ raw: JSON.stringify(v),
68
+ });
69
+ }
70
+ db.finishPatrolRun(run.run_id, "completed", {
71
+ scanned: parsed.length,
72
+ ready: readyCount,
73
+ note: issue.status === "completed" ? undefined : `issue terminal as ${issue.status}`,
74
+ });
75
+ log.info(`patrol run ${run.run_id}: completed (scanned=${parsed.length}, ready=${readyCount})`);
76
+ }
77
+ /**
78
+ * ่งฃๆž link-patrol issue ็š„ result(JSON ๆ•ฐ็ป„),้€ๆก:
79
+ * - UPDATE links SET category/tags/title
80
+ * - INSERT link_patrol_logs
81
+ * ็„ถๅŽๅˆๅนถๆœฌ่ฝฎๅญฆๅˆฐ็š„่ง„ๅˆ™ๅ†™ๅ…ฅ agent_memory(scope=global, tags=link_classification)ใ€‚
82
+ * ่งฃๆžๅคฑ่ดฅ โ†’ run status='error'ใ€‚
83
+ */
84
+ export function handleLinkPatrolIssueTerminal(db, issue) {
85
+ const run = db.getLinkPatrolRunByIssueId(issue.id);
86
+ if (!run) {
87
+ log.warn(`link-patrol issue ${issue.id} terminal, but no run found (orphan?)`);
88
+ return;
89
+ }
90
+ const raw = (issue.result ?? "").trim();
91
+ if (!raw) {
92
+ db.finishLinkPatrolRun(run.run_id, "error", { note: "empty result" });
93
+ log.warn(`link-patrol run ${run.run_id}: empty result`);
94
+ return;
95
+ }
96
+ const parsed = parseLinkVerdicts(raw);
97
+ if (!parsed) {
98
+ db.finishLinkPatrolRun(run.run_id, "error", { note: `bad result JSON: ${raw.slice(0, 200)}` });
99
+ log.error(`link-patrol run ${run.run_id}: bad result JSON`);
100
+ return;
101
+ }
102
+ let classifiedCount = 0;
103
+ /** ๆŒ‰่šๅˆ:ๅŒ host ็š„ๅคš link ๅ…ฑ็”จไธ€ๆก่ง„ๅˆ™ */
104
+ const rulesByHost = new Map();
105
+ for (const v of parsed) {
106
+ const linkId = v.link_id;
107
+ const category = (v.category ?? "other").toString();
108
+ const tags = Array.isArray(v.tags)
109
+ ? v.tags.filter((t) => typeof t === "string" && t.trim()).map((t) => t.trim())
110
+ : [];
111
+ const title = typeof v.title === "string" ? v.title : null;
112
+ const rationale = typeof v.rationale === "string" ? v.rationale : null;
113
+ if (!linkId)
114
+ continue;
115
+ const link = db.getLink(linkId);
116
+ if (!link) {
117
+ log.warn(`link-patrol: link ${linkId} not found, skip`);
118
+ continue;
119
+ }
120
+ db.updateLinkClassification(linkId, { category, tags, title });
121
+ db.insertLinkPatrolLog({
122
+ id: randomUUID(),
123
+ runId: run.run_id,
124
+ linkId,
125
+ category,
126
+ tags,
127
+ title,
128
+ rationale,
129
+ raw: JSON.stringify(v),
130
+ });
131
+ classifiedCount++;
132
+ // ็ดฏ็งฏ host ่ง„ๅˆ™
133
+ const entry = rulesByHost.get(link.host);
134
+ if (entry) {
135
+ entry.tagsSet = new Set([...entry.tagsSet, ...tags]);
136
+ }
137
+ else {
138
+ rulesByHost.set(link.host, { category, tagsSet: new Set(tags), sampleLink: link.url_raw });
139
+ }
140
+ }
141
+ // ๅˆๅนถๅ†™ๅ…ฅ agent_memory(ๆฏ host ไธ€ๆก,upsert:ๅŒ key ๅทฒๅญ˜ๅœจๅฐฑ update value/tags)
142
+ for (const [host, info] of rulesByHost.entries()) {
143
+ upsertLinkRuleMemory(db, issue.group_id, host, info.category, [...info.tagsSet], info.sampleLink, "system:link-patrol");
144
+ }
145
+ db.finishLinkPatrolRun(run.run_id, "completed", {
146
+ classified: classifiedCount,
147
+ note: issue.status === "completed" ? undefined : `issue terminal as ${issue.status}`,
148
+ });
149
+ log.info(`link-patrol run ${run.run_id}: completed (classified=${classifiedCount}, rules_learned=${rulesByHost.size})`);
150
+ }
151
+ /**
152
+ * ๆŠŠไธ€ๆก host ็บง่ง„ๅˆ™ๅ†™ๅ…ฅ agent_memory,scope=group / group_id=patrolGroupId / category=convention
153
+ * / tags=["link_classification"]ใ€‚ๆณจๆ„:link-patrol ่‡ชๅŠจๅญฆๅˆฐ็š„่ง„ๅˆ™ๆ”พ patrol-link ็พค,ไธๆ”พ global
154
+ * โ€”โ€” global namespace ๅฟ…้กปไบบๅทฅ review / promote ๆ‰่ƒฝ่ฟ›ใ€‚ไธ‹ไธ€่ฝฎ link-patrol handler ๆ‹ผ prompt ๆ—ถ
155
+ * ไปๅฏ่ขซ listMemory({ groupId, tags:["link_classification"] }) ๆ‹‰ๅ‡บใ€‚
156
+ * key = `link_rule:${host}`ใ€‚ๅŒ key ๅทฒๅญ˜ๅœจ โ†’ ๅˆๅนถ tags,ๅˆทๆ–ฐ value;ไธๅญ˜ๅœจ โ†’ INSERTใ€‚
157
+ */
158
+ function upsertLinkRuleMemory(db, groupId, host, category, tags, sampleUrl, createdBy) {
159
+ const key = `link_rule:${host}`;
160
+ const existing = db.db.prepare(`SELECT id, tags FROM agent_memory WHERE key = ? AND group_id = ? AND active = 1 LIMIT 1`).get(key, groupId);
161
+ const value = `host=${host} ้ป˜่ฎคๅˆ†็ฑป ${category}; tags=[${tags.join(", ")}]; ๆ ทไพ‹=${sampleUrl}`;
162
+ const summary = `${host} โ†’ ${category}`;
163
+ const mergedTags = Array.from(new Set(["link_classification", ...tags]));
164
+ if (existing) {
165
+ let oldTags = [];
166
+ try {
167
+ const parsed = JSON.parse(existing.tags);
168
+ if (Array.isArray(parsed))
169
+ oldTags = parsed.filter((t) => typeof t === "string");
170
+ }
171
+ catch { /* ignore */ }
172
+ const finalTags = Array.from(new Set([...mergedTags, ...oldTags]));
173
+ db.updateMemory(existing.id, { value, summary, tags: finalTags, category: "convention" });
174
+ }
175
+ else {
176
+ const memId = `mem_link_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
177
+ db.addMemory({
178
+ id: memId,
179
+ scope: "group",
180
+ groupId,
181
+ category: "convention",
182
+ sourceType: "manual",
183
+ key,
184
+ value,
185
+ summary,
186
+ tags: mergedTags,
187
+ visibility: "group",
188
+ agentVisible: true,
189
+ createdBy,
190
+ });
191
+ }
192
+ }
193
+ function parseVerdicts(raw) {
194
+ // 1. ็›ดๆŽฅ JSON.parse
195
+ try {
196
+ const v = JSON.parse(raw);
197
+ if (Array.isArray(v))
198
+ return v;
199
+ }
200
+ catch { /* fall through */ }
201
+ // 2. ไปŽ markdown ```json ... ``` ้‡ŒๆŠ 
202
+ const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
203
+ if (fenceMatch) {
204
+ try {
205
+ const v = JSON.parse(fenceMatch[1]);
206
+ if (Array.isArray(v))
207
+ return v;
208
+ }
209
+ catch { /* fall through */ }
210
+ }
211
+ // 3. ๆŠ ๆœ€ๅค–ๅฑ‚ [ ... ]
212
+ const bracketMatch = raw.match(/\[[\s\S]*\]/);
213
+ if (bracketMatch) {
214
+ try {
215
+ const v = JSON.parse(bracketMatch[0]);
216
+ if (Array.isArray(v))
217
+ return v;
218
+ }
219
+ catch { /* fall through */ }
220
+ }
221
+ return null;
222
+ }
223
+ /** ่งฃๆž link-patrol result JSONใ€‚ๅค็”จ parseVerdicts ็š„ไธ‰็ง fallback,ไฝ†็ฑปๅž‹ไธๅŒใ€‚ */
224
+ function parseLinkVerdicts(raw) {
225
+ try {
226
+ const v = JSON.parse(raw);
227
+ if (Array.isArray(v))
228
+ return v;
229
+ }
230
+ catch { /* fall through */ }
231
+ const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
232
+ if (fenceMatch) {
233
+ try {
234
+ const v = JSON.parse(fenceMatch[1]);
235
+ if (Array.isArray(v))
236
+ return v;
237
+ }
238
+ catch { /* fall through */ }
239
+ }
240
+ const bracketMatch = raw.match(/\[[\s\S]*\]/);
241
+ if (bracketMatch) {
242
+ try {
243
+ const v = JSON.parse(bracketMatch[0]);
244
+ if (Array.isArray(v))
245
+ return v;
246
+ }
247
+ catch { /* fall through */ }
248
+ }
249
+ return null;
250
+ }
251
+ function normalizeVerdict(v) {
252
+ if (!v)
253
+ return "uncertain";
254
+ const s = String(v).toLowerCase().replace(/[-\s]/g, "_");
255
+ if (s === "ready")
256
+ return "ready";
257
+ if (s === "not_ready")
258
+ return "not_ready";
259
+ if (s === "uncertain")
260
+ return "uncertain";
261
+ if (s === "skipped")
262
+ return "skipped";
263
+ // ๅ…œๅบ•:ๅ…ณ้”ฎๅญ—็Œœๆต‹
264
+ if (s.includes("ready") && !s.includes("not"))
265
+ return "ready";
266
+ if (s.includes("not_ready") || s.includes("not ready"))
267
+ return "not_ready";
268
+ return "uncertain";
269
+ }
270
+ function resolveCandidateGroupId(db, issueId) {
271
+ if (!issueId)
272
+ return null;
273
+ const issue = db.getIssueById(issueId);
274
+ return issue?.group_id ?? null;
275
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Master ไพง็š„ worktree ๆ‰ซๆ(ๅช่ฏป)ใ€‚
3
+ *
4
+ * worktree ็‰ฉ็†ไธŠๅœจ executor ๆœฌๆœบ(`~/.rotom/repos/`),ไฝ† master ไธŽ executor
5
+ * ๅŒๆœบ้ƒจ็ฝฒๆ—ถๅฏ็›ดๆŽฅ่ฏป FS ๆšด้œฒ็ป™ Dashboardใ€‚่ทจๆœบๅ™จๆ—ถๆœฌๆจกๅ—่ฟ”ๅ›ž็ฉบๅˆ—่กจ(ๆœฌๆœบๆ— ็ผ“ๅญ˜)ใ€‚
6
+ *
7
+ * ไธไพ่ต– executor/repo-cache.ts(้‚ฃๆ˜ฏ executor ไพง spawn git ็š„ๆจกๅ—),่ฟ™้‡Œ็‹ฌ็ซ‹
8
+ * ๅฎž็Žฐ scan ้€ป่พ‘,ไฟๆŒ master/executor ๆจกๅ—่พน็•Œๆธ…ๆ™ฐใ€‚็ฎ—ๆณ•ไธŽ repo-cache ไธ€่‡ด:
9
+ * - repoId = SHA-1(ๅฝ’ไธ€ๅŒ– URL) ๅ‰ 12 ไฝ
10
+ * - repoName = URL ๆœ€ๅŽไธ€ๆฎต(ๅŽป .git)
11
+ * - bare ็›ฎๅฝ•ๅ = <repoName>-<repoId8>.git
12
+ * - worktree ๆ น = <repoName>-<repoId8>-wt/<slot>/
13
+ *
14
+ * slot ๆŽจ็ฎ—:group ๆจกๅผ = `group-<groupId8>`,issue ๆจกๅผ = `<issueId8>`ใ€‚
15
+ * Dashboard ็”จ่ฟ™ไธชๆŽจ็ฎ—"ๅฝ“ๅ‰ group ็š„ worktree ๅบ”่ฏฅๅœจๅ“ช"ใ€‚
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { spawnSync } from "node:child_process";
19
+ import fs from "node:fs";
20
+ import os from "node:os";
21
+ import path from "node:path";
22
+ const REPOS_ROOT = path.join(os.homedir(), ".rotom", "repos");
23
+ export function repoIdFor(url) {
24
+ let u = url.trim();
25
+ if (u.endsWith(".git"))
26
+ u = u.slice(0, -4);
27
+ u = u.replace(/^ssh:\/\/([^@]+@)?/, "").replace(/^https?:\/\//, "");
28
+ u = u.replace(/^git@/, "").replace(/\/$/, "");
29
+ return createHash("sha1").update(u).digest("hex").slice(0, 12);
30
+ }
31
+ export function repoNameFor(url) {
32
+ let u = url.trim();
33
+ if (u.endsWith(".git"))
34
+ u = u.slice(0, -4);
35
+ u = u.split("?")[0].split("#")[0].replace(/\/$/, "");
36
+ const last = u.split("/").pop() || "repo";
37
+ return last || "repo";
38
+ }
39
+ /** bare clone ่ทฏๅพ„(ไธๆŸฅ FS,ๅช็ฎ—)ใ€‚ */
40
+ export function barePathForUrl(url) {
41
+ const repoId = repoIdFor(url);
42
+ const repoName = repoNameFor(url);
43
+ return path.join(REPOS_ROOT, `${repoName}-${repoId.slice(0, 8)}.git`);
44
+ }
45
+ /** worktree ่ทฏๅพ„(ไธๆŸฅ FS,ๅช็ฎ—)ใ€‚slot ็”ฑ่ฐƒ็”จๆ–นๅ†ณๅฎš(group-<groupId8> ๆˆ– <issueId8>)ใ€‚ */
46
+ export function worktreePathForUrl(url, slot) {
47
+ const repoId = repoIdFor(url);
48
+ const repoName = repoNameFor(url);
49
+ return path.join(REPOS_ROOT, `${repoName}-${repoId.slice(0, 8)}-wt`, slot);
50
+ }
51
+ /** ๆŽจ็ฎ— group ๆจกๅผไธ‹ๆŸ group ็š„ worktree ่ทฏๅพ„ใ€‚ */
52
+ export function groupWorktreePath(url, groupId) {
53
+ return worktreePathForUrl(url, `group-${groupId.slice(0, 8)}`);
54
+ }
55
+ function runGitSync(args, opts) {
56
+ const r = spawnSync("git", args, {
57
+ cwd: opts?.cwd,
58
+ encoding: "utf-8",
59
+ stdio: ["ignore", "pipe", "pipe"],
60
+ });
61
+ return {
62
+ ok: r.status === 0,
63
+ stdout: typeof r.stdout === "string" ? r.stdout : "",
64
+ stderr: typeof r.stderr === "string" ? r.stderr : "",
65
+ };
66
+ }
67
+ function dirSize(p) {
68
+ let total = 0;
69
+ try {
70
+ const walk = (dir) => {
71
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
72
+ const full = path.join(dir, e.name);
73
+ if (e.isDirectory())
74
+ walk(full);
75
+ else if (e.isFile()) {
76
+ try {
77
+ total += fs.statSync(full).size;
78
+ }
79
+ catch { /* skip */ }
80
+ }
81
+ }
82
+ };
83
+ walk(p);
84
+ }
85
+ catch { /* skip */ }
86
+ return total;
87
+ }
88
+ /**
89
+ * ๆ‰ซๆๆœฌๆœบ `~/.rotom/repos/`,ๅˆ—ๅ‡บๆ‰€ๆœ‰ bare clone ๅŠๅ…ถ worktreeใ€‚
90
+ * ๆœฌๆœบๆ—  repos ็›ฎๅฝ•(่ทจๆœบๅ™จ้ƒจ็ฝฒ master ไธๅœจ executor ๆœบๅ™จ)ๆ—ถ่ฟ”ๅ›ž็ฉบๆ•ฐ็ป„ใ€‚
91
+ */
92
+ export function scanAllRepos() {
93
+ if (!fs.existsSync(REPOS_ROOT))
94
+ return [];
95
+ const entries = fs.readdirSync(REPOS_ROOT, { withFileTypes: true });
96
+ const result = [];
97
+ for (const e of entries) {
98
+ if (!e.isDirectory() || !e.name.endsWith(".git"))
99
+ continue;
100
+ const repoKey = e.name.slice(0, -4); // ๅŽป .git
101
+ const bp = path.join(REPOS_ROOT, e.name);
102
+ // repoName = ๅŽปๆމๆœ€ๅŽไธ€ๆฎต -<repoId8>
103
+ const lastDash = repoKey.lastIndexOf("-");
104
+ const repoName = lastDash > 0 ? repoKey.slice(0, lastDash) : repoKey;
105
+ const wtList = runGitSync(["worktree", "list", "--porcelain"], { cwd: bp });
106
+ const worktrees = [];
107
+ if (wtList.ok) {
108
+ let cur = {};
109
+ for (const line of wtList.stdout.split("\n")) {
110
+ if (line.startsWith("worktree ")) {
111
+ if (cur.path)
112
+ worktrees.push(cur);
113
+ cur = { path: line.slice("worktree ".length), branch: "", head: "" };
114
+ }
115
+ else if (line.startsWith("HEAD ")) {
116
+ cur.head = line.slice("HEAD ".length).slice(0, 8);
117
+ }
118
+ else if (line.startsWith("branch ")) {
119
+ const ref = line.slice("branch ".length);
120
+ cur.branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
121
+ }
122
+ else if (line === "") {
123
+ if (cur.path)
124
+ worktrees.push(cur);
125
+ cur = {};
126
+ }
127
+ }
128
+ if (cur.path)
129
+ worktrees.push(cur);
130
+ }
131
+ // ็ฌฌไธ€ๆกๆ˜ฏ bare clone ่‡ชๅทฑ,่ฟ‡ๆปคๆމ
132
+ const realWorktrees = worktrees.filter(w => !w.path.endsWith(".git"));
133
+ result.push({
134
+ repoKey,
135
+ repoName,
136
+ barePath: bp,
137
+ worktrees: realWorktrees,
138
+ sizeBytes: dirSize(bp),
139
+ });
140
+ }
141
+ return result;
142
+ }
143
+ /**
144
+ * ๆŽจ็ฎ—ๆŸ group ็š„ worktree ไฟกๆฏ(็”จไบŽ Dashboard ๆ˜พ็คบ"ๅฝ“ๅ‰ worktree")ใ€‚
145
+ *
146
+ * ่ฟ”ๅ›ž:
147
+ * - ่‹ฅ group ๆฒก้… repo_url โ†’ null
148
+ * - ่‹ฅ้…ไบ† โ†’ { url, branch, mode, primaryPath, extraRepos[], exists }
149
+ * primaryPath ๆ˜ฏๆŽจ็ฎ—่ทฏๅพ„(group ๆจกๅผ slot = group-<groupId8>);
150
+ * exists ่กจ็คบ่ฏฅ่ทฏๅพ„ๅœจๆœฌๆœบ FS ๆ˜ฏๅฆๅทฒๅญ˜ๅœจ(executor ๆ˜ฏๅฆๅทฒๅˆ›ๅปบ)ใ€‚
151
+ */
152
+ export function resolveGroupWorktreeInfo(db, groupId) {
153
+ const group = db.getGroupById(groupId);
154
+ if (!group)
155
+ return null;
156
+ const url = group.repo_url?.trim();
157
+ if (!url)
158
+ return null;
159
+ const branch = group.repo_default_branch?.trim() || null;
160
+ const mode = group.worktree_mode === "issue" ? "issue" : "group";
161
+ const slot = mode === "group" ? `group-${groupId.slice(0, 8)}` : "issue-mode";
162
+ const primaryPath = worktreePathForUrl(url, slot);
163
+ const primaryExists = fs.existsSync(primaryPath);
164
+ let extras = [];
165
+ if (group.extra_repos) {
166
+ try {
167
+ const parsed = JSON.parse(group.extra_repos);
168
+ if (Array.isArray(parsed)) {
169
+ extras = parsed
170
+ .filter((e) => !!e && typeof e === "object" && typeof e.id === "string" && typeof e.url === "string" && typeof e.mountPath === "string")
171
+ .map(e => {
172
+ const extraSlot = mode === "group" ? `group-${groupId.slice(0, 8)}` : "issue-mode";
173
+ const p = worktreePathForUrl(e.url, extraSlot);
174
+ return {
175
+ id: e.id,
176
+ url: e.url,
177
+ branch: typeof e.branch === "string" && e.branch ? e.branch : null,
178
+ mountPath: e.mountPath,
179
+ path: p,
180
+ exists: fs.existsSync(p),
181
+ };
182
+ });
183
+ }
184
+ }
185
+ catch { /* malformed */ }
186
+ }
187
+ return { url, branch, mode, primaryPath, primaryExists, extras };
188
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Digital Employee Mesh โ€” Router
3
+ *
4
+ * One routing mode: exact.
5
+ * Router only makes decisions โ€” it does NOT send messages.
6
+ * WSHub reads the result and handles actual delivery.
7
+ */
8
+ import { MessageDedup } from "../shared/dedup.js";
9
+ import { DEDUP_TTL_MS, CLEANUP_INTERVAL_MS, PENDING_REQUEST_TTL_MS, } from "../shared/constants.js";
10
+ import { parseAgentRef } from "../shared/protocol/federation.js";
11
+ /** Sentinel fromAgentId for federated pending requests(ๆœฌๅœฐ ws-hub ๆฐธ่ฟœไธไผšๆ‹ฟ่ฟ™ไธช id ๅŽป่ทฏ็”ฑ) */
12
+ export const FEDERATED_REPLY_MARKER = "__federation__";
13
+ export class Router {
14
+ db;
15
+ logger;
16
+ dedup = new MessageDedup(DEDUP_TTL_MS);
17
+ pendingRequests = new Map();
18
+ cleanupTimer;
19
+ /** Federation ๅฎขๆˆท็ซฏ(member ๆจกๅผ);standalone ๆ—ถไธบ undefined */
20
+ fedClient;
21
+ /** ๅทฒๅŠ ๅ…ฅ็š„ teamId(็”จไบŽๆŸฅ agent_visibility ็ผ“ๅญ˜) */
22
+ teamId;
23
+ /** ๆœฌๆœบ hostname(็”จไบŽ from.hostname ๅญ—ๆฎต) */
24
+ localHostname;
25
+ /** Federation ๅ›žๅค hook:ๆœฌๅœฐ agent ๅ›žๅค federated ่ฏทๆฑ‚ๆ—ถ่ฐƒ็”จ,็”ฑ FederationManager ๆณจๅ…ฅ */
26
+ fedReplyHook;
27
+ constructor(db, logger) {
28
+ this.db = db;
29
+ this.logger = logger;
30
+ // Periodic cleanup of dedup and pending requests
31
+ this.cleanupTimer = setInterval(() => {
32
+ this.dedup.cleanup();
33
+ const now = Date.now();
34
+ for (const [id, entry] of this.pendingRequests) {
35
+ if (now - entry.createdAt > PENDING_REQUEST_TTL_MS) {
36
+ this.pendingRequests.delete(id);
37
+ }
38
+ }
39
+ }, CLEANUP_INTERVAL_MS);
40
+ }
41
+ /**
42
+ * ๆณจๅ…ฅ federation ๅฎขๆˆท็ซฏ(member ๆจกๅผๆ—ถ็”ฑ server.ts ่ฐƒ็”จ)ใ€‚
43
+ * ๆณจๅ…ฅๅŽ Router ๆ‰ไผš่ตฐ federated ่ทฏ็”ฑๅˆ†ๆ”ฏใ€‚
44
+ */
45
+ setFederation(client, teamId, localHostname) {
46
+ this.fedClient = client;
47
+ this.teamId = teamId;
48
+ this.localHostname = localHostname;
49
+ }
50
+ /**
51
+ * Federation deliver ๅ…ฅๅฃ:ๅ่ฐƒ master / member ๆ”ถๅˆฐ FedDeliver ๅŽ่ฐƒ่ฟ™ไธช,
52
+ * ่ฎฉๆœฌๆœบ WSHub ๆŠ•้€’ๅˆฐ็›ฎๆ ‡ๆœฌๅœฐ agentใ€‚
53
+ * ่ฟ”ๅ›ž true=ๅทฒๆ‰พๅˆฐ็›ฎๆ ‡ๅนถๆŠ•้€’(็”ฑ่ฐƒ็”จๆ–นๅฎž้™… send)ใ€‚
54
+ */
55
+ handleFedDeliver(msg, sendToLocal) {
56
+ // to.name ๅœจๆœฌๆœบๆŸฅ
57
+ const target = this.db.getLocalAgentByName(msg.to.name)
58
+ ?? this.db.getAgentByName(msg.to.name);
59
+ if (!target)
60
+ return false;
61
+ return sendToLocal(target.name, msg.payload);
62
+ }
63
+ /** Federation reply ๅ…ฅๅฃ:FedReply ๅˆฐ่พพๅŽ,resolve pendingRequest ่ทฏ็”ฑๅ›žๆฅๆบ */
64
+ handleFedReply(msg, sendReply) {
65
+ const entry = this.pendingRequests.get(msg.requestId);
66
+ if (entry) {
67
+ sendReply(entry.fromAgentId, msg.payload);
68
+ }
69
+ }
70
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
71
+ // Public API
72
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
73
+ /**
74
+ * Route a message. Returns a decision (targetAgentId) โ€” caller handles delivery.
75
+ */
76
+ route(fromAgentId, msg) {
77
+ // Dedup
78
+ if (this.dedup.isDuplicate(msg.requestId)) {
79
+ return { delivered: false, queued: false, error: "Duplicate message" };
80
+ }
81
+ this.dedup.mark(msg.requestId);
82
+ // Resolve sender info
83
+ const fromAgent = this.db.getAgentById(fromAgentId);
84
+ const fromName = fromAgent?.name || "unknown";
85
+ const fromDomain = fromAgent?.domain;
86
+ const summary = (msg.payload?.message || "").slice(0, 100);
87
+ // Record for reply correlation (preserve conversation for reply path)
88
+ this.pendingRequests.set(msg.requestId, { fromAgentId, createdAt: Date.now(), conversation: msg.conversation });
89
+ // Route
90
+ if (msg.target) {
91
+ return this.routeExact(fromAgentId, fromName, fromDomain, msg.target, summary, msg.requestId, msg.payload);
92
+ }
93
+ return { delivered: false, queued: false, error: "No target specified" };
94
+ }
95
+ /**
96
+ * Resolve the original sender for a reply. Returns agentId or undefined.
97
+ * Does NOT consume the entry โ€” allows multi-round replies on the same requestId.
98
+ * Entry is cleaned up by TTL instead.
99
+ */
100
+ resolveReplyTarget(requestId) {
101
+ const entry = this.pendingRequests.get(requestId);
102
+ if (entry) {
103
+ entry.createdAt = Date.now();
104
+ return entry.fromAgentId;
105
+ }
106
+ return undefined;
107
+ }
108
+ /** ๆŸฅ่ฏข requestId ๆ˜ฏๅฆๆฅ่‡ช federation(ๆœฌๅœฐ agent ๅ›žๅคๆ—ถ่‹ฅไธบ true,่ตฐ fedReplyHook ่€Œ้ž sendToAgent) */
109
+ isFederatedRequest(requestId) {
110
+ return this.pendingRequests.get(requestId)?.isFederated === true;
111
+ }
112
+ /**
113
+ * ๆณจๅ†Œไธ€ไธช federated ๆฅๆบ็š„ pending requestใ€‚
114
+ *
115
+ * ๅ่ฐƒ master / member ๆ”ถๅˆฐ FedDeliver ๆŠ•ๅˆฐๆœฌๅœฐ agent ไน‹ๅ‰่ฐƒ็”จใ€‚
116
+ * fromAgentId ็”จไธ€ไธช sentinel,ไฝฟ ws-hub ็š„ sendToAgent ไธไผš็œŸๆŠ•้€’ๅˆฐไปปไฝ•ๆœฌๅœฐ agent
117
+ * (ๅฎž้™…ไธŠ ws-hub reply handler ๅœจ isFederated ๆ—ถไผšๆ”น่ตฐ fedReplyHook,ไธไผš sendToAgent)ใ€‚
118
+ */
119
+ registerFederatedPendingRequest(requestId, conversation) {
120
+ this.pendingRequests.set(requestId, {
121
+ fromAgentId: FEDERATED_REPLY_MARKER,
122
+ createdAt: Date.now(),
123
+ conversation,
124
+ isFederated: true,
125
+ });
126
+ }
127
+ /** ๆถˆ่ดนๅนถๆธ…ๆމไธ€ไธช federated pending request(ๆˆๅŠŸ dispatch reply ๅŽ่ฐƒ็”จ) */
128
+ consumeFederatedPendingRequest(requestId) {
129
+ return this.pendingRequests.delete(requestId);
130
+ }
131
+ /**
132
+ * Get the conversation context associated with a pending request.
133
+ */
134
+ getConversation(requestId) {
135
+ return this.pendingRequests.get(requestId)?.conversation;
136
+ }
137
+ stop() {
138
+ clearInterval(this.cleanupTimer);
139
+ }
140
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
141
+ // Private routing methods
142
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
143
+ routeExact(_fromId, fromName, fromDomain, targetName, summary, requestId, payload) {
144
+ // 1. ๆœฌๆœบ agent ๆŸฅ่ฏข(ไผ˜ๅ…ˆ hostname ๅคๅˆ้”ฎ,fallback ๅ…จๅฑ€ name)
145
+ const target = this.db.getLocalAgentByName(targetName)
146
+ ?? this.db.getAgentByName(targetName);
147
+ if (target) {
148
+ // Check if target agent is disabled
149
+ if (target.enabled === 0) {
150
+ this.db.audit({ fromName, fromDomain, toName: target.name, toDomain: target.domain, routeType: "exact", result: "rejected", messageSummary: summary });
151
+ return { delivered: false, queued: false, error: `Agent "${targetName}" is disabled` };
152
+ }
153
+ if (!this.db.canCrossDomain(fromDomain, target.domain)) {
154
+ this.db.audit({ fromName, fromDomain, toName: target.name, toDomain: target.domain, routeType: "exact", result: "rejected", messageSummary: summary });
155
+ return { delivered: false, queued: false, error: "Cross-domain not allowed" };
156
+ }
157
+ this.db.audit({ fromName, fromDomain, toName: target.name, toDomain: target.domain, routeType: "exact", result: "routed", messageSummary: summary });
158
+ return { targetAgentId: target.id, targetName: target.name, delivered: false, queued: false };
159
+ }
160
+ // 2. ๆœฌๆœบๆ‰พไธๅˆฐ โ†’ ่ตฐ federation ่ทฏ็”ฑ(ๅฆ‚ๆžœ้…ไบ†)
161
+ if (this.fedClient && this.teamId && requestId && payload) {
162
+ const fedResult = this.routeFederated(fromName, targetName, requestId, payload, summary);
163
+ if (fedResult)
164
+ return fedResult;
165
+ }
166
+ this.db.audit({ fromName, fromDomain, toName: targetName, routeType: "exact", result: "failed", messageSummary: summary });
167
+ return { delivered: false, queued: false, error: `Agent "${targetName}" not found` };
168
+ }
169
+ /**
170
+ * ่ทจ master ่ทฏ็”ฑ:่งฃๆž targetName(alice@hostB ๆˆ–่ฃธ alice)โ†’ ๆŸฅ agent_visibility
171
+ * โ†’ ่ฐƒ federationClient.route ๅ‘ FedRouteMessageใ€‚
172
+ */
173
+ routeFederated(fromName, targetName, requestId, payload, summary) {
174
+ if (!this.fedClient || !this.teamId || !this.localHostname)
175
+ return null;
176
+ const ref = parseAgentRef(targetName);
177
+ let targetMasterId;
178
+ let targetHostname;
179
+ if (ref.hostname && ref.hostname !== this.localHostname) {
180
+ // ๅฝขๅฆ‚ "alice@hostB" โ†’ ๆŒ‰ (hostname, name) ๅๆŸฅ
181
+ const vis = this.db.findVisibleAgentByHostAndName(this.teamId, ref.hostname, ref.name);
182
+ if (!vis)
183
+ return null;
184
+ targetMasterId = vis.master_id;
185
+ targetHostname = vis.hostname;
186
+ }
187
+ else {
188
+ // ่ฃธ name โ†’ ๆŒ‰ name ๅๆŸฅ(department ๅ†…ๅฟ…้กปๅ”ฏไธ€)
189
+ const candidates = this.db.findVisibleAgentsByName(this.teamId, ref.name);
190
+ if (candidates.length === 0)
191
+ return null;
192
+ if (candidates.length > 1) {
193
+ this.db.audit({ fromName, toName: targetName, routeType: "federated", result: "failed", messageSummary: `ambiguous: ${candidates.length} matches` });
194
+ return { delivered: false, queued: false, error: `Ambiguous agent "${ref.name}" across masters (use name@hostname)` };
195
+ }
196
+ targetMasterId = candidates[0].master_id;
197
+ targetHostname = candidates[0].hostname;
198
+ }
199
+ // ๅ‘ FedRouteMessage ็ป™ๅ่ฐƒ master(็”ฑๅ่ฐƒ master ่ฝฌ FedDeliver)
200
+ const ok = this.fedClient.route(requestId, { hostname: this.localHostname, name: fromName }, { hostname: targetHostname, name: ref.name }, { message: payload.message, files: payload.files });
201
+ if (!ok) {
202
+ return { delivered: false, queued: false, error: "Federation client not connected" };
203
+ }
204
+ this.db.audit({
205
+ fromName,
206
+ toName: `${ref.name}@${targetHostname}`,
207
+ routeType: "federated",
208
+ result: "routed",
209
+ messageSummary: summary,
210
+ });
211
+ // delivered=true ๅ‘Š่ฏ‰ WSHub ไธ่ฆๅ†ๅฐ่ฏ•ๆœฌๅœฐๆŠ•้€’
212
+ return { delivered: true, queued: false, targetName: `${ref.name}@${targetHostname}` };
213
+ }
214
+ }