@wrongstack/cli 0.313.1 → 0.316.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 (233) hide show
  1. package/dist/{acp-LQMSMV32.js → acp-CEQINXWB.js} +2 -2
  2. package/dist/acp-registry-cache.d.ts +0 -2
  3. package/dist/acp-server-agent.d.ts +2 -1
  4. package/dist/arg-parser.d.ts +2 -1
  5. package/dist/{auth-VQMSZGKE.js → auth-GZ5A5OF5.js} +3 -3
  6. package/dist/auth-menu/local.d.ts +3 -2
  7. package/dist/boot/boot-auto-thin.d.ts +44 -0
  8. package/dist/boot/brain-decision-log.d.ts +2 -1
  9. package/dist/boot/config-backup-recovery.d.ts +2 -6
  10. package/dist/boot/dispatch-singleshot.d.ts +2 -1
  11. package/dist/boot/dispatch-webui.d.ts +5 -1
  12. package/dist/boot/event-wiring.d.ts +1 -1
  13. package/dist/boot/execution-mode.d.ts +2 -1
  14. package/dist/boot/governance-shadow-bridge.d.ts +4 -3
  15. package/dist/boot/governance-trace-context.d.ts +4 -4
  16. package/dist/boot/launch-menu.d.ts +2 -2
  17. package/dist/boot/system-prompt-builder.d.ts +1 -1
  18. package/dist/boot/system-prompt-menu.d.ts +10 -58
  19. package/dist/boot/system-prompt.d.ts +3 -3
  20. package/dist/boot/tool-registry.d.ts +3 -1
  21. package/dist/boot/tui-coordinator-callbacks.d.ts +2 -1
  22. package/dist/boot/tui-coordinator-setup.d.ts +3 -2
  23. package/dist/boot/tui-goal-wiring.d.ts +2 -1
  24. package/dist/boot/tui-live-sessions.d.ts +3 -1
  25. package/dist/boot/tui-session-resume.d.ts +82 -10
  26. package/dist/boot/tui-session-stub-enrich.d.ts +7 -1
  27. package/dist/boot/tui-settings-adapter.d.ts +3 -2
  28. package/dist/boot/tui-theme-adapter.d.ts +3 -2
  29. package/dist/boot/webui-session-child.d.ts +2 -12
  30. package/dist/boot-auto-thin-BZ675EVT.js +102 -0
  31. package/dist/boot-config.d.ts +1 -1
  32. package/dist/brain-menu/panel-service.d.ts +2 -1
  33. package/dist/chimera-cascade-evidence.d.ts +4 -21
  34. package/dist/chimera-review-task.d.ts +0 -7
  35. package/dist/chimera-subagent-ladder.d.ts +2 -2
  36. package/dist/{chunk-USYOJIV2.js → chunk-32MTOI2I.js} +2 -2
  37. package/dist/{chunk-CNQZ4HG4.js → chunk-37SPPLUD.js} +57 -8
  38. package/dist/chunk-ALE5KHCA.js +1 -0
  39. package/dist/{chunk-GWVLRUVH.js → chunk-G3IMKQRH.js} +25 -6
  40. package/dist/{chunk-BZJ4MP32.js → chunk-GKHUHAIK.js} +8 -3
  41. package/dist/{chunk-3JPTNADJ.js → chunk-GPPRNAKW.js} +1 -1
  42. package/dist/{chunk-KE7E7DPX.js → chunk-HXJYHAR4.js} +1 -1
  43. package/dist/{chunk-X3N5TS2S.js → chunk-JACOMTIH.js} +2 -76
  44. package/dist/{chunk-DGXNL7OT.js → chunk-JHUJUIOH.js} +1 -18
  45. package/dist/{chunk-WOTGMHAH.js → chunk-KG4X5F7Z.js} +1 -1
  46. package/dist/{chunk-NUIHRGPY.js → chunk-NV3JDK4F.js} +1 -1
  47. package/dist/{chunk-E3X4KM7T.js → chunk-NZZOYLQ7.js} +5 -10
  48. package/dist/{chunk-H6C7SO5H.js → chunk-QC5I4VWZ.js} +1 -1
  49. package/dist/chunk-SXJTVNWF.js +78 -0
  50. package/dist/{chunk-AOAIPYFB.js → chunk-SYIBTG3X.js} +2 -2
  51. package/dist/{chunk-PKHGLWUJ.js → chunk-T642Q732.js} +5 -4
  52. package/dist/{chunk-BGFM3PD7.js → chunk-UZ2TMHNH.js} +1 -1
  53. package/dist/{chunk-4S52MIIM.js → chunk-VXCCBI2W.js} +2 -2
  54. package/dist/{chunk-FKWHSFX4.js → chunk-XYKYXFEE.js} +1 -1
  55. package/dist/cli-entry-point.d.ts +9 -0
  56. package/dist/cli-eternal-flag.d.ts +1 -1
  57. package/dist/{cli-main-UOQ2ICUO.js → cli-main-IEAM6SK7.js} +3578 -1626
  58. package/dist/cli-main-helpers.d.ts +3 -3
  59. package/dist/config-doctor.d.ts +34 -3
  60. package/dist/context-limit.d.ts +4 -3
  61. package/dist/{diag-doctor-OPTB2NWY.js → diag-doctor-LFCCBPMD.js} +3 -3
  62. package/dist/execute-deps.d.ts +36 -7
  63. package/dist/{execution-L4CXQEND.js → execution-TZUGCKFU.js} +459 -77
  64. package/dist/execution-chimera-cascade.d.ts +1 -1
  65. package/dist/execution-chimera-review.d.ts +1 -1
  66. package/dist/execution-cleanup.d.ts +2 -1
  67. package/dist/execution-kanban-dispatch.d.ts +2 -1
  68. package/dist/execution-repl-fleet-callbacks.d.ts +3 -2
  69. package/dist/fleet/budget-source.d.ts +2 -1
  70. package/dist/fleet/host-context.d.ts +8 -1
  71. package/dist/fleet/host-director-event-bridges.d.ts +28 -3
  72. package/dist/fleet/host-event-bridge.d.ts +6 -1
  73. package/dist/fleet/host-explore-companion-registry.d.ts +60 -0
  74. package/dist/fleet/host-explore-companion.d.ts +7 -3
  75. package/dist/fleet/host-provider.d.ts +2 -1
  76. package/dist/fleet/host-session-writer.d.ts +9 -1
  77. package/dist/fleet/host-shadow-manager.d.ts +82 -20
  78. package/dist/fleet/host-shadow-pass.d.ts +9 -1
  79. package/dist/fleet/host-spawn-types.d.ts +9 -0
  80. package/dist/fleet/host-status.d.ts +2 -1
  81. package/dist/fleet/host-subagent-factory.d.ts +2 -1
  82. package/dist/fleet/host-subagent-session-audit.d.ts +56 -0
  83. package/dist/fleet/host-supervisor.d.ts +11 -1
  84. package/dist/fleet/host-task-result-report.d.ts +2 -1
  85. package/dist/fleet/host.d.ts +38 -6
  86. package/dist/fleet/status-broadcast.d.ts +2 -1
  87. package/dist/fleet-statusline.d.ts +2 -1
  88. package/dist/goal-host.d.ts +3 -2
  89. package/dist/{governance-V2Z42RBE.js → governance-JSGHHA2R.js} +1 -1
  90. package/dist/governance-hq-telemetry.d.ts +2 -2
  91. package/dist/{hq-2XIXGAER.js → hq-NDTJRRX3.js} +3 -3
  92. package/dist/hq-publisher.d.ts +1 -1
  93. package/dist/hq-recovery-html.d.ts +1 -6
  94. package/dist/hq-server/auth-state.d.ts +2 -1
  95. package/dist/hq-server/auth.d.ts +3 -2
  96. package/dist/hq-server/login-attempt-store.d.ts +2 -1
  97. package/dist/hq-server/mailbox-gateway-manager.d.ts +2 -1
  98. package/dist/hq-server/preflight.d.ts +3 -2
  99. package/dist/hq-server/server-lifecycle.d.ts +2 -1
  100. package/dist/hq-server/snapshot.d.ts +0 -9
  101. package/dist/hq-server/startup.d.ts +2 -3
  102. package/dist/hq-server/types.d.ts +2 -2
  103. package/dist/hq-server/upgrade-handler.d.ts +2 -1
  104. package/dist/hq-server/ws-leader.d.ts +0 -6
  105. package/dist/{hq-server-DBT3OOYD.js → hq-server-OH3SPSE2.js} +3 -13
  106. package/dist/hq-server.d.ts +1 -6
  107. package/dist/hq-static-serve.d.ts +2 -1
  108. package/dist/{hq-ttl-U2FAYFAT.js → hq-ttl-KUO3AU7X.js} +2 -2
  109. package/dist/{hq-tunnel-RFKGC4HI.js → hq-tunnel-LO55RCZE.js} +1 -1
  110. package/dist/hq-tunnel.d.ts +2 -1
  111. package/dist/index.js +113 -109
  112. package/dist/input-reader.d.ts +2 -1
  113. package/dist/kanban-dispatch-route.d.ts +27 -2
  114. package/dist/kanban-hq-sync.d.ts +2 -1
  115. package/dist/launch-hints.d.ts +2 -1
  116. package/dist/mailbox-bridge-bootstrap.d.ts +2 -3
  117. package/dist/{mailbox-serve-5S3PHG5D.js → mailbox-serve-VKQIRCCB.js} +47 -28
  118. package/dist/{mcp-RRPKNRSN.js → mcp-AQKSASHD.js} +1 -1
  119. package/dist/mcp-serve.d.ts +2 -1
  120. package/dist/{modeldiag-H6DKWJ2N.js → modeldiag-4NYBRSFP.js} +1 -1
  121. package/dist/next-task-predictor.d.ts +3 -2
  122. package/dist/permission-prompt.d.ts +2 -1
  123. package/dist/picker.d.ts +0 -18
  124. package/dist/plugin-management.d.ts +3 -14
  125. package/dist/{plugin-usage-GCTYOZPH.js → plugin-usage-AP7SJCDA.js} +2 -2
  126. package/dist/{plugins-EEKHCMBE.js → plugins-HQ5FU2AM.js} +3 -5
  127. package/dist/preflight.d.ts +2 -1
  128. package/dist/{proxy-wiring-G26YGP7S.js → proxy-wiring-EQB5X5YH.js} +3 -3
  129. package/dist/renderer.d.ts +18 -1
  130. package/dist/repl-client-registration.d.ts +2 -1
  131. package/dist/repl.d.ts +1 -1
  132. package/dist/{replay-XU6ZD7GM.js → replay-FJ2JBXUA.js} +1 -1
  133. package/dist/{sdd-runtime-GFZ3NJKS.js → sdd-runtime-FLKCSTYV.js} +3 -5
  134. package/dist/services/commit-message.d.ts +2 -1
  135. package/dist/services/sdd/state.d.ts +0 -2
  136. package/dist/services/sdd-runtime.d.ts +1 -1
  137. package/dist/services/statusline-config.d.ts +27 -4
  138. package/dist/session-event-wiring.d.ts +3 -5
  139. package/dist/{sessions-config-7Q7QJBKJ.js → sessions-config-P2LK2L32.js} +5 -1
  140. package/dist/sessions-doctor-EXHAZSSE.js +146 -0
  141. package/dist/settings-menu.d.ts +2 -1
  142. package/dist/shutdown-cleanup.d.ts +3 -2
  143. package/dist/{simpleui-dist-33VO3QHF.js → simpleui-dist-JKNRCRRK.js} +1 -1
  144. package/dist/simpleui-dist.d.ts +3 -2
  145. package/dist/slash-commands/brain-council.d.ts +0 -4
  146. package/dist/slash-commands/brain-status.d.ts +0 -1
  147. package/dist/slash-commands/command-context.d.ts +16 -2
  148. package/dist/slash-commands/fix-classifier.d.ts +2 -1
  149. package/dist/slash-commands/kanban-agent-helpers.d.ts +2 -1
  150. package/dist/slash-commands/kanban-board-handlers.d.ts +2 -1
  151. package/dist/slash-commands/kanban-lifecycle-diagnostics.d.ts +2 -1
  152. package/dist/slash-commands/kanban-task-subcommands.d.ts +0 -1
  153. package/dist/slash-commands/memory-audience.d.ts +1 -10
  154. package/dist/slash-commands/memory-flags.d.ts +2 -1
  155. package/dist/slash-commands/sdd-command-helpers.d.ts +3 -2
  156. package/dist/slash-commands/sdd.d.ts +2 -2
  157. package/dist/slash-commands/settings-mutations.d.ts +0 -1
  158. package/dist/slash-commands/statusline.d.ts +4 -4
  159. package/dist/slash-commands/telegram-pairing.d.ts +2 -2
  160. package/dist/slash-commands/telegram-setup.d.ts +2 -1
  161. package/dist/slash-commands/tier.d.ts +13 -0
  162. package/dist/subcommands/handlers/acp-connection-gate.d.ts +4 -3
  163. package/dist/subcommands/handlers/bench-run-help.d.ts +3 -8
  164. package/dist/subcommands/handlers/daemon-inventory.d.ts +3 -2
  165. package/dist/subcommands/handlers/governance.d.ts +2 -1
  166. package/dist/subcommands/handlers/model-smoke-test.d.ts +3 -2
  167. package/dist/subcommands/handlers/modeldiag-eval.d.ts +0 -4
  168. package/dist/subcommands/handlers/modeldiag-profiles.d.ts +1 -1
  169. package/dist/subcommands/handlers/models-add-help.d.ts +3 -8
  170. package/dist/subcommands/handlers/sessions-doctor.d.ts +3 -0
  171. package/dist/subcommands/handlers/update.d.ts +1 -1
  172. package/dist/terminal-format.d.ts +3 -2
  173. package/dist/tui-resource-menus.d.ts +2 -1
  174. package/dist/tuneup.d.ts +5 -5
  175. package/dist/{update-ZXNC6I6G.js → update-XKARO3CY.js} +3 -3
  176. package/dist/{update-check-WARHSVZA.js → update-check-LICQNYFK.js} +2 -2
  177. package/dist/update-check.d.ts +1 -1
  178. package/dist/utils/hq-ttl.d.ts +2 -1
  179. package/dist/webui-server/connection-handler.d.ts +34 -2
  180. package/dist/webui-server/prefs-seeding.d.ts +11 -1
  181. package/dist/webui-server/route-contexts.d.ts +45 -5
  182. package/dist/webui-server/session-start-payload.d.ts +13 -0
  183. package/dist/webui-server/setup-events.d.ts +14 -1
  184. package/dist/{webui-server-I53IYBW4.js → webui-server-LZOEH7O6.js} +374 -169
  185. package/dist/webui-server-options.d.ts +30 -4
  186. package/dist/wiring/brain-and-orchestration.d.ts +3 -3
  187. package/dist/wiring/cli-execute-builder.d.ts +3 -0
  188. package/dist/wiring/cli-heap-watchdog.d.ts +2 -1
  189. package/dist/wiring/cli-prompt-and-tools-setup.d.ts +10 -8
  190. package/dist/wiring/codebase-index.d.ts +2 -1
  191. package/dist/wiring/command-host-state.d.ts +61 -5
  192. package/dist/wiring/controllers.d.ts +3 -4
  193. package/dist/wiring/dep-watcher-bridge.d.ts +3 -2
  194. package/dist/wiring/dep-watcher.d.ts +2 -1
  195. package/dist/wiring/design-studio.d.ts +2 -1
  196. package/dist/wiring/director-setup.d.ts +3 -2
  197. package/dist/wiring/domain-terms-mirror.d.ts +2 -1
  198. package/dist/wiring/external-plugins.d.ts +3 -2
  199. package/dist/wiring/fleet-command-handlers.d.ts +2 -2
  200. package/dist/wiring/heap-watchdog-setup.d.ts +2 -1
  201. package/dist/wiring/hq-telemetry.d.ts +3 -2
  202. package/dist/wiring/lifecycle-plugins.d.ts +2 -2
  203. package/dist/wiring/management-tools.d.ts +30 -2
  204. package/dist/wiring/metrics.d.ts +2 -2
  205. package/dist/wiring/plugins.d.ts +0 -1
  206. package/dist/wiring/prompt-journal-recorder.d.ts +1 -1
  207. package/dist/wiring/provider-runtime-setup.d.ts +9 -2
  208. package/dist/wiring/provider-runtime.d.ts +23 -3
  209. package/dist/wiring/provider-status.d.ts +2 -1
  210. package/dist/wiring/provider-utility-tools.d.ts +2 -1
  211. package/dist/wiring/provider.d.ts +2 -1
  212. package/dist/wiring/replay-governance-setup.d.ts +2 -1
  213. package/dist/wiring/replay.d.ts +2 -1
  214. package/dist/wiring/resume-candidate.d.ts +27 -0
  215. package/dist/wiring/runtime-controller-deps.d.ts +8 -1
  216. package/dist/wiring/runtime-dispatch-state.d.ts +2 -1
  217. package/dist/wiring/runtime-lifecycle-deps.d.ts +2 -1
  218. package/dist/wiring/runtime-picker-deps.d.ts +2 -1
  219. package/dist/wiring/session-command-handlers.d.ts +2 -2
  220. package/dist/wiring/session-establishment.d.ts +2 -1
  221. package/dist/wiring/session-registry.d.ts +3 -2
  222. package/dist/wiring/session-runtime.d.ts +1 -1
  223. package/dist/wiring/slash-commands.d.ts +5 -4
  224. package/dist/wiring/teardown-registrar.d.ts +3 -2
  225. package/dist/wiring/vector-memory-setup.d.ts +3 -2
  226. package/dist/wiring/wrongtrace-hooks.d.ts +1 -1
  227. package/dist/wiring/wrongtrace-telemetry.d.ts +2 -1
  228. package/dist/{wrongtrace-telemetry-6M2CCBX5.js → wrongtrace-telemetry-FNLANZ5I.js} +1 -1
  229. package/package.json +30 -30
  230. package/dist/auth-menu.d.ts +0 -7
  231. package/dist/chunk-WHOIR577.js +0 -1
  232. package/dist/webui-server/terminal-log-view.d.ts +0 -47
  233. package/dist/wiring/tools.d.ts +0 -29
@@ -6,10 +6,10 @@ import {
6
6
  } from "./chunk-VCYZ3WZV.js";
7
7
  import {
8
8
  startCliHqConnection
9
- } from "./chunk-FKWHSFX4.js";
9
+ } from "./chunk-XYKYXFEE.js";
10
10
  import {
11
11
  WEBUI_SESSION_CHILD_CAPABILITIES
12
- } from "./chunk-3JPTNADJ.js";
12
+ } from "./chunk-GPPRNAKW.js";
13
13
  import {
14
14
  loadConfigProviders,
15
15
  mutateConfigProviders
@@ -19,19 +19,31 @@ import "./chunk-7OCVIDC7.js";
19
19
  // src/webui-server.ts
20
20
  import * as path7 from "node:path";
21
21
  import { createCompatibilityTrustBoundary, DefaultSecretScrubber } from "@wrongstack/core/security";
22
- import { startSharedHeapWatchdog, wstackGlobalRoot } from "@wrongstack/core/utils";
22
+ import {
23
+ addFatalSalvageHook,
24
+ startSharedHeapWatchdog,
25
+ wstackGlobalRoot
26
+ } from "@wrongstack/core/utils";
23
27
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
24
28
  import {
25
29
  buildWebUIAccessUrl,
30
+ clientWantsSession,
31
+ collectDisplayedSessionIds,
26
32
  createCustomModeStore,
27
33
  createEmbeddedMessageRouter,
28
34
  createEmbeddedProviderOperations,
35
+ createSessionAgentRegistry,
29
36
  envFlag,
30
37
  findFreePort,
31
38
  findInstalledPackageJson,
32
39
  isStrictPort,
33
40
  resolveAuthToken,
34
- sendSerialized
41
+ resolvePendingConfirmsForSession,
42
+ scheduleOwnerlessEmptySessionCleanup,
43
+ sendSerialized,
44
+ startTerminalDashboard,
45
+ startWebUILiveStatusLogger,
46
+ toSessionHistoryEntries
35
47
  } from "@wrongstack/webui-server";
36
48
  import { WebSocketServer } from "ws";
37
49
 
@@ -64,11 +76,12 @@ function createWebuiClientRegistration(deps) {
64
76
  }
65
77
 
66
78
  // src/webui-server/connection-handler.ts
79
+ import { deriveSessionAgents } from "@wrongstack/core/storage";
80
+ import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
67
81
  import {
68
82
  createConnectionLifecycle,
69
83
  verifyClient as verifyWsClient
70
84
  } from "@wrongstack/webui-server";
71
- import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
72
85
  function createConnectionHandler(deps) {
73
86
  const clientHandlers = [
74
87
  deps.goalHandler,
@@ -139,19 +152,46 @@ function createConnectionHandler(deps) {
139
152
  send: deps.send,
140
153
  sessionPayload: deps.sessionPayload,
141
154
  rateLimitMax: deps.rateLimitMax,
142
- onClose: (_ws, client) => {
143
- if (client?.sessionId) deps.abortControllers.delete(client.sessionId);
144
- },
155
+ // NOTE a closed SOCKET is not a stopped run.
156
+ //
157
+ // This used to `abortControllers.delete(client.sessionId)` — a delete
158
+ // WITHOUT an abort, and only for the tab this connection last acted on.
159
+ // Both halves were wrong. The run kept going while `isRunActive` reported
160
+ // `false`, so `session.delete` would happily unlink the journal a live run
161
+ // was still appending to, and the next `user_message` claimed a second
162
+ // lock for the same session and hit `Agent.run() is already in progress`.
163
+ // The other three declared tabs were never touched at all.
164
+ //
165
+ // Doing nothing is the correct behaviour: a background run outlives the
166
+ // tab that started it (the same invariant `retireUndisplayedSessions`
167
+ // honours), and the run's own `finally` releases the lock when it unwinds.
168
+ // A page that comes back is told what is still running by
169
+ // `session.run_state`; a browser that never comes back drains its
170
+ // unanswerable permission prompts through the last-client drain in
171
+ // `connection-lifecycle.ts`, which lets a wedged run finish and release.
172
+ //
173
+ // So there is deliberately NO `onClose` here.
145
174
  buildInitialPayload: async () => {
146
175
  const payload = { ...await deps.buildSessionStartPayload({}, deps.needsSetup) };
176
+ try {
177
+ const open = deps.openSessionIds?.();
178
+ if (open) payload["openSessionIds"] = [.../* @__PURE__ */ new Set([...open, deps.currentSessionId()])];
179
+ } catch {
180
+ console.debug("[WebUI] Failed to list open sessions");
181
+ }
182
+ let replayEvents;
147
183
  try {
148
184
  const replay = await deps.loadReplay?.();
149
- if (replay) Object.assign(payload, buildReplayPayload(replay));
185
+ if (replay) {
186
+ Object.assign(payload, buildReplayPayload(replay));
187
+ replayEvents = replay.events;
188
+ }
150
189
  } catch {
151
190
  console.debug("[WebUI] Failed to load replay");
152
191
  }
153
192
  try {
154
- const sessions = await deps.loadAgentSessions?.();
193
+ const roster = deriveSessionAgents(replayEvents ?? []);
194
+ const sessions = roster.length > 0 ? await deps.loadAgentSessions?.(roster.map((agent) => agent.agentId)) : [];
155
195
  if (sessions?.length) payload["agentSessions"] = sessions;
156
196
  } catch {
157
197
  console.debug("[WebUI] Failed to load agent sessions");
@@ -755,6 +795,9 @@ function createPrefsSeeding(opts) {
755
795
  if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
756
796
  patchLiveAppConfig({ modelMatrix: payload["modelMatrix"] });
757
797
  }
798
+ if (payload["modelTiers"] && typeof payload["modelTiers"] === "object" && !Array.isArray(payload["modelTiers"])) {
799
+ patchLiveAppConfig({ modelTiers: payload["modelTiers"] });
800
+ }
758
801
  if (!opts.profileConfigPath) return;
759
802
  const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : path4.dirname(opts.profileConfigPath);
760
803
  const vault = new DefaultSecretVault2({ keyFile: path4.join(globalRoot, ".key") });
@@ -780,7 +823,7 @@ function createPrefsSeeding(opts) {
780
823
  );
781
824
  };
782
825
  return {
783
- prefSnapshot: () => snapshotPrefs(opts.agent.ctx.meta),
826
+ prefSnapshot: (meta) => snapshotPrefs(meta ?? opts.agent.ctx.meta),
784
827
  persistPrefs
785
828
  };
786
829
  }
@@ -792,7 +835,9 @@ import { SkillInstaller } from "@wrongstack/core/skills";
792
835
  import { PromptUsageStore } from "@wrongstack/core/storage";
793
836
  import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
794
837
  import {
795
- createMailboxRouteHandlers
838
+ createChimeraRouteHandlers,
839
+ createMailboxRouteHandlers,
840
+ rebuildSystemPrompt
796
841
  } from "@wrongstack/webui-server";
797
842
  function createWebuiRouteContexts({
798
843
  opts,
@@ -807,10 +852,17 @@ function createWebuiRouteContexts({
807
852
  persistPrefs,
808
853
  pendingConfirms,
809
854
  abortControllers,
810
- getAbortController,
811
- clearAbortController,
855
+ getSessionAgent,
856
+ peekSessionAgent,
857
+ isSessionLive,
858
+ getForegroundSession,
859
+ setForegroundSession,
860
+ clients,
861
+ onSessionsUndisplayed,
862
+ stopSessionFleet,
812
863
  send,
813
- broadcast
864
+ broadcast,
865
+ broadcastEveryone
814
866
  }) {
815
867
  const brainCtx = {
816
868
  brainSettings: opts.brainSettings,
@@ -823,6 +875,14 @@ function createWebuiRouteContexts({
823
875
  const introspectionConfigStore = opts.agent.container?.safeResolve?.(TOKENS2.ConfigStore);
824
876
  const introspectionCtx = {
825
877
  agent: opts.agent,
878
+ // Answer `diag.get` / `stats.get` / `side_effects.list` from the agent that
879
+ // owns the ASKING session. `opts.agent` is the leader, i.e. whichever tab
880
+ // the runtime last switched to, so a background tab asking for its own
881
+ // stats got the foreground's numbers stamped with the foreground's id —
882
+ // which the browser's session filter then dropped, leaving the panel blank.
883
+ // Read path, so `peek` first: `getAgent` creates, and materialising an
884
+ // agent for a stale id can evict a live tab's.
885
+ getAgent: (sessionId) => peekSessionAgent?.(sessionId) ?? (sessionId ? getSessionAgent(sessionId) : opts.agent),
826
886
  modelsRegistry: opts.modelsRegistry,
827
887
  configStore: introspectionConfigStore,
828
888
  getConfig: () => {
@@ -863,10 +923,10 @@ function createWebuiRouteContexts({
863
923
  skillsPaths?.promptUsage ?? path5.join(profileDir, "prompt-usage.json")
864
924
  )
865
925
  };
866
- const designCtx = {
926
+ const designCtx = (sessionId) => ({
867
927
  projectRoot: skillsProjectRoot,
868
- agentMeta: opts.agent.ctx
869
- };
928
+ agentMeta: (sessionId ? getSessionAgent(sessionId)?.ctx : void 0) ?? opts.agent.ctx
929
+ });
870
930
  const agentConfigCtx = {
871
931
  agent: opts.agent,
872
932
  modeStore: opts.modeStore,
@@ -881,31 +941,86 @@ function createWebuiRouteContexts({
881
941
  broadcast,
882
942
  log: (m) => console.log(m)
883
943
  };
944
+ const promptProjectRoot = () => opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
945
+ const metaForSession = (sessionId) => (sessionId ? getSessionAgent(sessionId)?.ctx.meta : void 0) ?? opts.agent.ctx.meta;
884
946
  const prefsCtx = {
885
947
  meta: opts.agent.ctx.meta,
886
- snapshot: prefSnapshot,
948
+ metaFor: metaForSession,
949
+ snapshot: (sessionId) => prefSnapshot(sessionId ? metaForSession(sessionId) : void 0),
887
950
  persist: persistPrefs,
888
951
  setYolo: opts.onYoloSwitch,
889
952
  setAutonomy: opts.onAutonomySwitch,
890
953
  applyWrongProxyPrefs: opts.onWrongProxyPrefsChange,
891
954
  pendingConfirms,
892
955
  configStore: opts.agent.container?.safeResolve?.(TOKENS2.ConfigStore),
956
+ systemPrompt: {
957
+ paths: () => {
958
+ const wpaths = resolveWstackPaths3({ projectRoot: promptProjectRoot(), globalRoot });
959
+ return {
960
+ globalDir: wpaths.globalInstructions,
961
+ projectDir: wpaths.inProjectInstructions
962
+ };
963
+ },
964
+ profileConfigPath,
965
+ current: () => opts.appConfig?.systemPrompt?.variant ?? "default",
966
+ // Patch the live config before rebuilding — `persistPrefs` writes the
967
+ // file, and the builder reads the variant off the in-memory object.
968
+ applyVariant: async (variant, sessionId) => {
969
+ if (opts.appConfig) {
970
+ opts.appConfig = {
971
+ ...opts.appConfig,
972
+ systemPrompt: { ...opts.appConfig.systemPrompt ?? {}, variant }
973
+ };
974
+ }
975
+ const tools = opts.agent.tools;
976
+ if (!tools || !opts.appConfig) return;
977
+ const targetCtx = sessionId ? getSessionAgent(sessionId).ctx : opts.agent.ctx;
978
+ targetCtx.meta["systemPromptVariant"] = variant;
979
+ await rebuildSystemPrompt(
980
+ {
981
+ modeStore: opts.modeStore,
982
+ memoryStore: opts.memoryStore,
983
+ skillLoader: opts.skillLoader,
984
+ modelCapabilities: {
985
+ maxContextTokens: opts.agent.ctx.provider?.capabilities?.maxContext,
986
+ supportsTools: !!opts.agent.ctx.provider?.capabilities?.tools,
987
+ supportsVision: !!opts.agent.ctx.provider?.capabilities?.vision,
988
+ supportsReasoning: !!opts.agent.ctx.provider?.capabilities?.reasoning
989
+ },
990
+ context: targetCtx,
991
+ toolRegistry: tools,
992
+ getConfig: () => opts.appConfig,
993
+ projectRoot: promptProjectRoot(),
994
+ globalRoot,
995
+ container: opts.agent.container
996
+ },
997
+ typeof targetCtx.meta["mode"] === "string" ? targetCtx.meta["mode"] : "default"
998
+ );
999
+ }
1000
+ },
893
1001
  send,
894
1002
  broadcast
895
1003
  };
896
1004
  const projectsCtx = {
897
1005
  opts,
1006
+ getForegroundSession,
1007
+ setForegroundSession,
898
1008
  abortControllers,
1009
+ // Switching projects re-roots the server, so every conversation's run
1010
+ // has to stop — there is no single "legacy" run to cancel here. The name
1011
+ // is the shared context's; this host answers it with the only registry it
1012
+ // has, one controller per tab. It used to read a process-wide controller
1013
+ // that nothing ever set, so the teardown ran on nothing.
899
1014
  abortLegacyRun: () => {
900
- const controller = getAbortController();
901
- if (controller) {
902
- controller.abort();
903
- clearAbortController();
904
- }
1015
+ for (const controller of abortControllers.values()) controller.abort();
1016
+ abortControllers.clear();
905
1017
  },
906
1018
  buildSessionStart: (overrides) => buildSessionStartPayload(overrides),
907
1019
  send,
908
1020
  broadcast,
1021
+ // A project switch re-roots the whole server; it is the one announcement
1022
+ // that must reach connections which are displaying something else.
1023
+ ...broadcastEveryone ? { broadcastEveryone } : {},
909
1024
  log: (m) => console.log(m)
910
1025
  };
911
1026
  const mailboxRoutes = createMailboxRouteHandlers({
@@ -913,18 +1028,53 @@ function createWebuiRouteContexts({
913
1028
  getGlobalRoot: () => opts.globalConfigPath ? path5.dirname(opts.globalConfigPath) : "",
914
1029
  events: opts.events
915
1030
  });
1031
+ const chimeraRoutes = createChimeraRouteHandlers({
1032
+ // Same lazily-resolved layout the mailbox routes read roots through:
1033
+ // projectDir is the per-project WrongStack home where review-reports.jsonl
1034
+ // lives. A project switch re-roots it, hence the getter.
1035
+ projectDir: () => resolveWstackPaths3({
1036
+ projectRoot: opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "",
1037
+ globalRoot: opts.globalConfigPath ? path5.dirname(opts.globalConfigPath) : ""
1038
+ }).projectDir,
1039
+ send,
1040
+ log: (m) => console.warn(m)
1041
+ });
916
1042
  const sessionsCtx = {
917
1043
  opts,
918
1044
  buildSessionStart: (overrides) => buildSessionStartPayload(overrides),
919
1045
  getCustomModeStore,
1046
+ // Session transitions must re-point the TARGET session's context, not the
1047
+ // leader's — resuming tab 2 was rewriting the context tab 1 ran in.
1048
+ getAgent: getSessionAgent,
1049
+ // Non-creating peek for the hasSession ownership gate.
1050
+ ...peekSessionAgent ? { peekAgent: peekSessionAgent } : {},
1051
+ // Session-keyed, so "is this tab running" is answered per tab and not
1052
+ // "is anything running in this process".
1053
+ isRunActive: (sessionId) => sessionId ? abortControllers.has(sessionId) : abortControllers.size > 0,
1054
+ isSessionLive,
1055
+ // A replayed session brings its own fleet back: the roster comes from that
1056
+ // session's journal, the bodies from here. Scoped by id, because the
1057
+ // transcripts directory is shared by every session of the project.
1058
+ ...opts.agentTranscripts ? {
1059
+ loadAgentSessions: async (subagentIds) => await opts.agentTranscripts?.loadSessionsFromDisk(subagentIds) ?? []
1060
+ } : {},
1061
+ getForegroundSession,
1062
+ setForegroundSession,
1063
+ clients,
1064
+ onSessionsUndisplayed,
920
1065
  send,
921
1066
  broadcast,
922
1067
  log: (m) => console.log(m)
923
1068
  };
924
1069
  const connectionCtx = {
925
1070
  agent: opts.agent,
1071
+ getAgent: getSessionAgent,
1072
+ // Non-creating peek for the hasSession ownership gate (background-tab
1073
+ // requests are legitimate; arbitrary strings are not).
1074
+ ...peekSessionAgent ? { peekAgent: peekSessionAgent } : {},
926
1075
  abortControllers,
927
1076
  pendingConfirms,
1077
+ ...stopSessionFleet ? { stopSessionFleet } : {},
928
1078
  send,
929
1079
  broadcast,
930
1080
  log: (m) => console.log(m)
@@ -939,6 +1089,7 @@ function createWebuiRouteContexts({
939
1089
  prefsCtx,
940
1090
  projectsCtx,
941
1091
  mailboxRoutes,
1092
+ chimeraRoutes,
942
1093
  sessionsCtx,
943
1094
  connectionCtx
944
1095
  };
@@ -958,6 +1109,9 @@ import {
958
1109
  // src/webui-server/session-start-payload.ts
959
1110
  function createSessionStartPayloadBuilder(deps) {
960
1111
  return async function buildSessionStartPayload(overrides, needsSetup = false) {
1112
+ const targetSessionId = typeof overrides?.["sessionId"] === "string" && overrides["sessionId"] ? overrides["sessionId"] : deps.agent.ctx.session?.id ?? deps.session.id;
1113
+ const ctx = deps.getSessionContext?.(targetSessionId) ?? deps.agent.ctx;
1114
+ const startedAt = typeof ctx.session?.startedAt === "string" && ctx.session.startedAt.length > 0 ? ctx.session.startedAt : void 0;
961
1115
  let maxContext = 0;
962
1116
  let inputCost = 0;
963
1117
  let outputCost = 0;
@@ -965,37 +1119,33 @@ function createSessionStartPayloadBuilder(deps) {
965
1119
  const updateInfo = deps.updateInfo;
966
1120
  try {
967
1121
  if (deps.modelsRegistry) {
968
- const m = await deps.modelsRegistry.getModel(
969
- deps.agent.ctx.provider.id,
970
- deps.agent.ctx.model
971
- );
1122
+ const m = await deps.modelsRegistry.getModel(ctx.provider.id, ctx.model);
972
1123
  const registryMax = m?.capabilities.maxContext;
973
- maxContext = registryMax ?? deps.agent.ctx.provider.capabilities?.maxContext ?? 0;
1124
+ maxContext = registryMax ?? ctx.provider.capabilities?.maxContext ?? 0;
974
1125
  const rates = getCostRates(m);
975
1126
  inputCost = rates.input;
976
1127
  outputCost = rates.output;
977
1128
  cacheReadCost = rates.cacheRead;
978
1129
  } else {
979
- maxContext = deps.agent.ctx.provider.capabilities?.maxContext ?? 0;
1130
+ maxContext = ctx.provider.capabilities?.maxContext ?? 0;
980
1131
  }
981
1132
  } catch {
982
1133
  }
983
- const lastInputTokens = typeof deps.agent.ctx.lastRequestTokens === "number" && deps.agent.ctx.lastRequestTokens > 0 ? deps.agent.ctx.lastRequestTokens : 0;
1134
+ const lastInputTokens = typeof ctx.lastRequestTokens === "number" && ctx.lastRequestTokens > 0 ? ctx.lastRequestTokens : 0;
984
1135
  return {
985
- sessionId: deps.agent.ctx.session?.id ?? deps.session.id,
986
- model: deps.agent.ctx.model,
987
- provider: deps.agent.ctx.provider.id,
1136
+ sessionId: targetSessionId,
1137
+ ...startedAt ? { startedAt } : {},
1138
+ model: ctx.model,
1139
+ provider: ctx.provider.id,
988
1140
  mode: deps.modeId ?? "default",
989
1141
  projectName: deps.projectRoot ? path6.basename(deps.projectRoot) : void 0,
990
1142
  // Frontend reads `projectRoot` from session.start (ws-handlers setEnv) —
991
1143
  // omitting it left the store's projectRoot empty after a project switch.
992
- projectRoot: deps.projectRoot ?? deps.agent.ctx.projectRoot ?? "",
993
- cwd: deps.projectRoot ?? deps.agent.ctx.projectRoot ?? "",
1144
+ projectRoot: deps.projectRoot ?? ctx.projectRoot ?? "",
1145
+ cwd: deps.projectRoot ?? ctx.projectRoot ?? "",
994
1146
  needsSetup,
995
1147
  // true when provider/model not configured and running in --webui mode
996
- contextMode: String(
997
- deps.agent.ctx.meta?.["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID
998
- ),
1148
+ contextMode: String(ctx.meta?.["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
999
1149
  maxContext,
1000
1150
  inputCost,
1001
1151
  outputCost,
@@ -1013,6 +1163,7 @@ function createSessionStartPayloadBuilder(deps) {
1013
1163
  }
1014
1164
 
1015
1165
  // src/webui-server/setup-events.ts
1166
+ import { mailboxSessionTag } from "@wrongstack/core/coordination";
1016
1167
  import {
1017
1168
  createEternalSubscription,
1018
1169
  setupEvents as setupCanonicalEvents
@@ -1024,7 +1175,14 @@ function createSetupEvents(deps) {
1024
1175
  const budget = deps.getFleetBudget?.() ?? null;
1025
1176
  deps.broadcast({
1026
1177
  type: "fleet.concurrency_update",
1027
- payload: deps.sessionPayload({
1178
+ // Deliberately UNSTAMPED. The spawn/concurrency budget is one ceiling
1179
+ // for the whole process — every tab draws from it and the client keeps
1180
+ // it in a single global store — so this is a project-wide fact, not a
1181
+ // conversation's. Stamping it with the runtime's session made it a
1182
+ // conversation's by accident: `clientWantsSession` then dropped it for
1183
+ // any page whose declared tabs did not include that session, and the
1184
+ // fleet gauges froze the moment the boot tab was closed.
1185
+ payload: {
1028
1186
  fleetConcurrency: budget?.activeAgents ?? fleetConcurrency,
1029
1187
  fleetConcurrencyMax: budget?.maxConcurrent ?? fleetConcurrencyMax,
1030
1188
  ...budget ? {
@@ -1037,7 +1195,7 @@ function createSetupEvents(deps) {
1037
1195
  checkpointMaxSpawns: budget.checkpointMaxSpawns,
1038
1196
  ceilingMismatch: budget.ceilingMismatch
1039
1197
  } : {}
1040
- })
1198
+ }
1041
1199
  });
1042
1200
  };
1043
1201
  return function setupEvents() {
@@ -1046,7 +1204,11 @@ function createSetupEvents(deps) {
1046
1204
  const maxIterations = deps.agent.ctx?.meta?.["maxIterations"];
1047
1205
  const disposeCanonical = setupCanonicalEvents({
1048
1206
  events: deps.events,
1049
- broadcast: (_clients, message) => deps.broadcast(message),
1207
+ // The third argument is not decoration: it names the tab that OWNS a
1208
+ // subagent, and the frames that pass it carry the subagent's own
1209
+ // session on the payload. Dropping it here routed every subagent
1210
+ // codemap frame at a session no tab subscribes to.
1211
+ broadcast: (_clients, message, targetSessionId) => targetSessionId !== void 0 ? deps.broadcast(message, targetSessionId) : deps.broadcast(message),
1050
1212
  clients: deps.getClients(),
1051
1213
  config: {
1052
1214
  tools: {
@@ -1054,6 +1216,7 @@ function createSetupEvents(deps) {
1054
1216
  }
1055
1217
  },
1056
1218
  context: deps.agent.ctx,
1219
+ ...deps.sessionContext ? { sessionContext: deps.sessionContext } : {},
1057
1220
  pendingConfirms: deps.pendingConfirms,
1058
1221
  globalConfigPath: deps.globalConfigPath,
1059
1222
  onFleetBroadcaster: deps.onFleetBroadcaster,
@@ -1071,7 +1234,7 @@ function createSetupEvents(deps) {
1071
1234
  type: "subagent.event",
1072
1235
  payload: deps.sessionPayload({
1073
1236
  kind: "leader_updated",
1074
- subagentId: "leader",
1237
+ subagentId: `leader@${mailboxSessionTag(deps.currentSessionId())}`,
1075
1238
  isLeader: true,
1076
1239
  name: "Leader",
1077
1240
  status: "running"
@@ -1115,96 +1278,8 @@ import {
1115
1278
  createStreamCoalescer
1116
1279
  } from "@wrongstack/webui-server";
1117
1280
 
1118
- // src/webui-server/terminal-log-view.ts
1119
- import { format } from "node:util";
1120
- var DEFAULT_BUFFER_LINES = 200;
1121
- var MUTED_LEVELS = ["log", "info", "debug"];
1122
- var FORWARDED_LEVELS = ["warn", "error"];
1123
- var DISABLED = {
1124
- enabled: false,
1125
- mutedCount: 0,
1126
- recent: () => [],
1127
- stop: () => {
1128
- }
1129
- };
1130
- function envVerbose() {
1131
- for (const name of ["WEBUI_VERBOSE", "WRONGSTACK_WEBUI_VERBOSE"]) {
1132
- const value = process.env[name]?.trim().toLowerCase();
1133
- if (value === "1" || value === "true" || value === "yes" || value === "on") return true;
1134
- }
1135
- return false;
1136
- }
1137
- function startQuietSurfaceConsole(options = {}) {
1138
- const isTTY = options.isTTY ?? process.stdout.isTTY === true;
1139
- const verbose = options.verbose ?? envVerbose();
1140
- if (!isTTY || verbose) return DISABLED;
1141
- const bufferLines = Math.max(1, Math.trunc(options.bufferLines ?? DEFAULT_BUFFER_LINES));
1142
- const target = options.consoleTarget ?? console;
1143
- const muted = [];
1144
- let mutedCount = 0;
1145
- let stopped = false;
1146
- let lastLine = null;
1147
- let lastEmit = null;
1148
- let repeats = 0;
1149
- const originals = {
1150
- log: target.log,
1151
- info: target.info,
1152
- warn: target.warn,
1153
- error: target.error,
1154
- debug: target.debug
1155
- };
1156
- const wrappers = {};
1157
- const flushRepeats = () => {
1158
- if (repeats === 0 || !lastEmit) return;
1159
- const count = repeats;
1160
- repeats = 0;
1161
- lastEmit.call(target, ` \u2191 previous line repeated ${count}\xD7`);
1162
- };
1163
- for (const level of MUTED_LEVELS) {
1164
- const wrapper = (...args) => {
1165
- mutedCount += 1;
1166
- muted.push(format(...args));
1167
- if (muted.length > bufferLines) muted.shift();
1168
- };
1169
- wrappers[level] = wrapper;
1170
- target[level] = wrapper;
1171
- }
1172
- for (const level of FORWARDED_LEVELS) {
1173
- const original = originals[level];
1174
- const wrapper = (...args) => {
1175
- const line = format(...args);
1176
- if (line === lastLine) {
1177
- repeats += 1;
1178
- return;
1179
- }
1180
- flushRepeats();
1181
- lastLine = line;
1182
- lastEmit = original;
1183
- original.apply(target, args);
1184
- };
1185
- wrappers[level] = wrapper;
1186
- target[level] = wrapper;
1187
- }
1188
- return {
1189
- enabled: true,
1190
- get mutedCount() {
1191
- return mutedCount;
1192
- },
1193
- recent: () => [...muted],
1194
- stop: () => {
1195
- if (stopped) return;
1196
- stopped = true;
1197
- flushRepeats();
1198
- for (const level of Object.keys(originals)) {
1199
- if (target[level] === wrappers[level]) target[level] = originals[level];
1200
- }
1201
- }
1202
- };
1203
- }
1204
-
1205
1281
  // src/webui-server.ts
1206
1282
  async function runWebUI(opts) {
1207
- const terminalLogView = startQuietSurfaceConsole();
1208
1283
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "cli-webui-trusted-host-compat-v1" });
1209
1284
  const host = opts.host ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
1210
1285
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
@@ -1213,6 +1288,11 @@ async function runWebUI(opts) {
1213
1288
  const surface = opts.surface ?? "webui";
1214
1289
  const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
1215
1290
  const requestedHttpPort = opts.httpPort ?? opts.port ?? surfaceDefaults.http;
1291
+ const terminalLogView = startTerminalDashboard({
1292
+ title: surface === "simpleui" ? "SimpleUI" : "WebUI",
1293
+ quiet: !envFlag("WEBUI_LOGS"),
1294
+ getUrl: () => `http://${host}:${httpPort}`
1295
+ });
1216
1296
  const strictPort = opts.strictPort ?? isStrictPort();
1217
1297
  let httpPort = requestedHttpPort;
1218
1298
  if (!strictPort) {
@@ -1225,7 +1305,6 @@ async function runWebUI(opts) {
1225
1305
  const clients = /* @__PURE__ */ new Map();
1226
1306
  const pendingConfirms = /* @__PURE__ */ new Map();
1227
1307
  const secretScrubber = new DefaultSecretScrubber();
1228
- let abortController = null;
1229
1308
  const abortControllers = /* @__PURE__ */ new Map();
1230
1309
  const profileDir = path7.dirname(profileConfigPath);
1231
1310
  let customModeStoreP = null;
@@ -1274,7 +1353,24 @@ async function runWebUI(opts) {
1274
1353
  }
1275
1354
  const { prefSnapshot, persistPrefs } = createPrefsSeeding(opts);
1276
1355
  const sessionStartedAt = Date.now();
1277
- const buildSessionStartPayload = createSessionStartPayloadBuilder(opts);
1356
+ let sessionAgentsRef;
1357
+ const buildSessionStartPayload = createSessionStartPayloadBuilder({
1358
+ ...opts,
1359
+ // Read through to the live `opts`, do NOT snapshot: `projects.select`
1360
+ // re-roots the host by assigning `opts.projectRoot` / `opts.session` on
1361
+ // this very object. A spread copy froze both at boot, so every
1362
+ // `session.start` broadcast after a project switch still announced the
1363
+ // previous project's root — the switch looked like it had not happened.
1364
+ get projectRoot() {
1365
+ return opts.projectRoot;
1366
+ },
1367
+ get session() {
1368
+ return opts.session;
1369
+ },
1370
+ // `peek`, never `get`: building a payload must not materialise an agent
1371
+ // for a session id that arrived from a stale browser tab.
1372
+ getSessionContext: (sessionId) => sessionAgentsRef?.peek(sessionId)?.ctx
1373
+ });
1278
1374
  const { register: registerWebuiClient, unregister: unregisterWebuiClient } = createWebuiClientRegistration({
1279
1375
  projectRoot: opts.projectRoot,
1280
1376
  appConfig: opts.appConfig,
@@ -1282,19 +1378,23 @@ async function runWebUI(opts) {
1282
1378
  hqSessionId: opts.session.id,
1283
1379
  getSessionId: () => opts.agent.ctx.session?.id ?? opts.session.id,
1284
1380
  hqControl: {
1381
+ // HQ speaks for the LEADER — the boot session, the one it registered
1382
+ // itself under (`hqSessionId`) — not for whatever else the browser
1383
+ // has open. This used to abort every controller in the map and clear
1384
+ // it, so a remote "interrupt" issued against the leader also killed
1385
+ // the three other tabs' in-flight runs. Deleting the entries was
1386
+ // wrong on its own terms too: the run's own `end()` owns removal, and
1387
+ // clearing early makes `isRunActive` lie to every tab still running.
1285
1388
  interruptLeader: () => {
1286
- let aborted = false;
1287
- if (abortController) {
1288
- abortController.abort();
1289
- abortController = null;
1290
- aborted = true;
1291
- }
1292
- for (const c of abortControllers.values()) {
1293
- c.abort();
1294
- aborted = true;
1389
+ const leaderId = opts.session.id;
1390
+ const controller = abortControllers.get(leaderId);
1391
+ if (!controller) return false;
1392
+ controller.abort();
1393
+ try {
1394
+ void Promise.resolve(opts.stopSessionFleet?.(leaderId)).catch(() => void 0);
1395
+ } catch {
1295
1396
  }
1296
- abortControllers.clear();
1297
- return aborted;
1397
+ return true;
1298
1398
  },
1299
1399
  allowRunCommand: () => opts.hqAllowExec === true
1300
1400
  }
@@ -1343,6 +1443,16 @@ async function runWebUI(opts) {
1343
1443
  ...opts.vectorMemoryModelCacheDir ? { vectorMemoryModelCacheDir: opts.vectorMemoryModelCacheDir } : {}
1344
1444
  });
1345
1445
  const wss = httpServer ? new WebSocketServer({ server: httpServer.server, maxPayload: 20 * 1024 * 1024 }) : new WebSocketServer({ port: httpPort, host, maxPayload: 20 * 1024 * 1024 });
1446
+ wss.on("error", (err) => {
1447
+ console.error(
1448
+ JSON.stringify({
1449
+ level: "error",
1450
+ event: "webui_server.error",
1451
+ message: err instanceof Error ? err.message : String(err),
1452
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1453
+ })
1454
+ );
1455
+ });
1346
1456
  if (httpServer) {
1347
1457
  const boundPort = surface === "simpleui" ? await startDeferredHttpListen({
1348
1458
  server: httpServer.server,
@@ -1381,7 +1491,8 @@ async function runWebUI(opts) {
1381
1491
  `[WebUI] Frontend not served (run \`pnpm --filter @wrongstack/webui build\`). WS bridge still active on ws://${host}:${httpPort}.`
1382
1492
  );
1383
1493
  }
1384
- const currentSessionId = () => opts.agent.ctx.session?.id ?? opts.session.id;
1494
+ let foregroundSession = opts.agent.ctx.session ?? opts.session;
1495
+ const currentSessionId = () => foregroundSession.id;
1385
1496
  const registryBaseDir = globalRoot;
1386
1497
  let webuiInstanceRegistered = false;
1387
1498
  if (opts.projectRoot) {
@@ -1433,6 +1544,7 @@ async function runWebUI(opts) {
1433
1544
  flushAllStreamBuffers
1434
1545
  } = createStreamCoalescer({ broadcast, sessionPayload });
1435
1546
  const setupEvents = createSetupEvents({
1547
+ sessionContext: (sessionId) => sessionAgentsRef?.peek(sessionId)?.ctx,
1436
1548
  events: opts.events,
1437
1549
  agent: opts.agent,
1438
1550
  subscribeEternalIteration: opts.subscribeEternalIteration,
@@ -1471,6 +1583,90 @@ async function runWebUI(opts) {
1471
1583
  broadcast,
1472
1584
  broadcastSaved: (providers) => embeddedProviderOperations.broadcastSaved(providers)
1473
1585
  });
1586
+ const sessionAgents = createSessionAgentRegistry({
1587
+ template: opts.agent,
1588
+ ...opts.modelsRegistry ? { modelsRegistry: opts.modelsRegistry } : {},
1589
+ isRunActive: (sessionId) => abortControllers.has(sessionId),
1590
+ // A tab that is still on screen must outlive one that was closed, whatever
1591
+ // order their agents were created in.
1592
+ isDisplayed: (sessionId) => {
1593
+ for (const client of clients.values()) {
1594
+ if (client.sessionId === sessionId) return true;
1595
+ if (client.sessionIds?.has(sessionId) === true) return true;
1596
+ }
1597
+ return false;
1598
+ }
1599
+ });
1600
+ sessionAgentsRef = sessionAgents;
1601
+ const stopLiveStatusLogger = startWebUILiveStatusLogger({
1602
+ events: opts.events,
1603
+ dashboard: terminalLogView,
1604
+ getSessionList: () => {
1605
+ const ids = /* @__PURE__ */ new Set();
1606
+ for (const client of clients.values()) {
1607
+ if (client.sessionId) ids.add(client.sessionId);
1608
+ for (const id of client.sessionIds ?? []) ids.add(id);
1609
+ }
1610
+ const currentId = opts.agent.ctx.session?.id ?? opts.session.id;
1611
+ if (ids.size === 0 && currentId) ids.add(currentId);
1612
+ return Array.from(ids).map((id) => {
1613
+ const ctx = sessionAgentsRef?.peek(id)?.ctx;
1614
+ return {
1615
+ id,
1616
+ model: ctx?.model ?? opts.agent.ctx.model ?? "",
1617
+ provider: ctx?.provider?.id ?? opts.agent.ctx.provider?.id ?? "",
1618
+ isRunning: abortControllers.has(id)
1619
+ };
1620
+ });
1621
+ }
1622
+ });
1623
+ const releaseSessionSalvage = addFatalSalvageHook(() => {
1624
+ try {
1625
+ sessionAgents.flushAllSync();
1626
+ } catch {
1627
+ }
1628
+ });
1629
+ const retireUndisplayedSessions = (sessionIds) => {
1630
+ for (const sessionId of sessionIds) {
1631
+ if (!sessionId || sessionId === opts.session.id) continue;
1632
+ if (sessionId === foregroundSession.id) continue;
1633
+ const orphaned = resolvePendingConfirmsForSession(pendingConfirms, sessionId);
1634
+ if (orphaned > 0) {
1635
+ console.log(
1636
+ JSON.stringify({
1637
+ level: "warn",
1638
+ event: "webui.confirm_orphaned_by_tab_close",
1639
+ sessionId,
1640
+ count: orphaned,
1641
+ message: `Denied ${orphaned} unanswerable permission prompt(s) for closed session ${sessionId}.`
1642
+ })
1643
+ );
1644
+ }
1645
+ if (abortControllers.has(sessionId)) continue;
1646
+ opts.onSessionRetired?.(sessionId);
1647
+ const agent = sessionAgents.peek(sessionId);
1648
+ if (!agent || agent === opts.agent) continue;
1649
+ void sessionAgents.endAndClose(sessionId);
1650
+ }
1651
+ };
1652
+ const stopEmptySessionCleanup = opts.sessionStore ? scheduleOwnerlessEmptySessionCleanup({
1653
+ getSessionStore: () => opts.sessionStore,
1654
+ getActiveSessionId: () => foregroundSession.id,
1655
+ getActiveSessionIds: () => collectDisplayedSessionIds({ getSession: () => foregroundSession, clients }),
1656
+ // A run outlives the tab that started it, and a turn that has not
1657
+ // written its first record yet looks exactly like a session nobody
1658
+ // ever used.
1659
+ hasParticipants: (sessionId) => abortControllers.has(sessionId),
1660
+ refreshSessions: async () => {
1661
+ const list = await opts.sessionStore?.list(200);
1662
+ if (!list) return;
1663
+ broadcastEveryone({
1664
+ type: "sessions.list",
1665
+ payload: { sessions: toSessionHistoryEntries(list, foregroundSession.id) }
1666
+ });
1667
+ },
1668
+ logger: consoleLogger
1669
+ }) : null;
1474
1670
  const routeContexts = createWebuiRouteContexts({
1475
1671
  opts,
1476
1672
  profileConfigPath,
@@ -1484,12 +1680,19 @@ async function runWebUI(opts) {
1484
1680
  persistPrefs,
1485
1681
  pendingConfirms,
1486
1682
  abortControllers,
1487
- getAbortController: () => abortController,
1488
- clearAbortController: () => {
1489
- abortController = null;
1683
+ getSessionAgent: (sessionId) => sessionAgents.get(sessionId),
1684
+ peekSessionAgent: (sessionId) => sessionAgents.peek(sessionId),
1685
+ onSessionsUndisplayed: retireUndisplayedSessions,
1686
+ isSessionLive: (sessionId) => sessionAgents.isLive(sessionId),
1687
+ getForegroundSession: () => foregroundSession,
1688
+ setForegroundSession: (next) => {
1689
+ foregroundSession = next;
1490
1690
  },
1691
+ clients,
1692
+ ...opts.stopSessionFleet ? { stopSessionFleet: opts.stopSessionFleet } : {},
1491
1693
  send,
1492
- broadcast
1694
+ broadcast,
1695
+ broadcastEveryone
1493
1696
  });
1494
1697
  const kanbanHostRoutes = createCliKanbanHostRoutes({
1495
1698
  opts,
@@ -1526,6 +1729,7 @@ async function runWebUI(opts) {
1526
1729
  prefsCtx: routeContexts.prefsCtx,
1527
1730
  projectCtx: routeContexts.projectsCtx,
1528
1731
  mailboxRoutes: routeContexts.mailboxRoutes,
1732
+ chimeraRoutes: routeContexts.chimeraRoutes,
1529
1733
  sessionCtx: routeContexts.sessionsCtx,
1530
1734
  conversationCtx: routeContexts.connectionCtx,
1531
1735
  goalHandler,
@@ -1579,12 +1783,11 @@ async function runWebUI(opts) {
1579
1783
  send,
1580
1784
  sessionPayload,
1581
1785
  handleMessage,
1582
- abortControllers,
1583
1786
  pendingConfirms,
1584
1787
  buildSessionStartPayload,
1585
1788
  loadReplay: async () => {
1586
1789
  const activeSession = opts.agent.ctx.session ?? opts.session;
1587
- await activeSession.flush().catch(() => void 0);
1790
+ await activeSession.flush();
1588
1791
  if (opts.sessionStore) {
1589
1792
  const data = await opts.sessionStore.load(activeSession.id);
1590
1793
  return { messages: data.messages, events: data.events, usage: data.usage };
@@ -1592,27 +1795,17 @@ async function runWebUI(opts) {
1592
1795
  const usage = opts.agent.ctx.tokenCounter.total();
1593
1796
  return { messages: opts.agent.ctx.messages, usage };
1594
1797
  },
1595
- loadAgentSessions: async () => await opts.agentTranscripts?.loadSessionsFromDisk() ?? [],
1798
+ loadAgentSessions: async (subagentIds) => await opts.agentTranscripts?.loadSessionsFromDisk(subagentIds) ?? [],
1799
+ // What this process is actually holding right now. The browser
1800
+ // reconciles its persisted tab strip against it, so a restarted server
1801
+ // is not dressed in the previous run's tabs.
1802
+ openSessionIds: () => sessionAgents.ids(),
1596
1803
  needsSetup: opts.needsSetup ?? false
1597
1804
  })
1598
1805
  );
1599
- wss.on("error", (err) => {
1600
- console.error(
1601
- JSON.stringify({
1602
- level: "error",
1603
- event: "webui_server.error",
1604
- message: err instanceof Error ? err.message : String(err),
1605
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1606
- })
1607
- );
1608
- });
1609
1806
  signalShutdown = createWebuiShutdown({
1610
1807
  abortInFlight: () => {
1611
1808
  void embeddedAutoHealDispose?.();
1612
- if (abortController) {
1613
- abortController.abort();
1614
- abortController = null;
1615
- }
1616
1809
  for (const c of abortControllers.values()) c.abort();
1617
1810
  abortControllers.clear();
1618
1811
  },
@@ -1640,6 +1833,9 @@ async function runWebUI(opts) {
1640
1833
  }
1641
1834
  },
1642
1835
  disposeResources: async () => {
1836
+ releaseSessionSalvage();
1837
+ await stopEmptySessionCleanup?.dispose();
1838
+ await sessionAgents.closeAll().catch(() => void 0);
1643
1839
  credentialWatcherClose?.();
1644
1840
  credentialWatcherClose = void 0;
1645
1841
  goalHandler.dispose();
@@ -1665,11 +1861,12 @@ async function runWebUI(opts) {
1665
1861
  pid: process.pid,
1666
1862
  registryBaseDir,
1667
1863
  onStopped: () => {
1864
+ stopLiveStatusLogger();
1668
1865
  const muted = terminalLogView.mutedCount;
1669
1866
  terminalLogView.stop();
1670
1867
  if (muted > 0) {
1671
1868
  console.log(
1672
- `[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_VERBOSE=1 to see them.`
1869
+ `[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_LOGS=1 to stream them.`
1673
1870
  );
1674
1871
  }
1675
1872
  opts.onExit?.();
@@ -1681,7 +1878,15 @@ async function runWebUI(opts) {
1681
1878
  function send(ws, msg) {
1682
1879
  sendSerialized(ws, JSON.stringify(msg));
1683
1880
  }
1684
- function broadcast(msg) {
1881
+ function broadcast(msg, targetSessionId) {
1882
+ const data = JSON.stringify(msg);
1883
+ const payload = msg.payload;
1884
+ const sessionId = targetSessionId ?? (payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0);
1885
+ for (const [ws, client] of clients) {
1886
+ if (clientWantsSession(client, sessionId)) sendSerialized(ws, data);
1887
+ }
1888
+ }
1889
+ function broadcastEveryone(msg) {
1685
1890
  const data = JSON.stringify(msg);
1686
1891
  for (const [ws] of clients) sendSerialized(ws, data);
1687
1892
  }
@@ -1693,4 +1898,4 @@ async function runWebUI(opts) {
1693
1898
  export {
1694
1899
  runWebUI
1695
1900
  };
1696
- //# sourceMappingURL=webui-server-I53IYBW4.js.map
1901
+ //# sourceMappingURL=webui-server-LZOEH7O6.js.map