@wrongstack/webui-server 0.302.0 → 0.303.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 +630 -308
- package/dist/server/agent-roster-handlers.d.ts +7 -6
- package/dist/server/codemap-cache.d.ts +13 -1
- 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/entry.js +901 -289
- package/dist/server/handlers/worklist-handlers.d.ts +23 -4
- package/dist/server/handlers.js +126 -23
- package/dist/server/kanban-route-protocol.d.ts +1 -1
- package/dist/server/kanban-routes.d.ts +10 -2
- 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 +22 -3
- 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/index.js
CHANGED
|
@@ -1464,7 +1464,13 @@ function indexDbVersion(projectRoot, indexDir) {
|
|
|
1464
1464
|
try {
|
|
1465
1465
|
const dir = resolveIndexDir(projectRoot, indexDir);
|
|
1466
1466
|
const st = fs.statSync(path.join(dir, DB_FILE));
|
|
1467
|
-
|
|
1467
|
+
let wal = "";
|
|
1468
|
+
try {
|
|
1469
|
+
const walSt = fs.statSync(path.join(dir, `${DB_FILE}-wal`));
|
|
1470
|
+
wal = `:${walSt.mtimeMs}:${walSt.size}`;
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
return `${st.mtimeMs}:${st.size}${wal}`;
|
|
1468
1474
|
} catch {
|
|
1469
1475
|
return "missing";
|
|
1470
1476
|
}
|
|
@@ -4409,7 +4415,8 @@ function createConversationOperations(ctx) {
|
|
|
4409
4415
|
userMessage: async (ws, msg) => {
|
|
4410
4416
|
if (!ensureCurrentSession(ws, msg, "user_message")) return;
|
|
4411
4417
|
const payload = msg.payload ?? {};
|
|
4412
|
-
const
|
|
4418
|
+
const originSessionId = ctx.getSessionId();
|
|
4419
|
+
const controller = ctx.runControl.begin(ws, originSessionId);
|
|
4413
4420
|
if (!controller) {
|
|
4414
4421
|
ctx.send(ws, {
|
|
4415
4422
|
type: "error",
|
|
@@ -4420,7 +4427,6 @@ function createConversationOperations(ctx) {
|
|
|
4420
4427
|
});
|
|
4421
4428
|
return;
|
|
4422
4429
|
}
|
|
4423
|
-
const originSessionId = ctx.getSessionId();
|
|
4424
4430
|
try {
|
|
4425
4431
|
const agent = ctx.getAgent();
|
|
4426
4432
|
if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
|
|
@@ -4479,12 +4485,13 @@ function createConversationOperations(ctx) {
|
|
|
4479
4485
|
});
|
|
4480
4486
|
}
|
|
4481
4487
|
} finally {
|
|
4482
|
-
ctx.runControl.end(ws, controller);
|
|
4488
|
+
ctx.runControl.end(ws, originSessionId, controller);
|
|
4483
4489
|
}
|
|
4484
4490
|
},
|
|
4485
4491
|
abort: (ws, msg) => {
|
|
4486
4492
|
if (!ensureCurrentSession(ws, msg, "abort")) return;
|
|
4487
|
-
ctx.
|
|
4493
|
+
const sessionId = requestedSessionId(msg) ?? ctx.getSessionId();
|
|
4494
|
+
ctx.runControl.abort(ws, sessionId);
|
|
4488
4495
|
ctx.notifyAbort(ws, {
|
|
4489
4496
|
type: "error",
|
|
4490
4497
|
payload: sessionPayload2({ phase: "abort", message: "User aborted" })
|
|
@@ -5536,6 +5543,7 @@ function createConnectionLifecycle(options) {
|
|
|
5536
5543
|
}
|
|
5537
5544
|
|
|
5538
5545
|
// src/server/connections-health-route.ts
|
|
5546
|
+
import * as net from "node:net";
|
|
5539
5547
|
import {
|
|
5540
5548
|
ChronicleProjectServerClient,
|
|
5541
5549
|
createChronicleProjectAccess as createChronicleProjectAccess2,
|
|
@@ -5545,13 +5553,22 @@ import {
|
|
|
5545
5553
|
isMailboxProjectServerAvailable,
|
|
5546
5554
|
MailboxProjectServerConnection
|
|
5547
5555
|
} from "@wrongstack/core/coordination";
|
|
5556
|
+
import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
|
|
5548
5557
|
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
|
|
5549
5558
|
import {
|
|
5550
5559
|
closeKanbanServerConnections,
|
|
5551
5560
|
getKanbanServerConnection,
|
|
5552
5561
|
isKanbanServerAvailable
|
|
5553
5562
|
} from "@wrongstack/kanban";
|
|
5554
|
-
import
|
|
5563
|
+
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5564
|
+
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5565
|
+
import {
|
|
5566
|
+
checkCodebaseIndexServerHealth,
|
|
5567
|
+
ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
|
|
5568
|
+
getIndexState,
|
|
5569
|
+
resolveProjectIndexDaemonAvailability,
|
|
5570
|
+
shutdownCodebaseIndexServer
|
|
5571
|
+
} from "@wrongstack/tools";
|
|
5555
5572
|
|
|
5556
5573
|
// src/server/privileged-actions.ts
|
|
5557
5574
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -5585,15 +5602,6 @@ async function authorizeWebUIAction(boundary, action, logger) {
|
|
|
5585
5602
|
}
|
|
5586
5603
|
|
|
5587
5604
|
// src/server/connections-health-route.ts
|
|
5588
|
-
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5589
|
-
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5590
|
-
import {
|
|
5591
|
-
checkCodebaseIndexServerHealth,
|
|
5592
|
-
ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
|
|
5593
|
-
getIndexState,
|
|
5594
|
-
resolveProjectIndexDaemonAvailability,
|
|
5595
|
-
shutdownCodebaseIndexServer
|
|
5596
|
-
} from "@wrongstack/tools";
|
|
5597
5605
|
async function handleConnectionsHealthRoute(context, ws, message) {
|
|
5598
5606
|
if (message.type !== "connections.health") return false;
|
|
5599
5607
|
try {
|
|
@@ -5614,6 +5622,7 @@ async function handleConnectionsHealthRoute(context, ws, message) {
|
|
|
5614
5622
|
async function collectConnectionsHealth(options) {
|
|
5615
5623
|
const services = await Promise.all([
|
|
5616
5624
|
Promise.resolve(webuiHealth(options.backend)),
|
|
5625
|
+
sessionCatalogHealth(options.projectRoot),
|
|
5617
5626
|
chronicleHealth(options.projectRoot),
|
|
5618
5627
|
codebaseIndexHealth(options.projectRoot, options.indexDir),
|
|
5619
5628
|
sageHealth(options.projectRoot),
|
|
@@ -5631,6 +5640,50 @@ async function collectConnectionsHealth(options) {
|
|
|
5631
5640
|
services
|
|
5632
5641
|
};
|
|
5633
5642
|
}
|
|
5643
|
+
async function sessionCatalogHealth(projectRoot) {
|
|
5644
|
+
const startedAt = Date.now();
|
|
5645
|
+
try {
|
|
5646
|
+
const paths = resolveWstackPaths2({ projectRoot });
|
|
5647
|
+
const client = new SessionCatalogProjectClient({
|
|
5648
|
+
projectDir: paths.projectDir,
|
|
5649
|
+
projectRoot
|
|
5650
|
+
});
|
|
5651
|
+
try {
|
|
5652
|
+
const health = await client.ping();
|
|
5653
|
+
return {
|
|
5654
|
+
id: "session-catalog",
|
|
5655
|
+
label: "Session Catalog",
|
|
5656
|
+
status: health.damagedRows > 0 ? "degraded" : "healthy",
|
|
5657
|
+
required: true,
|
|
5658
|
+
mode: "project-daemon",
|
|
5659
|
+
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).`,
|
|
5660
|
+
ownerPid: health.pid,
|
|
5661
|
+
endpoint: health.endpoint,
|
|
5662
|
+
storage: health.databasePath,
|
|
5663
|
+
uptimeMs: health.uptimeMs,
|
|
5664
|
+
latencyMs: Date.now() - startedAt,
|
|
5665
|
+
clients: health.clients,
|
|
5666
|
+
activeRequests: health.activeRequests,
|
|
5667
|
+
queuedWork: health.reservations + health.maintenanceLeases,
|
|
5668
|
+
control: "none"
|
|
5669
|
+
};
|
|
5670
|
+
} finally {
|
|
5671
|
+
await client.close().catch(() => void 0);
|
|
5672
|
+
}
|
|
5673
|
+
} catch (error2) {
|
|
5674
|
+
return {
|
|
5675
|
+
id: "session-catalog",
|
|
5676
|
+
label: "Session Catalog",
|
|
5677
|
+
status: "error",
|
|
5678
|
+
required: true,
|
|
5679
|
+
mode: "project-daemon",
|
|
5680
|
+
detail: "Project-scoped session ownership and catalog are unavailable.",
|
|
5681
|
+
latencyMs: Date.now() - startedAt,
|
|
5682
|
+
lastError: error2 instanceof Error ? error2.message : String(error2),
|
|
5683
|
+
control: "none"
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5634
5687
|
function webuiHealth(backend) {
|
|
5635
5688
|
return {
|
|
5636
5689
|
id: "webui",
|
|
@@ -6229,7 +6282,11 @@ async function restartSageServer(projectRoot) {
|
|
|
6229
6282
|
});
|
|
6230
6283
|
const verifyConn = new SageProjectServerConnection(projectRoot);
|
|
6231
6284
|
try {
|
|
6232
|
-
await verifyConn.call(
|
|
6285
|
+
await verifyConn.call(
|
|
6286
|
+
"ping",
|
|
6287
|
+
{},
|
|
6288
|
+
{ timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
|
|
6289
|
+
);
|
|
6233
6290
|
return {
|
|
6234
6291
|
serviceId: "sage",
|
|
6235
6292
|
action: "restart",
|
|
@@ -7638,12 +7695,34 @@ function handleTodosGet(ctx, ws) {
|
|
|
7638
7695
|
payload: sessionPayload(ctx, { todos: [...ctx.context.todos] })
|
|
7639
7696
|
});
|
|
7640
7697
|
}
|
|
7641
|
-
function
|
|
7642
|
-
ctx.
|
|
7643
|
-
|
|
7644
|
-
|
|
7698
|
+
async function commitTodos(ctx, todos) {
|
|
7699
|
+
if (ctx.mutateTodos) {
|
|
7700
|
+
const result = await ctx.mutateTodos(todos);
|
|
7701
|
+
return { todos: result.todos, warnings: result.warnings ?? [] };
|
|
7702
|
+
}
|
|
7703
|
+
ctx.replaceTodos?.(todos);
|
|
7704
|
+
return { todos: [...todos], warnings: [] };
|
|
7705
|
+
}
|
|
7706
|
+
function managedProjectionMessage() {
|
|
7707
|
+
return "Kanban-bound todos are task projections. Change or remove the task from Kanban.";
|
|
7645
7708
|
}
|
|
7646
|
-
function
|
|
7709
|
+
async function handleTodosClear(ctx, ws) {
|
|
7710
|
+
if (ctx.context.todos.some((todo) => todo.kanbanBoardId && todo.kanbanTaskId)) {
|
|
7711
|
+
sendResult3(ctx, ws, false, managedProjectionMessage());
|
|
7712
|
+
return;
|
|
7713
|
+
}
|
|
7714
|
+
try {
|
|
7715
|
+
const result = await commitTodos(ctx, []);
|
|
7716
|
+
sendResult3(ctx, ws, true, "Todos cleared");
|
|
7717
|
+
ctx.broadcast({
|
|
7718
|
+
type: "todos.updated",
|
|
7719
|
+
payload: sessionPayload(ctx, { todos: result.todos })
|
|
7720
|
+
});
|
|
7721
|
+
} catch (error2) {
|
|
7722
|
+
sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
|
|
7723
|
+
}
|
|
7724
|
+
}
|
|
7725
|
+
async function handleTodosRemove(ctx, ws, payload) {
|
|
7647
7726
|
if (!payload) {
|
|
7648
7727
|
sendResult3(ctx, ws, false, "Missing id or index");
|
|
7649
7728
|
return;
|
|
@@ -7660,12 +7739,27 @@ function handleTodosRemove(ctx, ws, payload) {
|
|
|
7660
7739
|
sendResult3(ctx, ws, false, "Todo not found");
|
|
7661
7740
|
return;
|
|
7662
7741
|
}
|
|
7742
|
+
if (removed.kanbanBoardId && removed.kanbanTaskId) {
|
|
7743
|
+
sendResult3(ctx, ws, false, managedProjectionMessage());
|
|
7744
|
+
return;
|
|
7745
|
+
}
|
|
7663
7746
|
const next = [...todos.slice(0, targetIndex), ...todos.slice(targetIndex + 1)];
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7747
|
+
try {
|
|
7748
|
+
const result = await commitTodos(ctx, next);
|
|
7749
|
+
sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
|
|
7750
|
+
ctx.broadcast({
|
|
7751
|
+
type: "todos.updated",
|
|
7752
|
+
payload: sessionPayload(ctx, { todos: result.todos })
|
|
7753
|
+
});
|
|
7754
|
+
} catch (error2) {
|
|
7755
|
+
sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
|
|
7756
|
+
}
|
|
7667
7757
|
}
|
|
7668
|
-
function handleTodoUpdate(ctx, ws, payload) {
|
|
7758
|
+
async function handleTodoUpdate(ctx, ws, payload) {
|
|
7759
|
+
if (!payload || typeof payload.id !== "string" || payload.status !== void 0 && payload.status !== "pending" && payload.status !== "in_progress" && payload.status !== "completed" || payload.activeForm !== void 0 && typeof payload.activeForm !== "string") {
|
|
7760
|
+
sendResult3(ctx, ws, false, "Invalid todo update payload");
|
|
7761
|
+
return;
|
|
7762
|
+
}
|
|
7669
7763
|
const index = ctx.context.todos.findIndex((todo) => todo.id === payload.id);
|
|
7670
7764
|
const existing = ctx.context.todos[index];
|
|
7671
7765
|
if (index === -1 || !existing) {
|
|
@@ -7678,9 +7772,25 @@ function handleTodoUpdate(ctx, ws, payload) {
|
|
|
7678
7772
|
status: payload.status ?? existing.status,
|
|
7679
7773
|
activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
|
|
7680
7774
|
};
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7775
|
+
try {
|
|
7776
|
+
const result = await commitTodos(ctx, next);
|
|
7777
|
+
const projected = result.todos.find((todo) => todo.id === existing.id);
|
|
7778
|
+
const requestedStatus = payload.status ?? existing.status;
|
|
7779
|
+
const projectionRejected = Boolean(existing.kanbanBoardId && existing.kanbanTaskId) && projected?.status !== requestedStatus;
|
|
7780
|
+
const warning = result.warnings[0];
|
|
7781
|
+
sendResult3(
|
|
7782
|
+
ctx,
|
|
7783
|
+
ws,
|
|
7784
|
+
!projectionRejected,
|
|
7785
|
+
projectionRejected ? warning ?? `Kanban kept "${existing.content}" at ${projected?.status ?? "its current state"}.` : warning ? `Todo "${existing.content}" updated. ${warning}` : `Todo "${existing.content}" updated`
|
|
7786
|
+
);
|
|
7787
|
+
ctx.broadcast({
|
|
7788
|
+
type: "todos.updated",
|
|
7789
|
+
payload: sessionPayload(ctx, { todos: result.todos })
|
|
7790
|
+
});
|
|
7791
|
+
} catch (error2) {
|
|
7792
|
+
sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
|
|
7793
|
+
}
|
|
7684
7794
|
}
|
|
7685
7795
|
async function handleTasksGet(ctx, ws) {
|
|
7686
7796
|
const taskPath = taskPathOf(ctx);
|
|
@@ -7708,14 +7818,32 @@ async function handleTaskUpdate(ctx, ws, payload) {
|
|
|
7708
7818
|
return;
|
|
7709
7819
|
}
|
|
7710
7820
|
try {
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7821
|
+
let file;
|
|
7822
|
+
if (ctx.mutateTaskStatus) {
|
|
7823
|
+
const result = await ctx.mutateTaskStatus(payload.id, payload.status);
|
|
7824
|
+
if (!result.ok) {
|
|
7825
|
+
sendResult3(ctx, ws, false, result.message);
|
|
7826
|
+
return;
|
|
7827
|
+
}
|
|
7828
|
+
file = await loadTasks(taskPath);
|
|
7829
|
+
if (!file) throw new Error("Task mutation succeeded but its persisted snapshot is missing.");
|
|
7830
|
+
sendResult3(ctx, ws, true, result.message);
|
|
7831
|
+
} else {
|
|
7832
|
+
let matched = false;
|
|
7833
|
+
file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
|
|
7834
|
+
const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
|
|
7835
|
+
if (!task) return tasks;
|
|
7836
|
+
matched = true;
|
|
7837
|
+
task.status = payload.status;
|
|
7838
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7839
|
+
return tasks;
|
|
7840
|
+
});
|
|
7841
|
+
if (!matched) {
|
|
7842
|
+
sendResult3(ctx, ws, false, `Task "${payload.id}" not found.`);
|
|
7843
|
+
return;
|
|
7844
|
+
}
|
|
7845
|
+
sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
|
|
7846
|
+
}
|
|
7719
7847
|
ctx.broadcast({
|
|
7720
7848
|
type: "tasks.updated",
|
|
7721
7849
|
payload: sessionPayload(ctx, { tasks: file.tasks })
|
|
@@ -7762,6 +7890,18 @@ async function handlePlanTemplateUse(ctx, ws, template) {
|
|
|
7762
7890
|
return;
|
|
7763
7891
|
}
|
|
7764
7892
|
try {
|
|
7893
|
+
if (ctx.mutatePlan) {
|
|
7894
|
+
const result = await ctx.mutatePlan({ action: "template_use", template });
|
|
7895
|
+
if (!result.ok) {
|
|
7896
|
+
sendResult3(ctx, ws, false, result.message);
|
|
7897
|
+
return;
|
|
7898
|
+
}
|
|
7899
|
+
const plan2 = await loadPlan(planPath);
|
|
7900
|
+
if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
|
|
7901
|
+
sendResult3(ctx, ws, true, result.message);
|
|
7902
|
+
ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
|
|
7903
|
+
return;
|
|
7904
|
+
}
|
|
7765
7905
|
const templateDefinition = getPlanTemplate(template);
|
|
7766
7906
|
if (!templateDefinition) {
|
|
7767
7907
|
sendResult3(ctx, ws, false, `Unknown template "${template}".`);
|
|
@@ -7790,6 +7930,22 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
|
7790
7930
|
return;
|
|
7791
7931
|
}
|
|
7792
7932
|
try {
|
|
7933
|
+
if (ctx.mutatePlan) {
|
|
7934
|
+
const result = await ctx.mutatePlan({
|
|
7935
|
+
action: "status",
|
|
7936
|
+
target: payload.target,
|
|
7937
|
+
status: payload.status
|
|
7938
|
+
});
|
|
7939
|
+
if (!result.ok) {
|
|
7940
|
+
sendResult3(ctx, ws, false, result.message);
|
|
7941
|
+
return;
|
|
7942
|
+
}
|
|
7943
|
+
const plan2 = await loadPlan(planPath);
|
|
7944
|
+
if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
|
|
7945
|
+
sendResult3(ctx, ws, true, result.message);
|
|
7946
|
+
ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
|
|
7947
|
+
return;
|
|
7948
|
+
}
|
|
7793
7949
|
let changed = false;
|
|
7794
7950
|
const plan = await mutatePlan(planPath, currentSessionId(ctx), async (currentPlan) => {
|
|
7795
7951
|
const before = currentPlan.updatedAt;
|
|
@@ -7813,13 +7969,17 @@ async function handleWorklistMessage(ctx, ws, message) {
|
|
|
7813
7969
|
handleTodosGet(ctx, ws);
|
|
7814
7970
|
return;
|
|
7815
7971
|
case "todos.clear":
|
|
7816
|
-
handleTodosClear(ctx, ws);
|
|
7972
|
+
await handleTodosClear(ctx, ws);
|
|
7817
7973
|
return;
|
|
7818
7974
|
case "todos.remove":
|
|
7819
|
-
handleTodosRemove(
|
|
7975
|
+
await handleTodosRemove(
|
|
7976
|
+
ctx,
|
|
7977
|
+
ws,
|
|
7978
|
+
message.payload
|
|
7979
|
+
);
|
|
7820
7980
|
return;
|
|
7821
7981
|
case "todo.update":
|
|
7822
|
-
handleTodoUpdate(
|
|
7982
|
+
await handleTodoUpdate(
|
|
7823
7983
|
ctx,
|
|
7824
7984
|
ws,
|
|
7825
7985
|
message.payload
|
|
@@ -8248,8 +8408,8 @@ async function handleApiSessions(res, globalRoot) {
|
|
|
8248
8408
|
return;
|
|
8249
8409
|
}
|
|
8250
8410
|
try {
|
|
8251
|
-
const {
|
|
8252
|
-
const registry =
|
|
8411
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8412
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8253
8413
|
const sessions = await registry.list();
|
|
8254
8414
|
const result = sessions.map((s) => ({
|
|
8255
8415
|
sessionId: s.sessionId,
|
|
@@ -8286,8 +8446,8 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
8286
8446
|
return;
|
|
8287
8447
|
}
|
|
8288
8448
|
try {
|
|
8289
|
-
const {
|
|
8290
|
-
const registry =
|
|
8449
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8450
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8291
8451
|
const entry = await registry.get(sessionId);
|
|
8292
8452
|
if (!entry) {
|
|
8293
8453
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8295,20 +8455,22 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
8295
8455
|
return;
|
|
8296
8456
|
}
|
|
8297
8457
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
8298
|
-
res.end(
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8458
|
+
res.end(
|
|
8459
|
+
JSON.stringify({
|
|
8460
|
+
sessionId: entry.sessionId,
|
|
8461
|
+
projectName: entry.projectName,
|
|
8462
|
+
status: entry.status,
|
|
8463
|
+
agents: entry.agents.map((a) => ({
|
|
8464
|
+
id: a.id,
|
|
8465
|
+
name: a.name,
|
|
8466
|
+
status: a.status,
|
|
8467
|
+
currentTool: a.currentTool,
|
|
8468
|
+
iterations: a.iterations,
|
|
8469
|
+
toolCalls: a.toolCalls,
|
|
8470
|
+
lastActivityAt: a.lastActivityAt
|
|
8471
|
+
}))
|
|
8472
|
+
})
|
|
8473
|
+
);
|
|
8312
8474
|
} catch (err) {
|
|
8313
8475
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
8314
8476
|
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
@@ -8426,9 +8588,9 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8426
8588
|
return;
|
|
8427
8589
|
}
|
|
8428
8590
|
try {
|
|
8429
|
-
const {
|
|
8591
|
+
const { getSessionRegistry: getSessionRegistry3, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
|
|
8430
8592
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8431
|
-
const registry =
|
|
8593
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8432
8594
|
const entry = await registry.get(sessionId);
|
|
8433
8595
|
if (!entry) {
|
|
8434
8596
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8436,7 +8598,10 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8436
8598
|
return;
|
|
8437
8599
|
}
|
|
8438
8600
|
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8439
|
-
const store = new DefaultSessionStore4({
|
|
8601
|
+
const store = new DefaultSessionStore4({
|
|
8602
|
+
dir: paths.projectSessions,
|
|
8603
|
+
projectRoot: entry.projectRoot
|
|
8604
|
+
});
|
|
8440
8605
|
const reader = new DefaultSessionReader2({ store });
|
|
8441
8606
|
const RING = Math.max(limit * 4, 2e3);
|
|
8442
8607
|
const ring = [];
|
|
@@ -8528,10 +8693,10 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
8528
8693
|
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
8529
8694
|
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
8530
8695
|
try {
|
|
8531
|
-
const {
|
|
8696
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8532
8697
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8533
8698
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8534
|
-
const registry =
|
|
8699
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8535
8700
|
const entry = await registry.get(sessionId);
|
|
8536
8701
|
if (!entry) {
|
|
8537
8702
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8556,10 +8721,10 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
8556
8721
|
return;
|
|
8557
8722
|
}
|
|
8558
8723
|
try {
|
|
8559
|
-
const {
|
|
8724
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8560
8725
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8561
8726
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8562
|
-
const registry =
|
|
8727
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8563
8728
|
const entry = await registry.get(sessionId);
|
|
8564
8729
|
if (!entry) {
|
|
8565
8730
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8616,10 +8781,10 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
8616
8781
|
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
8617
8782
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
8618
8783
|
try {
|
|
8619
|
-
const {
|
|
8784
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8620
8785
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8621
8786
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8622
|
-
const registry =
|
|
8787
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8623
8788
|
const entry = await registry.get(sessionId);
|
|
8624
8789
|
if (!entry) {
|
|
8625
8790
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8665,10 +8830,10 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
8665
8830
|
}
|
|
8666
8831
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
8667
8832
|
try {
|
|
8668
|
-
const {
|
|
8833
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8669
8834
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8670
8835
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8671
|
-
const registry =
|
|
8836
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8672
8837
|
const all = await registry.list();
|
|
8673
8838
|
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
8674
8839
|
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
@@ -10872,9 +11037,9 @@ import {
|
|
|
10872
11037
|
claimReadyTask,
|
|
10873
11038
|
copyTaskToBoard,
|
|
10874
11039
|
createBoard,
|
|
11040
|
+
createBoardFromText,
|
|
10875
11041
|
duplicateBoard,
|
|
10876
11042
|
exportBoardToTaskGraph,
|
|
10877
|
-
createBoardFromText,
|
|
10878
11043
|
getBoard,
|
|
10879
11044
|
getKanbanOrchestrationSnapshot,
|
|
10880
11045
|
getKanbanQueueHealth,
|
|
@@ -11062,8 +11227,35 @@ async function handleTaskVerification(ws, type, payload, ctx) {
|
|
|
11062
11227
|
}
|
|
11063
11228
|
}
|
|
11064
11229
|
|
|
11230
|
+
// src/server/kanban-route-pagination.ts
|
|
11231
|
+
function paginateKanbanBoards(boards, input) {
|
|
11232
|
+
const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
|
|
11233
|
+
const activeSessionIds = new Set(input.activeSessionIds ?? []);
|
|
11234
|
+
const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
|
|
11235
|
+
const sorted = [...boards].sort((left, right) => {
|
|
11236
|
+
const activityOrder = Number(isActive(right)) - Number(isActive(left));
|
|
11237
|
+
return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
|
|
11238
|
+
});
|
|
11239
|
+
const activeTotal = sorted.filter(isActive).length;
|
|
11240
|
+
const total = sorted.length;
|
|
11241
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
11242
|
+
const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
|
|
11243
|
+
const page = Math.min(totalPages, Math.max(1, requestedPage));
|
|
11244
|
+
const start = (page - 1) * pageSize;
|
|
11245
|
+
return {
|
|
11246
|
+
items: sorted.slice(start, start + pageSize),
|
|
11247
|
+
total,
|
|
11248
|
+
page,
|
|
11249
|
+
pageSize,
|
|
11250
|
+
totalPages,
|
|
11251
|
+
activeTotal,
|
|
11252
|
+
orphanedTotal: total - activeTotal
|
|
11253
|
+
};
|
|
11254
|
+
}
|
|
11255
|
+
|
|
11065
11256
|
// src/server/kanban-task-routes.ts
|
|
11066
11257
|
import {
|
|
11258
|
+
getKanbanWorkbench,
|
|
11067
11259
|
getTask,
|
|
11068
11260
|
listTaskActivity,
|
|
11069
11261
|
recordTaskActivity,
|
|
@@ -11071,6 +11263,16 @@ import {
|
|
|
11071
11263
|
} from "@wrongstack/kanban";
|
|
11072
11264
|
async function handleKanbanTaskRoute(ws, type, payload, ctx) {
|
|
11073
11265
|
switch (type) {
|
|
11266
|
+
case "kanban.workbench":
|
|
11267
|
+
ok(
|
|
11268
|
+
ws,
|
|
11269
|
+
type,
|
|
11270
|
+
await getKanbanWorkbench(ctx.projectRoot, {
|
|
11271
|
+
...typeof payload?.limitPerLane === "number" ? { limitPerLane: payload.limitPerLane } : {},
|
|
11272
|
+
...typeof payload?.alertLimit === "number" ? { alertLimit: payload.alertLimit } : {}
|
|
11273
|
+
})
|
|
11274
|
+
);
|
|
11275
|
+
return true;
|
|
11074
11276
|
case "kanban.task.remove":
|
|
11075
11277
|
await handleTaskRemove(ws, type, payload, ctx);
|
|
11076
11278
|
return true;
|
|
@@ -11163,40 +11365,11 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
|
|
|
11163
11365
|
outcome,
|
|
11164
11366
|
...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
|
|
11165
11367
|
},
|
|
11166
|
-
activityContext(
|
|
11167
|
-
ctx,
|
|
11168
|
-
payload?.actor ?? ctx.context?.agentId ?? "webui"
|
|
11169
|
-
)
|
|
11368
|
+
activityContext(ctx, payload?.actor ?? ctx.context?.agentId ?? "webui")
|
|
11170
11369
|
);
|
|
11171
11370
|
board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
|
|
11172
11371
|
}
|
|
11173
11372
|
|
|
11174
|
-
// src/server/kanban-route-pagination.ts
|
|
11175
|
-
function paginateKanbanBoards(boards, input) {
|
|
11176
|
-
const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
|
|
11177
|
-
const activeSessionIds = new Set(input.activeSessionIds ?? []);
|
|
11178
|
-
const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
|
|
11179
|
-
const sorted = [...boards].sort((left, right) => {
|
|
11180
|
-
const activityOrder = Number(isActive(right)) - Number(isActive(left));
|
|
11181
|
-
return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
|
|
11182
|
-
});
|
|
11183
|
-
const activeTotal = sorted.filter(isActive).length;
|
|
11184
|
-
const total = sorted.length;
|
|
11185
|
-
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
11186
|
-
const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
|
|
11187
|
-
const page = Math.min(totalPages, Math.max(1, requestedPage));
|
|
11188
|
-
const start = (page - 1) * pageSize;
|
|
11189
|
-
return {
|
|
11190
|
-
items: sorted.slice(start, start + pageSize),
|
|
11191
|
-
total,
|
|
11192
|
-
page,
|
|
11193
|
-
pageSize,
|
|
11194
|
-
totalPages,
|
|
11195
|
-
activeTotal,
|
|
11196
|
-
orphanedTotal: total - activeTotal
|
|
11197
|
-
};
|
|
11198
|
-
}
|
|
11199
|
-
|
|
11200
11373
|
// src/server/kanban-route-protocol.ts
|
|
11201
11374
|
var KANBAN_CLIENT_MESSAGE_TYPES = [
|
|
11202
11375
|
"kanban.capabilities",
|
|
@@ -11241,7 +11414,8 @@ var KANBAN_CLIENT_MESSAGE_TYPES = [
|
|
|
11241
11414
|
"kanban.task.verify",
|
|
11242
11415
|
"kanban.taskgraph.export",
|
|
11243
11416
|
"kanban.taskgraph.sync",
|
|
11244
|
-
"kanban.update"
|
|
11417
|
+
"kanban.update",
|
|
11418
|
+
"kanban.workbench"
|
|
11245
11419
|
];
|
|
11246
11420
|
|
|
11247
11421
|
// src/server/kanban-routes.ts
|
|
@@ -11304,6 +11478,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
11304
11478
|
fail(ws, type, `Board not found: ${boardId}`);
|
|
11305
11479
|
return true;
|
|
11306
11480
|
}
|
|
11481
|
+
if (ctx.supervisor) {
|
|
11482
|
+
const snapshots = await ctx.supervisor.auditNow(boardId);
|
|
11483
|
+
const snapshot = snapshots[0];
|
|
11484
|
+
if (snapshot) {
|
|
11485
|
+
ok(ws, type, snapshot);
|
|
11486
|
+
return true;
|
|
11487
|
+
}
|
|
11488
|
+
}
|
|
11307
11489
|
const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
|
|
11308
11490
|
let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
|
|
11309
11491
|
const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
|
|
@@ -12014,7 +12196,9 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
12014
12196
|
projectRoot,
|
|
12015
12197
|
async (event) => {
|
|
12016
12198
|
const family = event.event?.split(".")[0];
|
|
12017
|
-
if (family !== "board" && family !== "task" && family !== "column")
|
|
12199
|
+
if (family !== "board" && family !== "task" && family !== "column" && family !== "contract") {
|
|
12200
|
+
return;
|
|
12201
|
+
}
|
|
12018
12202
|
const evData = event.data;
|
|
12019
12203
|
const boardId = evData?.boardId;
|
|
12020
12204
|
if (!boardId) return;
|
|
@@ -12068,6 +12252,15 @@ function createShutdown(res) {
|
|
|
12068
12252
|
} catch {
|
|
12069
12253
|
}
|
|
12070
12254
|
}
|
|
12255
|
+
if (res.onPreShutdown) {
|
|
12256
|
+
try {
|
|
12257
|
+
await res.onPreShutdown();
|
|
12258
|
+
} catch (e) {
|
|
12259
|
+
log(
|
|
12260
|
+
`[WebUI] Error during pre-shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`
|
|
12261
|
+
);
|
|
12262
|
+
}
|
|
12263
|
+
}
|
|
12071
12264
|
for (const server of res.servers) server?.close();
|
|
12072
12265
|
if (res.onShutdown) {
|
|
12073
12266
|
try {
|
|
@@ -13322,14 +13515,17 @@ async function handleSageForFile(ws, msg, memoryStore) {
|
|
|
13322
13515
|
send(ws, { type: "memory.sage.forFile", payload: { error: "filePath is required" } });
|
|
13323
13516
|
return;
|
|
13324
13517
|
}
|
|
13518
|
+
const includeSuperseded = typeof payload["showSuperseded"] === "boolean" ? payload["showSuperseded"] : typeof payload["includeSuperseded"] === "boolean" ? payload["includeSuperseded"] : void 0;
|
|
13519
|
+
const includeDeleted = payload["showDeleted"] === true || payload["includeDeleted"] === true;
|
|
13325
13520
|
try {
|
|
13326
13521
|
const response = await Sage.findMemoriesForFile(filePath, {
|
|
13327
13522
|
...typeof payload["lineStart"] === "number" ? { lineStart: payload["lineStart"] } : {},
|
|
13328
13523
|
...typeof payload["lineEnd"] === "number" ? { lineEnd: payload["lineEnd"] } : {},
|
|
13329
13524
|
...typeof payload["limit"] === "number" ? { limit: payload["limit"] } : {},
|
|
13330
|
-
...
|
|
13525
|
+
...includeSuperseded !== void 0 ? { includeSuperseded } : {},
|
|
13526
|
+
...includeDeleted ? { includeDeleted: true } : {}
|
|
13331
13527
|
});
|
|
13332
|
-
send(ws, { type: "memory.sage.forFile", payload: response });
|
|
13528
|
+
send(ws, { type: "memory.sage.forFile", payload: { response } });
|
|
13333
13529
|
} catch (err) {
|
|
13334
13530
|
send(ws, { type: "memory.sage.forFile", payload: { error: errMessage(err) } });
|
|
13335
13531
|
}
|
|
@@ -14956,6 +15152,10 @@ import {
|
|
|
14956
15152
|
recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
|
|
14957
15153
|
resolveGateEnforcement
|
|
14958
15154
|
} from "@wrongstack/kanban";
|
|
15155
|
+
function resolveProjectRoot(deps2) {
|
|
15156
|
+
const root = deps2.projectRoot;
|
|
15157
|
+
return typeof root === "function" ? root() : root;
|
|
15158
|
+
}
|
|
14959
15159
|
var DEFAULT_INTERVAL_MS = 1e4;
|
|
14960
15160
|
var MIN_INTERVAL_MS = 2e3;
|
|
14961
15161
|
var DEFAULT_AGENT_COOLDOWN_MS = 5 * 6e4;
|
|
@@ -15020,14 +15220,15 @@ function createKanbanSupervisor(deps2) {
|
|
|
15020
15220
|
publish(snapshot2);
|
|
15021
15221
|
return snapshot2;
|
|
15022
15222
|
}
|
|
15023
|
-
const reconciled = await reconcileKanbanBoard2(deps2
|
|
15223
|
+
const reconciled = await reconcileKanbanBoard2(resolveProjectRoot(deps2), board.id);
|
|
15024
15224
|
const gateSwept = await sweepGateParkedTasks(deps2, reconciled?.board ?? board);
|
|
15025
|
-
let health = await getKanbanQueueHealth2(deps2
|
|
15026
|
-
const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(deps2
|
|
15225
|
+
let health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
|
|
15226
|
+
const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(resolveProjectRoot(deps2), board.id, {
|
|
15027
15227
|
mode: config.recoveryMode ?? "auto",
|
|
15028
15228
|
reason: "Kanban supervisor found an expired worker lease."
|
|
15029
15229
|
}) : null;
|
|
15030
|
-
if (recovered)
|
|
15230
|
+
if (recovered)
|
|
15231
|
+
health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
|
|
15031
15232
|
const anomalyCount = countAnomalies(health);
|
|
15032
15233
|
const snapshot = {
|
|
15033
15234
|
boardId: board.id,
|
|
@@ -15046,7 +15247,7 @@ function createKanbanSupervisor(deps2) {
|
|
|
15046
15247
|
await publishKanbanBoard(
|
|
15047
15248
|
deps2.broadcast,
|
|
15048
15249
|
changedBoard,
|
|
15049
|
-
() => listBoards4(deps2
|
|
15250
|
+
() => listBoards4(resolveProjectRoot(deps2))
|
|
15050
15251
|
);
|
|
15051
15252
|
}
|
|
15052
15253
|
if (config.mode === "agentic" && anomalyCount > 0) {
|
|
@@ -15084,11 +15285,11 @@ function createKanbanSupervisor(deps2) {
|
|
|
15084
15285
|
// tool-runtime boundary gate (`evaluateToolKanbanBoundary`) can resolve
|
|
15085
15286
|
// the live board policy instead of failing open. Whole-board agentic
|
|
15086
15287
|
// runs have no taskId, so only boardId is propagated.
|
|
15087
|
-
context: { kanban: { boardId: board.id, projectRoot: deps2
|
|
15288
|
+
context: { kanban: { boardId: board.id, projectRoot: resolveProjectRoot(deps2) } },
|
|
15088
15289
|
onDone: async (result) => {
|
|
15089
15290
|
clearTimeout(watchdog);
|
|
15090
15291
|
agentRunning.delete(board.id);
|
|
15091
|
-
if (await getBoard3(deps2
|
|
15292
|
+
if (await getBoard3(resolveProjectRoot(deps2), board.id) === null) return;
|
|
15092
15293
|
const current3 = snapshots.get(board.id) ?? snapshot;
|
|
15093
15294
|
publish({
|
|
15094
15295
|
...current3,
|
|
@@ -15112,7 +15313,7 @@ function createKanbanSupervisor(deps2) {
|
|
|
15112
15313
|
const auditNow = async (boardId) => {
|
|
15113
15314
|
let boards;
|
|
15114
15315
|
if (boardId) {
|
|
15115
|
-
const board = await getBoard3(deps2
|
|
15316
|
+
const board = await getBoard3(resolveProjectRoot(deps2), boardId);
|
|
15116
15317
|
if (board === null) {
|
|
15117
15318
|
forgetBoard(boardId);
|
|
15118
15319
|
boards = [];
|
|
@@ -15120,9 +15321,11 @@ function createKanbanSupervisor(deps2) {
|
|
|
15120
15321
|
boards = [board];
|
|
15121
15322
|
}
|
|
15122
15323
|
} else {
|
|
15123
|
-
const summaries = await listBoards4(deps2
|
|
15324
|
+
const summaries = await listBoards4(resolveProjectRoot(deps2));
|
|
15124
15325
|
pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
|
|
15125
|
-
boards = (await Promise.all(
|
|
15326
|
+
boards = (await Promise.all(
|
|
15327
|
+
summaries.map((summary) => getBoard3(resolveProjectRoot(deps2), summary.id))
|
|
15328
|
+
)).filter((board) => Boolean(board));
|
|
15126
15329
|
}
|
|
15127
15330
|
const results = [];
|
|
15128
15331
|
for (const board of boards) results.push(await auditBoard(board));
|
|
@@ -15157,11 +15360,11 @@ function createKanbanSupervisor(deps2) {
|
|
|
15157
15360
|
if (disposed) return;
|
|
15158
15361
|
try {
|
|
15159
15362
|
const now = Date.now();
|
|
15160
|
-
const summaries = await listBoards4(deps2
|
|
15363
|
+
const summaries = await listBoards4(resolveProjectRoot(deps2));
|
|
15161
15364
|
pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
|
|
15162
15365
|
for (const summary of summaries) {
|
|
15163
15366
|
if ((nextDue.get(summary.id) ?? 0) > now) continue;
|
|
15164
|
-
const board = await getBoard3(deps2
|
|
15367
|
+
const board = await getBoard3(resolveProjectRoot(deps2), summary.id);
|
|
15165
15368
|
if (board) await auditBoard(board);
|
|
15166
15369
|
}
|
|
15167
15370
|
} catch (error2) {
|
|
@@ -15204,7 +15407,7 @@ async function sweepGateParkedTasks(deps2, board) {
|
|
|
15204
15407
|
let lastBoard;
|
|
15205
15408
|
for (const task of parked) {
|
|
15206
15409
|
try {
|
|
15207
|
-
const finalized = await finalizeTaskCompletion(deps2
|
|
15410
|
+
const finalized = await finalizeTaskCompletion(resolveProjectRoot(deps2), board.id, task.id, {
|
|
15208
15411
|
eventContext: { actor: "kanban-supervisor" }
|
|
15209
15412
|
});
|
|
15210
15413
|
if (finalized) lastBoard = finalized.board;
|
|
@@ -17994,6 +18197,8 @@ function labelForEvent(e) {
|
|
|
17994
18197
|
const count = e.messagesOmitted ?? e.messages.length;
|
|
17995
18198
|
return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
|
|
17996
18199
|
}
|
|
18200
|
+
case "messages_dropped":
|
|
18201
|
+
return `Oldest ${e.count} message${e.count === 1 ? "" : "s"} evicted`;
|
|
17997
18202
|
case "message_truncated":
|
|
17998
18203
|
return `Message truncated: ${e.before} \u2192 ${e.after}`;
|
|
17999
18204
|
case "file_event":
|
|
@@ -18081,6 +18286,8 @@ function detailForEvent(e) {
|
|
|
18081
18286
|
return `at index ${e.index}`;
|
|
18082
18287
|
case "messages_replaced":
|
|
18083
18288
|
return `${e.messagesOmitted ?? e.messages.length} total`;
|
|
18289
|
+
case "messages_dropped":
|
|
18290
|
+
return `dropped ${e.count} from the front`;
|
|
18084
18291
|
case "message_truncated":
|
|
18085
18292
|
return `truncated to ${e.after} tokens`;
|
|
18086
18293
|
case "mode_changed":
|
|
@@ -18298,7 +18505,7 @@ function createSessionHandlers(ctx) {
|
|
|
18298
18505
|
const current2 = ctx.getSession();
|
|
18299
18506
|
if (current2 !== next) {
|
|
18300
18507
|
try {
|
|
18301
|
-
ctx.abortActiveRun?.();
|
|
18508
|
+
ctx.abortActiveRun?.(current2.id);
|
|
18302
18509
|
} catch {
|
|
18303
18510
|
}
|
|
18304
18511
|
await finalizeSession(current2);
|
|
@@ -18885,16 +19092,17 @@ function createEmbeddedConversationRoutes(ctx) {
|
|
|
18885
19092
|
getAgent: () => ctx.agent,
|
|
18886
19093
|
getSessionId: () => ctx.agent.ctx.session?.id ?? "",
|
|
18887
19094
|
runControl: {
|
|
18888
|
-
begin: (
|
|
18889
|
-
if (ctx.abortControllers.has(
|
|
19095
|
+
begin: (_ws, sessionId) => {
|
|
19096
|
+
if (ctx.abortControllers.has(sessionId)) return void 0;
|
|
18890
19097
|
const controller = new AbortController();
|
|
18891
|
-
ctx.abortControllers.set(
|
|
19098
|
+
ctx.abortControllers.set(sessionId, controller);
|
|
18892
19099
|
return controller;
|
|
18893
19100
|
},
|
|
18894
|
-
end: (
|
|
18895
|
-
if (ctx.abortControllers.get(
|
|
19101
|
+
end: (_ws, sessionId, controller) => {
|
|
19102
|
+
if (ctx.abortControllers.get(sessionId) === controller)
|
|
19103
|
+
ctx.abortControllers.delete(sessionId);
|
|
18896
19104
|
},
|
|
18897
|
-
abort: (
|
|
19105
|
+
abort: (_ws, sessionId) => ctx.abortControllers.get(sessionId)?.abort()
|
|
18898
19106
|
},
|
|
18899
19107
|
pendingConfirms: ctx.pendingConfirms,
|
|
18900
19108
|
send: ctx.send,
|
|
@@ -18989,89 +19197,56 @@ function createEmbeddedProjectRoutes(ctx) {
|
|
|
18989
19197
|
|
|
18990
19198
|
// src/server/embedded-message-router.ts
|
|
18991
19199
|
import { makeProviderFromConfig as makeProviderFromConfig2 } from "@wrongstack/providers";
|
|
19200
|
+
import { planTool, taskTool, todoTool } from "@wrongstack/tools";
|
|
18992
19201
|
|
|
18993
19202
|
// src/server/agent-roster-handlers.ts
|
|
18994
19203
|
import {
|
|
18995
19204
|
applyProjectAgentConfig,
|
|
18996
|
-
buildConsolidationInstruction,
|
|
18997
19205
|
captureLearnedFromAgentOutputDetailed,
|
|
18998
19206
|
clearProjectAgentConsolidated,
|
|
19207
|
+
clearProjectSkillAugmentation,
|
|
18999
19208
|
createProjectAgent,
|
|
19000
19209
|
detectLearnedConflicts,
|
|
19210
|
+
evaluateAutoOptimize,
|
|
19001
19211
|
FLEET_ROSTER,
|
|
19002
19212
|
getProjectAgentLearnStats,
|
|
19003
19213
|
isConsolidated,
|
|
19004
19214
|
listProjectAgentLearnedEntries,
|
|
19005
19215
|
listProjectAgentRoles,
|
|
19216
|
+
listProjectSkillAugmentations,
|
|
19006
19217
|
loadConsolidationMetadata,
|
|
19007
19218
|
loadProjectAgentConfig,
|
|
19008
19219
|
loadProjectAgentConsolidated,
|
|
19009
19220
|
loadProjectAgentIdentity,
|
|
19010
19221
|
loadProjectAgentLearned,
|
|
19011
19222
|
loadProjectAgentProfile,
|
|
19223
|
+
loadProjectSkillAugmentation,
|
|
19224
|
+
loadSkillAffinity,
|
|
19225
|
+
optimizeProjectAgentLearning,
|
|
19226
|
+
readRawLearnedEntries,
|
|
19012
19227
|
resetProjectAgentIdentity,
|
|
19228
|
+
resolveAutoOptimizePolicy,
|
|
19229
|
+
resolveRoleSkillCandidates,
|
|
19013
19230
|
saveProjectAgentConsolidated,
|
|
19231
|
+
saveProjectSkillAugmentation,
|
|
19232
|
+
setSkillPinned,
|
|
19014
19233
|
slugifyProjectAgentRole,
|
|
19015
19234
|
updateProjectAgentConfig,
|
|
19016
19235
|
updateProjectAgentIdentity,
|
|
19017
19236
|
updateProjectAgentLearned,
|
|
19018
19237
|
updateProjectAgentLearningPolicy
|
|
19019
19238
|
} from "@wrongstack/core/coordination";
|
|
19020
|
-
import { isTextBlock } from "@wrongstack/core/types";
|
|
19021
|
-
var CONSOLIDATION_MAX_TOKENS = 8e3;
|
|
19022
|
-
var CONSOLIDATION_TIMEOUT_MS = 12e4;
|
|
19023
19239
|
var AgentRosterWSHandler = class {
|
|
19024
19240
|
getProjectRoot;
|
|
19025
19241
|
getLlm;
|
|
19026
19242
|
broadcast;
|
|
19243
|
+
getAutoOptimizeSettings;
|
|
19027
19244
|
constructor(opts) {
|
|
19028
19245
|
this.getProjectRoot = typeof opts.projectRoot === "function" ? opts.projectRoot : () => opts.projectRoot;
|
|
19029
19246
|
this.getLlm = opts.getLlm ?? (() => void 0);
|
|
19030
19247
|
this.broadcast = opts.broadcast ?? (() => {
|
|
19031
19248
|
});
|
|
19032
|
-
|
|
19033
|
-
/**
|
|
19034
|
-
* Run the consolidation LLM synthesis headlessly and return the cleaned
|
|
19035
|
-
* document text. Returns undefined when no LLM is available so the caller
|
|
19036
|
-
* can fall back to the instruction-only path.
|
|
19037
|
-
*/
|
|
19038
|
-
async synthesizeConsolidation(instruction) {
|
|
19039
|
-
const llm = this.getLlm();
|
|
19040
|
-
if (!llm) return void 0;
|
|
19041
|
-
const req = {
|
|
19042
|
-
model: llm.model,
|
|
19043
|
-
system: [
|
|
19044
|
-
{
|
|
19045
|
-
type: "text",
|
|
19046
|
-
text: "You are a precise technical editor. You consolidate an AI agent role's captured learning entries into a single, durable, role-scoped instruction document. Output ONLY the consolidated markdown document \u2014 no preamble, no code fences, no commentary about the consolidation process."
|
|
19047
|
-
}
|
|
19048
|
-
],
|
|
19049
|
-
messages: [{ role: "user", content: instruction }],
|
|
19050
|
-
maxTokens: CONSOLIDATION_MAX_TOKENS
|
|
19051
|
-
};
|
|
19052
|
-
const timer = new AbortController();
|
|
19053
|
-
let timedOut = false;
|
|
19054
|
-
const to = setTimeout(() => {
|
|
19055
|
-
timedOut = true;
|
|
19056
|
-
timer.abort(new Error("consolidation timeout"));
|
|
19057
|
-
}, CONSOLIDATION_TIMEOUT_MS);
|
|
19058
|
-
to.unref();
|
|
19059
|
-
try {
|
|
19060
|
-
const res = await llm.provider.complete(req, { signal: timer.signal });
|
|
19061
|
-
const text2 = res.content.filter(isTextBlock).map((block) => block.text).join("\n").trim();
|
|
19062
|
-
const wholeDocFence = /^```(?:markdown|md)?[^\n]*\n([\s\S]*?)\n?```\s*$/i;
|
|
19063
|
-
const wrapped = wholeDocFence.exec(text2);
|
|
19064
|
-
const inner = wrapped?.[1];
|
|
19065
|
-
const unfenced = inner !== void 0 ? inner.trim() : text2;
|
|
19066
|
-
return { content: unfenced, model: llm.model };
|
|
19067
|
-
} catch (err) {
|
|
19068
|
-
if (timedOut) {
|
|
19069
|
-
throw new Error(`consolidation timed out after ${CONSOLIDATION_TIMEOUT_MS}ms`);
|
|
19070
|
-
}
|
|
19071
|
-
throw err;
|
|
19072
|
-
} finally {
|
|
19073
|
-
clearTimeout(to);
|
|
19074
|
-
}
|
|
19249
|
+
this.getAutoOptimizeSettings = opts.getAutoOptimizeSettings;
|
|
19075
19250
|
}
|
|
19076
19251
|
/** Handle an incoming client message. Returns a response payload. */
|
|
19077
19252
|
async handleMessage(_ws, type, payload) {
|
|
@@ -19276,70 +19451,47 @@ ${String(p.content ?? "")}`;
|
|
|
19276
19451
|
const conflicts = detectLearnedConflicts(projectRoot);
|
|
19277
19452
|
return { type, payload: { conflicts } };
|
|
19278
19453
|
}
|
|
19279
|
-
// ──
|
|
19280
|
-
//
|
|
19281
|
-
//
|
|
19282
|
-
//
|
|
19283
|
-
|
|
19284
|
-
|
|
19285
|
-
case "agent-roster.consolidate": {
|
|
19454
|
+
// ── Optimize: distil captures into skill addenda + a consolidated doc,
|
|
19455
|
+
// then archive and reset the raw buffer. Shared implementation with the
|
|
19456
|
+
// CLI (`optimizeProjectAgentLearning`) so both surfaces persist the same
|
|
19457
|
+
// artifacts instead of the CLI producing markdown nobody saved.
|
|
19458
|
+
case "agent-roster.consolidate":
|
|
19459
|
+
case "agent-roster.optimize": {
|
|
19286
19460
|
if (!role) return { type, payload: { error: "role required" } };
|
|
19287
|
-
const
|
|
19288
|
-
|
|
19461
|
+
const hasExistingConsolidation = isConsolidated(role, projectRoot);
|
|
19462
|
+
const pending = readRawLearnedEntries(role, projectRoot);
|
|
19463
|
+
if (pending.length === 0) {
|
|
19289
19464
|
return {
|
|
19290
19465
|
type: "agent-roster.consolidate",
|
|
19291
19466
|
payload: {
|
|
19292
19467
|
role,
|
|
19293
19468
|
consolidated: false,
|
|
19294
19469
|
rawEntryCount: 0,
|
|
19470
|
+
skills: [],
|
|
19295
19471
|
hasExistingConsolidation,
|
|
19296
19472
|
currentStats: getProjectAgentLearnStats(role, projectRoot)
|
|
19297
19473
|
}
|
|
19298
19474
|
};
|
|
19299
19475
|
}
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
}
|
|
19316
|
-
if (synth && synth.content.length > 0) {
|
|
19317
|
-
let stats;
|
|
19318
|
-
let metadata;
|
|
19319
|
-
try {
|
|
19320
|
-
saveProjectAgentConsolidated(role, synth.content, projectRoot, {
|
|
19321
|
-
trigger: "manual",
|
|
19322
|
-
model: synth.model
|
|
19323
|
-
});
|
|
19324
|
-
stats = getProjectAgentLearnStats(role, projectRoot);
|
|
19325
|
-
metadata = loadConsolidationMetadata(role, projectRoot);
|
|
19326
|
-
} catch (err) {
|
|
19327
|
-
return {
|
|
19328
|
-
type: "agent-roster.consolidate",
|
|
19329
|
-
payload: {
|
|
19330
|
-
role,
|
|
19331
|
-
consolidated: false,
|
|
19332
|
-
rawEntryCount: rawEntries.length,
|
|
19333
|
-
hasExistingConsolidation,
|
|
19334
|
-
currentStats: getProjectAgentLearnStats(role, projectRoot),
|
|
19335
|
-
error: err instanceof Error ? err.message : "failed to persist consolidation"
|
|
19336
|
-
}
|
|
19337
|
-
};
|
|
19338
|
-
}
|
|
19476
|
+
const llm = this.getLlm();
|
|
19477
|
+
const result = await optimizeProjectAgentLearning(role, projectRoot, {
|
|
19478
|
+
...llm ? { llm } : {},
|
|
19479
|
+
trigger: "manual"
|
|
19480
|
+
});
|
|
19481
|
+
const currentStats = getProjectAgentLearnStats(role, projectRoot);
|
|
19482
|
+
const metadata = loadConsolidationMetadata(role, projectRoot);
|
|
19483
|
+
const basePayload = {
|
|
19484
|
+
role,
|
|
19485
|
+
rawEntryCount: result.rawEntryCount,
|
|
19486
|
+
skills: result.skills,
|
|
19487
|
+
hasExistingConsolidation,
|
|
19488
|
+
currentStats
|
|
19489
|
+
};
|
|
19490
|
+
if (result.status === "optimized") {
|
|
19339
19491
|
try {
|
|
19340
19492
|
this.broadcast({
|
|
19341
19493
|
type: "agent-roster.updated",
|
|
19342
|
-
payload: { role, reason: "consolidated", currentStats
|
|
19494
|
+
payload: { role, reason: "consolidated", currentStats, metadata }
|
|
19343
19495
|
});
|
|
19344
19496
|
} catch (e) {
|
|
19345
19497
|
console.warn(
|
|
@@ -19355,44 +19507,101 @@ ${String(p.content ?? "")}`;
|
|
|
19355
19507
|
return {
|
|
19356
19508
|
type: "agent-roster.consolidate",
|
|
19357
19509
|
payload: {
|
|
19358
|
-
|
|
19510
|
+
...basePayload,
|
|
19359
19511
|
consolidated: true,
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
currentStats: stats,
|
|
19512
|
+
content: result.content,
|
|
19513
|
+
model: result.model,
|
|
19514
|
+
pruned: result.pruned,
|
|
19364
19515
|
metadata
|
|
19365
19516
|
}
|
|
19366
19517
|
};
|
|
19367
19518
|
}
|
|
19368
|
-
if (synth) {
|
|
19369
|
-
return {
|
|
19370
|
-
type: "agent-roster.consolidate",
|
|
19371
|
-
payload: {
|
|
19372
|
-
role,
|
|
19373
|
-
consolidated: false,
|
|
19374
|
-
emptySynthesis: true,
|
|
19375
|
-
model: synth.model,
|
|
19376
|
-
rawEntryCount: rawEntries.length,
|
|
19377
|
-
hasExistingConsolidation,
|
|
19378
|
-
currentStats: getProjectAgentLearnStats(role, projectRoot)
|
|
19379
|
-
}
|
|
19380
|
-
};
|
|
19381
|
-
}
|
|
19382
19519
|
return {
|
|
19383
19520
|
type: "agent-roster.consolidate",
|
|
19384
19521
|
payload: {
|
|
19385
|
-
|
|
19522
|
+
...basePayload,
|
|
19386
19523
|
consolidated: false,
|
|
19387
|
-
|
|
19388
|
-
|
|
19389
|
-
|
|
19390
|
-
|
|
19391
|
-
|
|
19392
|
-
|
|
19524
|
+
...result.status === "empty-synthesis" ? { emptySynthesis: true, model: result.model } : {},
|
|
19525
|
+
...result.status === "failed" ? { error: result.error } : {},
|
|
19526
|
+
...result.status === "no-llm" ? {
|
|
19527
|
+
instruction: result.instruction,
|
|
19528
|
+
leaderInstruction: `Optimize what the "${role}" agent has learned. Read its raw learned entries, synthesize them into a single narrowly-scoped document preserving every fact, and save the result. The instruction text contains the full details and raw entries.`
|
|
19529
|
+
} : {}
|
|
19393
19530
|
}
|
|
19394
19531
|
};
|
|
19395
19532
|
}
|
|
19533
|
+
// ── Automatic-optimization status ─────────────────────────────────
|
|
19534
|
+
// Read-only: says whether the background scheduler considers each role
|
|
19535
|
+
// eligible right now, and why not when it does not. Surfacing the reason
|
|
19536
|
+
// is what keeps "nothing happened" from looking like a broken feature.
|
|
19537
|
+
case "agent-roster.auto-optimize-status": {
|
|
19538
|
+
const policy = resolveAutoOptimizePolicy(
|
|
19539
|
+
this.getAutoOptimizeSettings?.() ?? void 0
|
|
19540
|
+
);
|
|
19541
|
+
const roles = role ? [role] : listProjectAgentRoles(projectRoot);
|
|
19542
|
+
return {
|
|
19543
|
+
type,
|
|
19544
|
+
payload: {
|
|
19545
|
+
policy,
|
|
19546
|
+
roles: roles.map((current2) => {
|
|
19547
|
+
try {
|
|
19548
|
+
const decision = evaluateAutoOptimize(current2, projectRoot, policy);
|
|
19549
|
+
return { role: current2, ...decision };
|
|
19550
|
+
} catch {
|
|
19551
|
+
return { role: current2, eligible: false, reason: "disabled" };
|
|
19552
|
+
}
|
|
19553
|
+
})
|
|
19554
|
+
}
|
|
19555
|
+
};
|
|
19556
|
+
}
|
|
19557
|
+
// ── Skill layer: what this project has developed for each role skill ──
|
|
19558
|
+
case "agent-roster.skills": {
|
|
19559
|
+
if (!role) return { type, payload: { error: "role required" } };
|
|
19560
|
+
const candidates = resolveRoleSkillCandidates(role, projectRoot);
|
|
19561
|
+
const developed = listProjectSkillAugmentations(role, projectRoot);
|
|
19562
|
+
const affinity = loadSkillAffinity(role, projectRoot);
|
|
19563
|
+
return {
|
|
19564
|
+
type,
|
|
19565
|
+
payload: {
|
|
19566
|
+
role,
|
|
19567
|
+
skills: candidates.map((skill) => ({
|
|
19568
|
+
skill,
|
|
19569
|
+
developed: developed.includes(skill),
|
|
19570
|
+
affinity: affinity.entries[skill] ?? null
|
|
19571
|
+
}))
|
|
19572
|
+
}
|
|
19573
|
+
};
|
|
19574
|
+
}
|
|
19575
|
+
case "agent-roster.read-skill": {
|
|
19576
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
19577
|
+
if (!role || !skill) return { type, payload: { error: "role and skill required" } };
|
|
19578
|
+
return {
|
|
19579
|
+
type,
|
|
19580
|
+
payload: { role, skill, content: loadProjectSkillAugmentation(role, skill, projectRoot) }
|
|
19581
|
+
};
|
|
19582
|
+
}
|
|
19583
|
+
case "agent-roster.save-skill": {
|
|
19584
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
19585
|
+
if (!role || !skill || typeof p.content !== "string") {
|
|
19586
|
+
return { type, payload: { error: "role, skill and content required" } };
|
|
19587
|
+
}
|
|
19588
|
+
const savedPath = saveProjectSkillAugmentation(role, skill, p.content, projectRoot);
|
|
19589
|
+
return { type, payload: { role, skill, path: savedPath, success: true } };
|
|
19590
|
+
}
|
|
19591
|
+
case "agent-roster.clear-skill": {
|
|
19592
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
19593
|
+
if (!role) return { type, payload: { error: "role required" } };
|
|
19594
|
+
clearProjectSkillAugmentation(role, skill || void 0, projectRoot);
|
|
19595
|
+
return { type, payload: { role, skill: skill || null, success: true } };
|
|
19596
|
+
}
|
|
19597
|
+
case "agent-roster.pin-skill": {
|
|
19598
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
19599
|
+
if (!role || !skill || typeof p.pinned !== "boolean") {
|
|
19600
|
+
return { type, payload: { error: "role, skill and boolean pinned required" } };
|
|
19601
|
+
}
|
|
19602
|
+
const affinity = setSkillPinned(role, skill, p.pinned, projectRoot);
|
|
19603
|
+
return { type, payload: { role, skill, pinned: p.pinned, affinity, success: true } };
|
|
19604
|
+
}
|
|
19396
19605
|
// ── Save consolidated document ────────────────────────────────────
|
|
19397
19606
|
case "agent-roster.save-consolidated": {
|
|
19398
19607
|
if (!role || typeof p.content !== "string") {
|
|
@@ -20316,9 +20525,13 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
20316
20525
|
// stream from the previous session would otherwise keep running in the
|
|
20317
20526
|
// background after session.new/resume. The run's own end() cleanup
|
|
20318
20527
|
// removes controllers from the map when it unwinds.
|
|
20319
|
-
abortActiveRun: () => {
|
|
20320
|
-
|
|
20321
|
-
|
|
20528
|
+
abortActiveRun: (sessionId) => {
|
|
20529
|
+
if (sessionId) {
|
|
20530
|
+
deps2.conversationCtx.abortControllers.get(sessionId)?.abort();
|
|
20531
|
+
} else {
|
|
20532
|
+
for (const controller of [...deps2.conversationCtx.abortControllers.values()]) {
|
|
20533
|
+
controller.abort();
|
|
20534
|
+
}
|
|
20322
20535
|
}
|
|
20323
20536
|
},
|
|
20324
20537
|
isRunActive: () => deps2.conversationCtx.abortControllers.size > 0
|
|
@@ -20361,7 +20574,20 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
20361
20574
|
},
|
|
20362
20575
|
send: send2,
|
|
20363
20576
|
broadcast: deps2.providerCtx.broadcast,
|
|
20364
|
-
replaceTodos: (todos) => opts.agent.ctx.state.replaceTodos(todos)
|
|
20577
|
+
replaceTodos: (todos) => opts.agent.ctx.state.replaceTodos(todos),
|
|
20578
|
+
mutateTodos: async (todos) => {
|
|
20579
|
+
const result = await todoTool.execute({ todos }, opts.agent.ctx, {
|
|
20580
|
+
signal: AbortSignal.timeout(3e4)
|
|
20581
|
+
});
|
|
20582
|
+
return {
|
|
20583
|
+
todos: [...opts.agent.ctx.todos],
|
|
20584
|
+
...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
|
|
20585
|
+
};
|
|
20586
|
+
},
|
|
20587
|
+
mutateTaskStatus: async (id, status) => taskTool.execute({ action: "status", id, status }, opts.agent.ctx, {
|
|
20588
|
+
signal: AbortSignal.timeout(3e4)
|
|
20589
|
+
}),
|
|
20590
|
+
mutatePlan: async (operation) => planTool.execute(operation, opts.agent.ctx, { signal: AbortSignal.timeout(3e4) })
|
|
20365
20591
|
})
|
|
20366
20592
|
});
|
|
20367
20593
|
const processRoutes = {
|
|
@@ -20445,6 +20671,7 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
20445
20671
|
agentRoster: {
|
|
20446
20672
|
rosterHandler: new AgentRosterWSHandler({
|
|
20447
20673
|
projectRoot,
|
|
20674
|
+
getAutoOptimizeSettings: () => deps2.agentConfigCtx.getConfig?.()?.fleet?.learning?.autoOptimize,
|
|
20448
20675
|
getLlm: () => {
|
|
20449
20676
|
const ctx = opts.agent.ctx;
|
|
20450
20677
|
return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
|
|
@@ -21496,7 +21723,6 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
21496
21723
|
}
|
|
21497
21724
|
|
|
21498
21725
|
// src/server/setup-events-fleet-broadcaster.ts
|
|
21499
|
-
import { watch as fsWatch } from "node:fs";
|
|
21500
21726
|
import * as path25 from "node:path";
|
|
21501
21727
|
function registerSetupEventsFleetBroadcaster(deps2) {
|
|
21502
21728
|
const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
|
|
@@ -21505,8 +21731,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
21505
21731
|
const disposers = [];
|
|
21506
21732
|
const broadcastSessions = async () => {
|
|
21507
21733
|
try {
|
|
21508
|
-
const {
|
|
21509
|
-
const registry =
|
|
21734
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
21735
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
21510
21736
|
const sessions = await registry.list();
|
|
21511
21737
|
const ownEntry = sessions.find((s) => s.pid === process.pid);
|
|
21512
21738
|
const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
|
|
@@ -21553,7 +21779,7 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
21553
21779
|
}
|
|
21554
21780
|
};
|
|
21555
21781
|
onFleetBroadcaster?.(broadcastSessions);
|
|
21556
|
-
let
|
|
21782
|
+
let subscriptionLive = false;
|
|
21557
21783
|
let statusTimer;
|
|
21558
21784
|
const scheduleStatusPoll = () => {
|
|
21559
21785
|
if (isDisposed()) return;
|
|
@@ -21562,35 +21788,31 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
21562
21788
|
void broadcastSessions();
|
|
21563
21789
|
scheduleStatusPoll();
|
|
21564
21790
|
},
|
|
21565
|
-
|
|
21791
|
+
subscriptionLive ? 3e4 : 5e3
|
|
21566
21792
|
);
|
|
21567
21793
|
if (statusTimer.unref) statusTimer.unref();
|
|
21568
21794
|
};
|
|
21569
21795
|
disposers.push(() => {
|
|
21570
21796
|
if (statusTimer) clearTimeout(statusTimer);
|
|
21571
21797
|
});
|
|
21572
|
-
let
|
|
21573
|
-
|
|
21574
|
-
|
|
21575
|
-
|
|
21576
|
-
|
|
21577
|
-
|
|
21578
|
-
|
|
21579
|
-
|
|
21580
|
-
|
|
21581
|
-
|
|
21582
|
-
|
|
21583
|
-
|
|
21584
|
-
|
|
21585
|
-
|
|
21586
|
-
|
|
21587
|
-
|
|
21588
|
-
|
|
21589
|
-
|
|
21590
|
-
regWatcher.close();
|
|
21591
|
-
});
|
|
21592
|
-
} catch {
|
|
21593
|
-
}
|
|
21798
|
+
let eventDebounce;
|
|
21799
|
+
let unsubscribe;
|
|
21800
|
+
void import("@wrongstack/core/storage").then(async ({ getSessionRegistry: getSessionRegistry3 }) => {
|
|
21801
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
21802
|
+
const projectSlug2 = wpaths?.projectSlug;
|
|
21803
|
+
if (!projectSlug2 || isDisposed()) return;
|
|
21804
|
+
unsubscribe = await registry.subscribeProject(projectSlug2, context.projectRoot, () => {
|
|
21805
|
+
if (eventDebounce) clearTimeout(eventDebounce);
|
|
21806
|
+
eventDebounce = setTimeout(() => void broadcastSessions(), 25);
|
|
21807
|
+
});
|
|
21808
|
+
subscriptionLive = true;
|
|
21809
|
+
}).catch(() => {
|
|
21810
|
+
subscriptionLive = false;
|
|
21811
|
+
});
|
|
21812
|
+
disposers.push(() => {
|
|
21813
|
+
if (eventDebounce) clearTimeout(eventDebounce);
|
|
21814
|
+
void unsubscribe?.();
|
|
21815
|
+
});
|
|
21594
21816
|
scheduleStatusPoll();
|
|
21595
21817
|
void broadcastSessions();
|
|
21596
21818
|
return () => {
|
|
@@ -21703,7 +21925,7 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
|
|
|
21703
21925
|
}
|
|
21704
21926
|
|
|
21705
21927
|
// src/server/setup-events-status-watcher.ts
|
|
21706
|
-
import { watch as
|
|
21928
|
+
import { watch as fsWatch } from "node:fs";
|
|
21707
21929
|
import * as fs20 from "node:fs/promises";
|
|
21708
21930
|
import * as path27 from "node:path";
|
|
21709
21931
|
|
|
@@ -21793,7 +22015,7 @@ function registerSetupEventsStatusWatcher(deps2) {
|
|
|
21793
22015
|
try {
|
|
21794
22016
|
await fs20.mkdir(projectsDir, { recursive: true });
|
|
21795
22017
|
if (isDisposed()) return;
|
|
21796
|
-
watcher =
|
|
22018
|
+
watcher = fsWatch(
|
|
21797
22019
|
projectsDir,
|
|
21798
22020
|
{ persistent: true, recursive: true },
|
|
21799
22021
|
async (eventType, filename) => {
|
|
@@ -22955,7 +23177,6 @@ import {
|
|
|
22955
23177
|
mailboxSessionTag,
|
|
22956
23178
|
ObservableBrainArbiter as ObservableBrainArbiterCtor
|
|
22957
23179
|
} from "@wrongstack/core/coordination";
|
|
22958
|
-
import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
|
|
22959
23180
|
import { installDesignStudioMiddleware } from "@wrongstack/core/design";
|
|
22960
23181
|
import {
|
|
22961
23182
|
AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
|
|
@@ -22967,6 +23188,7 @@ import {
|
|
|
22967
23188
|
import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
|
|
22968
23189
|
import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
|
|
22969
23190
|
import {
|
|
23191
|
+
DEFAULT_TOOLS_CONFIG,
|
|
22970
23192
|
resolveContextWindowPolicy as resolveContextWindowPolicy2
|
|
22971
23193
|
} from "@wrongstack/core/types";
|
|
22972
23194
|
import {
|
|
@@ -23848,6 +24070,11 @@ async function createAgentServices(input) {
|
|
|
23848
24070
|
taskAware: config.Sage?.inject?.taskAware,
|
|
23849
24071
|
minScore: config.Sage?.inject?.minScore,
|
|
23850
24072
|
minImportance: config.Sage?.inject?.minImportance,
|
|
24073
|
+
// Forward the explicit relation floor so an operator-configured
|
|
24074
|
+
// `Sage.inject.relationFloor` is honored in WebUI sessions. Without
|
|
24075
|
+
// this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
|
|
24076
|
+
// is the CLI default but masks operator overrides.
|
|
24077
|
+
relationFloor: config.Sage?.inject?.relationFloor,
|
|
23851
24078
|
repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
|
|
23852
24079
|
verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
|
|
23853
24080
|
triggers: config.Sage?.inject?.triggers,
|
|
@@ -23869,6 +24096,10 @@ async function createAgentServices(input) {
|
|
|
23869
24096
|
maxMemories: config.Sage?.inject?.maxTurnMemories,
|
|
23870
24097
|
maxChars: config.Sage?.inject?.maxCharsPerTurn,
|
|
23871
24098
|
minScore: config.Sage?.inject?.minScore,
|
|
24099
|
+
// CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
|
|
24100
|
+
// value drives both runtimes instead of silently falling back to the
|
|
24101
|
+
// 0.3 default. The undefined case keeps the middleware's own default.
|
|
24102
|
+
metadataWeight: config.Sage?.retrieval?.metadataWeight,
|
|
23872
24103
|
getSessionId: getSageSessionId,
|
|
23873
24104
|
tracker: sageInjectionTracker
|
|
23874
24105
|
})
|
|
@@ -23893,25 +24124,29 @@ async function createAgentServices(input) {
|
|
|
23893
24124
|
strategy: config.context?.strategy,
|
|
23894
24125
|
preserveK: config.context?.preserveK ?? 10,
|
|
23895
24126
|
eliseThreshold: config.context?.eliseThreshold ?? 2e3,
|
|
24127
|
+
// Match the CLI/TUI runtime: keep corrections, errors and decisions
|
|
24128
|
+
// verbatim while collapsing routine assistant chatter/tool protocol.
|
|
24129
|
+
// Without this WebUI's hybrid strategy builds an ever-growing lossless
|
|
24130
|
+
// digest and eventually relies on blunt emergency head/tail trimming.
|
|
24131
|
+
smart: true,
|
|
23896
24132
|
summarizerModel: config.context?.summarizerModel,
|
|
23897
24133
|
llmSelector: config.context?.llmSelector
|
|
23898
24134
|
});
|
|
23899
24135
|
const initialContextPolicy = resolveContextWindowPolicy2(config.context);
|
|
23900
24136
|
let autoCompactor;
|
|
23901
24137
|
if (config.context?.autoCompact !== false) {
|
|
23902
|
-
let effectiveMaxContext =
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
|
|
23911
|
-
|
|
23912
|
-
} catch {
|
|
23913
|
-
}
|
|
24138
|
+
let effectiveMaxContext = 0;
|
|
24139
|
+
try {
|
|
24140
|
+
const m = await resolveProviderModelMetadata(
|
|
24141
|
+
modelsRegistry,
|
|
24142
|
+
config.provider,
|
|
24143
|
+
context.model,
|
|
24144
|
+
config.providers?.[config.provider]
|
|
24145
|
+
);
|
|
24146
|
+
effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
|
|
24147
|
+
} catch {
|
|
23914
24148
|
}
|
|
24149
|
+
if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
|
|
23915
24150
|
if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
|
|
23916
24151
|
autoCompactor = new AutoCompactionMiddlewareCtor(
|
|
23917
24152
|
compactor,
|
|
@@ -24432,6 +24667,7 @@ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES)
|
|
|
24432
24667
|
|
|
24433
24668
|
// src/server/message-dispatcher.ts
|
|
24434
24669
|
import path28 from "node:path";
|
|
24670
|
+
import { planTool as planTool2, taskTool as taskTool2, todoTool as todoTool2 } from "@wrongstack/tools";
|
|
24435
24671
|
function createMessageDispatcher(opts) {
|
|
24436
24672
|
const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
|
|
24437
24673
|
function makeWorklistContext() {
|
|
@@ -24443,7 +24679,20 @@ function createMessageDispatcher(opts) {
|
|
|
24443
24679
|
},
|
|
24444
24680
|
send: (w, m) => send(w, m),
|
|
24445
24681
|
broadcast: (m) => broadcast(state.getClients(), m),
|
|
24446
|
-
replaceTodos: (todos) => deps2.context.state.replaceTodos(todos)
|
|
24682
|
+
replaceTodos: (todos) => deps2.context.state.replaceTodos(todos),
|
|
24683
|
+
mutateTodos: async (todos) => {
|
|
24684
|
+
const result = await todoTool2.execute({ todos }, deps2.context, {
|
|
24685
|
+
signal: AbortSignal.timeout(3e4)
|
|
24686
|
+
});
|
|
24687
|
+
return {
|
|
24688
|
+
todos: [...deps2.context.todos],
|
|
24689
|
+
...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
|
|
24690
|
+
};
|
|
24691
|
+
},
|
|
24692
|
+
mutateTaskStatus: async (id, status) => taskTool2.execute({ action: "status", id, status }, deps2.context, {
|
|
24693
|
+
signal: AbortSignal.timeout(3e4)
|
|
24694
|
+
}),
|
|
24695
|
+
mutatePlan: async (operation) => planTool2.execute(operation, deps2.context, { signal: AbortSignal.timeout(3e4) })
|
|
24447
24696
|
};
|
|
24448
24697
|
}
|
|
24449
24698
|
function makeSkillsContext() {
|
|
@@ -24533,16 +24782,24 @@ function createMessageDispatcher(opts) {
|
|
|
24533
24782
|
getAgent: () => deps2.agent,
|
|
24534
24783
|
getSessionId: () => state.getSession().id,
|
|
24535
24784
|
runControl: {
|
|
24536
|
-
begin: () => {
|
|
24785
|
+
begin: (_ws, sessionId) => {
|
|
24537
24786
|
if (runLock.get()) return void 0;
|
|
24538
24787
|
const controller = new AbortController();
|
|
24539
24788
|
runLock.set(controller);
|
|
24789
|
+
runLock.setSession(sessionId);
|
|
24540
24790
|
return controller;
|
|
24541
24791
|
},
|
|
24542
|
-
end: (_ws, controller) => {
|
|
24543
|
-
if (runLock.get() === controller)
|
|
24792
|
+
end: (_ws, _sessionId, controller) => {
|
|
24793
|
+
if (runLock.get() === controller) {
|
|
24794
|
+
runLock.set(null);
|
|
24795
|
+
runLock.setSession(null);
|
|
24796
|
+
}
|
|
24544
24797
|
},
|
|
24545
|
-
abort: () =>
|
|
24798
|
+
abort: (_ws, sessionId) => {
|
|
24799
|
+
if (runLock.getSession() === sessionId || !runLock.getSession()) {
|
|
24800
|
+
runLock.get()?.abort();
|
|
24801
|
+
}
|
|
24802
|
+
}
|
|
24546
24803
|
},
|
|
24547
24804
|
pendingConfirms,
|
|
24548
24805
|
send,
|
|
@@ -24564,10 +24821,20 @@ function createMessageDispatcher(opts) {
|
|
|
24564
24821
|
const goalSnapshotRoutes = {
|
|
24565
24822
|
getSnapshot: () => handleGoalGet(state.getProjectRoot(), (message) => broadcast(state.getClients(), message))
|
|
24566
24823
|
};
|
|
24824
|
+
const kanbanSupervisor = createKanbanSupervisor({
|
|
24825
|
+
projectRoot: () => state.getProjectRoot(),
|
|
24826
|
+
broadcast: (message) => broadcast(state.getClients(), message),
|
|
24827
|
+
log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
|
|
24828
|
+
});
|
|
24829
|
+
if (opts.onDispose) {
|
|
24830
|
+
const dispose = () => kanbanSupervisor.dispose();
|
|
24831
|
+
opts.onDispose(dispose);
|
|
24832
|
+
}
|
|
24567
24833
|
const kanbanContext = () => ({
|
|
24568
24834
|
projectRoot: state.getProjectRoot(),
|
|
24569
24835
|
context: deps2.context,
|
|
24570
|
-
broadcast: (message) => broadcast(state.getClients(), message)
|
|
24836
|
+
broadcast: (message) => broadcast(state.getClients(), message),
|
|
24837
|
+
supervisor: kanbanSupervisor
|
|
24571
24838
|
});
|
|
24572
24839
|
const kanbanHostRoutes = {
|
|
24573
24840
|
meta: async (ws) => {
|
|
@@ -24637,6 +24904,7 @@ function createMessageDispatcher(opts) {
|
|
|
24637
24904
|
agentRoster: {
|
|
24638
24905
|
rosterHandler: new AgentRosterWSHandler({
|
|
24639
24906
|
projectRoot: state.getProjectRoot,
|
|
24907
|
+
getAutoOptimizeSettings: () => state.getConfig().fleet?.learning?.autoOptimize,
|
|
24640
24908
|
getLlm: () => {
|
|
24641
24909
|
const ctx = deps2.agent.ctx;
|
|
24642
24910
|
return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
|
|
@@ -25083,6 +25351,14 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
|
|
|
25083
25351
|
transition = transition.then(async () => {
|
|
25084
25352
|
if (stopped) return;
|
|
25085
25353
|
if (pendingClaim?.sessionId === sessionId) {
|
|
25354
|
+
await pendingClaim.claim.activate({
|
|
25355
|
+
sessionId,
|
|
25356
|
+
...target,
|
|
25357
|
+
clientType: "webui",
|
|
25358
|
+
pid: process.pid,
|
|
25359
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25360
|
+
agents: statusTracker.getAgents()
|
|
25361
|
+
});
|
|
25086
25362
|
pendingClaim = void 0;
|
|
25087
25363
|
} else {
|
|
25088
25364
|
await register(sessionId, true, target);
|
|
@@ -25106,14 +25382,35 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
|
|
|
25106
25382
|
}
|
|
25107
25383
|
if (sessionId === activeSessionId) return async () => {
|
|
25108
25384
|
};
|
|
25109
|
-
const previousSessionId = activeSessionId;
|
|
25110
|
-
const previousTarget = activeTarget;
|
|
25111
25385
|
const token = Symbol(sessionId);
|
|
25112
|
-
|
|
25113
|
-
|
|
25386
|
+
if ("reserveResume" in registry && typeof registry.reserveResume === "function") {
|
|
25387
|
+
const reservation = await registry.reserveResume({
|
|
25388
|
+
sessionId,
|
|
25389
|
+
projectSlug: target.projectSlug,
|
|
25390
|
+
projectRoot: target.projectRoot
|
|
25391
|
+
});
|
|
25392
|
+
pendingClaim = { sessionId, token, claim: reservation, target };
|
|
25393
|
+
} else {
|
|
25394
|
+
await register(sessionId, true, target);
|
|
25395
|
+
pendingClaim = {
|
|
25396
|
+
sessionId,
|
|
25397
|
+
token,
|
|
25398
|
+
target,
|
|
25399
|
+
claim: {
|
|
25400
|
+
reservation: {
|
|
25401
|
+
reservationId: "legacy",
|
|
25402
|
+
targetSessionId: sessionId,
|
|
25403
|
+
requesterInstanceId: "legacy",
|
|
25404
|
+
expiresAt: Number.MAX_SAFE_INTEGER
|
|
25405
|
+
},
|
|
25406
|
+
activate: async () => void 0,
|
|
25407
|
+
cancel: async () => register(activeSessionId, true, activeTarget)
|
|
25408
|
+
}
|
|
25409
|
+
};
|
|
25410
|
+
}
|
|
25114
25411
|
return async () => {
|
|
25115
25412
|
if (pendingClaim?.token !== token) return;
|
|
25116
|
-
await
|
|
25413
|
+
await pendingClaim.claim.cancel();
|
|
25117
25414
|
pendingClaim = void 0;
|
|
25118
25415
|
};
|
|
25119
25416
|
};
|
|
@@ -25818,11 +26115,11 @@ function buildRoutes(state, deps2, cb) {
|
|
|
25818
26115
|
}
|
|
25819
26116
|
|
|
25820
26117
|
// src/server/server-runtime.ts
|
|
25821
|
-
import * as path33 from "node:path";
|
|
25822
26118
|
import { createRequire as createRequire4 } from "node:module";
|
|
26119
|
+
import * as path33 from "node:path";
|
|
25823
26120
|
import { fileURLToPath } from "node:url";
|
|
25824
|
-
import { WebSocketServer } from "ws";
|
|
25825
26121
|
import { toErrorMessage as toErrorMessage13 } from "@wrongstack/core/utils";
|
|
26122
|
+
import { WebSocketServer } from "ws";
|
|
25826
26123
|
async function resolvePorts(opts) {
|
|
25827
26124
|
const surface = opts.surface ?? "webui";
|
|
25828
26125
|
const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
|
|
@@ -26040,10 +26337,11 @@ function startHttpServer(opts) {
|
|
|
26040
26337
|
return httpServer;
|
|
26041
26338
|
}
|
|
26042
26339
|
function registerShutdown(deps2) {
|
|
26043
|
-
registerShutdownHandlers({
|
|
26340
|
+
return registerShutdownHandlers({
|
|
26044
26341
|
flushSession: deps2.flushSession,
|
|
26045
26342
|
clients: deps2.clients,
|
|
26046
26343
|
servers: deps2.servers,
|
|
26344
|
+
onPreShutdown: deps2.onPreShutdown,
|
|
26047
26345
|
onShutdown: deps2.onShutdown
|
|
26048
26346
|
});
|
|
26049
26347
|
}
|
|
@@ -26358,10 +26656,15 @@ async function startWebUI(opts = {}) {
|
|
|
26358
26656
|
);
|
|
26359
26657
|
}
|
|
26360
26658
|
let _runLock = null;
|
|
26659
|
+
let _runLockSession = null;
|
|
26361
26660
|
const runLockControl = {
|
|
26362
26661
|
get: () => _runLock,
|
|
26363
26662
|
set: (ctrl) => {
|
|
26364
26663
|
_runLock = ctrl;
|
|
26664
|
+
},
|
|
26665
|
+
getSession: () => _runLockSession,
|
|
26666
|
+
setSession: (id) => {
|
|
26667
|
+
_runLockSession = id;
|
|
26365
26668
|
}
|
|
26366
26669
|
};
|
|
26367
26670
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
@@ -26486,6 +26789,7 @@ async function startWebUI(opts = {}) {
|
|
|
26486
26789
|
if (ctrl) {
|
|
26487
26790
|
ctrl.abort();
|
|
26488
26791
|
runLockControl.set(null);
|
|
26792
|
+
runLockControl.setSession(null);
|
|
26489
26793
|
}
|
|
26490
26794
|
},
|
|
26491
26795
|
isRunActive: () => runLockControl.get() !== null,
|
|
@@ -26635,6 +26939,7 @@ async function startWebUI(opts = {}) {
|
|
|
26635
26939
|
})
|
|
26636
26940
|
});
|
|
26637
26941
|
const routes = buildRoutes(state, deps2, cb);
|
|
26942
|
+
let kanbanSupervisorDispose = null;
|
|
26638
26943
|
const handleMessage = createMessageDispatcher({
|
|
26639
26944
|
state,
|
|
26640
26945
|
deps: deps2,
|
|
@@ -26642,7 +26947,10 @@ async function startWebUI(opts = {}) {
|
|
|
26642
26947
|
promptsCtx,
|
|
26643
26948
|
codebaseIndexing,
|
|
26644
26949
|
runLock: runLockControl,
|
|
26645
|
-
pendingConfirms
|
|
26950
|
+
pendingConfirms,
|
|
26951
|
+
onDispose: (dispose) => {
|
|
26952
|
+
kanbanSupervisorDispose = dispose;
|
|
26953
|
+
}
|
|
26646
26954
|
});
|
|
26647
26955
|
const mailbox = getSharedProjectMailbox5(
|
|
26648
26956
|
resolveProjectDir4(context.projectRoot, wstackGlobalRoot4()),
|
|
@@ -26676,7 +26984,14 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
26676
26984
|
priority: "high",
|
|
26677
26985
|
senderSessionId: session.id
|
|
26678
26986
|
}).catch((err) => {
|
|
26679
|
-
console.warn(
|
|
26987
|
+
console.warn(
|
|
26988
|
+
JSON.stringify({
|
|
26989
|
+
level: "warn",
|
|
26990
|
+
event: "webui.security_rejection_mailbox_note_failed",
|
|
26991
|
+
message: String(err),
|
|
26992
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
26993
|
+
})
|
|
26994
|
+
);
|
|
26680
26995
|
});
|
|
26681
26996
|
},
|
|
26682
26997
|
goalHandler,
|
|
@@ -26690,7 +27005,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
26690
27005
|
});
|
|
26691
27006
|
wssPrimary.on("connection", handleConnection);
|
|
26692
27007
|
if (wssSecondary) wssSecondary.on("connection", handleConnection);
|
|
26693
|
-
|
|
27008
|
+
let unregisterShutdown = () => {
|
|
27009
|
+
};
|
|
27010
|
+
unregisterShutdown = registerShutdown({
|
|
26694
27011
|
flushSession: async () => {
|
|
26695
27012
|
await session.append({
|
|
26696
27013
|
type: "session_end",
|
|
@@ -26706,7 +27023,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
26706
27023
|
wssPrimary,
|
|
26707
27024
|
...wssSecondary ? [wssSecondary] : []
|
|
26708
27025
|
],
|
|
27026
|
+
onPreShutdown: () => {
|
|
27027
|
+
kanbanSupervisorDispose?.();
|
|
27028
|
+
kanbanSupervisorDispose = null;
|
|
27029
|
+
},
|
|
26709
27030
|
onShutdown: async () => {
|
|
27031
|
+
unregisterShutdown();
|
|
26710
27032
|
await todosCheckpoint.detach();
|
|
26711
27033
|
await stopHeapWatchdog();
|
|
26712
27034
|
credentialWatcherClose?.();
|