@wrongstack/webui-server 0.301.0 → 0.302.2
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 +436 -187
- package/dist/server/connections-health-route.d.ts +2 -2
- package/dist/server/conversation-operations.d.ts +9 -4
- package/dist/server/embedded-host-adapters.d.ts +7 -5
- package/dist/server/embedded-lifecycle.d.ts +17 -2
- package/dist/server/entry.js +615 -143
- package/dist/server/index.d.ts +1 -1
- package/dist/server/instance-registry.d.ts +51 -2
- package/dist/server/kanban-routes.d.ts +8 -0
- package/dist/server/kanban-supervisor.d.ts +1 -1
- package/dist/server/lifecycle.d.ts +7 -0
- package/dist/server/message-dispatcher.d.ts +10 -0
- package/dist/server/server-runtime.d.ts +8 -1
- package/dist/server/session-handlers.d.ts +3 -2
- package/dist/server/standalone-session-identity.d.ts +10 -2
- package/package.json +11 -11
package/dist/server/entry.js
CHANGED
|
@@ -4323,7 +4323,8 @@ function createConversationOperations(ctx) {
|
|
|
4323
4323
|
userMessage: async (ws, msg) => {
|
|
4324
4324
|
if (!ensureCurrentSession(ws, msg, "user_message")) return;
|
|
4325
4325
|
const payload = msg.payload ?? {};
|
|
4326
|
-
const
|
|
4326
|
+
const originSessionId = ctx.getSessionId();
|
|
4327
|
+
const controller = ctx.runControl.begin(ws, originSessionId);
|
|
4327
4328
|
if (!controller) {
|
|
4328
4329
|
ctx.send(ws, {
|
|
4329
4330
|
type: "error",
|
|
@@ -4334,7 +4335,6 @@ function createConversationOperations(ctx) {
|
|
|
4334
4335
|
});
|
|
4335
4336
|
return;
|
|
4336
4337
|
}
|
|
4337
|
-
const originSessionId = ctx.getSessionId();
|
|
4338
4338
|
try {
|
|
4339
4339
|
const agent = ctx.getAgent();
|
|
4340
4340
|
if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
|
|
@@ -4393,12 +4393,13 @@ function createConversationOperations(ctx) {
|
|
|
4393
4393
|
});
|
|
4394
4394
|
}
|
|
4395
4395
|
} finally {
|
|
4396
|
-
ctx.runControl.end(ws, controller);
|
|
4396
|
+
ctx.runControl.end(ws, originSessionId, controller);
|
|
4397
4397
|
}
|
|
4398
4398
|
},
|
|
4399
4399
|
abort: (ws, msg) => {
|
|
4400
4400
|
if (!ensureCurrentSession(ws, msg, "abort")) return;
|
|
4401
|
-
ctx.
|
|
4401
|
+
const sessionId = requestedSessionId(msg) ?? ctx.getSessionId();
|
|
4402
|
+
ctx.runControl.abort(ws, sessionId);
|
|
4402
4403
|
ctx.notifyAbort(ws, {
|
|
4403
4404
|
type: "error",
|
|
4404
4405
|
payload: sessionPayload2({ phase: "abort", message: "User aborted" })
|
|
@@ -5450,6 +5451,7 @@ function createConnectionLifecycle(options) {
|
|
|
5450
5451
|
}
|
|
5451
5452
|
|
|
5452
5453
|
// src/server/connections-health-route.ts
|
|
5454
|
+
import * as net from "node:net";
|
|
5453
5455
|
import {
|
|
5454
5456
|
ChronicleProjectServerClient,
|
|
5455
5457
|
createChronicleProjectAccess as createChronicleProjectAccess2,
|
|
@@ -5459,13 +5461,22 @@ import {
|
|
|
5459
5461
|
isMailboxProjectServerAvailable,
|
|
5460
5462
|
MailboxProjectServerConnection
|
|
5461
5463
|
} from "@wrongstack/core/coordination";
|
|
5464
|
+
import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
|
|
5462
5465
|
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
|
|
5463
5466
|
import {
|
|
5464
5467
|
closeKanbanServerConnections,
|
|
5465
5468
|
getKanbanServerConnection,
|
|
5466
5469
|
isKanbanServerAvailable
|
|
5467
5470
|
} from "@wrongstack/kanban";
|
|
5468
|
-
import
|
|
5471
|
+
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5472
|
+
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5473
|
+
import {
|
|
5474
|
+
checkCodebaseIndexServerHealth,
|
|
5475
|
+
ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
|
|
5476
|
+
getIndexState,
|
|
5477
|
+
resolveProjectIndexDaemonAvailability,
|
|
5478
|
+
shutdownCodebaseIndexServer
|
|
5479
|
+
} from "@wrongstack/tools";
|
|
5469
5480
|
|
|
5470
5481
|
// src/server/privileged-actions.ts
|
|
5471
5482
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -5499,15 +5510,6 @@ async function authorizeWebUIAction(boundary, action, logger) {
|
|
|
5499
5510
|
}
|
|
5500
5511
|
|
|
5501
5512
|
// src/server/connections-health-route.ts
|
|
5502
|
-
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5503
|
-
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5504
|
-
import {
|
|
5505
|
-
checkCodebaseIndexServerHealth,
|
|
5506
|
-
ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
|
|
5507
|
-
getIndexState,
|
|
5508
|
-
resolveProjectIndexDaemonAvailability,
|
|
5509
|
-
shutdownCodebaseIndexServer
|
|
5510
|
-
} from "@wrongstack/tools";
|
|
5511
5513
|
async function handleConnectionsHealthRoute(context, ws, message) {
|
|
5512
5514
|
if (message.type !== "connections.health") return false;
|
|
5513
5515
|
try {
|
|
@@ -5528,6 +5530,7 @@ async function handleConnectionsHealthRoute(context, ws, message) {
|
|
|
5528
5530
|
async function collectConnectionsHealth(options) {
|
|
5529
5531
|
const services = await Promise.all([
|
|
5530
5532
|
Promise.resolve(webuiHealth(options.backend)),
|
|
5533
|
+
sessionCatalogHealth(options.projectRoot),
|
|
5531
5534
|
chronicleHealth(options.projectRoot),
|
|
5532
5535
|
codebaseIndexHealth(options.projectRoot, options.indexDir),
|
|
5533
5536
|
sageHealth(options.projectRoot),
|
|
@@ -5545,6 +5548,50 @@ async function collectConnectionsHealth(options) {
|
|
|
5545
5548
|
services
|
|
5546
5549
|
};
|
|
5547
5550
|
}
|
|
5551
|
+
async function sessionCatalogHealth(projectRoot) {
|
|
5552
|
+
const startedAt = Date.now();
|
|
5553
|
+
try {
|
|
5554
|
+
const paths = resolveWstackPaths2({ projectRoot });
|
|
5555
|
+
const client = new SessionCatalogProjectClient({
|
|
5556
|
+
projectDir: paths.projectDir,
|
|
5557
|
+
projectRoot
|
|
5558
|
+
});
|
|
5559
|
+
try {
|
|
5560
|
+
const health = await client.ping();
|
|
5561
|
+
return {
|
|
5562
|
+
id: "session-catalog",
|
|
5563
|
+
label: "Session Catalog",
|
|
5564
|
+
status: health.damagedRows > 0 ? "degraded" : "healthy",
|
|
5565
|
+
required: true,
|
|
5566
|
+
mode: "project-daemon",
|
|
5567
|
+
detail: health.damagedRows > 0 ? `${health.damagedRows} damaged catalog row(s); rebuild is required.` : `${health.catalogRows} catalog session(s), ${health.liveLeases} live lease(s), ${health.reservations} reservation(s).`,
|
|
5568
|
+
ownerPid: health.pid,
|
|
5569
|
+
endpoint: health.endpoint,
|
|
5570
|
+
storage: health.databasePath,
|
|
5571
|
+
uptimeMs: health.uptimeMs,
|
|
5572
|
+
latencyMs: Date.now() - startedAt,
|
|
5573
|
+
clients: health.clients,
|
|
5574
|
+
activeRequests: health.activeRequests,
|
|
5575
|
+
queuedWork: health.reservations + health.maintenanceLeases,
|
|
5576
|
+
control: "none"
|
|
5577
|
+
};
|
|
5578
|
+
} finally {
|
|
5579
|
+
await client.close().catch(() => void 0);
|
|
5580
|
+
}
|
|
5581
|
+
} catch (error2) {
|
|
5582
|
+
return {
|
|
5583
|
+
id: "session-catalog",
|
|
5584
|
+
label: "Session Catalog",
|
|
5585
|
+
status: "error",
|
|
5586
|
+
required: true,
|
|
5587
|
+
mode: "project-daemon",
|
|
5588
|
+
detail: "Project-scoped session ownership and catalog are unavailable.",
|
|
5589
|
+
latencyMs: Date.now() - startedAt,
|
|
5590
|
+
lastError: error2 instanceof Error ? error2.message : String(error2),
|
|
5591
|
+
control: "none"
|
|
5592
|
+
};
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5548
5595
|
function webuiHealth(backend) {
|
|
5549
5596
|
return {
|
|
5550
5597
|
id: "webui",
|
|
@@ -6143,7 +6190,11 @@ async function restartSageServer(projectRoot) {
|
|
|
6143
6190
|
});
|
|
6144
6191
|
const verifyConn = new SageProjectServerConnection(projectRoot);
|
|
6145
6192
|
try {
|
|
6146
|
-
await verifyConn.call(
|
|
6193
|
+
await verifyConn.call(
|
|
6194
|
+
"ping",
|
|
6195
|
+
{},
|
|
6196
|
+
{ timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
|
|
6197
|
+
);
|
|
6147
6198
|
return {
|
|
6148
6199
|
serviceId: "sage",
|
|
6149
6200
|
action: "restart",
|
|
@@ -6377,28 +6428,28 @@ async function restartMailboxServer(projectRoot) {
|
|
|
6377
6428
|
var RESTART_POLL_INTERVAL_MS = 250;
|
|
6378
6429
|
var RESTART_DEADLINE_MS = 3e3;
|
|
6379
6430
|
function isEndpointAlive(endpoint) {
|
|
6380
|
-
return new Promise((
|
|
6431
|
+
return new Promise((resolve16) => {
|
|
6381
6432
|
const sock = net.createConnection(endpoint);
|
|
6382
6433
|
const timer = setTimeout(() => {
|
|
6383
6434
|
sock.destroy();
|
|
6384
|
-
|
|
6435
|
+
resolve16(false);
|
|
6385
6436
|
}, 500);
|
|
6386
6437
|
timer.unref?.();
|
|
6387
6438
|
sock.once("connect", () => {
|
|
6388
6439
|
clearTimeout(timer);
|
|
6389
6440
|
sock.destroy();
|
|
6390
|
-
|
|
6441
|
+
resolve16(true);
|
|
6391
6442
|
});
|
|
6392
6443
|
sock.once("error", () => {
|
|
6393
6444
|
clearTimeout(timer);
|
|
6394
6445
|
sock.destroy();
|
|
6395
|
-
|
|
6446
|
+
resolve16(false);
|
|
6396
6447
|
});
|
|
6397
6448
|
});
|
|
6398
6449
|
}
|
|
6399
6450
|
async function waitForShutdown(probe) {
|
|
6400
6451
|
if (!probe) {
|
|
6401
|
-
await new Promise((
|
|
6452
|
+
await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
|
|
6402
6453
|
return;
|
|
6403
6454
|
}
|
|
6404
6455
|
const deadline = Date.now() + RESTART_DEADLINE_MS;
|
|
@@ -6409,7 +6460,7 @@ async function waitForShutdown(probe) {
|
|
|
6409
6460
|
} catch {
|
|
6410
6461
|
return;
|
|
6411
6462
|
}
|
|
6412
|
-
await new Promise((
|
|
6463
|
+
await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
|
|
6413
6464
|
}
|
|
6414
6465
|
}
|
|
6415
6466
|
function failureService(id, label, required, mode, error2, latencyMs) {
|
|
@@ -6572,9 +6623,9 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
6572
6623
|
const cwd = projectRoot || void 0;
|
|
6573
6624
|
try {
|
|
6574
6625
|
const { execFile: ef } = await import("node:child_process");
|
|
6575
|
-
const git = (args) => new Promise((
|
|
6626
|
+
const git = (args) => new Promise((resolve16) => {
|
|
6576
6627
|
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
6577
|
-
|
|
6628
|
+
resolve16(err ? "" : stdout.trim());
|
|
6578
6629
|
});
|
|
6579
6630
|
});
|
|
6580
6631
|
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
@@ -6600,12 +6651,12 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
6600
6651
|
function makeGit(cwd) {
|
|
6601
6652
|
return async (args) => {
|
|
6602
6653
|
const { execFile: ef } = await import("node:child_process");
|
|
6603
|
-
return new Promise((
|
|
6654
|
+
return new Promise((resolve16) => {
|
|
6604
6655
|
ef(
|
|
6605
6656
|
"git",
|
|
6606
6657
|
args,
|
|
6607
6658
|
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
6608
|
-
(err, stdout) =>
|
|
6659
|
+
(err, stdout) => resolve16(err ? "" : stdout)
|
|
6609
6660
|
);
|
|
6610
6661
|
});
|
|
6611
6662
|
};
|
|
@@ -6770,7 +6821,7 @@ import { execFile } from "node:child_process";
|
|
|
6770
6821
|
var GIT_TIMEOUT_MS = 1e4;
|
|
6771
6822
|
var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
6772
6823
|
function gitStdout(cwd, args) {
|
|
6773
|
-
return new Promise((
|
|
6824
|
+
return new Promise((resolve16) => {
|
|
6774
6825
|
execFile(
|
|
6775
6826
|
"git",
|
|
6776
6827
|
[...args],
|
|
@@ -6781,7 +6832,7 @@ function gitStdout(cwd, args) {
|
|
|
6781
6832
|
timeout: GIT_TIMEOUT_MS,
|
|
6782
6833
|
maxBuffer: GIT_MAX_OUTPUT_BYTES
|
|
6783
6834
|
},
|
|
6784
|
-
(error2, stdout) =>
|
|
6835
|
+
(error2, stdout) => resolve16(error2 ? null : stdout)
|
|
6785
6836
|
);
|
|
6786
6837
|
});
|
|
6787
6838
|
}
|
|
@@ -7072,14 +7123,14 @@ var GoalWebSocketHandler = class {
|
|
|
7072
7123
|
const cwd = env?.cwd ?? this.projectRoot;
|
|
7073
7124
|
try {
|
|
7074
7125
|
const { execFile: execFile2 } = await import("node:child_process");
|
|
7075
|
-
const result = await new Promise((
|
|
7126
|
+
const result = await new Promise((resolve16) => {
|
|
7076
7127
|
const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
7077
7128
|
execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
|
|
7078
7129
|
if (err && err.code === "ENOENT") {
|
|
7079
|
-
|
|
7130
|
+
resolve16("[verify] tsc not found \u2014 skipping");
|
|
7080
7131
|
return;
|
|
7081
7132
|
}
|
|
7082
|
-
|
|
7133
|
+
resolve16(stdout + stderr);
|
|
7083
7134
|
});
|
|
7084
7135
|
});
|
|
7085
7136
|
if (result.includes("[verify]") || result.trim().length === 0) {
|
|
@@ -7817,7 +7868,7 @@ function pushEvent(event) {
|
|
|
7817
7868
|
}
|
|
7818
7869
|
}
|
|
7819
7870
|
function parseBody(req) {
|
|
7820
|
-
return new Promise((
|
|
7871
|
+
return new Promise((resolve16, reject) => {
|
|
7821
7872
|
let body = "";
|
|
7822
7873
|
let bodyBytes = 0;
|
|
7823
7874
|
let tooLarge = false;
|
|
@@ -7837,7 +7888,7 @@ function parseBody(req) {
|
|
|
7837
7888
|
return;
|
|
7838
7889
|
}
|
|
7839
7890
|
try {
|
|
7840
|
-
|
|
7891
|
+
resolve16(JSON.parse(body));
|
|
7841
7892
|
} catch {
|
|
7842
7893
|
reject(new Error("Invalid JSON"));
|
|
7843
7894
|
}
|
|
@@ -7924,7 +7975,7 @@ import * as path10 from "node:path";
|
|
|
7924
7975
|
import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
|
|
7925
7976
|
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
7926
7977
|
function readJsonBody(req) {
|
|
7927
|
-
return new Promise((
|
|
7978
|
+
return new Promise((resolve16, reject) => {
|
|
7928
7979
|
const chunks = [];
|
|
7929
7980
|
let total = 0;
|
|
7930
7981
|
req.on("data", (chunk) => {
|
|
@@ -7936,7 +7987,7 @@ function readJsonBody(req) {
|
|
|
7936
7987
|
}
|
|
7937
7988
|
chunks.push(chunk);
|
|
7938
7989
|
});
|
|
7939
|
-
req.on("end", () =>
|
|
7990
|
+
req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
|
|
7940
7991
|
req.on("error", (err) => reject(err));
|
|
7941
7992
|
});
|
|
7942
7993
|
}
|
|
@@ -8154,8 +8205,8 @@ async function handleApiSessions(res, globalRoot) {
|
|
|
8154
8205
|
return;
|
|
8155
8206
|
}
|
|
8156
8207
|
try {
|
|
8157
|
-
const {
|
|
8158
|
-
const registry =
|
|
8208
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8209
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8159
8210
|
const sessions = await registry.list();
|
|
8160
8211
|
const result = sessions.map((s) => ({
|
|
8161
8212
|
sessionId: s.sessionId,
|
|
@@ -8192,8 +8243,8 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
8192
8243
|
return;
|
|
8193
8244
|
}
|
|
8194
8245
|
try {
|
|
8195
|
-
const {
|
|
8196
|
-
const registry =
|
|
8246
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8247
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8197
8248
|
const entry = await registry.get(sessionId);
|
|
8198
8249
|
if (!entry) {
|
|
8199
8250
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8201,20 +8252,22 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
8201
8252
|
return;
|
|
8202
8253
|
}
|
|
8203
8254
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
8204
|
-
res.end(
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8255
|
+
res.end(
|
|
8256
|
+
JSON.stringify({
|
|
8257
|
+
sessionId: entry.sessionId,
|
|
8258
|
+
projectName: entry.projectName,
|
|
8259
|
+
status: entry.status,
|
|
8260
|
+
agents: entry.agents.map((a) => ({
|
|
8261
|
+
id: a.id,
|
|
8262
|
+
name: a.name,
|
|
8263
|
+
status: a.status,
|
|
8264
|
+
currentTool: a.currentTool,
|
|
8265
|
+
iterations: a.iterations,
|
|
8266
|
+
toolCalls: a.toolCalls,
|
|
8267
|
+
lastActivityAt: a.lastActivityAt
|
|
8268
|
+
}))
|
|
8269
|
+
})
|
|
8270
|
+
);
|
|
8218
8271
|
} catch (err) {
|
|
8219
8272
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
8220
8273
|
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
@@ -8332,9 +8385,9 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8332
8385
|
return;
|
|
8333
8386
|
}
|
|
8334
8387
|
try {
|
|
8335
|
-
const {
|
|
8388
|
+
const { getSessionRegistry: getSessionRegistry3, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
|
|
8336
8389
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8337
|
-
const registry =
|
|
8390
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8338
8391
|
const entry = await registry.get(sessionId);
|
|
8339
8392
|
if (!entry) {
|
|
8340
8393
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8342,7 +8395,10 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8342
8395
|
return;
|
|
8343
8396
|
}
|
|
8344
8397
|
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8345
|
-
const store = new DefaultSessionStore3({
|
|
8398
|
+
const store = new DefaultSessionStore3({
|
|
8399
|
+
dir: paths.projectSessions,
|
|
8400
|
+
projectRoot: entry.projectRoot
|
|
8401
|
+
});
|
|
8346
8402
|
const reader = new DefaultSessionReader2({ store });
|
|
8347
8403
|
const RING = Math.max(limit * 4, 2e3);
|
|
8348
8404
|
const ring = [];
|
|
@@ -8382,7 +8438,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8382
8438
|
}
|
|
8383
8439
|
}
|
|
8384
8440
|
function readJsonBody2(req) {
|
|
8385
|
-
return new Promise((
|
|
8441
|
+
return new Promise((resolve16, reject) => {
|
|
8386
8442
|
const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
8387
8443
|
if (contentType !== "application/json") {
|
|
8388
8444
|
reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
|
|
@@ -8398,7 +8454,7 @@ function readJsonBody2(req) {
|
|
|
8398
8454
|
});
|
|
8399
8455
|
req.on("end", () => {
|
|
8400
8456
|
try {
|
|
8401
|
-
|
|
8457
|
+
resolve16(data ? JSON.parse(data) : {});
|
|
8402
8458
|
} catch (err) {
|
|
8403
8459
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
8404
8460
|
}
|
|
@@ -8434,10 +8490,10 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
8434
8490
|
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
8435
8491
|
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
8436
8492
|
try {
|
|
8437
|
-
const {
|
|
8493
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8438
8494
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8439
8495
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8440
|
-
const registry =
|
|
8496
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8441
8497
|
const entry = await registry.get(sessionId);
|
|
8442
8498
|
if (!entry) {
|
|
8443
8499
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8462,10 +8518,10 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
8462
8518
|
return;
|
|
8463
8519
|
}
|
|
8464
8520
|
try {
|
|
8465
|
-
const {
|
|
8521
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8466
8522
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8467
8523
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8468
|
-
const registry =
|
|
8524
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8469
8525
|
const entry = await registry.get(sessionId);
|
|
8470
8526
|
if (!entry) {
|
|
8471
8527
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8522,10 +8578,10 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
8522
8578
|
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
8523
8579
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
8524
8580
|
try {
|
|
8525
|
-
const {
|
|
8581
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8526
8582
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8527
8583
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8528
|
-
const registry =
|
|
8584
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8529
8585
|
const entry = await registry.get(sessionId);
|
|
8530
8586
|
if (!entry) {
|
|
8531
8587
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8571,10 +8627,10 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
8571
8627
|
}
|
|
8572
8628
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
8573
8629
|
try {
|
|
8574
|
-
const {
|
|
8630
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8575
8631
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8576
8632
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8577
|
-
const registry =
|
|
8633
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8578
8634
|
const all = await registry.list();
|
|
8579
8635
|
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
8580
8636
|
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
@@ -8674,14 +8730,14 @@ async function readJsonBody3(res, req) {
|
|
|
8674
8730
|
});
|
|
8675
8731
|
return null;
|
|
8676
8732
|
}
|
|
8677
|
-
return new Promise((
|
|
8733
|
+
return new Promise((resolve16) => {
|
|
8678
8734
|
let data = "";
|
|
8679
8735
|
let failed = false;
|
|
8680
8736
|
const fail2 = (message) => {
|
|
8681
8737
|
if (failed) return;
|
|
8682
8738
|
failed = true;
|
|
8683
8739
|
sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
|
|
8684
|
-
|
|
8740
|
+
resolve16(null);
|
|
8685
8741
|
};
|
|
8686
8742
|
req.on("data", (chunk) => {
|
|
8687
8743
|
if (failed) return;
|
|
@@ -8694,7 +8750,7 @@ async function readJsonBody3(res, req) {
|
|
|
8694
8750
|
req.on("end", () => {
|
|
8695
8751
|
if (failed) return;
|
|
8696
8752
|
try {
|
|
8697
|
-
|
|
8753
|
+
resolve16(data.trim().length === 0 ? {} : JSON.parse(data));
|
|
8698
8754
|
} catch {
|
|
8699
8755
|
fail2("Request body is not valid JSON");
|
|
8700
8756
|
}
|
|
@@ -9533,7 +9589,7 @@ function strictDecodeParam(segment, res) {
|
|
|
9533
9589
|
function createHttpServer(opts) {
|
|
9534
9590
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
9535
9591
|
const distDir = path13.resolve(opts.distDir);
|
|
9536
|
-
const requireAccessToken =
|
|
9592
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
9537
9593
|
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
9538
9594
|
const trustedHostnames = (() => {
|
|
9539
9595
|
const names = [...opts.allowedHostnames ?? []];
|
|
@@ -10079,9 +10135,9 @@ function createHttpServer(opts) {
|
|
|
10079
10135
|
}
|
|
10080
10136
|
|
|
10081
10137
|
// src/server/instance-registry.ts
|
|
10138
|
+
import * as fs11 from "node:fs/promises";
|
|
10082
10139
|
import * as os from "node:os";
|
|
10083
10140
|
import * as path14 from "node:path";
|
|
10084
|
-
import * as fs11 from "node:fs/promises";
|
|
10085
10141
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
10086
10142
|
function defaultBaseDir() {
|
|
10087
10143
|
return path14.join(os.homedir(), ".wrongstack");
|
|
@@ -10434,11 +10490,11 @@ function kanbanListMessage(boards) {
|
|
|
10434
10490
|
function kanbanDeletedMessage(boardId) {
|
|
10435
10491
|
return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
|
|
10436
10492
|
}
|
|
10437
|
-
async function publishKanbanBoard(broadcast2, board,
|
|
10493
|
+
async function publishKanbanBoard(broadcast2, board, listBoards5) {
|
|
10438
10494
|
broadcast2(kanbanBoardMessage(board));
|
|
10439
|
-
if (!
|
|
10495
|
+
if (!listBoards5) return;
|
|
10440
10496
|
try {
|
|
10441
|
-
broadcast2(kanbanListMessage(await
|
|
10497
|
+
broadcast2(kanbanListMessage(await listBoards5()));
|
|
10442
10498
|
} catch {
|
|
10443
10499
|
}
|
|
10444
10500
|
}
|
|
@@ -11074,6 +11130,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
11074
11130
|
fail(ws, type, `Board not found: ${boardId}`);
|
|
11075
11131
|
return true;
|
|
11076
11132
|
}
|
|
11133
|
+
if (ctx.supervisor) {
|
|
11134
|
+
const snapshots = await ctx.supervisor.auditNow(boardId);
|
|
11135
|
+
const snapshot = snapshots[0];
|
|
11136
|
+
if (snapshot) {
|
|
11137
|
+
ok(ws, type, snapshot);
|
|
11138
|
+
return true;
|
|
11139
|
+
}
|
|
11140
|
+
}
|
|
11077
11141
|
const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
|
|
11078
11142
|
let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
|
|
11079
11143
|
const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
|
|
@@ -11833,6 +11897,15 @@ function createShutdown(res) {
|
|
|
11833
11897
|
} catch {
|
|
11834
11898
|
}
|
|
11835
11899
|
}
|
|
11900
|
+
if (res.onPreShutdown) {
|
|
11901
|
+
try {
|
|
11902
|
+
await res.onPreShutdown();
|
|
11903
|
+
} catch (e) {
|
|
11904
|
+
log(
|
|
11905
|
+
`[WebUI] Error during pre-shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`
|
|
11906
|
+
);
|
|
11907
|
+
}
|
|
11908
|
+
}
|
|
11836
11909
|
for (const server of res.servers) server?.close();
|
|
11837
11910
|
if (res.onShutdown) {
|
|
11838
11911
|
try {
|
|
@@ -13503,16 +13576,16 @@ function createModelOperations(context) {
|
|
|
13503
13576
|
import * as net2 from "node:net";
|
|
13504
13577
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
13505
13578
|
function isPortFree(host, port) {
|
|
13506
|
-
return new Promise((
|
|
13579
|
+
return new Promise((resolve16) => {
|
|
13507
13580
|
const srv = net2.createServer();
|
|
13508
|
-
srv.once("error", () =>
|
|
13581
|
+
srv.once("error", () => resolve16(false));
|
|
13509
13582
|
srv.once("listening", () => {
|
|
13510
|
-
srv.close(() =>
|
|
13583
|
+
srv.close(() => resolve16(true));
|
|
13511
13584
|
});
|
|
13512
13585
|
try {
|
|
13513
13586
|
srv.listen(port, host);
|
|
13514
13587
|
} catch {
|
|
13515
|
-
|
|
13588
|
+
resolve16(false);
|
|
13516
13589
|
}
|
|
13517
13590
|
});
|
|
13518
13591
|
}
|
|
@@ -13747,6 +13820,321 @@ async function handleBrainAsk(ctx, ws, question) {
|
|
|
13747
13820
|
}
|
|
13748
13821
|
}
|
|
13749
13822
|
|
|
13823
|
+
// src/server/kanban-supervisor.ts
|
|
13824
|
+
import {
|
|
13825
|
+
finalizeTaskCompletion,
|
|
13826
|
+
getBoard as getBoard2,
|
|
13827
|
+
getKanbanQueueHealth as getKanbanQueueHealth2,
|
|
13828
|
+
listBoards as listBoards3,
|
|
13829
|
+
reconcileKanbanBoard as reconcileKanbanBoard2,
|
|
13830
|
+
recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
|
|
13831
|
+
resolveGateEnforcement
|
|
13832
|
+
} from "@wrongstack/kanban";
|
|
13833
|
+
function resolveProjectRoot(deps2) {
|
|
13834
|
+
const root = deps2.projectRoot;
|
|
13835
|
+
return typeof root === "function" ? root() : root;
|
|
13836
|
+
}
|
|
13837
|
+
var DEFAULT_INTERVAL_MS = 1e4;
|
|
13838
|
+
var MIN_INTERVAL_MS = 2e3;
|
|
13839
|
+
var DEFAULT_AGENT_COOLDOWN_MS = 5 * 6e4;
|
|
13840
|
+
var DEFAULT_CONFIG = {
|
|
13841
|
+
enabled: true,
|
|
13842
|
+
mode: "deterministic",
|
|
13843
|
+
intervalMs: DEFAULT_INTERVAL_MS,
|
|
13844
|
+
recoveryMode: "auto"
|
|
13845
|
+
};
|
|
13846
|
+
function createKanbanSupervisor(deps2) {
|
|
13847
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
13848
|
+
const nextDue = /* @__PURE__ */ new Map();
|
|
13849
|
+
const agentLastRun = /* @__PURE__ */ new Map();
|
|
13850
|
+
const agentRunning = /* @__PURE__ */ new Set();
|
|
13851
|
+
let disposed = false;
|
|
13852
|
+
let nextTimer;
|
|
13853
|
+
const forgetBoard = (boardId) => {
|
|
13854
|
+
snapshots.delete(boardId);
|
|
13855
|
+
nextDue.delete(boardId);
|
|
13856
|
+
agentLastRun.delete(boardId);
|
|
13857
|
+
agentRunning.delete(boardId);
|
|
13858
|
+
};
|
|
13859
|
+
const pruneAbsentBoards = (presentBoardIds) => {
|
|
13860
|
+
for (const boardId of snapshots.keys()) {
|
|
13861
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13862
|
+
}
|
|
13863
|
+
for (const boardId of nextDue.keys()) {
|
|
13864
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13865
|
+
}
|
|
13866
|
+
for (const boardId of agentLastRun.keys()) {
|
|
13867
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13868
|
+
}
|
|
13869
|
+
for (const boardId of agentRunning) {
|
|
13870
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13871
|
+
}
|
|
13872
|
+
};
|
|
13873
|
+
const publish = (snapshot) => {
|
|
13874
|
+
snapshots.set(snapshot.boardId, snapshot);
|
|
13875
|
+
deps2.broadcast({
|
|
13876
|
+
type: "kanban.supervisor.status",
|
|
13877
|
+
payload: { success: true, data: snapshot }
|
|
13878
|
+
});
|
|
13879
|
+
};
|
|
13880
|
+
const auditBoard = async (board) => {
|
|
13881
|
+
const config = effectiveConfig(board);
|
|
13882
|
+
const auditedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13883
|
+
const intervalMs = Math.max(MIN_INTERVAL_MS, config.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
13884
|
+
const nextAuditAt = new Date(Date.now() + intervalMs).toISOString();
|
|
13885
|
+
nextDue.set(board.id, Date.now() + intervalMs);
|
|
13886
|
+
if (!config.enabled) {
|
|
13887
|
+
const snapshot2 = {
|
|
13888
|
+
boardId: board.id,
|
|
13889
|
+
status: "disabled",
|
|
13890
|
+
mode: config.mode,
|
|
13891
|
+
lastAuditAt: auditedAt,
|
|
13892
|
+
nextAuditAt,
|
|
13893
|
+
reconciledTaskIds: [],
|
|
13894
|
+
staleRecoveredTaskIds: [],
|
|
13895
|
+
anomalyCount: 0,
|
|
13896
|
+
summary: "Supervision is disabled for this board."
|
|
13897
|
+
};
|
|
13898
|
+
publish(snapshot2);
|
|
13899
|
+
return snapshot2;
|
|
13900
|
+
}
|
|
13901
|
+
const reconciled = await reconcileKanbanBoard2(resolveProjectRoot(deps2), board.id);
|
|
13902
|
+
const gateSwept = await sweepGateParkedTasks(deps2, reconciled?.board ?? board);
|
|
13903
|
+
let health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
|
|
13904
|
+
const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(resolveProjectRoot(deps2), board.id, {
|
|
13905
|
+
mode: config.recoveryMode ?? "auto",
|
|
13906
|
+
reason: "Kanban supervisor found an expired worker lease."
|
|
13907
|
+
}) : null;
|
|
13908
|
+
if (recovered) health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
|
|
13909
|
+
const anomalyCount = countAnomalies(health);
|
|
13910
|
+
const snapshot = {
|
|
13911
|
+
boardId: board.id,
|
|
13912
|
+
status: anomalyCount > 0 ? "attention" : "healthy",
|
|
13913
|
+
mode: config.mode,
|
|
13914
|
+
lastAuditAt: auditedAt,
|
|
13915
|
+
nextAuditAt,
|
|
13916
|
+
reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
|
|
13917
|
+
staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
|
|
13918
|
+
anomalyCount,
|
|
13919
|
+
summary: healthSummary(health)
|
|
13920
|
+
};
|
|
13921
|
+
publish(snapshot);
|
|
13922
|
+
const changedBoard = recovered?.board ?? gateSwept ?? reconciled?.board;
|
|
13923
|
+
if (changedBoard) {
|
|
13924
|
+
await publishKanbanBoard(
|
|
13925
|
+
deps2.broadcast,
|
|
13926
|
+
changedBoard,
|
|
13927
|
+
() => listBoards3(resolveProjectRoot(deps2))
|
|
13928
|
+
);
|
|
13929
|
+
}
|
|
13930
|
+
if (config.mode === "agentic" && anomalyCount > 0) {
|
|
13931
|
+
await maybeRunAgent(board, config, health, snapshot);
|
|
13932
|
+
}
|
|
13933
|
+
return snapshots.get(board.id) ?? snapshot;
|
|
13934
|
+
};
|
|
13935
|
+
const maybeRunAgent = async (board, config, health, snapshot) => {
|
|
13936
|
+
if (!deps2.dispatchTask || agentRunning.has(board.id)) return;
|
|
13937
|
+
const cooldownMs = Math.max(
|
|
13938
|
+
MIN_INTERVAL_MS,
|
|
13939
|
+
config.agentCooldownMs ?? DEFAULT_AGENT_COOLDOWN_MS
|
|
13940
|
+
);
|
|
13941
|
+
if (Date.now() - (agentLastRun.get(board.id) ?? 0) < cooldownMs) return;
|
|
13942
|
+
agentRunning.add(board.id);
|
|
13943
|
+
agentLastRun.set(board.id, Date.now());
|
|
13944
|
+
const watchdog = setTimeout(
|
|
13945
|
+
() => agentRunning.delete(board.id),
|
|
13946
|
+
Math.max(cooldownMs * 2, DEFAULT_AGENT_COOLDOWN_MS * 2)
|
|
13947
|
+
);
|
|
13948
|
+
watchdog.unref?.();
|
|
13949
|
+
publish({
|
|
13950
|
+
...snapshot,
|
|
13951
|
+
status: "running",
|
|
13952
|
+
lastAgentRunAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13953
|
+
summary: `Agentic anomaly review started. ${snapshot.summary ?? ""}`.trim()
|
|
13954
|
+
});
|
|
13955
|
+
const routing = config.routing ?? { mode: "session" };
|
|
13956
|
+
try {
|
|
13957
|
+
const spawnSummary = await deps2.dispatchTask(buildAuditPrompt(board, health), {
|
|
13958
|
+
...dispatchRoute(routing),
|
|
13959
|
+
...config.skills?.length ? { skills: config.skills } : {},
|
|
13960
|
+
name: `kanban-supervisor-${board.id.slice(0, 6)}`,
|
|
13961
|
+
// Carry the board identity into the spawned TaskSpec.context so the
|
|
13962
|
+
// tool-runtime boundary gate (`evaluateToolKanbanBoundary`) can resolve
|
|
13963
|
+
// the live board policy instead of failing open. Whole-board agentic
|
|
13964
|
+
// runs have no taskId, so only boardId is propagated.
|
|
13965
|
+
context: { kanban: { boardId: board.id, projectRoot: resolveProjectRoot(deps2) } },
|
|
13966
|
+
onDone: async (result) => {
|
|
13967
|
+
clearTimeout(watchdog);
|
|
13968
|
+
agentRunning.delete(board.id);
|
|
13969
|
+
if (await getBoard2(resolveProjectRoot(deps2), board.id) === null) return;
|
|
13970
|
+
const current3 = snapshots.get(board.id) ?? snapshot;
|
|
13971
|
+
publish({
|
|
13972
|
+
...current3,
|
|
13973
|
+
status: result.status === "failed" ? "error" : current3.anomalyCount ? "attention" : "healthy",
|
|
13974
|
+
lastAgentRunAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13975
|
+
summary: result.result ?? current3.summary,
|
|
13976
|
+
...result.error ? { error: result.error } : {}
|
|
13977
|
+
});
|
|
13978
|
+
}
|
|
13979
|
+
});
|
|
13980
|
+
const current2 = snapshots.get(board.id) ?? snapshot;
|
|
13981
|
+
publish({ ...current2, status: "running", summary: spawnSummary });
|
|
13982
|
+
} catch (error2) {
|
|
13983
|
+
clearTimeout(watchdog);
|
|
13984
|
+
agentRunning.delete(board.id);
|
|
13985
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
13986
|
+
deps2.log?.(`[KanbanSupervisor] ${board.id}: ${message}`);
|
|
13987
|
+
publish({ ...snapshot, status: "error", error: message });
|
|
13988
|
+
}
|
|
13989
|
+
};
|
|
13990
|
+
const auditNow = async (boardId) => {
|
|
13991
|
+
let boards;
|
|
13992
|
+
if (boardId) {
|
|
13993
|
+
const board = await getBoard2(resolveProjectRoot(deps2), boardId);
|
|
13994
|
+
if (board === null) {
|
|
13995
|
+
forgetBoard(boardId);
|
|
13996
|
+
boards = [];
|
|
13997
|
+
} else {
|
|
13998
|
+
boards = [board];
|
|
13999
|
+
}
|
|
14000
|
+
} else {
|
|
14001
|
+
const summaries = await listBoards3(resolveProjectRoot(deps2));
|
|
14002
|
+
pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
|
|
14003
|
+
boards = (await Promise.all(summaries.map((summary) => getBoard2(resolveProjectRoot(deps2), summary.id)))).filter((board) => Boolean(board));
|
|
14004
|
+
}
|
|
14005
|
+
const results = [];
|
|
14006
|
+
for (const board of boards) results.push(await auditBoard(board));
|
|
14007
|
+
scheduleNext();
|
|
14008
|
+
return results;
|
|
14009
|
+
};
|
|
14010
|
+
const scheduleNext = () => {
|
|
14011
|
+
if (disposed) return;
|
|
14012
|
+
if (nextTimer !== void 0) {
|
|
14013
|
+
clearTimeout(nextTimer);
|
|
14014
|
+
nextTimer = void 0;
|
|
14015
|
+
}
|
|
14016
|
+
const now = Date.now();
|
|
14017
|
+
let minDue = Infinity;
|
|
14018
|
+
for (const due of nextDue.values()) {
|
|
14019
|
+
if (due < minDue) minDue = due;
|
|
14020
|
+
}
|
|
14021
|
+
if (!Number.isFinite(minDue) || minDue <= now) {
|
|
14022
|
+
nextTimer = setTimeout(() => void tick(), MIN_INTERVAL_MS);
|
|
14023
|
+
nextTimer.unref?.();
|
|
14024
|
+
return;
|
|
14025
|
+
}
|
|
14026
|
+
const delay = Math.min(minDue - now, DEFAULT_INTERVAL_MS);
|
|
14027
|
+
if (delay <= 0) {
|
|
14028
|
+
nextTimer = setTimeout(() => void tick(), MIN_INTERVAL_MS);
|
|
14029
|
+
} else {
|
|
14030
|
+
nextTimer = setTimeout(() => void tick(), delay);
|
|
14031
|
+
}
|
|
14032
|
+
nextTimer.unref?.();
|
|
14033
|
+
};
|
|
14034
|
+
const tick = async () => {
|
|
14035
|
+
if (disposed) return;
|
|
14036
|
+
try {
|
|
14037
|
+
const now = Date.now();
|
|
14038
|
+
const summaries = await listBoards3(resolveProjectRoot(deps2));
|
|
14039
|
+
pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
|
|
14040
|
+
for (const summary of summaries) {
|
|
14041
|
+
if ((nextDue.get(summary.id) ?? 0) > now) continue;
|
|
14042
|
+
const board = await getBoard2(resolveProjectRoot(deps2), summary.id);
|
|
14043
|
+
if (board) await auditBoard(board);
|
|
14044
|
+
}
|
|
14045
|
+
} catch (error2) {
|
|
14046
|
+
deps2.log?.(`[KanbanSupervisor] ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
14047
|
+
} finally {
|
|
14048
|
+
scheduleNext();
|
|
14049
|
+
}
|
|
14050
|
+
};
|
|
14051
|
+
void scheduleNext();
|
|
14052
|
+
return {
|
|
14053
|
+
getSnapshot: (boardId) => snapshots.get(boardId),
|
|
14054
|
+
auditNow,
|
|
14055
|
+
getStats: () => ({
|
|
14056
|
+
snapshots: snapshots.size,
|
|
14057
|
+
scheduledBoards: nextDue.size,
|
|
14058
|
+
agentCooldowns: agentLastRun.size,
|
|
14059
|
+
runningAgents: agentRunning.size
|
|
14060
|
+
}),
|
|
14061
|
+
dispose() {
|
|
14062
|
+
disposed = true;
|
|
14063
|
+
if (nextTimer !== void 0) {
|
|
14064
|
+
clearTimeout(nextTimer);
|
|
14065
|
+
nextTimer = void 0;
|
|
14066
|
+
}
|
|
14067
|
+
snapshots.clear();
|
|
14068
|
+
nextDue.clear();
|
|
14069
|
+
agentLastRun.clear();
|
|
14070
|
+
agentRunning.clear();
|
|
14071
|
+
}
|
|
14072
|
+
};
|
|
14073
|
+
}
|
|
14074
|
+
function effectiveConfig(board) {
|
|
14075
|
+
return { ...DEFAULT_CONFIG, ...board.supervisor ?? {} };
|
|
14076
|
+
}
|
|
14077
|
+
async function sweepGateParkedTasks(deps2, board) {
|
|
14078
|
+
if (resolveGateEnforcement(board) === "off") return void 0;
|
|
14079
|
+
const parked = board.tasks.filter(
|
|
14080
|
+
(task) => task.status === "review" && task.assignment?.status === "completed" && !task.verificationReport
|
|
14081
|
+
);
|
|
14082
|
+
let lastBoard;
|
|
14083
|
+
for (const task of parked) {
|
|
14084
|
+
try {
|
|
14085
|
+
const finalized = await finalizeTaskCompletion(resolveProjectRoot(deps2), board.id, task.id, {
|
|
14086
|
+
eventContext: { actor: "kanban-supervisor" }
|
|
14087
|
+
});
|
|
14088
|
+
if (finalized) lastBoard = finalized.board;
|
|
14089
|
+
} catch (error2) {
|
|
14090
|
+
deps2.log?.(
|
|
14091
|
+
`[KanbanSupervisor] completion gate sweep failed for ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
14092
|
+
);
|
|
14093
|
+
}
|
|
14094
|
+
}
|
|
14095
|
+
return lastBoard;
|
|
14096
|
+
}
|
|
14097
|
+
function dispatchRoute(routing) {
|
|
14098
|
+
if (routing.mode === "session") return {};
|
|
14099
|
+
return {
|
|
14100
|
+
...routing.provider ? { provider: routing.provider } : {},
|
|
14101
|
+
...routing.model ? { model: routing.model } : {},
|
|
14102
|
+
...routing.fallbackProfile ? { fallbackProfile: routing.fallbackProfile } : {},
|
|
14103
|
+
...routing.fallbackModels?.length ? { fallbackModels: routing.fallbackModels } : {}
|
|
14104
|
+
};
|
|
14105
|
+
}
|
|
14106
|
+
function countAnomalies(health) {
|
|
14107
|
+
return health.staleAssignments.count + health.heartbeatDue.count + health.counts.failed + health.counts.blocked;
|
|
14108
|
+
}
|
|
14109
|
+
function healthSummary(health) {
|
|
14110
|
+
return [
|
|
14111
|
+
`${health.counts.running} running`,
|
|
14112
|
+
`${health.counts.ready} ready`,
|
|
14113
|
+
`${health.counts.review} review`,
|
|
14114
|
+
`${health.counts.blocked} blocked`,
|
|
14115
|
+
`${health.counts.failed} failed`,
|
|
14116
|
+
`${health.staleAssignments.count} stale`,
|
|
14117
|
+
`${health.dependencyBlocked.count} dependency-blocked`
|
|
14118
|
+
].join(" \xB7 ");
|
|
14119
|
+
}
|
|
14120
|
+
function buildAuditPrompt(board, health) {
|
|
14121
|
+
const taskLines = board.tasks.map(
|
|
14122
|
+
(task) => `- ${task.id}: ${task.title} [task=${task.status}; assignment=${task.assignment?.status ?? "none"}; column=${task.columnId}]`
|
|
14123
|
+
);
|
|
14124
|
+
return [
|
|
14125
|
+
"You are the explicitly configured WrongStack Kanban supervisor.",
|
|
14126
|
+
"Audit only this board. Do not implement product tasks.",
|
|
14127
|
+
`Board: ${board.title} (${board.id})`,
|
|
14128
|
+
`Health: ${healthSummary(health)}`,
|
|
14129
|
+
"",
|
|
14130
|
+
"Tasks:",
|
|
14131
|
+
...taskLines,
|
|
14132
|
+
"",
|
|
14133
|
+
"Use the kanban tool for corrections. Preserve manual blockers and dependencies.",
|
|
14134
|
+
"Fix only demonstrable status/assignment/column drift, then report every action and remaining anomaly."
|
|
14135
|
+
].join("\n");
|
|
14136
|
+
}
|
|
14137
|
+
|
|
13750
14138
|
// src/server/context-meta.ts
|
|
13751
14139
|
import { FallbackProfileManager } from "@wrongstack/core/agent";
|
|
13752
14140
|
import { resolvePluginEnablement } from "@wrongstack/core/plugin";
|
|
@@ -16222,6 +16610,8 @@ function labelForEvent(e) {
|
|
|
16222
16610
|
const count = e.messagesOmitted ?? e.messages.length;
|
|
16223
16611
|
return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
|
|
16224
16612
|
}
|
|
16613
|
+
case "messages_dropped":
|
|
16614
|
+
return `Oldest ${e.count} message${e.count === 1 ? "" : "s"} evicted`;
|
|
16225
16615
|
case "message_truncated":
|
|
16226
16616
|
return `Message truncated: ${e.before} \u2192 ${e.after}`;
|
|
16227
16617
|
case "file_event":
|
|
@@ -16309,6 +16699,8 @@ function detailForEvent(e) {
|
|
|
16309
16699
|
return `at index ${e.index}`;
|
|
16310
16700
|
case "messages_replaced":
|
|
16311
16701
|
return `${e.messagesOmitted ?? e.messages.length} total`;
|
|
16702
|
+
case "messages_dropped":
|
|
16703
|
+
return `dropped ${e.count} from the front`;
|
|
16312
16704
|
case "message_truncated":
|
|
16313
16705
|
return `truncated to ${e.after} tokens`;
|
|
16314
16706
|
case "mode_changed":
|
|
@@ -16526,7 +16918,7 @@ function createSessionHandlers(ctx) {
|
|
|
16526
16918
|
const current2 = ctx.getSession();
|
|
16527
16919
|
if (current2 !== next) {
|
|
16528
16920
|
try {
|
|
16529
|
-
ctx.abortActiveRun?.();
|
|
16921
|
+
ctx.abortActiveRun?.(current2.id);
|
|
16530
16922
|
} catch {
|
|
16531
16923
|
}
|
|
16532
16924
|
await finalizeSession(current2);
|
|
@@ -18211,7 +18603,7 @@ async function handleShellOpen(req, logger, options) {
|
|
|
18211
18603
|
import {
|
|
18212
18604
|
enqueueKanbanWorkflowCommand,
|
|
18213
18605
|
kanbanWorkflowId,
|
|
18214
|
-
listBoards as
|
|
18606
|
+
listBoards as listBoards4,
|
|
18215
18607
|
listKanbanWorkflowStates
|
|
18216
18608
|
} from "@wrongstack/kanban";
|
|
18217
18609
|
import {
|
|
@@ -18391,7 +18783,7 @@ var SddBoardWebSocketHandler = class {
|
|
|
18391
18783
|
this.broadcast({ type: "sdd.board.snapshot", payload: null });
|
|
18392
18784
|
if (this.lifecycle) {
|
|
18393
18785
|
try {
|
|
18394
|
-
const boards = await
|
|
18786
|
+
const boards = await listBoards4(this.lifecycle.projectRoot);
|
|
18395
18787
|
this.broadcast({
|
|
18396
18788
|
type: "kanban.list",
|
|
18397
18789
|
payload: { success: true, data: boards }
|
|
@@ -19093,7 +19485,6 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
19093
19485
|
}
|
|
19094
19486
|
|
|
19095
19487
|
// src/server/setup-events-fleet-broadcaster.ts
|
|
19096
|
-
import { watch as fsWatch } from "node:fs";
|
|
19097
19488
|
import * as path20 from "node:path";
|
|
19098
19489
|
function registerSetupEventsFleetBroadcaster(deps2) {
|
|
19099
19490
|
const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
|
|
@@ -19102,8 +19493,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
19102
19493
|
const disposers = [];
|
|
19103
19494
|
const broadcastSessions = async () => {
|
|
19104
19495
|
try {
|
|
19105
|
-
const {
|
|
19106
|
-
const registry =
|
|
19496
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
19497
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
19107
19498
|
const sessions = await registry.list();
|
|
19108
19499
|
const ownEntry = sessions.find((s) => s.pid === process.pid);
|
|
19109
19500
|
const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
|
|
@@ -19150,7 +19541,7 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
19150
19541
|
}
|
|
19151
19542
|
};
|
|
19152
19543
|
onFleetBroadcaster?.(broadcastSessions);
|
|
19153
|
-
let
|
|
19544
|
+
let subscriptionLive = false;
|
|
19154
19545
|
let statusTimer;
|
|
19155
19546
|
const scheduleStatusPoll = () => {
|
|
19156
19547
|
if (isDisposed()) return;
|
|
@@ -19159,35 +19550,31 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
19159
19550
|
void broadcastSessions();
|
|
19160
19551
|
scheduleStatusPoll();
|
|
19161
19552
|
},
|
|
19162
|
-
|
|
19553
|
+
subscriptionLive ? 3e4 : 5e3
|
|
19163
19554
|
);
|
|
19164
19555
|
if (statusTimer.unref) statusTimer.unref();
|
|
19165
19556
|
};
|
|
19166
19557
|
disposers.push(() => {
|
|
19167
19558
|
if (statusTimer) clearTimeout(statusTimer);
|
|
19168
19559
|
});
|
|
19169
|
-
let
|
|
19170
|
-
|
|
19171
|
-
|
|
19172
|
-
|
|
19173
|
-
|
|
19174
|
-
|
|
19175
|
-
|
|
19176
|
-
|
|
19177
|
-
|
|
19178
|
-
|
|
19179
|
-
|
|
19180
|
-
|
|
19181
|
-
|
|
19182
|
-
|
|
19183
|
-
|
|
19184
|
-
|
|
19185
|
-
|
|
19186
|
-
|
|
19187
|
-
regWatcher.close();
|
|
19188
|
-
});
|
|
19189
|
-
} catch {
|
|
19190
|
-
}
|
|
19560
|
+
let eventDebounce;
|
|
19561
|
+
let unsubscribe;
|
|
19562
|
+
void import("@wrongstack/core/storage").then(async ({ getSessionRegistry: getSessionRegistry3 }) => {
|
|
19563
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
19564
|
+
const projectSlug2 = wpaths?.projectSlug;
|
|
19565
|
+
if (!projectSlug2 || isDisposed()) return;
|
|
19566
|
+
unsubscribe = await registry.subscribeProject(projectSlug2, context.projectRoot, () => {
|
|
19567
|
+
if (eventDebounce) clearTimeout(eventDebounce);
|
|
19568
|
+
eventDebounce = setTimeout(() => void broadcastSessions(), 25);
|
|
19569
|
+
});
|
|
19570
|
+
subscriptionLive = true;
|
|
19571
|
+
}).catch(() => {
|
|
19572
|
+
subscriptionLive = false;
|
|
19573
|
+
});
|
|
19574
|
+
disposers.push(() => {
|
|
19575
|
+
if (eventDebounce) clearTimeout(eventDebounce);
|
|
19576
|
+
void unsubscribe?.();
|
|
19577
|
+
});
|
|
19191
19578
|
scheduleStatusPoll();
|
|
19192
19579
|
void broadcastSessions();
|
|
19193
19580
|
return () => {
|
|
@@ -19300,7 +19687,7 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
|
|
|
19300
19687
|
}
|
|
19301
19688
|
|
|
19302
19689
|
// src/server/setup-events-status-watcher.ts
|
|
19303
|
-
import { watch as
|
|
19690
|
+
import { watch as fsWatch } from "node:fs";
|
|
19304
19691
|
import * as fs18 from "node:fs/promises";
|
|
19305
19692
|
import * as path22 from "node:path";
|
|
19306
19693
|
|
|
@@ -19390,7 +19777,7 @@ function registerSetupEventsStatusWatcher(deps2) {
|
|
|
19390
19777
|
try {
|
|
19391
19778
|
await fs18.mkdir(projectsDir, { recursive: true });
|
|
19392
19779
|
if (isDisposed()) return;
|
|
19393
|
-
watcher =
|
|
19780
|
+
watcher = fsWatch(
|
|
19394
19781
|
projectsDir,
|
|
19395
19782
|
{ persistent: true, recursive: true },
|
|
19396
19783
|
async (eventType, filename) => {
|
|
@@ -20552,7 +20939,6 @@ import {
|
|
|
20552
20939
|
mailboxSessionTag,
|
|
20553
20940
|
ObservableBrainArbiter as ObservableBrainArbiterCtor
|
|
20554
20941
|
} from "@wrongstack/core/coordination";
|
|
20555
|
-
import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
|
|
20556
20942
|
import { installDesignStudioMiddleware } from "@wrongstack/core/design";
|
|
20557
20943
|
import {
|
|
20558
20944
|
AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
|
|
@@ -20564,6 +20950,7 @@ import {
|
|
|
20564
20950
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
20565
20951
|
import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
|
|
20566
20952
|
import {
|
|
20953
|
+
DEFAULT_TOOLS_CONFIG,
|
|
20567
20954
|
resolveContextWindowPolicy as resolveContextWindowPolicy2
|
|
20568
20955
|
} from "@wrongstack/core/types";
|
|
20569
20956
|
import {
|
|
@@ -20724,7 +21111,7 @@ function findWorkspaceCliEntry(projectRoot) {
|
|
|
20724
21111
|
return null;
|
|
20725
21112
|
}
|
|
20726
21113
|
function sleep(ms) {
|
|
20727
|
-
return new Promise((
|
|
21114
|
+
return new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
20728
21115
|
}
|
|
20729
21116
|
|
|
20730
21117
|
// src/server/terminal-ws-handler.ts
|
|
@@ -20967,7 +21354,7 @@ function clampDim(value, fallback) {
|
|
|
20967
21354
|
}
|
|
20968
21355
|
|
|
20969
21356
|
// src/server/worktree-ws-handler.ts
|
|
20970
|
-
import { join as join11, resolve as
|
|
21357
|
+
import { join as join11, resolve as resolve13, sep as sep5 } from "node:path";
|
|
20971
21358
|
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
20972
21359
|
import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
|
|
20973
21360
|
import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
|
|
@@ -21052,11 +21439,11 @@ var WorktreeWebSocketHandler = class {
|
|
|
21052
21439
|
// ── orphan management ─────────────────────────────────────────────────────
|
|
21053
21440
|
/** Absolute managed-worktrees root for this project. */
|
|
21054
21441
|
worktreesRoot() {
|
|
21055
|
-
return
|
|
21442
|
+
return resolve13(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
|
|
21056
21443
|
}
|
|
21057
21444
|
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
21058
21445
|
underRoot(dir) {
|
|
21059
|
-
const abs =
|
|
21446
|
+
const abs = resolve13(dir);
|
|
21060
21447
|
const root = this.worktreesRoot();
|
|
21061
21448
|
return abs !== root && abs.startsWith(root + sep5);
|
|
21062
21449
|
}
|
|
@@ -21280,7 +21667,7 @@ var WorktreeWebSocketHandler = class {
|
|
|
21280
21667
|
}
|
|
21281
21668
|
const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
|
|
21282
21669
|
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
21283
|
-
const summary = await wt.diffSummary(
|
|
21670
|
+
const summary = await wt.diffSummary(resolve13(dir), base);
|
|
21284
21671
|
this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
|
|
21285
21672
|
}
|
|
21286
21673
|
// ── internals ───────────────────────────────────────────────────────────
|
|
@@ -21445,6 +21832,11 @@ async function createAgentServices(input) {
|
|
|
21445
21832
|
taskAware: config.Sage?.inject?.taskAware,
|
|
21446
21833
|
minScore: config.Sage?.inject?.minScore,
|
|
21447
21834
|
minImportance: config.Sage?.inject?.minImportance,
|
|
21835
|
+
// Forward the explicit relation floor so an operator-configured
|
|
21836
|
+
// `Sage.inject.relationFloor` is honored in WebUI sessions. Without
|
|
21837
|
+
// this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
|
|
21838
|
+
// is the CLI default but masks operator overrides.
|
|
21839
|
+
relationFloor: config.Sage?.inject?.relationFloor,
|
|
21448
21840
|
repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
|
|
21449
21841
|
verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
|
|
21450
21842
|
triggers: config.Sage?.inject?.triggers,
|
|
@@ -21466,6 +21858,10 @@ async function createAgentServices(input) {
|
|
|
21466
21858
|
maxMemories: config.Sage?.inject?.maxTurnMemories,
|
|
21467
21859
|
maxChars: config.Sage?.inject?.maxCharsPerTurn,
|
|
21468
21860
|
minScore: config.Sage?.inject?.minScore,
|
|
21861
|
+
// CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
|
|
21862
|
+
// value drives both runtimes instead of silently falling back to the
|
|
21863
|
+
// 0.3 default. The undefined case keeps the middleware's own default.
|
|
21864
|
+
metadataWeight: config.Sage?.retrieval?.metadataWeight,
|
|
21469
21865
|
getSessionId: getSageSessionId,
|
|
21470
21866
|
tracker: sageInjectionTracker
|
|
21471
21867
|
})
|
|
@@ -21490,25 +21886,29 @@ async function createAgentServices(input) {
|
|
|
21490
21886
|
strategy: config.context?.strategy,
|
|
21491
21887
|
preserveK: config.context?.preserveK ?? 10,
|
|
21492
21888
|
eliseThreshold: config.context?.eliseThreshold ?? 2e3,
|
|
21889
|
+
// Match the CLI/TUI runtime: keep corrections, errors and decisions
|
|
21890
|
+
// verbatim while collapsing routine assistant chatter/tool protocol.
|
|
21891
|
+
// Without this WebUI's hybrid strategy builds an ever-growing lossless
|
|
21892
|
+
// digest and eventually relies on blunt emergency head/tail trimming.
|
|
21893
|
+
smart: true,
|
|
21493
21894
|
summarizerModel: config.context?.summarizerModel,
|
|
21494
21895
|
llmSelector: config.context?.llmSelector
|
|
21495
21896
|
});
|
|
21496
21897
|
const initialContextPolicy = resolveContextWindowPolicy2(config.context);
|
|
21497
21898
|
let autoCompactor;
|
|
21498
21899
|
if (config.context?.autoCompact !== false) {
|
|
21499
|
-
let effectiveMaxContext =
|
|
21500
|
-
|
|
21501
|
-
|
|
21502
|
-
|
|
21503
|
-
|
|
21504
|
-
|
|
21505
|
-
|
|
21506
|
-
|
|
21507
|
-
|
|
21508
|
-
|
|
21509
|
-
} catch {
|
|
21510
|
-
}
|
|
21900
|
+
let effectiveMaxContext = 0;
|
|
21901
|
+
try {
|
|
21902
|
+
const m = await resolveProviderModelMetadata(
|
|
21903
|
+
modelsRegistry,
|
|
21904
|
+
config.provider,
|
|
21905
|
+
context.model,
|
|
21906
|
+
config.providers?.[config.provider]
|
|
21907
|
+
);
|
|
21908
|
+
effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
|
|
21909
|
+
} catch {
|
|
21511
21910
|
}
|
|
21911
|
+
if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
|
|
21512
21912
|
if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
|
|
21513
21913
|
autoCompactor = new AutoCompactionMiddlewareCtor(
|
|
21514
21914
|
compactor,
|
|
@@ -22130,16 +22530,24 @@ function createMessageDispatcher(opts) {
|
|
|
22130
22530
|
getAgent: () => deps2.agent,
|
|
22131
22531
|
getSessionId: () => state.getSession().id,
|
|
22132
22532
|
runControl: {
|
|
22133
|
-
begin: () => {
|
|
22533
|
+
begin: (_ws, sessionId) => {
|
|
22134
22534
|
if (runLock.get()) return void 0;
|
|
22135
22535
|
const controller = new AbortController();
|
|
22136
22536
|
runLock.set(controller);
|
|
22537
|
+
runLock.setSession(sessionId);
|
|
22137
22538
|
return controller;
|
|
22138
22539
|
},
|
|
22139
|
-
end: (_ws, controller) => {
|
|
22140
|
-
if (runLock.get() === controller)
|
|
22540
|
+
end: (_ws, _sessionId, controller) => {
|
|
22541
|
+
if (runLock.get() === controller) {
|
|
22542
|
+
runLock.set(null);
|
|
22543
|
+
runLock.setSession(null);
|
|
22544
|
+
}
|
|
22141
22545
|
},
|
|
22142
|
-
abort: () =>
|
|
22546
|
+
abort: (_ws, sessionId) => {
|
|
22547
|
+
if (runLock.getSession() === sessionId || !runLock.getSession()) {
|
|
22548
|
+
runLock.get()?.abort();
|
|
22549
|
+
}
|
|
22550
|
+
}
|
|
22143
22551
|
},
|
|
22144
22552
|
pendingConfirms,
|
|
22145
22553
|
send,
|
|
@@ -22161,10 +22569,20 @@ function createMessageDispatcher(opts) {
|
|
|
22161
22569
|
const goalSnapshotRoutes = {
|
|
22162
22570
|
getSnapshot: () => handleGoalGet(state.getProjectRoot(), (message) => broadcast(state.getClients(), message))
|
|
22163
22571
|
};
|
|
22572
|
+
const kanbanSupervisor = createKanbanSupervisor({
|
|
22573
|
+
projectRoot: () => state.getProjectRoot(),
|
|
22574
|
+
broadcast: (message) => broadcast(state.getClients(), message),
|
|
22575
|
+
log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
|
|
22576
|
+
});
|
|
22577
|
+
if (opts.onDispose) {
|
|
22578
|
+
const dispose = () => kanbanSupervisor.dispose();
|
|
22579
|
+
opts.onDispose(dispose);
|
|
22580
|
+
}
|
|
22164
22581
|
const kanbanContext = () => ({
|
|
22165
22582
|
projectRoot: state.getProjectRoot(),
|
|
22166
22583
|
context: deps2.context,
|
|
22167
|
-
broadcast: (message) => broadcast(state.getClients(), message)
|
|
22584
|
+
broadcast: (message) => broadcast(state.getClients(), message),
|
|
22585
|
+
supervisor: kanbanSupervisor
|
|
22168
22586
|
});
|
|
22169
22587
|
const kanbanHostRoutes = {
|
|
22170
22588
|
meta: async (ws) => {
|
|
@@ -22680,6 +23098,14 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
|
|
|
22680
23098
|
transition = transition.then(async () => {
|
|
22681
23099
|
if (stopped) return;
|
|
22682
23100
|
if (pendingClaim?.sessionId === sessionId) {
|
|
23101
|
+
await pendingClaim.claim.activate({
|
|
23102
|
+
sessionId,
|
|
23103
|
+
...target,
|
|
23104
|
+
clientType: "webui",
|
|
23105
|
+
pid: process.pid,
|
|
23106
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23107
|
+
agents: statusTracker.getAgents()
|
|
23108
|
+
});
|
|
22683
23109
|
pendingClaim = void 0;
|
|
22684
23110
|
} else {
|
|
22685
23111
|
await register(sessionId, true, target);
|
|
@@ -22703,14 +23129,35 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
|
|
|
22703
23129
|
}
|
|
22704
23130
|
if (sessionId === activeSessionId) return async () => {
|
|
22705
23131
|
};
|
|
22706
|
-
const previousSessionId = activeSessionId;
|
|
22707
|
-
const previousTarget = activeTarget;
|
|
22708
23132
|
const token = Symbol(sessionId);
|
|
22709
|
-
|
|
22710
|
-
|
|
23133
|
+
if ("reserveResume" in registry && typeof registry.reserveResume === "function") {
|
|
23134
|
+
const reservation = await registry.reserveResume({
|
|
23135
|
+
sessionId,
|
|
23136
|
+
projectSlug: target.projectSlug,
|
|
23137
|
+
projectRoot: target.projectRoot
|
|
23138
|
+
});
|
|
23139
|
+
pendingClaim = { sessionId, token, claim: reservation, target };
|
|
23140
|
+
} else {
|
|
23141
|
+
await register(sessionId, true, target);
|
|
23142
|
+
pendingClaim = {
|
|
23143
|
+
sessionId,
|
|
23144
|
+
token,
|
|
23145
|
+
target,
|
|
23146
|
+
claim: {
|
|
23147
|
+
reservation: {
|
|
23148
|
+
reservationId: "legacy",
|
|
23149
|
+
targetSessionId: sessionId,
|
|
23150
|
+
requesterInstanceId: "legacy",
|
|
23151
|
+
expiresAt: Number.MAX_SAFE_INTEGER
|
|
23152
|
+
},
|
|
23153
|
+
activate: async () => void 0,
|
|
23154
|
+
cancel: async () => register(activeSessionId, true, activeTarget)
|
|
23155
|
+
}
|
|
23156
|
+
};
|
|
23157
|
+
}
|
|
22711
23158
|
return async () => {
|
|
22712
23159
|
if (pendingClaim?.token !== token) return;
|
|
22713
|
-
await
|
|
23160
|
+
await pendingClaim.claim.cancel();
|
|
22714
23161
|
pendingClaim = void 0;
|
|
22715
23162
|
};
|
|
22716
23163
|
};
|
|
@@ -23637,10 +24084,11 @@ function startHttpServer(opts) {
|
|
|
23637
24084
|
return httpServer;
|
|
23638
24085
|
}
|
|
23639
24086
|
function registerShutdown(deps2) {
|
|
23640
|
-
registerShutdownHandlers({
|
|
24087
|
+
return registerShutdownHandlers({
|
|
23641
24088
|
flushSession: deps2.flushSession,
|
|
23642
24089
|
clients: deps2.clients,
|
|
23643
24090
|
servers: deps2.servers,
|
|
24091
|
+
onPreShutdown: deps2.onPreShutdown,
|
|
23644
24092
|
onShutdown: deps2.onShutdown
|
|
23645
24093
|
});
|
|
23646
24094
|
}
|
|
@@ -23910,7 +24358,7 @@ async function startWebUI(opts = {}) {
|
|
|
23910
24358
|
if (events.listenerCount("tool.confirm_needed") === 0) {
|
|
23911
24359
|
throw new Error("No permission confirmation surface is connected");
|
|
23912
24360
|
}
|
|
23913
|
-
const decision = await new Promise((
|
|
24361
|
+
const decision = await new Promise((resolve16) => {
|
|
23914
24362
|
events.emit("tool.confirm_needed", {
|
|
23915
24363
|
sessionId: context.session.id,
|
|
23916
24364
|
tool: confirmTool,
|
|
@@ -23920,7 +24368,7 @@ async function startWebUI(opts = {}) {
|
|
|
23920
24368
|
decisionSource: pending.decisionSource,
|
|
23921
24369
|
riskTier: pending.riskTier,
|
|
23922
24370
|
boundaryReason: pending.boundaryReason,
|
|
23923
|
-
resolve:
|
|
24371
|
+
resolve: resolve16
|
|
23924
24372
|
});
|
|
23925
24373
|
});
|
|
23926
24374
|
const rule = { tool: "language_package", pattern: pending.suggestedPattern };
|
|
@@ -23955,10 +24403,15 @@ async function startWebUI(opts = {}) {
|
|
|
23955
24403
|
);
|
|
23956
24404
|
}
|
|
23957
24405
|
let _runLock = null;
|
|
24406
|
+
let _runLockSession = null;
|
|
23958
24407
|
const runLockControl = {
|
|
23959
24408
|
get: () => _runLock,
|
|
23960
24409
|
set: (ctrl) => {
|
|
23961
24410
|
_runLock = ctrl;
|
|
24411
|
+
},
|
|
24412
|
+
getSession: () => _runLockSession,
|
|
24413
|
+
setSession: (id) => {
|
|
24414
|
+
_runLockSession = id;
|
|
23962
24415
|
}
|
|
23963
24416
|
};
|
|
23964
24417
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
@@ -24083,6 +24536,7 @@ async function startWebUI(opts = {}) {
|
|
|
24083
24536
|
if (ctrl) {
|
|
24084
24537
|
ctrl.abort();
|
|
24085
24538
|
runLockControl.set(null);
|
|
24539
|
+
runLockControl.setSession(null);
|
|
24086
24540
|
}
|
|
24087
24541
|
},
|
|
24088
24542
|
isRunActive: () => runLockControl.get() !== null,
|
|
@@ -24232,6 +24686,7 @@ async function startWebUI(opts = {}) {
|
|
|
24232
24686
|
})
|
|
24233
24687
|
});
|
|
24234
24688
|
const routes = buildRoutes(state, deps2, cb);
|
|
24689
|
+
let kanbanSupervisorDispose = null;
|
|
24235
24690
|
const handleMessage = createMessageDispatcher({
|
|
24236
24691
|
state,
|
|
24237
24692
|
deps: deps2,
|
|
@@ -24239,7 +24694,10 @@ async function startWebUI(opts = {}) {
|
|
|
24239
24694
|
promptsCtx,
|
|
24240
24695
|
codebaseIndexing,
|
|
24241
24696
|
runLock: runLockControl,
|
|
24242
|
-
pendingConfirms
|
|
24697
|
+
pendingConfirms,
|
|
24698
|
+
onDispose: (dispose) => {
|
|
24699
|
+
kanbanSupervisorDispose = dispose;
|
|
24700
|
+
}
|
|
24243
24701
|
});
|
|
24244
24702
|
const mailbox = getSharedProjectMailbox4(
|
|
24245
24703
|
resolveProjectDir3(context.projectRoot, wstackGlobalRoot2()),
|
|
@@ -24273,7 +24731,14 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
24273
24731
|
priority: "high",
|
|
24274
24732
|
senderSessionId: session.id
|
|
24275
24733
|
}).catch((err) => {
|
|
24276
|
-
console.warn(
|
|
24734
|
+
console.warn(
|
|
24735
|
+
JSON.stringify({
|
|
24736
|
+
level: "warn",
|
|
24737
|
+
event: "webui.security_rejection_mailbox_note_failed",
|
|
24738
|
+
message: String(err),
|
|
24739
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
24740
|
+
})
|
|
24741
|
+
);
|
|
24277
24742
|
});
|
|
24278
24743
|
},
|
|
24279
24744
|
goalHandler,
|
|
@@ -24287,7 +24752,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
24287
24752
|
});
|
|
24288
24753
|
wssPrimary.on("connection", handleConnection);
|
|
24289
24754
|
if (wssSecondary) wssSecondary.on("connection", handleConnection);
|
|
24290
|
-
|
|
24755
|
+
let unregisterShutdown = () => {
|
|
24756
|
+
};
|
|
24757
|
+
unregisterShutdown = registerShutdown({
|
|
24291
24758
|
flushSession: async () => {
|
|
24292
24759
|
await session.append({
|
|
24293
24760
|
type: "session_end",
|
|
@@ -24303,7 +24770,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
24303
24770
|
wssPrimary,
|
|
24304
24771
|
...wssSecondary ? [wssSecondary] : []
|
|
24305
24772
|
],
|
|
24773
|
+
onPreShutdown: () => {
|
|
24774
|
+
kanbanSupervisorDispose?.();
|
|
24775
|
+
kanbanSupervisorDispose = null;
|
|
24776
|
+
},
|
|
24306
24777
|
onShutdown: async () => {
|
|
24778
|
+
unregisterShutdown();
|
|
24307
24779
|
await todosCheckpoint.detach();
|
|
24308
24780
|
await stopHeapWatchdog();
|
|
24309
24781
|
credentialWatcherClose?.();
|