@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/index.js
CHANGED
|
@@ -48,6 +48,7 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
48
48
|
"chime",
|
|
49
49
|
"confirmExit",
|
|
50
50
|
"nextPrediction",
|
|
51
|
+
"nextStepsTool",
|
|
51
52
|
"titleAnimation",
|
|
52
53
|
"enhanceEnabled",
|
|
53
54
|
"featureMcp",
|
|
@@ -171,6 +172,7 @@ var ENUM_PREF_KEYS = {
|
|
|
171
172
|
fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"]),
|
|
172
173
|
// Chimera autoFix + auto-review cascade threshold
|
|
173
174
|
chimeraAutoFix: /* @__PURE__ */ new Set(["off", "ask", "auto"]),
|
|
175
|
+
autoReviewModelSelection: /* @__PURE__ */ new Set(["round-robin", "random"]),
|
|
174
176
|
autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
|
|
175
177
|
fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"]),
|
|
176
178
|
showAgentSwarmPanel: /* @__PURE__ */ new Set(["bottom", "sidebar", "off"])
|
|
@@ -4316,6 +4318,9 @@ async function handleConversationRoute(ws, msg, handlers) {
|
|
|
4316
4318
|
case "user_message":
|
|
4317
4319
|
await handlers.userMessage(ws, msg);
|
|
4318
4320
|
return true;
|
|
4321
|
+
case "topic.advice":
|
|
4322
|
+
await handlers.topicAdvice(ws, msg);
|
|
4323
|
+
return true;
|
|
4319
4324
|
case "abort":
|
|
4320
4325
|
await handlers.abort(ws, msg);
|
|
4321
4326
|
return true;
|
|
@@ -4331,6 +4336,7 @@ async function handleConversationRoute(ws, msg, handlers) {
|
|
|
4331
4336
|
}
|
|
4332
4337
|
|
|
4333
4338
|
// src/server/conversation-operations.ts
|
|
4339
|
+
import { startFreshTopicContext, TopicShiftAdvisor } from "@wrongstack/core/execution";
|
|
4334
4340
|
import {
|
|
4335
4341
|
buildUserContentBlocks,
|
|
4336
4342
|
IncomingImageError,
|
|
@@ -4347,6 +4353,7 @@ function requestedSessionId(msg) {
|
|
|
4347
4353
|
return payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0;
|
|
4348
4354
|
}
|
|
4349
4355
|
function createConversationOperations(ctx) {
|
|
4356
|
+
const topicShiftAdvisor = new TopicShiftAdvisor();
|
|
4350
4357
|
const sessionPayload2 = (payload) => {
|
|
4351
4358
|
const provided = payload["sessionId"];
|
|
4352
4359
|
const sessionId = typeof provided === "string" && provided.length > 0 ? provided : ctx.getSessionId();
|
|
@@ -4367,6 +4374,38 @@ function createConversationOperations(ctx) {
|
|
|
4367
4374
|
return false;
|
|
4368
4375
|
};
|
|
4369
4376
|
return {
|
|
4377
|
+
topicAdvice: async (ws, msg) => {
|
|
4378
|
+
if (!ensureCurrentSession(ws, msg, "topic.advice")) return;
|
|
4379
|
+
const payload = msg.payload ?? {};
|
|
4380
|
+
if (typeof payload.requestId !== "string" || typeof payload.prompt !== "string") {
|
|
4381
|
+
ctx.send(ws, {
|
|
4382
|
+
type: "topic.advice_result",
|
|
4383
|
+
payload: sessionPayload2({
|
|
4384
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : "",
|
|
4385
|
+
suggestNewContext: false,
|
|
4386
|
+
confidence: 0,
|
|
4387
|
+
reason: "Invalid topic advice request.",
|
|
4388
|
+
source: "local"
|
|
4389
|
+
})
|
|
4390
|
+
});
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
const agent = ctx.getAgent();
|
|
4394
|
+
const configuredMax = agent.ctx.meta["effectiveMaxContext"];
|
|
4395
|
+
const maxContext = typeof configuredMax === "number" ? configuredMax : agent.ctx.provider.capabilities.maxContext;
|
|
4396
|
+
const advice = await topicShiftAdvisor.advise({
|
|
4397
|
+
prompt: payload.prompt,
|
|
4398
|
+
messages: agent.ctx.messages,
|
|
4399
|
+
provider: agent.ctx.provider,
|
|
4400
|
+
model: agent.ctx.model,
|
|
4401
|
+
contextTokens: agent.ctx.lastRequestTokens,
|
|
4402
|
+
maxContext
|
|
4403
|
+
});
|
|
4404
|
+
ctx.send(ws, {
|
|
4405
|
+
type: "topic.advice_result",
|
|
4406
|
+
payload: sessionPayload2({ requestId: payload.requestId, ...advice })
|
|
4407
|
+
});
|
|
4408
|
+
},
|
|
4370
4409
|
userMessage: async (ws, msg) => {
|
|
4371
4410
|
if (!ensureCurrentSession(ws, msg, "user_message")) return;
|
|
4372
4411
|
const payload = msg.payload ?? {};
|
|
@@ -4384,6 +4423,7 @@ function createConversationOperations(ctx) {
|
|
|
4384
4423
|
const originSessionId = ctx.getSessionId();
|
|
4385
4424
|
try {
|
|
4386
4425
|
const agent = ctx.getAgent();
|
|
4426
|
+
if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
|
|
4387
4427
|
const content = typeof payload.content === "string" ? payload.content : "";
|
|
4388
4428
|
let input = content;
|
|
4389
4429
|
const imageBlocks = parseIncomingImages(payload.images, payload.imageBase64);
|
|
@@ -5512,6 +5552,39 @@ import {
|
|
|
5512
5552
|
isKanbanServerAvailable
|
|
5513
5553
|
} from "@wrongstack/kanban";
|
|
5514
5554
|
import * as net from "node:net";
|
|
5555
|
+
|
|
5556
|
+
// src/server/privileged-actions.ts
|
|
5557
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5558
|
+
import {
|
|
5559
|
+
isTrustDecisionAllowed
|
|
5560
|
+
} from "@wrongstack/core/security";
|
|
5561
|
+
async function authorizeWebUIAction(boundary, action, logger) {
|
|
5562
|
+
const request = {
|
|
5563
|
+
version: 1,
|
|
5564
|
+
requestId: randomUUID2(),
|
|
5565
|
+
actor: {
|
|
5566
|
+
kind: "remote-client",
|
|
5567
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
5568
|
+
},
|
|
5569
|
+
surface: "webui",
|
|
5570
|
+
capability: action.capability,
|
|
5571
|
+
subject: action.subject,
|
|
5572
|
+
risk: action.risk,
|
|
5573
|
+
scope: {
|
|
5574
|
+
...action.cwd ? { cwd: action.cwd } : {},
|
|
5575
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
5576
|
+
},
|
|
5577
|
+
authContext: { method: "session" },
|
|
5578
|
+
...action.metadata ? { metadata: action.metadata } : {}
|
|
5579
|
+
};
|
|
5580
|
+
const decision = await boundary.evaluate(request);
|
|
5581
|
+
logger?.debug?.(
|
|
5582
|
+
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
5583
|
+
);
|
|
5584
|
+
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
5585
|
+
}
|
|
5586
|
+
|
|
5587
|
+
// src/server/connections-health-route.ts
|
|
5515
5588
|
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5516
5589
|
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5517
5590
|
import {
|
|
@@ -5917,6 +5990,42 @@ async function handleConnectionsServiceAction(ws, message, context) {
|
|
|
5917
5990
|
return true;
|
|
5918
5991
|
}
|
|
5919
5992
|
const action = rawAction;
|
|
5993
|
+
if (!context.trustBoundary) {
|
|
5994
|
+
context.send(ws, {
|
|
5995
|
+
type: "connections.service_action_result",
|
|
5996
|
+
payload: {
|
|
5997
|
+
serviceId,
|
|
5998
|
+
action,
|
|
5999
|
+
success: false,
|
|
6000
|
+
message: "Service control is unavailable: no policy authority is configured."
|
|
6001
|
+
}
|
|
6002
|
+
});
|
|
6003
|
+
return true;
|
|
6004
|
+
}
|
|
6005
|
+
const projectRootForAuth = context.getProjectRoot();
|
|
6006
|
+
const authorization = await authorizeWebUIAction(
|
|
6007
|
+
context.trustBoundary,
|
|
6008
|
+
{
|
|
6009
|
+
capability: `connections.service.${action}`,
|
|
6010
|
+
subject: { kind: "process", id: `${serviceId}@${projectRootForAuth}` },
|
|
6011
|
+
risk: "elevated",
|
|
6012
|
+
cwd: projectRootForAuth,
|
|
6013
|
+
metadata: { transport: "websocket", serviceId, action }
|
|
6014
|
+
},
|
|
6015
|
+
context.logger
|
|
6016
|
+
);
|
|
6017
|
+
if (!authorization.allowed) {
|
|
6018
|
+
context.send(ws, {
|
|
6019
|
+
type: "connections.service_action_result",
|
|
6020
|
+
payload: {
|
|
6021
|
+
serviceId,
|
|
6022
|
+
action,
|
|
6023
|
+
success: false,
|
|
6024
|
+
message: authorization.reason ?? "Refused by policy."
|
|
6025
|
+
}
|
|
6026
|
+
});
|
|
6027
|
+
return true;
|
|
6028
|
+
}
|
|
5920
6029
|
if (serviceId === "webui") {
|
|
5921
6030
|
context.send(ws, {
|
|
5922
6031
|
type: "connections.service_action_result",
|
|
@@ -6354,28 +6463,28 @@ async function restartMailboxServer(projectRoot) {
|
|
|
6354
6463
|
var RESTART_POLL_INTERVAL_MS = 250;
|
|
6355
6464
|
var RESTART_DEADLINE_MS = 3e3;
|
|
6356
6465
|
function isEndpointAlive(endpoint) {
|
|
6357
|
-
return new Promise((
|
|
6466
|
+
return new Promise((resolve17) => {
|
|
6358
6467
|
const sock = net.createConnection(endpoint);
|
|
6359
6468
|
const timer = setTimeout(() => {
|
|
6360
6469
|
sock.destroy();
|
|
6361
|
-
|
|
6470
|
+
resolve17(false);
|
|
6362
6471
|
}, 500);
|
|
6363
6472
|
timer.unref?.();
|
|
6364
6473
|
sock.once("connect", () => {
|
|
6365
6474
|
clearTimeout(timer);
|
|
6366
6475
|
sock.destroy();
|
|
6367
|
-
|
|
6476
|
+
resolve17(true);
|
|
6368
6477
|
});
|
|
6369
6478
|
sock.once("error", () => {
|
|
6370
6479
|
clearTimeout(timer);
|
|
6371
6480
|
sock.destroy();
|
|
6372
|
-
|
|
6481
|
+
resolve17(false);
|
|
6373
6482
|
});
|
|
6374
6483
|
});
|
|
6375
6484
|
}
|
|
6376
6485
|
async function waitForShutdown(probe) {
|
|
6377
6486
|
if (!probe) {
|
|
6378
|
-
await new Promise((
|
|
6487
|
+
await new Promise((resolve17) => setTimeout(resolve17, RESTART_POLL_INTERVAL_MS));
|
|
6379
6488
|
return;
|
|
6380
6489
|
}
|
|
6381
6490
|
const deadline = Date.now() + RESTART_DEADLINE_MS;
|
|
@@ -6386,7 +6495,7 @@ async function waitForShutdown(probe) {
|
|
|
6386
6495
|
} catch {
|
|
6387
6496
|
return;
|
|
6388
6497
|
}
|
|
6389
|
-
await new Promise((
|
|
6498
|
+
await new Promise((resolve17) => setTimeout(resolve17, RESTART_POLL_INTERVAL_MS));
|
|
6390
6499
|
}
|
|
6391
6500
|
}
|
|
6392
6501
|
function failureService(id, label, required, mode, error2, latencyMs) {
|
|
@@ -6549,9 +6658,9 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
6549
6658
|
const cwd = projectRoot || void 0;
|
|
6550
6659
|
try {
|
|
6551
6660
|
const { execFile: ef } = await import("node:child_process");
|
|
6552
|
-
const git = (args) => new Promise((
|
|
6661
|
+
const git = (args) => new Promise((resolve17) => {
|
|
6553
6662
|
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
6554
|
-
|
|
6663
|
+
resolve17(err ? "" : stdout.trim());
|
|
6555
6664
|
});
|
|
6556
6665
|
});
|
|
6557
6666
|
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
@@ -6577,12 +6686,12 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
6577
6686
|
function makeGit(cwd) {
|
|
6578
6687
|
return async (args) => {
|
|
6579
6688
|
const { execFile: ef } = await import("node:child_process");
|
|
6580
|
-
return new Promise((
|
|
6689
|
+
return new Promise((resolve17) => {
|
|
6581
6690
|
ef(
|
|
6582
6691
|
"git",
|
|
6583
6692
|
args,
|
|
6584
6693
|
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
6585
|
-
(err, stdout) =>
|
|
6694
|
+
(err, stdout) => resolve17(err ? "" : stdout)
|
|
6586
6695
|
);
|
|
6587
6696
|
});
|
|
6588
6697
|
};
|
|
@@ -6747,7 +6856,7 @@ import { execFile } from "node:child_process";
|
|
|
6747
6856
|
var GIT_TIMEOUT_MS = 1e4;
|
|
6748
6857
|
var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
6749
6858
|
function gitStdout(cwd, args) {
|
|
6750
|
-
return new Promise((
|
|
6859
|
+
return new Promise((resolve17) => {
|
|
6751
6860
|
execFile(
|
|
6752
6861
|
"git",
|
|
6753
6862
|
[...args],
|
|
@@ -6758,7 +6867,7 @@ function gitStdout(cwd, args) {
|
|
|
6758
6867
|
timeout: GIT_TIMEOUT_MS,
|
|
6759
6868
|
maxBuffer: GIT_MAX_OUTPUT_BYTES
|
|
6760
6869
|
},
|
|
6761
|
-
(error2, stdout) =>
|
|
6870
|
+
(error2, stdout) => resolve17(error2 ? null : stdout)
|
|
6762
6871
|
);
|
|
6763
6872
|
});
|
|
6764
6873
|
}
|
|
@@ -7049,14 +7158,14 @@ var GoalWebSocketHandler = class {
|
|
|
7049
7158
|
const cwd = env?.cwd ?? this.projectRoot;
|
|
7050
7159
|
try {
|
|
7051
7160
|
const { execFile: execFile2 } = await import("node:child_process");
|
|
7052
|
-
const result = await new Promise((
|
|
7161
|
+
const result = await new Promise((resolve17) => {
|
|
7053
7162
|
const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
7054
7163
|
execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
|
|
7055
7164
|
if (err && err.code === "ENOENT") {
|
|
7056
|
-
|
|
7165
|
+
resolve17("[verify] tsc not found \u2014 skipping");
|
|
7057
7166
|
return;
|
|
7058
7167
|
}
|
|
7059
|
-
|
|
7168
|
+
resolve17(stdout + stderr);
|
|
7060
7169
|
});
|
|
7061
7170
|
});
|
|
7062
7171
|
if (result.includes("[verify]") || result.trim().length === 0) {
|
|
@@ -7097,12 +7206,12 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
|
|
|
7097
7206
|
...maybeVerify,
|
|
7098
7207
|
onPhaseComplete: (phase) => {
|
|
7099
7208
|
this.logger.info(`[Goal] Phase completed: ${phase.name}`);
|
|
7100
|
-
|
|
7209
|
+
this.persistDetached(graph);
|
|
7101
7210
|
this.broadcastState();
|
|
7102
7211
|
},
|
|
7103
7212
|
onPhaseFail: (phase, error2) => {
|
|
7104
7213
|
this.logger.error(`[Goal] Phase failed: ${phase.name} \u2014 ${error2.message}`);
|
|
7105
|
-
|
|
7214
|
+
this.persistDetached(graph);
|
|
7106
7215
|
this.broadcastState();
|
|
7107
7216
|
}
|
|
7108
7217
|
},
|
|
@@ -7119,7 +7228,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
|
|
|
7119
7228
|
this.broadcastState();
|
|
7120
7229
|
void this.orchestrator.start().then(() => {
|
|
7121
7230
|
this.orchestrator?.stop();
|
|
7122
|
-
|
|
7231
|
+
this.persistDetached(graph);
|
|
7123
7232
|
this.stopBroadcast();
|
|
7124
7233
|
const failed = graph.failedPhaseIds.length > 0;
|
|
7125
7234
|
this.broadcast(
|
|
@@ -7317,9 +7426,27 @@ ${result_.finalText.slice(0, 2e3)}`
|
|
|
7317
7426
|
this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
|
|
7318
7427
|
}
|
|
7319
7428
|
}
|
|
7429
|
+
/**
|
|
7430
|
+
* Fire-and-forget persist.
|
|
7431
|
+
*
|
|
7432
|
+
* Every detached `store.save()` used to be a bare `void`, so a rejection
|
|
7433
|
+
* became an unhandled rejection and — under Node 22's default
|
|
7434
|
+
* `--unhandled-rejections=throw` — killed the process mid-run. On Windows an
|
|
7435
|
+
* AV scanner or indexer holding the `.wrongstack/phases/<id>.json` rename
|
|
7436
|
+
* target for a few hundred ms is enough (EPERM from `atomicWrite`), and in
|
|
7437
|
+
* `--webui` mode that takes the CLI session down with it. `handleStop` at
|
|
7438
|
+
* `:549` already had the `.catch`; these call sites did not.
|
|
7439
|
+
*/
|
|
7440
|
+
persistDetached(graph) {
|
|
7441
|
+
void this.store.save(graph).catch((err) => {
|
|
7442
|
+
this.logger.warn(
|
|
7443
|
+
`[Goal] Failed to persist phase graph: ${err instanceof Error ? err.message : String(err)}`
|
|
7444
|
+
);
|
|
7445
|
+
});
|
|
7446
|
+
}
|
|
7320
7447
|
/** Persist + broadcast after an interactive board mutation. */
|
|
7321
7448
|
afterBoardMutation() {
|
|
7322
|
-
if (this.graph)
|
|
7449
|
+
if (this.graph) this.persistDetached(this.graph);
|
|
7323
7450
|
this.broadcastState();
|
|
7324
7451
|
}
|
|
7325
7452
|
async handleTaskStatusChange(taskId, status) {
|
|
@@ -7776,7 +7903,7 @@ function pushEvent(event) {
|
|
|
7776
7903
|
}
|
|
7777
7904
|
}
|
|
7778
7905
|
function parseBody(req) {
|
|
7779
|
-
return new Promise((
|
|
7906
|
+
return new Promise((resolve17, reject) => {
|
|
7780
7907
|
let body = "";
|
|
7781
7908
|
let bodyBytes = 0;
|
|
7782
7909
|
let tooLarge = false;
|
|
@@ -7796,7 +7923,7 @@ function parseBody(req) {
|
|
|
7796
7923
|
return;
|
|
7797
7924
|
}
|
|
7798
7925
|
try {
|
|
7799
|
-
|
|
7926
|
+
resolve17(JSON.parse(body));
|
|
7800
7927
|
} catch {
|
|
7801
7928
|
reject(new Error("Invalid JSON"));
|
|
7802
7929
|
}
|
|
@@ -7891,7 +8018,7 @@ import * as path10 from "node:path";
|
|
|
7891
8018
|
import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
|
|
7892
8019
|
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
7893
8020
|
function readJsonBody(req) {
|
|
7894
|
-
return new Promise((
|
|
8021
|
+
return new Promise((resolve17, reject) => {
|
|
7895
8022
|
const chunks = [];
|
|
7896
8023
|
let total = 0;
|
|
7897
8024
|
req.on("data", (chunk) => {
|
|
@@ -7903,7 +8030,7 @@ function readJsonBody(req) {
|
|
|
7903
8030
|
}
|
|
7904
8031
|
chunks.push(chunk);
|
|
7905
8032
|
});
|
|
7906
|
-
req.on("end", () =>
|
|
8033
|
+
req.on("end", () => resolve17(Buffer.concat(chunks).toString("utf8")));
|
|
7907
8034
|
req.on("error", (err) => reject(err));
|
|
7908
8035
|
});
|
|
7909
8036
|
}
|
|
@@ -8311,12 +8438,21 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8311
8438
|
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8312
8439
|
const store = new DefaultSessionStore4({ dir: paths.projectSessions });
|
|
8313
8440
|
const reader = new DefaultSessionReader2({ store });
|
|
8314
|
-
const
|
|
8441
|
+
const RING = Math.max(limit * 4, 2e3);
|
|
8442
|
+
const ring = [];
|
|
8443
|
+
let totalRaw = 0;
|
|
8444
|
+
let dropped = false;
|
|
8315
8445
|
for await (const ev of reader.replay(sessionId)) {
|
|
8316
8446
|
const mapped = mapWatchEntry(ev);
|
|
8317
|
-
if (mapped)
|
|
8447
|
+
if (!mapped) continue;
|
|
8448
|
+
totalRaw += 1;
|
|
8449
|
+
ring.push(mapped);
|
|
8450
|
+
if (ring.length > RING) {
|
|
8451
|
+
ring.shift();
|
|
8452
|
+
dropped = true;
|
|
8453
|
+
}
|
|
8318
8454
|
}
|
|
8319
|
-
const all = correlateToolEvents(
|
|
8455
|
+
const all = correlateToolEvents(ring);
|
|
8320
8456
|
const tail2 = all.slice(-limit);
|
|
8321
8457
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
8322
8458
|
res.end(
|
|
@@ -8325,7 +8461,12 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8325
8461
|
status: entry.status,
|
|
8326
8462
|
clientType: entry.clientType,
|
|
8327
8463
|
projectName: entry.projectName,
|
|
8328
|
-
|
|
8464
|
+
// Exact when the whole session fit in the ring (the previous
|
|
8465
|
+
// behaviour). Past that, correlation never ran over the dropped
|
|
8466
|
+
// prefix, so report the raw event count — an upper bound — and say so
|
|
8467
|
+
// rather than silently understating the session's size.
|
|
8468
|
+
total: dropped ? totalRaw : all.length,
|
|
8469
|
+
...dropped ? { truncated: true } : {},
|
|
8329
8470
|
entries: tail2
|
|
8330
8471
|
})
|
|
8331
8472
|
);
|
|
@@ -8335,7 +8476,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8335
8476
|
}
|
|
8336
8477
|
}
|
|
8337
8478
|
function readJsonBody2(req) {
|
|
8338
|
-
return new Promise((
|
|
8479
|
+
return new Promise((resolve17, reject) => {
|
|
8339
8480
|
const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
8340
8481
|
if (contentType !== "application/json") {
|
|
8341
8482
|
reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
|
|
@@ -8351,7 +8492,7 @@ function readJsonBody2(req) {
|
|
|
8351
8492
|
});
|
|
8352
8493
|
req.on("end", () => {
|
|
8353
8494
|
try {
|
|
8354
|
-
|
|
8495
|
+
resolve17(data ? JSON.parse(data) : {});
|
|
8355
8496
|
} catch (err) {
|
|
8356
8497
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
8357
8498
|
}
|
|
@@ -8627,14 +8768,14 @@ async function readJsonBody3(res, req) {
|
|
|
8627
8768
|
});
|
|
8628
8769
|
return null;
|
|
8629
8770
|
}
|
|
8630
|
-
return new Promise((
|
|
8771
|
+
return new Promise((resolve17) => {
|
|
8631
8772
|
let data = "";
|
|
8632
8773
|
let failed = false;
|
|
8633
8774
|
const fail2 = (message) => {
|
|
8634
8775
|
if (failed) return;
|
|
8635
8776
|
failed = true;
|
|
8636
8777
|
sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
|
|
8637
|
-
|
|
8778
|
+
resolve17(null);
|
|
8638
8779
|
};
|
|
8639
8780
|
req.on("data", (chunk) => {
|
|
8640
8781
|
if (failed) return;
|
|
@@ -8647,7 +8788,7 @@ async function readJsonBody3(res, req) {
|
|
|
8647
8788
|
req.on("end", () => {
|
|
8648
8789
|
if (failed) return;
|
|
8649
8790
|
try {
|
|
8650
|
-
|
|
8791
|
+
resolve17(data.trim().length === 0 ? {} : JSON.parse(data));
|
|
8651
8792
|
} catch {
|
|
8652
8793
|
fail2("Request body is not valid JSON");
|
|
8653
8794
|
}
|
|
@@ -9015,7 +9156,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
|
|
|
9015
9156
|
}
|
|
9016
9157
|
|
|
9017
9158
|
// src/server/techstack-handlers.ts
|
|
9018
|
-
import { randomUUID as
|
|
9159
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
9019
9160
|
var DEEP_DIVE_TIMEOUT_MS = 6e4;
|
|
9020
9161
|
function sendJson3(res, status, data) {
|
|
9021
9162
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -9056,7 +9197,7 @@ function requireJobDeps(res, deps2) {
|
|
|
9056
9197
|
}
|
|
9057
9198
|
function startJob(res, deps2, kind) {
|
|
9058
9199
|
if (!requireJobDeps(res, deps2)) return;
|
|
9059
|
-
const jobId =
|
|
9200
|
+
const jobId = randomUUID3();
|
|
9060
9201
|
const controller = new AbortController();
|
|
9061
9202
|
deps2.runningJobs?.set(jobId, controller);
|
|
9062
9203
|
deps2.emit?.({ type: "techstack.job.started", payload: { jobId, kind } });
|
|
@@ -9486,7 +9627,7 @@ function strictDecodeParam(segment, res) {
|
|
|
9486
9627
|
function createHttpServer(opts) {
|
|
9487
9628
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
9488
9629
|
const distDir = path13.resolve(opts.distDir);
|
|
9489
|
-
const requireAccessToken =
|
|
9630
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
9490
9631
|
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
9491
9632
|
const trustedHostnames = (() => {
|
|
9492
9633
|
const names = [...opts.allowedHostnames ?? []];
|
|
@@ -9522,7 +9663,7 @@ function createHttpServer(opts) {
|
|
|
9522
9663
|
res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
|
|
9523
9664
|
return;
|
|
9524
9665
|
}
|
|
9525
|
-
const providedAccessToken = requestToken(req, url);
|
|
9666
|
+
const providedAccessToken = requestToken(req, url, { allowQuery: true });
|
|
9526
9667
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
9527
9668
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
9528
9669
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
@@ -10032,10 +10173,78 @@ function createHttpServer(opts) {
|
|
|
10032
10173
|
}
|
|
10033
10174
|
|
|
10034
10175
|
// src/server/instance-registry.ts
|
|
10176
|
+
import * as fs11 from "node:fs/promises";
|
|
10035
10177
|
import * as os from "node:os";
|
|
10036
10178
|
import * as path14 from "node:path";
|
|
10037
|
-
import * as fs11 from "node:fs/promises";
|
|
10038
10179
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
10180
|
+
function normalizeRoot(root) {
|
|
10181
|
+
const resolved = path14.resolve(root);
|
|
10182
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
10183
|
+
}
|
|
10184
|
+
function isLiveSessionStatus(status) {
|
|
10185
|
+
return status === "active" || status === "idle";
|
|
10186
|
+
}
|
|
10187
|
+
function instanceRole(instance) {
|
|
10188
|
+
return instance.role ?? "standalone";
|
|
10189
|
+
}
|
|
10190
|
+
function resolveAttachability(input) {
|
|
10191
|
+
const { session, instance } = input;
|
|
10192
|
+
if (!isLiveSessionStatus(session.status)) {
|
|
10193
|
+
return { attachable: false, degradedReason: "session-not-live" };
|
|
10194
|
+
}
|
|
10195
|
+
if (!instance) {
|
|
10196
|
+
return { attachable: false, degradedReason: "live-session-no-webui-endpoint" };
|
|
10197
|
+
}
|
|
10198
|
+
if (instance.pid !== session.pid) {
|
|
10199
|
+
return { attachable: false, degradedReason: "endpoint-owner-mismatch" };
|
|
10200
|
+
}
|
|
10201
|
+
if (!instance.sessionId) {
|
|
10202
|
+
return { attachable: false, instance, degradedReason: "endpoint-missing-session-id" };
|
|
10203
|
+
}
|
|
10204
|
+
if (instance.sessionId !== session.sessionId) {
|
|
10205
|
+
return { attachable: false, instance, degradedReason: "endpoint-session-mismatch" };
|
|
10206
|
+
}
|
|
10207
|
+
if (instanceRole(instance) !== "session-child") {
|
|
10208
|
+
return { attachable: false, instance, degradedReason: "endpoint-not-session-child" };
|
|
10209
|
+
}
|
|
10210
|
+
if (instance.attachable === false) {
|
|
10211
|
+
return { attachable: false, instance, degradedReason: "endpoint-not-attachable" };
|
|
10212
|
+
}
|
|
10213
|
+
return {
|
|
10214
|
+
attachable: true,
|
|
10215
|
+
endpoint: {
|
|
10216
|
+
host: instance.host,
|
|
10217
|
+
httpPort: instance.httpPort,
|
|
10218
|
+
url: instance.url,
|
|
10219
|
+
...instance.authToken ? { authToken: instance.authToken } : {}
|
|
10220
|
+
}
|
|
10221
|
+
};
|
|
10222
|
+
}
|
|
10223
|
+
function joinSessionRegistryWithWebUIInstances(input) {
|
|
10224
|
+
const targetRoot = input.projectRoot ? normalizeRoot(input.projectRoot) : void 0;
|
|
10225
|
+
const sessions = input.sessions.filter((session) => {
|
|
10226
|
+
if (input.projectSlug && session.projectSlug !== input.projectSlug) return false;
|
|
10227
|
+
if (targetRoot && normalizeRoot(session.projectRoot) !== targetRoot) return false;
|
|
10228
|
+
return true;
|
|
10229
|
+
});
|
|
10230
|
+
return sessions.map((session) => {
|
|
10231
|
+
const instance = input.instances.find((candidate) => candidate.sessionId === session.sessionId) ?? input.instances.find(
|
|
10232
|
+
(candidate) => candidate.pid === session.pid && normalizeRoot(candidate.projectRoot) === normalizeRoot(session.projectRoot)
|
|
10233
|
+
);
|
|
10234
|
+
const resolved = resolveAttachability({ session, instance });
|
|
10235
|
+
return {
|
|
10236
|
+
sessionId: session.sessionId,
|
|
10237
|
+
projectRoot: session.projectRoot,
|
|
10238
|
+
workingDir: session.workingDir,
|
|
10239
|
+
sessionPid: session.pid,
|
|
10240
|
+
status: session.status,
|
|
10241
|
+
...instance ? { instance } : {},
|
|
10242
|
+
...resolved.endpoint ? { endpoint: resolved.endpoint } : {},
|
|
10243
|
+
attachable: resolved.attachable,
|
|
10244
|
+
...resolved.degradedReason ? { degradedReason: resolved.degradedReason } : {}
|
|
10245
|
+
};
|
|
10246
|
+
});
|
|
10247
|
+
}
|
|
10039
10248
|
function defaultBaseDir() {
|
|
10040
10249
|
return path14.join(os.homedir(), ".wrongstack");
|
|
10041
10250
|
}
|
|
@@ -10383,6 +10592,27 @@ import {
|
|
|
10383
10592
|
getServerKanbanStore
|
|
10384
10593
|
} from "@wrongstack/kanban";
|
|
10385
10594
|
import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
|
|
10595
|
+
|
|
10596
|
+
// src/server/kanban-broadcast.ts
|
|
10597
|
+
function kanbanBoardMessage(board) {
|
|
10598
|
+
return { type: "kanban.get", payload: { success: true, data: { board } } };
|
|
10599
|
+
}
|
|
10600
|
+
function kanbanListMessage(boards) {
|
|
10601
|
+
return { type: "kanban.list", payload: { success: true, data: boards } };
|
|
10602
|
+
}
|
|
10603
|
+
function kanbanDeletedMessage(boardId) {
|
|
10604
|
+
return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
|
|
10605
|
+
}
|
|
10606
|
+
async function publishKanbanBoard(broadcast2, board, listBoards6) {
|
|
10607
|
+
broadcast2(kanbanBoardMessage(board));
|
|
10608
|
+
if (!listBoards6) return;
|
|
10609
|
+
try {
|
|
10610
|
+
broadcast2(kanbanListMessage(await listBoards6()));
|
|
10611
|
+
} catch {
|
|
10612
|
+
}
|
|
10613
|
+
}
|
|
10614
|
+
|
|
10615
|
+
// src/server/kanban-dispatch.ts
|
|
10386
10616
|
function parseResolvedDispatchRoute(summary) {
|
|
10387
10617
|
const tags = summary.match(/Spawned subagent\s+\S+\s+\((.*?)\)\s+for task/i)?.[1];
|
|
10388
10618
|
if (!tags) return {};
|
|
@@ -10513,10 +10743,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
10513
10743
|
payload: { success: true, data: { boardId: board.id, task: completedTask } }
|
|
10514
10744
|
});
|
|
10515
10745
|
if (completedBoard) {
|
|
10516
|
-
ctx.broadcast?.(
|
|
10517
|
-
type: "kanban.get",
|
|
10518
|
-
payload: { success: true, data: { board: completedBoard } }
|
|
10519
|
-
});
|
|
10746
|
+
ctx.broadcast?.(kanbanBoardMessage(completedBoard));
|
|
10520
10747
|
}
|
|
10521
10748
|
ctx.broadcast?.({
|
|
10522
10749
|
type: "kanban.list",
|
|
@@ -10540,7 +10767,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
10540
10767
|
payload: { success: true, data: { boardId: board.id, task: runningTask } }
|
|
10541
10768
|
});
|
|
10542
10769
|
if (started?.board) {
|
|
10543
|
-
ctx.broadcast?.(
|
|
10770
|
+
ctx.broadcast?.(kanbanBoardMessage(started.board));
|
|
10544
10771
|
}
|
|
10545
10772
|
reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
|
|
10546
10773
|
} catch (error2) {
|
|
@@ -10783,14 +11010,11 @@ async function handleDecompositionResolution(ws, type, payload, ctx) {
|
|
|
10783
11010
|
type: "kanban.decomposition.applied",
|
|
10784
11011
|
payload: { success: true, data: { board: resolved.board } }
|
|
10785
11012
|
});
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
type: "kanban.list",
|
|
10792
|
-
payload: { success: true, data: await listBoards(ctx.projectRoot) }
|
|
10793
|
-
});
|
|
11013
|
+
await publishKanbanBoard(
|
|
11014
|
+
(message) => ctx.broadcast?.(message),
|
|
11015
|
+
resolved.board,
|
|
11016
|
+
() => listBoards(ctx.projectRoot)
|
|
11017
|
+
);
|
|
10794
11018
|
} else {
|
|
10795
11019
|
ctx.broadcast?.({
|
|
10796
11020
|
type: "kanban.decomposition.resolved",
|
|
@@ -10823,10 +11047,7 @@ async function handleTaskVerification(ws, type, payload, ctx) {
|
|
|
10823
11047
|
payload: { success: true, data: { boardId, task: freshTask } }
|
|
10824
11048
|
});
|
|
10825
11049
|
if (persisted) {
|
|
10826
|
-
ctx.broadcast?.(
|
|
10827
|
-
type: "kanban.get",
|
|
10828
|
-
payload: { success: true, data: { board: persisted } }
|
|
10829
|
-
});
|
|
11050
|
+
ctx.broadcast?.(kanbanBoardMessage(persisted));
|
|
10830
11051
|
}
|
|
10831
11052
|
} catch (err) {
|
|
10832
11053
|
ctx.broadcast?.({
|
|
@@ -11749,10 +11970,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11749
11970
|
let connectionCount = 0;
|
|
11750
11971
|
const broadcastDeleted = (boardId) => {
|
|
11751
11972
|
knownRevisions.delete(boardId);
|
|
11752
|
-
broadcastMessage(
|
|
11753
|
-
type: "kanban.delete",
|
|
11754
|
-
payload: { success: true, data: { removed: true, boardId } }
|
|
11755
|
-
});
|
|
11973
|
+
broadcastMessage(kanbanDeletedMessage(boardId));
|
|
11756
11974
|
};
|
|
11757
11975
|
const broadcastBoard = async (boardId) => {
|
|
11758
11976
|
const board = await store.getBoard(boardId);
|
|
@@ -11761,10 +11979,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11761
11979
|
return;
|
|
11762
11980
|
}
|
|
11763
11981
|
knownRevisions.set(boardId, board.updatedAt);
|
|
11764
|
-
broadcastMessage(
|
|
11765
|
-
type: "kanban.get",
|
|
11766
|
-
payload: { success: true, data: { board } }
|
|
11767
|
-
});
|
|
11982
|
+
broadcastMessage(kanbanBoardMessage(board));
|
|
11768
11983
|
};
|
|
11769
11984
|
const reconcileAfterConnect = async () => {
|
|
11770
11985
|
const summaries = await store.listBoards();
|
|
@@ -11783,20 +11998,36 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11783
11998
|
}
|
|
11784
11999
|
}
|
|
11785
12000
|
};
|
|
11786
|
-
|
|
12001
|
+
const COALESCE_MS = 300;
|
|
12002
|
+
const pendingBroadcasts = /* @__PURE__ */ new Map();
|
|
12003
|
+
const scheduleBroadcast = (boardId) => {
|
|
12004
|
+
if (pendingBroadcasts.has(boardId)) return;
|
|
12005
|
+
const timer = setTimeout(() => {
|
|
12006
|
+
pendingBroadcasts.delete(boardId);
|
|
12007
|
+
void broadcastBoard(boardId).catch(() => {
|
|
12008
|
+
});
|
|
12009
|
+
}, COALESCE_MS);
|
|
12010
|
+
timer.unref?.();
|
|
12011
|
+
pendingBroadcasts.set(boardId, timer);
|
|
12012
|
+
};
|
|
12013
|
+
const unsubscribe = bridgeKanbanSupervisor(
|
|
11787
12014
|
projectRoot,
|
|
11788
12015
|
async (event) => {
|
|
12016
|
+
const family = event.event?.split(".")[0];
|
|
12017
|
+
if (family !== "board" && family !== "task" && family !== "column") return;
|
|
11789
12018
|
const evData = event.data;
|
|
11790
12019
|
const boardId = evData?.boardId;
|
|
11791
12020
|
if (!boardId) return;
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
12021
|
+
if (event.event === "board.deleted") {
|
|
12022
|
+
const timer = pendingBroadcasts.get(boardId);
|
|
12023
|
+
if (timer) {
|
|
12024
|
+
clearTimeout(timer);
|
|
12025
|
+
pendingBroadcasts.delete(boardId);
|
|
11796
12026
|
}
|
|
11797
|
-
|
|
11798
|
-
|
|
12027
|
+
broadcastDeleted(boardId);
|
|
12028
|
+
return;
|
|
11799
12029
|
}
|
|
12030
|
+
scheduleBroadcast(boardId);
|
|
11800
12031
|
},
|
|
11801
12032
|
{
|
|
11802
12033
|
autoReconnect: true,
|
|
@@ -11804,6 +12035,11 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11804
12035
|
onConnected: reconcileAfterConnect
|
|
11805
12036
|
}
|
|
11806
12037
|
);
|
|
12038
|
+
return () => {
|
|
12039
|
+
for (const timer of pendingBroadcasts.values()) clearTimeout(timer);
|
|
12040
|
+
pendingBroadcasts.clear();
|
|
12041
|
+
unsubscribe();
|
|
12042
|
+
};
|
|
11807
12043
|
}
|
|
11808
12044
|
|
|
11809
12045
|
// src/server/kanban-board-watcher.ts
|
|
@@ -11825,7 +12061,13 @@ function createShutdown(res) {
|
|
|
11825
12061
|
} catch (e) {
|
|
11826
12062
|
log(`[WebUI] Error closing session: ${e instanceof Error ? e.message : String(e)}`);
|
|
11827
12063
|
}
|
|
11828
|
-
for (const ws of res.clients())
|
|
12064
|
+
for (const ws of res.clients()) {
|
|
12065
|
+
try {
|
|
12066
|
+
ws.close();
|
|
12067
|
+
ws.terminate?.();
|
|
12068
|
+
} catch {
|
|
12069
|
+
}
|
|
12070
|
+
}
|
|
11829
12071
|
for (const server of res.servers) server?.close();
|
|
11830
12072
|
if (res.onShutdown) {
|
|
11831
12073
|
try {
|
|
@@ -12262,39 +12504,6 @@ import {
|
|
|
12262
12504
|
restartMcp,
|
|
12263
12505
|
updateMcp
|
|
12264
12506
|
} from "@wrongstack/mcp";
|
|
12265
|
-
|
|
12266
|
-
// src/server/privileged-actions.ts
|
|
12267
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
12268
|
-
import {
|
|
12269
|
-
isTrustDecisionAllowed
|
|
12270
|
-
} from "@wrongstack/core/security";
|
|
12271
|
-
async function authorizeWebUIAction(boundary, action, logger) {
|
|
12272
|
-
const request = {
|
|
12273
|
-
version: 1,
|
|
12274
|
-
requestId: randomUUID3(),
|
|
12275
|
-
actor: {
|
|
12276
|
-
kind: "remote-client",
|
|
12277
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
12278
|
-
},
|
|
12279
|
-
surface: "webui",
|
|
12280
|
-
capability: action.capability,
|
|
12281
|
-
subject: action.subject,
|
|
12282
|
-
risk: action.risk,
|
|
12283
|
-
scope: {
|
|
12284
|
-
...action.cwd ? { cwd: action.cwd } : {},
|
|
12285
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
12286
|
-
},
|
|
12287
|
-
authContext: { method: "session" },
|
|
12288
|
-
...action.metadata ? { metadata: action.metadata } : {}
|
|
12289
|
-
};
|
|
12290
|
-
const decision = await boundary.evaluate(request);
|
|
12291
|
-
logger?.debug?.(
|
|
12292
|
-
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
12293
|
-
);
|
|
12294
|
-
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
12295
|
-
}
|
|
12296
|
-
|
|
12297
|
-
// src/server/mcp-handlers.ts
|
|
12298
12507
|
async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
|
|
12299
12508
|
if (!trustBoundary) return true;
|
|
12300
12509
|
const authorization = await authorizeWebUIAction(trustBoundary, {
|
|
@@ -13582,16 +13791,16 @@ function getSurfaceDefaultPorts(surface) {
|
|
|
13582
13791
|
return { http: SURFACE_DEFAULT_PORTS[surface].http };
|
|
13583
13792
|
}
|
|
13584
13793
|
function isPortFree(host, port) {
|
|
13585
|
-
return new Promise((
|
|
13794
|
+
return new Promise((resolve17) => {
|
|
13586
13795
|
const srv = net2.createServer();
|
|
13587
|
-
srv.once("error", () =>
|
|
13796
|
+
srv.once("error", () => resolve17(false));
|
|
13588
13797
|
srv.once("listening", () => {
|
|
13589
|
-
srv.close(() =>
|
|
13798
|
+
srv.close(() => resolve17(true));
|
|
13590
13799
|
});
|
|
13591
13800
|
try {
|
|
13592
13801
|
srv.listen(port, host);
|
|
13593
13802
|
} catch {
|
|
13594
|
-
|
|
13803
|
+
resolve17(false);
|
|
13595
13804
|
}
|
|
13596
13805
|
});
|
|
13597
13806
|
}
|
|
@@ -13747,7 +13956,7 @@ async function startStaticServe(opts, deps2 = {}) {
|
|
|
13747
13956
|
return { server, port: opts.httpPort };
|
|
13748
13957
|
}
|
|
13749
13958
|
function runPnpmBuild(cwd, workspace, timeoutMs) {
|
|
13750
|
-
return new Promise((
|
|
13959
|
+
return new Promise((resolve17, reject) => {
|
|
13751
13960
|
const child = spawn2("pnpm", ["--filter", workspace, "build"], {
|
|
13752
13961
|
cwd,
|
|
13753
13962
|
shell: process.platform === "win32",
|
|
@@ -13765,7 +13974,7 @@ function runPnpmBuild(cwd, workspace, timeoutMs) {
|
|
|
13765
13974
|
});
|
|
13766
13975
|
child.once("close", (code) => {
|
|
13767
13976
|
clearTimeout(timer);
|
|
13768
|
-
if (code === 0)
|
|
13977
|
+
if (code === 0) resolve17();
|
|
13769
13978
|
else reject(new Error(`pnpm build exited with code ${String(code)}`));
|
|
13770
13979
|
});
|
|
13771
13980
|
});
|
|
@@ -13829,27 +14038,41 @@ function formatExternalAccessUrls(opts) {
|
|
|
13829
14038
|
}
|
|
13830
14039
|
|
|
13831
14040
|
// src/server/embedded-lifecycle.ts
|
|
13832
|
-
function registerWebuiInstance(p, deps2 = {}) {
|
|
14041
|
+
async function registerWebuiInstance(p, deps2 = {}) {
|
|
13833
14042
|
const register = deps2.registerFn ?? registerInstance;
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
projectRoot: p.projectRoot,
|
|
13841
|
-
projectName: path16.basename(p.projectRoot) || p.projectRoot,
|
|
13842
|
-
startedAt: p.startedAt,
|
|
13843
|
-
url: buildWebUIAccessUrl({
|
|
14043
|
+
try {
|
|
14044
|
+
await register(
|
|
14045
|
+
{
|
|
14046
|
+
pid: p.pid,
|
|
14047
|
+
surface: p.surface,
|
|
14048
|
+
httpPort: p.httpPort,
|
|
13844
14049
|
host: p.host,
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
|
|
13852
|
-
|
|
14050
|
+
projectRoot: p.projectRoot,
|
|
14051
|
+
projectName: path16.basename(p.projectRoot) || p.projectRoot,
|
|
14052
|
+
startedAt: p.startedAt,
|
|
14053
|
+
url: buildWebUIAccessUrl({
|
|
14054
|
+
host: p.host,
|
|
14055
|
+
port: p.httpPort,
|
|
14056
|
+
publicUrl: p.publicUrl
|
|
14057
|
+
}),
|
|
14058
|
+
...p.authToken ? { authToken: p.authToken } : {},
|
|
14059
|
+
...p.role ? { role: p.role } : {},
|
|
14060
|
+
...p.sessionId ? { sessionId: p.sessionId } : {},
|
|
14061
|
+
...p.parentPid !== void 0 ? { parentPid: p.parentPid } : {},
|
|
14062
|
+
...p.parentShellId ? { parentShellId: p.parentShellId } : {},
|
|
14063
|
+
...p.runtimeId ? { runtimeId: p.runtimeId } : {},
|
|
14064
|
+
...p.attachable !== void 0 ? { attachable: p.attachable } : {},
|
|
14065
|
+
...p.authToken ? { auth: { scheme: "registry-token", tokenPresent: true } } : {},
|
|
14066
|
+
...p.lastReadyAt ? { lastReadyAt: p.lastReadyAt } : {},
|
|
14067
|
+
...p.protocolVersion !== void 0 ? { protocolVersion: p.protocolVersion } : {},
|
|
14068
|
+
...p.capabilities ? { capabilities: p.capabilities } : {}
|
|
14069
|
+
},
|
|
14070
|
+
p.registryBaseDir
|
|
14071
|
+
);
|
|
14072
|
+
return true;
|
|
14073
|
+
} catch {
|
|
14074
|
+
return false;
|
|
14075
|
+
}
|
|
13853
14076
|
}
|
|
13854
14077
|
function announceWebuiReady(p) {
|
|
13855
14078
|
const log = p.log ?? ((m) => console.log(m));
|
|
@@ -13887,10 +14110,10 @@ async function runBounded(work, timeoutMs, label, debug) {
|
|
|
13887
14110
|
Promise.resolve().then(() => work()).catch((err) => {
|
|
13888
14111
|
debug(`[webui-server] ${label} failed: ${err}`);
|
|
13889
14112
|
}),
|
|
13890
|
-
new Promise((
|
|
14113
|
+
new Promise((resolve17) => {
|
|
13891
14114
|
timer = setTimeout(() => {
|
|
13892
14115
|
debug(`[webui-server] ${label} timed out after ${timeoutMs}ms`);
|
|
13893
|
-
|
|
14116
|
+
resolve17();
|
|
13894
14117
|
}, timeoutMs);
|
|
13895
14118
|
timer.unref?.();
|
|
13896
14119
|
})
|
|
@@ -13923,8 +14146,8 @@ function createWebuiShutdown(res) {
|
|
|
13923
14146
|
const unregistered = unregister(res.pid, res.registryBaseDir).catch(
|
|
13924
14147
|
(err) => debug(`[webui-server] unregister failed: ${err}`)
|
|
13925
14148
|
);
|
|
13926
|
-
await new Promise((
|
|
13927
|
-
res.wss.close(() =>
|
|
14149
|
+
await new Promise((resolve17) => {
|
|
14150
|
+
res.wss.close(() => resolve17());
|
|
13928
14151
|
});
|
|
13929
14152
|
await unregistered;
|
|
13930
14153
|
log("[WebUI] Server stopped");
|
|
@@ -14415,12 +14638,9 @@ function createKanbanRunMirror(deps2) {
|
|
|
14415
14638
|
}
|
|
14416
14639
|
async function publish(board) {
|
|
14417
14640
|
if (board) {
|
|
14418
|
-
broadcast2(
|
|
14641
|
+
broadcast2(kanbanBoardMessage(board));
|
|
14419
14642
|
}
|
|
14420
|
-
broadcast2(
|
|
14421
|
-
type: "kanban.list",
|
|
14422
|
-
payload: { success: true, data: await listBoards3(projectRoot) }
|
|
14423
|
-
});
|
|
14643
|
+
broadcast2(kanbanListMessage(await listBoards3(projectRoot)));
|
|
14424
14644
|
}
|
|
14425
14645
|
async function projectSdd(runId, snapshot) {
|
|
14426
14646
|
const k = mapKey("sdd", runId);
|
|
@@ -14823,14 +15043,11 @@ function createKanbanSupervisor(deps2) {
|
|
|
14823
15043
|
publish(snapshot);
|
|
14824
15044
|
const changedBoard = recovered?.board ?? gateSwept ?? reconciled?.board;
|
|
14825
15045
|
if (changedBoard) {
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
type: "kanban.list",
|
|
14832
|
-
payload: { success: true, data: await listBoards4(deps2.projectRoot) }
|
|
14833
|
-
});
|
|
15046
|
+
await publishKanbanBoard(
|
|
15047
|
+
deps2.broadcast,
|
|
15048
|
+
changedBoard,
|
|
15049
|
+
() => listBoards4(deps2.projectRoot)
|
|
15050
|
+
);
|
|
14834
15051
|
}
|
|
14835
15052
|
if (config.mode === "agentic" && anomalyCount > 0) {
|
|
14836
15053
|
await maybeRunAgent(board, config, health, snapshot);
|
|
@@ -15059,6 +15276,7 @@ function seedContextMeta(config, context) {
|
|
|
15059
15276
|
meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
|
|
15060
15277
|
meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
15061
15278
|
meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
15279
|
+
meta["nextStepsTool"] = config.tools?.nextsteps?.enabled === true;
|
|
15062
15280
|
meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
15063
15281
|
meta["fallbackBridge"] = config.fallbackBridge ?? "";
|
|
15064
15282
|
meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
|
|
@@ -15147,6 +15365,7 @@ function seedContextMeta(config, context) {
|
|
|
15147
15365
|
meta["autoReviewProvider"] = autoReviewExt?.["provider"] ?? "";
|
|
15148
15366
|
meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
|
|
15149
15367
|
meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
|
|
15368
|
+
meta["autoReviewModelSelection"] = autoReviewExt?.["modelSelection"] === "random" ? "random" : "round-robin";
|
|
15150
15369
|
meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
|
|
15151
15370
|
meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
|
|
15152
15371
|
meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
|
|
@@ -15184,6 +15403,7 @@ var PREF_KEYS = [
|
|
|
15184
15403
|
"chime",
|
|
15185
15404
|
"confirmExit",
|
|
15186
15405
|
"nextPrediction",
|
|
15406
|
+
"nextStepsTool",
|
|
15187
15407
|
"enhanceEnabled",
|
|
15188
15408
|
"enhanceDelayMs",
|
|
15189
15409
|
"enhanceLanguage",
|
|
@@ -15247,6 +15467,7 @@ var PREF_KEYS = [
|
|
|
15247
15467
|
"autoReviewProvider",
|
|
15248
15468
|
"autoReviewModel",
|
|
15249
15469
|
"autoReviewFallbackProfile",
|
|
15470
|
+
"autoReviewModelSelection",
|
|
15250
15471
|
"autoReviewFallbackModels",
|
|
15251
15472
|
"autoReviewDebounceMs",
|
|
15252
15473
|
"autoReviewMaxFilesPerBatch",
|
|
@@ -15460,6 +15681,11 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15460
15681
|
toolsCfg.maxIterations = payload["maxIterations"];
|
|
15461
15682
|
decrypted.tools = toolsCfg;
|
|
15462
15683
|
}
|
|
15684
|
+
if (typeof payload["nextStepsTool"] === "boolean") {
|
|
15685
|
+
const toolsCfg = decrypted.tools ?? {};
|
|
15686
|
+
toolsCfg.nextsteps = { enabled: payload["nextStepsTool"] };
|
|
15687
|
+
decrypted.tools = toolsCfg;
|
|
15688
|
+
}
|
|
15463
15689
|
const hqTouched = typeof payload["hqEnabled"] === "boolean" || typeof payload["hqUrl"] === "string" || typeof payload["hqToken"] === "string" || typeof payload["hqRawContent"] === "boolean";
|
|
15464
15690
|
if (hqTouched) {
|
|
15465
15691
|
const hqCfg = decrypted.hq ?? {};
|
|
@@ -15567,7 +15793,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15567
15793
|
ext["wstack-chimera"] = chimera;
|
|
15568
15794
|
decrypted.extensions = ext;
|
|
15569
15795
|
}
|
|
15570
|
-
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";
|
|
15796
|
+
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";
|
|
15571
15797
|
if (autoReviewTouched) {
|
|
15572
15798
|
const ext = decrypted.extensions ?? {};
|
|
15573
15799
|
const ar = ext["wstack-auto-review"] ?? {};
|
|
@@ -15584,6 +15810,9 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15584
15810
|
ar["fallbackProfile"] = payload["autoReviewFallbackProfile"];
|
|
15585
15811
|
}
|
|
15586
15812
|
}
|
|
15813
|
+
if (payload["autoReviewModelSelection"] === "round-robin" || payload["autoReviewModelSelection"] === "random") {
|
|
15814
|
+
ar["modelSelection"] = payload["autoReviewModelSelection"];
|
|
15815
|
+
}
|
|
15587
15816
|
if (typeof payload["autoReviewDebounceMs"] === "number" && payload["autoReviewDebounceMs"] >= 0) {
|
|
15588
15817
|
ar["debounceMs"] = payload["autoReviewDebounceMs"];
|
|
15589
15818
|
}
|
|
@@ -15952,6 +16181,7 @@ function createProjectHandlers(ctx) {
|
|
|
15952
16181
|
ctx.context.session = next;
|
|
15953
16182
|
ctx.context.state.replaceMessages([]);
|
|
15954
16183
|
ctx.context.state.replaceTodos([]);
|
|
16184
|
+
ctx.context.clearMemoryEvidence?.();
|
|
15955
16185
|
ctx.context.readFiles.clear();
|
|
15956
16186
|
ctx.context.fileMtimes.clear();
|
|
15957
16187
|
ctx.tokenCounter.reset();
|
|
@@ -15996,7 +16226,7 @@ function createProjectHandlers(ctx) {
|
|
|
15996
16226
|
}
|
|
15997
16227
|
|
|
15998
16228
|
// src/server/provider-handlers.ts
|
|
15999
|
-
import { resolveProviderModelList } from "@wrongstack/core/models";
|
|
16229
|
+
import { hasProviderCredential, resolveProviderModelList } from "@wrongstack/core/models";
|
|
16000
16230
|
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
16001
16231
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
16002
16232
|
import {
|
|
@@ -16303,7 +16533,7 @@ function createProviderOperations(deps2) {
|
|
|
16303
16533
|
}
|
|
16304
16534
|
try {
|
|
16305
16535
|
const providers = await deps2.modelsRegistry.listProviders();
|
|
16306
|
-
const
|
|
16536
|
+
const savedProviders = await loadConfigProviders();
|
|
16307
16537
|
sendMessage(ws, {
|
|
16308
16538
|
type: "provider.catalog",
|
|
16309
16539
|
payload: {
|
|
@@ -16314,7 +16544,7 @@ function createProviderOperations(deps2) {
|
|
|
16314
16544
|
apiBase: provider.apiBase,
|
|
16315
16545
|
envVars: provider.envVars,
|
|
16316
16546
|
modelCount: provider.models.length,
|
|
16317
|
-
hasApiKey:
|
|
16547
|
+
hasApiKey: hasProviderCredential(provider, { providers: savedProviders })
|
|
16318
16548
|
}))
|
|
16319
16549
|
}
|
|
16320
16550
|
});
|
|
@@ -16861,6 +17091,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16861
17091
|
"ping",
|
|
16862
17092
|
"user_message",
|
|
16863
17093
|
"tool.confirm_result",
|
|
17094
|
+
"topic.advice",
|
|
16864
17095
|
"completion.request",
|
|
16865
17096
|
"model.switch",
|
|
16866
17097
|
"model.refine",
|
|
@@ -17163,6 +17394,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
17163
17394
|
"tool.loop_detected",
|
|
17164
17395
|
"tool.progress",
|
|
17165
17396
|
"tool.started",
|
|
17397
|
+
"topic.advice_result",
|
|
17166
17398
|
"tools.list",
|
|
17167
17399
|
"trust.persisted"
|
|
17168
17400
|
];
|
|
@@ -17672,6 +17904,8 @@ var SURFACE_PROTOCOL_CAPABILITIES = [
|
|
|
17672
17904
|
"chronicle.metrics",
|
|
17673
17905
|
"chronicle.status",
|
|
17674
17906
|
"connections.health",
|
|
17907
|
+
/** Bounded topic-shift advice plus same-session provider-context boundaries. */
|
|
17908
|
+
"context.topic-boundary",
|
|
17675
17909
|
/** Interview resume/discard + lastAgentText/lastRunId continuity. */
|
|
17676
17910
|
"sdd.interview.continuity",
|
|
17677
17911
|
/** Launch multi-agent runs from a graph id or resolved spec id. */
|
|
@@ -18076,6 +18310,7 @@ function createSessionHandlers(ctx) {
|
|
|
18076
18310
|
await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
|
|
18077
18311
|
ctx.context.state.replaceTodos(todos);
|
|
18078
18312
|
resetContextAccounting();
|
|
18313
|
+
ctx.context.clearMemoryEvidence?.();
|
|
18079
18314
|
ctx.context.readFiles.clear();
|
|
18080
18315
|
ctx.context.fileMtimes.clear();
|
|
18081
18316
|
ctx.context.state.setMeta?.(
|
|
@@ -18123,6 +18358,7 @@ function createSessionHandlers(ctx) {
|
|
|
18123
18358
|
ctx.context.state.replaceMessages([]);
|
|
18124
18359
|
ctx.context.state.replaceTodos([]);
|
|
18125
18360
|
resetContextAccounting();
|
|
18361
|
+
ctx.context.clearMemoryEvidence?.();
|
|
18126
18362
|
ctx.context.readFiles.clear();
|
|
18127
18363
|
ctx.context.fileMtimes.clear();
|
|
18128
18364
|
ctx.tokenCounter.reset?.();
|
|
@@ -18137,6 +18373,7 @@ function createSessionHandlers(ctx) {
|
|
|
18137
18373
|
ctx.context.state.replaceMessages([]);
|
|
18138
18374
|
ctx.context.state.replaceTodos([]);
|
|
18139
18375
|
resetContextAccounting();
|
|
18376
|
+
ctx.context.clearMemoryEvidence?.();
|
|
18140
18377
|
ctx.context.readFiles.clear();
|
|
18141
18378
|
ctx.context.fileMtimes.clear();
|
|
18142
18379
|
ctx.tokenCounter.reset?.();
|
|
@@ -19922,6 +20159,7 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19922
20159
|
};
|
|
19923
20160
|
const guardedTypes = /* @__PURE__ */ new Set([
|
|
19924
20161
|
"user_message",
|
|
20162
|
+
"topic.advice",
|
|
19925
20163
|
"abort",
|
|
19926
20164
|
"tool.confirm_result",
|
|
19927
20165
|
"session.new",
|
|
@@ -20249,6 +20487,8 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
20249
20487
|
))
|
|
20250
20488
|
return;
|
|
20251
20489
|
if (await handleConnectionsServiceAction(ws, message, {
|
|
20490
|
+
trustBoundary: deps2.trustBoundary,
|
|
20491
|
+
logger: deps2.logger,
|
|
20252
20492
|
getProjectRoot: projectRoot,
|
|
20253
20493
|
getIndexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
|
|
20254
20494
|
send: send2,
|
|
@@ -20300,8 +20540,8 @@ function createConfigWriteLock() {
|
|
|
20300
20540
|
acquire() {
|
|
20301
20541
|
const prev = lock;
|
|
20302
20542
|
let release = () => void 0;
|
|
20303
|
-
lock = new Promise((
|
|
20304
|
-
release =
|
|
20543
|
+
lock = new Promise((resolve17) => {
|
|
20544
|
+
release = resolve17;
|
|
20305
20545
|
});
|
|
20306
20546
|
return { prev, release };
|
|
20307
20547
|
}
|
|
@@ -21235,7 +21475,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
21235
21475
|
const on = (event, listener) => events.on(event, listener);
|
|
21236
21476
|
return on("client.status", async (e) => {
|
|
21237
21477
|
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
21238
|
-
if (wpaths?.projectStatus) {
|
|
21478
|
+
if (wpaths?.projectStatus && e.projectHash !== "unknown") {
|
|
21239
21479
|
try {
|
|
21240
21480
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
21241
21481
|
const dir = path24.dirname(statusFile);
|
|
@@ -21410,7 +21650,10 @@ function registerSetupEventsProviderHandlers({
|
|
|
21410
21650
|
sessionId: e.sessionId,
|
|
21411
21651
|
providerId: e.providerId,
|
|
21412
21652
|
modelId: e.modelId,
|
|
21413
|
-
maxContext: e.maxContext
|
|
21653
|
+
maxContext: e.maxContext,
|
|
21654
|
+
...e.previousMaxContext !== void 0 ? { previousMaxContext: e.previousMaxContext } : {},
|
|
21655
|
+
...e.source !== void 0 ? { source: e.source } : {},
|
|
21656
|
+
...e.decreased !== void 0 ? { decreased: e.decreased } : {}
|
|
21414
21657
|
})
|
|
21415
21658
|
});
|
|
21416
21659
|
});
|
|
@@ -22520,7 +22763,15 @@ var SpecsWebSocketHandler = class {
|
|
|
22520
22763
|
this.clients.add(client);
|
|
22521
22764
|
ws.on("close", () => this.clients.delete(client));
|
|
22522
22765
|
ws.on("error", () => this.clients.delete(client));
|
|
22523
|
-
void this.sendList(client)
|
|
22766
|
+
void this.sendList(client).catch((err) => {
|
|
22767
|
+
console.warn(
|
|
22768
|
+
JSON.stringify({
|
|
22769
|
+
level: "warn",
|
|
22770
|
+
event: "specs.initial_send_failed",
|
|
22771
|
+
message: err instanceof Error ? err.message : String(err)
|
|
22772
|
+
})
|
|
22773
|
+
);
|
|
22774
|
+
});
|
|
22524
22775
|
}
|
|
22525
22776
|
dispose() {
|
|
22526
22777
|
this.clients.clear();
|
|
@@ -22734,15 +22985,17 @@ import {
|
|
|
22734
22985
|
|
|
22735
22986
|
// src/server/discover-mailbox-bridge.ts
|
|
22736
22987
|
import { spawn as spawn4 } from "node:child_process";
|
|
22737
|
-
import { createRequire } from "node:module";
|
|
22738
22988
|
import { existsSync as existsSync2 } from "node:fs";
|
|
22989
|
+
import { createRequire } from "node:module";
|
|
22739
22990
|
import { dirname as dirname10, join as join13 } from "node:path";
|
|
22740
|
-
import {
|
|
22991
|
+
import {
|
|
22992
|
+
readLiveLock,
|
|
22993
|
+
resolveProjectDir as resolveProjectDir3
|
|
22994
|
+
} from "@wrongstack/core/coordination";
|
|
22741
22995
|
import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
|
|
22742
|
-
import { readLiveLock } from "@wrongstack/core/coordination";
|
|
22743
22996
|
var MAILBOX_BRIDGE_BOOT_TIMEOUT_MS = 5e3;
|
|
22744
22997
|
async function discoverMailboxBridgeForWebui(params) {
|
|
22745
|
-
const mode = params.config?.features?.mailboxBridge ?? "
|
|
22998
|
+
const mode = params.config?.features?.mailboxBridge ?? "off";
|
|
22746
22999
|
if (mode === "off") return;
|
|
22747
23000
|
const projectDir = resolveProjectDir3(params.projectRoot, wstackGlobalRoot3());
|
|
22748
23001
|
let result = await readLiveLock(projectDir);
|
|
@@ -22874,7 +23127,7 @@ function findWorkspaceCliEntry(projectRoot) {
|
|
|
22874
23127
|
return null;
|
|
22875
23128
|
}
|
|
22876
23129
|
function sleep(ms) {
|
|
22877
|
-
return new Promise((
|
|
23130
|
+
return new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
22878
23131
|
}
|
|
22879
23132
|
|
|
22880
23133
|
// src/server/terminal-ws-handler.ts
|
|
@@ -23004,6 +23257,13 @@ var TerminalWebSocketHandler = class {
|
|
|
23004
23257
|
this.send(ws, { type: "terminal.exit", payload: { id: payload.id, exitCode: -1 } });
|
|
23005
23258
|
return;
|
|
23006
23259
|
}
|
|
23260
|
+
if (this.sessions.get(ws) !== map) {
|
|
23261
|
+
this.logger.info?.(
|
|
23262
|
+
`terminal.create raced a disconnect (id=${payload.id}) \u2014 killing the orphan`
|
|
23263
|
+
);
|
|
23264
|
+
this.killPty(pty, "terminal create after disconnect");
|
|
23265
|
+
return;
|
|
23266
|
+
}
|
|
23007
23267
|
map.set(payload.id, pty);
|
|
23008
23268
|
this.logger.info?.(`terminal.create spawned (id=${payload.id}, pid=${pty.pid ?? "?"}) in ${cwd}`);
|
|
23009
23269
|
pty.onData((data) => {
|
|
@@ -23110,7 +23370,7 @@ function clampDim(value, fallback) {
|
|
|
23110
23370
|
}
|
|
23111
23371
|
|
|
23112
23372
|
// src/server/worktree-ws-handler.ts
|
|
23113
|
-
import { join as join14, resolve as
|
|
23373
|
+
import { join as join14, resolve as resolve14, sep as sep5 } from "node:path";
|
|
23114
23374
|
import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
|
|
23115
23375
|
import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
|
|
23116
23376
|
import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
|
|
@@ -23195,11 +23455,11 @@ var WorktreeWebSocketHandler = class {
|
|
|
23195
23455
|
// ── orphan management ─────────────────────────────────────────────────────
|
|
23196
23456
|
/** Absolute managed-worktrees root for this project. */
|
|
23197
23457
|
worktreesRoot() {
|
|
23198
|
-
return
|
|
23458
|
+
return resolve14(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
|
|
23199
23459
|
}
|
|
23200
23460
|
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
23201
23461
|
underRoot(dir) {
|
|
23202
|
-
const abs =
|
|
23462
|
+
const abs = resolve14(dir);
|
|
23203
23463
|
const root = this.worktreesRoot();
|
|
23204
23464
|
return abs !== root && abs.startsWith(root + sep5);
|
|
23205
23465
|
}
|
|
@@ -23423,7 +23683,7 @@ var WorktreeWebSocketHandler = class {
|
|
|
23423
23683
|
}
|
|
23424
23684
|
const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
|
|
23425
23685
|
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
23426
|
-
const summary = await wt.diffSummary(
|
|
23686
|
+
const summary = await wt.diffSummary(resolve14(dir), base);
|
|
23427
23687
|
this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
|
|
23428
23688
|
}
|
|
23429
23689
|
// ── internals ───────────────────────────────────────────────────────────
|
|
@@ -24429,6 +24689,15 @@ function createMessageDispatcher(opts) {
|
|
|
24429
24689
|
msg
|
|
24430
24690
|
))
|
|
24431
24691
|
return;
|
|
24692
|
+
if (await handleConnectionsServiceAction(ws, msg, {
|
|
24693
|
+
trustBoundary: deps2.trustBoundary,
|
|
24694
|
+
logger: deps2.logger,
|
|
24695
|
+
getProjectRoot: state.getProjectRoot,
|
|
24696
|
+
getIndexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
|
|
24697
|
+
send,
|
|
24698
|
+
backend: "standalone"
|
|
24699
|
+
}))
|
|
24700
|
+
return;
|
|
24432
24701
|
if (await handleCodebaseIndexServerControl(ws, msg, {
|
|
24433
24702
|
trustBoundary: deps2.trustBoundary,
|
|
24434
24703
|
logger: deps2.logger,
|
|
@@ -24935,6 +25204,7 @@ async function createPreContextServices(input) {
|
|
|
24935
25204
|
registry: toolRegistry,
|
|
24936
25205
|
tier: normalizeTokenSavingTier(config.features.tokenSavingMode),
|
|
24937
25206
|
memory: { enabled: config.features.memory, store: memoryStore },
|
|
25207
|
+
nextSteps: { enabled: config.tools?.nextsteps?.enabled === true },
|
|
24938
25208
|
coordinationTools: [
|
|
24939
25209
|
makeMailboxTool({ projectDir: wpaths.projectDir, events }),
|
|
24940
25210
|
makeMailSendTool({ projectDir: wpaths.projectDir, events }),
|
|
@@ -26043,7 +26313,7 @@ async function startWebUI(opts = {}) {
|
|
|
26043
26313
|
if (events.listenerCount("tool.confirm_needed") === 0) {
|
|
26044
26314
|
throw new Error("No permission confirmation surface is connected");
|
|
26045
26315
|
}
|
|
26046
|
-
const decision = await new Promise((
|
|
26316
|
+
const decision = await new Promise((resolve17) => {
|
|
26047
26317
|
events.emit("tool.confirm_needed", {
|
|
26048
26318
|
sessionId: context.session.id,
|
|
26049
26319
|
tool: confirmTool,
|
|
@@ -26053,7 +26323,7 @@ async function startWebUI(opts = {}) {
|
|
|
26053
26323
|
decisionSource: pending.decisionSource,
|
|
26054
26324
|
riskTier: pending.riskTier,
|
|
26055
26325
|
boundaryReason: pending.boundaryReason,
|
|
26056
|
-
resolve:
|
|
26326
|
+
resolve: resolve17
|
|
26057
26327
|
});
|
|
26058
26328
|
});
|
|
26059
26329
|
const rule = { tool: "language_package", pattern: pending.suggestedPattern };
|
|
@@ -26122,7 +26392,8 @@ async function startWebUI(opts = {}) {
|
|
|
26122
26392
|
watcherMetricsRef
|
|
26123
26393
|
);
|
|
26124
26394
|
httpServer.listen(httpPort, wsHost, () => {
|
|
26125
|
-
|
|
26395
|
+
const tokenQuery = accessToken ? `/?token=${encodeURIComponent(accessToken)}` : "";
|
|
26396
|
+
console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}${tokenQuery}`);
|
|
26126
26397
|
const extraUrls = formatExternalAccessUrls({
|
|
26127
26398
|
bindHost: wsHost,
|
|
26128
26399
|
port: httpPort,
|
|
@@ -26144,8 +26415,11 @@ async function startWebUI(opts = {}) {
|
|
|
26144
26415
|
(req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
|
|
26145
26416
|
);
|
|
26146
26417
|
companionServer.on("error", (err) => {
|
|
26147
|
-
|
|
26148
|
-
|
|
26418
|
+
const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
|
|
26419
|
+
if (!expected) {
|
|
26420
|
+
console.warn(
|
|
26421
|
+
`[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
|
|
26422
|
+
);
|
|
26149
26423
|
}
|
|
26150
26424
|
});
|
|
26151
26425
|
companionServer.listen(httpPort, companion, () => {
|
|
@@ -26387,24 +26661,23 @@ async function startWebUI(opts = {}) {
|
|
|
26387
26661
|
clients,
|
|
26388
26662
|
pendingConfirms,
|
|
26389
26663
|
onSecurityRejection: (ev) => {
|
|
26390
|
-
|
|
26391
|
-
|
|
26392
|
-
|
|
26393
|
-
|
|
26394
|
-
|
|
26395
|
-
|
|
26396
|
-
|
|
26397
|
-
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
26664
|
+
void mailbox.send({
|
|
26665
|
+
from: context.agentId,
|
|
26666
|
+
to: "*",
|
|
26667
|
+
type: "note",
|
|
26668
|
+
audience: "leaders",
|
|
26669
|
+
subject: `Security rejection: ${ev.issueCode}`,
|
|
26670
|
+
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
26398
26671
|
|
|
26399
26672
|
connectionId: ${ev.connectionId ?? "?"}
|
|
26400
26673
|
sessionId: ${ev.sessionId ?? "?"}
|
|
26401
26674
|
agentId: ${ev.agentId ?? "?"}
|
|
26402
26675
|
projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
26403
|
-
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
}
|
|
26676
|
+
priority: "high",
|
|
26677
|
+
senderSessionId: session.id
|
|
26678
|
+
}).catch((err) => {
|
|
26679
|
+
console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
|
|
26680
|
+
});
|
|
26408
26681
|
},
|
|
26409
26682
|
goalHandler,
|
|
26410
26683
|
specsHandler,
|
|
@@ -26489,6 +26762,7 @@ export {
|
|
|
26489
26762
|
SddWizardWebSocketHandler,
|
|
26490
26763
|
SpecsWebSocketHandler,
|
|
26491
26764
|
TerminalWebSocketHandler,
|
|
26765
|
+
WEBUI_WS_MAX_BUFFERED_BYTES,
|
|
26492
26766
|
WorktreeWebSocketHandler,
|
|
26493
26767
|
addProvider,
|
|
26494
26768
|
announceWebuiReady,
|
|
@@ -26691,6 +26965,7 @@ export {
|
|
|
26691
26965
|
isPortFree,
|
|
26692
26966
|
isRegisteredMessageType,
|
|
26693
26967
|
isWildcardBind,
|
|
26968
|
+
joinSessionRegistryWithWebUIInstances,
|
|
26694
26969
|
listInstances,
|
|
26695
26970
|
loadManifest,
|
|
26696
26971
|
loadSavedProviders,
|
|
@@ -26743,6 +27018,7 @@ export {
|
|
|
26743
27018
|
seedContextMeta,
|
|
26744
27019
|
send,
|
|
26745
27020
|
sendResult2 as sendResult,
|
|
27021
|
+
sendSerialized,
|
|
26746
27022
|
setActiveKey,
|
|
26747
27023
|
setupEvents,
|
|
26748
27024
|
setupWebUICodebaseIndexing,
|