@a-company/paradigm 3.44.0 → 5.3.3

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 (168) hide show
  1. package/dist/{accept-orchestration-ZUWQUHSK.js → accept-orchestration-GX2YRWM4.js} +5 -5
  2. package/dist/{add-VSPZ6FM4.js → add-FZRKEGH4.js} +1 -1
  3. package/dist/agent-HYKC2LAK.js +387 -0
  4. package/dist/agent-loader-SJPJJS33.js +36 -0
  5. package/dist/{agents-suggest-65SER5IS.js → agents-suggest-DNSYJ6IA.js} +1 -1
  6. package/dist/{aggregate-SV3VGEIL.js → aggregate-H57K7PNV.js} +1 -1
  7. package/dist/{assess-UHBDYIK7.js → assess-4WVXZLZQ.js} +2 -2
  8. package/dist/{auto-24ICVUH4.js → auto-QFS5NHQU.js} +1 -1
  9. package/dist/{beacon-3SJV4DAP.js → beacon-KXZXYQHX.js} +1 -1
  10. package/dist/{calibration-WWHK73WU.js → calibration-V46G7JTY.js} +2 -2
  11. package/dist/{check-OLI6AUS6.js → check-OWAIWV23.js} +1 -1
  12. package/dist/{chunk-RP6TZYGE.js → chunk-2IO7JAG2.js} +1 -1
  13. package/dist/chunk-2T6BTYBN.js +712 -0
  14. package/dist/{chunk-CDMAMDSG.js → chunk-5VKJBNJL.js} +13 -5
  15. package/dist/{chunk-KB4XJWE3.js → chunk-6N3JTACN.js} +98 -437
  16. package/dist/chunk-7N7GSU6K.js +34 -0
  17. package/dist/chunk-A2L4TSLZ.js +526 -0
  18. package/dist/{chunk-P7XSBJE3.js → chunk-ABVQGRF7.js} +1 -1
  19. package/dist/{chunk-HIKKOCXY.js → chunk-EI32ZBE6.js} +1 -1
  20. package/dist/{chunk-QIOCFXDQ.js → chunk-EKGMAM62.js} +1 -1
  21. package/dist/chunk-EZ3GOCYC.js +132 -0
  22. package/dist/chunk-GGMI6C2L.js +1075 -0
  23. package/dist/{chunk-DS5QY37M.js → chunk-GTR2TBIJ.js} +247 -15
  24. package/dist/{chunk-QDXI2DHR.js → chunk-J2JEQRT3.js} +1 -1
  25. package/dist/{chunk-AKIMFN6I.js → chunk-JASGXLK3.js} +2 -2
  26. package/dist/chunk-KVDYJLTC.js +121 -0
  27. package/dist/{chunk-J4E6K5MG.js → chunk-LSRABQIY.js} +25 -1
  28. package/dist/chunk-MCMOGQMU.js +145 -0
  29. package/dist/{chunk-ZXMDA7VB.js → chunk-PDX44BCA.js} +1 -6
  30. package/dist/{chunk-SOBTKFSP.js → chunk-S2HO5MLR.js} +5 -0
  31. package/dist/{chunk-2SKXFXIT.js → chunk-S3ORKP3V.js} +10 -15
  32. package/dist/{chunk-ZMQA6SCO.js → chunk-S6MZ2IEX.js} +628 -228
  33. package/dist/chunk-TAIJOFOE.js +124 -0
  34. package/dist/{chunk-FS3WTUHY.js → chunk-TXESEO7Y.js} +6 -6
  35. package/dist/{chunk-7COU5S2Z.js → chunk-VL67H5IC.js} +1 -1
  36. package/dist/{chunk-QWA26UNO.js → chunk-WQITYKHM.js} +7 -7
  37. package/dist/{chunk-MW5DMGBB.js → chunk-YMDLDELF.js} +114 -55
  38. package/dist/{claude-63ISJAZK.js → claude-FRRWJSTJ.js} +1 -1
  39. package/dist/{claude-cli-ABML5RHX.js → claude-cli-XJLK2X4L.js} +1 -1
  40. package/dist/{claude-code-JRLMRPTO.js → claude-code-HTBA4XRB.js} +1 -1
  41. package/dist/{claude-code-teams-CAJBEFIZ.js → claude-code-teams-T4SP24MD.js} +1 -1
  42. package/dist/{conductor-HLWYWUVH.js → conductor-PGPDVIVE.js} +1 -1
  43. package/dist/{config-schema-3YNIFJCJ.js → config-schema-EA4XALGG.js} +4 -2
  44. package/dist/{constellation-FAGT45TU.js → constellation-A26CCGQS.js} +1 -1
  45. package/dist/{context-audit-557EO6PK.js → context-audit-RLO3ETRP.js} +8 -5
  46. package/dist/{cost-XEBADYFT.js → cost-BGM32XJU.js} +1 -1
  47. package/dist/{cost-UD3WPEKZ.js → cost-VI46A4XL.js} +1 -1
  48. package/dist/{cursor-cli-QUOOF2N4.js → cursor-cli-JVEZGHWQ.js} +1 -1
  49. package/dist/{cursorrules-3TKZ4E4R.js → cursorrules-HLIKJJZT.js} +1 -1
  50. package/dist/decision-loader-WWCLIQPJ.js +20 -0
  51. package/dist/{delete-RRK4RL6Y.js → delete-KBRPQLPC.js} +2 -2
  52. package/dist/{diff-IP5CIARP.js → diff-RQLLNAFI.js} +5 -5
  53. package/dist/{discipline-5F5OVTXB.js → discipline-FA4OZXIS.js} +1 -1
  54. package/dist/{dist-UXWV4OKX.js → dist-34NA5RS5.js} +1 -1
  55. package/dist/{dist-5QE2BB2B-X6DYVSUL.js → dist-5QE2BB2B-5S3T6Y3T.js} +1 -1
  56. package/dist/{dist-CM3MVWWW.js → dist-77JDTVAY.js} +1 -0
  57. package/dist/{dist-POMVY6WP.js → dist-QK4SQAK7.js} +1 -1
  58. package/dist/{dist-3RVKEJRT.js → dist-TA6LSC2Q.js} +1 -1
  59. package/dist/docs-LVLRPBAW.js +155 -0
  60. package/dist/docs-PBZB7LYP.js +89 -0
  61. package/dist/{doctor-GKZJU7QG.js → doctor-ULBOHEIC.js} +3 -3
  62. package/dist/{drift-YGT4LJ7Q.js → drift-R5NRKFHI.js} +1 -1
  63. package/dist/{echo-A6HD5UP7.js → echo-O2LY7CC2.js} +1 -1
  64. package/dist/{edit-4CLNN5JG.js → edit-R2HNLMOG.js} +2 -2
  65. package/dist/event-25OJKDCE.js +31 -0
  66. package/dist/{export-T7CMMJIB.js → export-IWVL7XLF.js} +1 -1
  67. package/dist/{flow-UFMPVOEM.js → flow-CRRVV3O3.js} +2 -2
  68. package/dist/{global-HHUJSBG5.js → global-3NG5JXUB.js} +1 -1
  69. package/dist/graduate-USAWGBJM.js +160 -0
  70. package/dist/{graph-YYUXI3F7.js → graph-VHUMAAS6.js} +2 -2
  71. package/dist/{graph-server-ZPXRSGCW.js → graph-server-YL22VBBN.js} +1 -1
  72. package/dist/{habits-RG5SVKXP.js → habits-OL5NGPXO.js} +3 -3
  73. package/dist/{history-CETCSUCP.js → history-WOWC573W.js} +1 -1
  74. package/dist/{hooks-TCUHQMPF.js → hooks-HFWSCGPV.js} +2 -2
  75. package/dist/index.js +307 -184
  76. package/dist/{integrity-MK2OP5TA.js → integrity-IHO4FZTS.js} +1 -1
  77. package/dist/{integrity-checker-J7YXRTBT.js → integrity-checker-PSKJA5SB.js} +1 -0
  78. package/dist/journal-loader-5EYSBFFY.js +18 -0
  79. package/dist/{lint-HYWGS3JJ.js → lint-K6CJGGPH.js} +1 -1
  80. package/dist/{list-IUCYPGMK.js → list-4YK7QKFF.js} +1 -1
  81. package/dist/{list-BTLFHSRC.js → list-ENR7Q4CR.js} +2 -2
  82. package/dist/{lore-loader-VTEEZDX3.js → lore-loader-7NO6N6FT.js} +4 -1
  83. package/dist/{lore-server-NOOAHKJX.js → lore-server-UNJY5KC3.js} +1 -1
  84. package/dist/{manual-AFJ2J2V3.js → manual-G6FISID5.js} +1 -1
  85. package/dist/mcp.js +3954 -479
  86. package/dist/{migrate-FQVGQNXZ.js → migrate-LS45DNEV.js} +2 -2
  87. package/dist/{migrate-assessments-JP6Q5KME.js → migrate-assessments-RGH4O6IX.js} +2 -2
  88. package/dist/nomination-engine-Q4XSXFKT.js +40 -0
  89. package/dist/notebook-YWIYGEHV.js +155 -0
  90. package/dist/{orchestrate-A226N6FC.js → orchestrate-XZA33TJC.js} +5 -5
  91. package/dist/peers-DEOUIZM6.js +82 -0
  92. package/dist/persona-UHAHIVST.js +390 -0
  93. package/dist/{pipeline-3G2FRAKM.js → pipeline-L4HCSBGN.js} +1 -1
  94. package/dist/{platform-server-KHL6ZPPN.js → platform-server-PMD57BEG.js} +264 -18
  95. package/dist/{plugin-update-checker-HMRPGY5Z.js → plugin-update-checker-ELOEEQYS.js} +1 -0
  96. package/dist/{portal-check-FF5EKZE5.js → portal-check-NPYGII2D.js} +2 -2
  97. package/dist/{portal-compliance-VU4NIFEN.js → portal-compliance-J7DGAPFX.js} +2 -2
  98. package/dist/{probe-7JK7IDNI.js → probe-MHL5HQZ2.js} +3 -3
  99. package/dist/{promote-XO63XMAN.js → promote-F6ZYZZAL.js} +2 -2
  100. package/dist/{providers-YNFSL6HK.js → providers-GK7PB2OL.js} +2 -2
  101. package/dist/{quiz-I75NU2QQ.js → quiz-M66SC7F7.js} +1 -1
  102. package/dist/{record-46CLR4OG.js → record-RA4WR2BO.js} +2 -2
  103. package/dist/{reindex-WIJMCJ4A.js → reindex-HRA2AUS6.js} +3 -2
  104. package/dist/{remember-4EUZKIIB.js → remember-HBWJ655S.js} +1 -1
  105. package/dist/{retag-KC4JVRLE.js → retag-3OLCVDEQ.js} +2 -2
  106. package/dist/{review-Q7M4CRB5.js → review-27ATYTD2.js} +2 -2
  107. package/dist/review-57QMURZV.js +334 -0
  108. package/dist/{ripple-RI3LOT6R.js → ripple-JPBXP5I3.js} +1 -1
  109. package/dist/{sentinel-UOIGJWHH.js → sentinel-4XIG4STA.js} +2 -2
  110. package/dist/{sentinel-bridge-APDXYAZS.js → sentinel-bridge-MDUXTQRL.js} +2 -2
  111. package/dist/{serve-JVXSRSUB.js → serve-FLTFTM3P.js} +2 -2
  112. package/dist/{serve-22A4XOIG.js → serve-INL7SNBK.js} +2 -2
  113. package/dist/{serve-2YJ6D2Y6.js → serve-KBSE36PL.js} +4 -4
  114. package/dist/{server-JV6UFGWZ.js → server-54SKYFFY.js} +2 -2
  115. package/dist/{server-RDLQ3DK7.js → server-XUOIO7E6.js} +1 -1
  116. package/dist/{setup-YNZJQLW7.js → setup-EDS27WUR.js} +1 -1
  117. package/dist/{setup-M2ZKLKNN.js → setup-KO5AFC4K.js} +2 -2
  118. package/dist/{shift-LNMKFYLR.js → shift-VFG23DLA.js} +16 -16
  119. package/dist/{show-P7GYO43X.js → show-5PV5KFJE.js} +2 -2
  120. package/dist/{show-PKZMYKRN.js → show-NQKYX6WQ.js} +1 -1
  121. package/dist/{snapshot-Y3COXK4T.js → snapshot-BK4RBPCG.js} +1 -1
  122. package/dist/{spawn-SSXZX45U.js → spawn-AW6GDECS.js} +3 -3
  123. package/dist/{status-KLHALGW4.js → status-WGIAQODY.js} +1 -1
  124. package/dist/{summary-5NQNOD3F.js → summary-NIRABMF5.js} +2 -2
  125. package/dist/{sweep-EZU3GU6S.js → sweep-QMHNSIY5.js} +2 -2
  126. package/dist/{switch-WYUMVNA5.js → switch-6EJPZDIA.js} +1 -1
  127. package/dist/{symphony-EYRGGVNE.js → symphony-4OCY36AI.js} +350 -29
  128. package/dist/{symphony-QWOEKZMC.js → symphony-B75X2MME.js} +20 -2
  129. package/dist/symphony-peers-2ZQYLRNI.js +34 -0
  130. package/dist/symphony-peers-OL7F6M5S.js +121 -0
  131. package/dist/symphony-relay-UJYUXN65.js +710 -0
  132. package/dist/{sync-ZM4Q3R4U.js → sync-VEHUH4OA.js} +3 -3
  133. package/dist/{sync-llms-JIPP3XX4.js → sync-llms-YHCFIE6X.js} +2 -2
  134. package/dist/{task-loader-7M2FCBX6.js → task-loader-LDYWQSLM.js} +1 -0
  135. package/dist/{team-HGLJXWQG.js → team-7HG7XK5C.js} +6 -6
  136. package/dist/{test-WTR5Q33E.js → test-566CP5KC.js} +1 -1
  137. package/dist/{thread-3WM7KKID.js → thread-N754I4D5.js} +1 -1
  138. package/dist/{timeline-ANC7LVDL.js → timeline-M3CICQFE.js} +2 -2
  139. package/dist/{triage-IZ4MDYNB.js → triage-HHYGT3HY.js} +1 -1
  140. package/dist/{tutorial-GC6QL4US.js → tutorial-KD22SUNO.js} +1 -1
  141. package/dist/university-content/courses/.purpose +66 -0
  142. package/dist/university-content/courses/para-401.json +146 -0
  143. package/dist/university-content/courses/para-501.json +151 -0
  144. package/dist/university-content/courses/para-601.json +608 -0
  145. package/dist/university-content/plsat/.purpose +6 -0
  146. package/dist/university-content/plsat/v2.0.json +2 -2
  147. package/dist/university-content/plsat/v3.0.json +563 -3
  148. package/dist/university-content/reference.json +91 -0
  149. package/dist/{upgrade-ANX3LVSA.js → upgrade-H5PF32BW.js} +2 -2
  150. package/dist/{validate-GD5XWILV.js → validate-CNKEKO6A.js} +1 -1
  151. package/dist/{validate-ITC5D6QG.js → validate-MB5ULIHS.js} +1 -1
  152. package/dist/{validate-ZVPNN4FL.js → validate-QH3LADM6.js} +1 -1
  153. package/dist/{watch-X64UK7K4.js → watch-2TKP5PVL.js} +3 -3
  154. package/dist/{watch-ERBEJUJW.js → watch-ZF4ML6CD.js} +2 -2
  155. package/dist/{wisdom-L2WC7J62.js → wisdom-AATMGNFA.js} +1 -1
  156. package/dist/work-log-loader-5L45XNYZ.js +14 -0
  157. package/dist/{workspace-UIUTHZTD.js → workspace-6E6OSRNU.js} +4 -4
  158. package/package.json +1 -1
  159. package/platform-ui/dist/assets/DocsSection-ByAgPzWV.js +1 -0
  160. package/platform-ui/dist/assets/DocsSection-CjdO6R-u.css +1 -0
  161. package/platform-ui/dist/assets/{GitSection-BD3Ze06e.js → GitSection-BLovj9yT.js} +1 -1
  162. package/platform-ui/dist/assets/{GraphSection-SglITfSs.js → GraphSection-C5PCPUFl.js} +1 -1
  163. package/platform-ui/dist/assets/{LoreSection-bR5Km4Fd.js → LoreSection-BftejTla.js} +1 -1
  164. package/platform-ui/dist/assets/{SentinelSection-QSpAZArG.js → SentinelSection-CnYcasN7.js} +1 -1
  165. package/platform-ui/dist/assets/{SymphonySection-CobYJgvg.js → SymphonySection-BpmqCHeK.js} +1 -1
  166. package/platform-ui/dist/assets/{index-DbxeSMkV.js → index-G9JnWEs_.js} +10 -10
  167. package/platform-ui/dist/index.html +1 -1
  168. package/dist/dist-PSF5CP4I.js +0 -7294
@@ -0,0 +1,710 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addPeer,
4
+ computeHmacProof,
5
+ generatePairing,
6
+ loadPeers,
7
+ updatePeerAgents,
8
+ updatePeerLastSeen,
9
+ verifyHmacProof,
10
+ verifyPairingCode
11
+ } from "./chunk-KVDYJLTC.js";
12
+ import {
13
+ appendToInbox,
14
+ isAgentAsleep,
15
+ listAgents,
16
+ readOutbox
17
+ } from "./chunk-S2HO5MLR.js";
18
+ import "./chunk-PDX44BCA.js";
19
+
20
+ // ../paradigm-mcp/src/utils/symphony-relay.ts
21
+ import * as fs from "fs";
22
+ import * as path from "path";
23
+ import * as os from "os";
24
+ import * as crypto from "crypto";
25
+ import { WebSocketServer, WebSocket } from "ws";
26
+ var SCORE_DIR = path.join(os.homedir(), ".paradigm", "score");
27
+ var OUTBOX_POLL_INTERVAL_MS = 2e3;
28
+ var KEEPALIVE_INTERVAL_MS = 3e4;
29
+ var PONG_TIMEOUT_MS = 1e4;
30
+ var MAX_AUTH_ATTEMPTS = 3;
31
+ var AUTH_COOLDOWN_MS = 6e4;
32
+ var RECONNECT_MIN_MS = 1e3;
33
+ var RECONNECT_MAX_MS = 3e4;
34
+ function sendFrame(ws, frame) {
35
+ if (ws.readyState === WebSocket.OPEN) {
36
+ ws.send(JSON.stringify(frame));
37
+ }
38
+ }
39
+ function parseFrame(data) {
40
+ try {
41
+ const text = typeof data === "string" ? data : String(data);
42
+ return JSON.parse(text);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ var SymphonyRelay = class _SymphonyRelay {
48
+ wss = null;
49
+ wsClient = null;
50
+ mode;
51
+ pairingState = null;
52
+ /** peerId → WebSocket for authenticated connections. */
53
+ connectedPeers = /* @__PURE__ */ new Map();
54
+ /** Bounded dedup set of message IDs already processed. */
55
+ seenMessageIds = /* @__PURE__ */ new Set();
56
+ outboxWatchInterval = null;
57
+ keepaliveInterval = null;
58
+ reconnectTimer = null;
59
+ reconnectDelay = RECONNECT_MIN_MS;
60
+ /** agentId → count of outbox lines already forwarded. */
61
+ outboxPositions = /* @__PURE__ */ new Map();
62
+ events;
63
+ myPeerId;
64
+ port;
65
+ stopped = false;
66
+ /** IP/address → { count, cooldownUntil } for rate limiting. */
67
+ failedAuthAttempts = /* @__PURE__ */ new Map();
68
+ /** Per-connection pong tracking: peerId → pending timeout handle. */
69
+ pongTimers = /* @__PURE__ */ new Map();
70
+ /** Server-side address stored for client reconnect. */
71
+ serverAddress = null;
72
+ /** Pairing code stored for client reconnect. */
73
+ serverCode = null;
74
+ /** Maximum number of message IDs to keep for dedup. */
75
+ static MAX_SEEN_IDS = 1e4;
76
+ constructor(options) {
77
+ this.mode = options.mode;
78
+ this.myPeerId = options.peerId;
79
+ this.port = options.port ?? 3939;
80
+ this.events = options.events ?? {};
81
+ }
82
+ // ────────────────────────────────────────────────────────
83
+ // Server Mode
84
+ // ────────────────────────────────────────────────────────
85
+ /**
86
+ * Start a WebSocket relay server (hub mode).
87
+ *
88
+ * Generates a fresh pairing state, binds to `this.port`, and begins
89
+ * accepting peer connections. Returns the pairing state so the caller
90
+ * can display the code to the user.
91
+ */
92
+ async startServer() {
93
+ if (this.mode !== "server") {
94
+ throw new Error('startServer() requires mode "server"');
95
+ }
96
+ this.pairingState = generatePairing();
97
+ this.wss = new WebSocketServer({ port: this.port });
98
+ this.wss.on("connection", (ws, req) => {
99
+ const remoteAddress = req.socket.remoteAddress ?? "unknown";
100
+ if (this.isRateLimited(remoteAddress)) {
101
+ sendFrame(ws, { type: "auth_fail", reason: "Too many failed attempts \u2014 try again later" });
102
+ ws.close();
103
+ return;
104
+ }
105
+ const challenge = crypto.randomBytes(32).toString("hex");
106
+ sendFrame(ws, {
107
+ type: "hello",
108
+ version: "1.0",
109
+ peerId: this.myPeerId,
110
+ challenge
111
+ });
112
+ let authenticated = false;
113
+ ws.on("message", (raw) => {
114
+ const frame = parseFrame(raw);
115
+ if (!frame) return;
116
+ if (!authenticated) {
117
+ this.handleServerAuth(ws, frame, challenge, remoteAddress).then((peerId) => {
118
+ if (peerId) {
119
+ authenticated = true;
120
+ this.registerPeerConnection(peerId, ws);
121
+ }
122
+ }).catch((err) => {
123
+ this.events.onError?.(err instanceof Error ? err : new Error(String(err)));
124
+ });
125
+ return;
126
+ }
127
+ this.handleAuthenticatedFrame(ws, frame);
128
+ });
129
+ ws.on("close", () => {
130
+ if (authenticated) {
131
+ this.handlePeerDisconnect(ws);
132
+ }
133
+ });
134
+ ws.on("error", (err) => {
135
+ this.events.onError?.(err);
136
+ });
137
+ });
138
+ this.wss.on("error", (err) => {
139
+ this.events.onError?.(err);
140
+ });
141
+ await new Promise((resolve, reject) => {
142
+ this.wss.on("listening", resolve);
143
+ this.wss.on("error", reject);
144
+ });
145
+ this.startOutboxWatcher();
146
+ this.startKeepalive();
147
+ return this.pairingState;
148
+ }
149
+ /**
150
+ * Process an auth frame from an incoming client connection.
151
+ * Returns the authenticated peerId on success, null on failure.
152
+ */
153
+ async handleServerAuth(ws, frame, challenge, remoteAddress) {
154
+ if (frame.type !== "auth") {
155
+ sendFrame(ws, { type: "auth_fail", reason: "Expected auth frame" });
156
+ ws.close();
157
+ return null;
158
+ }
159
+ if (!this.pairingState || !verifyPairingCode(this.pairingState, frame.code)) {
160
+ this.recordFailedAuth(remoteAddress);
161
+ const reason = "Invalid or expired pairing code";
162
+ sendFrame(ws, { type: "auth_fail", reason });
163
+ this.events.onPeerAuthFailed?.(remoteAddress, reason);
164
+ ws.close();
165
+ return null;
166
+ }
167
+ const codeHash = this.pairingState.codeHash;
168
+ if (!verifyHmacProof(challenge, codeHash, frame.proof)) {
169
+ this.recordFailedAuth(remoteAddress);
170
+ const reason = "HMAC proof verification failed";
171
+ sendFrame(ws, { type: "auth_fail", reason });
172
+ this.events.onPeerAuthFailed?.(remoteAddress, reason);
173
+ ws.close();
174
+ return null;
175
+ }
176
+ const localAgents = this.getLocalAgentSummaries();
177
+ const displayName = this.myPeerId;
178
+ sendFrame(ws, {
179
+ type: "auth_ok",
180
+ peerId: this.myPeerId,
181
+ displayName,
182
+ agents: localAgents
183
+ });
184
+ addPeer({
185
+ id: frame.peerId,
186
+ displayName: frame.peerId,
187
+ address: remoteAddress,
188
+ sharedSecret: this.pairingState.sharedSecret,
189
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
190
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
191
+ revoked: false,
192
+ agents: []
193
+ });
194
+ return frame.peerId;
195
+ }
196
+ // ────────────────────────────────────────────────────────
197
+ // Client Mode
198
+ // ────────────────────────────────────────────────────────
199
+ /**
200
+ * Connect to a remote relay server as a spoke.
201
+ *
202
+ * Resolves once authentication completes successfully.
203
+ * Rejects if the connection fails or auth is denied.
204
+ */
205
+ async connectToServer(address, code) {
206
+ if (this.mode !== "client") {
207
+ throw new Error('connectToServer() requires mode "client"');
208
+ }
209
+ this.serverAddress = address;
210
+ this.serverCode = code;
211
+ await this.attemptConnection(address, code);
212
+ }
213
+ /**
214
+ * Inner connection attempt — used for both initial connect and reconnects.
215
+ */
216
+ attemptConnection(address, code) {
217
+ return new Promise((resolve, reject) => {
218
+ if (this.stopped) {
219
+ reject(new Error("Relay has been stopped"));
220
+ return;
221
+ }
222
+ const wsUrl = address.includes("://") ? address : `ws://${address}`;
223
+ const ws = new WebSocket(wsUrl);
224
+ let settled = false;
225
+ ws.on("open", () => {
226
+ this.wsClient = ws;
227
+ });
228
+ ws.on("message", (raw) => {
229
+ const frame = parseFrame(raw);
230
+ if (!frame) return;
231
+ switch (frame.type) {
232
+ case "hello": {
233
+ const codeHash = crypto.createHash("sha256").update(code).digest("hex");
234
+ const proof = computeHmacProof(frame.challenge, codeHash);
235
+ sendFrame(ws, {
236
+ type: "auth",
237
+ peerId: this.myPeerId,
238
+ code,
239
+ proof
240
+ });
241
+ break;
242
+ }
243
+ case "auth_ok": {
244
+ addPeer({
245
+ id: frame.peerId,
246
+ displayName: frame.displayName,
247
+ address,
248
+ sharedSecret: code,
249
+ // Store code for reconnect
250
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
251
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
252
+ revoked: false,
253
+ agents: frame.agents
254
+ });
255
+ sendFrame(ws, {
256
+ type: "agents_sync",
257
+ agents: this.getLocalAgentSummaries()
258
+ });
259
+ this.registerPeerConnection(frame.peerId, ws);
260
+ this.startOutboxWatcher();
261
+ this.startKeepalive();
262
+ this.reconnectDelay = RECONNECT_MIN_MS;
263
+ if (!settled) {
264
+ settled = true;
265
+ resolve();
266
+ }
267
+ break;
268
+ }
269
+ case "auth_fail": {
270
+ if (!settled) {
271
+ settled = true;
272
+ reject(new Error(`Auth failed: ${frame.reason}`));
273
+ }
274
+ ws.close();
275
+ break;
276
+ }
277
+ default:
278
+ this.handleAuthenticatedFrame(ws, frame);
279
+ break;
280
+ }
281
+ });
282
+ ws.on("close", () => {
283
+ this.handlePeerDisconnect(ws);
284
+ if (!settled) {
285
+ settled = true;
286
+ reject(new Error("Connection closed before auth completed"));
287
+ } else if (!this.stopped) {
288
+ this.scheduleReconnect();
289
+ }
290
+ });
291
+ ws.on("error", (err) => {
292
+ this.events.onError?.(err);
293
+ if (!settled) {
294
+ settled = true;
295
+ reject(err);
296
+ }
297
+ });
298
+ });
299
+ }
300
+ // ────────────────────────────────────────────────────────
301
+ // Authenticated Frame Handler
302
+ // ────────────────────────────────────────────────────────
303
+ /**
304
+ * Dispatch a frame received on an authenticated connection.
305
+ */
306
+ handleAuthenticatedFrame(ws, frame) {
307
+ switch (frame.type) {
308
+ case "message":
309
+ this.handleIncomingMessage(ws, frame.message, frame.origin);
310
+ break;
311
+ case "message_ack":
312
+ break;
313
+ case "nomination_forward":
314
+ this.handleNominationForward(ws, frame.nomination, frame.origin);
315
+ break;
316
+ case "agents_sync":
317
+ this.handleAgentsSync(ws, frame.agents);
318
+ break;
319
+ case "agent_joined": {
320
+ const peerId = this.peerIdForSocket(ws);
321
+ if (peerId) {
322
+ const peers = loadPeers();
323
+ const peer = peers.find((p) => p.id === peerId);
324
+ if (peer) {
325
+ const agents = [...peer.agents || [], frame.agent];
326
+ updatePeerAgents(peerId, agents);
327
+ }
328
+ }
329
+ break;
330
+ }
331
+ case "agent_left": {
332
+ const peerId = this.peerIdForSocket(ws);
333
+ if (peerId) {
334
+ const peers = loadPeers();
335
+ const peer = peers.find((p) => p.id === peerId);
336
+ if (peer) {
337
+ const agents = (peer.agents || []).filter((a) => a.id !== frame.agentId);
338
+ updatePeerAgents(peerId, agents);
339
+ }
340
+ }
341
+ break;
342
+ }
343
+ case "peer_leaving":
344
+ this.handlePeerDisconnect(ws);
345
+ ws.close();
346
+ break;
347
+ case "ping":
348
+ sendFrame(ws, { type: "pong" });
349
+ break;
350
+ case "pong":
351
+ this.handlePong(ws);
352
+ break;
353
+ default:
354
+ break;
355
+ }
356
+ }
357
+ // ────────────────────────────────────────────────────────
358
+ // Message Handling
359
+ // ────────────────────────────────────────────────────────
360
+ /**
361
+ * Process an incoming relayed message.
362
+ *
363
+ * 1. Dedup check — skip if already seen
364
+ * 2. Deliver to matching local agents
365
+ * 3. In server mode, relay to other connected peers (not the sender)
366
+ * 4. Send ack back to the sender
367
+ */
368
+ handleIncomingMessage(senderWs, message, origin) {
369
+ if (this.seenMessageIds.has(message.id)) {
370
+ sendFrame(senderWs, { type: "message_ack", messageId: message.id });
371
+ return;
372
+ }
373
+ this.addToSeenIds(message.id);
374
+ const localAgents = listAgents();
375
+ if (message.recipients && message.recipients.length > 0) {
376
+ for (const recipient of message.recipients) {
377
+ const localMatch = localAgents.find((a) => a.id === recipient.id);
378
+ if (localMatch) {
379
+ appendToInbox(localMatch.id, message);
380
+ this.events.onMessageRelayed?.(message.id, origin, localMatch.id);
381
+ }
382
+ }
383
+ } else {
384
+ for (const agent of localAgents) {
385
+ appendToInbox(agent.id, message);
386
+ this.events.onMessageRelayed?.(message.id, origin, agent.id);
387
+ }
388
+ }
389
+ if (this.mode === "server") {
390
+ const senderPeerId = this.peerIdForSocket(senderWs);
391
+ for (const [peerId, peerWs] of this.connectedPeers) {
392
+ if (peerId !== senderPeerId && peerId !== origin) {
393
+ sendFrame(peerWs, { type: "message", message, origin });
394
+ }
395
+ }
396
+ }
397
+ sendFrame(senderWs, { type: "message_ack", messageId: message.id });
398
+ }
399
+ /**
400
+ * Forward a nomination from a remote agent to local nomination storage.
401
+ * Remote nominations are stored in the same nominations.jsonl but tagged with origin.
402
+ */
403
+ handleNominationForward(senderWs, nomination, origin) {
404
+ if (!nomination?.id) return;
405
+ const tagged = { ...nomination, remote_origin: origin, forwarded_at: (/* @__PURE__ */ new Date()).toISOString() };
406
+ try {
407
+ const eventsDir = path.join(os.homedir(), ".paradigm", "events");
408
+ fs.mkdirSync(eventsDir, { recursive: true });
409
+ const nomPath = path.join(eventsDir, "nominations.jsonl");
410
+ fs.appendFileSync(nomPath, JSON.stringify(tagged) + "\n", "utf8");
411
+ } catch {
412
+ }
413
+ if (this.mode === "server") {
414
+ for (const [peerId, peerWs] of this.connectedPeers) {
415
+ if (peerWs !== senderWs && peerWs.readyState === WebSocket.OPEN) {
416
+ sendFrame(peerWs, { type: "nomination_forward", nomination: tagged, origin });
417
+ }
418
+ }
419
+ }
420
+ sendFrame(senderWs, { type: "nomination_ack", nominationId: nomination.id });
421
+ }
422
+ /**
423
+ * Update stored agent list for a peer after receiving agents_sync.
424
+ */
425
+ handleAgentsSync(ws, agents) {
426
+ const peerId = this.peerIdForSocket(ws);
427
+ if (peerId) {
428
+ updatePeerAgents(peerId, agents);
429
+ updatePeerLastSeen(peerId);
430
+ }
431
+ }
432
+ // ────────────────────────────────────────────────────────
433
+ // Outbox Watcher
434
+ // ────────────────────────────────────────────────────────
435
+ /**
436
+ * Start polling local agent outboxes for new messages to relay.
437
+ *
438
+ * Reads each outbox as an array, compares length against the stored
439
+ * position, and forwards any new entries to all connected peers.
440
+ */
441
+ startOutboxWatcher() {
442
+ if (this.outboxWatchInterval) return;
443
+ this.outboxWatchInterval = setInterval(() => {
444
+ if (this.connectedPeers.size === 0) return;
445
+ try {
446
+ const agents = listAgents();
447
+ for (const agent of agents) {
448
+ const messages = readOutbox(agent.id);
449
+ const lastPosition = this.outboxPositions.get(agent.id) ?? 0;
450
+ if (messages.length <= lastPosition) continue;
451
+ const newMessages = messages.slice(lastPosition);
452
+ for (const msg of newMessages) {
453
+ if (this.seenMessageIds.has(msg.id)) continue;
454
+ this.addToSeenIds(msg.id);
455
+ const frame = {
456
+ type: "message",
457
+ message: msg,
458
+ origin: this.myPeerId
459
+ };
460
+ for (const [_peerId, peerWs] of this.connectedPeers) {
461
+ sendFrame(peerWs, frame);
462
+ }
463
+ }
464
+ this.outboxPositions.set(agent.id, messages.length);
465
+ }
466
+ } catch (err) {
467
+ this.events.onError?.(err instanceof Error ? err : new Error(String(err)));
468
+ }
469
+ }, OUTBOX_POLL_INTERVAL_MS);
470
+ }
471
+ /**
472
+ * Stop the outbox watcher.
473
+ */
474
+ stopOutboxWatcher() {
475
+ if (this.outboxWatchInterval) {
476
+ clearInterval(this.outboxWatchInterval);
477
+ this.outboxWatchInterval = null;
478
+ }
479
+ }
480
+ // ────────────────────────────────────────────────────────
481
+ // Keepalive
482
+ // ────────────────────────────────────────────────────────
483
+ /**
484
+ * Start periodic ping/pong keepalive for all connected peers.
485
+ */
486
+ startKeepalive() {
487
+ if (this.keepaliveInterval) return;
488
+ this.keepaliveInterval = setInterval(() => {
489
+ for (const [peerId, ws] of this.connectedPeers) {
490
+ sendFrame(ws, { type: "ping" });
491
+ const timer = setTimeout(() => {
492
+ this.handlePeerDisconnect(ws);
493
+ ws.terminate();
494
+ }, PONG_TIMEOUT_MS);
495
+ this.pongTimers.set(peerId, timer);
496
+ }
497
+ }, KEEPALIVE_INTERVAL_MS);
498
+ }
499
+ /**
500
+ * Stop keepalive pings.
501
+ */
502
+ stopKeepalive() {
503
+ if (this.keepaliveInterval) {
504
+ clearInterval(this.keepaliveInterval);
505
+ this.keepaliveInterval = null;
506
+ }
507
+ for (const timer of this.pongTimers.values()) {
508
+ clearTimeout(timer);
509
+ }
510
+ this.pongTimers.clear();
511
+ }
512
+ /**
513
+ * Handle a pong response — clear the dead-connection timer.
514
+ */
515
+ handlePong(ws) {
516
+ const peerId = this.peerIdForSocket(ws);
517
+ if (peerId) {
518
+ const timer = this.pongTimers.get(peerId);
519
+ if (timer) {
520
+ clearTimeout(timer);
521
+ this.pongTimers.delete(peerId);
522
+ }
523
+ updatePeerLastSeen(peerId);
524
+ }
525
+ }
526
+ // ────────────────────────────────────────────────────────
527
+ // Peer Lifecycle
528
+ // ────────────────────────────────────────────────────────
529
+ /**
530
+ * Register an authenticated peer connection.
531
+ */
532
+ registerPeerConnection(peerId, ws) {
533
+ const existing = this.connectedPeers.get(peerId);
534
+ if (existing && existing !== ws) {
535
+ existing.close();
536
+ }
537
+ this.connectedPeers.set(peerId, ws);
538
+ updatePeerLastSeen(peerId);
539
+ this.events.onPeerConnected?.(peerId, peerId);
540
+ }
541
+ /**
542
+ * Clean up after a peer disconnects (or is terminated).
543
+ */
544
+ handlePeerDisconnect(ws) {
545
+ const peerId = this.peerIdForSocket(ws);
546
+ if (!peerId) return;
547
+ this.connectedPeers.delete(peerId);
548
+ const timer = this.pongTimers.get(peerId);
549
+ if (timer) {
550
+ clearTimeout(timer);
551
+ this.pongTimers.delete(peerId);
552
+ }
553
+ this.events.onPeerDisconnected?.(peerId);
554
+ }
555
+ /**
556
+ * Find the peerId associated with a WebSocket connection.
557
+ */
558
+ peerIdForSocket(ws) {
559
+ for (const [peerId, peerWs] of this.connectedPeers) {
560
+ if (peerWs === ws) return peerId;
561
+ }
562
+ return null;
563
+ }
564
+ // ────────────────────────────────────────────────────────
565
+ // Reconnect (Client Mode)
566
+ // ────────────────────────────────────────────────────────
567
+ /**
568
+ * Schedule an automatic reconnect with exponential backoff.
569
+ */
570
+ scheduleReconnect() {
571
+ if (this.stopped || this.mode !== "client") return;
572
+ if (!this.serverAddress || !this.serverCode) return;
573
+ this.stopOutboxWatcher();
574
+ this.stopKeepalive();
575
+ this.wsClient = null;
576
+ const delay = this.reconnectDelay;
577
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
578
+ this.reconnectTimer = setTimeout(() => {
579
+ if (this.stopped) return;
580
+ this.attemptConnection(this.serverAddress, this.serverCode).catch((err) => {
581
+ this.events.onError?.(err instanceof Error ? err : new Error(String(err)));
582
+ });
583
+ }, delay);
584
+ }
585
+ // ────────────────────────────────────────────────────────
586
+ // Rate Limiting
587
+ // ────────────────────────────────────────────────────────
588
+ /**
589
+ * Check whether an address is currently rate-limited.
590
+ */
591
+ isRateLimited(address) {
592
+ const entry = this.failedAuthAttempts.get(address);
593
+ if (!entry) return false;
594
+ if (Date.now() < entry.cooldownUntil) return true;
595
+ if (entry.count >= MAX_AUTH_ATTEMPTS) {
596
+ entry.cooldownUntil = Date.now() + AUTH_COOLDOWN_MS;
597
+ return true;
598
+ }
599
+ return false;
600
+ }
601
+ /**
602
+ * Record a failed auth attempt from a given address.
603
+ */
604
+ recordFailedAuth(address) {
605
+ const entry = this.failedAuthAttempts.get(address);
606
+ if (entry) {
607
+ entry.count++;
608
+ } else {
609
+ this.failedAuthAttempts.set(address, { count: 1, cooldownUntil: 0 });
610
+ }
611
+ }
612
+ // ────────────────────────────────────────────────────────
613
+ // Dedup
614
+ // ────────────────────────────────────────────────────────
615
+ /**
616
+ * Add a message ID to the dedup set, evicting the oldest half when
617
+ * the set exceeds {@link MAX_SEEN_IDS}.
618
+ */
619
+ addToSeenIds(messageId) {
620
+ this.seenMessageIds.add(messageId);
621
+ if (this.seenMessageIds.size > _SymphonyRelay.MAX_SEEN_IDS) {
622
+ const entries = Array.from(this.seenMessageIds);
623
+ const keepFrom = Math.floor(entries.length / 2);
624
+ this.seenMessageIds = new Set(entries.slice(keepFrom));
625
+ }
626
+ }
627
+ // ────────────────────────────────────────────────────────
628
+ // Local Agent Helpers
629
+ // ────────────────────────────────────────────────────────
630
+ /**
631
+ * Build a summary list of all locally registered agents.
632
+ */
633
+ getLocalAgentSummaries() {
634
+ return listAgents().map((a) => ({
635
+ id: a.id,
636
+ project: a.project,
637
+ role: a.role,
638
+ status: isAgentAsleep(a) ? "asleep" : "awake"
639
+ }));
640
+ }
641
+ // ────────────────────────────────────────────────────────
642
+ // Public API
643
+ // ────────────────────────────────────────────────────────
644
+ /**
645
+ * Gracefully shut down the relay.
646
+ *
647
+ * Sends `peer_leaving` to all connected peers, closes every WebSocket,
648
+ * clears all timers, and shuts down the server (if running).
649
+ */
650
+ stop() {
651
+ this.stopped = true;
652
+ for (const [_peerId, ws] of this.connectedPeers) {
653
+ sendFrame(ws, { type: "peer_leaving" });
654
+ ws.close();
655
+ }
656
+ this.connectedPeers.clear();
657
+ if (this.wsClient) {
658
+ this.wsClient.close();
659
+ this.wsClient = null;
660
+ }
661
+ this.stopOutboxWatcher();
662
+ this.stopKeepalive();
663
+ if (this.reconnectTimer) {
664
+ clearTimeout(this.reconnectTimer);
665
+ this.reconnectTimer = null;
666
+ }
667
+ if (this.wss) {
668
+ this.wss.close();
669
+ this.wss = null;
670
+ }
671
+ }
672
+ /**
673
+ * Return the list of currently connected peer IDs.
674
+ */
675
+ getConnectedPeers() {
676
+ return Array.from(this.connectedPeers.keys());
677
+ }
678
+ /**
679
+ * Return the aggregated agent summaries from all connected peers.
680
+ */
681
+ getRemoteAgents() {
682
+ const result = [];
683
+ const peers = loadPeers();
684
+ for (const peerId of this.connectedPeers.keys()) {
685
+ const peer = peers.find((p) => p.id === peerId);
686
+ if (peer?.agents) {
687
+ for (const agent of peer.agents) {
688
+ result.push({ ...agent, peerId });
689
+ }
690
+ }
691
+ }
692
+ return result;
693
+ }
694
+ /**
695
+ * Generate a new pairing code, invalidating the previous one.
696
+ *
697
+ * Only meaningful in server mode — clients don't generate pairing codes.
698
+ */
699
+ rotatePairingCode() {
700
+ if (this.mode !== "server") {
701
+ throw new Error('rotatePairingCode() requires mode "server"');
702
+ }
703
+ this.pairingState = generatePairing();
704
+ return this.pairingState;
705
+ }
706
+ };
707
+ export {
708
+ SCORE_DIR,
709
+ SymphonyRelay
710
+ };