@wrongstack/webui-server 0.300.0 → 0.301.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +310 -117
- 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/entry.js +803 -106
- package/dist/server/goal-ws-handler.d.ts +12 -0
- package/dist/server/index.d.ts +1 -1
- 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",
|
|
@@ -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) {
|
|
@@ -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
|
);
|
|
@@ -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 } });
|
|
@@ -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)) {
|
|
@@ -10383,6 +10524,27 @@ import {
|
|
|
10383
10524
|
getServerKanbanStore
|
|
10384
10525
|
} from "@wrongstack/kanban";
|
|
10385
10526
|
import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
|
|
10527
|
+
|
|
10528
|
+
// src/server/kanban-broadcast.ts
|
|
10529
|
+
function kanbanBoardMessage(board) {
|
|
10530
|
+
return { type: "kanban.get", payload: { success: true, data: { board } } };
|
|
10531
|
+
}
|
|
10532
|
+
function kanbanListMessage(boards) {
|
|
10533
|
+
return { type: "kanban.list", payload: { success: true, data: boards } };
|
|
10534
|
+
}
|
|
10535
|
+
function kanbanDeletedMessage(boardId) {
|
|
10536
|
+
return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
|
|
10537
|
+
}
|
|
10538
|
+
async function publishKanbanBoard(broadcast2, board, listBoards6) {
|
|
10539
|
+
broadcast2(kanbanBoardMessage(board));
|
|
10540
|
+
if (!listBoards6) return;
|
|
10541
|
+
try {
|
|
10542
|
+
broadcast2(kanbanListMessage(await listBoards6()));
|
|
10543
|
+
} catch {
|
|
10544
|
+
}
|
|
10545
|
+
}
|
|
10546
|
+
|
|
10547
|
+
// src/server/kanban-dispatch.ts
|
|
10386
10548
|
function parseResolvedDispatchRoute(summary) {
|
|
10387
10549
|
const tags = summary.match(/Spawned subagent\s+\S+\s+\((.*?)\)\s+for task/i)?.[1];
|
|
10388
10550
|
if (!tags) return {};
|
|
@@ -10513,10 +10675,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
10513
10675
|
payload: { success: true, data: { boardId: board.id, task: completedTask } }
|
|
10514
10676
|
});
|
|
10515
10677
|
if (completedBoard) {
|
|
10516
|
-
ctx.broadcast?.(
|
|
10517
|
-
type: "kanban.get",
|
|
10518
|
-
payload: { success: true, data: { board: completedBoard } }
|
|
10519
|
-
});
|
|
10678
|
+
ctx.broadcast?.(kanbanBoardMessage(completedBoard));
|
|
10520
10679
|
}
|
|
10521
10680
|
ctx.broadcast?.({
|
|
10522
10681
|
type: "kanban.list",
|
|
@@ -10540,7 +10699,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
10540
10699
|
payload: { success: true, data: { boardId: board.id, task: runningTask } }
|
|
10541
10700
|
});
|
|
10542
10701
|
if (started?.board) {
|
|
10543
|
-
ctx.broadcast?.(
|
|
10702
|
+
ctx.broadcast?.(kanbanBoardMessage(started.board));
|
|
10544
10703
|
}
|
|
10545
10704
|
reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
|
|
10546
10705
|
} catch (error2) {
|
|
@@ -10783,14 +10942,11 @@ async function handleDecompositionResolution(ws, type, payload, ctx) {
|
|
|
10783
10942
|
type: "kanban.decomposition.applied",
|
|
10784
10943
|
payload: { success: true, data: { board: resolved.board } }
|
|
10785
10944
|
});
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
type: "kanban.list",
|
|
10792
|
-
payload: { success: true, data: await listBoards(ctx.projectRoot) }
|
|
10793
|
-
});
|
|
10945
|
+
await publishKanbanBoard(
|
|
10946
|
+
(message) => ctx.broadcast?.(message),
|
|
10947
|
+
resolved.board,
|
|
10948
|
+
() => listBoards(ctx.projectRoot)
|
|
10949
|
+
);
|
|
10794
10950
|
} else {
|
|
10795
10951
|
ctx.broadcast?.({
|
|
10796
10952
|
type: "kanban.decomposition.resolved",
|
|
@@ -10823,10 +10979,7 @@ async function handleTaskVerification(ws, type, payload, ctx) {
|
|
|
10823
10979
|
payload: { success: true, data: { boardId, task: freshTask } }
|
|
10824
10980
|
});
|
|
10825
10981
|
if (persisted) {
|
|
10826
|
-
ctx.broadcast?.(
|
|
10827
|
-
type: "kanban.get",
|
|
10828
|
-
payload: { success: true, data: { board: persisted } }
|
|
10829
|
-
});
|
|
10982
|
+
ctx.broadcast?.(kanbanBoardMessage(persisted));
|
|
10830
10983
|
}
|
|
10831
10984
|
} catch (err) {
|
|
10832
10985
|
ctx.broadcast?.({
|
|
@@ -11749,10 +11902,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11749
11902
|
let connectionCount = 0;
|
|
11750
11903
|
const broadcastDeleted = (boardId) => {
|
|
11751
11904
|
knownRevisions.delete(boardId);
|
|
11752
|
-
broadcastMessage(
|
|
11753
|
-
type: "kanban.delete",
|
|
11754
|
-
payload: { success: true, data: { removed: true, boardId } }
|
|
11755
|
-
});
|
|
11905
|
+
broadcastMessage(kanbanDeletedMessage(boardId));
|
|
11756
11906
|
};
|
|
11757
11907
|
const broadcastBoard = async (boardId) => {
|
|
11758
11908
|
const board = await store.getBoard(boardId);
|
|
@@ -11761,10 +11911,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11761
11911
|
return;
|
|
11762
11912
|
}
|
|
11763
11913
|
knownRevisions.set(boardId, board.updatedAt);
|
|
11764
|
-
broadcastMessage(
|
|
11765
|
-
type: "kanban.get",
|
|
11766
|
-
payload: { success: true, data: { board } }
|
|
11767
|
-
});
|
|
11914
|
+
broadcastMessage(kanbanBoardMessage(board));
|
|
11768
11915
|
};
|
|
11769
11916
|
const reconcileAfterConnect = async () => {
|
|
11770
11917
|
const summaries = await store.listBoards();
|
|
@@ -11783,20 +11930,36 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11783
11930
|
}
|
|
11784
11931
|
}
|
|
11785
11932
|
};
|
|
11786
|
-
|
|
11933
|
+
const COALESCE_MS = 300;
|
|
11934
|
+
const pendingBroadcasts = /* @__PURE__ */ new Map();
|
|
11935
|
+
const scheduleBroadcast = (boardId) => {
|
|
11936
|
+
if (pendingBroadcasts.has(boardId)) return;
|
|
11937
|
+
const timer = setTimeout(() => {
|
|
11938
|
+
pendingBroadcasts.delete(boardId);
|
|
11939
|
+
void broadcastBoard(boardId).catch(() => {
|
|
11940
|
+
});
|
|
11941
|
+
}, COALESCE_MS);
|
|
11942
|
+
timer.unref?.();
|
|
11943
|
+
pendingBroadcasts.set(boardId, timer);
|
|
11944
|
+
};
|
|
11945
|
+
const unsubscribe = bridgeKanbanSupervisor(
|
|
11787
11946
|
projectRoot,
|
|
11788
11947
|
async (event) => {
|
|
11948
|
+
const family = event.event?.split(".")[0];
|
|
11949
|
+
if (family !== "board" && family !== "task" && family !== "column") return;
|
|
11789
11950
|
const evData = event.data;
|
|
11790
11951
|
const boardId = evData?.boardId;
|
|
11791
11952
|
if (!boardId) return;
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11953
|
+
if (event.event === "board.deleted") {
|
|
11954
|
+
const timer = pendingBroadcasts.get(boardId);
|
|
11955
|
+
if (timer) {
|
|
11956
|
+
clearTimeout(timer);
|
|
11957
|
+
pendingBroadcasts.delete(boardId);
|
|
11796
11958
|
}
|
|
11797
|
-
|
|
11798
|
-
|
|
11959
|
+
broadcastDeleted(boardId);
|
|
11960
|
+
return;
|
|
11799
11961
|
}
|
|
11962
|
+
scheduleBroadcast(boardId);
|
|
11800
11963
|
},
|
|
11801
11964
|
{
|
|
11802
11965
|
autoReconnect: true,
|
|
@@ -11804,6 +11967,11 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11804
11967
|
onConnected: reconcileAfterConnect
|
|
11805
11968
|
}
|
|
11806
11969
|
);
|
|
11970
|
+
return () => {
|
|
11971
|
+
for (const timer of pendingBroadcasts.values()) clearTimeout(timer);
|
|
11972
|
+
pendingBroadcasts.clear();
|
|
11973
|
+
unsubscribe();
|
|
11974
|
+
};
|
|
11807
11975
|
}
|
|
11808
11976
|
|
|
11809
11977
|
// src/server/kanban-board-watcher.ts
|
|
@@ -11825,7 +11993,13 @@ function createShutdown(res) {
|
|
|
11825
11993
|
} catch (e) {
|
|
11826
11994
|
log(`[WebUI] Error closing session: ${e instanceof Error ? e.message : String(e)}`);
|
|
11827
11995
|
}
|
|
11828
|
-
for (const ws of res.clients())
|
|
11996
|
+
for (const ws of res.clients()) {
|
|
11997
|
+
try {
|
|
11998
|
+
ws.close();
|
|
11999
|
+
ws.terminate?.();
|
|
12000
|
+
} catch {
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
11829
12003
|
for (const server of res.servers) server?.close();
|
|
11830
12004
|
if (res.onShutdown) {
|
|
11831
12005
|
try {
|
|
@@ -12262,39 +12436,6 @@ import {
|
|
|
12262
12436
|
restartMcp,
|
|
12263
12437
|
updateMcp
|
|
12264
12438
|
} 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
12439
|
async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
|
|
12299
12440
|
if (!trustBoundary) return true;
|
|
12300
12441
|
const authorization = await authorizeWebUIAction(trustBoundary, {
|
|
@@ -14415,12 +14556,9 @@ function createKanbanRunMirror(deps2) {
|
|
|
14415
14556
|
}
|
|
14416
14557
|
async function publish(board) {
|
|
14417
14558
|
if (board) {
|
|
14418
|
-
broadcast2(
|
|
14559
|
+
broadcast2(kanbanBoardMessage(board));
|
|
14419
14560
|
}
|
|
14420
|
-
broadcast2(
|
|
14421
|
-
type: "kanban.list",
|
|
14422
|
-
payload: { success: true, data: await listBoards3(projectRoot) }
|
|
14423
|
-
});
|
|
14561
|
+
broadcast2(kanbanListMessage(await listBoards3(projectRoot)));
|
|
14424
14562
|
}
|
|
14425
14563
|
async function projectSdd(runId, snapshot) {
|
|
14426
14564
|
const k = mapKey("sdd", runId);
|
|
@@ -14823,14 +14961,11 @@ function createKanbanSupervisor(deps2) {
|
|
|
14823
14961
|
publish(snapshot);
|
|
14824
14962
|
const changedBoard = recovered?.board ?? gateSwept ?? reconciled?.board;
|
|
14825
14963
|
if (changedBoard) {
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
type: "kanban.list",
|
|
14832
|
-
payload: { success: true, data: await listBoards4(deps2.projectRoot) }
|
|
14833
|
-
});
|
|
14964
|
+
await publishKanbanBoard(
|
|
14965
|
+
deps2.broadcast,
|
|
14966
|
+
changedBoard,
|
|
14967
|
+
() => listBoards4(deps2.projectRoot)
|
|
14968
|
+
);
|
|
14834
14969
|
}
|
|
14835
14970
|
if (config.mode === "agentic" && anomalyCount > 0) {
|
|
14836
14971
|
await maybeRunAgent(board, config, health, snapshot);
|
|
@@ -15059,6 +15194,7 @@ function seedContextMeta(config, context) {
|
|
|
15059
15194
|
meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
|
|
15060
15195
|
meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
15061
15196
|
meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
15197
|
+
meta["nextStepsTool"] = config.tools?.nextsteps?.enabled === true;
|
|
15062
15198
|
meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
15063
15199
|
meta["fallbackBridge"] = config.fallbackBridge ?? "";
|
|
15064
15200
|
meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
|
|
@@ -15147,6 +15283,7 @@ function seedContextMeta(config, context) {
|
|
|
15147
15283
|
meta["autoReviewProvider"] = autoReviewExt?.["provider"] ?? "";
|
|
15148
15284
|
meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
|
|
15149
15285
|
meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
|
|
15286
|
+
meta["autoReviewModelSelection"] = autoReviewExt?.["modelSelection"] === "random" ? "random" : "round-robin";
|
|
15150
15287
|
meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
|
|
15151
15288
|
meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
|
|
15152
15289
|
meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
|
|
@@ -15184,6 +15321,7 @@ var PREF_KEYS = [
|
|
|
15184
15321
|
"chime",
|
|
15185
15322
|
"confirmExit",
|
|
15186
15323
|
"nextPrediction",
|
|
15324
|
+
"nextStepsTool",
|
|
15187
15325
|
"enhanceEnabled",
|
|
15188
15326
|
"enhanceDelayMs",
|
|
15189
15327
|
"enhanceLanguage",
|
|
@@ -15247,6 +15385,7 @@ var PREF_KEYS = [
|
|
|
15247
15385
|
"autoReviewProvider",
|
|
15248
15386
|
"autoReviewModel",
|
|
15249
15387
|
"autoReviewFallbackProfile",
|
|
15388
|
+
"autoReviewModelSelection",
|
|
15250
15389
|
"autoReviewFallbackModels",
|
|
15251
15390
|
"autoReviewDebounceMs",
|
|
15252
15391
|
"autoReviewMaxFilesPerBatch",
|
|
@@ -15460,6 +15599,11 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15460
15599
|
toolsCfg.maxIterations = payload["maxIterations"];
|
|
15461
15600
|
decrypted.tools = toolsCfg;
|
|
15462
15601
|
}
|
|
15602
|
+
if (typeof payload["nextStepsTool"] === "boolean") {
|
|
15603
|
+
const toolsCfg = decrypted.tools ?? {};
|
|
15604
|
+
toolsCfg.nextsteps = { enabled: payload["nextStepsTool"] };
|
|
15605
|
+
decrypted.tools = toolsCfg;
|
|
15606
|
+
}
|
|
15463
15607
|
const hqTouched = typeof payload["hqEnabled"] === "boolean" || typeof payload["hqUrl"] === "string" || typeof payload["hqToken"] === "string" || typeof payload["hqRawContent"] === "boolean";
|
|
15464
15608
|
if (hqTouched) {
|
|
15465
15609
|
const hqCfg = decrypted.hq ?? {};
|
|
@@ -15567,7 +15711,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15567
15711
|
ext["wstack-chimera"] = chimera;
|
|
15568
15712
|
decrypted.extensions = ext;
|
|
15569
15713
|
}
|
|
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";
|
|
15714
|
+
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
15715
|
if (autoReviewTouched) {
|
|
15572
15716
|
const ext = decrypted.extensions ?? {};
|
|
15573
15717
|
const ar = ext["wstack-auto-review"] ?? {};
|
|
@@ -15584,6 +15728,9 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15584
15728
|
ar["fallbackProfile"] = payload["autoReviewFallbackProfile"];
|
|
15585
15729
|
}
|
|
15586
15730
|
}
|
|
15731
|
+
if (payload["autoReviewModelSelection"] === "round-robin" || payload["autoReviewModelSelection"] === "random") {
|
|
15732
|
+
ar["modelSelection"] = payload["autoReviewModelSelection"];
|
|
15733
|
+
}
|
|
15587
15734
|
if (typeof payload["autoReviewDebounceMs"] === "number" && payload["autoReviewDebounceMs"] >= 0) {
|
|
15588
15735
|
ar["debounceMs"] = payload["autoReviewDebounceMs"];
|
|
15589
15736
|
}
|
|
@@ -15952,6 +16099,7 @@ function createProjectHandlers(ctx) {
|
|
|
15952
16099
|
ctx.context.session = next;
|
|
15953
16100
|
ctx.context.state.replaceMessages([]);
|
|
15954
16101
|
ctx.context.state.replaceTodos([]);
|
|
16102
|
+
ctx.context.clearMemoryEvidence?.();
|
|
15955
16103
|
ctx.context.readFiles.clear();
|
|
15956
16104
|
ctx.context.fileMtimes.clear();
|
|
15957
16105
|
ctx.tokenCounter.reset();
|
|
@@ -15996,7 +16144,7 @@ function createProjectHandlers(ctx) {
|
|
|
15996
16144
|
}
|
|
15997
16145
|
|
|
15998
16146
|
// src/server/provider-handlers.ts
|
|
15999
|
-
import { resolveProviderModelList } from "@wrongstack/core/models";
|
|
16147
|
+
import { hasProviderCredential, resolveProviderModelList } from "@wrongstack/core/models";
|
|
16000
16148
|
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
16001
16149
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
16002
16150
|
import {
|
|
@@ -16303,7 +16451,7 @@ function createProviderOperations(deps2) {
|
|
|
16303
16451
|
}
|
|
16304
16452
|
try {
|
|
16305
16453
|
const providers = await deps2.modelsRegistry.listProviders();
|
|
16306
|
-
const
|
|
16454
|
+
const savedProviders = await loadConfigProviders();
|
|
16307
16455
|
sendMessage(ws, {
|
|
16308
16456
|
type: "provider.catalog",
|
|
16309
16457
|
payload: {
|
|
@@ -16314,7 +16462,7 @@ function createProviderOperations(deps2) {
|
|
|
16314
16462
|
apiBase: provider.apiBase,
|
|
16315
16463
|
envVars: provider.envVars,
|
|
16316
16464
|
modelCount: provider.models.length,
|
|
16317
|
-
hasApiKey:
|
|
16465
|
+
hasApiKey: hasProviderCredential(provider, { providers: savedProviders })
|
|
16318
16466
|
}))
|
|
16319
16467
|
}
|
|
16320
16468
|
});
|
|
@@ -16861,6 +17009,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16861
17009
|
"ping",
|
|
16862
17010
|
"user_message",
|
|
16863
17011
|
"tool.confirm_result",
|
|
17012
|
+
"topic.advice",
|
|
16864
17013
|
"completion.request",
|
|
16865
17014
|
"model.switch",
|
|
16866
17015
|
"model.refine",
|
|
@@ -17163,6 +17312,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
17163
17312
|
"tool.loop_detected",
|
|
17164
17313
|
"tool.progress",
|
|
17165
17314
|
"tool.started",
|
|
17315
|
+
"topic.advice_result",
|
|
17166
17316
|
"tools.list",
|
|
17167
17317
|
"trust.persisted"
|
|
17168
17318
|
];
|
|
@@ -17672,6 +17822,8 @@ var SURFACE_PROTOCOL_CAPABILITIES = [
|
|
|
17672
17822
|
"chronicle.metrics",
|
|
17673
17823
|
"chronicle.status",
|
|
17674
17824
|
"connections.health",
|
|
17825
|
+
/** Bounded topic-shift advice plus same-session provider-context boundaries. */
|
|
17826
|
+
"context.topic-boundary",
|
|
17675
17827
|
/** Interview resume/discard + lastAgentText/lastRunId continuity. */
|
|
17676
17828
|
"sdd.interview.continuity",
|
|
17677
17829
|
/** Launch multi-agent runs from a graph id or resolved spec id. */
|
|
@@ -18076,6 +18228,7 @@ function createSessionHandlers(ctx) {
|
|
|
18076
18228
|
await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
|
|
18077
18229
|
ctx.context.state.replaceTodos(todos);
|
|
18078
18230
|
resetContextAccounting();
|
|
18231
|
+
ctx.context.clearMemoryEvidence?.();
|
|
18079
18232
|
ctx.context.readFiles.clear();
|
|
18080
18233
|
ctx.context.fileMtimes.clear();
|
|
18081
18234
|
ctx.context.state.setMeta?.(
|
|
@@ -18123,6 +18276,7 @@ function createSessionHandlers(ctx) {
|
|
|
18123
18276
|
ctx.context.state.replaceMessages([]);
|
|
18124
18277
|
ctx.context.state.replaceTodos([]);
|
|
18125
18278
|
resetContextAccounting();
|
|
18279
|
+
ctx.context.clearMemoryEvidence?.();
|
|
18126
18280
|
ctx.context.readFiles.clear();
|
|
18127
18281
|
ctx.context.fileMtimes.clear();
|
|
18128
18282
|
ctx.tokenCounter.reset?.();
|
|
@@ -18137,6 +18291,7 @@ function createSessionHandlers(ctx) {
|
|
|
18137
18291
|
ctx.context.state.replaceMessages([]);
|
|
18138
18292
|
ctx.context.state.replaceTodos([]);
|
|
18139
18293
|
resetContextAccounting();
|
|
18294
|
+
ctx.context.clearMemoryEvidence?.();
|
|
18140
18295
|
ctx.context.readFiles.clear();
|
|
18141
18296
|
ctx.context.fileMtimes.clear();
|
|
18142
18297
|
ctx.tokenCounter.reset?.();
|
|
@@ -19922,6 +20077,7 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19922
20077
|
};
|
|
19923
20078
|
const guardedTypes = /* @__PURE__ */ new Set([
|
|
19924
20079
|
"user_message",
|
|
20080
|
+
"topic.advice",
|
|
19925
20081
|
"abort",
|
|
19926
20082
|
"tool.confirm_result",
|
|
19927
20083
|
"session.new",
|
|
@@ -20249,6 +20405,8 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
20249
20405
|
))
|
|
20250
20406
|
return;
|
|
20251
20407
|
if (await handleConnectionsServiceAction(ws, message, {
|
|
20408
|
+
trustBoundary: deps2.trustBoundary,
|
|
20409
|
+
logger: deps2.logger,
|
|
20252
20410
|
getProjectRoot: projectRoot,
|
|
20253
20411
|
getIndexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
|
|
20254
20412
|
send: send2,
|
|
@@ -21235,7 +21393,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
21235
21393
|
const on = (event, listener) => events.on(event, listener);
|
|
21236
21394
|
return on("client.status", async (e) => {
|
|
21237
21395
|
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
21238
|
-
if (wpaths?.projectStatus) {
|
|
21396
|
+
if (wpaths?.projectStatus && e.projectHash !== "unknown") {
|
|
21239
21397
|
try {
|
|
21240
21398
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
21241
21399
|
const dir = path24.dirname(statusFile);
|
|
@@ -21410,7 +21568,10 @@ function registerSetupEventsProviderHandlers({
|
|
|
21410
21568
|
sessionId: e.sessionId,
|
|
21411
21569
|
providerId: e.providerId,
|
|
21412
21570
|
modelId: e.modelId,
|
|
21413
|
-
maxContext: e.maxContext
|
|
21571
|
+
maxContext: e.maxContext,
|
|
21572
|
+
...e.previousMaxContext !== void 0 ? { previousMaxContext: e.previousMaxContext } : {},
|
|
21573
|
+
...e.source !== void 0 ? { source: e.source } : {},
|
|
21574
|
+
...e.decreased !== void 0 ? { decreased: e.decreased } : {}
|
|
21414
21575
|
})
|
|
21415
21576
|
});
|
|
21416
21577
|
});
|
|
@@ -22520,7 +22681,15 @@ var SpecsWebSocketHandler = class {
|
|
|
22520
22681
|
this.clients.add(client);
|
|
22521
22682
|
ws.on("close", () => this.clients.delete(client));
|
|
22522
22683
|
ws.on("error", () => this.clients.delete(client));
|
|
22523
|
-
void this.sendList(client)
|
|
22684
|
+
void this.sendList(client).catch((err) => {
|
|
22685
|
+
console.warn(
|
|
22686
|
+
JSON.stringify({
|
|
22687
|
+
level: "warn",
|
|
22688
|
+
event: "specs.initial_send_failed",
|
|
22689
|
+
message: err instanceof Error ? err.message : String(err)
|
|
22690
|
+
})
|
|
22691
|
+
);
|
|
22692
|
+
});
|
|
22524
22693
|
}
|
|
22525
22694
|
dispose() {
|
|
22526
22695
|
this.clients.clear();
|
|
@@ -22734,15 +22903,17 @@ import {
|
|
|
22734
22903
|
|
|
22735
22904
|
// src/server/discover-mailbox-bridge.ts
|
|
22736
22905
|
import { spawn as spawn4 } from "node:child_process";
|
|
22737
|
-
import { createRequire } from "node:module";
|
|
22738
22906
|
import { existsSync as existsSync2 } from "node:fs";
|
|
22907
|
+
import { createRequire } from "node:module";
|
|
22739
22908
|
import { dirname as dirname10, join as join13 } from "node:path";
|
|
22740
|
-
import {
|
|
22909
|
+
import {
|
|
22910
|
+
readLiveLock,
|
|
22911
|
+
resolveProjectDir as resolveProjectDir3
|
|
22912
|
+
} from "@wrongstack/core/coordination";
|
|
22741
22913
|
import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
|
|
22742
|
-
import { readLiveLock } from "@wrongstack/core/coordination";
|
|
22743
22914
|
var MAILBOX_BRIDGE_BOOT_TIMEOUT_MS = 5e3;
|
|
22744
22915
|
async function discoverMailboxBridgeForWebui(params) {
|
|
22745
|
-
const mode = params.config?.features?.mailboxBridge ?? "
|
|
22916
|
+
const mode = params.config?.features?.mailboxBridge ?? "off";
|
|
22746
22917
|
if (mode === "off") return;
|
|
22747
22918
|
const projectDir = resolveProjectDir3(params.projectRoot, wstackGlobalRoot3());
|
|
22748
22919
|
let result = await readLiveLock(projectDir);
|
|
@@ -23004,6 +23175,13 @@ var TerminalWebSocketHandler = class {
|
|
|
23004
23175
|
this.send(ws, { type: "terminal.exit", payload: { id: payload.id, exitCode: -1 } });
|
|
23005
23176
|
return;
|
|
23006
23177
|
}
|
|
23178
|
+
if (this.sessions.get(ws) !== map) {
|
|
23179
|
+
this.logger.info?.(
|
|
23180
|
+
`terminal.create raced a disconnect (id=${payload.id}) \u2014 killing the orphan`
|
|
23181
|
+
);
|
|
23182
|
+
this.killPty(pty, "terminal create after disconnect");
|
|
23183
|
+
return;
|
|
23184
|
+
}
|
|
23007
23185
|
map.set(payload.id, pty);
|
|
23008
23186
|
this.logger.info?.(`terminal.create spawned (id=${payload.id}, pid=${pty.pid ?? "?"}) in ${cwd}`);
|
|
23009
23187
|
pty.onData((data) => {
|
|
@@ -24429,6 +24607,15 @@ function createMessageDispatcher(opts) {
|
|
|
24429
24607
|
msg
|
|
24430
24608
|
))
|
|
24431
24609
|
return;
|
|
24610
|
+
if (await handleConnectionsServiceAction(ws, msg, {
|
|
24611
|
+
trustBoundary: deps2.trustBoundary,
|
|
24612
|
+
logger: deps2.logger,
|
|
24613
|
+
getProjectRoot: state.getProjectRoot,
|
|
24614
|
+
getIndexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
|
|
24615
|
+
send,
|
|
24616
|
+
backend: "standalone"
|
|
24617
|
+
}))
|
|
24618
|
+
return;
|
|
24432
24619
|
if (await handleCodebaseIndexServerControl(ws, msg, {
|
|
24433
24620
|
trustBoundary: deps2.trustBoundary,
|
|
24434
24621
|
logger: deps2.logger,
|
|
@@ -24935,6 +25122,7 @@ async function createPreContextServices(input) {
|
|
|
24935
25122
|
registry: toolRegistry,
|
|
24936
25123
|
tier: normalizeTokenSavingTier(config.features.tokenSavingMode),
|
|
24937
25124
|
memory: { enabled: config.features.memory, store: memoryStore },
|
|
25125
|
+
nextSteps: { enabled: config.tools?.nextsteps?.enabled === true },
|
|
24938
25126
|
coordinationTools: [
|
|
24939
25127
|
makeMailboxTool({ projectDir: wpaths.projectDir, events }),
|
|
24940
25128
|
makeMailSendTool({ projectDir: wpaths.projectDir, events }),
|
|
@@ -26122,7 +26310,8 @@ async function startWebUI(opts = {}) {
|
|
|
26122
26310
|
watcherMetricsRef
|
|
26123
26311
|
);
|
|
26124
26312
|
httpServer.listen(httpPort, wsHost, () => {
|
|
26125
|
-
|
|
26313
|
+
const tokenQuery = accessToken ? `/?token=${encodeURIComponent(accessToken)}` : "";
|
|
26314
|
+
console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}${tokenQuery}`);
|
|
26126
26315
|
const extraUrls = formatExternalAccessUrls({
|
|
26127
26316
|
bindHost: wsHost,
|
|
26128
26317
|
port: httpPort,
|
|
@@ -26144,8 +26333,11 @@ async function startWebUI(opts = {}) {
|
|
|
26144
26333
|
(req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
|
|
26145
26334
|
);
|
|
26146
26335
|
companionServer.on("error", (err) => {
|
|
26147
|
-
|
|
26148
|
-
|
|
26336
|
+
const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
|
|
26337
|
+
if (!expected) {
|
|
26338
|
+
console.warn(
|
|
26339
|
+
`[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
|
|
26340
|
+
);
|
|
26149
26341
|
}
|
|
26150
26342
|
});
|
|
26151
26343
|
companionServer.listen(httpPort, companion, () => {
|
|
@@ -26387,24 +26579,23 @@ async function startWebUI(opts = {}) {
|
|
|
26387
26579
|
clients,
|
|
26388
26580
|
pendingConfirms,
|
|
26389
26581
|
onSecurityRejection: (ev) => {
|
|
26390
|
-
|
|
26391
|
-
|
|
26392
|
-
|
|
26393
|
-
|
|
26394
|
-
|
|
26395
|
-
|
|
26396
|
-
|
|
26397
|
-
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
26582
|
+
void mailbox.send({
|
|
26583
|
+
from: context.agentId,
|
|
26584
|
+
to: "*",
|
|
26585
|
+
type: "note",
|
|
26586
|
+
audience: "leaders",
|
|
26587
|
+
subject: `Security rejection: ${ev.issueCode}`,
|
|
26588
|
+
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
26398
26589
|
|
|
26399
26590
|
connectionId: ${ev.connectionId ?? "?"}
|
|
26400
26591
|
sessionId: ${ev.sessionId ?? "?"}
|
|
26401
26592
|
agentId: ${ev.agentId ?? "?"}
|
|
26402
26593
|
projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
26403
|
-
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
}
|
|
26594
|
+
priority: "high",
|
|
26595
|
+
senderSessionId: session.id
|
|
26596
|
+
}).catch((err) => {
|
|
26597
|
+
console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
|
|
26598
|
+
});
|
|
26408
26599
|
},
|
|
26409
26600
|
goalHandler,
|
|
26410
26601
|
specsHandler,
|
|
@@ -26489,6 +26680,7 @@ export {
|
|
|
26489
26680
|
SddWizardWebSocketHandler,
|
|
26490
26681
|
SpecsWebSocketHandler,
|
|
26491
26682
|
TerminalWebSocketHandler,
|
|
26683
|
+
WEBUI_WS_MAX_BUFFERED_BYTES,
|
|
26492
26684
|
WorktreeWebSocketHandler,
|
|
26493
26685
|
addProvider,
|
|
26494
26686
|
announceWebuiReady,
|
|
@@ -26743,6 +26935,7 @@ export {
|
|
|
26743
26935
|
seedContextMeta,
|
|
26744
26936
|
send,
|
|
26745
26937
|
sendResult2 as sendResult,
|
|
26938
|
+
sendSerialized,
|
|
26746
26939
|
setActiveKey,
|
|
26747
26940
|
setupEvents,
|
|
26748
26941
|
setupWebUICodebaseIndexing,
|