@wrongstack/cli 1.0.24 → 1.0.26

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 (124) hide show
  1. package/data/providers.json +44 -0
  2. package/dist/{acp-AJWSKWE5.js → acp-LI665LO2.js} +5 -5
  3. package/dist/ascii-mode-SG4PRNLP.js +7 -0
  4. package/dist/{auth-GRBZ7Y3D.js → auth-TE55JF4Z.js} +14 -14
  5. package/dist/{bench-N5K7Z5O3.js → bench-GTLPM22B.js} +2 -2
  6. package/dist/boot/append-system-prompt.d.ts +10 -0
  7. package/dist/boot/ascii-mode.d.ts +12 -0
  8. package/dist/boot/container-wiring.d.ts +4 -2
  9. package/dist/boot/dispatch-singleshot.d.ts +4 -1
  10. package/dist/boot/mcp-config-flag.d.ts +22 -0
  11. package/dist/boot/piped-stdin.d.ts +40 -0
  12. package/dist/boot/restricted-mode.d.ts +15 -0
  13. package/dist/boot/safe-mode.d.ts +30 -0
  14. package/dist/boot/single-shot-budget.d.ts +30 -0
  15. package/dist/boot/stream-json.d.ts +36 -0
  16. package/dist/boot/structured-output.d.ts +16 -0
  17. package/dist/boot/system-prompt-builder.d.ts +2 -0
  18. package/dist/boot/tool-restriction-flags.d.ts +36 -0
  19. package/dist/boot/worktree-sessions.d.ts +21 -0
  20. package/dist/boot-provider-selection.d.ts +67 -0
  21. package/dist/boot.d.ts +1 -38
  22. package/dist/{chunk-LFMVHT3R.js → chunk-2ERX3RPN.js} +11 -13
  23. package/dist/chunk-4TVYGG6G.js +62 -0
  24. package/dist/chunk-5JYDKG5B.js +154 -0
  25. package/dist/{chunk-3VXYY5JC.js → chunk-5UZBUV2H.js} +3 -19
  26. package/dist/chunk-BEHGRNOQ.js +58 -0
  27. package/dist/chunk-CZTXF6HY.js +140 -0
  28. package/dist/{chunk-SKPDE62W.js → chunk-EPR42SWM.js} +36 -6
  29. package/dist/{chunk-YBUCXXYZ.js → chunk-F5DCFAKI.js} +1 -1
  30. package/dist/chunk-HLSHFXTO.js +97 -0
  31. package/dist/{chunk-BLD5DSVA.js → chunk-KJS5543T.js} +2 -2
  32. package/dist/{chunk-QFPQXKCG.js → chunk-KOEO4CJG.js} +21 -6
  33. package/dist/chunk-LBPLTTZ6.js +105 -0
  34. package/dist/{chunk-3OCUS3VN.js → chunk-LDCGZJTC.js} +4 -4
  35. package/dist/chunk-LKPEBZBE.js +143 -0
  36. package/dist/{chunk-DRM7PSW7.js → chunk-OAVBM55Q.js} +3 -3
  37. package/dist/chunk-PZQBGUVZ.js +18 -0
  38. package/dist/{chunk-MHEBWVXL.js → chunk-Q4INH2DG.js} +46 -2
  39. package/dist/{chunk-ZZRSG7VK.js → chunk-RZ4ZDRYA.js} +3 -3
  40. package/dist/{chunk-U46QR4WR.js → chunk-TI7KLHYX.js} +132 -3
  41. package/dist/{chunk-PGKJ2OQP.js → chunk-WQV2NP57.js} +5 -5
  42. package/dist/chunk-X756ZS72.js +329 -0
  43. package/dist/{chunk-JF2MULWF.js → chunk-YHYINL47.js} +47 -15
  44. package/dist/{cli-context-34Q5IJRA.js → cli-context-UVGAN3NO.js} +407 -327
  45. package/dist/{cli-main-AELJDBGU.js → cli-main-TN3KZ2P6.js} +2652 -1978
  46. package/dist/cli-update-notice.d.ts +2 -1
  47. package/dist/config-doctor.d.ts +1 -1
  48. package/dist/{diag-doctor-WIFJH3XH.js → diag-doctor-BIZAFIAT.js} +2 -2
  49. package/dist/{execution-PYC5FCS4.js → execution-7QIP6GRY.js} +1948 -1666
  50. package/dist/fleet/host-types.d.ts +3 -1
  51. package/dist/goal-commands.d.ts +47 -0
  52. package/dist/goal-host.d.ts +1 -14
  53. package/dist/{hq-JTUTFI4L.js → hq-GLZ7QESM.js} +6 -6
  54. package/dist/{hq-server-PC7GZMPT.js → hq-server-YTKDVQPY.js} +2 -2
  55. package/dist/{hq-service-NW56MUP2.js → hq-service-BMMJAHRT.js} +4 -4
  56. package/dist/import-claude-code-32PCSIQI.js +236 -0
  57. package/dist/index.js +12 -8
  58. package/dist/{mailbox-serve-WDOCH6KR.js → mailbox-serve-NW5VZG7R.js} +3 -3
  59. package/dist/{modeldiag-36OGAL5C.js → modeldiag-36VODVIH.js} +8 -8
  60. package/dist/{per-subcommand-help-XE77MTGJ.js → per-subcommand-help-QDAQASPZ.js} +2 -2
  61. package/dist/permission-prompt.d.ts +14 -0
  62. package/dist/permission-rules-view.d.ts +18 -0
  63. package/dist/{permissions-CFDLXETM.js → permissions-PL27UHYD.js} +67 -45
  64. package/dist/{plugin-usage-S5LVGMBW.js → plugin-usage-CIGOOTX3.js} +2 -2
  65. package/dist/{plugins-RHBIVLI7.js → plugins-HQTXUD6A.js} +4 -2
  66. package/dist/preflight.d.ts +1 -1
  67. package/dist/{projects-LMNAGGEU.js → projects-T4CZCA25.js} +3 -6
  68. package/dist/provider-helpers.d.ts +7 -11
  69. package/dist/{providers-models-2CFG46RD.js → providers-models-5Z2PJTEW.js} +5 -5
  70. package/dist/release-asset-download.d.ts +10 -0
  71. package/dist/release-attestation.d.ts +28 -0
  72. package/dist/remote/remote-session.d.ts +64 -0
  73. package/dist/remote/remote-target.d.ts +69 -0
  74. package/dist/remote-O5WXHLWV.js +561 -0
  75. package/dist/{replay-Z3FTJEMM.js → replay-DEI566CW.js} +2 -2
  76. package/dist/services/commit-message.d.ts +2 -1
  77. package/dist/session-move-P2EC2COA.js +44 -0
  78. package/dist/session-move.d.ts +20 -0
  79. package/dist/session-stats.d.ts +10 -0
  80. package/dist/{sessions-config-IQUH2GXT.js → sessions-config-SXTTLNXA.js} +35 -5
  81. package/dist/short-circuit-flags-74Y5N6CF.js +10 -0
  82. package/dist/slash-commands/command-context.d.ts +15 -5
  83. package/dist/slash-commands/goal-refiner.d.ts +4 -49
  84. package/dist/slash-commands/mcp.d.ts +2 -1
  85. package/dist/slash-commands/permissions.d.ts +4 -0
  86. package/dist/slash-commands/settings-context-mutations.d.ts +5 -0
  87. package/dist/slash-commands/settings-limits-mutations.d.ts +12 -0
  88. package/dist/standalone-auto-update-TOTRGCSA.js +249 -0
  89. package/dist/standalone-auto-update.d.ts +65 -0
  90. package/dist/standalone-update-LZO3MNLH.js +24 -0
  91. package/dist/standalone-update.d.ts +37 -1
  92. package/dist/subcommands/handlers/import-claude-code.d.ts +42 -0
  93. package/dist/subcommands/handlers/permissions.d.ts +9 -3
  94. package/dist/subcommands/handlers/remote.d.ts +6 -0
  95. package/dist/subcommands/handlers/version-help.d.ts +5 -0
  96. package/dist/{subcommands-ZTSIN7LG.js → subcommands-Q6ORSB2E.js} +2 -2
  97. package/dist/tui-resume-callback.d.ts +18 -0
  98. package/dist/{typesafe-HVEVPA6R.js → typesafe-U7YB2RRV.js} +5 -5
  99. package/dist/{typesafe-topic-replay-4W73BOLV.js → typesafe-topic-replay-ZILIU3WS.js} +18 -2
  100. package/dist/{update-7XIOEXTD.js → update-TDZX4IL2.js} +4 -4
  101. package/dist/{update-check-2POMBHFB.js → update-check-J5ZVKS73.js} +3 -3
  102. package/dist/version-JB2UAV2A.js +11 -0
  103. package/dist/version-help-XJJQ7YSH.js +10 -0
  104. package/dist/webui-server/route-contexts.d.ts +1 -1
  105. package/dist/webui-server/session-start-payload.d.ts +5 -1
  106. package/dist/{webui-server-L2SICPTB.js → webui-server-YTHLRFD2.js} +509 -366
  107. package/dist/webui-server-lifecycle.d.ts +57 -0
  108. package/dist/webui-server.d.ts +0 -14
  109. package/dist/wiring/brain-and-orchestration.d.ts +3 -1
  110. package/dist/wiring/cli-prompt-and-tools-setup.d.ts +9 -0
  111. package/dist/wiring/cli-slash-commands-setup.d.ts +1 -0
  112. package/dist/wiring/lifecycle-plugins.d.ts +4 -2
  113. package/dist/wiring/metrics.d.ts +9 -1
  114. package/dist/wiring/pipeline.d.ts +2 -7
  115. package/dist/wiring/plugins.d.ts +3 -0
  116. package/dist/wiring/provider-status.d.ts +1 -0
  117. package/dist/wiring/provider-utility-tools.d.ts +3 -1
  118. package/package.json +28 -28
  119. package/dist/chunk-2BKFWIER.js +0 -98
  120. package/dist/chunk-424ORSE7.js +0 -56
  121. package/dist/chunk-QBBF7GIK.js +0 -65
  122. package/dist/short-circuit-flags-SDT53OML.js +0 -10
  123. package/dist/standalone-update-MW47J3AK.js +0 -164
  124. package/dist/version-help-WZ623NWM.js +0 -10
@@ -1,12 +1,12 @@
1
- import {
2
- createKanbanRunMirror
3
- } from "./chunk-VCYZ3WZV.js";
4
1
  import {
5
2
  createKanbanSupervisor
6
3
  } from "./chunk-R4QGL6GB.js";
7
4
  import {
8
5
  WEBUI_SESSION_CHILD_CAPABILITIES
9
6
  } from "./chunk-ZMX5HRLI.js";
7
+ import {
8
+ createKanbanRunMirror
9
+ } from "./chunk-VCYZ3WZV.js";
10
10
  import {
11
11
  createHqCommandDispatcher,
12
12
  createProjectKanbanAssignHandler,
@@ -14,14 +14,365 @@ import {
14
14
  } from "./chunk-2EBVTYJR.js";
15
15
  import {
16
16
  startCliHqConnection
17
- } from "./chunk-BLD5DSVA.js";
17
+ } from "./chunk-KJS5543T.js";
18
+ import {
19
+ CLI_VERSION
20
+ } from "./chunk-F5DCFAKI.js";
18
21
  import {
19
22
  loadConfigProviders,
20
23
  mutateConfigProviders
21
24
  } from "./chunk-LUMETJFO.js";
25
+
26
+ // src/webui-server-lifecycle.ts
27
+ import { toErrorMessage } from "@wrongstack/core/utils/error";
28
+
29
+ // src/webui-server/connection-handler.ts
30
+ import { deriveSessionAgents } from "@wrongstack/core/storage";
31
+ import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
22
32
  import {
23
- CLI_VERSION
24
- } from "./chunk-YBUCXXYZ.js";
33
+ createConnectionLifecycle,
34
+ verifyClient as verifyWsClient
35
+ } from "@wrongstack/webui-server";
36
+ function createConnectionHandler(deps) {
37
+ const clientHandlers = [
38
+ deps.goalHandler,
39
+ deps.specsHandler,
40
+ deps.sddBoardHandler,
41
+ ...deps.sddWizardHandler ? [deps.sddWizardHandler] : [],
42
+ deps.worktreeHandler,
43
+ deps.terminalHandler
44
+ ];
45
+ return createConnectionLifecycle({
46
+ clients: deps.clients,
47
+ pendingConfirms: deps.pendingConfirms,
48
+ // ── Auth trust model ────────────────────────────────────────────────
49
+ // Three layered defenses (see verifyClient in @wrongstack/webui-server/ws-auth):
50
+ // 1. DNS-rebinding guard (Host header must be loopback on loopback bind)
51
+ // 2. Shared-token auth via Cookie (ws_token) or URL query param (?token=)
52
+ // 3. Loopback bootstrap: when requireToken=false (default) AND the server
53
+ // is bound to a loopback interface, browser clients bypass token auth
54
+ // entirely — the Origin header being http://localhost is sufficient.
55
+ //
56
+ // COOKIE PERSISTENCE AFTER TOKEN ROTATION:
57
+ // The HttpOnly ws_token cookie, once set by /ws-auth, survives a process
58
+ // restart. On the next process the expectedToken is a fresh random value,
59
+ // so the old cookie alone would fail the token check. However, on a
60
+ // loopback bind with requireToken=false (default), the loopback-bootstrap
61
+ // path at layer 3 accepts the connection before the cookie is ever
62
+ // compared — the old cookie is harmless because no token is needed.
63
+ //
64
+ // This means a token rotation (e.g. the operator sets a new accessToken
65
+ // in config) does NOT invalidate already-distributed cookies for loopback
66
+ // clients. The mitigating factors:
67
+ // - Loopback bind + requireToken=false already trusts any local process
68
+ // that can reach 127.0.0.1 — a trivial perimeter.
69
+ // - A LAN/0.0.0.0 bind with requireToken=true DOES enforce cookie
70
+ // matching against the new expectedToken on every reconnect.
71
+ // If stronger isolation is needed, set requireToken=true or rebind the
72
+ // server to a non-loopback interface with token enforcement.
73
+ authenticate: (ws, req) => {
74
+ const allowed = verifyWsClient({
75
+ origin: req.headers.origin,
76
+ url: req.url ?? "/",
77
+ hostHeader: req.headers.host,
78
+ remoteAddress: req.socket.remoteAddress,
79
+ cookieHeader: req.headers.cookie,
80
+ wsHost: deps.host,
81
+ expectedToken: deps.wsToken,
82
+ requireToken: deps.requireToken,
83
+ allowedHostnames: deps.publicHostnames,
84
+ allowBrowserUrlToken: Boolean(deps.publicWsUrl)
85
+ });
86
+ if (!allowed) ws.close(4003, "Forbidden");
87
+ return allowed;
88
+ },
89
+ createClient: (ws) => ({ ws, sessionId: deps.currentSessionId() }),
90
+ // Enrich parse-failure / unknown-type logs with per-connection identity
91
+ // so operators can identify which client sent a malformed frame. The
92
+ // `connectionId`/`sessionId` fields are read from the `client` argument
93
+ // directly inside `logRejection`; the CLI's `ConnectedClient` only has
94
+ // `sessionId` (no per-socket `connId`), so the verbose payload carries
95
+ // `sessionId` only. Server-wide identity (`agentId`/`projectRoot`) isn't
96
+ // in scope at this handler and is omitted.
97
+ verboseWsLogging: true,
98
+ registerClient: (ws) => {
99
+ for (const handler of clientHandlers) handler.addClient(ws);
100
+ },
101
+ decode: (raw) => decodeProtocolFrame(raw, "client"),
102
+ dispatch: deps.handleMessage,
103
+ send: deps.send,
104
+ sessionPayload: deps.sessionPayload,
105
+ rateLimitMax: deps.rateLimitMax,
106
+ // NOTE — a closed SOCKET is not a stopped run.
107
+ //
108
+ // This used to `abortControllers.delete(client.sessionId)` — a delete
109
+ // WITHOUT an abort, and only for the tab this connection last acted on.
110
+ // Both halves were wrong. The run kept going while `isRunActive` reported
111
+ // `false`, so `session.delete` would happily unlink the journal a live run
112
+ // was still appending to, and the next `user_message` claimed a second
113
+ // lock for the same session and hit `Agent.run() is already in progress`.
114
+ // The other three declared tabs were never touched at all.
115
+ //
116
+ // Doing nothing is the correct behaviour: a background run outlives the
117
+ // tab that started it (the same invariant `retireUndisplayedSessions`
118
+ // honours), and the run's own `finally` releases the lock when it unwinds.
119
+ // A page that comes back is told what is still running by
120
+ // `session.run_state`; a browser that never comes back drains its
121
+ // unanswerable permission prompts through the last-client drain in
122
+ // `connection-lifecycle.ts`, which lets a wedged run finish and release.
123
+ //
124
+ // So there is deliberately NO `onClose` here.
125
+ buildInitialPayload: async () => {
126
+ const payload = { ...await deps.buildSessionStartPayload({}, deps.needsSetup) };
127
+ try {
128
+ const open = deps.openSessionIds?.();
129
+ if (open) payload["openSessionIds"] = [.../* @__PURE__ */ new Set([...open, deps.currentSessionId()])];
130
+ } catch {
131
+ console.debug("[WebUI] Failed to list open sessions");
132
+ }
133
+ let replayEvents;
134
+ try {
135
+ const replay = await deps.loadReplay?.();
136
+ if (replay) {
137
+ Object.assign(payload, buildReplayPayload(replay));
138
+ replayEvents = replay.events;
139
+ }
140
+ } catch {
141
+ console.debug("[WebUI] Failed to load replay");
142
+ }
143
+ try {
144
+ const roster = deriveSessionAgents(replayEvents ?? []);
145
+ const sessions = roster.length > 0 ? await deps.loadAgentSessions?.(roster.map((agent) => agent.agentId)) : [];
146
+ if (sessions?.length) payload["agentSessions"] = sessions;
147
+ } catch {
148
+ console.debug("[WebUI] Failed to load agent sessions");
149
+ }
150
+ return payload;
151
+ }
152
+ });
153
+ }
154
+
155
+ // src/webui-server/lifecycle.ts
156
+ import {
157
+ announceWebuiReady,
158
+ createWebuiShutdown,
159
+ registerWebuiInstance,
160
+ registerWebuiSignalHandlers
161
+ } from "@wrongstack/webui-server";
162
+
163
+ // src/webui-server/logger-shim.ts
164
+ var structuredLine = (level, message) => JSON.stringify({
165
+ level,
166
+ event: "webui.goal",
167
+ message,
168
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
169
+ });
170
+ var consoleLogger = {
171
+ level: "debug",
172
+ error(msg, _ctx) {
173
+ console.error(structuredLine("error", msg));
174
+ },
175
+ warn(msg, _ctx) {
176
+ console.warn(structuredLine("warn", msg));
177
+ },
178
+ info(msg, _ctx) {
179
+ console.log(structuredLine("info", msg));
180
+ },
181
+ debug(msg, _ctx) {
182
+ console.debug(structuredLine("debug", msg));
183
+ },
184
+ trace(msg, _ctx) {
185
+ console.debug(structuredLine("trace", msg));
186
+ },
187
+ child(_bindings) {
188
+ return this;
189
+ }
190
+ };
191
+
192
+ // src/webui-server-lifecycle.ts
193
+ function runWebuiServerLifecycle({
194
+ host,
195
+ httpPort,
196
+ setupEvents,
197
+ opts,
198
+ wsPort,
199
+ accessUrl,
200
+ wsToken,
201
+ webuiInstanceRegistered,
202
+ wss,
203
+ httpServer,
204
+ requireToken,
205
+ publicHostnames,
206
+ publicWsUrl,
207
+ clients,
208
+ currentSessionId,
209
+ goalHandler,
210
+ specsHandler,
211
+ sddBoardHandler,
212
+ sddWizardHandler,
213
+ worktreeHandler,
214
+ terminalHandler,
215
+ rateLimitMax,
216
+ send,
217
+ sessionPayload,
218
+ handleMessage,
219
+ pendingConfirms,
220
+ buildSessionStartPayload,
221
+ sessionAgents,
222
+ lifecycleState,
223
+ abortControllers,
224
+ flushAllStreamBuffers,
225
+ eventUnsubscribers,
226
+ releaseSessionSalvage,
227
+ autoWakeHost,
228
+ stopEmptySessionCleanup,
229
+ credentialWatcherClose,
230
+ kanbanRunMirror,
231
+ kanbanSupervisor,
232
+ stopKanbanSupervisorMemoryStats,
233
+ unregisterWebuiClient,
234
+ ipv6LoopbackServer,
235
+ registryBaseDir,
236
+ stopLiveStatusLogger,
237
+ terminalLogView
238
+ }) {
239
+ const stopped = new Promise((resolve) => {
240
+ let listeningAnnounced = false;
241
+ const announceListening = () => {
242
+ if (listeningAnnounced) return;
243
+ listeningAnnounced = true;
244
+ console.log(`[WebUI] WebSocket server running on ws://${host}:${httpPort}`);
245
+ try {
246
+ setupEvents();
247
+ opts.onListening?.({
248
+ httpPort,
249
+ wsPort,
250
+ host,
251
+ url: accessUrl,
252
+ authToken: wsToken,
253
+ webuiInstanceRegistered
254
+ });
255
+ } catch (err) {
256
+ consoleLogger.error("setup_events_failed", { message: toErrorMessage(err) });
257
+ }
258
+ };
259
+ wss.on("listening", announceListening);
260
+ if (httpServer?.server.listening || wss.address()) queueMicrotask(announceListening);
261
+ wss.on(
262
+ "connection",
263
+ createConnectionHandler({
264
+ host,
265
+ wsToken,
266
+ requireToken,
267
+ publicHostnames,
268
+ publicWsUrl,
269
+ clients,
270
+ currentSessionId,
271
+ goalHandler,
272
+ specsHandler,
273
+ sddBoardHandler,
274
+ sddWizardHandler,
275
+ worktreeHandler,
276
+ terminalHandler,
277
+ rateLimitMax,
278
+ send,
279
+ sessionPayload,
280
+ handleMessage,
281
+ pendingConfirms,
282
+ buildSessionStartPayload,
283
+ loadReplay: async () => {
284
+ const activeSession = opts.agent.ctx.session ?? opts.session;
285
+ await activeSession.flush();
286
+ if (opts.sessionStore) {
287
+ const data = await opts.sessionStore.load(activeSession.id);
288
+ return { messages: data.messages, events: data.events, usage: data.usage };
289
+ }
290
+ const usage = opts.agent.ctx.tokenCounter.total();
291
+ return { messages: opts.agent.ctx.messages, usage };
292
+ },
293
+ loadAgentSessions: async (subagentIds) => await opts.agentTranscripts?.loadSessionsFromDisk(subagentIds) ?? [],
294
+ // What this process is actually holding right now. The browser
295
+ // reconciles its persisted tab strip against it, so a restarted server
296
+ // is not dressed in the previous run's tabs.
297
+ openSessionIds: () => sessionAgents.ids(),
298
+ needsSetup: opts.needsSetup ?? false
299
+ })
300
+ );
301
+ lifecycleState.signalShutdown = createWebuiShutdown({
302
+ abortInFlight: () => {
303
+ void lifecycleState.embeddedAutoHealDispose?.();
304
+ for (const c of abortControllers.values()) c.abort();
305
+ abortControllers.clear();
306
+ },
307
+ unsubscribeEvents: () => {
308
+ flushAllStreamBuffers();
309
+ for (const unsub of eventUnsubscribers) unsub();
310
+ },
311
+ stopOwnedChildren: async () => {
312
+ try {
313
+ const { getProcessRegistry } = await import("@wrongstack/tools");
314
+ getProcessRegistry().killAll({ force: true, includeProtected: true });
315
+ } catch (err) {
316
+ console.debug(
317
+ `[webui-server] process-registry killAll failed: ${err instanceof Error ? err.message : String(err)}`
318
+ );
319
+ }
320
+ if (opts.mcpRegistry) {
321
+ try {
322
+ await opts.mcpRegistry.stopAll();
323
+ } catch (err) {
324
+ console.debug(
325
+ `[webui-server] mcpRegistry.stopAll failed: ${err instanceof Error ? err.message : String(err)}`
326
+ );
327
+ }
328
+ }
329
+ },
330
+ disposeResources: async () => {
331
+ releaseSessionSalvage();
332
+ autoWakeHost?.dispose();
333
+ await stopEmptySessionCleanup?.dispose();
334
+ await sessionAgents.closeAll().catch(() => void 0);
335
+ credentialWatcherClose?.();
336
+ credentialWatcherClose = void 0;
337
+ goalHandler.dispose();
338
+ sddBoardHandler.dispose();
339
+ worktreeHandler.dispose();
340
+ terminalHandler.dispose();
341
+ kanbanRunMirror?.dispose();
342
+ kanbanSupervisor?.dispose();
343
+ void stopKanbanSupervisorMemoryStats?.();
344
+ await lifecycleState.embeddedAutoHealDispose?.();
345
+ lifecycleState.embeddedAutoHealDispose = null;
346
+ unregisterWebuiClient();
347
+ },
348
+ closeClients: () => {
349
+ for (const [ws] of clients) ws.close();
350
+ clients.clear();
351
+ },
352
+ closeHttpServer: () => {
353
+ ipv6LoopbackServer?.close();
354
+ httpServer?.server.close();
355
+ },
356
+ wss,
357
+ pid: process.pid,
358
+ registryBaseDir,
359
+ onStopped: () => {
360
+ stopLiveStatusLogger();
361
+ const muted = terminalLogView.mutedCount;
362
+ terminalLogView.stop();
363
+ if (muted > 0) {
364
+ console.log(
365
+ `[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_LOGS=1 to stream them.`
366
+ );
367
+ }
368
+ opts.onExit?.();
369
+ resolve();
370
+ }
371
+ });
372
+ registerWebuiSignalHandlers(lifecycleState.signalShutdown);
373
+ });
374
+ return { stopped };
375
+ }
25
376
 
26
377
  // src/webui-server.ts
27
378
  import * as path7 from "node:path";
@@ -31,7 +382,6 @@ import {
31
382
  startSharedHeapWatchdog,
32
383
  wstackGlobalRoot
33
384
  } from "@wrongstack/core/utils";
34
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
35
385
  import {
36
386
  buildWebUIAccessUrl,
37
387
  clientWantsSession,
@@ -51,7 +401,8 @@ import {
51
401
  sendSerialized,
52
402
  stampDispatchSession,
53
403
  startTerminalDashboard,
54
- toSessionHistoryEntries
404
+ toSessionHistoryEntries,
405
+ webuiSessionFrameLog
55
406
  } from "@wrongstack/webui-server";
56
407
  import { verifyClient as verifyWsClient2 } from "@wrongstack/webui-server/server/ws-auth";
57
408
  import { WebSocketServer } from "ws";
@@ -136,182 +487,56 @@ function createEmbeddedClientRegistration(opts, clients, abortControllers, getSe
136
487
  getSessionWriter: (sessionId) => getSessionAgents()?.peek(sessionId)?.ctx.session,
137
488
  hqControl: {
138
489
  // HQ speaks for the LEADER — the boot session, the one it registered
139
- // itself under (`hqSessionId`) — not for whatever else the browser
140
- // has open. This used to abort every controller in the map and clear
141
- // it, so a remote "interrupt" issued against the leader also killed
142
- // the three other tabs' in-flight runs. Deleting the entries was
143
- // wrong on its own terms too: the run's own `end()` owns removal, and
144
- // clearing early makes `isRunActive` lie to every tab still running.
145
- // Every open tab is its own session with its own abort controller, so
146
- // the command's session is the one that gets stopped. Falling back to
147
- // the boot session keeps a dashboard that sends no session — every one
148
- // before this existed — behaving exactly as before.
149
- interruptLeader: (sessionId) => {
150
- const leaderId = sessionId ?? opts.session.id;
151
- const controller = abortControllers.get(leaderId);
152
- if (!controller) return false;
153
- controller.abort();
154
- try {
155
- void Promise.resolve(opts.stopSessionFleet?.(leaderId)).catch(() => void 0);
156
- } catch {
157
- }
158
- return true;
159
- },
160
- allowRunCommand: () => opts.hqAllowExec === true,
161
- // A command naming a tab this process no longer holds is refused, not
162
- // redirected onto the boot session: the operator picked a terminal,
163
- // and steering a different one is worse than not steering at all.
164
- ownsSession: (sessionId) => sessionId === opts.session.id || getSessionAgents()?.has(sessionId) === true
165
- }
166
- });
167
- }
168
- function startEmbeddedLiveStatusLogger(opts, clients, abortControllers, getSessionAgents, terminalLogView) {
169
- return startWebUILiveStatusLogger({
170
- events: opts.events,
171
- dashboard: terminalLogView,
172
- getSessionList: () => {
173
- const ids = /* @__PURE__ */ new Set();
174
- for (const client of clients.values()) {
175
- if (client.sessionId) ids.add(client.sessionId);
176
- for (const id of client.sessionIds ?? []) ids.add(id);
177
- }
178
- const currentId = opts.agent.ctx.session?.id ?? opts.session.id;
179
- if (ids.size === 0 && currentId) ids.add(currentId);
180
- return Array.from(ids).map((id) => {
181
- const ctx = getSessionAgents()?.peek(id)?.ctx;
182
- return {
183
- id,
184
- model: ctx?.model ?? opts.agent.ctx.model ?? "",
185
- provider: ctx?.provider?.id ?? opts.agent.ctx.provider?.id ?? "",
186
- isRunning: abortControllers.has(id)
187
- };
188
- });
189
- }
190
- });
191
- }
192
-
193
- // src/webui-server/connection-handler.ts
194
- import { deriveSessionAgents } from "@wrongstack/core/storage";
195
- import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
196
- import {
197
- createConnectionLifecycle,
198
- verifyClient as verifyWsClient
199
- } from "@wrongstack/webui-server";
200
- function createConnectionHandler(deps) {
201
- const clientHandlers = [
202
- deps.goalHandler,
203
- deps.specsHandler,
204
- deps.sddBoardHandler,
205
- ...deps.sddWizardHandler ? [deps.sddWizardHandler] : [],
206
- deps.worktreeHandler,
207
- deps.terminalHandler
208
- ];
209
- return createConnectionLifecycle({
210
- clients: deps.clients,
211
- pendingConfirms: deps.pendingConfirms,
212
- // ── Auth trust model ────────────────────────────────────────────────
213
- // Three layered defenses (see verifyClient in @wrongstack/webui-server/ws-auth):
214
- // 1. DNS-rebinding guard (Host header must be loopback on loopback bind)
215
- // 2. Shared-token auth via Cookie (ws_token) or URL query param (?token=)
216
- // 3. Loopback bootstrap: when requireToken=false (default) AND the server
217
- // is bound to a loopback interface, browser clients bypass token auth
218
- // entirely — the Origin header being http://localhost is sufficient.
219
- //
220
- // COOKIE PERSISTENCE AFTER TOKEN ROTATION:
221
- // The HttpOnly ws_token cookie, once set by /ws-auth, survives a process
222
- // restart. On the next process the expectedToken is a fresh random value,
223
- // so the old cookie alone would fail the token check. However, on a
224
- // loopback bind with requireToken=false (default), the loopback-bootstrap
225
- // path at layer 3 accepts the connection before the cookie is ever
226
- // compared — the old cookie is harmless because no token is needed.
227
- //
228
- // This means a token rotation (e.g. the operator sets a new accessToken
229
- // in config) does NOT invalidate already-distributed cookies for loopback
230
- // clients. The mitigating factors:
231
- // - Loopback bind + requireToken=false already trusts any local process
232
- // that can reach 127.0.0.1 — a trivial perimeter.
233
- // - A LAN/0.0.0.0 bind with requireToken=true DOES enforce cookie
234
- // matching against the new expectedToken on every reconnect.
235
- // If stronger isolation is needed, set requireToken=true or rebind the
236
- // server to a non-loopback interface with token enforcement.
237
- authenticate: (ws, req) => {
238
- const allowed = verifyWsClient({
239
- origin: req.headers.origin,
240
- url: req.url ?? "/",
241
- hostHeader: req.headers.host,
242
- remoteAddress: req.socket.remoteAddress,
243
- cookieHeader: req.headers.cookie,
244
- wsHost: deps.host,
245
- expectedToken: deps.wsToken,
246
- requireToken: deps.requireToken,
247
- allowedHostnames: deps.publicHostnames,
248
- allowBrowserUrlToken: Boolean(deps.publicWsUrl)
249
- });
250
- if (!allowed) ws.close(4003, "Forbidden");
251
- return allowed;
252
- },
253
- createClient: (ws) => ({ ws, sessionId: deps.currentSessionId() }),
254
- // Enrich parse-failure / unknown-type logs with per-connection identity
255
- // so operators can identify which client sent a malformed frame. The
256
- // `connectionId`/`sessionId` fields are read from the `client` argument
257
- // directly inside `logRejection`; the CLI's `ConnectedClient` only has
258
- // `sessionId` (no per-socket `connId`), so the verbose payload carries
259
- // `sessionId` only. Server-wide identity (`agentId`/`projectRoot`) isn't
260
- // in scope at this handler and is omitted.
261
- verboseWsLogging: true,
262
- registerClient: (ws) => {
263
- for (const handler of clientHandlers) handler.addClient(ws);
264
- },
265
- decode: (raw) => decodeProtocolFrame(raw, "client"),
266
- dispatch: deps.handleMessage,
267
- send: deps.send,
268
- sessionPayload: deps.sessionPayload,
269
- rateLimitMax: deps.rateLimitMax,
270
- // NOTE — a closed SOCKET is not a stopped run.
271
- //
272
- // This used to `abortControllers.delete(client.sessionId)` — a delete
273
- // WITHOUT an abort, and only for the tab this connection last acted on.
274
- // Both halves were wrong. The run kept going while `isRunActive` reported
275
- // `false`, so `session.delete` would happily unlink the journal a live run
276
- // was still appending to, and the next `user_message` claimed a second
277
- // lock for the same session and hit `Agent.run() is already in progress`.
278
- // The other three declared tabs were never touched at all.
279
- //
280
- // Doing nothing is the correct behaviour: a background run outlives the
281
- // tab that started it (the same invariant `retireUndisplayedSessions`
282
- // honours), and the run's own `finally` releases the lock when it unwinds.
283
- // A page that comes back is told what is still running by
284
- // `session.run_state`; a browser that never comes back drains its
285
- // unanswerable permission prompts through the last-client drain in
286
- // `connection-lifecycle.ts`, which lets a wedged run finish and release.
287
- //
288
- // So there is deliberately NO `onClose` here.
289
- buildInitialPayload: async () => {
290
- const payload = { ...await deps.buildSessionStartPayload({}, deps.needsSetup) };
291
- try {
292
- const open = deps.openSessionIds?.();
293
- if (open) payload["openSessionIds"] = [.../* @__PURE__ */ new Set([...open, deps.currentSessionId()])];
294
- } catch {
295
- console.debug("[WebUI] Failed to list open sessions");
296
- }
297
- let replayEvents;
298
- try {
299
- const replay = await deps.loadReplay?.();
300
- if (replay) {
301
- Object.assign(payload, buildReplayPayload(replay));
302
- replayEvents = replay.events;
490
+ // itself under (`hqSessionId`) — not for whatever else the browser
491
+ // has open. This used to abort every controller in the map and clear
492
+ // it, so a remote "interrupt" issued against the leader also killed
493
+ // the three other tabs' in-flight runs. Deleting the entries was
494
+ // wrong on its own terms too: the run's own `end()` owns removal, and
495
+ // clearing early makes `isRunActive` lie to every tab still running.
496
+ // Every open tab is its own session with its own abort controller, so
497
+ // the command's session is the one that gets stopped. Falling back to
498
+ // the boot session keeps a dashboard that sends no session — every one
499
+ // before this existed — behaving exactly as before.
500
+ interruptLeader: (sessionId) => {
501
+ const leaderId = sessionId ?? opts.session.id;
502
+ const controller = abortControllers.get(leaderId);
503
+ if (!controller) return false;
504
+ controller.abort();
505
+ try {
506
+ void Promise.resolve(opts.stopSessionFleet?.(leaderId)).catch(() => void 0);
507
+ } catch {
303
508
  }
304
- } catch {
305
- console.debug("[WebUI] Failed to load replay");
306
- }
307
- try {
308
- const roster = deriveSessionAgents(replayEvents ?? []);
309
- const sessions = roster.length > 0 ? await deps.loadAgentSessions?.(roster.map((agent) => agent.agentId)) : [];
310
- if (sessions?.length) payload["agentSessions"] = sessions;
311
- } catch {
312
- console.debug("[WebUI] Failed to load agent sessions");
509
+ return true;
510
+ },
511
+ allowRunCommand: () => opts.hqAllowExec === true,
512
+ // A command naming a tab this process no longer holds is refused, not
513
+ // redirected onto the boot session: the operator picked a terminal,
514
+ // and steering a different one is worse than not steering at all.
515
+ ownsSession: (sessionId) => sessionId === opts.session.id || getSessionAgents()?.has(sessionId) === true
516
+ }
517
+ });
518
+ }
519
+ function startEmbeddedLiveStatusLogger(opts, clients, abortControllers, getSessionAgents, terminalLogView) {
520
+ return startWebUILiveStatusLogger({
521
+ events: opts.events,
522
+ dashboard: terminalLogView,
523
+ getSessionList: () => {
524
+ const ids = /* @__PURE__ */ new Set();
525
+ for (const client of clients.values()) {
526
+ if (client.sessionId) ids.add(client.sessionId);
527
+ for (const id of client.sessionIds ?? []) ids.add(id);
313
528
  }
314
- return payload;
529
+ const currentId = opts.agent.ctx.session?.id ?? opts.session.id;
530
+ if (ids.size === 0 && currentId) ids.add(currentId);
531
+ return Array.from(ids).map((id) => {
532
+ const ctx = getSessionAgents()?.peek(id)?.ctx;
533
+ return {
534
+ id,
535
+ model: ctx?.model ?? opts.agent.ctx.model ?? "",
536
+ provider: ctx?.provider?.id ?? opts.agent.ctx.provider?.id ?? "",
537
+ isRunning: abortControllers.has(id)
538
+ };
539
+ });
315
540
  }
316
541
  });
317
542
  }
@@ -319,7 +544,7 @@ function createConnectionHandler(deps) {
319
544
  // src/webui-server/credential-watcher.ts
320
545
  import { TOKENS } from "@wrongstack/core/kernel";
321
546
  import { watchProviderConfig } from "@wrongstack/core/storage";
322
- import { toErrorMessage } from "@wrongstack/core/utils/error";
547
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
323
548
  import { makeProviderFromConfig } from "@wrongstack/providers";
324
549
 
325
550
  // src/webui-server/provider-config.ts
@@ -474,7 +699,7 @@ function startWebuiCredentialWatcher({
474
699
  console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
475
700
  } catch (err) {
476
701
  console.warn(
477
- `[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage(err)}`
702
+ `[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage2(err)}`
478
703
  );
479
704
  }
480
705
  },
@@ -506,35 +731,6 @@ import {
506
731
  WorktreeWebSocketHandler
507
732
  } from "@wrongstack/webui-server";
508
733
 
509
- // src/webui-server/logger-shim.ts
510
- var structuredLine = (level, message) => JSON.stringify({
511
- level,
512
- event: "webui.goal",
513
- message,
514
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
515
- });
516
- var consoleLogger = {
517
- level: "debug",
518
- error(msg, _ctx) {
519
- console.error(structuredLine("error", msg));
520
- },
521
- warn(msg, _ctx) {
522
- console.warn(structuredLine("warn", msg));
523
- },
524
- info(msg, _ctx) {
525
- console.log(structuredLine("info", msg));
526
- },
527
- debug(msg, _ctx) {
528
- console.debug(structuredLine("debug", msg));
529
- },
530
- trace(msg, _ctx) {
531
- console.debug(structuredLine("trace", msg));
532
- },
533
- child(_bindings) {
534
- return this;
535
- }
536
- };
537
-
538
734
  // src/webui-server/node-pty-loader.ts
539
735
  import { existsSync } from "node:fs";
540
736
  import { createRequire } from "node:module";
@@ -602,7 +798,7 @@ function createNodePtyLoader(importMetaUrl, logger) {
602
798
 
603
799
  // src/webui-server/domain-handlers.ts
604
800
  function createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror) {
605
- const goalStoreDir = opts.projectRoot ? path3.join(opts.projectRoot, ".wrongstack", "goal") : path3.join(os.tmpdir(), ".wrongstack", "goal");
801
+ const goalStoreDir = opts.projectRoot ? resolveWstackPaths({ projectRoot: opts.projectRoot }).projectAutophase : path3.join(os.tmpdir(), ".wrongstack", "goal");
606
802
  const goalHandler = new GoalWebSocketHandler(
607
803
  opts.agent,
608
804
  opts.agent.ctx,
@@ -610,7 +806,8 @@ function createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror) {
610
806
  goalStoreDir,
611
807
  opts.events,
612
808
  opts.projectRoot,
613
- kanbanRunMirror ? (graphId, state) => kanbanRunMirror.onGoalState(graphId, state) : void 0
809
+ kanbanRunMirror ? (graphId, state) => kanbanRunMirror.onGoalState(graphId, state) : void 0,
810
+ opts.sddSubagentFactory
614
811
  );
615
812
  const worktreeHandler = new WorktreeWebSocketHandler(opts.events, consoleLogger);
616
813
  const loadNodePtyViaWebui = createNodePtyLoader(import.meta.url, consoleLogger);
@@ -837,14 +1034,6 @@ function createCliKanbanHostRoutes(deps) {
837
1034
  };
838
1035
  }
839
1036
 
840
- // src/webui-server/lifecycle.ts
841
- import {
842
- announceWebuiReady,
843
- createWebuiShutdown,
844
- registerWebuiInstance,
845
- registerWebuiSignalHandlers
846
- } from "@wrongstack/webui-server";
847
-
848
1037
  // src/webui-server/listen-helpers.ts
849
1038
  import * as http from "node:http";
850
1039
  import { listenWithRetry } from "@wrongstack/webui-server";
@@ -1008,6 +1197,7 @@ import {
1008
1197
  import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
1009
1198
  import { SkillInstaller } from "@wrongstack/core/skills";
1010
1199
  import { PromptUsageStore } from "@wrongstack/core/storage";
1200
+ import { resolveTokenSavingTier } from "@wrongstack/core/types";
1011
1201
  import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
1012
1202
  import {
1013
1203
  createChimeraRouteHandlers,
@@ -1142,6 +1332,16 @@ function createWebuiRouteContexts({
1142
1332
  pendingConfirms,
1143
1333
  configStore: opts.agent.container?.safeResolve?.(TOKENS2.ConfigStore),
1144
1334
  systemPrompt: {
1335
+ previewContext: (sessionId) => {
1336
+ const target = sessionId ? getSessionAgent(sessionId).ctx : opts.agent.ctx;
1337
+ return {
1338
+ toolNames: target.tools.map((tool) => tool.name),
1339
+ tier: resolveTokenSavingTier(
1340
+ opts.appConfig?.features?.tokenSavingMode,
1341
+ target.provider?.capabilities?.maxContext
1342
+ )
1343
+ };
1344
+ },
1145
1345
  paths: () => {
1146
1346
  const wpaths = resolveWstackPaths3({ projectRoot: promptProjectRoot(), globalRoot });
1147
1347
  return {
@@ -1291,6 +1491,7 @@ function createWebuiRouteContexts({
1291
1491
  // src/webui-server/session-start-payload.ts
1292
1492
  import * as path6 from "node:path";
1293
1493
  import { DEFAULT_CONTEXT_WINDOW_MODE_ID } from "@wrongstack/core/types";
1494
+ import { catalogProviderIdFor } from "@wrongstack/providers";
1294
1495
  import { protocolAdvertisement } from "@wrongstack/webui-protocol";
1295
1496
 
1296
1497
  // src/webui-server/cost-helpers.ts
@@ -1312,7 +1513,11 @@ function createSessionStartPayloadBuilder(deps) {
1312
1513
  const updateInfo = deps.updateInfo;
1313
1514
  try {
1314
1515
  if (deps.modelsRegistry) {
1315
- const m = await deps.modelsRegistry.getModel(ctx.provider.id, ctx.model);
1516
+ const catalogId = catalogProviderIdFor(
1517
+ ctx.provider.id,
1518
+ deps.appConfig?.providers?.[ctx.provider.id]?.type
1519
+ );
1520
+ const m = await deps.modelsRegistry.getModel(catalogId, ctx.model);
1316
1521
  const registryMax = m?.capabilities.maxContext;
1317
1522
  maxContext = registryMax ?? ctx.provider.capabilities?.maxContext ?? 0;
1318
1523
  const rates = getCostRates(m);
@@ -1562,6 +1767,9 @@ async function runWebUI(opts) {
1562
1767
  get session() {
1563
1768
  return opts.session;
1564
1769
  },
1770
+ get appConfig() {
1771
+ return opts.appConfig;
1772
+ },
1565
1773
  // `peek`, never `get`: building a payload must not materialise an agent
1566
1774
  // for a session id that arrived from a stale browser tab.
1567
1775
  getSessionContext: (sessionId) => sessionAgentsRef?.peek(sessionId)?.ctx
@@ -1768,7 +1976,7 @@ async function runWebUI(opts) {
1768
1976
  log: (m) => console.log(m)
1769
1977
  };
1770
1978
  const embeddedProviderOperations = createEmbeddedProviderOperations(wsHandlerCtx);
1771
- let credentialWatcherClose = startWebuiCredentialWatcher({
1979
+ const credentialWatcherClose = startWebuiCredentialWatcher({
1772
1980
  opts,
1773
1981
  profileConfigPath,
1774
1982
  broadcast,
@@ -1842,7 +2050,15 @@ async function runWebUI(opts) {
1842
2050
  void sessionAgents.endAndClose(sessionId);
1843
2051
  }
1844
2052
  };
1845
- const stopEmptySessionCleanup = opts.sessionStore ? scheduleOwnerlessEmptySessionCleanup({
2053
+ const refreshSessionHistory = async () => {
2054
+ const list = await opts.sessionStore?.list(200);
2055
+ if (!list) return;
2056
+ broadcastEveryone({
2057
+ type: "sessions.list",
2058
+ payload: { sessions: toSessionHistoryEntries(list, foregroundSession.id) }
2059
+ });
2060
+ };
2061
+ const emptySessionCleanup = opts.sessionStore ? scheduleOwnerlessEmptySessionCleanup({
1846
2062
  getSessionStore: () => opts.sessionStore,
1847
2063
  getActiveSessionId: () => foregroundSession.id,
1848
2064
  getActiveSessionIds: () => collectDisplayedSessionIds({ getSession: () => foregroundSession, clients }),
@@ -1850,16 +2066,19 @@ async function runWebUI(opts) {
1850
2066
  // written its first record yet looks exactly like a session nobody
1851
2067
  // ever used.
1852
2068
  hasParticipants: (sessionId) => abortControllers.has(sessionId),
1853
- refreshSessions: async () => {
1854
- const list = await opts.sessionStore?.list(200);
1855
- if (!list) return;
1856
- broadcastEveryone({
1857
- type: "sessions.list",
1858
- payload: { sessions: toSessionHistoryEntries(list, foregroundSession.id) }
1859
- });
1860
- },
2069
+ refreshSessions: refreshSessionHistory,
1861
2070
  logger: consoleLogger
1862
2071
  }) : null;
2072
+ const offSessionRenamed = opts.sessionStore ? opts.events.on("session.renamed", () => {
2073
+ void refreshSessionHistory().catch(() => void 0);
2074
+ }) : void 0;
2075
+ const stopEmptySessionCleanup = emptySessionCleanup && {
2076
+ runNow: emptySessionCleanup.runNow,
2077
+ dispose: async () => {
2078
+ offSessionRenamed?.();
2079
+ await emptySessionCleanup.dispose();
2080
+ }
2081
+ };
1863
2082
  const routeContexts = createWebuiRouteContexts({
1864
2083
  opts,
1865
2084
  profileConfigPath,
@@ -1936,148 +2155,72 @@ async function runWebUI(opts) {
1936
2155
  kanbanHostRoutes,
1937
2156
  statusTracker: opts.statusTracker
1938
2157
  });
1939
- const stopped = new Promise((resolve) => {
1940
- let listeningAnnounced = false;
1941
- const announceListening = () => {
1942
- if (listeningAnnounced) return;
1943
- listeningAnnounced = true;
1944
- console.log(`[WebUI] WebSocket server running on ws://${host}:${httpPort}`);
1945
- try {
1946
- setupEvents();
1947
- opts.onListening?.({
1948
- httpPort,
1949
- wsPort,
1950
- host,
1951
- url: accessUrl,
1952
- authToken: wsToken,
1953
- webuiInstanceRegistered
1954
- });
1955
- } catch (err) {
1956
- consoleLogger.error("setup_events_failed", { message: toErrorMessage2(err) });
1957
- }
1958
- };
1959
- wss.on("listening", announceListening);
1960
- if (httpServer?.server.listening || wss.address()) queueMicrotask(announceListening);
1961
- wss.on(
1962
- "connection",
1963
- createConnectionHandler({
1964
- host,
1965
- wsToken,
1966
- requireToken,
1967
- publicHostnames,
1968
- publicWsUrl,
1969
- clients,
1970
- currentSessionId,
1971
- goalHandler,
1972
- specsHandler,
1973
- sddBoardHandler,
1974
- sddWizardHandler,
1975
- worktreeHandler,
1976
- terminalHandler,
1977
- rateLimitMax,
1978
- send,
1979
- sessionPayload,
1980
- handleMessage,
1981
- pendingConfirms,
1982
- buildSessionStartPayload,
1983
- loadReplay: async () => {
1984
- const activeSession = opts.agent.ctx.session ?? opts.session;
1985
- await activeSession.flush();
1986
- if (opts.sessionStore) {
1987
- const data = await opts.sessionStore.load(activeSession.id);
1988
- return { messages: data.messages, events: data.events, usage: data.usage };
1989
- }
1990
- const usage = opts.agent.ctx.tokenCounter.total();
1991
- return { messages: opts.agent.ctx.messages, usage };
1992
- },
1993
- loadAgentSessions: async (subagentIds) => await opts.agentTranscripts?.loadSessionsFromDisk(subagentIds) ?? [],
1994
- // What this process is actually holding right now. The browser
1995
- // reconciles its persisted tab strip against it, so a restarted server
1996
- // is not dressed in the previous run's tabs.
1997
- openSessionIds: () => sessionAgents.ids(),
1998
- needsSetup: opts.needsSetup ?? false
1999
- })
2000
- );
2001
- signalShutdown = createWebuiShutdown({
2002
- abortInFlight: () => {
2003
- void embeddedAutoHealDispose?.();
2004
- for (const c of abortControllers.values()) c.abort();
2005
- abortControllers.clear();
2006
- },
2007
- unsubscribeEvents: () => {
2008
- flushAllStreamBuffers();
2009
- for (const unsub of eventUnsubscribers) unsub();
2010
- },
2011
- stopOwnedChildren: async () => {
2012
- try {
2013
- const { getProcessRegistry } = await import("@wrongstack/tools");
2014
- getProcessRegistry().killAll({ force: true, includeProtected: true });
2015
- } catch (err) {
2016
- console.debug(
2017
- `[webui-server] process-registry killAll failed: ${err instanceof Error ? err.message : String(err)}`
2018
- );
2019
- }
2020
- if (opts.mcpRegistry) {
2021
- try {
2022
- await opts.mcpRegistry.stopAll();
2023
- } catch (err) {
2024
- console.debug(
2025
- `[webui-server] mcpRegistry.stopAll failed: ${err instanceof Error ? err.message : String(err)}`
2026
- );
2027
- }
2028
- }
2029
- },
2030
- disposeResources: async () => {
2031
- releaseSessionSalvage();
2032
- autoWakeHost?.dispose();
2033
- await stopEmptySessionCleanup?.dispose();
2034
- await sessionAgents.closeAll().catch(() => void 0);
2035
- credentialWatcherClose?.();
2036
- credentialWatcherClose = void 0;
2037
- goalHandler.dispose();
2038
- sddBoardHandler.dispose();
2039
- worktreeHandler.dispose();
2040
- terminalHandler.dispose();
2041
- kanbanRunMirror?.dispose();
2042
- kanbanSupervisor?.dispose();
2043
- void stopKanbanSupervisorMemoryStats?.();
2044
- await embeddedAutoHealDispose?.();
2045
- embeddedAutoHealDispose = null;
2046
- unregisterWebuiClient();
2158
+ const { stopped } = runWebuiServerLifecycle({
2159
+ host,
2160
+ httpPort,
2161
+ setupEvents,
2162
+ opts,
2163
+ wsPort,
2164
+ accessUrl,
2165
+ wsToken,
2166
+ webuiInstanceRegistered,
2167
+ wss,
2168
+ httpServer,
2169
+ requireToken,
2170
+ publicHostnames,
2171
+ publicWsUrl,
2172
+ clients,
2173
+ currentSessionId,
2174
+ goalHandler,
2175
+ specsHandler,
2176
+ sddBoardHandler,
2177
+ sddWizardHandler,
2178
+ worktreeHandler,
2179
+ terminalHandler,
2180
+ rateLimitMax,
2181
+ send,
2182
+ sessionPayload,
2183
+ handleMessage,
2184
+ pendingConfirms,
2185
+ buildSessionStartPayload,
2186
+ sessionAgents,
2187
+ lifecycleState: {
2188
+ get signalShutdown() {
2189
+ return signalShutdown;
2047
2190
  },
2048
- closeClients: () => {
2049
- for (const [ws] of clients) ws.close();
2050
- clients.clear();
2191
+ set signalShutdown(value) {
2192
+ signalShutdown = value;
2051
2193
  },
2052
- closeHttpServer: () => {
2053
- ipv6LoopbackServer?.close();
2054
- httpServer?.server.close();
2194
+ get embeddedAutoHealDispose() {
2195
+ return embeddedAutoHealDispose;
2055
2196
  },
2056
- wss,
2057
- pid: process.pid,
2058
- registryBaseDir,
2059
- onStopped: () => {
2060
- stopLiveStatusLogger();
2061
- const muted = terminalLogView.mutedCount;
2062
- terminalLogView.stop();
2063
- if (muted > 0) {
2064
- console.log(
2065
- `[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_LOGS=1 to stream them.`
2066
- );
2067
- }
2068
- opts.onExit?.();
2069
- resolve();
2197
+ set embeddedAutoHealDispose(value) {
2198
+ embeddedAutoHealDispose = value;
2070
2199
  }
2071
- });
2072
- registerWebuiSignalHandlers(signalShutdown);
2200
+ },
2201
+ abortControllers,
2202
+ flushAllStreamBuffers,
2203
+ eventUnsubscribers,
2204
+ releaseSessionSalvage,
2205
+ autoWakeHost,
2206
+ stopEmptySessionCleanup,
2207
+ credentialWatcherClose,
2208
+ kanbanRunMirror,
2209
+ kanbanSupervisor,
2210
+ stopKanbanSupervisorMemoryStats,
2211
+ unregisterWebuiClient,
2212
+ ipv6LoopbackServer,
2213
+ registryBaseDir,
2214
+ stopLiveStatusLogger,
2215
+ terminalLogView
2073
2216
  });
2074
2217
  function send(ws, msg) {
2075
2218
  sendSerialized(ws, JSON.stringify(stampDispatchSession(msg)));
2076
2219
  }
2077
2220
  function broadcast(msg, targetSessionId) {
2078
- const data = JSON.stringify(msg);
2079
2221
  const payload = msg.payload;
2080
2222
  const sessionId = targetSessionId ?? (payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0);
2223
+ const data = sessionId ? webuiSessionFrameLog().sequence(sessionId, msg) : JSON.stringify(msg);
2081
2224
  for (const [ws, client] of clients) {
2082
2225
  if (clientWantsSession(client, sessionId)) sendSerialized(ws, data);
2083
2226
  }
@@ -2094,4 +2237,4 @@ async function runWebUI(opts) {
2094
2237
  export {
2095
2238
  runWebUI
2096
2239
  };
2097
- //# sourceMappingURL=webui-server-L2SICPTB.js.map
2240
+ //# sourceMappingURL=webui-server-YTHLRFD2.js.map