@cuongtran001/kanna 1.3.0 → 1.3.1

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 (186) hide show
  1. package/dist/client/assets/{arc-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
  5. package/dist/client/assets/channel-BEYTrUAm.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
  27. package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
  28. package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
  29. package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
  30. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
  31. package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
  32. package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
  33. package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
  34. package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
  35. package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
  36. package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
  37. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
  38. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
  39. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
  40. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
  41. package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
  42. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
  43. package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
  44. package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
  45. package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
  46. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
  47. package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
  48. package/dist/client/index.html +1 -1
  49. package/package.json +1 -1
  50. package/src/server/agent-coordinator-types.ts +158 -0
  51. package/src/server/agent-coordinator.ts +919 -0
  52. package/src/server/agent-deps-builders.ts +425 -0
  53. package/src/server/agent.test.ts +22 -6
  54. package/src/server/agent.ts +35 -4987
  55. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  56. package/src/server/claude-autocontinue-commands.ts +256 -0
  57. package/src/server/claude-cancel-handler.test.ts +472 -0
  58. package/src/server/claude-cancel-handler.ts +241 -0
  59. package/src/server/claude-chat-management.test.ts +395 -0
  60. package/src/server/claude-chat-management.ts +251 -0
  61. package/src/server/claude-harness-stream.test.ts +273 -0
  62. package/src/server/claude-harness-stream.ts +158 -0
  63. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  64. package/src/server/claude-loop-orch-commands.ts +466 -0
  65. package/src/server/claude-message-normalizer.test.ts +213 -0
  66. package/src/server/claude-message-normalizer.ts +436 -0
  67. package/src/server/claude-prompt-helpers.test.ts +248 -0
  68. package/src/server/claude-prompt-helpers.ts +139 -0
  69. package/src/server/claude-sdk-queue.test.ts +169 -0
  70. package/src/server/claude-sdk-queue.ts +82 -0
  71. package/src/server/claude-send-command.test.ts +414 -0
  72. package/src/server/claude-send-command.ts +420 -0
  73. package/src/server/claude-session-config-helpers.test.ts +279 -0
  74. package/src/server/claude-session-config-helpers.ts +151 -0
  75. package/src/server/claude-session-config.test.ts +250 -0
  76. package/src/server/claude-session-config.ts +159 -0
  77. package/src/server/claude-session-error-handler.test.ts +498 -0
  78. package/src/server/claude-session-error-handler.ts +361 -0
  79. package/src/server/claude-session-lifecycle.test.ts +669 -0
  80. package/src/server/claude-session-lifecycle.ts +283 -0
  81. package/src/server/claude-session-rebuild.test.ts +224 -0
  82. package/src/server/claude-session-rebuild.ts +101 -0
  83. package/src/server/claude-session-runner.test.ts +523 -0
  84. package/src/server/claude-session-runner.ts +521 -0
  85. package/src/server/claude-session-spawner.test.ts +401 -0
  86. package/src/server/claude-session-spawner.ts +401 -0
  87. package/src/server/claude-session-start.test.ts +205 -0
  88. package/src/server/claude-session-start.ts +277 -0
  89. package/src/server/claude-session-state-queries.test.ts +344 -0
  90. package/src/server/claude-session-state-queries.ts +160 -0
  91. package/src/server/claude-session-state.ts +104 -0
  92. package/src/server/claude-slash-commands.test.ts +277 -0
  93. package/src/server/claude-slash-commands.ts +234 -0
  94. package/src/server/claude-spawn-helpers.test.ts +221 -0
  95. package/src/server/claude-spawn-helpers.ts +179 -0
  96. package/src/server/claude-steer-log.test.ts +243 -0
  97. package/src/server/claude-steer-log.ts +58 -0
  98. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  99. package/src/server/claude-subagent-tool-response.ts +165 -0
  100. package/src/server/claude-subagent-wiring.test.ts +263 -0
  101. package/src/server/claude-subagent-wiring.ts +324 -0
  102. package/src/server/claude-tool-respond.test.ts +321 -0
  103. package/src/server/claude-tool-respond.ts +142 -0
  104. package/src/server/claude-turn-runner.test.ts +310 -0
  105. package/src/server/claude-turn-runner.ts +228 -0
  106. package/src/server/claude-turn-starter.test.ts +266 -0
  107. package/src/server/claude-turn-starter.ts +566 -0
  108. package/src/server/claude-usage-math.test.ts +169 -0
  109. package/src/server/claude-usage-math.ts +132 -0
  110. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  111. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  112. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  113. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  114. package/src/server/diff-store-errors.ts +85 -0
  115. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  116. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  117. package/src/server/diff-store-io.adapter.ts +27 -0
  118. package/src/server/diff-store-parse.test.ts +212 -0
  119. package/src/server/diff-store-parse.ts +122 -0
  120. package/src/server/diff-store-state.ts +82 -0
  121. package/src/server/diff-store.ts +76 -1854
  122. package/src/server/event-store-apply.test.ts +280 -0
  123. package/src/server/event-store-apply.ts +132 -0
  124. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  125. package/src/server/event-store-chat-lifecycle.ts +441 -0
  126. package/src/server/event-store-entity-write.ts +356 -0
  127. package/src/server/event-store-helpers.test.ts +230 -0
  128. package/src/server/event-store-helpers.ts +235 -0
  129. package/src/server/event-store-init.ts +276 -0
  130. package/src/server/event-store-messages.adapter.test.ts +365 -0
  131. package/src/server/event-store-messages.adapter.ts +213 -0
  132. package/src/server/event-store-orch-subscription.ts +124 -0
  133. package/src/server/event-store-orch.test.ts +462 -0
  134. package/src/server/event-store-orch.ts +312 -0
  135. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  136. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  137. package/src/server/event-store-snapshot.test.ts +597 -0
  138. package/src/server/event-store-snapshot.ts +658 -0
  139. package/src/server/event-store-subagent.test.ts +408 -0
  140. package/src/server/event-store-subagent.ts +198 -0
  141. package/src/server/event-store-tool-requests.test.ts +210 -0
  142. package/src/server/event-store-tool-requests.ts +131 -0
  143. package/src/server/event-store-transcript-write.adapter.ts +285 -0
  144. package/src/server/event-store-write-ops.test.ts +554 -0
  145. package/src/server/event-store-write-ops.ts +529 -0
  146. package/src/server/event-store.test.ts +35 -0
  147. package/src/server/event-store.ts +251 -2285
  148. package/src/server/harness-types.ts +15 -1
  149. package/src/server/oauth-errors.ts +19 -0
  150. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  151. package/src/server/ws-router-agent-ctrl.ts +137 -0
  152. package/src/server/ws-router-broadcast.ts +488 -0
  153. package/src/server/ws-router-chat.test.ts +461 -0
  154. package/src/server/ws-router-chat.ts +279 -0
  155. package/src/server/ws-router-defaults.test.ts +283 -0
  156. package/src/server/ws-router-defaults.ts +434 -0
  157. package/src/server/ws-router-diff.test.ts +167 -0
  158. package/src/server/ws-router-diff.ts +250 -0
  159. package/src/server/ws-router-envelope.ts +329 -0
  160. package/src/server/ws-router-misc.test.ts +422 -0
  161. package/src/server/ws-router-misc.ts +265 -0
  162. package/src/server/ws-router-orch.test.ts +190 -0
  163. package/src/server/ws-router-orch.ts +93 -0
  164. package/src/server/ws-router-project.test.ts +403 -0
  165. package/src/server/ws-router-project.ts +256 -0
  166. package/src/server/ws-router-push.test.ts +193 -0
  167. package/src/server/ws-router-push.ts +120 -0
  168. package/src/server/ws-router-settings.test.ts +173 -0
  169. package/src/server/ws-router-settings.ts +416 -0
  170. package/src/server/ws-router-skills.test.ts +194 -0
  171. package/src/server/ws-router-skills.ts +204 -0
  172. package/src/server/ws-router-utils.ts +224 -0
  173. package/src/server/ws-router.ts +339 -2254
  174. package/src/shared/app-settings-types.ts +365 -0
  175. package/src/shared/core-types.ts +27 -0
  176. package/src/shared/git-diff-types.ts +165 -0
  177. package/src/shared/mcp-types.ts +105 -0
  178. package/src/shared/provider-model-types.ts +437 -0
  179. package/src/shared/subagent-types.ts +198 -0
  180. package/src/shared/tool-call-types.ts +314 -0
  181. package/src/shared/transcript-types.ts +270 -0
  182. package/src/shared/types.ts +25 -1711
  183. package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
  184. package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
  185. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
  186. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
@@ -0,0 +1,488 @@
1
+ /**
2
+ * ws-router-broadcast.ts
3
+ *
4
+ * BroadcastManager — owns the connected-socket set, pending-broadcast state,
5
+ * and all snapshot-push / broadcast methods. Wires event subscriptions
6
+ * (terminals, keybindings, appSettings, update, ptyInstances, workflows, orch)
7
+ * and exposes a `dispose()` to tear them down.
8
+ *
9
+ * Extracted from ws-router.ts to reduce its size.
10
+ */
11
+ import { log } from "../shared/log"
12
+ import { PROTOCOL_VERSION } from "../shared/types"
13
+ import type { ServerWebSocket } from "bun"
14
+ import type { PtyInstanceDelta } from "../shared/pty-instance"
15
+ import type { PtyInstanceRegistry } from "./claude-pty/pty-instance-registry"
16
+ import type { WorkflowRegistry } from "./workflow-registry"
17
+ import type { EventStore } from "./event-store"
18
+ import type { AgentCoordinator } from "./agent"
19
+ import type { TerminalManager } from "./terminal-manager"
20
+ import type { KeybindingsManager } from "./keybindings"
21
+ import type { UpdateManager } from "./update-manager"
22
+ import type { ServerEnvelope } from "../shared/protocol"
23
+ import type { ResolvedAppSettings } from "./ws-router-defaults"
24
+ import type { EnvelopeBuilder } from "./ws-router-envelope"
25
+ import {
26
+ countSubscriptionsByTopic,
27
+ ensureSnapshotSignatures,
28
+ getStableChatSnapshotSignature,
29
+ isSendToStartingProfilingEnabled,
30
+ logSendToStartingProfile,
31
+ send,
32
+ shouldIncludeTopic,
33
+ } from "./ws-router-utils"
34
+ import type { ClientState, SnapshotBroadcastFilter, SnapshotComputationCache } from "./ws-router-utils"
35
+
36
+ // ── Deps ──────────────────────────────────────────────────────────────────────
37
+
38
+ export interface BroadcastManagerDeps {
39
+ agent: AgentCoordinator
40
+ store: EventStore
41
+ terminals: TerminalManager
42
+ keybindings: KeybindingsManager
43
+ resolvedAppSettings: ResolvedAppSettings
44
+ updateManager: UpdateManager | null
45
+ ptyInstances?: PtyInstanceRegistry
46
+ workflowRegistry?: WorkflowRegistry
47
+ envelopeBuilder: EnvelopeBuilder
48
+ }
49
+
50
+ // ── BroadcastManager ─────────────────────────────────────────────────────────
51
+
52
+ export class BroadcastManager {
53
+ private readonly sockets = new Set<ServerWebSocket<ClientState>>()
54
+ private pendingBroadcastTimer: ReturnType<typeof setTimeout> | null = null
55
+ private pendingBroadcastAll = false
56
+ private readonly pendingBroadcastChatIds = new Set<string>()
57
+
58
+ private readonly disposeTerminalEvents: () => void
59
+ private readonly disposeKeybindingEvents: () => void
60
+ private readonly disposeAppSettingsEvents: () => void
61
+ private readonly disposeUpdateEvents: () => void
62
+ private readonly disposePtyInstances: () => void
63
+ private readonly disposeWorkflows: () => void
64
+ private readonly disposeOrchRuns: () => void
65
+
66
+ constructor(private readonly deps: BroadcastManagerDeps) {
67
+ const {
68
+ agent,
69
+ terminals,
70
+ keybindings,
71
+ resolvedAppSettings,
72
+ updateManager,
73
+ ptyInstances,
74
+ workflowRegistry,
75
+ store,
76
+ } = deps
77
+
78
+ // Wire background error reporter
79
+ agent.setBackgroundErrorReporter?.(this.broadcastError.bind(this))
80
+
81
+ // Terminal events
82
+ this.disposeTerminalEvents = terminals.onEvent((event) => {
83
+ this.pushTerminalEvent(event.terminalId, event)
84
+ })
85
+
86
+ // Keybinding snapshot push on change
87
+ this.disposeKeybindingEvents = keybindings.onChange(() => {
88
+ for (const ws of this.sockets) {
89
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
90
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
91
+ if (topic.type !== "keybindings") continue
92
+ const envelope = deps.envelopeBuilder.createEnvelope(id, topic, undefined, ws)
93
+ if (envelope.type !== "snapshot") continue
94
+ const signature = JSON.stringify(envelope.snapshot)
95
+ if (snapshotSignatures.get(id) === signature) continue
96
+ snapshotSignatures.set(id, signature)
97
+ send(ws, envelope)
98
+ }
99
+ }
100
+ })
101
+
102
+ // App-settings snapshot push on change
103
+ this.disposeAppSettingsEvents = resolvedAppSettings.onChange(() => {
104
+ for (const ws of this.sockets) {
105
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
106
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
107
+ if (topic.type !== "app-settings") continue
108
+ const envelope = deps.envelopeBuilder.createEnvelope(id, topic, undefined, ws)
109
+ if (envelope.type !== "snapshot") continue
110
+ const signature = JSON.stringify(envelope.snapshot)
111
+ if (snapshotSignatures.get(id) === signature) continue
112
+ snapshotSignatures.set(id, signature)
113
+ send(ws, envelope)
114
+ }
115
+ }
116
+ })
117
+
118
+ // Update snapshot push on change
119
+ this.disposeUpdateEvents = updateManager?.onChange(() => {
120
+ for (const ws of this.sockets) {
121
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
122
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
123
+ if (topic.type !== "update") continue
124
+ const envelope = deps.envelopeBuilder.createEnvelope(id, topic, undefined, ws)
125
+ if (envelope.type !== "snapshot") continue
126
+ const signature = JSON.stringify(envelope.snapshot)
127
+ if (snapshotSignatures.get(id) === signature) continue
128
+ snapshotSignatures.set(id, signature)
129
+ send(ws, envelope)
130
+ }
131
+ }
132
+ }) ?? (() => {})
133
+
134
+ // PTY instance events
135
+ this.disposePtyInstances = ptyInstances?.subscribe((delta: PtyInstanceDelta) => {
136
+ if (delta.type === "added") {
137
+ this.pushPtyInstancesEvent({ type: "pty-instances.added", instance: delta.instance })
138
+ } else if (delta.type === "updated") {
139
+ this.pushPtyInstancesEvent({ type: "pty-instances.updated", instance: delta.instance })
140
+ } else {
141
+ this.pushPtyInstancesEvent({ type: "pty-instances.removed", chatId: delta.chatId })
142
+ }
143
+ }) ?? (() => {})
144
+
145
+ // Workflow snapshot push on change
146
+ this.disposeWorkflows = workflowRegistry?.subscribe((chatId) => {
147
+ for (const ws of this.sockets) {
148
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
149
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
150
+ if (topic.type !== "workflows" || topic.chatId !== chatId) continue
151
+ const envelope = deps.envelopeBuilder.createEnvelope(id, topic, undefined, ws)
152
+ if (envelope.type !== "snapshot") continue
153
+ const signature = JSON.stringify(envelope.snapshot)
154
+ if (snapshotSignatures.get(id) === signature) continue
155
+ snapshotSignatures.set(id, signature)
156
+ send(ws, envelope)
157
+ }
158
+ }
159
+ }) ?? (() => {})
160
+
161
+ // Orchestration run pushes
162
+ // Optional like the registry subscriptions above: partial store fakes in
163
+ // tests may not implement it. The real EventStore always does.
164
+ this.disposeOrchRuns = typeof store.subscribeOrchRuns === "function"
165
+ ? store.subscribeOrchRuns(() => this.pushOrchRuns())
166
+ : () => {}
167
+ }
168
+
169
+ // ── Socket lifecycle ────────────────────────────────────────────────────────
170
+
171
+ addSocket(ws: ServerWebSocket<ClientState>): void {
172
+ this.sockets.add(ws)
173
+ }
174
+
175
+ removeSocket(ws: ServerWebSocket<ClientState>): void {
176
+ this.sockets.delete(ws)
177
+ }
178
+
179
+ // ── Protected-chat helpers ──────────────────────────────────────────────────
180
+
181
+ getProtectedChatIds(): Set<string> {
182
+ const { agent } = this.deps
183
+ const activeStatuses = agent.getActiveStatuses()
184
+ const drainingChatIds = typeof agent.getDrainingChatIds === "function"
185
+ ? agent.getDrainingChatIds()
186
+ : new Set<string>()
187
+ return new Set([
188
+ ...activeStatuses.keys(),
189
+ ...drainingChatIds.values(),
190
+ ])
191
+ }
192
+
193
+ getProtectedDraftChatIds(extraSockets?: Iterable<ServerWebSocket<ClientState>>): Set<string> {
194
+ const protectedChatIds = new Set<string>()
195
+
196
+ for (const socket of this.sockets) {
197
+ for (const chatId of socket.data.protectedDraftChatIds ?? []) {
198
+ protectedChatIds.add(chatId)
199
+ }
200
+ }
201
+
202
+ for (const socket of extraSockets ?? []) {
203
+ for (const chatId of socket.data.protectedDraftChatIds ?? []) {
204
+ protectedChatIds.add(chatId)
205
+ }
206
+ }
207
+
208
+ return protectedChatIds
209
+ }
210
+
211
+ async maybePruneStaleEmptyChats(extraSockets?: Iterable<ServerWebSocket<ClientState>>): Promise<void> {
212
+ const { store } = this.deps
213
+ const startedAt = performance.now()
214
+ const activeChatIds = this.getProtectedChatIds()
215
+ const protectedDraftChatIds = this.getProtectedDraftChatIds(extraSockets)
216
+ const prunedChatIds = await store.pruneStaleEmptyChats?.({
217
+ activeChatIds,
218
+ protectedChatIds: protectedDraftChatIds,
219
+ })
220
+ if (isSendToStartingProfilingEnabled()) {
221
+ log.info("[kanna/send->starting][server]", JSON.stringify({
222
+ stage: "ws.prune_stale_empty_chats",
223
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
224
+ activeChatCount: activeChatIds.size,
225
+ protectedDraftChatCount: protectedDraftChatIds.size,
226
+ prunedCount: prunedChatIds?.length ?? 0,
227
+ totalChatCount: store.state.chatsById.size,
228
+ totalProjectCount: store.state.projectsById.size,
229
+ }))
230
+ }
231
+ }
232
+
233
+ // ── Snapshot push ───────────────────────────────────────────────────────────
234
+
235
+ async pushSnapshots(
236
+ ws: ServerWebSocket<ClientState>,
237
+ options?: { skipPrune?: boolean; filter?: SnapshotBroadcastFilter; cache?: SnapshotComputationCache }
238
+ ): Promise<void> {
239
+ const { agent } = this.deps
240
+ const pushStartedAt = performance.now()
241
+ if (!options?.skipPrune) {
242
+ await this.maybePruneStaleEmptyChats([ws])
243
+ }
244
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
245
+ let sentCount = 0
246
+ let skippedCount = 0
247
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
248
+ if (!shouldIncludeTopic(topic, options?.filter)) {
249
+ continue
250
+ }
251
+ const envelopeStartedAt = performance.now()
252
+ const envelope = this.deps.envelopeBuilder.createEnvelope(id, topic, options?.cache, ws)
253
+ const createdAt = performance.now()
254
+ if (envelope.type !== "snapshot") continue
255
+ const signature = topic.type === "sidebar"
256
+ ? this.deps.envelopeBuilder.getSidebarSnapshotCacheEntry(options?.cache).signature
257
+ : getStableChatSnapshotSignature(envelope.snapshot)
258
+ const signatureReadyAt = topic.type === "sidebar" ? createdAt : performance.now()
259
+ if (snapshotSignatures.get(id) === signature) {
260
+ skippedCount += 1
261
+ continue
262
+ }
263
+ snapshotSignatures.set(id, signature)
264
+ if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
265
+ const profile = agent.getActiveTurnProfile(topic.chatId)
266
+ logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_sent", {
267
+ chatId: topic.chatId,
268
+ status: envelope.snapshot.data.runtime.status,
269
+ messageCount: envelope.snapshot.data.messages.length,
270
+ buildMs: Number((createdAt - envelopeStartedAt).toFixed(1)),
271
+ signatureMs: Number((signatureReadyAt - createdAt).toFixed(1)),
272
+ signatureBytes: signature.length,
273
+ })
274
+ }
275
+ const payloadBytes = send(ws, envelope)
276
+ sentCount += 1
277
+ if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
278
+ const profile = agent.getActiveTurnProfile(topic.chatId)
279
+ logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_send_completed", {
280
+ chatId: topic.chatId,
281
+ payloadBytes,
282
+ })
283
+ }
284
+ }
285
+ if (isSendToStartingProfilingEnabled()) {
286
+ log.info("[kanna/send->starting][server]", JSON.stringify({
287
+ stage: "ws.push_snapshots_completed",
288
+ elapsedMs: Number((performance.now() - pushStartedAt).toFixed(1)),
289
+ skipPrune: Boolean(options?.skipPrune),
290
+ sentCount,
291
+ skippedCount,
292
+ ...countSubscriptionsByTopic(ws),
293
+ }))
294
+ }
295
+ }
296
+
297
+ // ── Broadcast ───────────────────────────────────────────────────────────────
298
+
299
+ async broadcastSnapshots(): Promise<void> {
300
+ const { store } = this.deps
301
+ const startedAt = performance.now()
302
+ let socketCount = 0
303
+ const cache: SnapshotComputationCache = {}
304
+ for (const ws of this.sockets) {
305
+ socketCount += 1
306
+ await this.pushSnapshots(ws, { skipPrune: true, cache })
307
+ }
308
+ if (isSendToStartingProfilingEnabled()) {
309
+ log.info("[kanna/send->starting][server]", JSON.stringify({
310
+ stage: "ws.broadcast_snapshots_completed",
311
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
312
+ pruneMs: 0,
313
+ socketCount,
314
+ totalChatCount: store.state.chatsById.size,
315
+ totalProjectCount: store.state.projectsById.size,
316
+ }))
317
+ }
318
+ }
319
+
320
+ async broadcastFilteredSnapshots(filter: SnapshotBroadcastFilter): Promise<void> {
321
+ const startedAt = performance.now()
322
+ let socketCount = 0
323
+ const cache: SnapshotComputationCache = {}
324
+ for (const ws of this.sockets) {
325
+ socketCount += 1
326
+ await this.pushSnapshots(ws, { skipPrune: true, filter, cache })
327
+ }
328
+ if (isSendToStartingProfilingEnabled()) {
329
+ log.info("[kanna/send->starting][server]", JSON.stringify({
330
+ stage: "ws.broadcast_filtered_snapshots_completed",
331
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
332
+ socketCount,
333
+ includeSidebar: Boolean(filter.includeSidebar),
334
+ chatCount: filter.chatIds?.size ?? 0,
335
+ projectCount: filter.projectIds?.size ?? 0,
336
+ }))
337
+ }
338
+ }
339
+
340
+ scheduleBroadcast(): void {
341
+ this.pendingBroadcastAll = true
342
+ this.pendingBroadcastChatIds.clear()
343
+ if (this.pendingBroadcastTimer) {
344
+ return
345
+ }
346
+ this.pendingBroadcastTimer = setTimeout(() => {
347
+ this.pendingBroadcastTimer = null
348
+ const shouldBroadcastAll = this.pendingBroadcastAll
349
+ const chatIds = new Set(this.pendingBroadcastChatIds)
350
+ this.pendingBroadcastAll = false
351
+ this.pendingBroadcastChatIds.clear()
352
+ if (shouldBroadcastAll) {
353
+ void this.broadcastSnapshots()
354
+ return
355
+ }
356
+ if (chatIds.size > 0) {
357
+ void this.broadcastFilteredSnapshots({
358
+ includeSidebar: true,
359
+ chatIds,
360
+ })
361
+ }
362
+ }, 16)
363
+ }
364
+
365
+ scheduleChatStateBroadcast(chatId: string): void {
366
+ if (!this.pendingBroadcastAll) {
367
+ this.pendingBroadcastChatIds.add(chatId)
368
+ }
369
+ if (this.pendingBroadcastTimer) {
370
+ return
371
+ }
372
+ this.pendingBroadcastTimer = setTimeout(() => {
373
+ this.pendingBroadcastTimer = null
374
+ const shouldBroadcastAll = this.pendingBroadcastAll
375
+ const chatIds = new Set(this.pendingBroadcastChatIds)
376
+ this.pendingBroadcastAll = false
377
+ this.pendingBroadcastChatIds.clear()
378
+ if (shouldBroadcastAll) {
379
+ void this.broadcastSnapshots()
380
+ return
381
+ }
382
+ if (chatIds.size > 0) {
383
+ void this.broadcastFilteredSnapshots({
384
+ includeSidebar: true,
385
+ chatIds,
386
+ })
387
+ }
388
+ }, 16)
389
+ }
390
+
391
+ async broadcastChatAndSidebar(chatId: string): Promise<void> {
392
+ await this.broadcastFilteredSnapshots({
393
+ includeSidebar: true,
394
+ chatIds: new Set([chatId]),
395
+ })
396
+ }
397
+
398
+ async broadcastChatStateImmediately(chatId: string): Promise<void> {
399
+ await this.broadcastChatAndSidebar(chatId)
400
+ }
401
+
402
+ broadcastError(message: string): void {
403
+ for (const ws of this.sockets) {
404
+ send(ws, {
405
+ v: PROTOCOL_VERSION,
406
+ type: "error",
407
+ message,
408
+ })
409
+ }
410
+ }
411
+
412
+ // ── Per-topic targeted pushes ───────────────────────────────────────────────
413
+
414
+ pushTerminalSnapshot(terminalId: string): void {
415
+ for (const ws of this.sockets) {
416
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
417
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
418
+ if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
419
+ const envelope = this.deps.envelopeBuilder.createEnvelope(id, topic, undefined, ws)
420
+ if (envelope.type !== "snapshot") continue
421
+ const signature = JSON.stringify(envelope.snapshot)
422
+ if (snapshotSignatures.get(id) === signature) continue
423
+ snapshotSignatures.set(id, signature)
424
+ send(ws, envelope)
425
+ }
426
+ }
427
+ }
428
+
429
+ pushTerminalEvent(
430
+ terminalId: string,
431
+ event: Extract<ServerEnvelope, { type: "event" }>["event"]
432
+ ): void {
433
+ for (const ws of this.sockets) {
434
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
435
+ if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
436
+ send(ws, {
437
+ v: PROTOCOL_VERSION,
438
+ type: "event",
439
+ id,
440
+ event,
441
+ })
442
+ }
443
+ }
444
+ }
445
+
446
+ private pushPtyInstancesEvent(
447
+ event: Extract<ServerEnvelope, { type: "event" }>["event"]
448
+ ): void {
449
+ for (const ws of this.sockets) {
450
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
451
+ if (topic.type !== "pty-instances") continue
452
+ send(ws, { v: PROTOCOL_VERSION, type: "event", id, event })
453
+ }
454
+ }
455
+ }
456
+
457
+ private pushOrchRuns(): void {
458
+ for (const ws of this.sockets) {
459
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
460
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
461
+ if (topic.type !== "orch-runs") continue
462
+ const envelope = this.deps.envelopeBuilder.createEnvelope(id, topic, undefined, ws)
463
+ if (envelope.type !== "snapshot") continue
464
+ const signature = JSON.stringify(envelope.snapshot)
465
+ if (snapshotSignatures.get(id) === signature) continue
466
+ snapshotSignatures.set(id, signature)
467
+ send(ws, envelope)
468
+ }
469
+ }
470
+ }
471
+
472
+ // ── Cleanup ─────────────────────────────────────────────────────────────────
473
+
474
+ dispose(): void {
475
+ if (this.pendingBroadcastTimer) {
476
+ clearTimeout(this.pendingBroadcastTimer)
477
+ this.pendingBroadcastTimer = null
478
+ }
479
+ this.deps.agent.setBackgroundErrorReporter?.(null)
480
+ this.disposeTerminalEvents()
481
+ this.disposeKeybindingEvents()
482
+ this.disposeAppSettingsEvents()
483
+ this.disposeUpdateEvents()
484
+ this.disposePtyInstances()
485
+ this.disposeWorkflows()
486
+ this.disposeOrchRuns()
487
+ }
488
+ }