@wrongstack/cli 1.0.33 → 1.0.35
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.
- package/dist/{acp-EDGUWU7N.js → acp-T77EAEDB.js} +469 -465
- package/dist/{auth-SUHKFFO4.js → auth-M6LXZTDA.js} +4 -4
- package/dist/auth-menu/openai-codex-oauth.d.ts +1 -1
- package/dist/auth-menu/panel-provider-edits.d.ts +15 -0
- package/dist/auth-menu/panel-service.d.ts +1 -1
- package/dist/boot/governance-evidence-candidate.d.ts +1 -2
- package/dist/boot/system-prompt-builder.d.ts +1 -1
- package/dist/boot/tui-project-spawn.d.ts +0 -1
- package/dist/boot/tui-session-resume-contracts.d.ts +95 -0
- package/dist/boot/tui-session-resume.d.ts +2 -88
- package/dist/boot/tui-settings-read.d.ts +3 -0
- package/dist/{boot-auto-thin-K2DHHYIW.js → boot-auto-thin-YMAZI3QL.js} +2 -2
- package/dist/boot-launch-choices.d.ts +22 -0
- package/dist/boot-provider-gate.d.ts +28 -0
- package/dist/boot-subcommand-dispatch.d.ts +24 -0
- package/dist/boot.d.ts +1 -1
- package/dist/chimera-cascade-evidence.d.ts +1 -1
- package/dist/{chunk-TI7KLHYX.js → chunk-2CMMWRQH.js} +81 -80
- package/dist/{chunk-3LEFXLAW.js → chunk-6SIUTX75.js} +36 -36
- package/dist/{chunk-RQ4UBKYH.js → chunk-6Z6C6ZHH.js} +3 -3
- package/dist/{chunk-5EA6OTLJ.js → chunk-AFOH6CHO.js} +2 -2
- package/dist/{chunk-WHEYHDIE.js → chunk-AKESWZH5.js} +2 -1
- package/dist/{chunk-JHUJUIOH.js → chunk-AT2A7RJZ.js} +4 -16
- package/dist/{chunk-U3SS4NRH.js → chunk-BHXAKTBE.js} +1 -1
- package/dist/{chunk-MPQCHBDJ.js → chunk-CJSHS2K7.js} +5 -1
- package/dist/{chunk-P3GRLV47.js → chunk-GZSKLXYG.js} +208 -141
- package/dist/{chunk-4MDRHMX4.js → chunk-KMU7GF7X.js} +3 -26
- package/dist/{chunk-SWOPJHXI.js → chunk-LDV4ERNV.js} +4 -4
- package/dist/{chunk-24C6VVLM.js → chunk-LDXXMPMS.js} +12 -12
- package/dist/{chunk-UQUYBDMG.js → chunk-M4G5FO7F.js} +1 -1
- package/dist/{chunk-5UZBUV2H.js → chunk-RKU66D2O.js} +6 -4
- package/dist/{chunk-PGNERAZL.js → chunk-RWNI5OWT.js} +2 -2
- package/dist/chunk-TGSPCFBT.js +1 -0
- package/dist/{cli-context-7HK67B2Z.js → cli-context-II57EHD2.js} +1554 -1450
- package/dist/cli-initial-tools.d.ts +54 -0
- package/dist/{cli-main-TQGLKB6D.js → cli-main-YR2WJCPL.js} +38062 -37035
- package/dist/cli-main-boot.d.ts +133 -0
- package/dist/cli-main-orchestration.d.ts +170 -0
- package/dist/cli-main-state.d.ts +19 -0
- package/dist/config-doctor.d.ts +1 -1
- package/dist/{config-transfer-UE37X4B5.js → config-transfer-VOIFY7U4.js} +2 -2
- package/dist/{execution-C2IECWF7.js → execution-IBAZ2EIU.js} +138 -124
- package/dist/fleet/host-director-setup.d.ts +41 -0
- package/dist/fleet/host-spawn-types.d.ts +17 -0
- package/dist/fleet/host.d.ts +2 -1
- package/dist/goal-host-prompts.d.ts +5 -0
- package/dist/goal-host-types.d.ts +94 -0
- package/dist/goal-host.d.ts +2 -91
- package/dist/{hq-KBFDKTDD.js → hq-5OIEKB4V.js} +5 -6
- package/dist/hq-server/handle-types.d.ts +1 -1
- package/dist/hq-server/routes/auth-handlers.d.ts +1 -1
- package/dist/hq-server/routes.d.ts +1 -13
- package/dist/hq-server/utils.d.ts +1 -1
- package/dist/{hq-server-AVNKZ2PV.js → hq-server-DU3ZH3X7.js} +3 -3
- package/dist/{hq-service-FGB7BY2D.js → hq-service-LYZV7EK7.js} +3 -3
- package/dist/index.js +4 -4
- package/dist/live-settings-input.d.ts +2 -0
- package/dist/mailbox-bridge-bootstrap.d.ts +1 -4
- package/dist/picker-effort.d.ts +32 -0
- package/dist/picker-model-picker.d.ts +13 -2
- package/dist/picker-provider-list.d.ts +66 -0
- package/dist/picker.d.ts +12 -12
- package/dist/{providers-models-O75MKLN2.js → providers-models-37DUV2GW.js} +247 -237
- package/dist/repl-suggestions.d.ts +0 -5
- package/dist/{sdd-runtime-PTOSPYI4.js → sdd-runtime-PF72WHLO.js} +3 -20
- package/dist/services/sdd/task-manager.d.ts +1 -12
- package/dist/services/sdd-runtime.d.ts +3 -4
- package/dist/services/statusline-config.d.ts +0 -2
- package/dist/settings-behavior-sections.d.ts +0 -1
- package/dist/short-circuit-flags-EXJBJ5GT.js +10 -0
- package/dist/{skill-suggest-L2EVWB6R.js → skill-suggest-THANK3LV.js} +1 -1
- package/dist/slash-commands/command-context.d.ts +2 -0
- package/dist/slash-commands/fallback-chain-view.d.ts +3 -0
- package/dist/slash-commands/fallback-ref-validation.d.ts +34 -0
- package/dist/slash-commands/goal-refiner.d.ts +10 -2
- package/dist/slash-commands/goals.d.ts +6 -0
- package/dist/slash-commands/kanban-task-completion.d.ts +17 -0
- package/dist/slash-commands/mcp.d.ts +0 -1
- package/dist/slash-commands/memory-purge-domain-terms.d.ts +5 -0
- package/dist/slash-commands/nextsteps-mode.d.ts +13 -0
- package/dist/slash-commands/session-live-status.d.ts +21 -0
- package/dist/slash-commands/session.d.ts +1 -1
- package/dist/slash-commands/settings-feature-ui-mutations.d.ts +8 -0
- package/dist/slash-commands/settings-hq-autonomy-mutations.d.ts +8 -0
- package/dist/slash-commands/settings-persist-deps.d.ts +10 -0
- package/dist/slash-commands/settings-runtime-mutations.d.ts +12 -0
- package/dist/slash-commands/shadow.d.ts +0 -1
- package/dist/slash-commands/tool-auto-thin-commands.d.ts +15 -0
- package/dist/subcommands/handlers/acp-agents.d.ts +9 -0
- package/dist/subcommands/handlers/acp-server.d.ts +7 -0
- package/dist/subcommands/handlers/hq.d.ts +0 -1
- package/dist/subcommands/handlers/providers-models-custom.d.ts +33 -0
- package/dist/subcommands/handlers/providers-models-visibility.d.ts +29 -0
- package/dist/subcommands/handlers/skill-suggest.d.ts +0 -2
- package/dist/{subcommands-CLI6QTLU.js → subcommands-DTKBXLPU.js} +2 -2
- package/dist/{update-J5WHAFPE.js → update-6OEPAEUV.js} +3 -3
- package/dist/utils/win32-cmd.d.ts +1 -1
- package/dist/{version-help-ATOKGMTK.js → version-help-JQGRPDJW.js} +2 -2
- package/dist/webui-server/host-settings.d.ts +17 -0
- package/dist/webui-server/http-bridge.d.ts +32 -0
- package/dist/webui-server/instance-registration.d.ts +19 -0
- package/dist/webui-server/kanban-services.d.ts +9 -0
- package/dist/webui-server/message-router.d.ts +14 -0
- package/dist/webui-server/prefs-seeding.d.ts +1 -2
- package/dist/webui-server/provider-config.d.ts +0 -1
- package/dist/webui-server/provider-plane.d.ts +14 -0
- package/dist/webui-server/session-agents.d.ts +18 -0
- package/dist/webui-server/session-start-live.d.ts +11 -0
- package/dist/webui-server/stream-coalescer.d.ts +1 -1
- package/dist/webui-server/transport.d.ts +12 -0
- package/dist/{webui-server-GQOVLQS3.js → webui-server-6IC3SLZG.js} +1600 -1435
- package/dist/webui-session-maintenance.d.ts +16 -0
- package/dist/wiring/brain-and-orchestration.d.ts +1 -1
- package/dist/wiring/cli-slash-commands-setup.d.ts +1 -0
- package/dist/wiring/command-host-state.d.ts +11 -2
- package/dist/wiring/dep-watcher-bridge.d.ts +8 -1
- package/dist/wiring/session-command-handlers.d.ts +2 -1
- package/dist/worktree-command-host.d.ts +31 -0
- package/package.json +30 -30
- package/dist/chunk-WHOIR577.js +0 -1
- package/dist/short-circuit-flags-D6VAG47Y.js +0 -10
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WEBUI_SESSION_CHILD_CAPABILITIES
|
|
3
|
-
} from "./chunk-P3Q3KMFH.js";
|
|
4
1
|
import {
|
|
5
2
|
createKanbanRunMirror
|
|
6
3
|
} from "./chunk-VCYZ3WZV.js";
|
|
7
4
|
import {
|
|
8
5
|
createKanbanSupervisor
|
|
9
6
|
} from "./chunk-R4QGL6GB.js";
|
|
7
|
+
import {
|
|
8
|
+
WEBUI_SESSION_CHILD_CAPABILITIES
|
|
9
|
+
} from "./chunk-P3Q3KMFH.js";
|
|
10
10
|
import {
|
|
11
11
|
loadConfigProviders,
|
|
12
12
|
mutateConfigProviders
|
|
@@ -23,821 +23,273 @@ import {
|
|
|
23
23
|
CLI_VERSION
|
|
24
24
|
} from "./chunk-F5DCFAKI.js";
|
|
25
25
|
|
|
26
|
-
// src/webui-server
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import { deriveSessionAgents } from "@wrongstack/core/storage";
|
|
31
|
-
import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
|
|
26
|
+
// src/webui-server.ts
|
|
27
|
+
import * as path8 from "node:path";
|
|
28
|
+
import { createCompatibilityTrustBoundary, DefaultSecretScrubber } from "@wrongstack/core/security";
|
|
29
|
+
import { addFatalSalvageHook } from "@wrongstack/core/utils";
|
|
32
30
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
buildWebUIAccessUrl,
|
|
32
|
+
createCustomModeStore,
|
|
33
|
+
createWebuiLeaderAutoWakeHost,
|
|
34
|
+
envFlag as envFlag2,
|
|
35
|
+
findFreePort,
|
|
36
|
+
resolveAuthToken,
|
|
37
|
+
startTerminalDashboard
|
|
35
38
|
} from "@wrongstack/webui-server";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
}
|
|
39
|
+
|
|
40
|
+
// src/webui-client-observability.ts
|
|
41
|
+
import { startWebUILiveStatusLogger } from "@wrongstack/webui-server";
|
|
42
|
+
|
|
43
|
+
// src/webui-server/client-registration.ts
|
|
44
|
+
import { mailboxSessionTag } from "@wrongstack/core/coordination";
|
|
45
|
+
import { createWebuiClientPresence } from "@wrongstack/webui-server";
|
|
46
|
+
function createWebuiClientRegistration(deps) {
|
|
47
|
+
const control = deps.hqControl;
|
|
48
|
+
return createWebuiClientPresence({
|
|
49
|
+
projectRoot: deps.projectRoot,
|
|
50
|
+
appConfig: deps.appConfig,
|
|
51
|
+
events: deps.events,
|
|
52
|
+
hqSessionId: deps.hqSessionId,
|
|
53
|
+
getSessionId: deps.getSessionId,
|
|
54
|
+
clientVersion: CLI_VERSION,
|
|
55
|
+
...deps.listSessions ? { listSessions: deps.listSessions } : {},
|
|
56
|
+
...deps.isSessionOwnedElsewhere ? { isSessionOwnedElsewhere: deps.isSessionOwnedElsewhere } : {},
|
|
57
|
+
...deps.getSessionWriter ? { getSessionWriter: deps.getSessionWriter } : {},
|
|
58
|
+
startHqConnection: (options) => startCliHqConnection({
|
|
59
|
+
...options,
|
|
60
|
+
capabilities: options.capabilities,
|
|
61
|
+
// Embedded in the CLI host, whose own HQ connection already syncs
|
|
62
|
+
// this project's Kanban boards; a second sync is a second writer.
|
|
63
|
+
ownKanbanSync: false
|
|
64
|
+
}),
|
|
65
|
+
...control ? {
|
|
66
|
+
createCommandHandler: (mailbox, approvals) => createHqCommandDispatcher({
|
|
67
|
+
steerMailbox: mailbox,
|
|
68
|
+
interruptLeader: control.interruptLeader,
|
|
69
|
+
allowRunCommand: control.allowRunCommand,
|
|
70
|
+
// Same registry the approval bridge publishes from, so every
|
|
71
|
+
// prompt HQ shows is one this handler can actually answer.
|
|
72
|
+
resolveApproval: (toolUseId, decision, sessionId) => approvals.resolve(toolUseId, decision, sessionId),
|
|
73
|
+
// Same registry again: it publishes `user_input.requested`, so
|
|
74
|
+
// it has to be the one that answers `answer-input`.
|
|
75
|
+
resolveUserInput: (requestId, response, sessionId) => approvals.resolveUserInput?.(requestId, response, sessionId) ?? false,
|
|
76
|
+
...deps.projectRoot ? {
|
|
77
|
+
kanbanTransition: createProjectKanbanTransitionHandler(deps.projectRoot),
|
|
78
|
+
kanbanAssign: createProjectKanbanAssignHandler(deps.projectRoot)
|
|
79
|
+
} : {},
|
|
80
|
+
// The session HQ actually SHOWS for this client — the boot
|
|
81
|
+
// session the telemetry bridge publishes under and the only one
|
|
82
|
+
// `interruptLeader` speaks for. `getSessionId` follows whichever
|
|
83
|
+
// browser tab is in front, which is a different session HQ has
|
|
84
|
+
// no view of and cannot control.
|
|
85
|
+
sessionTag: () => mailboxSessionTag(deps.hqSessionId),
|
|
86
|
+
// The session HQ falls back to when a command names none — the
|
|
87
|
+
// boot tab. A command that DOES name one is honoured (and
|
|
88
|
+
// refused when this host no longer has it) via `ownsSession`.
|
|
89
|
+
sessionId: () => deps.hqSessionId,
|
|
90
|
+
...control.ownsSession ? { ownsSession: control.ownsSession } : {},
|
|
91
|
+
...control.spawnAgent ? { spawnAgent: control.spawnAgent } : {},
|
|
92
|
+
...control.killFleet ? { killFleet: control.killFleet } : {},
|
|
93
|
+
...control.terminateAgent ? { terminateAgent: control.terminateAgent } : {}
|
|
94
|
+
})
|
|
95
|
+
} : {}
|
|
152
96
|
});
|
|
153
97
|
}
|
|
154
98
|
|
|
155
|
-
// src/webui-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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;
|
|
99
|
+
// src/webui-client-observability.ts
|
|
100
|
+
function createEmbeddedClientRegistration(opts, clients, abortControllers, getSessionAgents) {
|
|
101
|
+
const interruptSession = (sessionId) => {
|
|
102
|
+
const leaderId = sessionId ?? opts.session.id;
|
|
103
|
+
const controller = abortControllers.get(leaderId);
|
|
104
|
+
if (!controller) return false;
|
|
105
|
+
controller.abort();
|
|
106
|
+
try {
|
|
107
|
+
void Promise.resolve(opts.stopSessionFleet?.(leaderId)).catch(() => void 0);
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
};
|
|
112
|
+
if (opts.interruptController) {
|
|
113
|
+
opts.interruptController.abortLeader = () => interruptSession(opts.agent.ctx.session?.id ?? opts.session.id);
|
|
189
114
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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();
|
|
115
|
+
return createWebuiClientRegistration({
|
|
116
|
+
projectRoot: opts.projectRoot,
|
|
117
|
+
appConfig: opts.appConfig,
|
|
118
|
+
events: opts.events,
|
|
119
|
+
hqSessionId: opts.session.id,
|
|
120
|
+
getSessionId: () => opts.agent.ctx.session?.id ?? opts.session.id,
|
|
121
|
+
// One HQ session per open tab. The set is the same one the terminal
|
|
122
|
+
// panel lists: every session a connected browser is displaying.
|
|
123
|
+
listSessions: () => {
|
|
124
|
+
const ids = /* @__PURE__ */ new Set();
|
|
125
|
+
for (const client of clients.values()) {
|
|
126
|
+
if (client.sessionId) ids.add(client.sessionId);
|
|
127
|
+
for (const id of client.sessionIds ?? []) ids.add(id);
|
|
370
128
|
}
|
|
371
|
-
|
|
372
|
-
|
|
129
|
+
return [...ids];
|
|
130
|
+
},
|
|
131
|
+
// `cli-main` already runs a tracker and a bridge for the root
|
|
132
|
+
// conversation; announcing it here too would put two trackers on one bus
|
|
133
|
+
// flushing the same agent list. Compared against the LIVE root writer:
|
|
134
|
+
// the CLI host's telemetry follows `ctx.session`, so once the root moves
|
|
135
|
+
// (session.new / resume) the old boot id is an ordinary tab this client
|
|
136
|
+
// must publish, and the new root id must not be published twice.
|
|
137
|
+
isSessionOwnedElsewhere: (sessionId) => sessionId === (opts.agent.ctx.session?.id ?? opts.session.id),
|
|
138
|
+
// The tab's own journal, so HQ takes its turns from the write path
|
|
139
|
+
// instead of tailing the file.
|
|
140
|
+
getSessionWriter: (sessionId) => getSessionAgents()?.peek(sessionId)?.ctx.session,
|
|
141
|
+
hqControl: {
|
|
142
|
+
interruptLeader: interruptSession,
|
|
143
|
+
...opts.hqFleetControl ? {
|
|
144
|
+
killFleet: opts.hqFleetControl.killFleet,
|
|
145
|
+
terminateAgent: opts.hqFleetControl.terminateAgent,
|
|
146
|
+
spawnAgent: opts.hqFleetControl.spawnAgent
|
|
147
|
+
} : {},
|
|
148
|
+
allowRunCommand: () => opts.hqAllowExec === true,
|
|
149
|
+
// A command naming a tab this process no longer holds is refused, not
|
|
150
|
+
// redirected onto the boot session: the operator picked a terminal,
|
|
151
|
+
// and steering a different one is worse than not steering at all.
|
|
152
|
+
ownsSession: (sessionId) => sessionId === opts.session.id || getSessionAgents()?.has(sessionId) === true
|
|
153
|
+
}
|
|
373
154
|
});
|
|
374
|
-
return { stopped };
|
|
375
155
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (orphaned > 0) {
|
|
387
|
-
console.log(
|
|
388
|
-
JSON.stringify({
|
|
389
|
-
level: "warn",
|
|
390
|
-
event: "webui.confirm_orphaned_by_tab_close",
|
|
391
|
-
sessionId,
|
|
392
|
-
count: orphaned,
|
|
393
|
-
message: `Denied ${orphaned} unanswerable permission prompt(s) for closed session ${sessionId}.`
|
|
394
|
-
})
|
|
395
|
-
);
|
|
156
|
+
function startEmbeddedLiveStatusLogger(opts, clients, abortControllers, getSessionAgents, terminalLogView) {
|
|
157
|
+
return startWebUILiveStatusLogger({
|
|
158
|
+
title: opts.surface === "simpleui" ? "SimpleUI" : "WebUI",
|
|
159
|
+
events: opts.events,
|
|
160
|
+
dashboard: terminalLogView,
|
|
161
|
+
getSessionList: () => {
|
|
162
|
+
const ids = /* @__PURE__ */ new Set();
|
|
163
|
+
for (const client of clients.values()) {
|
|
164
|
+
if (client.sessionId) ids.add(client.sessionId);
|
|
165
|
+
for (const id of client.sessionIds ?? []) ids.add(id);
|
|
396
166
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
167
|
+
const currentId = opts.agent.ctx.session?.id ?? opts.session.id;
|
|
168
|
+
if (ids.size === 0 && currentId) ids.add(currentId);
|
|
169
|
+
return Array.from(ids).map((id) => {
|
|
170
|
+
const ctx = getSessionAgents()?.peek(id)?.ctx;
|
|
171
|
+
return {
|
|
172
|
+
id,
|
|
173
|
+
model: ctx?.model ?? opts.agent.ctx.model ?? "",
|
|
174
|
+
provider: ctx?.provider?.id ?? opts.agent.ctx.provider?.id ?? "",
|
|
175
|
+
isRunning: abortControllers.has(id)
|
|
176
|
+
};
|
|
177
|
+
});
|
|
402
178
|
}
|
|
403
|
-
};
|
|
404
|
-
return { retireUndisplayedSessions };
|
|
179
|
+
});
|
|
405
180
|
}
|
|
406
181
|
|
|
407
|
-
// src/webui-server.ts
|
|
408
|
-
import * as
|
|
409
|
-
import
|
|
410
|
-
import {
|
|
411
|
-
addFatalSalvageHook,
|
|
412
|
-
startSharedHeapWatchdog,
|
|
413
|
-
wstackGlobalRoot
|
|
414
|
-
} from "@wrongstack/core/utils";
|
|
182
|
+
// src/webui-server/domain-handlers.ts
|
|
183
|
+
import * as os from "node:os";
|
|
184
|
+
import * as path2 from "node:path";
|
|
185
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
415
186
|
import {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
createSessionAgentRegistry,
|
|
424
|
-
createWebuiLeaderAutoWakeHost,
|
|
425
|
-
envFlag,
|
|
426
|
-
findFreePort,
|
|
427
|
-
findInstalledPackageJson,
|
|
428
|
-
isStrictPort,
|
|
429
|
-
resolveAuthToken,
|
|
430
|
-
scheduleOwnerlessEmptySessionCleanup,
|
|
431
|
-
sendSerialized,
|
|
432
|
-
stampDispatchSession,
|
|
433
|
-
startTerminalDashboard,
|
|
434
|
-
toSessionHistoryEntries,
|
|
435
|
-
webuiSessionFrameLog
|
|
187
|
+
buildSddWizardDeps,
|
|
188
|
+
GoalWebSocketHandler,
|
|
189
|
+
SddBoardWebSocketHandler,
|
|
190
|
+
SddWizardWebSocketHandler,
|
|
191
|
+
SpecsWebSocketHandler,
|
|
192
|
+
TerminalWebSocketHandler,
|
|
193
|
+
WorktreeWebSocketHandler
|
|
436
194
|
} from "@wrongstack/webui-server";
|
|
437
|
-
import { verifyClient as verifyWsClient2 } from "@wrongstack/webui-server/server/ws-auth";
|
|
438
|
-
import { WebSocketServer } from "ws";
|
|
439
|
-
|
|
440
|
-
// src/webui-client-observability.ts
|
|
441
|
-
import { startWebUILiveStatusLogger } from "@wrongstack/webui-server";
|
|
442
195
|
|
|
443
|
-
// src/webui-server/
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// prompt HQ shows is one this handler can actually answer.
|
|
472
|
-
resolveApproval: (toolUseId, decision, sessionId) => approvals.resolve(toolUseId, decision, sessionId),
|
|
473
|
-
// Same registry again: it publishes `user_input.requested`, so
|
|
474
|
-
// it has to be the one that answers `answer-input`.
|
|
475
|
-
resolveUserInput: (requestId, response, sessionId) => approvals.resolveUserInput?.(requestId, response, sessionId) ?? false,
|
|
476
|
-
...deps.projectRoot ? {
|
|
477
|
-
kanbanTransition: createProjectKanbanTransitionHandler(deps.projectRoot),
|
|
478
|
-
kanbanAssign: createProjectKanbanAssignHandler(deps.projectRoot)
|
|
479
|
-
} : {},
|
|
480
|
-
// The session HQ actually SHOWS for this client — the boot
|
|
481
|
-
// session the telemetry bridge publishes under and the only one
|
|
482
|
-
// `interruptLeader` speaks for. `getSessionId` follows whichever
|
|
483
|
-
// browser tab is in front, which is a different session HQ has
|
|
484
|
-
// no view of and cannot control.
|
|
485
|
-
sessionTag: () => mailboxSessionTag(deps.hqSessionId),
|
|
486
|
-
// The session HQ falls back to when a command names none — the
|
|
487
|
-
// boot tab. A command that DOES name one is honoured (and
|
|
488
|
-
// refused when this host no longer has it) via `ownsSession`.
|
|
489
|
-
sessionId: () => deps.hqSessionId,
|
|
490
|
-
...control.ownsSession ? { ownsSession: control.ownsSession } : {},
|
|
491
|
-
...control.spawnAgent ? { spawnAgent: control.spawnAgent } : {},
|
|
492
|
-
...control.killFleet ? { killFleet: control.killFleet } : {},
|
|
493
|
-
...control.terminateAgent ? { terminateAgent: control.terminateAgent } : {}
|
|
494
|
-
})
|
|
495
|
-
} : {}
|
|
496
|
-
});
|
|
497
|
-
}
|
|
196
|
+
// src/webui-server/logger-shim.ts
|
|
197
|
+
var structuredLine = (level, message) => JSON.stringify({
|
|
198
|
+
level,
|
|
199
|
+
event: "webui.goal",
|
|
200
|
+
message,
|
|
201
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
202
|
+
});
|
|
203
|
+
var consoleLogger = {
|
|
204
|
+
level: "debug",
|
|
205
|
+
error(msg, _ctx) {
|
|
206
|
+
console.error(structuredLine("error", msg));
|
|
207
|
+
},
|
|
208
|
+
warn(msg, _ctx) {
|
|
209
|
+
console.warn(structuredLine("warn", msg));
|
|
210
|
+
},
|
|
211
|
+
info(msg, _ctx) {
|
|
212
|
+
console.log(structuredLine("info", msg));
|
|
213
|
+
},
|
|
214
|
+
debug(msg, _ctx) {
|
|
215
|
+
console.debug(structuredLine("debug", msg));
|
|
216
|
+
},
|
|
217
|
+
trace(msg, _ctx) {
|
|
218
|
+
console.debug(structuredLine("trace", msg));
|
|
219
|
+
},
|
|
220
|
+
child(_bindings) {
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
498
224
|
|
|
499
|
-
// src/webui-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
225
|
+
// src/webui-server/node-pty-loader.ts
|
|
226
|
+
import { existsSync } from "node:fs";
|
|
227
|
+
import { createRequire } from "node:module";
|
|
228
|
+
import * as path from "node:path";
|
|
229
|
+
import { fileURLToPath } from "node:url";
|
|
230
|
+
import { isStandaloneBinary } from "@wrongstack/core/utils";
|
|
231
|
+
import { repairNodePtySpawnHelper } from "@wrongstack/webui-server";
|
|
232
|
+
function createNodePtyLoader(importMetaUrl, logger) {
|
|
233
|
+
const requireFromCli = createRequire(importMetaUrl);
|
|
234
|
+
let cachedNodePty;
|
|
235
|
+
const loaded = (req, via) => {
|
|
236
|
+
logger.debug?.(`[terminal] node-pty loaded via ${via}`);
|
|
237
|
+
repairNodePtySpawnHelper(req, logger);
|
|
238
|
+
return cachedNodePty;
|
|
239
|
+
};
|
|
240
|
+
return () => {
|
|
241
|
+
if (cachedNodePty !== void 0) return cachedNodePty;
|
|
242
|
+
if (isStandaloneBinary()) {
|
|
243
|
+
cachedNodePty = null;
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
506
246
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
247
|
+
const webuiEntry = requireFromCli.resolve("@wrongstack/webui");
|
|
248
|
+
const webuiDir = webuiEntry.replace(/[\\/]dist.*$/, "");
|
|
249
|
+
const webuiRequire = createRequire(path.join(webuiDir, "package.json"));
|
|
250
|
+
cachedNodePty = webuiRequire("node-pty");
|
|
251
|
+
if (cachedNodePty) return loaded(webuiRequire, `webui package (${webuiDir})`);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger.debug?.(`[terminal] webui-route failed: ${err.message}`);
|
|
509
254
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return createWebuiClientRegistration({
|
|
516
|
-
projectRoot: opts.projectRoot,
|
|
517
|
-
appConfig: opts.appConfig,
|
|
518
|
-
events: opts.events,
|
|
519
|
-
hqSessionId: opts.session.id,
|
|
520
|
-
getSessionId: () => opts.agent.ctx.session?.id ?? opts.session.id,
|
|
521
|
-
// One HQ session per open tab. The set is the same one the terminal
|
|
522
|
-
// panel lists: every session a connected browser is displaying.
|
|
523
|
-
listSessions: () => {
|
|
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);
|
|
528
|
-
}
|
|
529
|
-
return [...ids];
|
|
530
|
-
},
|
|
531
|
-
// `cli-main` already runs a tracker and a bridge for the root
|
|
532
|
-
// conversation; announcing it here too would put two trackers on one bus
|
|
533
|
-
// flushing the same agent list. Compared against the LIVE root writer:
|
|
534
|
-
// the CLI host's telemetry follows `ctx.session`, so once the root moves
|
|
535
|
-
// (session.new / resume) the old boot id is an ordinary tab this client
|
|
536
|
-
// must publish, and the new root id must not be published twice.
|
|
537
|
-
isSessionOwnedElsewhere: (sessionId) => sessionId === (opts.agent.ctx.session?.id ?? opts.session.id),
|
|
538
|
-
// The tab's own journal, so HQ takes its turns from the write path
|
|
539
|
-
// instead of tailing the file.
|
|
540
|
-
getSessionWriter: (sessionId) => getSessionAgents()?.peek(sessionId)?.ctx.session,
|
|
541
|
-
hqControl: {
|
|
542
|
-
interruptLeader: interruptSession,
|
|
543
|
-
...opts.hqFleetControl ? {
|
|
544
|
-
killFleet: opts.hqFleetControl.killFleet,
|
|
545
|
-
terminateAgent: opts.hqFleetControl.terminateAgent,
|
|
546
|
-
spawnAgent: opts.hqFleetControl.spawnAgent
|
|
547
|
-
} : {},
|
|
548
|
-
allowRunCommand: () => opts.hqAllowExec === true,
|
|
549
|
-
// A command naming a tab this process no longer holds is refused, not
|
|
550
|
-
// redirected onto the boot session: the operator picked a terminal,
|
|
551
|
-
// and steering a different one is worse than not steering at all.
|
|
552
|
-
ownsSession: (sessionId) => sessionId === opts.session.id || getSessionAgents()?.has(sessionId) === true
|
|
255
|
+
try {
|
|
256
|
+
cachedNodePty = requireFromCli("node-pty");
|
|
257
|
+
return loaded(requireFromCli, "direct requireFromCli");
|
|
258
|
+
} catch (err) {
|
|
259
|
+
logger.debug?.(`[terminal] direct require failed: ${err.message}`);
|
|
553
260
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
261
|
+
try {
|
|
262
|
+
const cliPkgJson = path.join(
|
|
263
|
+
path.dirname(fileURLToPath(importMetaUrl)),
|
|
264
|
+
"..",
|
|
265
|
+
"package.json"
|
|
266
|
+
);
|
|
267
|
+
let dir = path.dirname(cliPkgJson);
|
|
268
|
+
for (let i = 0; i < 6; i++) {
|
|
269
|
+
const candidate = path.join(dir, "node_modules", "node-pty");
|
|
270
|
+
if (existsSync(candidate)) {
|
|
271
|
+
const candidateRequire = createRequire(candidate);
|
|
272
|
+
cachedNodePty = candidateRequire("node-pty");
|
|
273
|
+
if (cachedNodePty) return loaded(candidateRequire, `workspace root (${candidate})`);
|
|
274
|
+
}
|
|
275
|
+
const parent = path.dirname(dir);
|
|
276
|
+
if (parent === dir) break;
|
|
277
|
+
dir = parent;
|
|
566
278
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return Array.from(ids).map((id) => {
|
|
570
|
-
const ctx = getSessionAgents()?.peek(id)?.ctx;
|
|
571
|
-
return {
|
|
572
|
-
id,
|
|
573
|
-
model: ctx?.model ?? opts.agent.ctx.model ?? "",
|
|
574
|
-
provider: ctx?.provider?.id ?? opts.agent.ctx.provider?.id ?? "",
|
|
575
|
-
isRunning: abortControllers.has(id)
|
|
576
|
-
};
|
|
577
|
-
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
logger.debug?.(`[terminal] workspace-root walk failed: ${err.message}`);
|
|
578
281
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
|
|
586
|
-
import { makeProviderFromConfig } from "@wrongstack/providers";
|
|
587
|
-
|
|
588
|
-
// src/webui-server/provider-config.ts
|
|
589
|
-
import * as path from "node:path";
|
|
590
|
-
import { DefaultSecretVault } from "@wrongstack/core/security";
|
|
591
|
-
import { ProviderConfigSnapshots } from "@wrongstack/providers";
|
|
592
|
-
var snapshots = new ProviderConfigSnapshots();
|
|
593
|
-
function getVault(globalConfigPath) {
|
|
594
|
-
const configDir = path.dirname(globalConfigPath ?? "");
|
|
595
|
-
const parentDir = path.dirname(configDir);
|
|
596
|
-
const globalRoot = path.basename(parentDir) === "profiles" ? path.dirname(parentDir) : configDir;
|
|
597
|
-
const keyFile = path.join(globalRoot, ".key");
|
|
598
|
-
return new DefaultSecretVault({ keyFile });
|
|
599
|
-
}
|
|
600
|
-
async function loadSavedProviders(globalConfigPath) {
|
|
601
|
-
if (!globalConfigPath) return {};
|
|
602
|
-
return snapshots.track(await loadConfigProviders(globalConfigPath, getVault(globalConfigPath)));
|
|
603
|
-
}
|
|
604
|
-
async function saveProviders(globalConfigPath, providers) {
|
|
605
|
-
if (!globalConfigPath) return;
|
|
606
|
-
await mutateConfigProviders(
|
|
607
|
-
globalConfigPath,
|
|
608
|
-
getVault(globalConfigPath),
|
|
609
|
-
(existing) => {
|
|
610
|
-
const merged = snapshots.merge(existing, providers);
|
|
611
|
-
for (const key of Object.keys(existing)) delete existing[key];
|
|
612
|
-
Object.assign(existing, merged);
|
|
613
|
-
for (const key of Object.keys(providers)) delete providers[key];
|
|
614
|
-
Object.assign(providers, merged);
|
|
615
|
-
}
|
|
616
|
-
);
|
|
617
|
-
snapshots.track(providers);
|
|
618
|
-
}
|
|
619
|
-
function createProviderConfigStore(globalConfigPath, configProvidersRef) {
|
|
620
|
-
if (configProvidersRef && globalConfigPath) {
|
|
621
|
-
const views = /* @__PURE__ */ new WeakMap();
|
|
622
|
-
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
623
|
-
return {
|
|
624
|
-
load: async () => {
|
|
625
|
-
const disk = await loadSavedProviders(globalConfigPath);
|
|
626
|
-
const providers = structuredClone(configProvidersRef());
|
|
627
|
-
views.set(providers, { view: structuredClone(providers), disk });
|
|
628
|
-
return providers;
|
|
629
|
-
},
|
|
630
|
-
save: async (providers) => {
|
|
631
|
-
const baseline = views.get(providers);
|
|
632
|
-
if (!baseline) {
|
|
633
|
-
throw new Error("Load the provider list before saving. Refresh and try again.");
|
|
634
|
-
}
|
|
635
|
-
const pending = structuredClone(baseline.disk);
|
|
636
|
-
snapshots.track(pending);
|
|
637
|
-
for (const id of /* @__PURE__ */ new Set([...Object.keys(baseline.view), ...Object.keys(providers)])) {
|
|
638
|
-
if (equal(baseline.view[id], providers[id])) continue;
|
|
639
|
-
if (!equal(baseline.view[id], baseline.disk[id])) {
|
|
640
|
-
throw new Error(
|
|
641
|
-
`Provider "${id}" comes from another config file. Edit its source config or create a new auth profile alias.`
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
if (Object.hasOwn(providers, id)) pending[id] = structuredClone(providers[id]);
|
|
645
|
-
else delete pending[id];
|
|
646
|
-
}
|
|
647
|
-
await saveProviders(globalConfigPath, pending);
|
|
648
|
-
for (const id of Object.keys(providers)) {
|
|
649
|
-
if (equal(baseline.view[id], baseline.disk[id])) delete providers[id];
|
|
650
|
-
}
|
|
651
|
-
for (const [id, record] of Object.entries(pending)) {
|
|
652
|
-
if (equal(baseline.view[id], baseline.disk[id])) providers[id] = record;
|
|
653
|
-
}
|
|
654
|
-
views.set(providers, { view: structuredClone(providers), disk: structuredClone(pending) });
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
return {
|
|
659
|
-
load: () => configProvidersRef ? Promise.resolve(snapshots.track(structuredClone(configProvidersRef()))) : loadSavedProviders(globalConfigPath),
|
|
660
|
-
save: (providers) => saveProviders(globalConfigPath, providers)
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// src/webui-server/credential-watcher.ts
|
|
665
|
-
function startWebuiCredentialWatcher({
|
|
666
|
-
opts,
|
|
667
|
-
profileConfigPath,
|
|
668
|
-
broadcast,
|
|
669
|
-
broadcastSaved
|
|
670
|
-
}) {
|
|
671
|
-
const watchConfigPath = profileConfigPath;
|
|
672
|
-
if (!watchConfigPath || process.env["WRONGSTACK_DISABLE_CONFIG_WATCH"] === "1") {
|
|
673
|
-
return void 0;
|
|
674
|
-
}
|
|
675
|
-
let lastActiveCfg = JSON.stringify(
|
|
676
|
-
opts.appConfig?.providers?.[opts.agent.ctx.provider.id] ?? null
|
|
677
|
-
);
|
|
678
|
-
let lastUiLocale = opts.appConfig?.uiLocale;
|
|
679
|
-
const watcher = watchProviderConfig(
|
|
680
|
-
watchConfigPath,
|
|
681
|
-
// Vault key lives at ~/.wrongstack/.key (derived from globalConfigPath),
|
|
682
|
-
// not inside the profile directory — resolve via global root to stay correct.
|
|
683
|
-
getVault(opts.globalConfigPath),
|
|
684
|
-
(snapshot) => {
|
|
685
|
-
const hadFallbackBridge = Boolean(opts.appConfig?.fallbackBridge?.trim());
|
|
686
|
-
try {
|
|
687
|
-
if (opts.appConfig && !Object.isFrozen(opts.appConfig)) {
|
|
688
|
-
opts.appConfig.providers = snapshot.providers;
|
|
689
|
-
if (snapshot.uiLocale) opts.appConfig.uiLocale = snapshot.uiLocale;
|
|
690
|
-
if (snapshot.fallbackBridge !== void 0) {
|
|
691
|
-
opts.appConfig.fallbackBridge = snapshot.fallbackBridge;
|
|
692
|
-
} else {
|
|
693
|
-
delete opts.appConfig.fallbackBridge;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
} catch {
|
|
697
|
-
}
|
|
698
|
-
const routingChanged = snapshot.fallbackModels !== void 0 || snapshot.fallbackBridge !== void 0 || hadFallbackBridge || snapshot.fallbackProfiles !== void 0 || snapshot.favoriteModels !== void 0 || snapshot.disabledModels !== void 0 || snapshot.disabledProviders !== void 0 || snapshot.favoriteModelsOnly !== void 0 || snapshot.modelMatrix !== void 0 || snapshot.fallbackAuto !== void 0;
|
|
699
|
-
if (routingChanged) {
|
|
700
|
-
const configStore = opts.agent.container?.safeResolve?.(TOKENS.ConfigStore);
|
|
701
|
-
configStore?.update({
|
|
702
|
-
...snapshot.fallbackModels !== void 0 ? { fallbackModels: snapshot.fallbackModels } : {},
|
|
703
|
-
fallbackBridge: snapshot.fallbackBridge ?? "",
|
|
704
|
-
...snapshot.fallbackProfiles !== void 0 ? { fallbackProfiles: snapshot.fallbackProfiles } : {},
|
|
705
|
-
...snapshot.favoriteModels !== void 0 ? { favoriteModels: snapshot.favoriteModels } : {},
|
|
706
|
-
...snapshot.disabledModels !== void 0 ? { disabledModels: snapshot.disabledModels } : {},
|
|
707
|
-
...snapshot.disabledProviders !== void 0 ? { disabledProviders: snapshot.disabledProviders } : {},
|
|
708
|
-
...snapshot.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snapshot.favoriteModelsOnly } : {},
|
|
709
|
-
...snapshot.modelMatrix !== void 0 ? { modelMatrix: snapshot.modelMatrix } : {},
|
|
710
|
-
...snapshot.fallbackAuto !== void 0 ? { fallbackAuto: snapshot.fallbackAuto } : {}
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
broadcastSaved(snapshot.providers);
|
|
714
|
-
if (snapshot.uiLocale !== lastUiLocale) {
|
|
715
|
-
lastUiLocale = snapshot.uiLocale;
|
|
716
|
-
if (snapshot.uiLocale) {
|
|
717
|
-
opts.agent.ctx.meta["uiLocale"] = snapshot.uiLocale;
|
|
718
|
-
broadcast({ type: "prefs.updated", payload: { uiLocale: snapshot.uiLocale } });
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
const activeId = opts.agent.ctx.provider.id;
|
|
722
|
-
const newCfgStr = JSON.stringify(snapshot.providers[activeId] ?? null);
|
|
723
|
-
if (newCfgStr === lastActiveCfg) return;
|
|
724
|
-
lastActiveCfg = newCfgStr;
|
|
725
|
-
try {
|
|
726
|
-
const newCfg = snapshot.providers[activeId] ?? {
|
|
727
|
-
type: activeId,
|
|
728
|
-
...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
|
|
729
|
-
...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
|
|
730
|
-
};
|
|
731
|
-
const oldMax = opts.agent.ctx.provider.capabilities?.maxContext;
|
|
732
|
-
const prov = makeProviderFromConfig(activeId, {
|
|
733
|
-
...newCfg,
|
|
734
|
-
type: newCfg.type ?? activeId
|
|
735
|
-
});
|
|
736
|
-
if (oldMax != null && prov.capabilities) prov.capabilities.maxContext = oldMax;
|
|
737
|
-
opts.agent.ctx.provider = prov;
|
|
738
|
-
console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
|
|
739
|
-
} catch (err) {
|
|
740
|
-
console.warn(
|
|
741
|
-
`[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage2(err)}`
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
warn: (m) => console.warn(
|
|
747
|
-
JSON.stringify({
|
|
748
|
-
level: "warn",
|
|
749
|
-
event: "webui.config_watcher",
|
|
750
|
-
message: m,
|
|
751
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
752
|
-
})
|
|
753
|
-
)
|
|
754
|
-
}
|
|
755
|
-
);
|
|
756
|
-
return watcher.close;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// src/webui-server/domain-handlers.ts
|
|
760
|
-
import * as os from "node:os";
|
|
761
|
-
import * as path3 from "node:path";
|
|
762
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
763
|
-
import {
|
|
764
|
-
buildSddWizardDeps,
|
|
765
|
-
GoalWebSocketHandler,
|
|
766
|
-
SddBoardWebSocketHandler,
|
|
767
|
-
SddWizardWebSocketHandler,
|
|
768
|
-
SpecsWebSocketHandler,
|
|
769
|
-
TerminalWebSocketHandler,
|
|
770
|
-
WorktreeWebSocketHandler
|
|
771
|
-
} from "@wrongstack/webui-server";
|
|
772
|
-
|
|
773
|
-
// src/webui-server/node-pty-loader.ts
|
|
774
|
-
import { existsSync } from "node:fs";
|
|
775
|
-
import { createRequire } from "node:module";
|
|
776
|
-
import * as path2 from "node:path";
|
|
777
|
-
import { fileURLToPath } from "node:url";
|
|
778
|
-
import { isStandaloneBinary } from "@wrongstack/core/utils";
|
|
779
|
-
import { repairNodePtySpawnHelper } from "@wrongstack/webui-server";
|
|
780
|
-
function createNodePtyLoader(importMetaUrl, logger) {
|
|
781
|
-
const requireFromCli = createRequire(importMetaUrl);
|
|
782
|
-
let cachedNodePty;
|
|
783
|
-
const loaded = (req, via) => {
|
|
784
|
-
logger.debug?.(`[terminal] node-pty loaded via ${via}`);
|
|
785
|
-
repairNodePtySpawnHelper(req, logger);
|
|
786
|
-
return cachedNodePty;
|
|
787
|
-
};
|
|
788
|
-
return () => {
|
|
789
|
-
if (cachedNodePty !== void 0) return cachedNodePty;
|
|
790
|
-
if (isStandaloneBinary()) {
|
|
791
|
-
cachedNodePty = null;
|
|
792
|
-
return null;
|
|
793
|
-
}
|
|
794
|
-
try {
|
|
795
|
-
const webuiEntry = requireFromCli.resolve("@wrongstack/webui");
|
|
796
|
-
const webuiDir = webuiEntry.replace(/[\\/]dist.*$/, "");
|
|
797
|
-
const webuiRequire = createRequire(path2.join(webuiDir, "package.json"));
|
|
798
|
-
cachedNodePty = webuiRequire("node-pty");
|
|
799
|
-
if (cachedNodePty) return loaded(webuiRequire, `webui package (${webuiDir})`);
|
|
800
|
-
} catch (err) {
|
|
801
|
-
logger.debug?.(`[terminal] webui-route failed: ${err.message}`);
|
|
802
|
-
}
|
|
803
|
-
try {
|
|
804
|
-
cachedNodePty = requireFromCli("node-pty");
|
|
805
|
-
return loaded(requireFromCli, "direct requireFromCli");
|
|
806
|
-
} catch (err) {
|
|
807
|
-
logger.debug?.(`[terminal] direct require failed: ${err.message}`);
|
|
808
|
-
}
|
|
809
|
-
try {
|
|
810
|
-
const cliPkgJson = path2.join(
|
|
811
|
-
path2.dirname(fileURLToPath(importMetaUrl)),
|
|
812
|
-
"..",
|
|
813
|
-
"package.json"
|
|
814
|
-
);
|
|
815
|
-
let dir = path2.dirname(cliPkgJson);
|
|
816
|
-
for (let i = 0; i < 6; i++) {
|
|
817
|
-
const candidate = path2.join(dir, "node_modules", "node-pty");
|
|
818
|
-
if (existsSync(candidate)) {
|
|
819
|
-
const candidateRequire = createRequire(candidate);
|
|
820
|
-
cachedNodePty = candidateRequire("node-pty");
|
|
821
|
-
if (cachedNodePty) return loaded(candidateRequire, `workspace root (${candidate})`);
|
|
822
|
-
}
|
|
823
|
-
const parent = path2.dirname(dir);
|
|
824
|
-
if (parent === dir) break;
|
|
825
|
-
dir = parent;
|
|
826
|
-
}
|
|
827
|
-
} catch (err) {
|
|
828
|
-
logger.debug?.(`[terminal] workspace-root walk failed: ${err.message}`);
|
|
829
|
-
}
|
|
830
|
-
cachedNodePty = null;
|
|
831
|
-
logger.debug?.(
|
|
832
|
-
'[terminal] node-pty resolution failed; terminal panel will report "unavailable"'
|
|
833
|
-
);
|
|
834
|
-
return cachedNodePty;
|
|
835
|
-
};
|
|
282
|
+
cachedNodePty = null;
|
|
283
|
+
logger.debug?.(
|
|
284
|
+
'[terminal] node-pty resolution failed; terminal panel will report "unavailable"'
|
|
285
|
+
);
|
|
286
|
+
return cachedNodePty;
|
|
287
|
+
};
|
|
836
288
|
}
|
|
837
289
|
|
|
838
290
|
// src/webui-server/domain-handlers.ts
|
|
839
291
|
function createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror) {
|
|
840
|
-
const goalStoreDir = opts.projectRoot ? resolveWstackPaths({ projectRoot: opts.projectRoot }).projectAutophase :
|
|
292
|
+
const goalStoreDir = opts.projectRoot ? resolveWstackPaths({ projectRoot: opts.projectRoot }).projectAutophase : path2.join(os.tmpdir(), ".wrongstack", "goal");
|
|
841
293
|
const goalHandler = new GoalWebSocketHandler(
|
|
842
294
|
opts.agent,
|
|
843
295
|
opts.agent.ctx,
|
|
@@ -859,11 +311,11 @@ function createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror) {
|
|
|
859
311
|
);
|
|
860
312
|
const specsPaths = opts.projectRoot ? resolveWstackPaths({ projectRoot: opts.projectRoot }) : null;
|
|
861
313
|
const specsHandler = new SpecsWebSocketHandler(
|
|
862
|
-
specsPaths?.projectSpecs ??
|
|
863
|
-
specsPaths?.projectTaskGraphs ??
|
|
314
|
+
specsPaths?.projectSpecs ?? path2.join(os.tmpdir(), ".wrongstack", "specs"),
|
|
315
|
+
specsPaths?.projectTaskGraphs ?? path2.join(os.tmpdir(), ".wrongstack", "task-graphs")
|
|
864
316
|
);
|
|
865
317
|
const sddBoardHandler = new SddBoardWebSocketHandler(
|
|
866
|
-
specsPaths?.projectSddBoards ??
|
|
318
|
+
specsPaths?.projectSddBoards ?? path2.join(os.tmpdir(), ".wrongstack", "sdd-boards"),
|
|
867
319
|
opts.events,
|
|
868
320
|
specsPaths && opts.projectRoot ? {
|
|
869
321
|
projectRoot: opts.projectRoot,
|
|
@@ -903,33 +355,301 @@ function createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror) {
|
|
|
903
355
|
};
|
|
904
356
|
}
|
|
905
357
|
|
|
906
|
-
// src/webui-server/
|
|
907
|
-
import
|
|
908
|
-
import {
|
|
909
|
-
import {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
return
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
return {
|
|
358
|
+
// src/webui-server/host-settings.ts
|
|
359
|
+
import * as path3 from "node:path";
|
|
360
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
361
|
+
import { envFlag, isStrictPort } from "@wrongstack/webui-server";
|
|
362
|
+
function resolveWebuiHostSettings(opts) {
|
|
363
|
+
const host = opts.host ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
364
|
+
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
365
|
+
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
366
|
+
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
367
|
+
const surface = opts.surface ?? "webui";
|
|
368
|
+
const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
|
|
369
|
+
const requestedHttpPort = opts.httpPort ?? opts.port ?? surfaceDefaults.http;
|
|
370
|
+
const strictPort = opts.strictPort ?? isStrictPort();
|
|
371
|
+
const globalRoot = opts.globalConfigPath ? path3.dirname(opts.globalConfigPath) : wstackGlobalRoot();
|
|
372
|
+
const profileConfigPath = opts.profileConfigPath ?? opts.globalConfigPath ?? path3.join(globalRoot, "config.json");
|
|
373
|
+
const rawRateLimit = process.env["WEBUI_RATE_LIMIT"]?.trim() ?? "";
|
|
374
|
+
const parsedRateLimit = /^\d+$/.test(rawRateLimit) ? Number(rawRateLimit) : Number.NaN;
|
|
375
|
+
const rateLimitMax = Number.isSafeInteger(parsedRateLimit) && parsedRateLimit >= 0 ? parsedRateLimit : 600;
|
|
376
|
+
const publicHostnames = [publicUrl, publicWsUrl].map((value) => {
|
|
377
|
+
if (!value) return void 0;
|
|
378
|
+
try {
|
|
379
|
+
return new URL(value).hostname;
|
|
380
|
+
} catch {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
}).filter((value) => Boolean(value));
|
|
384
|
+
return {
|
|
385
|
+
host,
|
|
386
|
+
publicUrl,
|
|
387
|
+
publicWsUrl,
|
|
388
|
+
requireToken,
|
|
389
|
+
surface,
|
|
390
|
+
requestedHttpPort,
|
|
391
|
+
strictPort,
|
|
392
|
+
globalRoot,
|
|
393
|
+
profileConfigPath,
|
|
394
|
+
profileDir: path3.dirname(profileConfigPath),
|
|
395
|
+
rateLimitMax,
|
|
396
|
+
publicHostnames
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/webui-server/http-bridge.ts
|
|
401
|
+
import {
|
|
402
|
+
createDefaultFileWatcherMetrics,
|
|
403
|
+
findInstalledPackageJson
|
|
404
|
+
} from "@wrongstack/webui-server";
|
|
405
|
+
import { verifyClient as verifyWsClient } from "@wrongstack/webui-server/server/ws-auth";
|
|
406
|
+
import { WebSocketServer } from "ws";
|
|
407
|
+
|
|
408
|
+
// src/webui-server/lifecycle.ts
|
|
409
|
+
import {
|
|
410
|
+
announceWebuiReady,
|
|
411
|
+
createWebuiShutdown,
|
|
412
|
+
registerWebuiInstance,
|
|
413
|
+
registerWebuiSignalHandlers
|
|
414
|
+
} from "@wrongstack/webui-server";
|
|
415
|
+
|
|
416
|
+
// src/webui-server/listen-helpers.ts
|
|
417
|
+
import * as http from "node:http";
|
|
418
|
+
import { listenWithRetry } from "@wrongstack/webui-server";
|
|
419
|
+
async function startDeferredHttpListen(args) {
|
|
420
|
+
const { server, host, httpPort, logger, strictPort } = args;
|
|
421
|
+
const boundPort = await listenWithRetry(server, host, httpPort, {
|
|
422
|
+
maxTries: strictPort ? 1 : 10
|
|
423
|
+
});
|
|
424
|
+
server.on("error", (err) => {
|
|
425
|
+
logger.error("http_server_error", {
|
|
426
|
+
message: err.message,
|
|
427
|
+
port: boundPort
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
return boundPort;
|
|
431
|
+
}
|
|
432
|
+
async function startIpv6LoopbackProxy(args) {
|
|
433
|
+
const { primary, httpPort, logger } = args;
|
|
434
|
+
const v6 = http.createServer();
|
|
435
|
+
v6.on("request", (req, res) => primary.emit("request", req, res));
|
|
436
|
+
v6.on("upgrade", (req, socket, head) => primary.emit("upgrade", req, socket, head));
|
|
437
|
+
const logIpv6Error = (err) => {
|
|
438
|
+
if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE") {
|
|
439
|
+
logger.warn("ipv6_loopback_unavailable", { code: err.code, port: httpPort });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
logger.error("http_server_error", { message: err.message, port: httpPort });
|
|
443
|
+
};
|
|
444
|
+
v6.on("error", logIpv6Error);
|
|
445
|
+
const ipv6Listening = await new Promise((resolveListen) => {
|
|
446
|
+
const onListening = () => {
|
|
447
|
+
v6.off("error", onInitialError);
|
|
448
|
+
resolveListen(true);
|
|
449
|
+
};
|
|
450
|
+
const onInitialError = () => {
|
|
451
|
+
v6.off("listening", onListening);
|
|
452
|
+
resolveListen(false);
|
|
453
|
+
};
|
|
454
|
+
v6.once("listening", onListening);
|
|
455
|
+
v6.once("error", onInitialError);
|
|
456
|
+
v6.listen(httpPort, "::1");
|
|
457
|
+
});
|
|
458
|
+
if (!ipv6Listening) return null;
|
|
459
|
+
console.log(`[WebUI] Also listening on http://[::1]:${httpPort}`);
|
|
460
|
+
return v6;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/webui-server/static-serve.ts
|
|
464
|
+
import {
|
|
465
|
+
ensureDistDir,
|
|
466
|
+
resolveDistDir,
|
|
467
|
+
startStaticServe
|
|
468
|
+
} from "@wrongstack/webui-server";
|
|
469
|
+
|
|
470
|
+
// src/webui-server/http-bridge.ts
|
|
471
|
+
async function startWebuiHttpBridge(input) {
|
|
472
|
+
const {
|
|
473
|
+
opts,
|
|
474
|
+
surface,
|
|
475
|
+
host,
|
|
476
|
+
strictPort,
|
|
477
|
+
globalRoot,
|
|
478
|
+
wsToken,
|
|
479
|
+
publicUrl,
|
|
480
|
+
publicWsUrl,
|
|
481
|
+
requireToken,
|
|
482
|
+
getSessionAgents,
|
|
483
|
+
onFleetPing,
|
|
484
|
+
broadcast,
|
|
485
|
+
onPortRebound
|
|
486
|
+
} = input;
|
|
487
|
+
let httpPort = input.httpPort;
|
|
488
|
+
const httpServer = await startStaticServe({
|
|
489
|
+
getSessionProjectRoot: (sessionId) => getSessionAgents()?.peek(sessionId)?.ctx.projectRoot ?? (opts.agent.ctx.session?.id === sessionId ? opts.agent.ctx.projectRoot : void 0),
|
|
490
|
+
host,
|
|
491
|
+
httpPort,
|
|
492
|
+
globalRoot,
|
|
493
|
+
distDir: opts.frontendDistDir,
|
|
494
|
+
ensureDistDeps: {
|
|
495
|
+
resolvePackageJson: (id) => {
|
|
496
|
+
const packageJson = findInstalledPackageJson(id, import.meta.url);
|
|
497
|
+
if (!packageJson) throw new Error(`Package not found: ${id}`);
|
|
498
|
+
return packageJson;
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
onFleetPing,
|
|
502
|
+
// Without this, /debug/watcher-metrics 503s and the Debug Dashboard has to
|
|
503
|
+
// report the watcher as "Unavailable". The CLI host runs no status watcher
|
|
504
|
+
// of its own, so this object stays all-zero and watcherActive reads false —
|
|
505
|
+
// which is the truthful state for an embedded host.
|
|
506
|
+
watcherMetrics: createDefaultFileWatcherMetrics(),
|
|
507
|
+
onTechStackEvent: (event) => broadcast(event),
|
|
508
|
+
getLlm: () => opts.agent.ctx.provider && opts.agent.ctx.model ? { provider: opts.agent.ctx.provider, model: opts.agent.ctx.model } : void 0,
|
|
509
|
+
projectRoot: opts.projectRoot,
|
|
510
|
+
publicWsUrl,
|
|
511
|
+
apiToken: wsToken,
|
|
512
|
+
requireToken,
|
|
513
|
+
deferListen: surface === "simpleui",
|
|
514
|
+
strictPort,
|
|
515
|
+
// Resolve the same-origin health probes from live preferences, including
|
|
516
|
+
// changes made after boot, just like the standalone WebUI host.
|
|
517
|
+
getIntegrationTarget: (kind) => {
|
|
518
|
+
const meta = opts.agent.ctx.meta;
|
|
519
|
+
const enabledKey = kind === "hq" ? "hqEnabled" : "wrongProxyEnabled";
|
|
520
|
+
const urlKey = kind === "hq" ? "hqUrl" : "wrongProxyUrl";
|
|
521
|
+
return meta[enabledKey] === true && typeof meta[urlKey] === "string" ? meta[urlKey] : void 0;
|
|
522
|
+
},
|
|
523
|
+
...opts.getVectorMemoryStore ? { getVectorMemoryStore: opts.getVectorMemoryStore } : {},
|
|
524
|
+
...opts.vectorMemoryModelCacheDir ? { vectorMemoryModelCacheDir: opts.vectorMemoryModelCacheDir } : {}
|
|
525
|
+
});
|
|
526
|
+
const verifyClient = (info) => verifyWsClient({
|
|
527
|
+
origin: info.origin,
|
|
528
|
+
url: info.req.url ?? "",
|
|
529
|
+
hostHeader: info.req.headers.host,
|
|
530
|
+
remoteAddress: info.req.socket.remoteAddress,
|
|
531
|
+
cookieHeader: info.req.headers.cookie,
|
|
532
|
+
wsHost: host,
|
|
533
|
+
expectedToken: wsToken,
|
|
534
|
+
requireToken,
|
|
535
|
+
allowedHostnames: [publicUrl, publicWsUrl].filter((value) => Boolean(value)),
|
|
536
|
+
allowBrowserUrlToken: Boolean(publicWsUrl),
|
|
537
|
+
allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
|
|
538
|
+
});
|
|
539
|
+
const wss = httpServer ? new WebSocketServer({ server: httpServer.server, verifyClient, maxPayload: 20 * 1024 * 1024 }) : new WebSocketServer({ port: httpPort, host, verifyClient, maxPayload: 20 * 1024 * 1024 });
|
|
540
|
+
wss.on("error", (err) => {
|
|
541
|
+
console.error(
|
|
542
|
+
JSON.stringify({
|
|
543
|
+
level: "error",
|
|
544
|
+
event: "webui_server.error",
|
|
545
|
+
message: err instanceof Error ? err.message : String(err),
|
|
546
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
547
|
+
})
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
if (httpServer) {
|
|
551
|
+
const boundPort = surface === "simpleui" ? await startDeferredHttpListen({
|
|
552
|
+
server: httpServer.server,
|
|
553
|
+
host,
|
|
554
|
+
httpPort,
|
|
555
|
+
logger: consoleLogger,
|
|
556
|
+
strictPort
|
|
557
|
+
}) : httpServer.port;
|
|
558
|
+
if (boundPort !== httpPort) {
|
|
559
|
+
httpPort = boundPort;
|
|
560
|
+
onPortRebound(boundPort);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
let ipv6LoopbackServer = null;
|
|
564
|
+
if (httpServer && host === "127.0.0.1") {
|
|
565
|
+
ipv6LoopbackServer = await startIpv6LoopbackProxy({
|
|
566
|
+
primary: httpServer.server,
|
|
567
|
+
httpPort,
|
|
568
|
+
logger: consoleLogger
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
console.log(`[WebUI] WebSocket server starting on ws://${host}:${httpPort}`);
|
|
572
|
+
if (httpServer) {
|
|
573
|
+
announceWebuiReady({
|
|
574
|
+
surface,
|
|
575
|
+
server: httpServer.server,
|
|
576
|
+
host,
|
|
577
|
+
httpPort,
|
|
578
|
+
open: !!opts.open,
|
|
579
|
+
wsToken,
|
|
580
|
+
publicUrl
|
|
581
|
+
});
|
|
582
|
+
} else {
|
|
583
|
+
console.warn(
|
|
584
|
+
`[WebUI] Frontend not served (run \`pnpm --filter @wrongstack/webui build\`). WS bridge still active on ws://${host}:${httpPort}.`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return { httpServer, wss, ipv6LoopbackServer };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/webui-server/instance-registration.ts
|
|
591
|
+
async function registerEmbeddedWebuiInstance(input) {
|
|
592
|
+
const { opts, surface, host, httpPort, publicUrl, registryBaseDir, wsToken, currentSessionId } = input;
|
|
593
|
+
if (!opts.projectRoot) return false;
|
|
594
|
+
const registration = Promise.resolve(
|
|
595
|
+
registerWebuiInstance({
|
|
596
|
+
pid: process.pid,
|
|
597
|
+
surface,
|
|
598
|
+
host,
|
|
599
|
+
httpPort,
|
|
600
|
+
publicUrl,
|
|
601
|
+
projectRoot: opts.projectRoot,
|
|
602
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
603
|
+
registryBaseDir,
|
|
604
|
+
authToken: wsToken,
|
|
605
|
+
...opts.webuiSessionChild ? {
|
|
606
|
+
role: "session-child",
|
|
607
|
+
sessionId: currentSessionId(),
|
|
608
|
+
parentPid: opts.webuiSessionChild.parentPid,
|
|
609
|
+
parentShellId: opts.webuiSessionChild.parentShellId,
|
|
610
|
+
runtimeId: opts.webuiSessionChild.runtimeId,
|
|
611
|
+
attachable: opts.webuiSessionChild.attachable,
|
|
612
|
+
lastReadyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
613
|
+
protocolVersion: opts.webuiSessionChild.protocolVersion,
|
|
614
|
+
capabilities: [...WEBUI_SESSION_CHILD_CAPABILITIES]
|
|
615
|
+
} : {}
|
|
616
|
+
})
|
|
617
|
+
).then(
|
|
618
|
+
(value) => value !== false,
|
|
619
|
+
() => false
|
|
620
|
+
);
|
|
621
|
+
if (opts.webuiSessionChild) return await registration;
|
|
622
|
+
void registration;
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/webui-server/kanban-host-adapter.ts
|
|
627
|
+
import { PhaseGraphBuilder } from "@wrongstack/core/goal";
|
|
628
|
+
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
|
|
629
|
+
import { exportBoardToTaskGraph } from "@wrongstack/kanban";
|
|
630
|
+
import { TaskGraphStore } from "@wrongstack/sdd";
|
|
631
|
+
import {
|
|
632
|
+
startSddRunFromGraph
|
|
633
|
+
} from "@wrongstack/webui-server";
|
|
634
|
+
function createCliKanbanHostRoutes(deps) {
|
|
635
|
+
const { opts, send, goalHandler } = deps;
|
|
636
|
+
let isolatedSeq = 0;
|
|
637
|
+
const runIsolatedTurn = opts.sddSubagentFactory ? async (prompt, name) => {
|
|
638
|
+
const result = await opts.sddSubagentFactory({
|
|
639
|
+
id: `sdd-kanban-${name.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
640
|
+
role: "executor",
|
|
641
|
+
name,
|
|
642
|
+
disabledTools: ["delegate", "write", "edit", "patch", "bash", "exec"],
|
|
643
|
+
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
644
|
+
});
|
|
645
|
+
try {
|
|
646
|
+
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
647
|
+
return res.finalText ?? "";
|
|
648
|
+
} finally {
|
|
649
|
+
await result.dispose?.();
|
|
650
|
+
}
|
|
651
|
+
} : void 0;
|
|
652
|
+
return {
|
|
933
653
|
meta: async (ws) => {
|
|
934
654
|
const tools = (opts.agent.ctx.tools ?? []).map((tool) => ({
|
|
935
655
|
name: tool.name,
|
|
@@ -1073,59 +793,160 @@ function createCliKanbanHostRoutes(deps) {
|
|
|
1073
793
|
};
|
|
1074
794
|
}
|
|
1075
795
|
|
|
1076
|
-
// src/webui-server/
|
|
1077
|
-
import
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
|
|
796
|
+
// src/webui-server/kanban-services.ts
|
|
797
|
+
import { startSharedHeapWatchdog } from "@wrongstack/core/utils";
|
|
798
|
+
function createWebuiKanbanServices(opts, broadcast) {
|
|
799
|
+
const kanbanRunMirror = opts.projectRoot ? createKanbanRunMirror({
|
|
800
|
+
projectRoot: opts.projectRoot,
|
|
801
|
+
events: opts.events,
|
|
802
|
+
broadcast,
|
|
803
|
+
log: (m) => consoleLogger.info(m)
|
|
804
|
+
}) : null;
|
|
805
|
+
const kanbanSupervisor = opts.projectRoot ? createKanbanSupervisor({
|
|
806
|
+
projectRoot: opts.projectRoot,
|
|
807
|
+
broadcast,
|
|
808
|
+
...opts.onKanbanDispatch ? { dispatchTask: opts.onKanbanDispatch } : {},
|
|
809
|
+
log: (message) => consoleLogger.info(message)
|
|
810
|
+
}) : null;
|
|
811
|
+
const stopKanbanSupervisorMemoryStats = kanbanSupervisor ? startSharedHeapWatchdog({
|
|
812
|
+
collectStats: () => {
|
|
813
|
+
const stats = kanbanSupervisor.getStats();
|
|
814
|
+
return {
|
|
815
|
+
kanbanSupervisorSnapshots: stats.snapshots,
|
|
816
|
+
kanbanSupervisorScheduledBoards: stats.scheduledBoards,
|
|
817
|
+
kanbanSupervisorAgentCooldowns: stats.agentCooldowns,
|
|
818
|
+
kanbanSupervisorRunningAgents: stats.runningAgents
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}) : void 0;
|
|
822
|
+
return { kanbanRunMirror, kanbanSupervisor, stopKanbanSupervisorMemoryStats };
|
|
1091
823
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
824
|
+
|
|
825
|
+
// src/webui-server/message-router.ts
|
|
826
|
+
import { createEmbeddedMessageRouter } from "@wrongstack/webui-server";
|
|
827
|
+
|
|
828
|
+
// src/webui-server/provider-config.ts
|
|
829
|
+
import * as path4 from "node:path";
|
|
830
|
+
import { DefaultSecretVault } from "@wrongstack/core/security";
|
|
831
|
+
import { ProviderConfigSnapshots } from "@wrongstack/providers";
|
|
832
|
+
var snapshots = new ProviderConfigSnapshots();
|
|
833
|
+
function getVault(globalConfigPath) {
|
|
834
|
+
const configDir = path4.dirname(globalConfigPath ?? "");
|
|
835
|
+
const parentDir = path4.dirname(configDir);
|
|
836
|
+
const globalRoot = path4.basename(parentDir) === "profiles" ? path4.dirname(parentDir) : configDir;
|
|
837
|
+
const keyFile = path4.join(globalRoot, ".key");
|
|
838
|
+
return new DefaultSecretVault({ keyFile });
|
|
839
|
+
}
|
|
840
|
+
async function loadSavedProviders(globalConfigPath) {
|
|
841
|
+
if (!globalConfigPath) return {};
|
|
842
|
+
return snapshots.track(await loadConfigProviders(globalConfigPath, getVault(globalConfigPath)));
|
|
843
|
+
}
|
|
844
|
+
async function saveProviders(globalConfigPath, providers) {
|
|
845
|
+
if (!globalConfigPath) return;
|
|
846
|
+
await mutateConfigProviders(
|
|
847
|
+
globalConfigPath,
|
|
848
|
+
getVault(globalConfigPath),
|
|
849
|
+
(existing) => {
|
|
850
|
+
const merged = snapshots.merge(existing, providers);
|
|
851
|
+
for (const key of Object.keys(existing)) delete existing[key];
|
|
852
|
+
Object.assign(existing, merged);
|
|
853
|
+
for (const key of Object.keys(providers)) delete providers[key];
|
|
854
|
+
Object.assign(providers, merged);
|
|
1101
855
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
856
|
+
);
|
|
857
|
+
snapshots.track(providers);
|
|
858
|
+
}
|
|
859
|
+
function createProviderConfigStore(globalConfigPath, configProvidersRef) {
|
|
860
|
+
if (configProvidersRef && globalConfigPath) {
|
|
861
|
+
const views = /* @__PURE__ */ new WeakMap();
|
|
862
|
+
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
863
|
+
return {
|
|
864
|
+
load: async () => {
|
|
865
|
+
const disk = await loadSavedProviders(globalConfigPath);
|
|
866
|
+
const providers = structuredClone(configProvidersRef());
|
|
867
|
+
views.set(providers, { view: structuredClone(providers), disk });
|
|
868
|
+
return providers;
|
|
869
|
+
},
|
|
870
|
+
save: async (providers) => {
|
|
871
|
+
const baseline = views.get(providers);
|
|
872
|
+
if (!baseline) {
|
|
873
|
+
throw new Error("Load the provider list before saving. Refresh and try again.");
|
|
874
|
+
}
|
|
875
|
+
const pending = structuredClone(baseline.disk);
|
|
876
|
+
snapshots.track(pending);
|
|
877
|
+
for (const id of /* @__PURE__ */ new Set([...Object.keys(baseline.view), ...Object.keys(providers)])) {
|
|
878
|
+
if (equal(baseline.view[id], providers[id])) continue;
|
|
879
|
+
if (!equal(baseline.view[id], baseline.disk[id])) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`Provider "${id}" comes from another config file. Edit its source config or create a new auth profile alias.`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (Object.hasOwn(providers, id)) pending[id] = structuredClone(providers[id]);
|
|
885
|
+
else delete pending[id];
|
|
886
|
+
}
|
|
887
|
+
await saveProviders(globalConfigPath, pending);
|
|
888
|
+
for (const id of Object.keys(providers)) {
|
|
889
|
+
if (equal(baseline.view[id], baseline.disk[id])) delete providers[id];
|
|
890
|
+
}
|
|
891
|
+
for (const [id, record] of Object.entries(pending)) {
|
|
892
|
+
if (equal(baseline.view[id], baseline.disk[id])) providers[id] = record;
|
|
893
|
+
}
|
|
894
|
+
views.set(providers, { view: structuredClone(providers), disk: structuredClone(pending) });
|
|
895
|
+
}
|
|
1113
896
|
};
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
load: () => configProvidersRef ? Promise.resolve(snapshots.track(structuredClone(configProvidersRef()))) : loadSavedProviders(globalConfigPath),
|
|
900
|
+
save: (providers) => saveProviders(globalConfigPath, providers)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/webui-server/message-router.ts
|
|
905
|
+
function createCliEmbeddedMessageRouter(input) {
|
|
906
|
+
const { opts, routeContexts, domainHandlers } = input;
|
|
907
|
+
return createEmbeddedMessageRouter({
|
|
908
|
+
jevVault: getVault(opts.globalConfigPath ?? opts.profileConfigPath),
|
|
909
|
+
trustBoundary: input.trustBoundary,
|
|
910
|
+
opts,
|
|
911
|
+
logger: consoleLogger,
|
|
912
|
+
send: input.send,
|
|
913
|
+
sendResult: input.sendResult,
|
|
914
|
+
sessionPayload: input.sessionPayload,
|
|
915
|
+
currentSessionId: input.currentSessionId,
|
|
916
|
+
shutdown: input.shutdown,
|
|
917
|
+
// Auto-heal watchdog disposer — `disposeResources` awaits it (bounded) so
|
|
918
|
+
// an in-flight daemon restart drains before the host exits.
|
|
919
|
+
onDispose: input.onDispose,
|
|
920
|
+
providerCtx: input.providerCtx,
|
|
921
|
+
brainCtx: routeContexts.brainCtx,
|
|
922
|
+
introspectionCtx: routeContexts.introspectionCtx,
|
|
923
|
+
skillsCtx: routeContexts.skillsCtx,
|
|
924
|
+
promptsCtx: routeContexts.promptsCtx,
|
|
925
|
+
designCtx: routeContexts.designCtx,
|
|
926
|
+
agentConfigCtx: routeContexts.agentConfigCtx,
|
|
927
|
+
prefsCtx: routeContexts.prefsCtx,
|
|
928
|
+
projectCtx: routeContexts.projectsCtx,
|
|
929
|
+
mailboxRoutes: routeContexts.mailboxRoutes,
|
|
930
|
+
chimeraRoutes: routeContexts.chimeraRoutes,
|
|
931
|
+
codeAssistRoutes: routeContexts.codeAssistRoutes,
|
|
932
|
+
sessionCtx: routeContexts.sessionsCtx,
|
|
933
|
+
conversationCtx: routeContexts.connectionCtx,
|
|
934
|
+
goalHandler: domainHandlers.goalHandler,
|
|
935
|
+
specsHandler: domainHandlers.specsHandler,
|
|
936
|
+
sddBoardHandler: domainHandlers.sddBoardHandler,
|
|
937
|
+
sddWizardHandler: domainHandlers.sddWizardHandler,
|
|
938
|
+
worktreeHandler: domainHandlers.worktreeHandler,
|
|
939
|
+
terminalHandler: domainHandlers.terminalHandler,
|
|
940
|
+
kanbanHostRoutes: input.kanbanHostRoutes,
|
|
941
|
+
statusTracker: opts.statusTracker
|
|
1117
942
|
});
|
|
1118
|
-
if (!ipv6Listening) return null;
|
|
1119
|
-
console.log(`[WebUI] Also listening on http://[::1]:${httpPort}`);
|
|
1120
|
-
return v6;
|
|
1121
943
|
}
|
|
1122
944
|
|
|
1123
945
|
// src/webui-server/prefs-seeding.ts
|
|
1124
946
|
import * as fs from "node:fs/promises";
|
|
1125
|
-
import * as
|
|
947
|
+
import * as path5 from "node:path";
|
|
1126
948
|
import { DefaultSecretVault as DefaultSecretVault2 } from "@wrongstack/core/security";
|
|
1127
949
|
import {
|
|
1128
|
-
PREF_KEYS,
|
|
1129
950
|
persistPrefsToConfig,
|
|
1130
951
|
seedContextMeta,
|
|
1131
952
|
prefSnapshot as snapshotPrefs
|
|
@@ -1199,8 +1020,8 @@ function createPrefsSeeding(opts) {
|
|
|
1199
1020
|
patchLiveAppConfig({ modelTiers: payload["modelTiers"] });
|
|
1200
1021
|
}
|
|
1201
1022
|
if (!opts.profileConfigPath) return;
|
|
1202
|
-
const globalRoot = opts.globalConfigPath ?
|
|
1203
|
-
const vault = new DefaultSecretVault2({ keyFile:
|
|
1023
|
+
const globalRoot = opts.globalConfigPath ? path5.dirname(opts.globalConfigPath) : path5.dirname(opts.profileConfigPath);
|
|
1024
|
+
const vault = new DefaultSecretVault2({ keyFile: path5.join(globalRoot, ".key") });
|
|
1204
1025
|
await persistPrefsToConfig(
|
|
1205
1026
|
{
|
|
1206
1027
|
profileConfigPath: opts.profileConfigPath,
|
|
@@ -1228,122 +1049,252 @@ function createPrefsSeeding(opts) {
|
|
|
1228
1049
|
};
|
|
1229
1050
|
}
|
|
1230
1051
|
|
|
1231
|
-
// src/webui-server/
|
|
1232
|
-
import * as path5 from "node:path";
|
|
1052
|
+
// src/webui-server/provider-plane.ts
|
|
1233
1053
|
import {
|
|
1234
|
-
|
|
1235
|
-
seedSessionSubagentPolicy,
|
|
1236
|
-
setSessionSubagentModelPlan,
|
|
1237
|
-
setSessionSubagentPolicy,
|
|
1238
|
-
subagentPolicyModeFrom
|
|
1239
|
-
} from "@wrongstack/core/coordination";
|
|
1240
|
-
import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
|
|
1241
|
-
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
1242
|
-
import { PromptUsageStore } from "@wrongstack/core/storage";
|
|
1243
|
-
import { resolveTokenSavingTier } from "@wrongstack/core/types";
|
|
1244
|
-
import { isOutsideProject, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
1245
|
-
import {
|
|
1246
|
-
createChimeraRouteHandlers,
|
|
1247
|
-
createCodeAssistRouteHandlers,
|
|
1248
|
-
createMailboxRouteHandlers,
|
|
1249
|
-
rebuildSystemPrompt
|
|
1054
|
+
createEmbeddedProviderOperations
|
|
1250
1055
|
} from "@wrongstack/webui-server";
|
|
1251
|
-
|
|
1056
|
+
|
|
1057
|
+
// src/webui-server/credential-watcher.ts
|
|
1058
|
+
import { TOKENS } from "@wrongstack/core/kernel";
|
|
1059
|
+
import { watchProviderConfig } from "@wrongstack/core/storage";
|
|
1060
|
+
import { toErrorMessage } from "@wrongstack/core/utils/error";
|
|
1061
|
+
import { makeProviderFromConfig } from "@wrongstack/providers";
|
|
1062
|
+
function startWebuiCredentialWatcher({
|
|
1252
1063
|
opts,
|
|
1253
1064
|
profileConfigPath,
|
|
1254
|
-
profileDir,
|
|
1255
|
-
globalRoot,
|
|
1256
|
-
sessionStartedAt,
|
|
1257
|
-
currentSessionId,
|
|
1258
|
-
getCustomModeStore,
|
|
1259
|
-
buildSessionStartPayload,
|
|
1260
|
-
prefSnapshot,
|
|
1261
|
-
persistPrefs,
|
|
1262
|
-
pendingConfirms,
|
|
1263
|
-
abortControllers,
|
|
1264
|
-
getSessionAgent,
|
|
1265
|
-
peekSessionAgent,
|
|
1266
|
-
isSessionLive,
|
|
1267
|
-
getForegroundSession,
|
|
1268
|
-
setForegroundSession,
|
|
1269
|
-
clients,
|
|
1270
|
-
onSessionsUndisplayed,
|
|
1271
|
-
autoWake,
|
|
1272
|
-
stopSessionFleet,
|
|
1273
|
-
send,
|
|
1274
1065
|
broadcast,
|
|
1275
|
-
|
|
1066
|
+
broadcastSaved
|
|
1276
1067
|
}) {
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1068
|
+
const watchConfigPath = profileConfigPath;
|
|
1069
|
+
if (!watchConfigPath || process.env["WRONGSTACK_DISABLE_CONFIG_WATCH"] === "1") {
|
|
1070
|
+
return void 0;
|
|
1071
|
+
}
|
|
1072
|
+
let lastActiveCfg = JSON.stringify(
|
|
1073
|
+
opts.appConfig?.providers?.[opts.agent.ctx.provider.id] ?? null
|
|
1074
|
+
);
|
|
1075
|
+
let lastUiLocale = opts.appConfig?.uiLocale;
|
|
1076
|
+
const watcher = watchProviderConfig(
|
|
1077
|
+
watchConfigPath,
|
|
1078
|
+
// Vault key lives at ~/.wrongstack/.key (derived from globalConfigPath),
|
|
1079
|
+
// not inside the profile directory — resolve via global root to stay correct.
|
|
1080
|
+
getVault(opts.globalConfigPath),
|
|
1081
|
+
(snapshot) => {
|
|
1082
|
+
const hadFallbackBridge = Boolean(opts.appConfig?.fallbackBridge?.trim());
|
|
1083
|
+
try {
|
|
1084
|
+
if (opts.appConfig && !Object.isFrozen(opts.appConfig)) {
|
|
1085
|
+
opts.appConfig.providers = snapshot.providers;
|
|
1086
|
+
if (snapshot.uiLocale) opts.appConfig.uiLocale = snapshot.uiLocale;
|
|
1087
|
+
if (snapshot.fallbackBridge !== void 0) {
|
|
1088
|
+
opts.appConfig.fallbackBridge = snapshot.fallbackBridge;
|
|
1089
|
+
} else {
|
|
1090
|
+
delete opts.appConfig.fallbackBridge;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
} catch {
|
|
1094
|
+
}
|
|
1095
|
+
const routingChanged = snapshot.fallbackModels !== void 0 || snapshot.fallbackBridge !== void 0 || hadFallbackBridge || snapshot.fallbackProfiles !== void 0 || snapshot.favoriteModels !== void 0 || snapshot.disabledModels !== void 0 || snapshot.disabledProviders !== void 0 || snapshot.favoriteModelsOnly !== void 0 || snapshot.modelMatrix !== void 0 || snapshot.fallbackAuto !== void 0;
|
|
1096
|
+
if (routingChanged) {
|
|
1097
|
+
const configStore = opts.agent.container?.safeResolve?.(TOKENS.ConfigStore);
|
|
1098
|
+
configStore?.update({
|
|
1099
|
+
...snapshot.fallbackModels !== void 0 ? { fallbackModels: snapshot.fallbackModels } : {},
|
|
1100
|
+
fallbackBridge: snapshot.fallbackBridge ?? "",
|
|
1101
|
+
...snapshot.fallbackProfiles !== void 0 ? { fallbackProfiles: snapshot.fallbackProfiles } : {},
|
|
1102
|
+
...snapshot.favoriteModels !== void 0 ? { favoriteModels: snapshot.favoriteModels } : {},
|
|
1103
|
+
...snapshot.disabledModels !== void 0 ? { disabledModels: snapshot.disabledModels } : {},
|
|
1104
|
+
...snapshot.disabledProviders !== void 0 ? { disabledProviders: snapshot.disabledProviders } : {},
|
|
1105
|
+
...snapshot.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snapshot.favoriteModelsOnly } : {},
|
|
1106
|
+
...snapshot.modelMatrix !== void 0 ? { modelMatrix: snapshot.modelMatrix } : {},
|
|
1107
|
+
...snapshot.fallbackAuto !== void 0 ? { fallbackAuto: snapshot.fallbackAuto } : {}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
broadcastSaved(snapshot.providers);
|
|
1111
|
+
if (snapshot.uiLocale !== lastUiLocale) {
|
|
1112
|
+
lastUiLocale = snapshot.uiLocale;
|
|
1113
|
+
if (snapshot.uiLocale) {
|
|
1114
|
+
opts.agent.ctx.meta["uiLocale"] = snapshot.uiLocale;
|
|
1115
|
+
broadcast({ type: "prefs.updated", payload: { uiLocale: snapshot.uiLocale } });
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const activeId = opts.agent.ctx.provider.id;
|
|
1119
|
+
const newCfgStr = JSON.stringify(snapshot.providers[activeId] ?? null);
|
|
1120
|
+
if (newCfgStr === lastActiveCfg) return;
|
|
1121
|
+
lastActiveCfg = newCfgStr;
|
|
1122
|
+
try {
|
|
1123
|
+
const newCfg = snapshot.providers[activeId] ?? {
|
|
1124
|
+
type: activeId,
|
|
1125
|
+
...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
|
|
1126
|
+
...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
|
|
1127
|
+
};
|
|
1128
|
+
const oldMax = opts.agent.ctx.provider.capabilities?.maxContext;
|
|
1129
|
+
const prov = makeProviderFromConfig(activeId, {
|
|
1130
|
+
...newCfg,
|
|
1131
|
+
type: newCfg.type ?? activeId
|
|
1132
|
+
});
|
|
1133
|
+
if (oldMax != null && prov.capabilities) prov.capabilities.maxContext = oldMax;
|
|
1134
|
+
opts.agent.ctx.provider = prov;
|
|
1135
|
+
console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
console.warn(
|
|
1138
|
+
`[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage(err)}`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
warn: (m) => console.warn(
|
|
1144
|
+
JSON.stringify({
|
|
1145
|
+
level: "warn",
|
|
1146
|
+
event: "webui.config_watcher",
|
|
1147
|
+
message: m,
|
|
1148
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1149
|
+
})
|
|
1150
|
+
)
|
|
1151
|
+
}
|
|
1152
|
+
);
|
|
1153
|
+
return watcher.close;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/webui-server/provider-plane.ts
|
|
1157
|
+
function createEmbeddedProviderPlane(input) {
|
|
1158
|
+
const { opts, profileConfigPath, send, broadcast } = input;
|
|
1159
|
+
const wsHandlerCtx = {
|
|
1160
|
+
providerStore: createProviderConfigStore(
|
|
1161
|
+
profileConfigPath,
|
|
1162
|
+
() => opts.appConfig?.providers ?? {}
|
|
1163
|
+
),
|
|
1164
|
+
modelsRegistry: opts.modelsRegistry,
|
|
1165
|
+
providerAuthRegistry: opts.providerAuthRegistry,
|
|
1166
|
+
getDisabledModels: () => opts.appConfig?.disabledModels ?? [],
|
|
1167
|
+
getDisabledProviders: () => opts.appConfig?.disabledProviders ?? [],
|
|
1168
|
+
send,
|
|
1169
|
+
broadcast,
|
|
1170
|
+
log: (m) => console.log(m)
|
|
1171
|
+
};
|
|
1172
|
+
const embeddedProviderOperations = createEmbeddedProviderOperations(wsHandlerCtx);
|
|
1173
|
+
const credentialWatcherClose = startWebuiCredentialWatcher({
|
|
1174
|
+
opts,
|
|
1175
|
+
profileConfigPath,
|
|
1176
|
+
broadcast,
|
|
1177
|
+
broadcastSaved: (providers) => embeddedProviderOperations.broadcastSaved(providers)
|
|
1178
|
+
});
|
|
1179
|
+
return { wsHandlerCtx, credentialWatcherClose };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/webui-server/route-contexts.ts
|
|
1183
|
+
import * as path6 from "node:path";
|
|
1184
|
+
import {
|
|
1185
|
+
normalizeSubagentModelPlan,
|
|
1186
|
+
seedSessionSubagentPolicy,
|
|
1187
|
+
setSessionSubagentModelPlan,
|
|
1188
|
+
setSessionSubagentPolicy,
|
|
1189
|
+
subagentPolicyModeFrom
|
|
1190
|
+
} from "@wrongstack/core/coordination";
|
|
1191
|
+
import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
|
|
1192
|
+
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
1193
|
+
import { PromptUsageStore } from "@wrongstack/core/storage";
|
|
1194
|
+
import { resolveTokenSavingTier } from "@wrongstack/core/types";
|
|
1195
|
+
import { isOutsideProject, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
1196
|
+
import {
|
|
1197
|
+
createChimeraRouteHandlers,
|
|
1198
|
+
createCodeAssistRouteHandlers,
|
|
1199
|
+
createMailboxRouteHandlers,
|
|
1200
|
+
rebuildSystemPrompt
|
|
1201
|
+
} from "@wrongstack/webui-server";
|
|
1202
|
+
function createWebuiRouteContexts({
|
|
1203
|
+
opts,
|
|
1204
|
+
profileConfigPath,
|
|
1205
|
+
profileDir,
|
|
1206
|
+
globalRoot,
|
|
1207
|
+
sessionStartedAt,
|
|
1208
|
+
currentSessionId,
|
|
1209
|
+
getCustomModeStore,
|
|
1210
|
+
buildSessionStartPayload,
|
|
1211
|
+
prefSnapshot,
|
|
1212
|
+
persistPrefs,
|
|
1213
|
+
pendingConfirms,
|
|
1214
|
+
abortControllers,
|
|
1215
|
+
getSessionAgent,
|
|
1216
|
+
peekSessionAgent,
|
|
1217
|
+
isSessionLive,
|
|
1218
|
+
getForegroundSession,
|
|
1219
|
+
setForegroundSession,
|
|
1220
|
+
clients,
|
|
1221
|
+
onSessionsUndisplayed,
|
|
1222
|
+
autoWake,
|
|
1223
|
+
stopSessionFleet,
|
|
1224
|
+
send,
|
|
1225
|
+
broadcast,
|
|
1226
|
+
broadcastEveryone
|
|
1227
|
+
}) {
|
|
1228
|
+
const brainCtx = {
|
|
1229
|
+
brainSettings: opts.brainSettings,
|
|
1230
|
+
brainRuntime: opts.brainRuntime,
|
|
1231
|
+
getBrainLog: opts.getBrainLog,
|
|
1232
|
+
resolveArbiter: () => opts.brain ?? (opts.agent.container.has(TOKENS2.BrainArbiter) ? opts.agent.container.resolve(TOKENS2.BrainArbiter) : void 0),
|
|
1233
|
+
getSessionId: currentSessionId,
|
|
1234
|
+
send
|
|
1235
|
+
};
|
|
1236
|
+
const introspectionConfigStore = opts.agent.container?.safeResolve?.(TOKENS2.ConfigStore);
|
|
1237
|
+
const introspectionCtx = {
|
|
1238
|
+
agent: opts.agent,
|
|
1239
|
+
// Answer `diag.get` / `stats.get` / `side_effects.list` from the agent that
|
|
1240
|
+
// owns the ASKING session. `opts.agent` is the leader, i.e. whichever tab
|
|
1241
|
+
// the runtime last switched to, so a background tab asking for its own
|
|
1242
|
+
// stats got the foreground's numbers stamped with the foreground's id —
|
|
1243
|
+
// which the browser's session filter then dropped, leaving the panel blank.
|
|
1244
|
+
// Read path, so `peek` first: `getAgent` creates, and materialising an
|
|
1245
|
+
// agent for a stale id can evict a live tab's.
|
|
1246
|
+
getAgent: (sessionId) => peekSessionAgent?.(sessionId) ?? (sessionId ? getSessionAgent(sessionId) : opts.agent),
|
|
1247
|
+
modelsRegistry: opts.modelsRegistry,
|
|
1248
|
+
configStore: introspectionConfigStore,
|
|
1249
|
+
getConfig: () => {
|
|
1250
|
+
const cfg = introspectionConfigStore?.get() ?? opts.appConfig;
|
|
1251
|
+
if (!cfg)
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
"Introspection route requires a config but neither ConfigStore nor opts.appConfig is available"
|
|
1254
|
+
);
|
|
1255
|
+
return cfg;
|
|
1256
|
+
},
|
|
1257
|
+
getProjectRoot: () => opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "",
|
|
1258
|
+
getSessionId: currentSessionId,
|
|
1259
|
+
getSessionStartedAt: () => sessionStartedAt,
|
|
1260
|
+
getModeId: () => opts.modeId ?? "default",
|
|
1261
|
+
send
|
|
1262
|
+
};
|
|
1263
|
+
const skillsProjectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
|
|
1264
|
+
const skillsPaths = skillsProjectRoot ? resolveWstackPaths3({
|
|
1265
|
+
projectRoot: skillsProjectRoot,
|
|
1266
|
+
globalRoot
|
|
1267
|
+
}) : void 0;
|
|
1268
|
+
const skillsCtx = {
|
|
1269
|
+
skillLoader: opts.skillLoader,
|
|
1270
|
+
skillInstaller: opts.skillLoader ? new SkillInstaller({
|
|
1271
|
+
manifestPath: path6.join(skillsPaths?.configDir ?? profileDir, "installed-skills.json"),
|
|
1272
|
+
projectSkillsDir: skillsPaths?.inProjectSkills ?? path6.join(skillsProjectRoot, ".wrongstack", "skills"),
|
|
1273
|
+
globalSkillsDir: skillsPaths?.globalSkills ?? path6.join(profileDir, "skills"),
|
|
1274
|
+
projectHash: skillsPaths?.projectHash ?? "",
|
|
1275
|
+
skillLoader: opts.skillLoader
|
|
1276
|
+
}) : void 0,
|
|
1277
|
+
projectRoot: skillsProjectRoot,
|
|
1278
|
+
projectSkillsDir: skillsPaths?.inProjectSkills,
|
|
1279
|
+
globalSkillsDir: skillsPaths?.globalSkills
|
|
1280
|
+
};
|
|
1281
|
+
const promptsCtx = {
|
|
1282
|
+
promptLoader: opts.promptLoader,
|
|
1283
|
+
promptUsage: new PromptUsageStore(
|
|
1284
|
+
skillsPaths?.promptUsage ?? path6.join(profileDir, "prompt-usage.json")
|
|
1285
|
+
)
|
|
1286
|
+
};
|
|
1287
|
+
const designCtx = (sessionId) => ({
|
|
1288
|
+
projectRoot: skillsProjectRoot,
|
|
1289
|
+
agentMeta: (sessionId ? getSessionAgent(sessionId)?.ctx : void 0) ?? opts.agent.ctx
|
|
1290
|
+
});
|
|
1291
|
+
const agentConfigCtx = {
|
|
1292
|
+
agent: opts.agent,
|
|
1293
|
+
modeStore: opts.modeStore,
|
|
1294
|
+
loadSavedProviders: () => loadSavedProviders(profileConfigPath),
|
|
1295
|
+
buildSessionStart: (overrides) => buildSessionStartPayload(overrides),
|
|
1296
|
+
modelsRegistry: opts.modelsRegistry,
|
|
1297
|
+
memoryStore: opts.memoryStore,
|
|
1347
1298
|
getConfig: () => opts.appConfig,
|
|
1348
1299
|
onMaxContextResolved: opts.onModelContextResolved,
|
|
1349
1300
|
persistPrefs,
|
|
@@ -1458,7 +1409,7 @@ function createWebuiRouteContexts({
|
|
|
1458
1409
|
};
|
|
1459
1410
|
const mailboxRoutes = createMailboxRouteHandlers({
|
|
1460
1411
|
getProjectRoot: () => opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "",
|
|
1461
|
-
getGlobalRoot: () => opts.globalConfigPath ?
|
|
1412
|
+
getGlobalRoot: () => opts.globalConfigPath ? path6.dirname(opts.globalConfigPath) : "",
|
|
1462
1413
|
events: opts.events
|
|
1463
1414
|
});
|
|
1464
1415
|
const chimeraRoutes = createChimeraRouteHandlers({
|
|
@@ -1467,7 +1418,7 @@ function createWebuiRouteContexts({
|
|
|
1467
1418
|
// lives. A project switch re-roots it, hence the getter.
|
|
1468
1419
|
projectDir: () => resolveWstackPaths3({
|
|
1469
1420
|
projectRoot: opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "",
|
|
1470
|
-
globalRoot: opts.globalConfigPath ?
|
|
1421
|
+
globalRoot: opts.globalConfigPath ? path6.dirname(opts.globalConfigPath) : ""
|
|
1471
1422
|
}).projectDir,
|
|
1472
1423
|
send,
|
|
1473
1424
|
log: (m) => console.warn(m)
|
|
@@ -1540,8 +1491,29 @@ function createWebuiRouteContexts({
|
|
|
1540
1491
|
};
|
|
1541
1492
|
}
|
|
1542
1493
|
|
|
1494
|
+
// src/webui-server/session-agents.ts
|
|
1495
|
+
import { createSessionAgentRegistry } from "@wrongstack/webui-server";
|
|
1496
|
+
function createEmbeddedSessionAgents(opts, clients, abortControllers) {
|
|
1497
|
+
const isSessionDisplayed = (sessionId) => {
|
|
1498
|
+
for (const client of clients.values()) {
|
|
1499
|
+
if (client.sessionId === sessionId) return true;
|
|
1500
|
+
if (client.sessionIds?.has(sessionId) === true) return true;
|
|
1501
|
+
}
|
|
1502
|
+
return false;
|
|
1503
|
+
};
|
|
1504
|
+
const sessionAgents = createSessionAgentRegistry({
|
|
1505
|
+
template: opts.agent,
|
|
1506
|
+
...opts.modelsRegistry ? { modelsRegistry: opts.modelsRegistry } : {},
|
|
1507
|
+
isRunActive: (sessionId) => abortControllers.has(sessionId),
|
|
1508
|
+
// A tab that is still on screen must outlive one that was closed, whatever
|
|
1509
|
+
// order their agents were created in.
|
|
1510
|
+
isDisplayed: isSessionDisplayed
|
|
1511
|
+
});
|
|
1512
|
+
return { sessionAgents, isSessionDisplayed };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1543
1515
|
// src/webui-server/session-start-payload.ts
|
|
1544
|
-
import * as
|
|
1516
|
+
import * as path7 from "node:path";
|
|
1545
1517
|
import { DEFAULT_CONTEXT_WINDOW_MODE_ID } from "@wrongstack/core/types";
|
|
1546
1518
|
import { catalogProviderIdFor } from "@wrongstack/providers";
|
|
1547
1519
|
import { protocolAdvertisement } from "@wrongstack/webui-protocol";
|
|
@@ -1579,220 +1551,635 @@ function createSessionStartPayloadBuilder(deps) {
|
|
|
1579
1551
|
} else {
|
|
1580
1552
|
maxContext = ctx.provider.capabilities?.maxContext ?? 0;
|
|
1581
1553
|
}
|
|
1582
|
-
} catch {
|
|
1583
|
-
}
|
|
1584
|
-
const lastInputTokens = typeof ctx.lastRequestTokens === "number" && ctx.lastRequestTokens > 0 ? ctx.lastRequestTokens : 0;
|
|
1585
|
-
return {
|
|
1586
|
-
sessionId: targetSessionId,
|
|
1587
|
-
...startedAt ? { startedAt } : {},
|
|
1588
|
-
model: ctx.model,
|
|
1589
|
-
provider: ctx.provider.id,
|
|
1590
|
-
mode: deps.modeId ?? "default",
|
|
1591
|
-
projectName: deps.projectRoot ?
|
|
1592
|
-
// Frontend reads `projectRoot` from session.start (ws-handlers setEnv) —
|
|
1593
|
-
// omitting it left the store's projectRoot empty after a project switch.
|
|
1594
|
-
projectRoot: deps.projectRoot ?? ctx.projectRoot ?? "",
|
|
1595
|
-
cwd: deps.projectRoot ?? ctx.projectRoot ?? "",
|
|
1596
|
-
needsSetup,
|
|
1597
|
-
// true when provider/model not configured and running in --webui mode
|
|
1598
|
-
contextMode: String(ctx.meta?.["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
1599
|
-
maxContext,
|
|
1600
|
-
inputCost,
|
|
1601
|
-
outputCost,
|
|
1602
|
-
cacheReadCost,
|
|
1603
|
-
lastInputTokens,
|
|
1604
|
-
providerStatuses: deps.statusTracker?.getAllStatuses() ?? [],
|
|
1605
|
-
appVersion: updateInfo?.current,
|
|
1606
|
-
latestVersion: updateInfo?.latest,
|
|
1607
|
-
updateAvailable: updateInfo?.outdated ?? false,
|
|
1608
|
-
updateCheckFailed: updateInfo?.checkFailed ?? false,
|
|
1609
|
-
...protocolAdvertisement(),
|
|
1610
|
-
...overrides
|
|
1611
|
-
};
|
|
1612
|
-
};
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
const lastInputTokens = typeof ctx.lastRequestTokens === "number" && ctx.lastRequestTokens > 0 ? ctx.lastRequestTokens : 0;
|
|
1557
|
+
return {
|
|
1558
|
+
sessionId: targetSessionId,
|
|
1559
|
+
...startedAt ? { startedAt } : {},
|
|
1560
|
+
model: ctx.model,
|
|
1561
|
+
provider: ctx.provider.id,
|
|
1562
|
+
mode: deps.modeId ?? "default",
|
|
1563
|
+
projectName: deps.projectRoot ? path7.basename(deps.projectRoot) : void 0,
|
|
1564
|
+
// Frontend reads `projectRoot` from session.start (ws-handlers setEnv) —
|
|
1565
|
+
// omitting it left the store's projectRoot empty after a project switch.
|
|
1566
|
+
projectRoot: deps.projectRoot ?? ctx.projectRoot ?? "",
|
|
1567
|
+
cwd: deps.projectRoot ?? ctx.projectRoot ?? "",
|
|
1568
|
+
needsSetup,
|
|
1569
|
+
// true when provider/model not configured and running in --webui mode
|
|
1570
|
+
contextMode: String(ctx.meta?.["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
1571
|
+
maxContext,
|
|
1572
|
+
inputCost,
|
|
1573
|
+
outputCost,
|
|
1574
|
+
cacheReadCost,
|
|
1575
|
+
lastInputTokens,
|
|
1576
|
+
providerStatuses: deps.statusTracker?.getAllStatuses() ?? [],
|
|
1577
|
+
appVersion: updateInfo?.current,
|
|
1578
|
+
latestVersion: updateInfo?.latest,
|
|
1579
|
+
updateAvailable: updateInfo?.outdated ?? false,
|
|
1580
|
+
updateCheckFailed: updateInfo?.checkFailed ?? false,
|
|
1581
|
+
...protocolAdvertisement(),
|
|
1582
|
+
...overrides
|
|
1583
|
+
};
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/webui-server/session-start-live.ts
|
|
1588
|
+
function createLiveSessionStartPayloadBuilder(opts, getSessionAgents) {
|
|
1589
|
+
return createSessionStartPayloadBuilder({
|
|
1590
|
+
...opts,
|
|
1591
|
+
// Read through to the live `opts`, do NOT snapshot: `projects.select`
|
|
1592
|
+
// re-roots the host by assigning `opts.projectRoot` / `opts.session` on
|
|
1593
|
+
// this very object. A spread copy froze both at boot, so every
|
|
1594
|
+
// `session.start` broadcast after a project switch still announced the
|
|
1595
|
+
// previous project's root — the switch looked like it had not happened.
|
|
1596
|
+
get projectRoot() {
|
|
1597
|
+
return opts.projectRoot;
|
|
1598
|
+
},
|
|
1599
|
+
get session() {
|
|
1600
|
+
return opts.session;
|
|
1601
|
+
},
|
|
1602
|
+
get appConfig() {
|
|
1603
|
+
return opts.appConfig;
|
|
1604
|
+
},
|
|
1605
|
+
// `peek`, never `get`: building a payload must not materialise an agent
|
|
1606
|
+
// for a session id that arrived from a stale browser tab.
|
|
1607
|
+
getSessionContext: (sessionId) => getSessionAgents()?.peek(sessionId)?.ctx
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/webui-server/setup-events.ts
|
|
1612
|
+
import { mailboxSessionTag as mailboxSessionTag2 } from "@wrongstack/core/coordination";
|
|
1613
|
+
import {
|
|
1614
|
+
createEternalSubscription,
|
|
1615
|
+
setupEvents as setupCanonicalEvents
|
|
1616
|
+
} from "@wrongstack/webui-server";
|
|
1617
|
+
function createSetupEvents(deps) {
|
|
1618
|
+
let fleetConcurrency = 0;
|
|
1619
|
+
let fleetConcurrencyMax = 4;
|
|
1620
|
+
const emitConcurrency = () => {
|
|
1621
|
+
const budget = deps.getFleetBudget?.() ?? null;
|
|
1622
|
+
deps.broadcast({
|
|
1623
|
+
type: "fleet.concurrency_update",
|
|
1624
|
+
// Deliberately UNSTAMPED. The spawn/concurrency budget is one ceiling
|
|
1625
|
+
// for the whole process — every tab draws from it and the client keeps
|
|
1626
|
+
// it in a single global store — so this is a project-wide fact, not a
|
|
1627
|
+
// conversation's. Stamping it with the runtime's session made it a
|
|
1628
|
+
// conversation's by accident: `clientWantsSession` then dropped it for
|
|
1629
|
+
// any page whose declared tabs did not include that session, and the
|
|
1630
|
+
// fleet gauges froze the moment the boot tab was closed.
|
|
1631
|
+
payload: {
|
|
1632
|
+
fleetConcurrency: budget?.activeAgents ?? fleetConcurrency,
|
|
1633
|
+
fleetConcurrencyMax: budget?.maxConcurrent ?? fleetConcurrencyMax,
|
|
1634
|
+
...budget ? {
|
|
1635
|
+
maxSpawns: budget.maxSpawns,
|
|
1636
|
+
usedSpawns: budget.usedSpawns,
|
|
1637
|
+
remainingSpawns: budget.remainingSpawns,
|
|
1638
|
+
maxSpawnsSource: budget.maxSpawnsSource,
|
|
1639
|
+
maxConcurrentSource: budget.maxConcurrentSource,
|
|
1640
|
+
effectiveSource: budget.effectiveSource,
|
|
1641
|
+
checkpointMaxSpawns: budget.checkpointMaxSpawns,
|
|
1642
|
+
ceilingMismatch: budget.ceilingMismatch
|
|
1643
|
+
} : {}
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
};
|
|
1647
|
+
return function setupEvents() {
|
|
1648
|
+
for (const unsubscribe of deps.eventUnsubscribers) unsubscribe();
|
|
1649
|
+
deps.eventUnsubscribers.length = 0;
|
|
1650
|
+
const maxIterations = deps.agent.ctx?.meta?.["maxIterations"];
|
|
1651
|
+
const disposeCanonical = setupCanonicalEvents({
|
|
1652
|
+
events: deps.events,
|
|
1653
|
+
// The third argument is not decoration: it names the tab that OWNS a
|
|
1654
|
+
// subagent, and the frames that pass it carry the subagent's own
|
|
1655
|
+
// session on the payload. Dropping it here routed every subagent
|
|
1656
|
+
// codemap frame at a session no tab subscribes to.
|
|
1657
|
+
broadcast: (_clients, message, targetSessionId) => targetSessionId !== void 0 ? deps.broadcast(message, targetSessionId) : deps.broadcast(message),
|
|
1658
|
+
clients: deps.getClients(),
|
|
1659
|
+
config: {
|
|
1660
|
+
tools: {
|
|
1661
|
+
...typeof maxIterations === "number" ? { maxIterations } : {}
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
context: deps.agent.ctx,
|
|
1665
|
+
...deps.sessionContext ? { sessionContext: deps.sessionContext } : {},
|
|
1666
|
+
pendingConfirms: deps.pendingConfirms,
|
|
1667
|
+
globalConfigPath: deps.globalConfigPath,
|
|
1668
|
+
onFleetBroadcaster: deps.onFleetBroadcaster,
|
|
1669
|
+
projection: {
|
|
1670
|
+
scrubObject: (value) => deps.secretScrubber.scrubObject(value),
|
|
1671
|
+
queueTextDelta: deps.queueTextDelta,
|
|
1672
|
+
queueThinkingDelta: deps.queueThinkingDelta,
|
|
1673
|
+
queueToolProgress: (payload) => deps.queueToolProgress(payload),
|
|
1674
|
+
flushThinkingDelta: deps.flushThinkingDelta,
|
|
1675
|
+
flushAllStreamBuffers: deps.flushAllStreamBuffers
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
deps.eventUnsubscribers.push(disposeCanonical);
|
|
1679
|
+
deps.broadcast({
|
|
1680
|
+
type: "subagent.event",
|
|
1681
|
+
payload: deps.sessionPayload({
|
|
1682
|
+
kind: "leader_updated",
|
|
1683
|
+
subagentId: `leader@${mailboxSessionTag2(deps.currentSessionId())}`,
|
|
1684
|
+
isLeader: true,
|
|
1685
|
+
name: "Leader",
|
|
1686
|
+
status: "running"
|
|
1687
|
+
})
|
|
1688
|
+
});
|
|
1689
|
+
emitConcurrency();
|
|
1690
|
+
deps.eventUnsubscribers.push(
|
|
1691
|
+
deps.events.on("concurrency.changed", (event) => {
|
|
1692
|
+
fleetConcurrencyMax = Math.max(1, event.n);
|
|
1693
|
+
emitConcurrency();
|
|
1694
|
+
}),
|
|
1695
|
+
deps.events.on("subagent.spawned", () => {
|
|
1696
|
+
fleetConcurrency += 1;
|
|
1697
|
+
emitConcurrency();
|
|
1698
|
+
}),
|
|
1699
|
+
deps.events.on("subagent.task_completed", () => {
|
|
1700
|
+
fleetConcurrency = Math.max(0, fleetConcurrency - 1);
|
|
1701
|
+
emitConcurrency();
|
|
1702
|
+
})
|
|
1703
|
+
);
|
|
1704
|
+
if (deps.subscribeEternalIteration) {
|
|
1705
|
+
const subscription = createEternalSubscription(
|
|
1706
|
+
deps.subscribeEternalIteration,
|
|
1707
|
+
(_clients, message) => deps.broadcast(message),
|
|
1708
|
+
deps.getClients
|
|
1709
|
+
);
|
|
1710
|
+
deps.eventUnsubscribers.push(() => subscription.dispose());
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// src/webui-server/stream-coalescer.ts
|
|
1716
|
+
import {
|
|
1717
|
+
createStreamCoalescer
|
|
1718
|
+
} from "@wrongstack/webui-server";
|
|
1719
|
+
|
|
1720
|
+
// src/webui-server/transport.ts
|
|
1721
|
+
import {
|
|
1722
|
+
clientWantsSession,
|
|
1723
|
+
sendSerialized,
|
|
1724
|
+
stampDispatchSession,
|
|
1725
|
+
webuiSessionFrameLog
|
|
1726
|
+
} from "@wrongstack/webui-server";
|
|
1727
|
+
function createWebuiTransport(clients) {
|
|
1728
|
+
function send(ws, msg) {
|
|
1729
|
+
sendSerialized(ws, JSON.stringify(stampDispatchSession(msg)));
|
|
1730
|
+
}
|
|
1731
|
+
function broadcast(msg, targetSessionId) {
|
|
1732
|
+
const payload = msg.payload;
|
|
1733
|
+
const sessionId = targetSessionId ?? (payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0);
|
|
1734
|
+
const data = sessionId ? webuiSessionFrameLog().sequence(sessionId, msg) : JSON.stringify(msg);
|
|
1735
|
+
for (const [ws, client] of clients) {
|
|
1736
|
+
if (clientWantsSession(client, sessionId)) sendSerialized(ws, data);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function broadcastEveryone(msg) {
|
|
1740
|
+
const data = JSON.stringify(msg);
|
|
1741
|
+
for (const [ws] of clients) sendSerialized(ws, data);
|
|
1742
|
+
}
|
|
1743
|
+
function sendResult(ws, success, message) {
|
|
1744
|
+
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
1745
|
+
}
|
|
1746
|
+
return { send, broadcast, broadcastEveryone, sendResult };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// src/webui-server-lifecycle.ts
|
|
1750
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
|
|
1751
|
+
|
|
1752
|
+
// src/webui-server/connection-handler.ts
|
|
1753
|
+
import { deriveSessionAgents } from "@wrongstack/core/storage";
|
|
1754
|
+
import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
|
|
1755
|
+
import {
|
|
1756
|
+
createConnectionLifecycle,
|
|
1757
|
+
verifyClient as verifyWsClient2
|
|
1758
|
+
} from "@wrongstack/webui-server";
|
|
1759
|
+
function createConnectionHandler(deps) {
|
|
1760
|
+
const clientHandlers = [
|
|
1761
|
+
deps.goalHandler,
|
|
1762
|
+
deps.specsHandler,
|
|
1763
|
+
deps.sddBoardHandler,
|
|
1764
|
+
...deps.sddWizardHandler ? [deps.sddWizardHandler] : [],
|
|
1765
|
+
deps.worktreeHandler,
|
|
1766
|
+
deps.terminalHandler
|
|
1767
|
+
];
|
|
1768
|
+
return createConnectionLifecycle({
|
|
1769
|
+
clients: deps.clients,
|
|
1770
|
+
pendingConfirms: deps.pendingConfirms,
|
|
1771
|
+
// ── Auth trust model ────────────────────────────────────────────────
|
|
1772
|
+
// Three layered defenses (see verifyClient in @wrongstack/webui-server/ws-auth):
|
|
1773
|
+
// 1. DNS-rebinding guard (Host header must be loopback on loopback bind)
|
|
1774
|
+
// 2. Shared-token auth via Cookie (ws_token) or URL query param (?token=)
|
|
1775
|
+
// 3. Loopback bootstrap: when requireToken=false (default) AND the server
|
|
1776
|
+
// is bound to a loopback interface, browser clients bypass token auth
|
|
1777
|
+
// entirely — the Origin header being http://localhost is sufficient.
|
|
1778
|
+
//
|
|
1779
|
+
// COOKIE PERSISTENCE AFTER TOKEN ROTATION:
|
|
1780
|
+
// The HttpOnly ws_token cookie, once set by /ws-auth, survives a process
|
|
1781
|
+
// restart. On the next process the expectedToken is a fresh random value,
|
|
1782
|
+
// so the old cookie alone would fail the token check. However, on a
|
|
1783
|
+
// loopback bind with requireToken=false (default), the loopback-bootstrap
|
|
1784
|
+
// path at layer 3 accepts the connection before the cookie is ever
|
|
1785
|
+
// compared — the old cookie is harmless because no token is needed.
|
|
1786
|
+
//
|
|
1787
|
+
// This means a token rotation (e.g. the operator sets a new accessToken
|
|
1788
|
+
// in config) does NOT invalidate already-distributed cookies for loopback
|
|
1789
|
+
// clients. The mitigating factors:
|
|
1790
|
+
// - Loopback bind + requireToken=false already trusts any local process
|
|
1791
|
+
// that can reach 127.0.0.1 — a trivial perimeter.
|
|
1792
|
+
// - A LAN/0.0.0.0 bind with requireToken=true DOES enforce cookie
|
|
1793
|
+
// matching against the new expectedToken on every reconnect.
|
|
1794
|
+
// If stronger isolation is needed, set requireToken=true or rebind the
|
|
1795
|
+
// server to a non-loopback interface with token enforcement.
|
|
1796
|
+
authenticate: (ws, req) => {
|
|
1797
|
+
const allowed = verifyWsClient2({
|
|
1798
|
+
origin: req.headers.origin,
|
|
1799
|
+
url: req.url ?? "/",
|
|
1800
|
+
hostHeader: req.headers.host,
|
|
1801
|
+
remoteAddress: req.socket.remoteAddress,
|
|
1802
|
+
cookieHeader: req.headers.cookie,
|
|
1803
|
+
wsHost: deps.host,
|
|
1804
|
+
expectedToken: deps.wsToken,
|
|
1805
|
+
requireToken: deps.requireToken,
|
|
1806
|
+
allowedHostnames: deps.publicHostnames,
|
|
1807
|
+
allowBrowserUrlToken: Boolean(deps.publicWsUrl)
|
|
1808
|
+
});
|
|
1809
|
+
if (!allowed) ws.close(4003, "Forbidden");
|
|
1810
|
+
return allowed;
|
|
1811
|
+
},
|
|
1812
|
+
createClient: (ws) => ({ ws, sessionId: deps.currentSessionId() }),
|
|
1813
|
+
// Enrich parse-failure / unknown-type logs with per-connection identity
|
|
1814
|
+
// so operators can identify which client sent a malformed frame. The
|
|
1815
|
+
// `connectionId`/`sessionId` fields are read from the `client` argument
|
|
1816
|
+
// directly inside `logRejection`; the CLI's `ConnectedClient` only has
|
|
1817
|
+
// `sessionId` (no per-socket `connId`), so the verbose payload carries
|
|
1818
|
+
// `sessionId` only. Server-wide identity (`agentId`/`projectRoot`) isn't
|
|
1819
|
+
// in scope at this handler and is omitted.
|
|
1820
|
+
verboseWsLogging: true,
|
|
1821
|
+
registerClient: (ws) => {
|
|
1822
|
+
for (const handler of clientHandlers) handler.addClient(ws);
|
|
1823
|
+
},
|
|
1824
|
+
decode: (raw) => decodeProtocolFrame(raw, "client"),
|
|
1825
|
+
dispatch: deps.handleMessage,
|
|
1826
|
+
send: deps.send,
|
|
1827
|
+
sessionPayload: deps.sessionPayload,
|
|
1828
|
+
rateLimitMax: deps.rateLimitMax,
|
|
1829
|
+
// NOTE — a closed SOCKET is not a stopped run.
|
|
1830
|
+
//
|
|
1831
|
+
// This used to `abortControllers.delete(client.sessionId)` — a delete
|
|
1832
|
+
// WITHOUT an abort, and only for the tab this connection last acted on.
|
|
1833
|
+
// Both halves were wrong. The run kept going while `isRunActive` reported
|
|
1834
|
+
// `false`, so `session.delete` would happily unlink the journal a live run
|
|
1835
|
+
// was still appending to, and the next `user_message` claimed a second
|
|
1836
|
+
// lock for the same session and hit `Agent.run() is already in progress`.
|
|
1837
|
+
// The other three declared tabs were never touched at all.
|
|
1838
|
+
//
|
|
1839
|
+
// Doing nothing is the correct behaviour: a background run outlives the
|
|
1840
|
+
// tab that started it (the same invariant `retireUndisplayedSessions`
|
|
1841
|
+
// honours), and the run's own `finally` releases the lock when it unwinds.
|
|
1842
|
+
// A page that comes back is told what is still running by
|
|
1843
|
+
// `session.run_state`; a browser that never comes back drains its
|
|
1844
|
+
// unanswerable permission prompts through the last-client drain in
|
|
1845
|
+
// `connection-lifecycle.ts`, which lets a wedged run finish and release.
|
|
1846
|
+
//
|
|
1847
|
+
// So there is deliberately NO `onClose` here.
|
|
1848
|
+
buildInitialPayload: async () => {
|
|
1849
|
+
const payload = { ...await deps.buildSessionStartPayload({}, deps.needsSetup) };
|
|
1850
|
+
try {
|
|
1851
|
+
const open = deps.openSessionIds?.();
|
|
1852
|
+
if (open) payload["openSessionIds"] = [.../* @__PURE__ */ new Set([...open, deps.currentSessionId()])];
|
|
1853
|
+
} catch {
|
|
1854
|
+
console.debug("[WebUI] Failed to list open sessions");
|
|
1855
|
+
}
|
|
1856
|
+
let replayEvents;
|
|
1857
|
+
try {
|
|
1858
|
+
const replay = await deps.loadReplay?.();
|
|
1859
|
+
if (replay) {
|
|
1860
|
+
Object.assign(payload, buildReplayPayload(replay));
|
|
1861
|
+
replayEvents = replay.events;
|
|
1862
|
+
}
|
|
1863
|
+
} catch {
|
|
1864
|
+
console.debug("[WebUI] Failed to load replay");
|
|
1865
|
+
}
|
|
1866
|
+
try {
|
|
1867
|
+
const roster = deriveSessionAgents(replayEvents ?? []);
|
|
1868
|
+
const sessions = roster.length > 0 ? await deps.loadAgentSessions?.(roster.map((agent) => agent.agentId)) : [];
|
|
1869
|
+
if (sessions?.length) payload["agentSessions"] = sessions;
|
|
1870
|
+
} catch {
|
|
1871
|
+
console.debug("[WebUI] Failed to load agent sessions");
|
|
1872
|
+
}
|
|
1873
|
+
return payload;
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/webui-server-lifecycle.ts
|
|
1879
|
+
function runWebuiServerLifecycle({
|
|
1880
|
+
host,
|
|
1881
|
+
httpPort,
|
|
1882
|
+
setupEvents,
|
|
1883
|
+
opts,
|
|
1884
|
+
wsPort,
|
|
1885
|
+
accessUrl,
|
|
1886
|
+
wsToken,
|
|
1887
|
+
webuiInstanceRegistered,
|
|
1888
|
+
wss,
|
|
1889
|
+
httpServer,
|
|
1890
|
+
requireToken,
|
|
1891
|
+
publicHostnames,
|
|
1892
|
+
publicWsUrl,
|
|
1893
|
+
clients,
|
|
1894
|
+
currentSessionId,
|
|
1895
|
+
goalHandler,
|
|
1896
|
+
specsHandler,
|
|
1897
|
+
sddBoardHandler,
|
|
1898
|
+
sddWizardHandler,
|
|
1899
|
+
worktreeHandler,
|
|
1900
|
+
terminalHandler,
|
|
1901
|
+
rateLimitMax,
|
|
1902
|
+
send,
|
|
1903
|
+
sessionPayload,
|
|
1904
|
+
handleMessage,
|
|
1905
|
+
pendingConfirms,
|
|
1906
|
+
buildSessionStartPayload,
|
|
1907
|
+
sessionAgents,
|
|
1908
|
+
lifecycleState,
|
|
1909
|
+
abortControllers,
|
|
1910
|
+
flushAllStreamBuffers,
|
|
1911
|
+
eventUnsubscribers,
|
|
1912
|
+
releaseSessionSalvage,
|
|
1913
|
+
autoWakeHost,
|
|
1914
|
+
stopEmptySessionCleanup,
|
|
1915
|
+
credentialWatcherClose,
|
|
1916
|
+
kanbanRunMirror,
|
|
1917
|
+
kanbanSupervisor,
|
|
1918
|
+
stopKanbanSupervisorMemoryStats,
|
|
1919
|
+
unregisterWebuiClient,
|
|
1920
|
+
ipv6LoopbackServer,
|
|
1921
|
+
registryBaseDir,
|
|
1922
|
+
stopLiveStatusLogger,
|
|
1923
|
+
terminalLogView
|
|
1924
|
+
}) {
|
|
1925
|
+
const stopped = new Promise((resolve) => {
|
|
1926
|
+
let listeningAnnounced = false;
|
|
1927
|
+
const announceListening = () => {
|
|
1928
|
+
if (listeningAnnounced) return;
|
|
1929
|
+
listeningAnnounced = true;
|
|
1930
|
+
console.log(`[WebUI] WebSocket server running on ws://${host}:${httpPort}`);
|
|
1931
|
+
try {
|
|
1932
|
+
setupEvents();
|
|
1933
|
+
opts.onListening?.({
|
|
1934
|
+
httpPort,
|
|
1935
|
+
wsPort,
|
|
1936
|
+
host,
|
|
1937
|
+
url: accessUrl,
|
|
1938
|
+
authToken: wsToken,
|
|
1939
|
+
webuiInstanceRegistered
|
|
1940
|
+
});
|
|
1941
|
+
} catch (err) {
|
|
1942
|
+
consoleLogger.error("setup_events_failed", { message: toErrorMessage2(err) });
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
wss.on("listening", announceListening);
|
|
1946
|
+
if (httpServer?.server.listening || wss.address()) queueMicrotask(announceListening);
|
|
1947
|
+
wss.on(
|
|
1948
|
+
"connection",
|
|
1949
|
+
createConnectionHandler({
|
|
1950
|
+
host,
|
|
1951
|
+
wsToken,
|
|
1952
|
+
requireToken,
|
|
1953
|
+
publicHostnames,
|
|
1954
|
+
publicWsUrl,
|
|
1955
|
+
clients,
|
|
1956
|
+
currentSessionId,
|
|
1957
|
+
goalHandler,
|
|
1958
|
+
specsHandler,
|
|
1959
|
+
sddBoardHandler,
|
|
1960
|
+
sddWizardHandler,
|
|
1961
|
+
worktreeHandler,
|
|
1962
|
+
terminalHandler,
|
|
1963
|
+
rateLimitMax,
|
|
1964
|
+
send,
|
|
1965
|
+
sessionPayload,
|
|
1966
|
+
handleMessage,
|
|
1967
|
+
pendingConfirms,
|
|
1968
|
+
buildSessionStartPayload,
|
|
1969
|
+
loadReplay: async () => {
|
|
1970
|
+
const activeSession = opts.agent.ctx.session ?? opts.session;
|
|
1971
|
+
await activeSession.flush();
|
|
1972
|
+
if (opts.sessionStore) {
|
|
1973
|
+
const data = await opts.sessionStore.load(activeSession.id);
|
|
1974
|
+
return { messages: data.messages, events: data.events, usage: data.usage };
|
|
1975
|
+
}
|
|
1976
|
+
const usage = opts.agent.ctx.tokenCounter.total();
|
|
1977
|
+
return { messages: opts.agent.ctx.messages, usage };
|
|
1978
|
+
},
|
|
1979
|
+
loadAgentSessions: async (subagentIds) => await opts.agentTranscripts?.loadSessionsFromDisk(subagentIds) ?? [],
|
|
1980
|
+
// What this process is actually holding right now. The browser
|
|
1981
|
+
// reconciles its persisted tab strip against it, so a restarted server
|
|
1982
|
+
// is not dressed in the previous run's tabs.
|
|
1983
|
+
openSessionIds: () => sessionAgents.ids(),
|
|
1984
|
+
needsSetup: opts.needsSetup ?? false
|
|
1985
|
+
})
|
|
1986
|
+
);
|
|
1987
|
+
lifecycleState.signalShutdown = createWebuiShutdown({
|
|
1988
|
+
abortInFlight: () => {
|
|
1989
|
+
void lifecycleState.embeddedAutoHealDispose?.();
|
|
1990
|
+
for (const c of abortControllers.values()) c.abort();
|
|
1991
|
+
abortControllers.clear();
|
|
1992
|
+
},
|
|
1993
|
+
unsubscribeEvents: () => {
|
|
1994
|
+
flushAllStreamBuffers();
|
|
1995
|
+
for (const unsub of eventUnsubscribers) unsub();
|
|
1996
|
+
},
|
|
1997
|
+
stopOwnedChildren: async () => {
|
|
1998
|
+
try {
|
|
1999
|
+
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
2000
|
+
getProcessRegistry().killAll({ force: true, includeProtected: true });
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
console.debug(
|
|
2003
|
+
`[webui-server] process-registry killAll failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
if (opts.mcpRegistry) {
|
|
2007
|
+
try {
|
|
2008
|
+
await opts.mcpRegistry.stopAll();
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
console.debug(
|
|
2011
|
+
`[webui-server] mcpRegistry.stopAll failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
},
|
|
2016
|
+
disposeResources: async () => {
|
|
2017
|
+
releaseSessionSalvage();
|
|
2018
|
+
autoWakeHost?.dispose();
|
|
2019
|
+
await stopEmptySessionCleanup?.dispose();
|
|
2020
|
+
await sessionAgents.closeAll().catch(() => void 0);
|
|
2021
|
+
credentialWatcherClose?.();
|
|
2022
|
+
credentialWatcherClose = void 0;
|
|
2023
|
+
goalHandler.dispose();
|
|
2024
|
+
sddBoardHandler.dispose();
|
|
2025
|
+
worktreeHandler.dispose();
|
|
2026
|
+
terminalHandler.dispose();
|
|
2027
|
+
kanbanRunMirror?.dispose();
|
|
2028
|
+
kanbanSupervisor?.dispose();
|
|
2029
|
+
void stopKanbanSupervisorMemoryStats?.();
|
|
2030
|
+
await lifecycleState.embeddedAutoHealDispose?.();
|
|
2031
|
+
lifecycleState.embeddedAutoHealDispose = null;
|
|
2032
|
+
unregisterWebuiClient();
|
|
2033
|
+
},
|
|
2034
|
+
closeClients: () => {
|
|
2035
|
+
for (const [ws] of clients) ws.close();
|
|
2036
|
+
clients.clear();
|
|
2037
|
+
},
|
|
2038
|
+
closeHttpServer: () => {
|
|
2039
|
+
ipv6LoopbackServer?.close();
|
|
2040
|
+
httpServer?.server.close();
|
|
2041
|
+
},
|
|
2042
|
+
wss,
|
|
2043
|
+
pid: process.pid,
|
|
2044
|
+
registryBaseDir,
|
|
2045
|
+
onStopped: () => {
|
|
2046
|
+
stopLiveStatusLogger();
|
|
2047
|
+
const muted = terminalLogView.mutedCount;
|
|
2048
|
+
terminalLogView.stop();
|
|
2049
|
+
if (muted > 0) {
|
|
2050
|
+
console.log(
|
|
2051
|
+
`[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_LOGS=1 to stream them.`
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
opts.onExit?.();
|
|
2055
|
+
resolve();
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
registerWebuiSignalHandlers(lifecycleState.signalShutdown);
|
|
2059
|
+
});
|
|
2060
|
+
return { stopped };
|
|
1613
2061
|
}
|
|
1614
2062
|
|
|
1615
|
-
// src/webui-
|
|
1616
|
-
import { mailboxSessionTag as mailboxSessionTag2 } from "@wrongstack/core/coordination";
|
|
2063
|
+
// src/webui-session-maintenance.ts
|
|
1617
2064
|
import {
|
|
1618
|
-
|
|
1619
|
-
|
|
2065
|
+
collectDisplayedSessionIds,
|
|
2066
|
+
scheduleOwnerlessEmptySessionCleanup,
|
|
2067
|
+
toSessionHistoryEntries
|
|
1620
2068
|
} from "@wrongstack/webui-server";
|
|
1621
|
-
function
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
// fleet gauges froze the moment the boot tab was closed.
|
|
1635
|
-
payload: {
|
|
1636
|
-
fleetConcurrency: budget?.activeAgents ?? fleetConcurrency,
|
|
1637
|
-
fleetConcurrencyMax: budget?.maxConcurrent ?? fleetConcurrencyMax,
|
|
1638
|
-
...budget ? {
|
|
1639
|
-
maxSpawns: budget.maxSpawns,
|
|
1640
|
-
usedSpawns: budget.usedSpawns,
|
|
1641
|
-
remainingSpawns: budget.remainingSpawns,
|
|
1642
|
-
maxSpawnsSource: budget.maxSpawnsSource,
|
|
1643
|
-
maxConcurrentSource: budget.maxConcurrentSource,
|
|
1644
|
-
effectiveSource: budget.effectiveSource,
|
|
1645
|
-
checkpointMaxSpawns: budget.checkpointMaxSpawns,
|
|
1646
|
-
ceilingMismatch: budget.ceilingMismatch
|
|
1647
|
-
} : {}
|
|
1648
|
-
}
|
|
2069
|
+
function setupWebuiSessionMaintenance({
|
|
2070
|
+
opts,
|
|
2071
|
+
broadcastEveryone,
|
|
2072
|
+
getForegroundSession,
|
|
2073
|
+
clients,
|
|
2074
|
+
abortControllers
|
|
2075
|
+
}) {
|
|
2076
|
+
const refreshSessionHistory = async () => {
|
|
2077
|
+
const list = await opts.sessionStore?.list(200);
|
|
2078
|
+
if (!list) return;
|
|
2079
|
+
broadcastEveryone({
|
|
2080
|
+
type: "sessions.list",
|
|
2081
|
+
payload: { sessions: toSessionHistoryEntries(list, getForegroundSession().id) }
|
|
1649
2082
|
});
|
|
1650
2083
|
};
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
pendingConfirms: deps.pendingConfirms,
|
|
1671
|
-
globalConfigPath: deps.globalConfigPath,
|
|
1672
|
-
onFleetBroadcaster: deps.onFleetBroadcaster,
|
|
1673
|
-
projection: {
|
|
1674
|
-
scrubObject: (value) => deps.secretScrubber.scrubObject(value),
|
|
1675
|
-
queueTextDelta: deps.queueTextDelta,
|
|
1676
|
-
queueThinkingDelta: deps.queueThinkingDelta,
|
|
1677
|
-
queueToolProgress: (payload) => deps.queueToolProgress(payload),
|
|
1678
|
-
flushThinkingDelta: deps.flushThinkingDelta,
|
|
1679
|
-
flushAllStreamBuffers: deps.flushAllStreamBuffers
|
|
1680
|
-
}
|
|
1681
|
-
});
|
|
1682
|
-
deps.eventUnsubscribers.push(disposeCanonical);
|
|
1683
|
-
deps.broadcast({
|
|
1684
|
-
type: "subagent.event",
|
|
1685
|
-
payload: deps.sessionPayload({
|
|
1686
|
-
kind: "leader_updated",
|
|
1687
|
-
subagentId: `leader@${mailboxSessionTag2(deps.currentSessionId())}`,
|
|
1688
|
-
isLeader: true,
|
|
1689
|
-
name: "Leader",
|
|
1690
|
-
status: "running"
|
|
1691
|
-
})
|
|
1692
|
-
});
|
|
1693
|
-
emitConcurrency();
|
|
1694
|
-
deps.eventUnsubscribers.push(
|
|
1695
|
-
deps.events.on("concurrency.changed", (event) => {
|
|
1696
|
-
fleetConcurrencyMax = Math.max(1, event.n);
|
|
1697
|
-
emitConcurrency();
|
|
1698
|
-
}),
|
|
1699
|
-
deps.events.on("subagent.spawned", () => {
|
|
1700
|
-
fleetConcurrency += 1;
|
|
1701
|
-
emitConcurrency();
|
|
1702
|
-
}),
|
|
1703
|
-
deps.events.on("subagent.task_completed", () => {
|
|
1704
|
-
fleetConcurrency = Math.max(0, fleetConcurrency - 1);
|
|
1705
|
-
emitConcurrency();
|
|
1706
|
-
})
|
|
1707
|
-
);
|
|
1708
|
-
if (deps.subscribeEternalIteration) {
|
|
1709
|
-
const subscription = createEternalSubscription(
|
|
1710
|
-
deps.subscribeEternalIteration,
|
|
1711
|
-
(_clients, message) => deps.broadcast(message),
|
|
1712
|
-
deps.getClients
|
|
1713
|
-
);
|
|
1714
|
-
deps.eventUnsubscribers.push(() => subscription.dispose());
|
|
2084
|
+
const emptySessionCleanup = opts.sessionStore ? scheduleOwnerlessEmptySessionCleanup({
|
|
2085
|
+
getSessionStore: () => opts.sessionStore,
|
|
2086
|
+
getActiveSessionId: () => getForegroundSession().id,
|
|
2087
|
+
getActiveSessionIds: () => collectDisplayedSessionIds({ getSession: getForegroundSession, clients }),
|
|
2088
|
+
// A run outlives the tab that started it, and a turn that has not
|
|
2089
|
+
// written its first record yet looks exactly like a session nobody
|
|
2090
|
+
// ever used.
|
|
2091
|
+
hasParticipants: (sessionId) => abortControllers.has(sessionId),
|
|
2092
|
+
refreshSessions: refreshSessionHistory,
|
|
2093
|
+
logger: consoleLogger
|
|
2094
|
+
}) : null;
|
|
2095
|
+
const offSessionRenamed = opts.sessionStore ? opts.events.on("session.renamed", () => {
|
|
2096
|
+
void refreshSessionHistory().catch(() => void 0);
|
|
2097
|
+
}) : void 0;
|
|
2098
|
+
const stopEmptySessionCleanup = emptySessionCleanup && {
|
|
2099
|
+
runNow: emptySessionCleanup.runNow,
|
|
2100
|
+
dispose: async () => {
|
|
2101
|
+
offSessionRenamed?.();
|
|
2102
|
+
await emptySessionCleanup.dispose();
|
|
1715
2103
|
}
|
|
1716
2104
|
};
|
|
2105
|
+
return { stopEmptySessionCleanup };
|
|
1717
2106
|
}
|
|
1718
2107
|
|
|
1719
|
-
// src/webui-
|
|
1720
|
-
import {
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2108
|
+
// src/webui-session-retirement.ts
|
|
2109
|
+
import { resolvePendingConfirmsForSession } from "@wrongstack/webui-server";
|
|
2110
|
+
function createWebuiSessionRetirement(inputs) {
|
|
2111
|
+
const { opts, getForegroundSession, pendingConfirms, abortControllers, sessionAgents } = inputs;
|
|
2112
|
+
const retireUndisplayedSessions = (sessionIds) => {
|
|
2113
|
+
for (const sessionId of sessionIds) {
|
|
2114
|
+
if (!sessionId || sessionId === opts.session.id) continue;
|
|
2115
|
+
if (sessionId === getForegroundSession().id) continue;
|
|
2116
|
+
const orphaned = resolvePendingConfirmsForSession(pendingConfirms, sessionId);
|
|
2117
|
+
if (orphaned > 0) {
|
|
2118
|
+
console.log(
|
|
2119
|
+
JSON.stringify({
|
|
2120
|
+
level: "warn",
|
|
2121
|
+
event: "webui.confirm_orphaned_by_tab_close",
|
|
2122
|
+
sessionId,
|
|
2123
|
+
count: orphaned,
|
|
2124
|
+
message: `Denied ${orphaned} unanswerable permission prompt(s) for closed session ${sessionId}.`
|
|
2125
|
+
})
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
if (abortControllers.has(sessionId)) continue;
|
|
2129
|
+
opts.onSessionRetired?.(sessionId);
|
|
2130
|
+
const agent = sessionAgents.peek(sessionId);
|
|
2131
|
+
if (!agent || agent === opts.agent) continue;
|
|
2132
|
+
void sessionAgents.endAndClose(sessionId);
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
return { retireUndisplayedSessions };
|
|
2136
|
+
}
|
|
1730
2137
|
|
|
1731
2138
|
// src/webui-server.ts
|
|
1732
2139
|
async function runWebUI(opts) {
|
|
1733
2140
|
const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "cli-webui-trusted-host-compat-v1" });
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2141
|
+
const {
|
|
2142
|
+
host,
|
|
2143
|
+
publicUrl,
|
|
2144
|
+
publicWsUrl,
|
|
2145
|
+
requireToken,
|
|
2146
|
+
surface,
|
|
2147
|
+
requestedHttpPort,
|
|
2148
|
+
strictPort,
|
|
2149
|
+
globalRoot,
|
|
2150
|
+
profileConfigPath,
|
|
2151
|
+
profileDir,
|
|
2152
|
+
rateLimitMax,
|
|
2153
|
+
publicHostnames
|
|
2154
|
+
} = resolveWebuiHostSettings(opts);
|
|
1741
2155
|
const terminalLogView = startTerminalDashboard({
|
|
1742
2156
|
title: surface === "simpleui" ? "SimpleUI" : "WebUI",
|
|
1743
|
-
quiet: !
|
|
2157
|
+
quiet: !envFlag2("WEBUI_LOGS"),
|
|
1744
2158
|
// The dashboard is operator-owned terminal output, so retain the full
|
|
1745
2159
|
// authenticated access URL here even though startup logs remain redacted.
|
|
1746
2160
|
getUrl: () => accessUrl
|
|
1747
2161
|
});
|
|
1748
|
-
const strictPort = opts.strictPort ?? isStrictPort();
|
|
1749
2162
|
let httpPort = requestedHttpPort;
|
|
1750
2163
|
if (!strictPort) {
|
|
1751
2164
|
httpPort = await findFreePort(host, requestedHttpPort);
|
|
1752
2165
|
}
|
|
1753
2166
|
let wsPort = httpPort;
|
|
1754
|
-
const globalRoot = opts.globalConfigPath ? path7.dirname(opts.globalConfigPath) : wstackGlobalRoot();
|
|
1755
|
-
const profileConfigPath = opts.profileConfigPath ?? opts.globalConfigPath ?? path7.join(globalRoot, "config.json");
|
|
1756
|
-
const rawRateLimit = process.env["WEBUI_RATE_LIMIT"]?.trim() ?? "";
|
|
1757
|
-
const parsedRateLimit = /^\d+$/.test(rawRateLimit) ? Number(rawRateLimit) : Number.NaN;
|
|
1758
|
-
const rateLimitMax = Number.isSafeInteger(parsedRateLimit) && parsedRateLimit >= 0 ? parsedRateLimit : 600;
|
|
1759
2167
|
const clients = /* @__PURE__ */ new Map();
|
|
2168
|
+
const { send, broadcast, broadcastEveryone, sendResult } = createWebuiTransport(clients);
|
|
1760
2169
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
1761
2170
|
const secretScrubber = new DefaultSecretScrubber();
|
|
1762
2171
|
const abortControllers = /* @__PURE__ */ new Map();
|
|
1763
|
-
const profileDir = path7.dirname(profileConfigPath);
|
|
1764
2172
|
let customModeStoreP = null;
|
|
1765
2173
|
const getCustomModeStore = () => {
|
|
1766
|
-
customModeStoreP ??= (async () => {
|
|
1767
|
-
const store = createCustomModeStore(profileDir);
|
|
1768
|
-
await store.load();
|
|
1769
|
-
return store;
|
|
1770
|
-
})();
|
|
1771
|
-
return customModeStoreP;
|
|
1772
|
-
};
|
|
1773
|
-
const kanbanRunMirror = opts
|
|
1774
|
-
|
|
1775
|
-
events: opts.events,
|
|
1776
|
-
broadcast,
|
|
1777
|
-
log: (m) => consoleLogger.info(m)
|
|
1778
|
-
}) : null;
|
|
1779
|
-
const kanbanSupervisor = opts.projectRoot ? createKanbanSupervisor({
|
|
1780
|
-
projectRoot: opts.projectRoot,
|
|
1781
|
-
broadcast,
|
|
1782
|
-
...opts.onKanbanDispatch ? { dispatchTask: opts.onKanbanDispatch } : {},
|
|
1783
|
-
log: (message) => consoleLogger.info(message)
|
|
1784
|
-
}) : null;
|
|
1785
|
-
const stopKanbanSupervisorMemoryStats = kanbanSupervisor ? startSharedHeapWatchdog({
|
|
1786
|
-
collectStats: () => {
|
|
1787
|
-
const stats = kanbanSupervisor.getStats();
|
|
1788
|
-
return {
|
|
1789
|
-
kanbanSupervisorSnapshots: stats.snapshots,
|
|
1790
|
-
kanbanSupervisorScheduledBoards: stats.scheduledBoards,
|
|
1791
|
-
kanbanSupervisorAgentCooldowns: stats.agentCooldowns,
|
|
1792
|
-
kanbanSupervisorRunningAgents: stats.runningAgents
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
}) : void 0;
|
|
2174
|
+
customModeStoreP ??= (async () => {
|
|
2175
|
+
const store = createCustomModeStore(profileDir);
|
|
2176
|
+
await store.load();
|
|
2177
|
+
return store;
|
|
2178
|
+
})();
|
|
2179
|
+
return customModeStoreP;
|
|
2180
|
+
};
|
|
2181
|
+
const { kanbanRunMirror, kanbanSupervisor, stopKanbanSupervisorMemoryStats } = createWebuiKanbanServices(opts, broadcast);
|
|
2182
|
+
const domainHandlers = createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror);
|
|
1796
2183
|
const {
|
|
1797
2184
|
goalHandler,
|
|
1798
2185
|
worktreeHandler,
|
|
@@ -1800,7 +2187,7 @@ async function runWebUI(opts) {
|
|
|
1800
2187
|
specsHandler,
|
|
1801
2188
|
sddBoardHandler,
|
|
1802
2189
|
sddWizardHandler
|
|
1803
|
-
} =
|
|
2190
|
+
} = domainHandlers;
|
|
1804
2191
|
await seedConfigToMeta(opts);
|
|
1805
2192
|
if (typeof opts.agent.ctx?.meta?.["yolo"] === "boolean") {
|
|
1806
2193
|
opts.onYoloSwitch?.(opts.agent.ctx.meta["yolo"]);
|
|
@@ -1808,37 +2195,13 @@ async function runWebUI(opts) {
|
|
|
1808
2195
|
const { prefSnapshot, persistPrefs } = createPrefsSeeding(opts);
|
|
1809
2196
|
const sessionStartedAt = Date.now();
|
|
1810
2197
|
let sessionAgentsRef;
|
|
1811
|
-
const buildSessionStartPayload =
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
// this very object. A spread copy froze both at boot, so every
|
|
1816
|
-
// `session.start` broadcast after a project switch still announced the
|
|
1817
|
-
// previous project's root — the switch looked like it had not happened.
|
|
1818
|
-
get projectRoot() {
|
|
1819
|
-
return opts.projectRoot;
|
|
1820
|
-
},
|
|
1821
|
-
get session() {
|
|
1822
|
-
return opts.session;
|
|
1823
|
-
},
|
|
1824
|
-
get appConfig() {
|
|
1825
|
-
return opts.appConfig;
|
|
1826
|
-
},
|
|
1827
|
-
// `peek`, never `get`: building a payload must not materialise an agent
|
|
1828
|
-
// for a session id that arrived from a stale browser tab.
|
|
1829
|
-
getSessionContext: (sessionId) => sessionAgentsRef?.peek(sessionId)?.ctx
|
|
1830
|
-
});
|
|
2198
|
+
const buildSessionStartPayload = createLiveSessionStartPayloadBuilder(
|
|
2199
|
+
opts,
|
|
2200
|
+
() => sessionAgentsRef
|
|
2201
|
+
);
|
|
1831
2202
|
const { register: registerWebuiClient, unregister: unregisterWebuiClient } = createEmbeddedClientRegistration(opts, clients, abortControllers, () => sessionAgentsRef);
|
|
1832
2203
|
registerWebuiClient();
|
|
1833
2204
|
const wsToken = resolveAuthToken(opts.accessToken);
|
|
1834
|
-
const publicHostnames = [publicUrl, publicWsUrl].map((value) => {
|
|
1835
|
-
if (!value) return void 0;
|
|
1836
|
-
try {
|
|
1837
|
-
return new URL(value).hostname;
|
|
1838
|
-
} catch {
|
|
1839
|
-
return void 0;
|
|
1840
|
-
}
|
|
1841
|
-
}).filter((value) => Boolean(value));
|
|
1842
2205
|
let accessUrl = buildWebUIAccessUrl({
|
|
1843
2206
|
host,
|
|
1844
2207
|
port: httpPort,
|
|
@@ -1846,147 +2209,41 @@ async function runWebUI(opts) {
|
|
|
1846
2209
|
publicUrl
|
|
1847
2210
|
});
|
|
1848
2211
|
let fleetBroadcastCli = null;
|
|
1849
|
-
const httpServer = await
|
|
1850
|
-
|
|
2212
|
+
const { httpServer, wss, ipv6LoopbackServer } = await startWebuiHttpBridge({
|
|
2213
|
+
opts,
|
|
2214
|
+
surface,
|
|
1851
2215
|
host,
|
|
1852
2216
|
httpPort,
|
|
2217
|
+
strictPort,
|
|
1853
2218
|
globalRoot,
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
resolvePackageJson: (id) => {
|
|
1857
|
-
const packageJson = findInstalledPackageJson(id, import.meta.url);
|
|
1858
|
-
if (!packageJson) throw new Error(`Package not found: ${id}`);
|
|
1859
|
-
return packageJson;
|
|
1860
|
-
}
|
|
1861
|
-
},
|
|
1862
|
-
onFleetPing: () => {
|
|
1863
|
-
void fleetBroadcastCli?.();
|
|
1864
|
-
},
|
|
1865
|
-
// Without this, /debug/watcher-metrics 503s and the Debug Dashboard has to
|
|
1866
|
-
// report the watcher as "Unavailable". The CLI host runs no status watcher
|
|
1867
|
-
// of its own, so this object stays all-zero and watcherActive reads false —
|
|
1868
|
-
// which is the truthful state for an embedded host.
|
|
1869
|
-
watcherMetrics: createDefaultFileWatcherMetrics(),
|
|
1870
|
-
onTechStackEvent: (event) => broadcast(event),
|
|
1871
|
-
getLlm: () => opts.agent.ctx.provider && opts.agent.ctx.model ? { provider: opts.agent.ctx.provider, model: opts.agent.ctx.model } : void 0,
|
|
1872
|
-
projectRoot: opts.projectRoot,
|
|
2219
|
+
wsToken,
|
|
2220
|
+
publicUrl,
|
|
1873
2221
|
publicWsUrl,
|
|
1874
|
-
apiToken: wsToken,
|
|
1875
2222
|
requireToken,
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
// changes made after boot, just like the standalone WebUI host.
|
|
1880
|
-
getIntegrationTarget: (kind) => {
|
|
1881
|
-
const meta = opts.agent.ctx.meta;
|
|
1882
|
-
const enabledKey = kind === "hq" ? "hqEnabled" : "wrongProxyEnabled";
|
|
1883
|
-
const urlKey = kind === "hq" ? "hqUrl" : "wrongProxyUrl";
|
|
1884
|
-
return meta[enabledKey] === true && typeof meta[urlKey] === "string" ? meta[urlKey] : void 0;
|
|
2223
|
+
getSessionAgents: () => sessionAgentsRef,
|
|
2224
|
+
onFleetPing: () => {
|
|
2225
|
+
void fleetBroadcastCli?.();
|
|
1885
2226
|
},
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
origin: info.origin,
|
|
1891
|
-
url: info.req.url ?? "",
|
|
1892
|
-
hostHeader: info.req.headers.host,
|
|
1893
|
-
remoteAddress: info.req.socket.remoteAddress,
|
|
1894
|
-
cookieHeader: info.req.headers.cookie,
|
|
1895
|
-
wsHost: host,
|
|
1896
|
-
expectedToken: wsToken,
|
|
1897
|
-
requireToken,
|
|
1898
|
-
allowedHostnames: [publicUrl, publicWsUrl].filter((value) => Boolean(value)),
|
|
1899
|
-
allowBrowserUrlToken: Boolean(publicWsUrl),
|
|
1900
|
-
allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
|
|
1901
|
-
});
|
|
1902
|
-
const wss = httpServer ? new WebSocketServer({ server: httpServer.server, verifyClient, maxPayload: 20 * 1024 * 1024 }) : new WebSocketServer({ port: httpPort, host, verifyClient, maxPayload: 20 * 1024 * 1024 });
|
|
1903
|
-
wss.on("error", (err) => {
|
|
1904
|
-
console.error(
|
|
1905
|
-
JSON.stringify({
|
|
1906
|
-
level: "error",
|
|
1907
|
-
event: "webui_server.error",
|
|
1908
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1909
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1910
|
-
})
|
|
1911
|
-
);
|
|
1912
|
-
});
|
|
1913
|
-
if (httpServer) {
|
|
1914
|
-
const boundPort = surface === "simpleui" ? await startDeferredHttpListen({
|
|
1915
|
-
server: httpServer.server,
|
|
1916
|
-
host,
|
|
1917
|
-
httpPort,
|
|
1918
|
-
logger: consoleLogger,
|
|
1919
|
-
strictPort
|
|
1920
|
-
}) : httpServer.port;
|
|
1921
|
-
if (boundPort !== httpPort) {
|
|
1922
|
-
httpPort = boundPort;
|
|
1923
|
-
wsPort = boundPort;
|
|
2227
|
+
broadcast,
|
|
2228
|
+
onPortRebound: (port) => {
|
|
2229
|
+
httpPort = port;
|
|
2230
|
+
wsPort = port;
|
|
1924
2231
|
accessUrl = buildWebUIAccessUrl({ host, port: httpPort, token: wsToken, publicUrl });
|
|
1925
2232
|
}
|
|
1926
|
-
}
|
|
1927
|
-
let ipv6LoopbackServer = null;
|
|
1928
|
-
if (httpServer && host === "127.0.0.1") {
|
|
1929
|
-
ipv6LoopbackServer = await startIpv6LoopbackProxy({
|
|
1930
|
-
primary: httpServer.server,
|
|
1931
|
-
httpPort,
|
|
1932
|
-
logger: consoleLogger
|
|
1933
|
-
});
|
|
1934
|
-
}
|
|
1935
|
-
console.log(`[WebUI] WebSocket server starting on ws://${host}:${httpPort}`);
|
|
1936
|
-
if (httpServer) {
|
|
1937
|
-
announceWebuiReady({
|
|
1938
|
-
surface,
|
|
1939
|
-
server: httpServer.server,
|
|
1940
|
-
host,
|
|
1941
|
-
httpPort,
|
|
1942
|
-
open: !!opts.open,
|
|
1943
|
-
wsToken,
|
|
1944
|
-
publicUrl
|
|
1945
|
-
});
|
|
1946
|
-
} else {
|
|
1947
|
-
console.warn(
|
|
1948
|
-
`[WebUI] Frontend not served (run \`pnpm --filter @wrongstack/webui build\`). WS bridge still active on ws://${host}:${httpPort}.`
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
2233
|
+
});
|
|
1951
2234
|
let foregroundSession = opts.agent.ctx.session ?? opts.session;
|
|
1952
2235
|
const currentSessionId = () => foregroundSession.id;
|
|
1953
2236
|
const registryBaseDir = globalRoot;
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1965
|
-
registryBaseDir,
|
|
1966
|
-
authToken: wsToken,
|
|
1967
|
-
...opts.webuiSessionChild ? {
|
|
1968
|
-
role: "session-child",
|
|
1969
|
-
sessionId: currentSessionId(),
|
|
1970
|
-
parentPid: opts.webuiSessionChild.parentPid,
|
|
1971
|
-
parentShellId: opts.webuiSessionChild.parentShellId,
|
|
1972
|
-
runtimeId: opts.webuiSessionChild.runtimeId,
|
|
1973
|
-
attachable: opts.webuiSessionChild.attachable,
|
|
1974
|
-
lastReadyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1975
|
-
protocolVersion: opts.webuiSessionChild.protocolVersion,
|
|
1976
|
-
capabilities: [...WEBUI_SESSION_CHILD_CAPABILITIES]
|
|
1977
|
-
} : {}
|
|
1978
|
-
})
|
|
1979
|
-
).then(
|
|
1980
|
-
(value) => value !== false,
|
|
1981
|
-
() => false
|
|
1982
|
-
);
|
|
1983
|
-
if (opts.webuiSessionChild) {
|
|
1984
|
-
webuiInstanceRegistered = await registration;
|
|
1985
|
-
} else {
|
|
1986
|
-
void registration;
|
|
1987
|
-
webuiInstanceRegistered = true;
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
2237
|
+
const webuiInstanceRegistered = await registerEmbeddedWebuiInstance({
|
|
2238
|
+
opts,
|
|
2239
|
+
surface,
|
|
2240
|
+
host,
|
|
2241
|
+
httpPort,
|
|
2242
|
+
publicUrl,
|
|
2243
|
+
registryBaseDir,
|
|
2244
|
+
wsToken,
|
|
2245
|
+
currentSessionId
|
|
2246
|
+
});
|
|
1990
2247
|
const eventUnsubscribers = [];
|
|
1991
2248
|
const sessionPayload = (payload) => {
|
|
1992
2249
|
const provided = payload["sessionId"];
|
|
@@ -2017,54 +2274,24 @@ async function runWebUI(opts) {
|
|
|
2017
2274
|
secretScrubber,
|
|
2018
2275
|
getClients: () => clients,
|
|
2019
2276
|
eventUnsubscribers,
|
|
2020
|
-
globalConfigPath:
|
|
2277
|
+
globalConfigPath: path8.join(globalRoot, "config.json"),
|
|
2021
2278
|
onFleetBroadcaster: (fn) => {
|
|
2022
2279
|
fleetBroadcastCli = fn;
|
|
2023
2280
|
},
|
|
2024
2281
|
...opts.getFleetBudget ? { getFleetBudget: opts.getFleetBudget } : {}
|
|
2025
2282
|
});
|
|
2026
|
-
const wsHandlerCtx = {
|
|
2027
|
-
providerStore: createProviderConfigStore(
|
|
2028
|
-
profileConfigPath,
|
|
2029
|
-
() => opts.appConfig?.providers ?? {}
|
|
2030
|
-
),
|
|
2031
|
-
modelsRegistry: opts.modelsRegistry,
|
|
2032
|
-
providerAuthRegistry: opts.providerAuthRegistry,
|
|
2033
|
-
getDisabledModels: () => opts.appConfig?.disabledModels ?? [],
|
|
2034
|
-
getDisabledProviders: () => opts.appConfig?.disabledProviders ?? [],
|
|
2035
|
-
send,
|
|
2036
|
-
broadcast,
|
|
2037
|
-
log: (m) => console.log(m)
|
|
2038
|
-
};
|
|
2039
|
-
const embeddedProviderOperations = createEmbeddedProviderOperations(wsHandlerCtx);
|
|
2040
|
-
const credentialWatcherClose = startWebuiCredentialWatcher({
|
|
2283
|
+
const { wsHandlerCtx, credentialWatcherClose } = createEmbeddedProviderPlane({
|
|
2041
2284
|
opts,
|
|
2042
2285
|
profileConfigPath,
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
});
|
|
2046
|
-
const sessionAgents = createSessionAgentRegistry({
|
|
2047
|
-
template: opts.agent,
|
|
2048
|
-
...opts.modelsRegistry ? { modelsRegistry: opts.modelsRegistry } : {},
|
|
2049
|
-
isRunActive: (sessionId) => abortControllers.has(sessionId),
|
|
2050
|
-
// A tab that is still on screen must outlive one that was closed, whatever
|
|
2051
|
-
// order their agents were created in.
|
|
2052
|
-
isDisplayed: (sessionId) => {
|
|
2053
|
-
for (const client of clients.values()) {
|
|
2054
|
-
if (client.sessionId === sessionId) return true;
|
|
2055
|
-
if (client.sessionIds?.has(sessionId) === true) return true;
|
|
2056
|
-
}
|
|
2057
|
-
return false;
|
|
2058
|
-
}
|
|
2286
|
+
send,
|
|
2287
|
+
broadcast
|
|
2059
2288
|
});
|
|
2289
|
+
const { sessionAgents, isSessionDisplayed } = createEmbeddedSessionAgents(
|
|
2290
|
+
opts,
|
|
2291
|
+
clients,
|
|
2292
|
+
abortControllers
|
|
2293
|
+
);
|
|
2060
2294
|
sessionAgentsRef = sessionAgents;
|
|
2061
|
-
const isSessionDisplayed = (sessionId) => {
|
|
2062
|
-
for (const client of clients.values()) {
|
|
2063
|
-
if (client.sessionId === sessionId) return true;
|
|
2064
|
-
if (client.sessionIds?.has(sessionId) === true) return true;
|
|
2065
|
-
}
|
|
2066
|
-
return false;
|
|
2067
|
-
};
|
|
2068
2295
|
const autoWakeHost = opts.leaderAutoWake ? createWebuiLeaderAutoWakeHost({
|
|
2069
2296
|
controller: opts.leaderAutoWake,
|
|
2070
2297
|
events: opts.events,
|
|
@@ -2095,35 +2322,13 @@ async function runWebUI(opts) {
|
|
|
2095
2322
|
abortControllers,
|
|
2096
2323
|
sessionAgents
|
|
2097
2324
|
});
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
};
|
|
2106
|
-
const emptySessionCleanup = opts.sessionStore ? scheduleOwnerlessEmptySessionCleanup({
|
|
2107
|
-
getSessionStore: () => opts.sessionStore,
|
|
2108
|
-
getActiveSessionId: () => foregroundSession.id,
|
|
2109
|
-
getActiveSessionIds: () => collectDisplayedSessionIds({ getSession: () => foregroundSession, clients }),
|
|
2110
|
-
// A run outlives the tab that started it, and a turn that has not
|
|
2111
|
-
// written its first record yet looks exactly like a session nobody
|
|
2112
|
-
// ever used.
|
|
2113
|
-
hasParticipants: (sessionId) => abortControllers.has(sessionId),
|
|
2114
|
-
refreshSessions: refreshSessionHistory,
|
|
2115
|
-
logger: consoleLogger
|
|
2116
|
-
}) : null;
|
|
2117
|
-
const offSessionRenamed = opts.sessionStore ? opts.events.on("session.renamed", () => {
|
|
2118
|
-
void refreshSessionHistory().catch(() => void 0);
|
|
2119
|
-
}) : void 0;
|
|
2120
|
-
const stopEmptySessionCleanup = emptySessionCleanup && {
|
|
2121
|
-
runNow: emptySessionCleanup.runNow,
|
|
2122
|
-
dispose: async () => {
|
|
2123
|
-
offSessionRenamed?.();
|
|
2124
|
-
await emptySessionCleanup.dispose();
|
|
2125
|
-
}
|
|
2126
|
-
};
|
|
2325
|
+
const { stopEmptySessionCleanup } = setupWebuiSessionMaintenance({
|
|
2326
|
+
opts,
|
|
2327
|
+
broadcastEveryone,
|
|
2328
|
+
getForegroundSession: () => foregroundSession,
|
|
2329
|
+
clients,
|
|
2330
|
+
abortControllers
|
|
2331
|
+
});
|
|
2127
2332
|
const routeContexts = createWebuiRouteContexts({
|
|
2128
2333
|
opts,
|
|
2129
2334
|
profileConfigPath,
|
|
@@ -2163,43 +2368,21 @@ async function runWebUI(opts) {
|
|
|
2163
2368
|
let signalShutdown;
|
|
2164
2369
|
const shutdown = () => signalShutdown?.();
|
|
2165
2370
|
let embeddedAutoHealDispose = null;
|
|
2166
|
-
const handleMessage =
|
|
2167
|
-
jevVault: getVault(opts.globalConfigPath ?? opts.profileConfigPath),
|
|
2371
|
+
const handleMessage = createCliEmbeddedMessageRouter({
|
|
2168
2372
|
trustBoundary,
|
|
2169
2373
|
opts,
|
|
2170
|
-
logger: consoleLogger,
|
|
2171
2374
|
send,
|
|
2172
2375
|
sendResult,
|
|
2173
2376
|
sessionPayload,
|
|
2174
2377
|
currentSessionId,
|
|
2175
2378
|
shutdown,
|
|
2176
|
-
// Auto-heal watchdog disposer — `disposeResources` awaits it (bounded) so
|
|
2177
|
-
// an in-flight daemon restart drains before the host exits.
|
|
2178
2379
|
onDispose: (dispose) => {
|
|
2179
2380
|
embeddedAutoHealDispose = dispose;
|
|
2180
2381
|
},
|
|
2181
2382
|
providerCtx: wsHandlerCtx,
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
promptsCtx: routeContexts.promptsCtx,
|
|
2186
|
-
designCtx: routeContexts.designCtx,
|
|
2187
|
-
agentConfigCtx: routeContexts.agentConfigCtx,
|
|
2188
|
-
prefsCtx: routeContexts.prefsCtx,
|
|
2189
|
-
projectCtx: routeContexts.projectsCtx,
|
|
2190
|
-
mailboxRoutes: routeContexts.mailboxRoutes,
|
|
2191
|
-
chimeraRoutes: routeContexts.chimeraRoutes,
|
|
2192
|
-
codeAssistRoutes: routeContexts.codeAssistRoutes,
|
|
2193
|
-
sessionCtx: routeContexts.sessionsCtx,
|
|
2194
|
-
conversationCtx: routeContexts.connectionCtx,
|
|
2195
|
-
goalHandler,
|
|
2196
|
-
specsHandler,
|
|
2197
|
-
sddBoardHandler,
|
|
2198
|
-
sddWizardHandler,
|
|
2199
|
-
worktreeHandler,
|
|
2200
|
-
terminalHandler,
|
|
2201
|
-
kanbanHostRoutes,
|
|
2202
|
-
statusTracker: opts.statusTracker
|
|
2383
|
+
routeContexts,
|
|
2384
|
+
domainHandlers,
|
|
2385
|
+
kanbanHostRoutes
|
|
2203
2386
|
});
|
|
2204
2387
|
const { stopped } = runWebuiServerLifecycle({
|
|
2205
2388
|
host,
|
|
@@ -2260,27 +2443,9 @@ async function runWebUI(opts) {
|
|
|
2260
2443
|
stopLiveStatusLogger,
|
|
2261
2444
|
terminalLogView
|
|
2262
2445
|
});
|
|
2263
|
-
function send(ws, msg) {
|
|
2264
|
-
sendSerialized(ws, JSON.stringify(stampDispatchSession(msg)));
|
|
2265
|
-
}
|
|
2266
|
-
function broadcast(msg, targetSessionId) {
|
|
2267
|
-
const payload = msg.payload;
|
|
2268
|
-
const sessionId = targetSessionId ?? (payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0);
|
|
2269
|
-
const data = sessionId ? webuiSessionFrameLog().sequence(sessionId, msg) : JSON.stringify(msg);
|
|
2270
|
-
for (const [ws, client] of clients) {
|
|
2271
|
-
if (clientWantsSession(client, sessionId)) sendSerialized(ws, data);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
function broadcastEveryone(msg) {
|
|
2275
|
-
const data = JSON.stringify(msg);
|
|
2276
|
-
for (const [ws] of clients) sendSerialized(ws, data);
|
|
2277
|
-
}
|
|
2278
|
-
function sendResult(ws, success, message) {
|
|
2279
|
-
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
2280
|
-
}
|
|
2281
2446
|
return stopped.finally(() => terminalLogView.stop());
|
|
2282
2447
|
}
|
|
2283
2448
|
export {
|
|
2284
2449
|
runWebUI
|
|
2285
2450
|
};
|
|
2286
|
-
//# sourceMappingURL=webui-server-
|
|
2451
|
+
//# sourceMappingURL=webui-server-6IC3SLZG.js.map
|