@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5

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 (347) hide show
  1. package/CHANGELOG.md +1654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1533 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +199 -0
  43. package/dist/history/ingest.js +468 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +466 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/bridge.js +750 -0
  53. package/dist/pi-intercom/broker/broker.js +426 -0
  54. package/dist/pi-intercom/broker/client.js +492 -0
  55. package/dist/pi-intercom/broker/framing.js +65 -0
  56. package/dist/pi-intercom/broker/paths.js +42 -0
  57. package/dist/pi-intercom/broker/pid.js +66 -0
  58. package/dist/pi-intercom/broker/spawn.js +221 -0
  59. package/dist/pi-intercom/config.js +102 -0
  60. package/dist/pi-intercom/index.js +2942 -0
  61. package/dist/pi-intercom/reply-tracker.js +182 -0
  62. package/dist/pi-intercom/session-targets.js +128 -0
  63. package/dist/pi-intercom/subagent-live-events.js +132 -0
  64. package/dist/pi-intercom/topics.js +157 -0
  65. package/dist/pi-intercom/types.js +163 -0
  66. package/dist/pi-intercom/ui/compose.js +269 -0
  67. package/dist/pi-intercom/ui/inline-message.js +80 -0
  68. package/dist/pi-intercom/ui/session-list.js +124 -0
  69. package/dist/runs/background/async-control.js +95 -0
  70. package/dist/runs/background/async-execution.js +676 -0
  71. package/dist/runs/background/async-job-tracker.js +383 -0
  72. package/dist/runs/background/async-result-file.js +58 -0
  73. package/dist/runs/background/async-resume.js +305 -0
  74. package/dist/runs/background/async-status.js +442 -0
  75. package/dist/runs/background/completion-dedupe.js +69 -0
  76. package/dist/runs/background/completion-delivery.js +245 -0
  77. package/dist/runs/background/notify.js +71 -0
  78. package/dist/runs/background/parallel-groups.js +55 -0
  79. package/dist/runs/background/result-watcher.js +426 -0
  80. package/dist/runs/background/run-id-resolver.js +64 -0
  81. package/dist/runs/background/run-status.js +382 -0
  82. package/dist/runs/background/stale-run-reconciler.js +335 -0
  83. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  84. package/dist/runs/background/subagent-runner.js +1872 -0
  85. package/dist/runs/background/top-level-async.js +5 -0
  86. package/dist/runs/foreground/chain-clarify.js +1152 -0
  87. package/dist/runs/foreground/clarify-invocation.js +72 -0
  88. package/dist/runs/foreground/execution-input.js +423 -0
  89. package/dist/runs/foreground/foreground-control.js +817 -0
  90. package/dist/runs/foreground/question-control.js +88 -0
  91. package/dist/runs/foreground/run-async-path.js +205 -0
  92. package/dist/runs/foreground/subagent-executor.js +442 -0
  93. package/dist/runs/foreground/subagent-params.js +165 -0
  94. package/dist/runs/foreground/wait-run.js +141 -0
  95. package/dist/runs/shared/acceptance-contract.js +253 -0
  96. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  97. package/dist/runs/shared/acceptance-finalization.js +280 -0
  98. package/dist/runs/shared/acceptance-reports.js +147 -0
  99. package/dist/runs/shared/acceptance.js +4 -0
  100. package/dist/runs/shared/chain-outputs.js +110 -0
  101. package/dist/runs/shared/child-attempt.js +693 -0
  102. package/dist/runs/shared/child-execution-cwd.js +96 -0
  103. package/dist/runs/shared/child-observations.js +114 -0
  104. package/dist/runs/shared/claude-code.js +199 -0
  105. package/dist/runs/shared/completion-guard.js +31 -0
  106. package/dist/runs/shared/dynamic-fanout.js +274 -0
  107. package/dist/runs/shared/history-index.js +72 -0
  108. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  109. package/dist/runs/shared/model-fallback.js +185 -0
  110. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  111. package/dist/runs/shared/native-finalization.js +101 -0
  112. package/dist/runs/shared/native-usage.js +153 -0
  113. package/dist/runs/shared/nested-events.js +700 -0
  114. package/dist/runs/shared/nested-path.js +48 -0
  115. package/dist/runs/shared/nested-render.js +108 -0
  116. package/dist/runs/shared/parallel-utils.js +61 -0
  117. package/dist/runs/shared/parent-receipts.js +68 -0
  118. package/dist/runs/shared/parent-usage.js +121 -0
  119. package/dist/runs/shared/pi-args.js +310 -0
  120. package/dist/runs/shared/pi-spawn.js +52 -0
  121. package/dist/runs/shared/run-history-queries.js +45 -0
  122. package/dist/runs/shared/run-history.js +110 -0
  123. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  124. package/dist/runs/shared/run-records.js +679 -0
  125. package/dist/runs/shared/session-cwd-preload.js +33 -0
  126. package/dist/runs/shared/single-output.js +222 -0
  127. package/dist/runs/shared/streaming-text.js +15 -0
  128. package/dist/runs/shared/structured-output.js +55 -0
  129. package/dist/runs/shared/subagent-control.js +179 -0
  130. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  131. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  132. package/dist/runs/shared/supervisor-questions.js +350 -0
  133. package/dist/runs/shared/workflow-graph.js +214 -0
  134. package/dist/runs/shared/workflow-policy.js +84 -0
  135. package/dist/runs/shared/worktree.js +525 -0
  136. package/dist/shared/agent-context-policy.js +126 -0
  137. package/dist/shared/artifacts.js +96 -0
  138. package/dist/shared/atomic-json.js +13 -0
  139. package/dist/shared/cooperative.js +20 -0
  140. package/dist/shared/execution-cwd.js +14 -0
  141. package/dist/shared/file-coalescer.js +21 -0
  142. package/dist/shared/fork-context.js +49 -0
  143. package/dist/shared/formatters.js +85 -0
  144. package/dist/shared/journal-reader.js +407 -0
  145. package/dist/shared/lazy-tools.js +32 -0
  146. package/dist/shared/model-info.js +68 -0
  147. package/dist/shared/native-session.js +15 -0
  148. package/dist/shared/native-tui.js +18 -0
  149. package/dist/shared/native-typebox.js +20 -0
  150. package/dist/shared/post-exit-stdio-guard.js +197 -0
  151. package/dist/shared/prompt-sections.js +8 -0
  152. package/dist/shared/session-entries.js +52 -0
  153. package/dist/shared/session-identity.js +14 -0
  154. package/dist/shared/settings.js +254 -0
  155. package/dist/shared/status-format.js +111 -0
  156. package/dist/shared/temp-root.js +114 -0
  157. package/dist/shared/types.js +207 -0
  158. package/dist/shared/ui-mode.js +3 -0
  159. package/dist/shared/utils.js +334 -0
  160. package/dist/slash/prompt-template-bridge.js +298 -0
  161. package/dist/slash/slash-bridge.js +116 -0
  162. package/dist/slash/slash-commands.js +607 -0
  163. package/dist/slash/slash-live-state.js +246 -0
  164. package/dist/tui/action-hints.js +75 -0
  165. package/dist/tui/agent-history.js +197 -0
  166. package/dist/tui/agent-view.js +1656 -0
  167. package/dist/tui/render.js +1515 -0
  168. package/docs/intercom-bridge.md +212 -0
  169. package/docs/intercom.md +581 -0
  170. package/docs/performance.md +121 -0
  171. package/package.json +102 -4
  172. package/prompts/gather-context-and-clarify.md +14 -0
  173. package/prompts/parallel-cleanup.md +62 -0
  174. package/prompts/parallel-context-build.md +56 -0
  175. package/prompts/parallel-handoff-plan.md +62 -0
  176. package/prompts/parallel-research.md +51 -0
  177. package/prompts/parallel-review.md +59 -0
  178. package/prompts/review-loop.md +42 -0
  179. package/scripts/build.mjs +134 -0
  180. package/scripts/check-compat.mjs +48 -0
  181. package/scripts/compat-host.mjs +19 -0
  182. package/scripts/intercom-bridge-credentials.mjs +143 -0
  183. package/scripts/intercom-remote.mjs +103 -0
  184. package/scripts/linux-smoke.sh +51 -0
  185. package/scripts/local-install-smoke.mjs +66 -0
  186. package/scripts/native-package-smoke.mjs +82 -0
  187. package/scripts/package-smoke.mjs +209 -0
  188. package/scripts/prepare.mjs +67 -0
  189. package/scripts/real-pi-smoke.mjs +415 -0
  190. package/scripts/run-tests.mjs +149 -0
  191. package/skills/pi-intercom/SKILL.md +113 -0
  192. package/skills/pi-intercom/evals/evals.json +29 -0
  193. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  194. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  195. package/skills/pi-subagents/SKILL.md +76 -0
  196. package/skills/pi-subagents/evals/evals.json +29 -0
  197. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  198. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  199. package/src/agents/agent-management.ts +813 -0
  200. package/src/agents/agent-scope.ts +7 -0
  201. package/src/agents/agent-selection.ts +23 -0
  202. package/src/agents/agent-serializer.ts +99 -0
  203. package/src/agents/agents.ts +916 -0
  204. package/src/agents/chain-serializer.ts +329 -0
  205. package/src/agents/frontmatter.ts +21 -0
  206. package/src/agents/identity.ts +30 -0
  207. package/src/agents/skills.ts +637 -0
  208. package/src/extension/build-info.ts +2 -0
  209. package/src/extension/compact-tools.ts +114 -0
  210. package/src/extension/config.ts +23 -0
  211. package/src/extension/control-notices.ts +47 -0
  212. package/src/extension/doctor.ts +171 -0
  213. package/src/extension/fanout-child.ts +312 -0
  214. package/src/extension/index.ts +649 -0
  215. package/src/extension/schemas.ts +444 -0
  216. package/src/extension/tool-input.ts +19 -0
  217. package/src/extension/tool-result.ts +16 -0
  218. package/src/history/canonical-result.ts +62 -0
  219. package/src/history/database.ts +59 -0
  220. package/src/history/index.ts +156 -0
  221. package/src/history/ingest.ts +349 -0
  222. package/src/history/queries.ts +241 -0
  223. package/src/history/store.ts +203 -0
  224. package/src/history/types.ts +166 -0
  225. package/src/history/worker.ts +321 -0
  226. package/src/intercom/intercom-bridge.ts +123 -0
  227. package/src/intercom/live-intercom.ts +104 -0
  228. package/src/intercom/result-intercom.ts +429 -0
  229. package/src/pi-intercom/agent-dir.ts +9 -0
  230. package/src/pi-intercom/bridge.ts +668 -0
  231. package/src/pi-intercom/broker/broker.ts +463 -0
  232. package/src/pi-intercom/broker/client.ts +558 -0
  233. package/src/pi-intercom/broker/framing.ts +77 -0
  234. package/src/pi-intercom/broker/paths.ts +47 -0
  235. package/src/pi-intercom/broker/pid.ts +59 -0
  236. package/src/pi-intercom/broker/spawn.ts +248 -0
  237. package/src/pi-intercom/config.ts +142 -0
  238. package/src/pi-intercom/index.ts +2929 -0
  239. package/src/pi-intercom/reply-tracker.ts +211 -0
  240. package/src/pi-intercom/session-targets.ts +172 -0
  241. package/src/pi-intercom/subagent-live-events.ts +133 -0
  242. package/src/pi-intercom/topics.ts +125 -0
  243. package/src/pi-intercom/types.ts +271 -0
  244. package/src/pi-intercom/ui/compose.ts +289 -0
  245. package/src/pi-intercom/ui/inline-message.ts +92 -0
  246. package/src/pi-intercom/ui/session-list.ts +139 -0
  247. package/src/runs/background/async-control.ts +89 -0
  248. package/src/runs/background/async-execution.ts +850 -0
  249. package/src/runs/background/async-job-tracker.ts +385 -0
  250. package/src/runs/background/async-result-file.ts +62 -0
  251. package/src/runs/background/async-resume.ts +333 -0
  252. package/src/runs/background/async-status.ts +481 -0
  253. package/src/runs/background/completion-dedupe.ts +79 -0
  254. package/src/runs/background/completion-delivery.ts +195 -0
  255. package/src/runs/background/notify.ts +123 -0
  256. package/src/runs/background/parallel-groups.ts +53 -0
  257. package/src/runs/background/result-watcher.ts +423 -0
  258. package/src/runs/background/run-id-resolver.ts +69 -0
  259. package/src/runs/background/run-status.ts +384 -0
  260. package/src/runs/background/stale-run-reconciler.ts +369 -0
  261. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  262. package/src/runs/background/subagent-runner.ts +2053 -0
  263. package/src/runs/background/top-level-async.ts +13 -0
  264. package/src/runs/foreground/chain-clarify.ts +1340 -0
  265. package/src/runs/foreground/clarify-invocation.ts +77 -0
  266. package/src/runs/foreground/execution-input.ts +467 -0
  267. package/src/runs/foreground/foreground-control.ts +821 -0
  268. package/src/runs/foreground/question-control.ts +82 -0
  269. package/src/runs/foreground/run-async-path.ts +243 -0
  270. package/src/runs/foreground/subagent-executor.ts +501 -0
  271. package/src/runs/foreground/subagent-params.ts +320 -0
  272. package/src/runs/foreground/wait-run.ts +124 -0
  273. package/src/runs/shared/acceptance-contract.ts +257 -0
  274. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  275. package/src/runs/shared/acceptance-finalization.ts +353 -0
  276. package/src/runs/shared/acceptance-reports.ts +152 -0
  277. package/src/runs/shared/acceptance.ts +33 -0
  278. package/src/runs/shared/chain-outputs.ts +114 -0
  279. package/src/runs/shared/child-attempt.ts +623 -0
  280. package/src/runs/shared/child-execution-cwd.ts +91 -0
  281. package/src/runs/shared/child-observations.ts +99 -0
  282. package/src/runs/shared/claude-code.ts +243 -0
  283. package/src/runs/shared/completion-guard.ts +35 -0
  284. package/src/runs/shared/dynamic-fanout.ts +295 -0
  285. package/src/runs/shared/history-index.ts +69 -0
  286. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  287. package/src/runs/shared/model-fallback.ts +219 -0
  288. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  289. package/src/runs/shared/native-finalization.ts +128 -0
  290. package/src/runs/shared/native-usage.ts +113 -0
  291. package/src/runs/shared/nested-events.ts +752 -0
  292. package/src/runs/shared/nested-path.ts +52 -0
  293. package/src/runs/shared/nested-render.ts +119 -0
  294. package/src/runs/shared/parallel-utils.ts +153 -0
  295. package/src/runs/shared/parent-receipts.ts +59 -0
  296. package/src/runs/shared/parent-usage.ts +121 -0
  297. package/src/runs/shared/pi-args.ts +350 -0
  298. package/src/runs/shared/pi-spawn.ts +52 -0
  299. package/src/runs/shared/run-history-queries.ts +44 -0
  300. package/src/runs/shared/run-history.ts +100 -0
  301. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  302. package/src/runs/shared/run-records.ts +607 -0
  303. package/src/runs/shared/session-cwd-preload.ts +27 -0
  304. package/src/runs/shared/single-output.ts +267 -0
  305. package/src/runs/shared/streaming-text.ts +18 -0
  306. package/src/runs/shared/structured-output.ts +68 -0
  307. package/src/runs/shared/subagent-control.ts +219 -0
  308. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  309. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  310. package/src/runs/shared/supervisor-questions.ts +415 -0
  311. package/src/runs/shared/workflow-graph.ts +220 -0
  312. package/src/runs/shared/workflow-policy.ts +125 -0
  313. package/src/runs/shared/worktree.ts +635 -0
  314. package/src/shared/agent-context-policy.ts +184 -0
  315. package/src/shared/artifacts.ts +95 -0
  316. package/src/shared/atomic-json.ts +16 -0
  317. package/src/shared/cooperative.ts +21 -0
  318. package/src/shared/execution-cwd.ts +14 -0
  319. package/src/shared/file-coalescer.ts +29 -0
  320. package/src/shared/fork-context.ts +68 -0
  321. package/src/shared/formatters.ts +84 -0
  322. package/src/shared/journal-reader.ts +333 -0
  323. package/src/shared/lazy-tools.ts +36 -0
  324. package/src/shared/model-info.ts +81 -0
  325. package/src/shared/native-session.ts +8 -0
  326. package/src/shared/native-tui.ts +10 -0
  327. package/src/shared/native-typebox.ts +14 -0
  328. package/src/shared/post-exit-stdio-guard.ts +204 -0
  329. package/src/shared/prompt-sections.ts +10 -0
  330. package/src/shared/session-entries.ts +49 -0
  331. package/src/shared/session-identity.ts +18 -0
  332. package/src/shared/settings.ts +384 -0
  333. package/src/shared/status-format.ts +123 -0
  334. package/src/shared/temp-root.ts +107 -0
  335. package/src/shared/types.ts +1260 -0
  336. package/src/shared/ui-mode.ts +5 -0
  337. package/src/shared/utils.ts +342 -0
  338. package/src/slash/prompt-template-bridge.ts +408 -0
  339. package/src/slash/slash-bridge.ts +178 -0
  340. package/src/slash/slash-commands.ts +617 -0
  341. package/src/slash/slash-live-state.ts +292 -0
  342. package/src/tui/action-hints.ts +69 -0
  343. package/src/tui/agent-history.ts +210 -0
  344. package/src/tui/agent-view.ts +1323 -0
  345. package/src/tui/render.ts +1550 -0
  346. package/tsconfig.build.json +12 -0
  347. package/tsconfig.json +19 -0
@@ -0,0 +1,463 @@
1
+ import net from "net";
2
+ import { chmodSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { randomUUID } from "crypto";
5
+ import { getPiAgentDir } from "../agent-dir.ts";
6
+ import { writeMessage, createMessageReader, validateIntercomMessageSize } from "./framing.ts";
7
+ import { prepareBrokerSocketPath } from "./paths.ts";
8
+ import { brokerPidRecord } from "./pid.ts";
9
+ import { compactTopicMessage, isSessionRegistration, isTopicSubscription, isTopicUpdate, normalizeMessage, normalizeSessionInfo } from "../types.ts";
10
+ import type { SessionInfo, Message, BrokerMessage, SendResult, SessionSnapshot, TopicUpdate } from "../types.ts";
11
+
12
+ const INTERCOM_DIR = join(getPiAgentDir(), "intercom");
13
+ const PID_PATH = join(INTERCOM_DIR, "broker.pid");
14
+
15
+ const REPLACE_DELIVERY_DELAY_MS = 1500;
16
+
17
+ interface ConnectedSession {
18
+ socket: net.Socket;
19
+ info: SessionInfo;
20
+ topicFrames: boolean;
21
+ }
22
+
23
+ function messageSender(info: SessionInfo): SessionInfo {
24
+ const { topics: _topics, subscriptions: _subscriptions, ...identity } = info;
25
+ return identity;
26
+ }
27
+
28
+ interface PendingReplaceDelivery {
29
+ from: SessionInfo;
30
+ fromId: string;
31
+ toId: string;
32
+ message: Message;
33
+ timer: NodeJS.Timeout;
34
+ }
35
+
36
+ class IntercomBroker {
37
+ private sessions = new Map<string, ConnectedSession>();
38
+ private pendingReplaceDeliveries = new Map<string, PendingReplaceDelivery>();
39
+ private server: net.Server;
40
+ private shutdownTimer: NodeJS.Timeout | null = null;
41
+ private readonly socketPath: string;
42
+
43
+ constructor() {
44
+ mkdirSync(INTERCOM_DIR, { recursive: true });
45
+ this.socketPath = prepareBrokerSocketPath();
46
+ try {
47
+ unlinkSync(this.socketPath);
48
+ } catch {
49
+ // A clean startup has no stale socket to remove.
50
+ }
51
+ this.server = net.createServer(this.handleConnection.bind(this));
52
+ this.server.on("error", (error) => {
53
+ console.error(`Intercom broker failed: ${error.message}`);
54
+ process.exitCode = 1;
55
+ });
56
+ }
57
+
58
+ start(): void {
59
+ this.server.listen(this.socketPath, () => {
60
+ chmodSync(this.socketPath, 0o600);
61
+ writeFileSync(PID_PATH, brokerPidRecord(), { mode: 0o600 });
62
+ console.log(`Intercom broker started (pid: ${process.pid})`);
63
+ // A broker no session ever registers with (for example, its spawning client exited first) must still exit.
64
+ this.scheduleShutdownCheck();
65
+ });
66
+ process.on("SIGTERM", () => this.shutdown());
67
+ process.on("SIGINT", () => this.shutdown());
68
+ }
69
+
70
+ private handleConnection(socket: net.Socket): void {
71
+ let sessionId: string | null = null;
72
+
73
+ const reader = createMessageReader((msg) => {
74
+ this.handleMessage(socket, msg, sessionId, (id) => {
75
+ sessionId = id;
76
+ });
77
+ }, (error) => {
78
+ socket.destroy(error);
79
+ });
80
+
81
+ socket.on("data", reader);
82
+
83
+ socket.on("close", () => {
84
+ if (sessionId) {
85
+ this.sessions.delete(sessionId);
86
+ this.clearPendingReplaceDeliveries(sessionId);
87
+ this.broadcast({ type: "session_left", sessionId }, sessionId);
88
+
89
+ this.scheduleShutdownCheck();
90
+ }
91
+ });
92
+
93
+ socket.on("error", (error) => {
94
+ console.error("Socket error:", error);
95
+ });
96
+ }
97
+
98
+ private scheduleShutdownCheck(): void {
99
+ if (this.shutdownTimer) return;
100
+
101
+ this.shutdownTimer = setTimeout(() => {
102
+ this.shutdownTimer = null;
103
+ if (this.sessions.size === 0) {
104
+ console.log("No sessions connected, shutting down");
105
+ this.shutdown();
106
+ }
107
+ }, 5000);
108
+ }
109
+
110
+ private handleMessage(
111
+ socket: net.Socket,
112
+ msg: unknown,
113
+ currentId: string | null,
114
+ setId: (id: string | null) => void,
115
+ ): void {
116
+ if (typeof msg !== "object" || msg === null || !("type" in msg) || typeof msg.type !== "string") {
117
+ throw new Error("Invalid client message");
118
+ }
119
+
120
+ const clientMessage = msg as { type: string } & Record<string, unknown>;
121
+
122
+ if (currentId === null && clientMessage.type !== "register") {
123
+ throw new Error(`Received ${clientMessage.type} before register`);
124
+ }
125
+
126
+ if (currentId !== null) {
127
+ this.touchActivity(currentId, false);
128
+ }
129
+
130
+ switch (clientMessage.type) {
131
+ case "register": {
132
+ if (!isSessionRegistration(clientMessage.session)) {
133
+ throw new Error("Invalid register message");
134
+ }
135
+
136
+ if (currentId) {
137
+ throw new Error("Received duplicate register message");
138
+ }
139
+
140
+ const requestedId = typeof clientMessage.requestedId === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(clientMessage.requestedId)
141
+ ? clientMessage.requestedId
142
+ : undefined;
143
+ const id = requestedId && !this.sessions.has(requestedId) ? requestedId : randomUUID();
144
+ const now = Date.now();
145
+ const info: SessionInfo = {
146
+ ...clientMessage.session,
147
+ id,
148
+ lastSeen: clientMessage.session.lastSeen ?? now,
149
+ };
150
+ this.snapshotFrames(randomUUID(), [info], true);
151
+ const topicFrames = clientMessage.topicFrames === true;
152
+ this.sessions.set(id, { socket, info, topicFrames });
153
+ setId(id);
154
+
155
+ if (this.shutdownTimer) {
156
+ clearTimeout(this.shutdownTimer);
157
+ this.shutdownTimer = null;
158
+ }
159
+
160
+ writeMessage(socket, { type: "registered", sessionId: id, ...(topicFrames ? { topicsSupported: true, topicFrames: true } : {}) });
161
+ break;
162
+ }
163
+
164
+ case "unregister": {
165
+ if (currentId === null) {
166
+ throw new Error("Received unregister before register");
167
+ }
168
+ const sessionId = currentId;
169
+ this.sessions.delete(sessionId);
170
+ this.clearPendingReplaceDeliveries(sessionId);
171
+ this.broadcast({ type: "session_left", sessionId }, sessionId);
172
+ setId(null);
173
+ this.scheduleShutdownCheck();
174
+ break;
175
+ }
176
+
177
+ case "list": {
178
+ if (typeof clientMessage.requestId !== "string") {
179
+ throw new Error("Invalid list message");
180
+ }
181
+
182
+ const requestId = clientMessage.requestId;
183
+ try {
184
+ const session = currentId && this.sessions.get(currentId);
185
+ if (!session) throw new Error("Sender session not found");
186
+ let nextInfo = session.info;
187
+ let publication: TopicUpdate | undefined;
188
+ const change = clientMessage.change as { action?: unknown; topic?: unknown; subscription?: unknown } | undefined;
189
+ if (change !== undefined) {
190
+ if (!session.topicFrames || !change || typeof change !== "object") throw new Error("Topic snapshots unavailable for this client");
191
+ const topics = new Map((session.info.topics ?? []).map((topic) => [topic.topic, topic]));
192
+ const subscriptions = new Map((session.info.subscriptions ?? []).map((entry) => [entry.topic, entry]));
193
+ if ((change.action === "publish" || change.action === "restore") && isTopicUpdate(change.topic)) {
194
+ if (!change.topic.text.trim() || (change.topic.event === "release" && (!change.topic.resource || change.topic.ownership !== "released"))) throw new Error("Invalid topic publication");
195
+ const update = { ...change.topic, ...(change.action === "publish" ? { revision: Math.max(topics.get(change.topic.topic)?.revision ?? 0, change.topic.revision - 1) + 1 } : {}) };
196
+ if (!isTopicUpdate(update)) throw new Error("Invalid topic revision");
197
+ if (change.action === "publish" || (topics.get(update.topic)?.revision ?? 0) < update.revision) topics.set(update.topic, update);
198
+ if (change.action === "publish") publication = update;
199
+ } else if ((change.action === "subscribe" || change.action === "restore") && isTopicSubscription(change.subscription)) {
200
+ subscriptions.set(change.subscription.topic, change.subscription);
201
+ } else if (change.action === "unsubscribe" && typeof change.topic === "string") {
202
+ subscriptions.delete(change.topic);
203
+ } else throw new Error("Invalid topic change");
204
+ nextInfo = { ...session.info, topics: [...topics.values()], subscriptions: [...subscriptions.values()] };
205
+ }
206
+ if (publication) nextInfo = { ...nextInfo, lastIntercomActivity: Date.now() };
207
+ const from = messageSender(nextInfo);
208
+ const deliveries: Array<{ to: string; message: Message }> = [];
209
+ if (publication) {
210
+ for (const target of this.sessions.values()) {
211
+ const subscription = target.info.subscriptions?.find((entry) => entry.topic === publication!.topic);
212
+ if (target.info.id === currentId || !subscription) continue;
213
+ const urgent = publication.event === "blocker" || publication.event === "decision" || publication.event === "release" && subscription.awaitRelease;
214
+ const message: Message = { id: randomUUID(), timestamp: Date.now(), topic: publication, content: { text: publication.text },
215
+ delivery: urgent ? "steer" : "queue", ...(urgent ? {} : { queueMode: "replace", threadId: `topic:${publication.topic}` }) };
216
+ const error = this.validateDeliveryPayload(from, message, target.topicFrames);
217
+ if (error) throw new Error(error);
218
+ deliveries.push({ to: target.info.id, message });
219
+ }
220
+ }
221
+ const infos = [...this.sessions.values()].map((entry) => entry === session ? nextInfo : entry.info);
222
+ // Validate every required frame before replacing the broker's usable state.
223
+ if (change?.action === "restore") this.snapshotFrames(requestId, [nextInfo], true);
224
+ const frames = this.snapshotFrames(requestId, change?.action === "restore" ? [] : infos, clientMessage.stream === true);
225
+ session.info = nextInfo;
226
+ const receipts: SessionSnapshot["receipts"] = deliveries.map(({ to, message }) => ({ to, ...this.sendToSession(currentId!, to, from, message) }));
227
+ const last = frames.pop()!;
228
+ for (const frame of frames) writeMessage(socket, frame);
229
+ for (const receipt of receipts) writeMessage(socket, { type: "sessions", requestId, sessions: [], receipts: [receipt], more: true });
230
+ writeMessage(socket, last);
231
+ } catch (error) {
232
+ writeMessage(socket, { type: "sessions", requestId, sessions: [], error: error instanceof Error ? error.message : String(error) });
233
+ }
234
+ break;
235
+ }
236
+
237
+ case "send": {
238
+ const message = normalizeMessage(clientMessage.message);
239
+ const messageId = message?.id ?? "unknown";
240
+
241
+ if (typeof clientMessage.to !== "string" || !message) {
242
+ writeMessage(socket, {
243
+ type: "delivery_failed",
244
+ messageId,
245
+ reason: "Invalid message format",
246
+ });
247
+ break;
248
+ }
249
+
250
+ if (currentId === null) {
251
+ writeMessage(socket, {
252
+ type: "delivery_failed",
253
+ messageId: message.id,
254
+ reason: "Sender session not found",
255
+ });
256
+ break;
257
+ }
258
+
259
+ const targets = this.findSessions(clientMessage.to);
260
+ if (targets.length === 1) {
261
+ const fromSession = this.sessions.get(currentId);
262
+ if (!fromSession) {
263
+ writeMessage(socket, {
264
+ type: "delivery_failed",
265
+ messageId: message.id,
266
+ reason: "Sender session not found",
267
+ });
268
+ break;
269
+ }
270
+ this.touchActivity(currentId, true);
271
+ const receipt = this.sendToSession(currentId, targets[0].info.id, messageSender(fromSession.info), message);
272
+ writeMessage(socket, { type: receipt.accepted ? receipt.queued ? "delivery_queued" : "delivered" : "delivery_failed", messageId: message.id, ...(receipt.reason ? { reason: receipt.reason } : {}) });
273
+ break;
274
+ }
275
+
276
+ if (targets.length > 1) {
277
+ writeMessage(socket, {
278
+ type: "delivery_failed",
279
+ messageId: message.id,
280
+ reason: `Multiple sessions named \"${clientMessage.to}\" are connected. Use the session ID instead.`,
281
+ });
282
+ break;
283
+ }
284
+
285
+ writeMessage(socket, {
286
+ type: "delivery_failed",
287
+ messageId: message.id,
288
+ reason: "Session not found",
289
+ });
290
+ break;
291
+ }
292
+
293
+ case "presence": {
294
+ if (currentId === null) {
295
+ throw new Error("Received presence before register");
296
+ }
297
+ const session = this.sessions.get(currentId);
298
+ if (session) {
299
+ const nextInfo = normalizeSessionInfo({
300
+ ...session.info,
301
+ ...(clientMessage.name !== undefined ? { name: clientMessage.name } : {}),
302
+ ...(clientMessage.status !== undefined ? { status: clientMessage.status } : {}),
303
+ ...(clientMessage.model !== undefined ? { model: clientMessage.model } : {}),
304
+ ...(clientMessage.pendingAsks !== undefined ? { pendingAsks: clientMessage.pendingAsks } : {}),
305
+ ...(clientMessage.acceptsAsks !== undefined ? { acceptsAsks: clientMessage.acceptsAsks } : {}),
306
+ ...(clientMessage.lastIntercomActivity !== undefined ? { lastIntercomActivity: clientMessage.lastIntercomActivity } : {}),
307
+ ...(clientMessage.subscriptions !== undefined ? { subscriptions: clientMessage.subscriptions } : {}),
308
+ ...(clientMessage.topics !== undefined ? { topics: clientMessage.topics } : {}),
309
+ lastSeen: Date.now(),
310
+ });
311
+ if (!nextInfo) throw new Error("Invalid presence update");
312
+ this.snapshotFrames(randomUUID(), [nextInfo], true);
313
+ session.info = nextInfo;
314
+ }
315
+ break;
316
+ }
317
+
318
+ default:
319
+ throw new Error(`Unknown client message type: ${clientMessage.type}`);
320
+ }
321
+ }
322
+
323
+ private snapshotFrames(requestId: string, infos: SessionInfo[], stream: boolean): BrokerMessage[] {
324
+ // Older clients have no topic UI or multipart reader; keep their original identity-only list.
325
+ if (!stream) {
326
+ const frame: BrokerMessage = { type: "sessions", requestId, sessions: infos.map(messageSender) };
327
+ const error = validateIntercomMessageSize(frame);
328
+ if (error) throw error;
329
+ return [frame];
330
+ }
331
+ // ponytail: one record per frame; pack records only if measured socket overhead warrants it.
332
+ const frames: BrokerMessage[] = [];
333
+ for (const info of infos) {
334
+ frames.push({ type: "sessions", requestId, sessions: [{ ...messageSender(info), ...(info.topics ? { topics: [] } : {}), ...(info.subscriptions ? { subscriptions: [] } : {}) }], more: true });
335
+ for (const topic of info.topics ?? []) frames.push({ type: "sessions", requestId, sessions: [{ id: info.id, topics: [topic] }], more: true });
336
+ for (const subscription of info.subscriptions ?? []) frames.push({ type: "sessions", requestId, sessions: [{ id: info.id, subscriptions: [subscription] }], more: true });
337
+ }
338
+ frames.push({ type: "sessions", requestId, sessions: [], more: false });
339
+ for (const frame of frames) {
340
+ const error = validateIntercomMessageSize(frame);
341
+ if (error) throw error;
342
+ }
343
+ return frames;
344
+ }
345
+
346
+ private sendToSession(fromId: string, toId: string, from: SessionInfo, message: Message): SendResult {
347
+ const target = this.sessions.get(toId);
348
+ if (!target) return { id: message.id, accepted: false, delivered: false, reason: "Recipient disconnected before delivery" };
349
+ const status = target.info.status ?? "";
350
+ const idle = status === "idle" || status.startsWith("idle ");
351
+ if (message.delivery === "queue" && message.queueMode === "replace" && message.threadId && (message.expectsReply || idle && target.info.acceptsAsks !== false)) return this.queueReplaceDelivery(fromId, toId, from, message);
352
+ const reason = this.deliverMessage(toId, from, message);
353
+ return { id: message.id, accepted: !reason, delivered: !reason, ...(reason ? { reason } : {}) };
354
+ }
355
+
356
+ /** Update liveness/intercom-activity timestamps for a connected session. */
357
+ private touchActivity(sessionId: string, comms: boolean): void {
358
+ const session = this.sessions.get(sessionId);
359
+ if (!session) {
360
+ return;
361
+ }
362
+ const now = Date.now();
363
+ session.info.lastSeen = now;
364
+ if (comms) {
365
+ session.info.lastIntercomActivity = now;
366
+ }
367
+ }
368
+
369
+ private replaceKey(fromId: string, toId: string, threadId: string): string {
370
+ return `${fromId}\0${toId}\0${threadId}`;
371
+ }
372
+
373
+ private queueReplaceDelivery(fromId: string, toId: string, from: SessionInfo, message: Message): SendResult {
374
+ const validationError = this.validateDeliveryPayload(from, message, this.sessions.get(toId)?.topicFrames === true);
375
+ if (validationError) {
376
+ return { id: message.id, accepted: false, delivered: false, reason: validationError };
377
+ }
378
+ const key = this.replaceKey(fromId, toId, message.threadId ?? "");
379
+ const existing = this.pendingReplaceDeliveries.get(key);
380
+ if (existing) clearTimeout(existing.timer);
381
+ const timer = setTimeout(() => {
382
+ this.pendingReplaceDeliveries.delete(key);
383
+ this.deliverMessage(toId, from, message);
384
+ }, REPLACE_DELIVERY_DELAY_MS);
385
+ timer.unref?.();
386
+ this.pendingReplaceDeliveries.set(key, { from, fromId, toId, message, timer });
387
+ return { id: message.id, accepted: true, delivered: false, queued: true, reason: "Queued for replace-mode delivery" };
388
+ }
389
+
390
+ private clearPendingReplaceDeliveries(sessionId: string): void {
391
+ for (const [key, pending] of this.pendingReplaceDeliveries) {
392
+ if (pending.toId === sessionId || (pending.fromId === sessionId && pending.message.expectsReply)) {
393
+ clearTimeout(pending.timer);
394
+ this.pendingReplaceDeliveries.delete(key);
395
+ }
396
+ }
397
+ }
398
+
399
+ private validateDeliveryPayload(from: SessionInfo, message: Message, topicFrames: boolean): string | null {
400
+ return validateIntercomMessageSize({ type: "message", from: messageSender(from), message: topicFrames ? compactTopicMessage(message) : message })?.message ?? null;
401
+ }
402
+
403
+ private deliverMessage(toId: string, from: SessionInfo, message: Message): string | null {
404
+ const target = this.sessions.get(toId);
405
+ if (!target || target.socket.destroyed || target.socket.writableEnded || !target.socket.writable) {
406
+ return "Recipient disconnected before delivery";
407
+ }
408
+ const validationError = this.validateDeliveryPayload(from, message, target.topicFrames);
409
+ if (validationError) return validationError;
410
+ this.touchActivity(toId, true);
411
+ try {
412
+ writeMessage(target.socket, {
413
+ type: "message",
414
+ from: messageSender(from),
415
+ message: target.topicFrames ? compactTopicMessage(message) : message,
416
+ });
417
+ return null;
418
+ } catch (error) {
419
+ return error instanceof Error ? error.message : String(error);
420
+ }
421
+ }
422
+
423
+ private findSessions(nameOrId: string): ConnectedSession[] {
424
+ const byId = this.sessions.get(nameOrId);
425
+ if (byId) {
426
+ return [byId];
427
+ }
428
+
429
+ const lowerName = nameOrId.toLowerCase();
430
+ return Array.from(this.sessions.values()).filter(session => session.info.name?.toLowerCase() === lowerName);
431
+ }
432
+
433
+ private broadcast(msg: BrokerMessage, exclude?: string): void {
434
+ for (const [id, session] of this.sessions) {
435
+ if (id !== exclude) {
436
+ writeMessage(session.socket, msg);
437
+ }
438
+ }
439
+ }
440
+
441
+ private shutdown(): void {
442
+ console.log("Broker shutting down");
443
+
444
+ for (const session of this.sessions.values()) {
445
+ session.socket.end();
446
+ }
447
+ this.sessions.clear();
448
+ try {
449
+ unlinkSync(this.socketPath);
450
+ } catch {
451
+ // The socket may already be gone if shutdown started after a disconnect.
452
+ }
453
+ try {
454
+ unlinkSync(PID_PATH);
455
+ } catch {
456
+ // The PID file may already be gone if startup never completed.
457
+ }
458
+ this.server.close();
459
+ process.exit(0);
460
+ }
461
+ }
462
+
463
+ new IntercomBroker().start();