@wrongstack/webui-server 0.300.0 → 0.301.0

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.
@@ -52,6 +52,7 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
52
52
  "chime",
53
53
  "confirmExit",
54
54
  "nextPrediction",
55
+ "nextStepsTool",
55
56
  "titleAnimation",
56
57
  "enhanceEnabled",
57
58
  "featureMcp",
@@ -175,6 +176,7 @@ var ENUM_PREF_KEYS = {
175
176
  fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"]),
176
177
  // Chimera autoFix + auto-review cascade threshold
177
178
  chimeraAutoFix: /* @__PURE__ */ new Set(["off", "ask", "auto"]),
179
+ autoReviewModelSelection: /* @__PURE__ */ new Set(["round-robin", "random"]),
178
180
  autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
179
181
  fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"]),
180
182
  showAgentSwarmPanel: /* @__PURE__ */ new Set(["bottom", "sidebar", "off"])
@@ -4230,6 +4232,9 @@ async function handleConversationRoute(ws, msg, handlers) {
4230
4232
  case "user_message":
4231
4233
  await handlers.userMessage(ws, msg);
4232
4234
  return true;
4235
+ case "topic.advice":
4236
+ await handlers.topicAdvice(ws, msg);
4237
+ return true;
4233
4238
  case "abort":
4234
4239
  await handlers.abort(ws, msg);
4235
4240
  return true;
@@ -4245,6 +4250,7 @@ async function handleConversationRoute(ws, msg, handlers) {
4245
4250
  }
4246
4251
 
4247
4252
  // src/server/conversation-operations.ts
4253
+ import { startFreshTopicContext, TopicShiftAdvisor } from "@wrongstack/core/execution";
4248
4254
  import {
4249
4255
  buildUserContentBlocks,
4250
4256
  IncomingImageError,
@@ -4261,6 +4267,7 @@ function requestedSessionId(msg) {
4261
4267
  return payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0;
4262
4268
  }
4263
4269
  function createConversationOperations(ctx) {
4270
+ const topicShiftAdvisor = new TopicShiftAdvisor();
4264
4271
  const sessionPayload2 = (payload) => {
4265
4272
  const provided = payload["sessionId"];
4266
4273
  const sessionId = typeof provided === "string" && provided.length > 0 ? provided : ctx.getSessionId();
@@ -4281,6 +4288,38 @@ function createConversationOperations(ctx) {
4281
4288
  return false;
4282
4289
  };
4283
4290
  return {
4291
+ topicAdvice: async (ws, msg) => {
4292
+ if (!ensureCurrentSession(ws, msg, "topic.advice")) return;
4293
+ const payload = msg.payload ?? {};
4294
+ if (typeof payload.requestId !== "string" || typeof payload.prompt !== "string") {
4295
+ ctx.send(ws, {
4296
+ type: "topic.advice_result",
4297
+ payload: sessionPayload2({
4298
+ requestId: typeof payload.requestId === "string" ? payload.requestId : "",
4299
+ suggestNewContext: false,
4300
+ confidence: 0,
4301
+ reason: "Invalid topic advice request.",
4302
+ source: "local"
4303
+ })
4304
+ });
4305
+ return;
4306
+ }
4307
+ const agent = ctx.getAgent();
4308
+ const configuredMax = agent.ctx.meta["effectiveMaxContext"];
4309
+ const maxContext = typeof configuredMax === "number" ? configuredMax : agent.ctx.provider.capabilities.maxContext;
4310
+ const advice = await topicShiftAdvisor.advise({
4311
+ prompt: payload.prompt,
4312
+ messages: agent.ctx.messages,
4313
+ provider: agent.ctx.provider,
4314
+ model: agent.ctx.model,
4315
+ contextTokens: agent.ctx.lastRequestTokens,
4316
+ maxContext
4317
+ });
4318
+ ctx.send(ws, {
4319
+ type: "topic.advice_result",
4320
+ payload: sessionPayload2({ requestId: payload.requestId, ...advice })
4321
+ });
4322
+ },
4284
4323
  userMessage: async (ws, msg) => {
4285
4324
  if (!ensureCurrentSession(ws, msg, "user_message")) return;
4286
4325
  const payload = msg.payload ?? {};
@@ -4298,6 +4337,7 @@ function createConversationOperations(ctx) {
4298
4337
  const originSessionId = ctx.getSessionId();
4299
4338
  try {
4300
4339
  const agent = ctx.getAgent();
4340
+ if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
4301
4341
  const content = typeof payload.content === "string" ? payload.content : "";
4302
4342
  let input = content;
4303
4343
  const imageBlocks = parseIncomingImages(payload.images, payload.imageBase64);
@@ -5425,6 +5465,40 @@ import {
5425
5465
  getKanbanServerConnection,
5426
5466
  isKanbanServerAvailable
5427
5467
  } from "@wrongstack/kanban";
5468
+ import * as net from "node:net";
5469
+
5470
+ // src/server/privileged-actions.ts
5471
+ import { randomUUID as randomUUID2 } from "node:crypto";
5472
+ import {
5473
+ isTrustDecisionAllowed
5474
+ } from "@wrongstack/core/security";
5475
+ async function authorizeWebUIAction(boundary, action, logger) {
5476
+ const request = {
5477
+ version: 1,
5478
+ requestId: randomUUID2(),
5479
+ actor: {
5480
+ kind: "remote-client",
5481
+ ...action.sessionId ? { sessionId: action.sessionId } : {}
5482
+ },
5483
+ surface: "webui",
5484
+ capability: action.capability,
5485
+ subject: action.subject,
5486
+ risk: action.risk,
5487
+ scope: {
5488
+ ...action.cwd ? { cwd: action.cwd } : {},
5489
+ ...action.sessionId ? { sessionId: action.sessionId } : {}
5490
+ },
5491
+ authContext: { method: "session" },
5492
+ ...action.metadata ? { metadata: action.metadata } : {}
5493
+ };
5494
+ const decision = await boundary.evaluate(request);
5495
+ logger?.debug?.(
5496
+ `[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
5497
+ );
5498
+ return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
5499
+ }
5500
+
5501
+ // src/server/connections-health-route.ts
5428
5502
  import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
5429
5503
  import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
5430
5504
  import {
@@ -5800,6 +5874,544 @@ async function governanceHealth(projectRoot) {
5800
5874
  }
5801
5875
  };
5802
5876
  }
5877
+ async function handleConnectionsServiceAction(ws, message, context) {
5878
+ if (message.type !== "connections.service_action") return false;
5879
+ const payload = message.payload;
5880
+ const serviceId = payload?.serviceId;
5881
+ const rawAction = payload?.action ?? "shutdown";
5882
+ if (!serviceId) {
5883
+ context.send(ws, {
5884
+ type: "connections.service_action_result",
5885
+ payload: {
5886
+ serviceId: null,
5887
+ action: rawAction,
5888
+ success: false,
5889
+ message: "Missing serviceId in payload"
5890
+ }
5891
+ });
5892
+ return true;
5893
+ }
5894
+ if (rawAction !== "shutdown" && rawAction !== "restart") {
5895
+ context.send(ws, {
5896
+ type: "connections.service_action_result",
5897
+ payload: {
5898
+ serviceId,
5899
+ action: rawAction,
5900
+ success: false,
5901
+ message: `Unsupported action "${rawAction}" \u2014 only "shutdown" and "restart" are supported`
5902
+ }
5903
+ });
5904
+ return true;
5905
+ }
5906
+ const action = rawAction;
5907
+ if (!context.trustBoundary) {
5908
+ context.send(ws, {
5909
+ type: "connections.service_action_result",
5910
+ payload: {
5911
+ serviceId,
5912
+ action,
5913
+ success: false,
5914
+ message: "Service control is unavailable: no policy authority is configured."
5915
+ }
5916
+ });
5917
+ return true;
5918
+ }
5919
+ const projectRootForAuth = context.getProjectRoot();
5920
+ const authorization = await authorizeWebUIAction(
5921
+ context.trustBoundary,
5922
+ {
5923
+ capability: `connections.service.${action}`,
5924
+ subject: { kind: "process", id: `${serviceId}@${projectRootForAuth}` },
5925
+ risk: "elevated",
5926
+ cwd: projectRootForAuth,
5927
+ metadata: { transport: "websocket", serviceId, action }
5928
+ },
5929
+ context.logger
5930
+ );
5931
+ if (!authorization.allowed) {
5932
+ context.send(ws, {
5933
+ type: "connections.service_action_result",
5934
+ payload: {
5935
+ serviceId,
5936
+ action,
5937
+ success: false,
5938
+ message: authorization.reason ?? "Refused by policy."
5939
+ }
5940
+ });
5941
+ return true;
5942
+ }
5943
+ if (serviceId === "webui") {
5944
+ context.send(ws, {
5945
+ type: "connections.service_action_result",
5946
+ payload: {
5947
+ serviceId: "webui",
5948
+ action,
5949
+ success: false,
5950
+ message: action === "restart" ? "Cannot restart the WebUI transport itself" : "Cannot shut down the WebUI transport itself"
5951
+ }
5952
+ });
5953
+ return true;
5954
+ }
5955
+ try {
5956
+ const result = await executeServiceAction(
5957
+ serviceId,
5958
+ action,
5959
+ context.getProjectRoot(),
5960
+ context.getIndexDir()
5961
+ );
5962
+ context.send(ws, {
5963
+ type: "connections.service_action_result",
5964
+ payload: result
5965
+ });
5966
+ } catch (error2) {
5967
+ context.send(ws, {
5968
+ type: "connections.service_action_result",
5969
+ payload: {
5970
+ serviceId,
5971
+ action,
5972
+ success: false,
5973
+ message: error2 instanceof Error ? error2.message : String(error2)
5974
+ }
5975
+ });
5976
+ }
5977
+ return true;
5978
+ }
5979
+ async function executeServiceAction(serviceId, action, projectRoot, indexDir) {
5980
+ switch (serviceId) {
5981
+ case "kanban":
5982
+ return killKanbanServer(projectRoot, action);
5983
+ case "sage":
5984
+ return killSageServer(projectRoot, action);
5985
+ case "chronicle":
5986
+ return killChronicleServer(projectRoot, action);
5987
+ case "codebase-index":
5988
+ return killCodebaseIndexServer(projectRoot, indexDir, action);
5989
+ case "mailbox":
5990
+ return killMailboxServer(projectRoot, action);
5991
+ case "governance":
5992
+ return {
5993
+ serviceId: "governance",
5994
+ action,
5995
+ success: false,
5996
+ message: "Governance health is read-only; daemon shutdown requires a separate admin control capability."
5997
+ };
5998
+ default:
5999
+ return {
6000
+ serviceId,
6001
+ action,
6002
+ success: false,
6003
+ message: `Unknown service: ${serviceId}`
6004
+ };
6005
+ }
6006
+ }
6007
+ async function killKanbanServer(projectRoot, action) {
6008
+ if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
6009
+ return {
6010
+ serviceId: "kanban",
6011
+ action,
6012
+ success: false,
6013
+ message: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0"
6014
+ };
6015
+ }
6016
+ let connection;
6017
+ try {
6018
+ connection = await getKanbanServerConnection(projectRoot);
6019
+ } catch (error2) {
6020
+ return {
6021
+ serviceId: "kanban",
6022
+ action,
6023
+ success: false,
6024
+ message: error2 instanceof Error ? error2.message : String(error2)
6025
+ };
6026
+ }
6027
+ if (!connection) {
6028
+ return {
6029
+ serviceId: "kanban",
6030
+ action,
6031
+ success: false,
6032
+ message: "Kanban IPC daemon is not running"
6033
+ };
6034
+ }
6035
+ try {
6036
+ const result = await connection.request("shutdown", {
6037
+ reason: `WebUI request: ${action}`
6038
+ });
6039
+ if (!result.stopping) {
6040
+ return {
6041
+ serviceId: "kanban",
6042
+ action,
6043
+ success: false,
6044
+ message: "Kanban IPC daemon shutdown failed (not confirmed)"
6045
+ };
6046
+ }
6047
+ if (action === "restart") {
6048
+ closeKanbanServerConnections();
6049
+ const restartResult = await restartKanbanServer(projectRoot);
6050
+ return restartResult;
6051
+ }
6052
+ return {
6053
+ serviceId: "kanban",
6054
+ action,
6055
+ success: true,
6056
+ message: "Kanban IPC daemon shutdown requested"
6057
+ };
6058
+ } catch (error2) {
6059
+ return {
6060
+ serviceId: "kanban",
6061
+ action,
6062
+ success: false,
6063
+ message: error2 instanceof Error ? error2.message : String(error2)
6064
+ };
6065
+ }
6066
+ }
6067
+ async function restartKanbanServer(projectRoot) {
6068
+ await waitForShutdown(() => isKanbanServerAvailable(projectRoot));
6069
+ try {
6070
+ const connection = await getKanbanServerConnection(projectRoot);
6071
+ if (!connection) {
6072
+ return {
6073
+ serviceId: "kanban",
6074
+ action: "restart",
6075
+ success: false,
6076
+ message: "Kanban IPC daemon failed to restart (no connection after re-init)"
6077
+ };
6078
+ }
6079
+ await connection.request("ping", {}, { timeoutMs: 1e4 });
6080
+ return {
6081
+ serviceId: "kanban",
6082
+ action: "restart",
6083
+ success: true,
6084
+ message: "Kanban IPC daemon restarted successfully"
6085
+ };
6086
+ } catch (error2) {
6087
+ return {
6088
+ serviceId: "kanban",
6089
+ action: "restart",
6090
+ success: false,
6091
+ message: `Kanban IPC daemon restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
6092
+ };
6093
+ }
6094
+ }
6095
+ async function killSageServer(projectRoot, action) {
6096
+ if (!isSageProjectServerAvailable()) {
6097
+ return {
6098
+ serviceId: "sage",
6099
+ action,
6100
+ success: false,
6101
+ message: "SAGE project server is unavailable in this runtime"
6102
+ };
6103
+ }
6104
+ const connection = new SageProjectServerConnection(projectRoot);
6105
+ try {
6106
+ const result = await connection.shutdown(`WebUI request: ${action}`);
6107
+ if (!result.stopped) {
6108
+ return {
6109
+ serviceId: "sage",
6110
+ action,
6111
+ success: false,
6112
+ message: `SAGE memory server shutdown failed: ${result.reason ?? "unknown"}`
6113
+ };
6114
+ }
6115
+ if (action === "restart") {
6116
+ return await restartSageServer(projectRoot);
6117
+ }
6118
+ return {
6119
+ serviceId: "sage",
6120
+ action,
6121
+ success: true,
6122
+ message: "SAGE memory server shutdown requested"
6123
+ };
6124
+ } catch (error2) {
6125
+ return {
6126
+ serviceId: "sage",
6127
+ action,
6128
+ success: false,
6129
+ message: error2 instanceof Error ? error2.message : String(error2)
6130
+ };
6131
+ } finally {
6132
+ connection.close();
6133
+ }
6134
+ }
6135
+ async function restartSageServer(projectRoot) {
6136
+ await waitForShutdown(async () => {
6137
+ const probe = new SageProjectServerConnection(projectRoot);
6138
+ try {
6139
+ return await probe.status() !== null;
6140
+ } finally {
6141
+ probe.close();
6142
+ }
6143
+ });
6144
+ const verifyConn = new SageProjectServerConnection(projectRoot);
6145
+ try {
6146
+ await verifyConn.call("ping", {}, { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } });
6147
+ return {
6148
+ serviceId: "sage",
6149
+ action: "restart",
6150
+ success: true,
6151
+ message: "SAGE memory server restarted successfully"
6152
+ };
6153
+ } catch (error2) {
6154
+ return {
6155
+ serviceId: "sage",
6156
+ action: "restart",
6157
+ success: false,
6158
+ message: `SAGE memory server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
6159
+ };
6160
+ } finally {
6161
+ verifyConn.close();
6162
+ }
6163
+ }
6164
+ async function killChronicleServer(projectRoot, action) {
6165
+ const options = resolveChronicleProjectServerOptions({ projectRoot });
6166
+ const client = new ChronicleProjectServerClient(options);
6167
+ try {
6168
+ const result = await client.shutdown(`WebUI request: ${action}`);
6169
+ if (!result.stopped) {
6170
+ return {
6171
+ serviceId: "chronicle",
6172
+ action,
6173
+ success: false,
6174
+ message: `Chronicle telemetry server shutdown failed: ${result.reason ?? "unknown"}`
6175
+ };
6176
+ }
6177
+ if (action === "restart") {
6178
+ return await restartChronicleServer(projectRoot);
6179
+ }
6180
+ return {
6181
+ serviceId: "chronicle",
6182
+ action,
6183
+ success: true,
6184
+ message: "Chronicle telemetry server shutdown requested"
6185
+ };
6186
+ } catch (error2) {
6187
+ return {
6188
+ serviceId: "chronicle",
6189
+ action,
6190
+ success: false,
6191
+ message: error2 instanceof Error ? error2.message : String(error2)
6192
+ };
6193
+ } finally {
6194
+ client.close();
6195
+ }
6196
+ }
6197
+ async function restartChronicleServer(projectRoot) {
6198
+ const options = resolveChronicleProjectServerOptions({ projectRoot });
6199
+ const endpoint = new ChronicleProjectServerClient(options).endpoint;
6200
+ await waitForShutdown(async () => isEndpointAlive(endpoint));
6201
+ let access2;
6202
+ try {
6203
+ access2 = createChronicleProjectAccess2({ projectRoot });
6204
+ await access2.call("ping", {}, { timeoutMs: 1e4 });
6205
+ if (access2.mode !== "server") {
6206
+ return {
6207
+ serviceId: "chronicle",
6208
+ action: "restart",
6209
+ success: false,
6210
+ message: `Chronicle telemetry server restarted but running in ${access2.mode} mode (expected server)`
6211
+ };
6212
+ }
6213
+ return {
6214
+ serviceId: "chronicle",
6215
+ action: "restart",
6216
+ success: true,
6217
+ message: "Chronicle telemetry server restarted successfully"
6218
+ };
6219
+ } catch (error2) {
6220
+ return {
6221
+ serviceId: "chronicle",
6222
+ action: "restart",
6223
+ success: false,
6224
+ message: `Chronicle telemetry server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
6225
+ };
6226
+ } finally {
6227
+ await access2?.close();
6228
+ }
6229
+ }
6230
+ async function killCodebaseIndexServer(projectRoot, indexDir, action) {
6231
+ try {
6232
+ const result = await shutdownCodebaseIndexServer(
6233
+ projectRoot,
6234
+ indexDir,
6235
+ `websocket-request:${action}`
6236
+ );
6237
+ if (!result.stopped) {
6238
+ return {
6239
+ serviceId: "codebase-index",
6240
+ action,
6241
+ success: false,
6242
+ message: `Codebase index server shutdown failed: ${result.reason ?? "unknown"}`
6243
+ };
6244
+ }
6245
+ if (action === "restart") {
6246
+ return await restartCodebaseIndexServer(projectRoot, indexDir);
6247
+ }
6248
+ return {
6249
+ serviceId: "codebase-index",
6250
+ action,
6251
+ success: true,
6252
+ message: "Codebase index server shutdown requested"
6253
+ };
6254
+ } catch (error2) {
6255
+ return {
6256
+ serviceId: "codebase-index",
6257
+ action,
6258
+ success: false,
6259
+ message: error2 instanceof Error ? error2.message : String(error2)
6260
+ };
6261
+ }
6262
+ }
6263
+ async function restartCodebaseIndexServer(projectRoot, indexDir) {
6264
+ await waitForShutdown(async () => {
6265
+ try {
6266
+ await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
6267
+ timeoutMs: 1e3
6268
+ });
6269
+ return true;
6270
+ } catch {
6271
+ return false;
6272
+ }
6273
+ });
6274
+ try {
6275
+ await ensureCodebaseIndexServer2({ projectRoot, indexDir });
6276
+ const health = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
6277
+ timeoutMs: 1e4
6278
+ });
6279
+ if (health.status === "unresponsive") {
6280
+ return {
6281
+ serviceId: "codebase-index",
6282
+ action: "restart",
6283
+ success: false,
6284
+ message: "Codebase index server restarted but is unresponsive"
6285
+ };
6286
+ }
6287
+ return {
6288
+ serviceId: "codebase-index",
6289
+ action: "restart",
6290
+ success: true,
6291
+ message: "Codebase index server restarted successfully"
6292
+ };
6293
+ } catch (error2) {
6294
+ return {
6295
+ serviceId: "codebase-index",
6296
+ action: "restart",
6297
+ success: false,
6298
+ message: `Codebase index server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
6299
+ };
6300
+ }
6301
+ }
6302
+ async function killMailboxServer(projectRoot, action) {
6303
+ if (!isMailboxProjectServerAvailable()) {
6304
+ return {
6305
+ serviceId: "mailbox",
6306
+ action,
6307
+ success: false,
6308
+ message: "Mailbox project server is unavailable in this runtime"
6309
+ };
6310
+ }
6311
+ const connection = new MailboxProjectServerConnection(
6312
+ resolveWstackPaths2({ projectRoot }).projectDir
6313
+ );
6314
+ try {
6315
+ const result = await connection.shutdown(`WebUI request: ${action}`);
6316
+ if (!result.stopped) {
6317
+ return {
6318
+ serviceId: "mailbox",
6319
+ action,
6320
+ success: false,
6321
+ message: `Mailbox IPC server shutdown failed: ${result.reason ?? "unknown"}`
6322
+ };
6323
+ }
6324
+ if (action === "restart") {
6325
+ return await restartMailboxServer(projectRoot);
6326
+ }
6327
+ return {
6328
+ serviceId: "mailbox",
6329
+ action,
6330
+ success: true,
6331
+ message: "Mailbox IPC server shutdown requested"
6332
+ };
6333
+ } catch (error2) {
6334
+ return {
6335
+ serviceId: "mailbox",
6336
+ action,
6337
+ success: false,
6338
+ message: error2 instanceof Error ? error2.message : String(error2)
6339
+ };
6340
+ } finally {
6341
+ connection.close();
6342
+ }
6343
+ }
6344
+ async function restartMailboxServer(projectRoot) {
6345
+ await waitForShutdown(async () => {
6346
+ const probe = new MailboxProjectServerConnection(
6347
+ resolveWstackPaths2({ projectRoot }).projectDir
6348
+ );
6349
+ try {
6350
+ return await probe.probeStatus() !== null;
6351
+ } finally {
6352
+ probe.close();
6353
+ }
6354
+ });
6355
+ const verifyConn = new MailboxProjectServerConnection(
6356
+ resolveWstackPaths2({ projectRoot }).projectDir
6357
+ );
6358
+ try {
6359
+ await verifyConn.call("ping", {}, { timeoutMs: 1e4 });
6360
+ return {
6361
+ serviceId: "mailbox",
6362
+ action: "restart",
6363
+ success: true,
6364
+ message: "Mailbox IPC server restarted successfully"
6365
+ };
6366
+ } catch (error2) {
6367
+ return {
6368
+ serviceId: "mailbox",
6369
+ action: "restart",
6370
+ success: false,
6371
+ message: `Mailbox IPC server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
6372
+ };
6373
+ } finally {
6374
+ verifyConn.close();
6375
+ }
6376
+ }
6377
+ var RESTART_POLL_INTERVAL_MS = 250;
6378
+ var RESTART_DEADLINE_MS = 3e3;
6379
+ function isEndpointAlive(endpoint) {
6380
+ return new Promise((resolve15) => {
6381
+ const sock = net.createConnection(endpoint);
6382
+ const timer = setTimeout(() => {
6383
+ sock.destroy();
6384
+ resolve15(false);
6385
+ }, 500);
6386
+ timer.unref?.();
6387
+ sock.once("connect", () => {
6388
+ clearTimeout(timer);
6389
+ sock.destroy();
6390
+ resolve15(true);
6391
+ });
6392
+ sock.once("error", () => {
6393
+ clearTimeout(timer);
6394
+ sock.destroy();
6395
+ resolve15(false);
6396
+ });
6397
+ });
6398
+ }
6399
+ async function waitForShutdown(probe) {
6400
+ if (!probe) {
6401
+ await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
6402
+ return;
6403
+ }
6404
+ const deadline = Date.now() + RESTART_DEADLINE_MS;
6405
+ while (Date.now() < deadline) {
6406
+ try {
6407
+ const stillUp = await probe();
6408
+ if (!stillUp) return;
6409
+ } catch {
6410
+ return;
6411
+ }
6412
+ await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
6413
+ }
6414
+ }
5803
6415
  function failureService(id, label, required, mode, error2, latencyMs) {
5804
6416
  const message = error2 instanceof Error ? error2.message : String(error2);
5805
6417
  return {
@@ -6508,12 +7120,12 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
6508
7120
  ...maybeVerify,
6509
7121
  onPhaseComplete: (phase) => {
6510
7122
  this.logger.info(`[Goal] Phase completed: ${phase.name}`);
6511
- void this.store.save(graph);
7123
+ this.persistDetached(graph);
6512
7124
  this.broadcastState();
6513
7125
  },
6514
7126
  onPhaseFail: (phase, error2) => {
6515
7127
  this.logger.error(`[Goal] Phase failed: ${phase.name} \u2014 ${error2.message}`);
6516
- void this.store.save(graph);
7128
+ this.persistDetached(graph);
6517
7129
  this.broadcastState();
6518
7130
  }
6519
7131
  },
@@ -6530,7 +7142,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
6530
7142
  this.broadcastState();
6531
7143
  void this.orchestrator.start().then(() => {
6532
7144
  this.orchestrator?.stop();
6533
- void this.store.save(graph);
7145
+ this.persistDetached(graph);
6534
7146
  this.stopBroadcast();
6535
7147
  const failed = graph.failedPhaseIds.length > 0;
6536
7148
  this.broadcast(
@@ -6728,9 +7340,27 @@ ${result_.finalText.slice(0, 2e3)}`
6728
7340
  this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
6729
7341
  }
6730
7342
  }
7343
+ /**
7344
+ * Fire-and-forget persist.
7345
+ *
7346
+ * Every detached `store.save()` used to be a bare `void`, so a rejection
7347
+ * became an unhandled rejection and — under Node 22's default
7348
+ * `--unhandled-rejections=throw` — killed the process mid-run. On Windows an
7349
+ * AV scanner or indexer holding the `.wrongstack/phases/<id>.json` rename
7350
+ * target for a few hundred ms is enough (EPERM from `atomicWrite`), and in
7351
+ * `--webui` mode that takes the CLI session down with it. `handleStop` at
7352
+ * `:549` already had the `.catch`; these call sites did not.
7353
+ */
7354
+ persistDetached(graph) {
7355
+ void this.store.save(graph).catch((err) => {
7356
+ this.logger.warn(
7357
+ `[Goal] Failed to persist phase graph: ${err instanceof Error ? err.message : String(err)}`
7358
+ );
7359
+ });
7360
+ }
6731
7361
  /** Persist + broadcast after an interactive board mutation. */
6732
7362
  afterBoardMutation() {
6733
- if (this.graph) void this.store.save(this.graph);
7363
+ if (this.graph) this.persistDetached(this.graph);
6734
7364
  this.broadcastState();
6735
7365
  }
6736
7366
  async handleTaskStatusChange(taskId, status) {
@@ -7714,12 +8344,21 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
7714
8344
  const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
7715
8345
  const store = new DefaultSessionStore3({ dir: paths.projectSessions });
7716
8346
  const reader = new DefaultSessionReader2({ store });
7717
- const rawEntries = [];
8347
+ const RING = Math.max(limit * 4, 2e3);
8348
+ const ring = [];
8349
+ let totalRaw = 0;
8350
+ let dropped = false;
7718
8351
  for await (const ev of reader.replay(sessionId)) {
7719
8352
  const mapped = mapWatchEntry(ev);
7720
- if (mapped) rawEntries.push(mapped);
8353
+ if (!mapped) continue;
8354
+ totalRaw += 1;
8355
+ ring.push(mapped);
8356
+ if (ring.length > RING) {
8357
+ ring.shift();
8358
+ dropped = true;
8359
+ }
7721
8360
  }
7722
- const all = correlateToolEvents(rawEntries);
8361
+ const all = correlateToolEvents(ring);
7723
8362
  const tail2 = all.slice(-limit);
7724
8363
  res.writeHead(200, { "Content-Type": "application/json" });
7725
8364
  res.end(
@@ -7728,7 +8367,12 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
7728
8367
  status: entry.status,
7729
8368
  clientType: entry.clientType,
7730
8369
  projectName: entry.projectName,
7731
- total: all.length,
8370
+ // Exact when the whole session fit in the ring (the previous
8371
+ // behaviour). Past that, correlation never ran over the dropped
8372
+ // prefix, so report the raw event count — an upper bound — and say so
8373
+ // rather than silently understating the session's size.
8374
+ total: dropped ? totalRaw : all.length,
8375
+ ...dropped ? { truncated: true } : {},
7732
8376
  entries: tail2
7733
8377
  })
7734
8378
  );
@@ -8418,7 +9062,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
8418
9062
  }
8419
9063
 
8420
9064
  // src/server/techstack-handlers.ts
8421
- import { randomUUID as randomUUID2 } from "node:crypto";
9065
+ import { randomUUID as randomUUID3 } from "node:crypto";
8422
9066
  var DEEP_DIVE_TIMEOUT_MS = 6e4;
8423
9067
  function sendJson3(res, status, data) {
8424
9068
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -8459,7 +9103,7 @@ function requireJobDeps(res, deps2) {
8459
9103
  }
8460
9104
  function startJob(res, deps2, kind) {
8461
9105
  if (!requireJobDeps(res, deps2)) return;
8462
- const jobId = randomUUID2();
9106
+ const jobId = randomUUID3();
8463
9107
  const controller = new AbortController();
8464
9108
  deps2.runningJobs?.set(jobId, controller);
8465
9109
  deps2.emit?.({ type: "techstack.job.started", payload: { jobId, kind } });
@@ -8925,7 +9569,7 @@ function createHttpServer(opts) {
8925
9569
  res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
8926
9570
  return;
8927
9571
  }
8928
- const providedAccessToken = requestToken(req, url);
9572
+ const providedAccessToken = requestToken(req, url, { allowQuery: true });
8929
9573
  const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
8930
9574
  const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
8931
9575
  if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
@@ -9779,6 +10423,27 @@ import {
9779
10423
  getServerKanbanStore
9780
10424
  } from "@wrongstack/kanban";
9781
10425
  import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
10426
+
10427
+ // src/server/kanban-broadcast.ts
10428
+ function kanbanBoardMessage(board) {
10429
+ return { type: "kanban.get", payload: { success: true, data: { board } } };
10430
+ }
10431
+ function kanbanListMessage(boards) {
10432
+ return { type: "kanban.list", payload: { success: true, data: boards } };
10433
+ }
10434
+ function kanbanDeletedMessage(boardId) {
10435
+ return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
10436
+ }
10437
+ async function publishKanbanBoard(broadcast2, board, listBoards4) {
10438
+ broadcast2(kanbanBoardMessage(board));
10439
+ if (!listBoards4) return;
10440
+ try {
10441
+ broadcast2(kanbanListMessage(await listBoards4()));
10442
+ } catch {
10443
+ }
10444
+ }
10445
+
10446
+ // src/server/kanban-dispatch.ts
9782
10447
  function reply(ws, type, success, value) {
9783
10448
  send(ws, {
9784
10449
  type,
@@ -9895,10 +10560,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
9895
10560
  payload: { success: true, data: { boardId: board.id, task: completedTask } }
9896
10561
  });
9897
10562
  if (completedBoard) {
9898
- ctx.broadcast?.({
9899
- type: "kanban.get",
9900
- payload: { success: true, data: { board: completedBoard } }
9901
- });
10563
+ ctx.broadcast?.(kanbanBoardMessage(completedBoard));
9902
10564
  }
9903
10565
  ctx.broadcast?.({
9904
10566
  type: "kanban.list",
@@ -9922,7 +10584,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
9922
10584
  payload: { success: true, data: { boardId: board.id, task: runningTask } }
9923
10585
  });
9924
10586
  if (started?.board) {
9925
- ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: started.board } } });
10587
+ ctx.broadcast?.(kanbanBoardMessage(started.board));
9926
10588
  }
9927
10589
  reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
9928
10590
  } catch (error2) {
@@ -10165,14 +10827,11 @@ async function handleDecompositionResolution(ws, type, payload, ctx) {
10165
10827
  type: "kanban.decomposition.applied",
10166
10828
  payload: { success: true, data: { board: resolved.board } }
10167
10829
  });
10168
- ctx.broadcast?.({
10169
- type: "kanban.get",
10170
- payload: { success: true, data: { board: resolved.board } }
10171
- });
10172
- ctx.broadcast?.({
10173
- type: "kanban.list",
10174
- payload: { success: true, data: await listBoards(ctx.projectRoot) }
10175
- });
10830
+ await publishKanbanBoard(
10831
+ (message) => ctx.broadcast?.(message),
10832
+ resolved.board,
10833
+ () => listBoards(ctx.projectRoot)
10834
+ );
10176
10835
  } else {
10177
10836
  ctx.broadcast?.({
10178
10837
  type: "kanban.decomposition.resolved",
@@ -10205,10 +10864,7 @@ async function handleTaskVerification(ws, type, payload, ctx) {
10205
10864
  payload: { success: true, data: { boardId, task: freshTask } }
10206
10865
  });
10207
10866
  if (persisted) {
10208
- ctx.broadcast?.({
10209
- type: "kanban.get",
10210
- payload: { success: true, data: { board: persisted } }
10211
- });
10867
+ ctx.broadcast?.(kanbanBoardMessage(persisted));
10212
10868
  }
10213
10869
  } catch (err) {
10214
10870
  ctx.broadcast?.({
@@ -11084,10 +11740,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
11084
11740
  let connectionCount = 0;
11085
11741
  const broadcastDeleted = (boardId) => {
11086
11742
  knownRevisions.delete(boardId);
11087
- broadcastMessage({
11088
- type: "kanban.delete",
11089
- payload: { success: true, data: { removed: true, boardId } }
11090
- });
11743
+ broadcastMessage(kanbanDeletedMessage(boardId));
11091
11744
  };
11092
11745
  const broadcastBoard = async (boardId) => {
11093
11746
  const board = await store.getBoard(boardId);
@@ -11096,10 +11749,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
11096
11749
  return;
11097
11750
  }
11098
11751
  knownRevisions.set(boardId, board.updatedAt);
11099
- broadcastMessage({
11100
- type: "kanban.get",
11101
- payload: { success: true, data: { board } }
11102
- });
11752
+ broadcastMessage(kanbanBoardMessage(board));
11103
11753
  };
11104
11754
  const reconcileAfterConnect = async () => {
11105
11755
  const summaries = await store.listBoards();
@@ -11118,20 +11768,36 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
11118
11768
  }
11119
11769
  }
11120
11770
  };
11121
- return bridgeKanbanSupervisor(
11771
+ const COALESCE_MS = 300;
11772
+ const pendingBroadcasts = /* @__PURE__ */ new Map();
11773
+ const scheduleBroadcast = (boardId) => {
11774
+ if (pendingBroadcasts.has(boardId)) return;
11775
+ const timer = setTimeout(() => {
11776
+ pendingBroadcasts.delete(boardId);
11777
+ void broadcastBoard(boardId).catch(() => {
11778
+ });
11779
+ }, COALESCE_MS);
11780
+ timer.unref?.();
11781
+ pendingBroadcasts.set(boardId, timer);
11782
+ };
11783
+ const unsubscribe = bridgeKanbanSupervisor(
11122
11784
  projectRoot,
11123
11785
  async (event) => {
11786
+ const family = event.event?.split(".")[0];
11787
+ if (family !== "board" && family !== "task" && family !== "column") return;
11124
11788
  const evData = event.data;
11125
11789
  const boardId = evData?.boardId;
11126
11790
  if (!boardId) return;
11127
- try {
11128
- if (event.event === "board.deleted") {
11129
- broadcastDeleted(boardId);
11130
- return;
11791
+ if (event.event === "board.deleted") {
11792
+ const timer = pendingBroadcasts.get(boardId);
11793
+ if (timer) {
11794
+ clearTimeout(timer);
11795
+ pendingBroadcasts.delete(boardId);
11131
11796
  }
11132
- await broadcastBoard(boardId);
11133
- } catch {
11797
+ broadcastDeleted(boardId);
11798
+ return;
11134
11799
  }
11800
+ scheduleBroadcast(boardId);
11135
11801
  },
11136
11802
  {
11137
11803
  autoReconnect: true,
@@ -11139,6 +11805,11 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
11139
11805
  onConnected: reconcileAfterConnect
11140
11806
  }
11141
11807
  );
11808
+ return () => {
11809
+ for (const timer of pendingBroadcasts.values()) clearTimeout(timer);
11810
+ pendingBroadcasts.clear();
11811
+ unsubscribe();
11812
+ };
11142
11813
  }
11143
11814
 
11144
11815
  // src/server/lifecycle.ts
@@ -11155,7 +11826,13 @@ function createShutdown(res) {
11155
11826
  } catch (e) {
11156
11827
  log(`[WebUI] Error closing session: ${e instanceof Error ? e.message : String(e)}`);
11157
11828
  }
11158
- for (const ws of res.clients()) ws.close();
11829
+ for (const ws of res.clients()) {
11830
+ try {
11831
+ ws.close();
11832
+ ws.terminate?.();
11833
+ } catch {
11834
+ }
11835
+ }
11159
11836
  for (const server of res.servers) server?.close();
11160
11837
  if (res.onShutdown) {
11161
11838
  try {
@@ -11592,39 +12269,6 @@ import {
11592
12269
  restartMcp,
11593
12270
  updateMcp
11594
12271
  } from "@wrongstack/mcp";
11595
-
11596
- // src/server/privileged-actions.ts
11597
- import { randomUUID as randomUUID3 } from "node:crypto";
11598
- import {
11599
- isTrustDecisionAllowed
11600
- } from "@wrongstack/core/security";
11601
- async function authorizeWebUIAction(boundary, action, logger) {
11602
- const request = {
11603
- version: 1,
11604
- requestId: randomUUID3(),
11605
- actor: {
11606
- kind: "remote-client",
11607
- ...action.sessionId ? { sessionId: action.sessionId } : {}
11608
- },
11609
- surface: "webui",
11610
- capability: action.capability,
11611
- subject: action.subject,
11612
- risk: action.risk,
11613
- scope: {
11614
- ...action.cwd ? { cwd: action.cwd } : {},
11615
- ...action.sessionId ? { sessionId: action.sessionId } : {}
11616
- },
11617
- authContext: { method: "session" },
11618
- ...action.metadata ? { metadata: action.metadata } : {}
11619
- };
11620
- const decision = await boundary.evaluate(request);
11621
- logger?.debug?.(
11622
- `[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
11623
- );
11624
- return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
11625
- }
11626
-
11627
- // src/server/mcp-handlers.ts
11628
12272
  async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
11629
12273
  if (!trustBoundary) return true;
11630
12274
  const authorization = await authorizeWebUIAction(trustBoundary, {
@@ -12856,11 +13500,11 @@ function createModelOperations(context) {
12856
13500
  }
12857
13501
 
12858
13502
  // src/server/port-utils.ts
12859
- import * as net from "node:net";
13503
+ import * as net2 from "node:net";
12860
13504
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
12861
13505
  function isPortFree(host, port) {
12862
13506
  return new Promise((resolve15) => {
12863
- const srv = net.createServer();
13507
+ const srv = net2.createServer();
12864
13508
  srv.once("error", () => resolve15(false));
12865
13509
  srv.once("listening", () => {
12866
13510
  srv.close(() => resolve15(true));
@@ -13122,6 +13766,7 @@ function seedContextMeta(config, context) {
13122
13766
  meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
13123
13767
  meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
13124
13768
  meta["nextPrediction"] = config.nextPrediction ?? false;
13769
+ meta["nextStepsTool"] = config.tools?.nextsteps?.enabled === true;
13125
13770
  meta["fallbackModels"] = config.fallbackModels ?? [];
13126
13771
  meta["fallbackBridge"] = config.fallbackBridge ?? "";
13127
13772
  meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
@@ -13210,6 +13855,7 @@ function seedContextMeta(config, context) {
13210
13855
  meta["autoReviewProvider"] = autoReviewExt?.["provider"] ?? "";
13211
13856
  meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
13212
13857
  meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
13858
+ meta["autoReviewModelSelection"] = autoReviewExt?.["modelSelection"] === "random" ? "random" : "round-robin";
13213
13859
  meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
13214
13860
  meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
13215
13861
  meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
@@ -13247,6 +13893,7 @@ var PREF_KEYS = [
13247
13893
  "chime",
13248
13894
  "confirmExit",
13249
13895
  "nextPrediction",
13896
+ "nextStepsTool",
13250
13897
  "enhanceEnabled",
13251
13898
  "enhanceDelayMs",
13252
13899
  "enhanceLanguage",
@@ -13310,6 +13957,7 @@ var PREF_KEYS = [
13310
13957
  "autoReviewProvider",
13311
13958
  "autoReviewModel",
13312
13959
  "autoReviewFallbackProfile",
13960
+ "autoReviewModelSelection",
13313
13961
  "autoReviewFallbackModels",
13314
13962
  "autoReviewDebounceMs",
13315
13963
  "autoReviewMaxFilesPerBatch",
@@ -13523,6 +14171,11 @@ async function persistPrefsToConfig(deps2, holder, payload) {
13523
14171
  toolsCfg.maxIterations = payload["maxIterations"];
13524
14172
  decrypted.tools = toolsCfg;
13525
14173
  }
14174
+ if (typeof payload["nextStepsTool"] === "boolean") {
14175
+ const toolsCfg = decrypted.tools ?? {};
14176
+ toolsCfg.nextsteps = { enabled: payload["nextStepsTool"] };
14177
+ decrypted.tools = toolsCfg;
14178
+ }
13526
14179
  const hqTouched = typeof payload["hqEnabled"] === "boolean" || typeof payload["hqUrl"] === "string" || typeof payload["hqToken"] === "string" || typeof payload["hqRawContent"] === "boolean";
13527
14180
  if (hqTouched) {
13528
14181
  const hqCfg = decrypted.hq ?? {};
@@ -13630,7 +14283,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
13630
14283
  ext["wstack-chimera"] = chimera;
13631
14284
  decrypted.extensions = ext;
13632
14285
  }
13633
- const autoReviewTouched = typeof payload["autoReviewEnabled"] === "boolean" || typeof payload["autoReviewProvider"] === "string" || typeof payload["autoReviewModel"] === "string" || typeof payload["autoReviewFallbackProfile"] === "string" || Array.isArray(payload["autoReviewFallbackModels"]) || typeof payload["autoReviewDebounceMs"] === "number" || typeof payload["autoReviewMaxFilesPerBatch"] === "number" || typeof payload["autoReviewMaxConcurrentReviews"] === "number" || typeof payload["autoReviewCascadeOn"] === "string";
14286
+ const autoReviewTouched = typeof payload["autoReviewEnabled"] === "boolean" || typeof payload["autoReviewProvider"] === "string" || typeof payload["autoReviewModel"] === "string" || typeof payload["autoReviewFallbackProfile"] === "string" || typeof payload["autoReviewModelSelection"] === "string" || Array.isArray(payload["autoReviewFallbackModels"]) || typeof payload["autoReviewDebounceMs"] === "number" || typeof payload["autoReviewMaxFilesPerBatch"] === "number" || typeof payload["autoReviewMaxConcurrentReviews"] === "number" || typeof payload["autoReviewCascadeOn"] === "string";
13634
14287
  if (autoReviewTouched) {
13635
14288
  const ext = decrypted.extensions ?? {};
13636
14289
  const ar = ext["wstack-auto-review"] ?? {};
@@ -13647,6 +14300,9 @@ async function persistPrefsToConfig(deps2, holder, payload) {
13647
14300
  ar["fallbackProfile"] = payload["autoReviewFallbackProfile"];
13648
14301
  }
13649
14302
  }
14303
+ if (payload["autoReviewModelSelection"] === "round-robin" || payload["autoReviewModelSelection"] === "random") {
14304
+ ar["modelSelection"] = payload["autoReviewModelSelection"];
14305
+ }
13650
14306
  if (typeof payload["autoReviewDebounceMs"] === "number" && payload["autoReviewDebounceMs"] >= 0) {
13651
14307
  ar["debounceMs"] = payload["autoReviewDebounceMs"];
13652
14308
  }
@@ -14007,6 +14663,7 @@ function createProjectHandlers(ctx) {
14007
14663
  ctx.context.session = next;
14008
14664
  ctx.context.state.replaceMessages([]);
14009
14665
  ctx.context.state.replaceTodos([]);
14666
+ ctx.context.clearMemoryEvidence?.();
14010
14667
  ctx.context.readFiles.clear();
14011
14668
  ctx.context.fileMtimes.clear();
14012
14669
  ctx.tokenCounter.reset();
@@ -14051,7 +14708,7 @@ function createProjectHandlers(ctx) {
14051
14708
  }
14052
14709
 
14053
14710
  // src/server/provider-handlers.ts
14054
- import { resolveProviderModelList } from "@wrongstack/core/models";
14711
+ import { hasProviderCredential, resolveProviderModelList } from "@wrongstack/core/models";
14055
14712
  import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
14056
14713
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
14057
14714
  import {
@@ -14358,7 +15015,7 @@ function createProviderOperations(deps2) {
14358
15015
  }
14359
15016
  try {
14360
15017
  const providers = await deps2.modelsRegistry.listProviders();
14361
- const savedIds = new Set(Object.keys(await loadConfigProviders()));
15018
+ const savedProviders = await loadConfigProviders();
14362
15019
  sendMessage(ws, {
14363
15020
  type: "provider.catalog",
14364
15021
  payload: {
@@ -14369,7 +15026,7 @@ function createProviderOperations(deps2) {
14369
15026
  apiBase: provider.apiBase,
14370
15027
  envVars: provider.envVars,
14371
15028
  modelCount: provider.models.length,
14372
- hasApiKey: savedIds.has(provider.id) || provider.envVars.some((name2) => !!process.env[name2])
15029
+ hasApiKey: hasProviderCredential(provider, { providers: savedProviders })
14373
15030
  }))
14374
15031
  }
14375
15032
  });
@@ -14853,6 +15510,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
14853
15510
  "ping",
14854
15511
  "user_message",
14855
15512
  "tool.confirm_result",
15513
+ "topic.advice",
14856
15514
  "completion.request",
14857
15515
  "model.switch",
14858
15516
  "model.refine",
@@ -15155,6 +15813,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
15155
15813
  "tool.loop_detected",
15156
15814
  "tool.progress",
15157
15815
  "tool.started",
15816
+ "topic.advice_result",
15158
15817
  "tools.list",
15159
15818
  "trust.persisted"
15160
15819
  ];
@@ -15486,6 +16145,8 @@ var SURFACE_PROTOCOL_CAPABILITIES = [
15486
16145
  "chronicle.metrics",
15487
16146
  "chronicle.status",
15488
16147
  "connections.health",
16148
+ /** Bounded topic-shift advice plus same-session provider-context boundaries. */
16149
+ "context.topic-boundary",
15489
16150
  /** Interview resume/discard + lastAgentText/lastRunId continuity. */
15490
16151
  "sdd.interview.continuity",
15491
16152
  /** Launch multi-agent runs from a graph id or resolved spec id. */
@@ -15877,6 +16538,7 @@ function createSessionHandlers(ctx) {
15877
16538
  await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
15878
16539
  ctx.context.state.replaceTodos(todos);
15879
16540
  resetContextAccounting();
16541
+ ctx.context.clearMemoryEvidence?.();
15880
16542
  ctx.context.readFiles.clear();
15881
16543
  ctx.context.fileMtimes.clear();
15882
16544
  ctx.context.state.setMeta?.(
@@ -15924,6 +16586,7 @@ function createSessionHandlers(ctx) {
15924
16586
  ctx.context.state.replaceMessages([]);
15925
16587
  ctx.context.state.replaceTodos([]);
15926
16588
  resetContextAccounting();
16589
+ ctx.context.clearMemoryEvidence?.();
15927
16590
  ctx.context.readFiles.clear();
15928
16591
  ctx.context.fileMtimes.clear();
15929
16592
  ctx.tokenCounter.reset?.();
@@ -15938,6 +16601,7 @@ function createSessionHandlers(ctx) {
15938
16601
  ctx.context.state.replaceMessages([]);
15939
16602
  ctx.context.state.replaceTodos([]);
15940
16603
  resetContextAccounting();
16604
+ ctx.context.clearMemoryEvidence?.();
15941
16605
  ctx.context.readFiles.clear();
15942
16606
  ctx.context.fileMtimes.clear();
15943
16607
  ctx.tokenCounter.reset?.();
@@ -18408,7 +19072,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
18408
19072
  const on = (event, listener) => events.on(event, listener);
18409
19073
  return on("client.status", async (e) => {
18410
19074
  broadcast2(clients, { type: "client.status_update", payload: e });
18411
- if (wpaths?.projectStatus) {
19075
+ if (wpaths?.projectStatus && e.projectHash !== "unknown") {
18412
19076
  try {
18413
19077
  const statusFile = wpaths.projectStatus(e.projectHash);
18414
19078
  const dir = path19.dirname(statusFile);
@@ -18583,7 +19247,10 @@ function registerSetupEventsProviderHandlers({
18583
19247
  sessionId: e.sessionId,
18584
19248
  providerId: e.providerId,
18585
19249
  modelId: e.modelId,
18586
- maxContext: e.maxContext
19250
+ maxContext: e.maxContext,
19251
+ ...e.previousMaxContext !== void 0 ? { previousMaxContext: e.previousMaxContext } : {},
19252
+ ...e.source !== void 0 ? { source: e.source } : {},
19253
+ ...e.decreased !== void 0 ? { decreased: e.decreased } : {}
18587
19254
  })
18588
19255
  });
18589
19256
  });
@@ -19693,7 +20360,15 @@ var SpecsWebSocketHandler = class {
19693
20360
  this.clients.add(client);
19694
20361
  ws.on("close", () => this.clients.delete(client));
19695
20362
  ws.on("error", () => this.clients.delete(client));
19696
- void this.sendList(client);
20363
+ void this.sendList(client).catch((err) => {
20364
+ console.warn(
20365
+ JSON.stringify({
20366
+ level: "warn",
20367
+ event: "specs.initial_send_failed",
20368
+ message: err instanceof Error ? err.message : String(err)
20369
+ })
20370
+ );
20371
+ });
19697
20372
  }
19698
20373
  dispose() {
19699
20374
  this.clients.clear();
@@ -19907,15 +20582,17 @@ import {
19907
20582
 
19908
20583
  // src/server/discover-mailbox-bridge.ts
19909
20584
  import { spawn as spawn2 } from "node:child_process";
19910
- import { createRequire } from "node:module";
19911
20585
  import { existsSync } from "node:fs";
20586
+ import { createRequire } from "node:module";
19912
20587
  import { dirname as dirname7, join as join10 } from "node:path";
19913
- import { resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core/coordination";
20588
+ import {
20589
+ readLiveLock,
20590
+ resolveProjectDir as resolveProjectDir2
20591
+ } from "@wrongstack/core/coordination";
19914
20592
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
19915
- import { readLiveLock } from "@wrongstack/core/coordination";
19916
20593
  var MAILBOX_BRIDGE_BOOT_TIMEOUT_MS = 5e3;
19917
20594
  async function discoverMailboxBridgeForWebui(params) {
19918
- const mode = params.config?.features?.mailboxBridge ?? "auto";
20595
+ const mode = params.config?.features?.mailboxBridge ?? "off";
19919
20596
  if (mode === "off") return;
19920
20597
  const projectDir = resolveProjectDir2(params.projectRoot, wstackGlobalRoot());
19921
20598
  let result = await readLiveLock(projectDir);
@@ -20177,6 +20854,13 @@ var TerminalWebSocketHandler = class {
20177
20854
  this.send(ws, { type: "terminal.exit", payload: { id: payload.id, exitCode: -1 } });
20178
20855
  return;
20179
20856
  }
20857
+ if (this.sessions.get(ws) !== map) {
20858
+ this.logger.info?.(
20859
+ `terminal.create raced a disconnect (id=${payload.id}) \u2014 killing the orphan`
20860
+ );
20861
+ this.killPty(pty, "terminal create after disconnect");
20862
+ return;
20863
+ }
20180
20864
  map.set(payload.id, pty);
20181
20865
  this.logger.info?.(`terminal.create spawned (id=${payload.id}, pid=${pty.pid ?? "?"}) in ${cwd}`);
20182
20866
  pty.onData((data) => {
@@ -21602,6 +22286,15 @@ function createMessageDispatcher(opts) {
21602
22286
  msg
21603
22287
  ))
21604
22288
  return;
22289
+ if (await handleConnectionsServiceAction(ws, msg, {
22290
+ trustBoundary: deps2.trustBoundary,
22291
+ logger: deps2.logger,
22292
+ getProjectRoot: state.getProjectRoot,
22293
+ getIndexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
22294
+ send,
22295
+ backend: "standalone"
22296
+ }))
22297
+ return;
21605
22298
  if (await handleCodebaseIndexServerControl(ws, msg, {
21606
22299
  trustBoundary: deps2.trustBoundary,
21607
22300
  logger: deps2.logger,
@@ -22108,6 +22801,7 @@ async function createPreContextServices(input) {
22108
22801
  registry: toolRegistry,
22109
22802
  tier: normalizeTokenSavingTier(config.features.tokenSavingMode),
22110
22803
  memory: { enabled: config.features.memory, store: memoryStore },
22804
+ nextSteps: { enabled: config.tools?.nextsteps?.enabled === true },
22111
22805
  coordinationTools: [
22112
22806
  makeMailboxTool({ projectDir: wpaths.projectDir, events }),
22113
22807
  makeMailSendTool({ projectDir: wpaths.projectDir, events }),
@@ -23295,7 +23989,8 @@ async function startWebUI(opts = {}) {
23295
23989
  watcherMetricsRef
23296
23990
  );
23297
23991
  httpServer.listen(httpPort, wsHost, () => {
23298
- console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}`);
23992
+ const tokenQuery = accessToken ? `/?token=${encodeURIComponent(accessToken)}` : "";
23993
+ console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}${tokenQuery}`);
23299
23994
  const extraUrls = formatExternalAccessUrls({
23300
23995
  bindHost: wsHost,
23301
23996
  port: httpPort,
@@ -23317,8 +24012,11 @@ async function startWebUI(opts = {}) {
23317
24012
  (req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
23318
24013
  );
23319
24014
  companionServer.on("error", (err) => {
23320
- if (err.code !== "EAFNOSUPPORT" && err.code !== "EADDRNOTAVAIL" && err.code !== "EADDRINUSE") {
23321
- throw err;
24015
+ const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
24016
+ if (!expected) {
24017
+ console.warn(
24018
+ `[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
24019
+ );
23322
24020
  }
23323
24021
  });
23324
24022
  companionServer.listen(httpPort, companion, () => {
@@ -23560,24 +24258,23 @@ async function startWebUI(opts = {}) {
23560
24258
  clients,
23561
24259
  pendingConfirms,
23562
24260
  onSecurityRejection: (ev) => {
23563
- try {
23564
- void mailbox.send({
23565
- from: context.agentId,
23566
- to: "*",
23567
- type: "note",
23568
- audience: "leaders",
23569
- subject: `Security rejection: ${ev.issueCode}`,
23570
- body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
24261
+ void mailbox.send({
24262
+ from: context.agentId,
24263
+ to: "*",
24264
+ type: "note",
24265
+ audience: "leaders",
24266
+ subject: `Security rejection: ${ev.issueCode}`,
24267
+ body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
23571
24268
 
23572
24269
  connectionId: ${ev.connectionId ?? "?"}
23573
24270
  sessionId: ${ev.sessionId ?? "?"}
23574
24271
  agentId: ${ev.agentId ?? "?"}
23575
24272
  projectRoot: ${ev.projectRoot ?? "?"}`,
23576
- priority: "high",
23577
- senderSessionId: session.id
23578
- });
23579
- } catch {
23580
- }
24273
+ priority: "high",
24274
+ senderSessionId: session.id
24275
+ }).catch((err) => {
24276
+ console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
24277
+ });
23581
24278
  },
23582
24279
  goalHandler,
23583
24280
  specsHandler,