@wrongstack/webui-server 0.300.0 → 0.302.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.
- package/dist/index.js +457 -181
- package/dist/protocol/client-conversation.d.ts +1 -1
- package/dist/protocol/index.js +4 -0
- package/dist/protocol/registry.d.ts +2 -2
- package/dist/protocol/server-conversation.d.ts +1 -1
- package/dist/protocol/version.d.ts +2 -0
- package/dist/server/connections-health-route.d.ts +10 -0
- package/dist/server/conversation-routes.d.ts +1 -0
- package/dist/server/discover-mailbox-bridge.d.ts +5 -0
- package/dist/server/embedded-lifecycle.d.ts +17 -2
- package/dist/server/entry.js +834 -137
- package/dist/server/goal-ws-handler.d.ts +12 -0
- package/dist/server/index.d.ts +2 -2
- package/dist/server/instance-registry.d.ts +51 -2
- package/dist/server/kanban-broadcast.d.ts +37 -0
- package/dist/server/lifecycle.d.ts +6 -0
- package/dist/server/pref-helpers.d.ts +1 -1
- package/package.json +11 -11
package/dist/server/entry.js
CHANGED
|
@@ -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((resolve16) => {
|
|
6381
|
+
const sock = net.createConnection(endpoint);
|
|
6382
|
+
const timer = setTimeout(() => {
|
|
6383
|
+
sock.destroy();
|
|
6384
|
+
resolve16(false);
|
|
6385
|
+
}, 500);
|
|
6386
|
+
timer.unref?.();
|
|
6387
|
+
sock.once("connect", () => {
|
|
6388
|
+
clearTimeout(timer);
|
|
6389
|
+
sock.destroy();
|
|
6390
|
+
resolve16(true);
|
|
6391
|
+
});
|
|
6392
|
+
sock.once("error", () => {
|
|
6393
|
+
clearTimeout(timer);
|
|
6394
|
+
sock.destroy();
|
|
6395
|
+
resolve16(false);
|
|
6396
|
+
});
|
|
6397
|
+
});
|
|
6398
|
+
}
|
|
6399
|
+
async function waitForShutdown(probe) {
|
|
6400
|
+
if (!probe) {
|
|
6401
|
+
await new Promise((resolve16) => setTimeout(resolve16, 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((resolve16) => setTimeout(resolve16, 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 {
|
|
@@ -5960,9 +6572,9 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
5960
6572
|
const cwd = projectRoot || void 0;
|
|
5961
6573
|
try {
|
|
5962
6574
|
const { execFile: ef } = await import("node:child_process");
|
|
5963
|
-
const git = (args) => new Promise((
|
|
6575
|
+
const git = (args) => new Promise((resolve16) => {
|
|
5964
6576
|
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
5965
|
-
|
|
6577
|
+
resolve16(err ? "" : stdout.trim());
|
|
5966
6578
|
});
|
|
5967
6579
|
});
|
|
5968
6580
|
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
@@ -5988,12 +6600,12 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
5988
6600
|
function makeGit(cwd) {
|
|
5989
6601
|
return async (args) => {
|
|
5990
6602
|
const { execFile: ef } = await import("node:child_process");
|
|
5991
|
-
return new Promise((
|
|
6603
|
+
return new Promise((resolve16) => {
|
|
5992
6604
|
ef(
|
|
5993
6605
|
"git",
|
|
5994
6606
|
args,
|
|
5995
6607
|
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
5996
|
-
(err, stdout) =>
|
|
6608
|
+
(err, stdout) => resolve16(err ? "" : stdout)
|
|
5997
6609
|
);
|
|
5998
6610
|
});
|
|
5999
6611
|
};
|
|
@@ -6158,7 +6770,7 @@ import { execFile } from "node:child_process";
|
|
|
6158
6770
|
var GIT_TIMEOUT_MS = 1e4;
|
|
6159
6771
|
var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
6160
6772
|
function gitStdout(cwd, args) {
|
|
6161
|
-
return new Promise((
|
|
6773
|
+
return new Promise((resolve16) => {
|
|
6162
6774
|
execFile(
|
|
6163
6775
|
"git",
|
|
6164
6776
|
[...args],
|
|
@@ -6169,7 +6781,7 @@ function gitStdout(cwd, args) {
|
|
|
6169
6781
|
timeout: GIT_TIMEOUT_MS,
|
|
6170
6782
|
maxBuffer: GIT_MAX_OUTPUT_BYTES
|
|
6171
6783
|
},
|
|
6172
|
-
(error2, stdout) =>
|
|
6784
|
+
(error2, stdout) => resolve16(error2 ? null : stdout)
|
|
6173
6785
|
);
|
|
6174
6786
|
});
|
|
6175
6787
|
}
|
|
@@ -6460,14 +7072,14 @@ var GoalWebSocketHandler = class {
|
|
|
6460
7072
|
const cwd = env?.cwd ?? this.projectRoot;
|
|
6461
7073
|
try {
|
|
6462
7074
|
const { execFile: execFile2 } = await import("node:child_process");
|
|
6463
|
-
const result = await new Promise((
|
|
7075
|
+
const result = await new Promise((resolve16) => {
|
|
6464
7076
|
const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
6465
7077
|
execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
|
|
6466
7078
|
if (err && err.code === "ENOENT") {
|
|
6467
|
-
|
|
7079
|
+
resolve16("[verify] tsc not found \u2014 skipping");
|
|
6468
7080
|
return;
|
|
6469
7081
|
}
|
|
6470
|
-
|
|
7082
|
+
resolve16(stdout + stderr);
|
|
6471
7083
|
});
|
|
6472
7084
|
});
|
|
6473
7085
|
if (result.includes("[verify]") || result.trim().length === 0) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
7363
|
+
if (this.graph) this.persistDetached(this.graph);
|
|
6734
7364
|
this.broadcastState();
|
|
6735
7365
|
}
|
|
6736
7366
|
async handleTaskStatusChange(taskId, status) {
|
|
@@ -7187,7 +7817,7 @@ function pushEvent(event) {
|
|
|
7187
7817
|
}
|
|
7188
7818
|
}
|
|
7189
7819
|
function parseBody(req) {
|
|
7190
|
-
return new Promise((
|
|
7820
|
+
return new Promise((resolve16, reject) => {
|
|
7191
7821
|
let body = "";
|
|
7192
7822
|
let bodyBytes = 0;
|
|
7193
7823
|
let tooLarge = false;
|
|
@@ -7207,7 +7837,7 @@ function parseBody(req) {
|
|
|
7207
7837
|
return;
|
|
7208
7838
|
}
|
|
7209
7839
|
try {
|
|
7210
|
-
|
|
7840
|
+
resolve16(JSON.parse(body));
|
|
7211
7841
|
} catch {
|
|
7212
7842
|
reject(new Error("Invalid JSON"));
|
|
7213
7843
|
}
|
|
@@ -7294,7 +7924,7 @@ import * as path10 from "node:path";
|
|
|
7294
7924
|
import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
|
|
7295
7925
|
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
7296
7926
|
function readJsonBody(req) {
|
|
7297
|
-
return new Promise((
|
|
7927
|
+
return new Promise((resolve16, reject) => {
|
|
7298
7928
|
const chunks = [];
|
|
7299
7929
|
let total = 0;
|
|
7300
7930
|
req.on("data", (chunk) => {
|
|
@@ -7306,7 +7936,7 @@ function readJsonBody(req) {
|
|
|
7306
7936
|
}
|
|
7307
7937
|
chunks.push(chunk);
|
|
7308
7938
|
});
|
|
7309
|
-
req.on("end", () =>
|
|
7939
|
+
req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
|
|
7310
7940
|
req.on("error", (err) => reject(err));
|
|
7311
7941
|
});
|
|
7312
7942
|
}
|
|
@@ -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
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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
|
);
|
|
@@ -7738,7 +8382,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
7738
8382
|
}
|
|
7739
8383
|
}
|
|
7740
8384
|
function readJsonBody2(req) {
|
|
7741
|
-
return new Promise((
|
|
8385
|
+
return new Promise((resolve16, reject) => {
|
|
7742
8386
|
const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
7743
8387
|
if (contentType !== "application/json") {
|
|
7744
8388
|
reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
|
|
@@ -7754,7 +8398,7 @@ function readJsonBody2(req) {
|
|
|
7754
8398
|
});
|
|
7755
8399
|
req.on("end", () => {
|
|
7756
8400
|
try {
|
|
7757
|
-
|
|
8401
|
+
resolve16(data ? JSON.parse(data) : {});
|
|
7758
8402
|
} catch (err) {
|
|
7759
8403
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
7760
8404
|
}
|
|
@@ -8030,14 +8674,14 @@ async function readJsonBody3(res, req) {
|
|
|
8030
8674
|
});
|
|
8031
8675
|
return null;
|
|
8032
8676
|
}
|
|
8033
|
-
return new Promise((
|
|
8677
|
+
return new Promise((resolve16) => {
|
|
8034
8678
|
let data = "";
|
|
8035
8679
|
let failed = false;
|
|
8036
8680
|
const fail2 = (message) => {
|
|
8037
8681
|
if (failed) return;
|
|
8038
8682
|
failed = true;
|
|
8039
8683
|
sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
|
|
8040
|
-
|
|
8684
|
+
resolve16(null);
|
|
8041
8685
|
};
|
|
8042
8686
|
req.on("data", (chunk) => {
|
|
8043
8687
|
if (failed) return;
|
|
@@ -8050,7 +8694,7 @@ async function readJsonBody3(res, req) {
|
|
|
8050
8694
|
req.on("end", () => {
|
|
8051
8695
|
if (failed) return;
|
|
8052
8696
|
try {
|
|
8053
|
-
|
|
8697
|
+
resolve16(data.trim().length === 0 ? {} : JSON.parse(data));
|
|
8054
8698
|
} catch {
|
|
8055
8699
|
fail2("Request body is not valid JSON");
|
|
8056
8700
|
}
|
|
@@ -8418,7 +9062,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
|
|
|
8418
9062
|
}
|
|
8419
9063
|
|
|
8420
9064
|
// src/server/techstack-handlers.ts
|
|
8421
|
-
import { randomUUID as
|
|
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 =
|
|
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 } });
|
|
@@ -8889,7 +9533,7 @@ function strictDecodeParam(segment, res) {
|
|
|
8889
9533
|
function createHttpServer(opts) {
|
|
8890
9534
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8891
9535
|
const distDir = path13.resolve(opts.distDir);
|
|
8892
|
-
const requireAccessToken =
|
|
9536
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
8893
9537
|
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
8894
9538
|
const trustedHostnames = (() => {
|
|
8895
9539
|
const names = [...opts.allowedHostnames ?? []];
|
|
@@ -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)) {
|
|
@@ -9435,9 +10079,9 @@ function createHttpServer(opts) {
|
|
|
9435
10079
|
}
|
|
9436
10080
|
|
|
9437
10081
|
// src/server/instance-registry.ts
|
|
10082
|
+
import * as fs11 from "node:fs/promises";
|
|
9438
10083
|
import * as os from "node:os";
|
|
9439
10084
|
import * as path14 from "node:path";
|
|
9440
|
-
import * as fs11 from "node:fs/promises";
|
|
9441
10085
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
9442
10086
|
function defaultBaseDir() {
|
|
9443
10087
|
return path14.join(os.homedir(), ".wrongstack");
|
|
@@ -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?.(
|
|
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
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
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
|
-
|
|
11133
|
-
|
|
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())
|
|
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,19 +13500,19 @@ function createModelOperations(context) {
|
|
|
12856
13500
|
}
|
|
12857
13501
|
|
|
12858
13502
|
// src/server/port-utils.ts
|
|
12859
|
-
import * as
|
|
13503
|
+
import * as net2 from "node:net";
|
|
12860
13504
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
12861
13505
|
function isPortFree(host, port) {
|
|
12862
|
-
return new Promise((
|
|
12863
|
-
const srv =
|
|
12864
|
-
srv.once("error", () =>
|
|
13506
|
+
return new Promise((resolve16) => {
|
|
13507
|
+
const srv = net2.createServer();
|
|
13508
|
+
srv.once("error", () => resolve16(false));
|
|
12865
13509
|
srv.once("listening", () => {
|
|
12866
|
-
srv.close(() =>
|
|
13510
|
+
srv.close(() => resolve16(true));
|
|
12867
13511
|
});
|
|
12868
13512
|
try {
|
|
12869
13513
|
srv.listen(port, host);
|
|
12870
13514
|
} catch {
|
|
12871
|
-
|
|
13515
|
+
resolve16(false);
|
|
12872
13516
|
}
|
|
12873
13517
|
});
|
|
12874
13518
|
}
|
|
@@ -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
|
|
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:
|
|
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 {
|
|
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 ?? "
|
|
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);
|
|
@@ -20047,7 +20724,7 @@ function findWorkspaceCliEntry(projectRoot) {
|
|
|
20047
20724
|
return null;
|
|
20048
20725
|
}
|
|
20049
20726
|
function sleep(ms) {
|
|
20050
|
-
return new Promise((
|
|
20727
|
+
return new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
20051
20728
|
}
|
|
20052
20729
|
|
|
20053
20730
|
// src/server/terminal-ws-handler.ts
|
|
@@ -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) => {
|
|
@@ -20283,7 +20967,7 @@ function clampDim(value, fallback) {
|
|
|
20283
20967
|
}
|
|
20284
20968
|
|
|
20285
20969
|
// src/server/worktree-ws-handler.ts
|
|
20286
|
-
import { join as join11, resolve as
|
|
20970
|
+
import { join as join11, resolve as resolve13, sep as sep5 } from "node:path";
|
|
20287
20971
|
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
20288
20972
|
import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
|
|
20289
20973
|
import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
|
|
@@ -20368,11 +21052,11 @@ var WorktreeWebSocketHandler = class {
|
|
|
20368
21052
|
// ── orphan management ─────────────────────────────────────────────────────
|
|
20369
21053
|
/** Absolute managed-worktrees root for this project. */
|
|
20370
21054
|
worktreesRoot() {
|
|
20371
|
-
return
|
|
21055
|
+
return resolve13(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
|
|
20372
21056
|
}
|
|
20373
21057
|
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
20374
21058
|
underRoot(dir) {
|
|
20375
|
-
const abs =
|
|
21059
|
+
const abs = resolve13(dir);
|
|
20376
21060
|
const root = this.worktreesRoot();
|
|
20377
21061
|
return abs !== root && abs.startsWith(root + sep5);
|
|
20378
21062
|
}
|
|
@@ -20596,7 +21280,7 @@ var WorktreeWebSocketHandler = class {
|
|
|
20596
21280
|
}
|
|
20597
21281
|
const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
|
|
20598
21282
|
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
20599
|
-
const summary = await wt.diffSummary(
|
|
21283
|
+
const summary = await wt.diffSummary(resolve13(dir), base);
|
|
20600
21284
|
this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
|
|
20601
21285
|
}
|
|
20602
21286
|
// ── internals ───────────────────────────────────────────────────────────
|
|
@@ -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 }),
|
|
@@ -23216,7 +23910,7 @@ async function startWebUI(opts = {}) {
|
|
|
23216
23910
|
if (events.listenerCount("tool.confirm_needed") === 0) {
|
|
23217
23911
|
throw new Error("No permission confirmation surface is connected");
|
|
23218
23912
|
}
|
|
23219
|
-
const decision = await new Promise((
|
|
23913
|
+
const decision = await new Promise((resolve16) => {
|
|
23220
23914
|
events.emit("tool.confirm_needed", {
|
|
23221
23915
|
sessionId: context.session.id,
|
|
23222
23916
|
tool: confirmTool,
|
|
@@ -23226,7 +23920,7 @@ async function startWebUI(opts = {}) {
|
|
|
23226
23920
|
decisionSource: pending.decisionSource,
|
|
23227
23921
|
riskTier: pending.riskTier,
|
|
23228
23922
|
boundaryReason: pending.boundaryReason,
|
|
23229
|
-
resolve:
|
|
23923
|
+
resolve: resolve16
|
|
23230
23924
|
});
|
|
23231
23925
|
});
|
|
23232
23926
|
const rule = { tool: "language_package", pattern: pending.suggestedPattern };
|
|
@@ -23295,7 +23989,8 @@ async function startWebUI(opts = {}) {
|
|
|
23295
23989
|
watcherMetricsRef
|
|
23296
23990
|
);
|
|
23297
23991
|
httpServer.listen(httpPort, wsHost, () => {
|
|
23298
|
-
|
|
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
|
-
|
|
23321
|
-
|
|
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
|
-
|
|
23564
|
-
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
|
|
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
|
-
|
|
23577
|
-
|
|
23578
|
-
|
|
23579
|
-
|
|
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,
|