@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/server/entry.js
CHANGED
|
@@ -1421,7 +1421,13 @@ function indexDbVersion(projectRoot, indexDir) {
|
|
|
1421
1421
|
try {
|
|
1422
1422
|
const dir = resolveIndexDir(projectRoot, indexDir);
|
|
1423
1423
|
const st = fs.statSync(path.join(dir, DB_FILE));
|
|
1424
|
-
|
|
1424
|
+
let wal = "";
|
|
1425
|
+
try {
|
|
1426
|
+
const walSt = fs.statSync(path.join(dir, `${DB_FILE}-wal`));
|
|
1427
|
+
wal = `:${walSt.mtimeMs}:${walSt.size}`;
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
return `${st.mtimeMs}:${st.size}${wal}`;
|
|
1425
1431
|
} catch {
|
|
1426
1432
|
return "missing";
|
|
1427
1433
|
}
|
|
@@ -4323,7 +4329,8 @@ function createConversationOperations(ctx) {
|
|
|
4323
4329
|
userMessage: async (ws, msg) => {
|
|
4324
4330
|
if (!ensureCurrentSession(ws, msg, "user_message")) return;
|
|
4325
4331
|
const payload = msg.payload ?? {};
|
|
4326
|
-
const
|
|
4332
|
+
const originSessionId = ctx.getSessionId();
|
|
4333
|
+
const controller = ctx.runControl.begin(ws, originSessionId);
|
|
4327
4334
|
if (!controller) {
|
|
4328
4335
|
ctx.send(ws, {
|
|
4329
4336
|
type: "error",
|
|
@@ -4334,7 +4341,6 @@ function createConversationOperations(ctx) {
|
|
|
4334
4341
|
});
|
|
4335
4342
|
return;
|
|
4336
4343
|
}
|
|
4337
|
-
const originSessionId = ctx.getSessionId();
|
|
4338
4344
|
try {
|
|
4339
4345
|
const agent = ctx.getAgent();
|
|
4340
4346
|
if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
|
|
@@ -4393,12 +4399,13 @@ function createConversationOperations(ctx) {
|
|
|
4393
4399
|
});
|
|
4394
4400
|
}
|
|
4395
4401
|
} finally {
|
|
4396
|
-
ctx.runControl.end(ws, controller);
|
|
4402
|
+
ctx.runControl.end(ws, originSessionId, controller);
|
|
4397
4403
|
}
|
|
4398
4404
|
},
|
|
4399
4405
|
abort: (ws, msg) => {
|
|
4400
4406
|
if (!ensureCurrentSession(ws, msg, "abort")) return;
|
|
4401
|
-
ctx.
|
|
4407
|
+
const sessionId = requestedSessionId(msg) ?? ctx.getSessionId();
|
|
4408
|
+
ctx.runControl.abort(ws, sessionId);
|
|
4402
4409
|
ctx.notifyAbort(ws, {
|
|
4403
4410
|
type: "error",
|
|
4404
4411
|
payload: sessionPayload2({ phase: "abort", message: "User aborted" })
|
|
@@ -5450,6 +5457,7 @@ function createConnectionLifecycle(options) {
|
|
|
5450
5457
|
}
|
|
5451
5458
|
|
|
5452
5459
|
// src/server/connections-health-route.ts
|
|
5460
|
+
import * as net from "node:net";
|
|
5453
5461
|
import {
|
|
5454
5462
|
ChronicleProjectServerClient,
|
|
5455
5463
|
createChronicleProjectAccess as createChronicleProjectAccess2,
|
|
@@ -5459,13 +5467,22 @@ import {
|
|
|
5459
5467
|
isMailboxProjectServerAvailable,
|
|
5460
5468
|
MailboxProjectServerConnection
|
|
5461
5469
|
} from "@wrongstack/core/coordination";
|
|
5470
|
+
import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
|
|
5462
5471
|
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
|
|
5463
5472
|
import {
|
|
5464
5473
|
closeKanbanServerConnections,
|
|
5465
5474
|
getKanbanServerConnection,
|
|
5466
5475
|
isKanbanServerAvailable
|
|
5467
5476
|
} from "@wrongstack/kanban";
|
|
5468
|
-
import
|
|
5477
|
+
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5478
|
+
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5479
|
+
import {
|
|
5480
|
+
checkCodebaseIndexServerHealth,
|
|
5481
|
+
ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
|
|
5482
|
+
getIndexState,
|
|
5483
|
+
resolveProjectIndexDaemonAvailability,
|
|
5484
|
+
shutdownCodebaseIndexServer
|
|
5485
|
+
} from "@wrongstack/tools";
|
|
5469
5486
|
|
|
5470
5487
|
// src/server/privileged-actions.ts
|
|
5471
5488
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -5499,15 +5516,6 @@ async function authorizeWebUIAction(boundary, action, logger) {
|
|
|
5499
5516
|
}
|
|
5500
5517
|
|
|
5501
5518
|
// 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
5519
|
async function handleConnectionsHealthRoute(context, ws, message) {
|
|
5512
5520
|
if (message.type !== "connections.health") return false;
|
|
5513
5521
|
try {
|
|
@@ -5528,6 +5536,7 @@ async function handleConnectionsHealthRoute(context, ws, message) {
|
|
|
5528
5536
|
async function collectConnectionsHealth(options) {
|
|
5529
5537
|
const services = await Promise.all([
|
|
5530
5538
|
Promise.resolve(webuiHealth(options.backend)),
|
|
5539
|
+
sessionCatalogHealth(options.projectRoot),
|
|
5531
5540
|
chronicleHealth(options.projectRoot),
|
|
5532
5541
|
codebaseIndexHealth(options.projectRoot, options.indexDir),
|
|
5533
5542
|
sageHealth(options.projectRoot),
|
|
@@ -5545,6 +5554,50 @@ async function collectConnectionsHealth(options) {
|
|
|
5545
5554
|
services
|
|
5546
5555
|
};
|
|
5547
5556
|
}
|
|
5557
|
+
async function sessionCatalogHealth(projectRoot) {
|
|
5558
|
+
const startedAt = Date.now();
|
|
5559
|
+
try {
|
|
5560
|
+
const paths = resolveWstackPaths2({ projectRoot });
|
|
5561
|
+
const client = new SessionCatalogProjectClient({
|
|
5562
|
+
projectDir: paths.projectDir,
|
|
5563
|
+
projectRoot
|
|
5564
|
+
});
|
|
5565
|
+
try {
|
|
5566
|
+
const health = await client.ping();
|
|
5567
|
+
return {
|
|
5568
|
+
id: "session-catalog",
|
|
5569
|
+
label: "Session Catalog",
|
|
5570
|
+
status: health.damagedRows > 0 ? "degraded" : "healthy",
|
|
5571
|
+
required: true,
|
|
5572
|
+
mode: "project-daemon",
|
|
5573
|
+
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).`,
|
|
5574
|
+
ownerPid: health.pid,
|
|
5575
|
+
endpoint: health.endpoint,
|
|
5576
|
+
storage: health.databasePath,
|
|
5577
|
+
uptimeMs: health.uptimeMs,
|
|
5578
|
+
latencyMs: Date.now() - startedAt,
|
|
5579
|
+
clients: health.clients,
|
|
5580
|
+
activeRequests: health.activeRequests,
|
|
5581
|
+
queuedWork: health.reservations + health.maintenanceLeases,
|
|
5582
|
+
control: "none"
|
|
5583
|
+
};
|
|
5584
|
+
} finally {
|
|
5585
|
+
await client.close().catch(() => void 0);
|
|
5586
|
+
}
|
|
5587
|
+
} catch (error2) {
|
|
5588
|
+
return {
|
|
5589
|
+
id: "session-catalog",
|
|
5590
|
+
label: "Session Catalog",
|
|
5591
|
+
status: "error",
|
|
5592
|
+
required: true,
|
|
5593
|
+
mode: "project-daemon",
|
|
5594
|
+
detail: "Project-scoped session ownership and catalog are unavailable.",
|
|
5595
|
+
latencyMs: Date.now() - startedAt,
|
|
5596
|
+
lastError: error2 instanceof Error ? error2.message : String(error2),
|
|
5597
|
+
control: "none"
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5548
5601
|
function webuiHealth(backend) {
|
|
5549
5602
|
return {
|
|
5550
5603
|
id: "webui",
|
|
@@ -6143,7 +6196,11 @@ async function restartSageServer(projectRoot) {
|
|
|
6143
6196
|
});
|
|
6144
6197
|
const verifyConn = new SageProjectServerConnection(projectRoot);
|
|
6145
6198
|
try {
|
|
6146
|
-
await verifyConn.call(
|
|
6199
|
+
await verifyConn.call(
|
|
6200
|
+
"ping",
|
|
6201
|
+
{},
|
|
6202
|
+
{ timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
|
|
6203
|
+
);
|
|
6147
6204
|
return {
|
|
6148
6205
|
serviceId: "sage",
|
|
6149
6206
|
action: "restart",
|
|
@@ -7552,12 +7609,34 @@ function handleTodosGet(ctx, ws) {
|
|
|
7552
7609
|
payload: sessionPayload(ctx, { todos: [...ctx.context.todos] })
|
|
7553
7610
|
});
|
|
7554
7611
|
}
|
|
7555
|
-
function
|
|
7556
|
-
ctx.
|
|
7557
|
-
|
|
7558
|
-
|
|
7612
|
+
async function commitTodos(ctx, todos) {
|
|
7613
|
+
if (ctx.mutateTodos) {
|
|
7614
|
+
const result = await ctx.mutateTodos(todos);
|
|
7615
|
+
return { todos: result.todos, warnings: result.warnings ?? [] };
|
|
7616
|
+
}
|
|
7617
|
+
ctx.replaceTodos?.(todos);
|
|
7618
|
+
return { todos: [...todos], warnings: [] };
|
|
7619
|
+
}
|
|
7620
|
+
function managedProjectionMessage() {
|
|
7621
|
+
return "Kanban-bound todos are task projections. Change or remove the task from Kanban.";
|
|
7559
7622
|
}
|
|
7560
|
-
function
|
|
7623
|
+
async function handleTodosClear(ctx, ws) {
|
|
7624
|
+
if (ctx.context.todos.some((todo) => todo.kanbanBoardId && todo.kanbanTaskId)) {
|
|
7625
|
+
sendResult3(ctx, ws, false, managedProjectionMessage());
|
|
7626
|
+
return;
|
|
7627
|
+
}
|
|
7628
|
+
try {
|
|
7629
|
+
const result = await commitTodos(ctx, []);
|
|
7630
|
+
sendResult3(ctx, ws, true, "Todos cleared");
|
|
7631
|
+
ctx.broadcast({
|
|
7632
|
+
type: "todos.updated",
|
|
7633
|
+
payload: sessionPayload(ctx, { todos: result.todos })
|
|
7634
|
+
});
|
|
7635
|
+
} catch (error2) {
|
|
7636
|
+
sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7639
|
+
async function handleTodosRemove(ctx, ws, payload) {
|
|
7561
7640
|
if (!payload) {
|
|
7562
7641
|
sendResult3(ctx, ws, false, "Missing id or index");
|
|
7563
7642
|
return;
|
|
@@ -7574,12 +7653,27 @@ function handleTodosRemove(ctx, ws, payload) {
|
|
|
7574
7653
|
sendResult3(ctx, ws, false, "Todo not found");
|
|
7575
7654
|
return;
|
|
7576
7655
|
}
|
|
7656
|
+
if (removed.kanbanBoardId && removed.kanbanTaskId) {
|
|
7657
|
+
sendResult3(ctx, ws, false, managedProjectionMessage());
|
|
7658
|
+
return;
|
|
7659
|
+
}
|
|
7577
7660
|
const next = [...todos.slice(0, targetIndex), ...todos.slice(targetIndex + 1)];
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7661
|
+
try {
|
|
7662
|
+
const result = await commitTodos(ctx, next);
|
|
7663
|
+
sendResult3(ctx, ws, true, `Removed: ${removed.content}`);
|
|
7664
|
+
ctx.broadcast({
|
|
7665
|
+
type: "todos.updated",
|
|
7666
|
+
payload: sessionPayload(ctx, { todos: result.todos })
|
|
7667
|
+
});
|
|
7668
|
+
} catch (error2) {
|
|
7669
|
+
sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
|
|
7670
|
+
}
|
|
7581
7671
|
}
|
|
7582
|
-
function handleTodoUpdate(ctx, ws, payload) {
|
|
7672
|
+
async function handleTodoUpdate(ctx, ws, payload) {
|
|
7673
|
+
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") {
|
|
7674
|
+
sendResult3(ctx, ws, false, "Invalid todo update payload");
|
|
7675
|
+
return;
|
|
7676
|
+
}
|
|
7583
7677
|
const index = ctx.context.todos.findIndex((todo) => todo.id === payload.id);
|
|
7584
7678
|
const existing = ctx.context.todos[index];
|
|
7585
7679
|
if (index === -1 || !existing) {
|
|
@@ -7592,9 +7686,25 @@ function handleTodoUpdate(ctx, ws, payload) {
|
|
|
7592
7686
|
status: payload.status ?? existing.status,
|
|
7593
7687
|
activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
|
|
7594
7688
|
};
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7689
|
+
try {
|
|
7690
|
+
const result = await commitTodos(ctx, next);
|
|
7691
|
+
const projected = result.todos.find((todo) => todo.id === existing.id);
|
|
7692
|
+
const requestedStatus = payload.status ?? existing.status;
|
|
7693
|
+
const projectionRejected = Boolean(existing.kanbanBoardId && existing.kanbanTaskId) && projected?.status !== requestedStatus;
|
|
7694
|
+
const warning = result.warnings[0];
|
|
7695
|
+
sendResult3(
|
|
7696
|
+
ctx,
|
|
7697
|
+
ws,
|
|
7698
|
+
!projectionRejected,
|
|
7699
|
+
projectionRejected ? warning ?? `Kanban kept "${existing.content}" at ${projected?.status ?? "its current state"}.` : warning ? `Todo "${existing.content}" updated. ${warning}` : `Todo "${existing.content}" updated`
|
|
7700
|
+
);
|
|
7701
|
+
ctx.broadcast({
|
|
7702
|
+
type: "todos.updated",
|
|
7703
|
+
payload: sessionPayload(ctx, { todos: result.todos })
|
|
7704
|
+
});
|
|
7705
|
+
} catch (error2) {
|
|
7706
|
+
sendResult3(ctx, ws, false, error2 instanceof Error ? error2.message : String(error2));
|
|
7707
|
+
}
|
|
7598
7708
|
}
|
|
7599
7709
|
async function handleTasksGet(ctx, ws) {
|
|
7600
7710
|
const taskPath = taskPathOf(ctx);
|
|
@@ -7622,14 +7732,32 @@ async function handleTaskUpdate(ctx, ws, payload) {
|
|
|
7622
7732
|
return;
|
|
7623
7733
|
}
|
|
7624
7734
|
try {
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7735
|
+
let file;
|
|
7736
|
+
if (ctx.mutateTaskStatus) {
|
|
7737
|
+
const result = await ctx.mutateTaskStatus(payload.id, payload.status);
|
|
7738
|
+
if (!result.ok) {
|
|
7739
|
+
sendResult3(ctx, ws, false, result.message);
|
|
7740
|
+
return;
|
|
7741
|
+
}
|
|
7742
|
+
file = await loadTasks(taskPath);
|
|
7743
|
+
if (!file) throw new Error("Task mutation succeeded but its persisted snapshot is missing.");
|
|
7744
|
+
sendResult3(ctx, ws, true, result.message);
|
|
7745
|
+
} else {
|
|
7746
|
+
let matched = false;
|
|
7747
|
+
file = await mutateTasks(taskPath, currentSessionId(ctx), async (tasks) => {
|
|
7748
|
+
const task = tasks.tasks.find((candidate) => candidate.id === payload.id);
|
|
7749
|
+
if (!task) return tasks;
|
|
7750
|
+
matched = true;
|
|
7751
|
+
task.status = payload.status;
|
|
7752
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7753
|
+
return tasks;
|
|
7754
|
+
});
|
|
7755
|
+
if (!matched) {
|
|
7756
|
+
sendResult3(ctx, ws, false, `Task "${payload.id}" not found.`);
|
|
7757
|
+
return;
|
|
7758
|
+
}
|
|
7759
|
+
sendResult3(ctx, ws, true, `Task status updated to "${payload.status}".`);
|
|
7760
|
+
}
|
|
7633
7761
|
ctx.broadcast({
|
|
7634
7762
|
type: "tasks.updated",
|
|
7635
7763
|
payload: sessionPayload(ctx, { tasks: file.tasks })
|
|
@@ -7676,6 +7804,18 @@ async function handlePlanTemplateUse(ctx, ws, template) {
|
|
|
7676
7804
|
return;
|
|
7677
7805
|
}
|
|
7678
7806
|
try {
|
|
7807
|
+
if (ctx.mutatePlan) {
|
|
7808
|
+
const result = await ctx.mutatePlan({ action: "template_use", template });
|
|
7809
|
+
if (!result.ok) {
|
|
7810
|
+
sendResult3(ctx, ws, false, result.message);
|
|
7811
|
+
return;
|
|
7812
|
+
}
|
|
7813
|
+
const plan2 = await loadPlan(planPath);
|
|
7814
|
+
if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
|
|
7815
|
+
sendResult3(ctx, ws, true, result.message);
|
|
7816
|
+
ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
|
|
7817
|
+
return;
|
|
7818
|
+
}
|
|
7679
7819
|
const templateDefinition = getPlanTemplate(template);
|
|
7680
7820
|
if (!templateDefinition) {
|
|
7681
7821
|
sendResult3(ctx, ws, false, `Unknown template "${template}".`);
|
|
@@ -7704,6 +7844,22 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
|
7704
7844
|
return;
|
|
7705
7845
|
}
|
|
7706
7846
|
try {
|
|
7847
|
+
if (ctx.mutatePlan) {
|
|
7848
|
+
const result = await ctx.mutatePlan({
|
|
7849
|
+
action: "status",
|
|
7850
|
+
target: payload.target,
|
|
7851
|
+
status: payload.status
|
|
7852
|
+
});
|
|
7853
|
+
if (!result.ok) {
|
|
7854
|
+
sendResult3(ctx, ws, false, result.message);
|
|
7855
|
+
return;
|
|
7856
|
+
}
|
|
7857
|
+
const plan2 = await loadPlan(planPath);
|
|
7858
|
+
if (!plan2) throw new Error("Plan mutation succeeded but its persisted snapshot is missing.");
|
|
7859
|
+
sendResult3(ctx, ws, true, result.message);
|
|
7860
|
+
ctx.broadcast({ type: "plan.updated", payload: sessionPayload(ctx, { plan: plan2 }) });
|
|
7861
|
+
return;
|
|
7862
|
+
}
|
|
7707
7863
|
let changed = false;
|
|
7708
7864
|
const plan = await mutatePlan(planPath, currentSessionId(ctx), async (currentPlan) => {
|
|
7709
7865
|
const before = currentPlan.updatedAt;
|
|
@@ -7727,13 +7883,17 @@ async function handleWorklistMessage(ctx, ws, message) {
|
|
|
7727
7883
|
handleTodosGet(ctx, ws);
|
|
7728
7884
|
return;
|
|
7729
7885
|
case "todos.clear":
|
|
7730
|
-
handleTodosClear(ctx, ws);
|
|
7886
|
+
await handleTodosClear(ctx, ws);
|
|
7731
7887
|
return;
|
|
7732
7888
|
case "todos.remove":
|
|
7733
|
-
handleTodosRemove(
|
|
7889
|
+
await handleTodosRemove(
|
|
7890
|
+
ctx,
|
|
7891
|
+
ws,
|
|
7892
|
+
message.payload
|
|
7893
|
+
);
|
|
7734
7894
|
return;
|
|
7735
7895
|
case "todo.update":
|
|
7736
|
-
handleTodoUpdate(
|
|
7896
|
+
await handleTodoUpdate(
|
|
7737
7897
|
ctx,
|
|
7738
7898
|
ws,
|
|
7739
7899
|
message.payload
|
|
@@ -8154,8 +8314,8 @@ async function handleApiSessions(res, globalRoot) {
|
|
|
8154
8314
|
return;
|
|
8155
8315
|
}
|
|
8156
8316
|
try {
|
|
8157
|
-
const {
|
|
8158
|
-
const registry =
|
|
8317
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8318
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8159
8319
|
const sessions = await registry.list();
|
|
8160
8320
|
const result = sessions.map((s) => ({
|
|
8161
8321
|
sessionId: s.sessionId,
|
|
@@ -8192,8 +8352,8 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
8192
8352
|
return;
|
|
8193
8353
|
}
|
|
8194
8354
|
try {
|
|
8195
|
-
const {
|
|
8196
|
-
const registry =
|
|
8355
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8356
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8197
8357
|
const entry = await registry.get(sessionId);
|
|
8198
8358
|
if (!entry) {
|
|
8199
8359
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8201,20 +8361,22 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
8201
8361
|
return;
|
|
8202
8362
|
}
|
|
8203
8363
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
8204
|
-
res.end(
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8364
|
+
res.end(
|
|
8365
|
+
JSON.stringify({
|
|
8366
|
+
sessionId: entry.sessionId,
|
|
8367
|
+
projectName: entry.projectName,
|
|
8368
|
+
status: entry.status,
|
|
8369
|
+
agents: entry.agents.map((a) => ({
|
|
8370
|
+
id: a.id,
|
|
8371
|
+
name: a.name,
|
|
8372
|
+
status: a.status,
|
|
8373
|
+
currentTool: a.currentTool,
|
|
8374
|
+
iterations: a.iterations,
|
|
8375
|
+
toolCalls: a.toolCalls,
|
|
8376
|
+
lastActivityAt: a.lastActivityAt
|
|
8377
|
+
}))
|
|
8378
|
+
})
|
|
8379
|
+
);
|
|
8218
8380
|
} catch (err) {
|
|
8219
8381
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
8220
8382
|
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
@@ -8332,9 +8494,9 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8332
8494
|
return;
|
|
8333
8495
|
}
|
|
8334
8496
|
try {
|
|
8335
|
-
const {
|
|
8497
|
+
const { getSessionRegistry: getSessionRegistry3, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
|
|
8336
8498
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8337
|
-
const registry =
|
|
8499
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8338
8500
|
const entry = await registry.get(sessionId);
|
|
8339
8501
|
if (!entry) {
|
|
8340
8502
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8342,7 +8504,10 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8342
8504
|
return;
|
|
8343
8505
|
}
|
|
8344
8506
|
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8345
|
-
const store = new DefaultSessionStore3({
|
|
8507
|
+
const store = new DefaultSessionStore3({
|
|
8508
|
+
dir: paths.projectSessions,
|
|
8509
|
+
projectRoot: entry.projectRoot
|
|
8510
|
+
});
|
|
8346
8511
|
const reader = new DefaultSessionReader2({ store });
|
|
8347
8512
|
const RING = Math.max(limit * 4, 2e3);
|
|
8348
8513
|
const ring = [];
|
|
@@ -8434,10 +8599,10 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
8434
8599
|
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
8435
8600
|
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
8436
8601
|
try {
|
|
8437
|
-
const {
|
|
8602
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8438
8603
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8439
8604
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8440
|
-
const registry =
|
|
8605
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8441
8606
|
const entry = await registry.get(sessionId);
|
|
8442
8607
|
if (!entry) {
|
|
8443
8608
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8462,10 +8627,10 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
8462
8627
|
return;
|
|
8463
8628
|
}
|
|
8464
8629
|
try {
|
|
8465
|
-
const {
|
|
8630
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8466
8631
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8467
8632
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8468
|
-
const registry =
|
|
8633
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8469
8634
|
const entry = await registry.get(sessionId);
|
|
8470
8635
|
if (!entry) {
|
|
8471
8636
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8522,10 +8687,10 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
8522
8687
|
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
8523
8688
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
8524
8689
|
try {
|
|
8525
|
-
const {
|
|
8690
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8526
8691
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8527
8692
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8528
|
-
const registry =
|
|
8693
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8529
8694
|
const entry = await registry.get(sessionId);
|
|
8530
8695
|
if (!entry) {
|
|
8531
8696
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -8571,10 +8736,10 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
8571
8736
|
}
|
|
8572
8737
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
8573
8738
|
try {
|
|
8574
|
-
const {
|
|
8739
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
8575
8740
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8576
8741
|
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8577
|
-
const registry =
|
|
8742
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
8578
8743
|
const all = await registry.list();
|
|
8579
8744
|
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
8580
8745
|
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
@@ -10434,11 +10599,11 @@ function kanbanListMessage(boards) {
|
|
|
10434
10599
|
function kanbanDeletedMessage(boardId) {
|
|
10435
10600
|
return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
|
|
10436
10601
|
}
|
|
10437
|
-
async function publishKanbanBoard(broadcast2, board,
|
|
10602
|
+
async function publishKanbanBoard(broadcast2, board, listBoards5) {
|
|
10438
10603
|
broadcast2(kanbanBoardMessage(board));
|
|
10439
|
-
if (!
|
|
10604
|
+
if (!listBoards5) return;
|
|
10440
10605
|
try {
|
|
10441
|
-
broadcast2(kanbanListMessage(await
|
|
10606
|
+
broadcast2(kanbanListMessage(await listBoards5()));
|
|
10442
10607
|
} catch {
|
|
10443
10608
|
}
|
|
10444
10609
|
}
|
|
@@ -10689,9 +10854,9 @@ import {
|
|
|
10689
10854
|
claimReadyTask,
|
|
10690
10855
|
copyTaskToBoard,
|
|
10691
10856
|
createBoard,
|
|
10857
|
+
createBoardFromText,
|
|
10692
10858
|
duplicateBoard,
|
|
10693
10859
|
exportBoardToTaskGraph,
|
|
10694
|
-
createBoardFromText,
|
|
10695
10860
|
getBoard,
|
|
10696
10861
|
getKanbanOrchestrationSnapshot,
|
|
10697
10862
|
getKanbanQueueHealth,
|
|
@@ -10879,8 +11044,35 @@ async function handleTaskVerification(ws, type, payload, ctx) {
|
|
|
10879
11044
|
}
|
|
10880
11045
|
}
|
|
10881
11046
|
|
|
11047
|
+
// src/server/kanban-route-pagination.ts
|
|
11048
|
+
function paginateKanbanBoards(boards, input) {
|
|
11049
|
+
const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
|
|
11050
|
+
const activeSessionIds = new Set(input.activeSessionIds ?? []);
|
|
11051
|
+
const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
|
|
11052
|
+
const sorted = [...boards].sort((left, right) => {
|
|
11053
|
+
const activityOrder = Number(isActive(right)) - Number(isActive(left));
|
|
11054
|
+
return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
|
|
11055
|
+
});
|
|
11056
|
+
const activeTotal = sorted.filter(isActive).length;
|
|
11057
|
+
const total = sorted.length;
|
|
11058
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
11059
|
+
const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
|
|
11060
|
+
const page = Math.min(totalPages, Math.max(1, requestedPage));
|
|
11061
|
+
const start = (page - 1) * pageSize;
|
|
11062
|
+
return {
|
|
11063
|
+
items: sorted.slice(start, start + pageSize),
|
|
11064
|
+
total,
|
|
11065
|
+
page,
|
|
11066
|
+
pageSize,
|
|
11067
|
+
totalPages,
|
|
11068
|
+
activeTotal,
|
|
11069
|
+
orphanedTotal: total - activeTotal
|
|
11070
|
+
};
|
|
11071
|
+
}
|
|
11072
|
+
|
|
10882
11073
|
// src/server/kanban-task-routes.ts
|
|
10883
11074
|
import {
|
|
11075
|
+
getKanbanWorkbench,
|
|
10884
11076
|
getTask,
|
|
10885
11077
|
listTaskActivity,
|
|
10886
11078
|
recordTaskActivity,
|
|
@@ -10888,6 +11080,16 @@ import {
|
|
|
10888
11080
|
} from "@wrongstack/kanban";
|
|
10889
11081
|
async function handleKanbanTaskRoute(ws, type, payload, ctx) {
|
|
10890
11082
|
switch (type) {
|
|
11083
|
+
case "kanban.workbench":
|
|
11084
|
+
ok(
|
|
11085
|
+
ws,
|
|
11086
|
+
type,
|
|
11087
|
+
await getKanbanWorkbench(ctx.projectRoot, {
|
|
11088
|
+
...typeof payload?.limitPerLane === "number" ? { limitPerLane: payload.limitPerLane } : {},
|
|
11089
|
+
...typeof payload?.alertLimit === "number" ? { alertLimit: payload.alertLimit } : {}
|
|
11090
|
+
})
|
|
11091
|
+
);
|
|
11092
|
+
return true;
|
|
10891
11093
|
case "kanban.task.remove":
|
|
10892
11094
|
await handleTaskRemove(ws, type, payload, ctx);
|
|
10893
11095
|
return true;
|
|
@@ -10980,40 +11182,11 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
|
|
|
10980
11182
|
outcome,
|
|
10981
11183
|
...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
|
|
10982
11184
|
},
|
|
10983
|
-
activityContext(
|
|
10984
|
-
ctx,
|
|
10985
|
-
payload?.actor ?? ctx.context?.agentId ?? "webui"
|
|
10986
|
-
)
|
|
11185
|
+
activityContext(ctx, payload?.actor ?? ctx.context?.agentId ?? "webui")
|
|
10987
11186
|
);
|
|
10988
11187
|
board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
|
|
10989
11188
|
}
|
|
10990
11189
|
|
|
10991
|
-
// src/server/kanban-route-pagination.ts
|
|
10992
|
-
function paginateKanbanBoards(boards, input) {
|
|
10993
|
-
const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
|
|
10994
|
-
const activeSessionIds = new Set(input.activeSessionIds ?? []);
|
|
10995
|
-
const isActive = (board) => board.presence?.some((entry) => entry.active) === true || board.tags?.some((tag) => tag.startsWith("session:") && activeSessionIds.has(tag.slice(8))) === true;
|
|
10996
|
-
const sorted = [...boards].sort((left, right) => {
|
|
10997
|
-
const activityOrder = Number(isActive(right)) - Number(isActive(left));
|
|
10998
|
-
return activityOrder || right.updatedAt.localeCompare(left.updatedAt);
|
|
10999
|
-
});
|
|
11000
|
-
const activeTotal = sorted.filter(isActive).length;
|
|
11001
|
-
const total = sorted.length;
|
|
11002
|
-
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
11003
|
-
const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page) : 1;
|
|
11004
|
-
const page = Math.min(totalPages, Math.max(1, requestedPage));
|
|
11005
|
-
const start = (page - 1) * pageSize;
|
|
11006
|
-
return {
|
|
11007
|
-
items: sorted.slice(start, start + pageSize),
|
|
11008
|
-
total,
|
|
11009
|
-
page,
|
|
11010
|
-
pageSize,
|
|
11011
|
-
totalPages,
|
|
11012
|
-
activeTotal,
|
|
11013
|
-
orphanedTotal: total - activeTotal
|
|
11014
|
-
};
|
|
11015
|
-
}
|
|
11016
|
-
|
|
11017
11190
|
// src/server/kanban-routes.ts
|
|
11018
11191
|
async function handleKanbanRoute(ws, msg, ctx) {
|
|
11019
11192
|
if (!msg.type.startsWith("kanban.")) return false;
|
|
@@ -11074,6 +11247,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
11074
11247
|
fail(ws, type, `Board not found: ${boardId}`);
|
|
11075
11248
|
return true;
|
|
11076
11249
|
}
|
|
11250
|
+
if (ctx.supervisor) {
|
|
11251
|
+
const snapshots = await ctx.supervisor.auditNow(boardId);
|
|
11252
|
+
const snapshot = snapshots[0];
|
|
11253
|
+
if (snapshot) {
|
|
11254
|
+
ok(ws, type, snapshot);
|
|
11255
|
+
return true;
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
11077
11258
|
const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
|
|
11078
11259
|
let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
|
|
11079
11260
|
const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
|
|
@@ -11784,7 +11965,9 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11784
11965
|
projectRoot,
|
|
11785
11966
|
async (event) => {
|
|
11786
11967
|
const family = event.event?.split(".")[0];
|
|
11787
|
-
if (family !== "board" && family !== "task" && family !== "column")
|
|
11968
|
+
if (family !== "board" && family !== "task" && family !== "column" && family !== "contract") {
|
|
11969
|
+
return;
|
|
11970
|
+
}
|
|
11788
11971
|
const evData = event.data;
|
|
11789
11972
|
const boardId = evData?.boardId;
|
|
11790
11973
|
if (!boardId) return;
|
|
@@ -11833,6 +12016,15 @@ function createShutdown(res) {
|
|
|
11833
12016
|
} catch {
|
|
11834
12017
|
}
|
|
11835
12018
|
}
|
|
12019
|
+
if (res.onPreShutdown) {
|
|
12020
|
+
try {
|
|
12021
|
+
await res.onPreShutdown();
|
|
12022
|
+
} catch (e) {
|
|
12023
|
+
log(
|
|
12024
|
+
`[WebUI] Error during pre-shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`
|
|
12025
|
+
);
|
|
12026
|
+
}
|
|
12027
|
+
}
|
|
11836
12028
|
for (const server of res.servers) server?.close();
|
|
11837
12029
|
if (res.onShutdown) {
|
|
11838
12030
|
try {
|
|
@@ -13087,14 +13279,17 @@ async function handleSageForFile(ws, msg, memoryStore) {
|
|
|
13087
13279
|
send(ws, { type: "memory.sage.forFile", payload: { error: "filePath is required" } });
|
|
13088
13280
|
return;
|
|
13089
13281
|
}
|
|
13282
|
+
const includeSuperseded = typeof payload["showSuperseded"] === "boolean" ? payload["showSuperseded"] : typeof payload["includeSuperseded"] === "boolean" ? payload["includeSuperseded"] : void 0;
|
|
13283
|
+
const includeDeleted = payload["showDeleted"] === true || payload["includeDeleted"] === true;
|
|
13090
13284
|
try {
|
|
13091
13285
|
const response = await Sage.findMemoriesForFile(filePath, {
|
|
13092
13286
|
...typeof payload["lineStart"] === "number" ? { lineStart: payload["lineStart"] } : {},
|
|
13093
13287
|
...typeof payload["lineEnd"] === "number" ? { lineEnd: payload["lineEnd"] } : {},
|
|
13094
13288
|
...typeof payload["limit"] === "number" ? { limit: payload["limit"] } : {},
|
|
13095
|
-
...
|
|
13289
|
+
...includeSuperseded !== void 0 ? { includeSuperseded } : {},
|
|
13290
|
+
...includeDeleted ? { includeDeleted: true } : {}
|
|
13096
13291
|
});
|
|
13097
|
-
send(ws, { type: "memory.sage.forFile", payload: response });
|
|
13292
|
+
send(ws, { type: "memory.sage.forFile", payload: { response } });
|
|
13098
13293
|
} catch (err) {
|
|
13099
13294
|
send(ws, { type: "memory.sage.forFile", payload: { error: errMessage(err) } });
|
|
13100
13295
|
}
|
|
@@ -13747,6 +13942,324 @@ async function handleBrainAsk(ctx, ws, question) {
|
|
|
13747
13942
|
}
|
|
13748
13943
|
}
|
|
13749
13944
|
|
|
13945
|
+
// src/server/kanban-supervisor.ts
|
|
13946
|
+
import {
|
|
13947
|
+
finalizeTaskCompletion,
|
|
13948
|
+
getBoard as getBoard2,
|
|
13949
|
+
getKanbanQueueHealth as getKanbanQueueHealth2,
|
|
13950
|
+
listBoards as listBoards3,
|
|
13951
|
+
reconcileKanbanBoard as reconcileKanbanBoard2,
|
|
13952
|
+
recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
|
|
13953
|
+
resolveGateEnforcement
|
|
13954
|
+
} from "@wrongstack/kanban";
|
|
13955
|
+
function resolveProjectRoot(deps2) {
|
|
13956
|
+
const root = deps2.projectRoot;
|
|
13957
|
+
return typeof root === "function" ? root() : root;
|
|
13958
|
+
}
|
|
13959
|
+
var DEFAULT_INTERVAL_MS = 1e4;
|
|
13960
|
+
var MIN_INTERVAL_MS = 2e3;
|
|
13961
|
+
var DEFAULT_AGENT_COOLDOWN_MS = 5 * 6e4;
|
|
13962
|
+
var DEFAULT_CONFIG = {
|
|
13963
|
+
enabled: true,
|
|
13964
|
+
mode: "deterministic",
|
|
13965
|
+
intervalMs: DEFAULT_INTERVAL_MS,
|
|
13966
|
+
recoveryMode: "auto"
|
|
13967
|
+
};
|
|
13968
|
+
function createKanbanSupervisor(deps2) {
|
|
13969
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
13970
|
+
const nextDue = /* @__PURE__ */ new Map();
|
|
13971
|
+
const agentLastRun = /* @__PURE__ */ new Map();
|
|
13972
|
+
const agentRunning = /* @__PURE__ */ new Set();
|
|
13973
|
+
let disposed = false;
|
|
13974
|
+
let nextTimer;
|
|
13975
|
+
const forgetBoard = (boardId) => {
|
|
13976
|
+
snapshots.delete(boardId);
|
|
13977
|
+
nextDue.delete(boardId);
|
|
13978
|
+
agentLastRun.delete(boardId);
|
|
13979
|
+
agentRunning.delete(boardId);
|
|
13980
|
+
};
|
|
13981
|
+
const pruneAbsentBoards = (presentBoardIds) => {
|
|
13982
|
+
for (const boardId of snapshots.keys()) {
|
|
13983
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13984
|
+
}
|
|
13985
|
+
for (const boardId of nextDue.keys()) {
|
|
13986
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13987
|
+
}
|
|
13988
|
+
for (const boardId of agentLastRun.keys()) {
|
|
13989
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13990
|
+
}
|
|
13991
|
+
for (const boardId of agentRunning) {
|
|
13992
|
+
if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
|
|
13993
|
+
}
|
|
13994
|
+
};
|
|
13995
|
+
const publish = (snapshot) => {
|
|
13996
|
+
snapshots.set(snapshot.boardId, snapshot);
|
|
13997
|
+
deps2.broadcast({
|
|
13998
|
+
type: "kanban.supervisor.status",
|
|
13999
|
+
payload: { success: true, data: snapshot }
|
|
14000
|
+
});
|
|
14001
|
+
};
|
|
14002
|
+
const auditBoard = async (board) => {
|
|
14003
|
+
const config = effectiveConfig(board);
|
|
14004
|
+
const auditedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14005
|
+
const intervalMs = Math.max(MIN_INTERVAL_MS, config.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
14006
|
+
const nextAuditAt = new Date(Date.now() + intervalMs).toISOString();
|
|
14007
|
+
nextDue.set(board.id, Date.now() + intervalMs);
|
|
14008
|
+
if (!config.enabled) {
|
|
14009
|
+
const snapshot2 = {
|
|
14010
|
+
boardId: board.id,
|
|
14011
|
+
status: "disabled",
|
|
14012
|
+
mode: config.mode,
|
|
14013
|
+
lastAuditAt: auditedAt,
|
|
14014
|
+
nextAuditAt,
|
|
14015
|
+
reconciledTaskIds: [],
|
|
14016
|
+
staleRecoveredTaskIds: [],
|
|
14017
|
+
anomalyCount: 0,
|
|
14018
|
+
summary: "Supervision is disabled for this board."
|
|
14019
|
+
};
|
|
14020
|
+
publish(snapshot2);
|
|
14021
|
+
return snapshot2;
|
|
14022
|
+
}
|
|
14023
|
+
const reconciled = await reconcileKanbanBoard2(resolveProjectRoot(deps2), board.id);
|
|
14024
|
+
const gateSwept = await sweepGateParkedTasks(deps2, reconciled?.board ?? board);
|
|
14025
|
+
let health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
|
|
14026
|
+
const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(resolveProjectRoot(deps2), board.id, {
|
|
14027
|
+
mode: config.recoveryMode ?? "auto",
|
|
14028
|
+
reason: "Kanban supervisor found an expired worker lease."
|
|
14029
|
+
}) : null;
|
|
14030
|
+
if (recovered)
|
|
14031
|
+
health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
|
|
14032
|
+
const anomalyCount = countAnomalies(health);
|
|
14033
|
+
const snapshot = {
|
|
14034
|
+
boardId: board.id,
|
|
14035
|
+
status: anomalyCount > 0 ? "attention" : "healthy",
|
|
14036
|
+
mode: config.mode,
|
|
14037
|
+
lastAuditAt: auditedAt,
|
|
14038
|
+
nextAuditAt,
|
|
14039
|
+
reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
|
|
14040
|
+
staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
|
|
14041
|
+
anomalyCount,
|
|
14042
|
+
summary: healthSummary(health)
|
|
14043
|
+
};
|
|
14044
|
+
publish(snapshot);
|
|
14045
|
+
const changedBoard = recovered?.board ?? gateSwept ?? reconciled?.board;
|
|
14046
|
+
if (changedBoard) {
|
|
14047
|
+
await publishKanbanBoard(
|
|
14048
|
+
deps2.broadcast,
|
|
14049
|
+
changedBoard,
|
|
14050
|
+
() => listBoards3(resolveProjectRoot(deps2))
|
|
14051
|
+
);
|
|
14052
|
+
}
|
|
14053
|
+
if (config.mode === "agentic" && anomalyCount > 0) {
|
|
14054
|
+
await maybeRunAgent(board, config, health, snapshot);
|
|
14055
|
+
}
|
|
14056
|
+
return snapshots.get(board.id) ?? snapshot;
|
|
14057
|
+
};
|
|
14058
|
+
const maybeRunAgent = async (board, config, health, snapshot) => {
|
|
14059
|
+
if (!deps2.dispatchTask || agentRunning.has(board.id)) return;
|
|
14060
|
+
const cooldownMs = Math.max(
|
|
14061
|
+
MIN_INTERVAL_MS,
|
|
14062
|
+
config.agentCooldownMs ?? DEFAULT_AGENT_COOLDOWN_MS
|
|
14063
|
+
);
|
|
14064
|
+
if (Date.now() - (agentLastRun.get(board.id) ?? 0) < cooldownMs) return;
|
|
14065
|
+
agentRunning.add(board.id);
|
|
14066
|
+
agentLastRun.set(board.id, Date.now());
|
|
14067
|
+
const watchdog = setTimeout(
|
|
14068
|
+
() => agentRunning.delete(board.id),
|
|
14069
|
+
Math.max(cooldownMs * 2, DEFAULT_AGENT_COOLDOWN_MS * 2)
|
|
14070
|
+
);
|
|
14071
|
+
watchdog.unref?.();
|
|
14072
|
+
publish({
|
|
14073
|
+
...snapshot,
|
|
14074
|
+
status: "running",
|
|
14075
|
+
lastAgentRunAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14076
|
+
summary: `Agentic anomaly review started. ${snapshot.summary ?? ""}`.trim()
|
|
14077
|
+
});
|
|
14078
|
+
const routing = config.routing ?? { mode: "session" };
|
|
14079
|
+
try {
|
|
14080
|
+
const spawnSummary = await deps2.dispatchTask(buildAuditPrompt(board, health), {
|
|
14081
|
+
...dispatchRoute(routing),
|
|
14082
|
+
...config.skills?.length ? { skills: config.skills } : {},
|
|
14083
|
+
name: `kanban-supervisor-${board.id.slice(0, 6)}`,
|
|
14084
|
+
// Carry the board identity into the spawned TaskSpec.context so the
|
|
14085
|
+
// tool-runtime boundary gate (`evaluateToolKanbanBoundary`) can resolve
|
|
14086
|
+
// the live board policy instead of failing open. Whole-board agentic
|
|
14087
|
+
// runs have no taskId, so only boardId is propagated.
|
|
14088
|
+
context: { kanban: { boardId: board.id, projectRoot: resolveProjectRoot(deps2) } },
|
|
14089
|
+
onDone: async (result) => {
|
|
14090
|
+
clearTimeout(watchdog);
|
|
14091
|
+
agentRunning.delete(board.id);
|
|
14092
|
+
if (await getBoard2(resolveProjectRoot(deps2), board.id) === null) return;
|
|
14093
|
+
const current3 = snapshots.get(board.id) ?? snapshot;
|
|
14094
|
+
publish({
|
|
14095
|
+
...current3,
|
|
14096
|
+
status: result.status === "failed" ? "error" : current3.anomalyCount ? "attention" : "healthy",
|
|
14097
|
+
lastAgentRunAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14098
|
+
summary: result.result ?? current3.summary,
|
|
14099
|
+
...result.error ? { error: result.error } : {}
|
|
14100
|
+
});
|
|
14101
|
+
}
|
|
14102
|
+
});
|
|
14103
|
+
const current2 = snapshots.get(board.id) ?? snapshot;
|
|
14104
|
+
publish({ ...current2, status: "running", summary: spawnSummary });
|
|
14105
|
+
} catch (error2) {
|
|
14106
|
+
clearTimeout(watchdog);
|
|
14107
|
+
agentRunning.delete(board.id);
|
|
14108
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
14109
|
+
deps2.log?.(`[KanbanSupervisor] ${board.id}: ${message}`);
|
|
14110
|
+
publish({ ...snapshot, status: "error", error: message });
|
|
14111
|
+
}
|
|
14112
|
+
};
|
|
14113
|
+
const auditNow = async (boardId) => {
|
|
14114
|
+
let boards;
|
|
14115
|
+
if (boardId) {
|
|
14116
|
+
const board = await getBoard2(resolveProjectRoot(deps2), boardId);
|
|
14117
|
+
if (board === null) {
|
|
14118
|
+
forgetBoard(boardId);
|
|
14119
|
+
boards = [];
|
|
14120
|
+
} else {
|
|
14121
|
+
boards = [board];
|
|
14122
|
+
}
|
|
14123
|
+
} else {
|
|
14124
|
+
const summaries = await listBoards3(resolveProjectRoot(deps2));
|
|
14125
|
+
pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
|
|
14126
|
+
boards = (await Promise.all(
|
|
14127
|
+
summaries.map((summary) => getBoard2(resolveProjectRoot(deps2), summary.id))
|
|
14128
|
+
)).filter((board) => Boolean(board));
|
|
14129
|
+
}
|
|
14130
|
+
const results = [];
|
|
14131
|
+
for (const board of boards) results.push(await auditBoard(board));
|
|
14132
|
+
scheduleNext();
|
|
14133
|
+
return results;
|
|
14134
|
+
};
|
|
14135
|
+
const scheduleNext = () => {
|
|
14136
|
+
if (disposed) return;
|
|
14137
|
+
if (nextTimer !== void 0) {
|
|
14138
|
+
clearTimeout(nextTimer);
|
|
14139
|
+
nextTimer = void 0;
|
|
14140
|
+
}
|
|
14141
|
+
const now = Date.now();
|
|
14142
|
+
let minDue = Infinity;
|
|
14143
|
+
for (const due of nextDue.values()) {
|
|
14144
|
+
if (due < minDue) minDue = due;
|
|
14145
|
+
}
|
|
14146
|
+
if (!Number.isFinite(minDue) || minDue <= now) {
|
|
14147
|
+
nextTimer = setTimeout(() => void tick(), MIN_INTERVAL_MS);
|
|
14148
|
+
nextTimer.unref?.();
|
|
14149
|
+
return;
|
|
14150
|
+
}
|
|
14151
|
+
const delay = Math.min(minDue - now, DEFAULT_INTERVAL_MS);
|
|
14152
|
+
if (delay <= 0) {
|
|
14153
|
+
nextTimer = setTimeout(() => void tick(), MIN_INTERVAL_MS);
|
|
14154
|
+
} else {
|
|
14155
|
+
nextTimer = setTimeout(() => void tick(), delay);
|
|
14156
|
+
}
|
|
14157
|
+
nextTimer.unref?.();
|
|
14158
|
+
};
|
|
14159
|
+
const tick = async () => {
|
|
14160
|
+
if (disposed) return;
|
|
14161
|
+
try {
|
|
14162
|
+
const now = Date.now();
|
|
14163
|
+
const summaries = await listBoards3(resolveProjectRoot(deps2));
|
|
14164
|
+
pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
|
|
14165
|
+
for (const summary of summaries) {
|
|
14166
|
+
if ((nextDue.get(summary.id) ?? 0) > now) continue;
|
|
14167
|
+
const board = await getBoard2(resolveProjectRoot(deps2), summary.id);
|
|
14168
|
+
if (board) await auditBoard(board);
|
|
14169
|
+
}
|
|
14170
|
+
} catch (error2) {
|
|
14171
|
+
deps2.log?.(`[KanbanSupervisor] ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
14172
|
+
} finally {
|
|
14173
|
+
scheduleNext();
|
|
14174
|
+
}
|
|
14175
|
+
};
|
|
14176
|
+
void scheduleNext();
|
|
14177
|
+
return {
|
|
14178
|
+
getSnapshot: (boardId) => snapshots.get(boardId),
|
|
14179
|
+
auditNow,
|
|
14180
|
+
getStats: () => ({
|
|
14181
|
+
snapshots: snapshots.size,
|
|
14182
|
+
scheduledBoards: nextDue.size,
|
|
14183
|
+
agentCooldowns: agentLastRun.size,
|
|
14184
|
+
runningAgents: agentRunning.size
|
|
14185
|
+
}),
|
|
14186
|
+
dispose() {
|
|
14187
|
+
disposed = true;
|
|
14188
|
+
if (nextTimer !== void 0) {
|
|
14189
|
+
clearTimeout(nextTimer);
|
|
14190
|
+
nextTimer = void 0;
|
|
14191
|
+
}
|
|
14192
|
+
snapshots.clear();
|
|
14193
|
+
nextDue.clear();
|
|
14194
|
+
agentLastRun.clear();
|
|
14195
|
+
agentRunning.clear();
|
|
14196
|
+
}
|
|
14197
|
+
};
|
|
14198
|
+
}
|
|
14199
|
+
function effectiveConfig(board) {
|
|
14200
|
+
return { ...DEFAULT_CONFIG, ...board.supervisor ?? {} };
|
|
14201
|
+
}
|
|
14202
|
+
async function sweepGateParkedTasks(deps2, board) {
|
|
14203
|
+
if (resolveGateEnforcement(board) === "off") return void 0;
|
|
14204
|
+
const parked = board.tasks.filter(
|
|
14205
|
+
(task) => task.status === "review" && task.assignment?.status === "completed" && !task.verificationReport
|
|
14206
|
+
);
|
|
14207
|
+
let lastBoard;
|
|
14208
|
+
for (const task of parked) {
|
|
14209
|
+
try {
|
|
14210
|
+
const finalized = await finalizeTaskCompletion(resolveProjectRoot(deps2), board.id, task.id, {
|
|
14211
|
+
eventContext: { actor: "kanban-supervisor" }
|
|
14212
|
+
});
|
|
14213
|
+
if (finalized) lastBoard = finalized.board;
|
|
14214
|
+
} catch (error2) {
|
|
14215
|
+
deps2.log?.(
|
|
14216
|
+
`[KanbanSupervisor] completion gate sweep failed for ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
14217
|
+
);
|
|
14218
|
+
}
|
|
14219
|
+
}
|
|
14220
|
+
return lastBoard;
|
|
14221
|
+
}
|
|
14222
|
+
function dispatchRoute(routing) {
|
|
14223
|
+
if (routing.mode === "session") return {};
|
|
14224
|
+
return {
|
|
14225
|
+
...routing.provider ? { provider: routing.provider } : {},
|
|
14226
|
+
...routing.model ? { model: routing.model } : {},
|
|
14227
|
+
...routing.fallbackProfile ? { fallbackProfile: routing.fallbackProfile } : {},
|
|
14228
|
+
...routing.fallbackModels?.length ? { fallbackModels: routing.fallbackModels } : {}
|
|
14229
|
+
};
|
|
14230
|
+
}
|
|
14231
|
+
function countAnomalies(health) {
|
|
14232
|
+
return health.staleAssignments.count + health.heartbeatDue.count + health.counts.failed + health.counts.blocked;
|
|
14233
|
+
}
|
|
14234
|
+
function healthSummary(health) {
|
|
14235
|
+
return [
|
|
14236
|
+
`${health.counts.running} running`,
|
|
14237
|
+
`${health.counts.ready} ready`,
|
|
14238
|
+
`${health.counts.review} review`,
|
|
14239
|
+
`${health.counts.blocked} blocked`,
|
|
14240
|
+
`${health.counts.failed} failed`,
|
|
14241
|
+
`${health.staleAssignments.count} stale`,
|
|
14242
|
+
`${health.dependencyBlocked.count} dependency-blocked`
|
|
14243
|
+
].join(" \xB7 ");
|
|
14244
|
+
}
|
|
14245
|
+
function buildAuditPrompt(board, health) {
|
|
14246
|
+
const taskLines = board.tasks.map(
|
|
14247
|
+
(task) => `- ${task.id}: ${task.title} [task=${task.status}; assignment=${task.assignment?.status ?? "none"}; column=${task.columnId}]`
|
|
14248
|
+
);
|
|
14249
|
+
return [
|
|
14250
|
+
"You are the explicitly configured WrongStack Kanban supervisor.",
|
|
14251
|
+
"Audit only this board. Do not implement product tasks.",
|
|
14252
|
+
`Board: ${board.title} (${board.id})`,
|
|
14253
|
+
`Health: ${healthSummary(health)}`,
|
|
14254
|
+
"",
|
|
14255
|
+
"Tasks:",
|
|
14256
|
+
...taskLines,
|
|
14257
|
+
"",
|
|
14258
|
+
"Use the kanban tool for corrections. Preserve manual blockers and dependencies.",
|
|
14259
|
+
"Fix only demonstrable status/assignment/column drift, then report every action and remaining anomaly."
|
|
14260
|
+
].join("\n");
|
|
14261
|
+
}
|
|
14262
|
+
|
|
13750
14263
|
// src/server/context-meta.ts
|
|
13751
14264
|
import { FallbackProfileManager } from "@wrongstack/core/agent";
|
|
13752
14265
|
import { resolvePluginEnablement } from "@wrongstack/core/plugin";
|
|
@@ -16222,6 +16735,8 @@ function labelForEvent(e) {
|
|
|
16222
16735
|
const count = e.messagesOmitted ?? e.messages.length;
|
|
16223
16736
|
return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
|
|
16224
16737
|
}
|
|
16738
|
+
case "messages_dropped":
|
|
16739
|
+
return `Oldest ${e.count} message${e.count === 1 ? "" : "s"} evicted`;
|
|
16225
16740
|
case "message_truncated":
|
|
16226
16741
|
return `Message truncated: ${e.before} \u2192 ${e.after}`;
|
|
16227
16742
|
case "file_event":
|
|
@@ -16309,6 +16824,8 @@ function detailForEvent(e) {
|
|
|
16309
16824
|
return `at index ${e.index}`;
|
|
16310
16825
|
case "messages_replaced":
|
|
16311
16826
|
return `${e.messagesOmitted ?? e.messages.length} total`;
|
|
16827
|
+
case "messages_dropped":
|
|
16828
|
+
return `dropped ${e.count} from the front`;
|
|
16312
16829
|
case "message_truncated":
|
|
16313
16830
|
return `truncated to ${e.after} tokens`;
|
|
16314
16831
|
case "mode_changed":
|
|
@@ -16526,7 +17043,7 @@ function createSessionHandlers(ctx) {
|
|
|
16526
17043
|
const current2 = ctx.getSession();
|
|
16527
17044
|
if (current2 !== next) {
|
|
16528
17045
|
try {
|
|
16529
|
-
ctx.abortActiveRun?.();
|
|
17046
|
+
ctx.abortActiveRun?.(current2.id);
|
|
16530
17047
|
} catch {
|
|
16531
17048
|
}
|
|
16532
17049
|
await finalizeSession(current2);
|
|
@@ -17059,85 +17576,51 @@ function createSessionHandlers(ctx) {
|
|
|
17059
17576
|
// src/server/agent-roster-handlers.ts
|
|
17060
17577
|
import {
|
|
17061
17578
|
applyProjectAgentConfig,
|
|
17062
|
-
buildConsolidationInstruction,
|
|
17063
17579
|
captureLearnedFromAgentOutputDetailed,
|
|
17064
17580
|
clearProjectAgentConsolidated,
|
|
17581
|
+
clearProjectSkillAugmentation,
|
|
17065
17582
|
createProjectAgent,
|
|
17066
17583
|
detectLearnedConflicts,
|
|
17584
|
+
evaluateAutoOptimize,
|
|
17067
17585
|
FLEET_ROSTER,
|
|
17068
17586
|
getProjectAgentLearnStats,
|
|
17069
17587
|
isConsolidated,
|
|
17070
17588
|
listProjectAgentLearnedEntries,
|
|
17071
17589
|
listProjectAgentRoles,
|
|
17590
|
+
listProjectSkillAugmentations,
|
|
17072
17591
|
loadConsolidationMetadata,
|
|
17073
17592
|
loadProjectAgentConfig,
|
|
17074
17593
|
loadProjectAgentConsolidated,
|
|
17075
17594
|
loadProjectAgentIdentity,
|
|
17076
17595
|
loadProjectAgentLearned,
|
|
17077
17596
|
loadProjectAgentProfile,
|
|
17597
|
+
loadProjectSkillAugmentation,
|
|
17598
|
+
loadSkillAffinity,
|
|
17599
|
+
optimizeProjectAgentLearning,
|
|
17600
|
+
readRawLearnedEntries,
|
|
17078
17601
|
resetProjectAgentIdentity,
|
|
17602
|
+
resolveAutoOptimizePolicy,
|
|
17603
|
+
resolveRoleSkillCandidates,
|
|
17079
17604
|
saveProjectAgentConsolidated,
|
|
17605
|
+
saveProjectSkillAugmentation,
|
|
17606
|
+
setSkillPinned,
|
|
17080
17607
|
slugifyProjectAgentRole,
|
|
17081
17608
|
updateProjectAgentConfig,
|
|
17082
17609
|
updateProjectAgentIdentity,
|
|
17083
17610
|
updateProjectAgentLearned,
|
|
17084
17611
|
updateProjectAgentLearningPolicy
|
|
17085
17612
|
} from "@wrongstack/core/coordination";
|
|
17086
|
-
import { isTextBlock } from "@wrongstack/core/types";
|
|
17087
|
-
var CONSOLIDATION_MAX_TOKENS = 8e3;
|
|
17088
|
-
var CONSOLIDATION_TIMEOUT_MS = 12e4;
|
|
17089
17613
|
var AgentRosterWSHandler = class {
|
|
17090
17614
|
getProjectRoot;
|
|
17091
17615
|
getLlm;
|
|
17092
17616
|
broadcast;
|
|
17617
|
+
getAutoOptimizeSettings;
|
|
17093
17618
|
constructor(opts) {
|
|
17094
17619
|
this.getProjectRoot = typeof opts.projectRoot === "function" ? opts.projectRoot : () => opts.projectRoot;
|
|
17095
17620
|
this.getLlm = opts.getLlm ?? (() => void 0);
|
|
17096
17621
|
this.broadcast = opts.broadcast ?? (() => {
|
|
17097
17622
|
});
|
|
17098
|
-
|
|
17099
|
-
/**
|
|
17100
|
-
* Run the consolidation LLM synthesis headlessly and return the cleaned
|
|
17101
|
-
* document text. Returns undefined when no LLM is available so the caller
|
|
17102
|
-
* can fall back to the instruction-only path.
|
|
17103
|
-
*/
|
|
17104
|
-
async synthesizeConsolidation(instruction) {
|
|
17105
|
-
const llm = this.getLlm();
|
|
17106
|
-
if (!llm) return void 0;
|
|
17107
|
-
const req = {
|
|
17108
|
-
model: llm.model,
|
|
17109
|
-
system: [
|
|
17110
|
-
{
|
|
17111
|
-
type: "text",
|
|
17112
|
-
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."
|
|
17113
|
-
}
|
|
17114
|
-
],
|
|
17115
|
-
messages: [{ role: "user", content: instruction }],
|
|
17116
|
-
maxTokens: CONSOLIDATION_MAX_TOKENS
|
|
17117
|
-
};
|
|
17118
|
-
const timer = new AbortController();
|
|
17119
|
-
let timedOut = false;
|
|
17120
|
-
const to = setTimeout(() => {
|
|
17121
|
-
timedOut = true;
|
|
17122
|
-
timer.abort(new Error("consolidation timeout"));
|
|
17123
|
-
}, CONSOLIDATION_TIMEOUT_MS);
|
|
17124
|
-
to.unref();
|
|
17125
|
-
try {
|
|
17126
|
-
const res = await llm.provider.complete(req, { signal: timer.signal });
|
|
17127
|
-
const text = res.content.filter(isTextBlock).map((block) => block.text).join("\n").trim();
|
|
17128
|
-
const wholeDocFence = /^```(?:markdown|md)?[^\n]*\n([\s\S]*?)\n?```\s*$/i;
|
|
17129
|
-
const wrapped = wholeDocFence.exec(text);
|
|
17130
|
-
const inner = wrapped?.[1];
|
|
17131
|
-
const unfenced = inner !== void 0 ? inner.trim() : text;
|
|
17132
|
-
return { content: unfenced, model: llm.model };
|
|
17133
|
-
} catch (err) {
|
|
17134
|
-
if (timedOut) {
|
|
17135
|
-
throw new Error(`consolidation timed out after ${CONSOLIDATION_TIMEOUT_MS}ms`);
|
|
17136
|
-
}
|
|
17137
|
-
throw err;
|
|
17138
|
-
} finally {
|
|
17139
|
-
clearTimeout(to);
|
|
17140
|
-
}
|
|
17623
|
+
this.getAutoOptimizeSettings = opts.getAutoOptimizeSettings;
|
|
17141
17624
|
}
|
|
17142
17625
|
/** Handle an incoming client message. Returns a response payload. */
|
|
17143
17626
|
async handleMessage(_ws, type, payload) {
|
|
@@ -17342,70 +17825,47 @@ ${String(p.content ?? "")}`;
|
|
|
17342
17825
|
const conflicts = detectLearnedConflicts(projectRoot);
|
|
17343
17826
|
return { type, payload: { conflicts } };
|
|
17344
17827
|
}
|
|
17345
|
-
// ──
|
|
17346
|
-
//
|
|
17347
|
-
//
|
|
17348
|
-
//
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
case "agent-roster.consolidate": {
|
|
17828
|
+
// ── Optimize: distil captures into skill addenda + a consolidated doc,
|
|
17829
|
+
// then archive and reset the raw buffer. Shared implementation with the
|
|
17830
|
+
// CLI (`optimizeProjectAgentLearning`) so both surfaces persist the same
|
|
17831
|
+
// artifacts instead of the CLI producing markdown nobody saved.
|
|
17832
|
+
case "agent-roster.consolidate":
|
|
17833
|
+
case "agent-roster.optimize": {
|
|
17352
17834
|
if (!role) return { type, payload: { error: "role required" } };
|
|
17353
|
-
const
|
|
17354
|
-
|
|
17835
|
+
const hasExistingConsolidation = isConsolidated(role, projectRoot);
|
|
17836
|
+
const pending = readRawLearnedEntries(role, projectRoot);
|
|
17837
|
+
if (pending.length === 0) {
|
|
17355
17838
|
return {
|
|
17356
17839
|
type: "agent-roster.consolidate",
|
|
17357
17840
|
payload: {
|
|
17358
17841
|
role,
|
|
17359
17842
|
consolidated: false,
|
|
17360
17843
|
rawEntryCount: 0,
|
|
17844
|
+
skills: [],
|
|
17361
17845
|
hasExistingConsolidation,
|
|
17362
17846
|
currentStats: getProjectAgentLearnStats(role, projectRoot)
|
|
17363
17847
|
}
|
|
17364
17848
|
};
|
|
17365
17849
|
}
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
}
|
|
17382
|
-
if (synth && synth.content.length > 0) {
|
|
17383
|
-
let stats;
|
|
17384
|
-
let metadata;
|
|
17385
|
-
try {
|
|
17386
|
-
saveProjectAgentConsolidated(role, synth.content, projectRoot, {
|
|
17387
|
-
trigger: "manual",
|
|
17388
|
-
model: synth.model
|
|
17389
|
-
});
|
|
17390
|
-
stats = getProjectAgentLearnStats(role, projectRoot);
|
|
17391
|
-
metadata = loadConsolidationMetadata(role, projectRoot);
|
|
17392
|
-
} catch (err) {
|
|
17393
|
-
return {
|
|
17394
|
-
type: "agent-roster.consolidate",
|
|
17395
|
-
payload: {
|
|
17396
|
-
role,
|
|
17397
|
-
consolidated: false,
|
|
17398
|
-
rawEntryCount: rawEntries.length,
|
|
17399
|
-
hasExistingConsolidation,
|
|
17400
|
-
currentStats: getProjectAgentLearnStats(role, projectRoot),
|
|
17401
|
-
error: err instanceof Error ? err.message : "failed to persist consolidation"
|
|
17402
|
-
}
|
|
17403
|
-
};
|
|
17404
|
-
}
|
|
17850
|
+
const llm = this.getLlm();
|
|
17851
|
+
const result = await optimizeProjectAgentLearning(role, projectRoot, {
|
|
17852
|
+
...llm ? { llm } : {},
|
|
17853
|
+
trigger: "manual"
|
|
17854
|
+
});
|
|
17855
|
+
const currentStats = getProjectAgentLearnStats(role, projectRoot);
|
|
17856
|
+
const metadata = loadConsolidationMetadata(role, projectRoot);
|
|
17857
|
+
const basePayload = {
|
|
17858
|
+
role,
|
|
17859
|
+
rawEntryCount: result.rawEntryCount,
|
|
17860
|
+
skills: result.skills,
|
|
17861
|
+
hasExistingConsolidation,
|
|
17862
|
+
currentStats
|
|
17863
|
+
};
|
|
17864
|
+
if (result.status === "optimized") {
|
|
17405
17865
|
try {
|
|
17406
17866
|
this.broadcast({
|
|
17407
17867
|
type: "agent-roster.updated",
|
|
17408
|
-
payload: { role, reason: "consolidated", currentStats
|
|
17868
|
+
payload: { role, reason: "consolidated", currentStats, metadata }
|
|
17409
17869
|
});
|
|
17410
17870
|
} catch (e) {
|
|
17411
17871
|
console.warn(
|
|
@@ -17421,44 +17881,101 @@ ${String(p.content ?? "")}`;
|
|
|
17421
17881
|
return {
|
|
17422
17882
|
type: "agent-roster.consolidate",
|
|
17423
17883
|
payload: {
|
|
17424
|
-
|
|
17884
|
+
...basePayload,
|
|
17425
17885
|
consolidated: true,
|
|
17426
|
-
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
currentStats: stats,
|
|
17886
|
+
content: result.content,
|
|
17887
|
+
model: result.model,
|
|
17888
|
+
pruned: result.pruned,
|
|
17430
17889
|
metadata
|
|
17431
17890
|
}
|
|
17432
17891
|
};
|
|
17433
17892
|
}
|
|
17434
|
-
if (synth) {
|
|
17435
|
-
return {
|
|
17436
|
-
type: "agent-roster.consolidate",
|
|
17437
|
-
payload: {
|
|
17438
|
-
role,
|
|
17439
|
-
consolidated: false,
|
|
17440
|
-
emptySynthesis: true,
|
|
17441
|
-
model: synth.model,
|
|
17442
|
-
rawEntryCount: rawEntries.length,
|
|
17443
|
-
hasExistingConsolidation,
|
|
17444
|
-
currentStats: getProjectAgentLearnStats(role, projectRoot)
|
|
17445
|
-
}
|
|
17446
|
-
};
|
|
17447
|
-
}
|
|
17448
17893
|
return {
|
|
17449
17894
|
type: "agent-roster.consolidate",
|
|
17450
17895
|
payload: {
|
|
17451
|
-
|
|
17896
|
+
...basePayload,
|
|
17452
17897
|
consolidated: false,
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17898
|
+
...result.status === "empty-synthesis" ? { emptySynthesis: true, model: result.model } : {},
|
|
17899
|
+
...result.status === "failed" ? { error: result.error } : {},
|
|
17900
|
+
...result.status === "no-llm" ? {
|
|
17901
|
+
instruction: result.instruction,
|
|
17902
|
+
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.`
|
|
17903
|
+
} : {}
|
|
17904
|
+
}
|
|
17905
|
+
};
|
|
17906
|
+
}
|
|
17907
|
+
// ── Automatic-optimization status ─────────────────────────────────
|
|
17908
|
+
// Read-only: says whether the background scheduler considers each role
|
|
17909
|
+
// eligible right now, and why not when it does not. Surfacing the reason
|
|
17910
|
+
// is what keeps "nothing happened" from looking like a broken feature.
|
|
17911
|
+
case "agent-roster.auto-optimize-status": {
|
|
17912
|
+
const policy = resolveAutoOptimizePolicy(
|
|
17913
|
+
this.getAutoOptimizeSettings?.() ?? void 0
|
|
17914
|
+
);
|
|
17915
|
+
const roles = role ? [role] : listProjectAgentRoles(projectRoot);
|
|
17916
|
+
return {
|
|
17917
|
+
type,
|
|
17918
|
+
payload: {
|
|
17919
|
+
policy,
|
|
17920
|
+
roles: roles.map((current2) => {
|
|
17921
|
+
try {
|
|
17922
|
+
const decision = evaluateAutoOptimize(current2, projectRoot, policy);
|
|
17923
|
+
return { role: current2, ...decision };
|
|
17924
|
+
} catch {
|
|
17925
|
+
return { role: current2, eligible: false, reason: "disabled" };
|
|
17926
|
+
}
|
|
17927
|
+
})
|
|
17459
17928
|
}
|
|
17460
17929
|
};
|
|
17461
17930
|
}
|
|
17931
|
+
// ── Skill layer: what this project has developed for each role skill ──
|
|
17932
|
+
case "agent-roster.skills": {
|
|
17933
|
+
if (!role) return { type, payload: { error: "role required" } };
|
|
17934
|
+
const candidates = resolveRoleSkillCandidates(role, projectRoot);
|
|
17935
|
+
const developed = listProjectSkillAugmentations(role, projectRoot);
|
|
17936
|
+
const affinity = loadSkillAffinity(role, projectRoot);
|
|
17937
|
+
return {
|
|
17938
|
+
type,
|
|
17939
|
+
payload: {
|
|
17940
|
+
role,
|
|
17941
|
+
skills: candidates.map((skill) => ({
|
|
17942
|
+
skill,
|
|
17943
|
+
developed: developed.includes(skill),
|
|
17944
|
+
affinity: affinity.entries[skill] ?? null
|
|
17945
|
+
}))
|
|
17946
|
+
}
|
|
17947
|
+
};
|
|
17948
|
+
}
|
|
17949
|
+
case "agent-roster.read-skill": {
|
|
17950
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
17951
|
+
if (!role || !skill) return { type, payload: { error: "role and skill required" } };
|
|
17952
|
+
return {
|
|
17953
|
+
type,
|
|
17954
|
+
payload: { role, skill, content: loadProjectSkillAugmentation(role, skill, projectRoot) }
|
|
17955
|
+
};
|
|
17956
|
+
}
|
|
17957
|
+
case "agent-roster.save-skill": {
|
|
17958
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
17959
|
+
if (!role || !skill || typeof p.content !== "string") {
|
|
17960
|
+
return { type, payload: { error: "role, skill and content required" } };
|
|
17961
|
+
}
|
|
17962
|
+
const savedPath = saveProjectSkillAugmentation(role, skill, p.content, projectRoot);
|
|
17963
|
+
return { type, payload: { role, skill, path: savedPath, success: true } };
|
|
17964
|
+
}
|
|
17965
|
+
case "agent-roster.clear-skill": {
|
|
17966
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
17967
|
+
if (!role) return { type, payload: { error: "role required" } };
|
|
17968
|
+
clearProjectSkillAugmentation(role, skill || void 0, projectRoot);
|
|
17969
|
+
return { type, payload: { role, skill: skill || null, success: true } };
|
|
17970
|
+
}
|
|
17971
|
+
case "agent-roster.pin-skill": {
|
|
17972
|
+
const skill = typeof p.skill === "string" ? p.skill : "";
|
|
17973
|
+
if (!role || !skill || typeof p.pinned !== "boolean") {
|
|
17974
|
+
return { type, payload: { error: "role, skill and boolean pinned required" } };
|
|
17975
|
+
}
|
|
17976
|
+
const affinity = setSkillPinned(role, skill, p.pinned, projectRoot);
|
|
17977
|
+
return { type, payload: { role, skill, pinned: p.pinned, affinity, success: true } };
|
|
17978
|
+
}
|
|
17462
17979
|
// ── Save consolidated document ────────────────────────────────────
|
|
17463
17980
|
case "agent-roster.save-consolidated": {
|
|
17464
17981
|
if (!role || typeof p.content !== "string") {
|
|
@@ -18211,7 +18728,7 @@ async function handleShellOpen(req, logger, options) {
|
|
|
18211
18728
|
import {
|
|
18212
18729
|
enqueueKanbanWorkflowCommand,
|
|
18213
18730
|
kanbanWorkflowId,
|
|
18214
|
-
listBoards as
|
|
18731
|
+
listBoards as listBoards4,
|
|
18215
18732
|
listKanbanWorkflowStates
|
|
18216
18733
|
} from "@wrongstack/kanban";
|
|
18217
18734
|
import {
|
|
@@ -18391,7 +18908,7 @@ var SddBoardWebSocketHandler = class {
|
|
|
18391
18908
|
this.broadcast({ type: "sdd.board.snapshot", payload: null });
|
|
18392
18909
|
if (this.lifecycle) {
|
|
18393
18910
|
try {
|
|
18394
|
-
const boards = await
|
|
18911
|
+
const boards = await listBoards4(this.lifecycle.projectRoot);
|
|
18395
18912
|
this.broadcast({
|
|
18396
18913
|
type: "kanban.list",
|
|
18397
18914
|
payload: { success: true, data: boards }
|
|
@@ -19093,7 +19610,6 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
19093
19610
|
}
|
|
19094
19611
|
|
|
19095
19612
|
// src/server/setup-events-fleet-broadcaster.ts
|
|
19096
|
-
import { watch as fsWatch } from "node:fs";
|
|
19097
19613
|
import * as path20 from "node:path";
|
|
19098
19614
|
function registerSetupEventsFleetBroadcaster(deps2) {
|
|
19099
19615
|
const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
|
|
@@ -19102,8 +19618,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
19102
19618
|
const disposers = [];
|
|
19103
19619
|
const broadcastSessions = async () => {
|
|
19104
19620
|
try {
|
|
19105
|
-
const {
|
|
19106
|
-
const registry =
|
|
19621
|
+
const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
|
|
19622
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
19107
19623
|
const sessions = await registry.list();
|
|
19108
19624
|
const ownEntry = sessions.find((s) => s.pid === process.pid);
|
|
19109
19625
|
const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
|
|
@@ -19150,7 +19666,7 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
19150
19666
|
}
|
|
19151
19667
|
};
|
|
19152
19668
|
onFleetBroadcaster?.(broadcastSessions);
|
|
19153
|
-
let
|
|
19669
|
+
let subscriptionLive = false;
|
|
19154
19670
|
let statusTimer;
|
|
19155
19671
|
const scheduleStatusPoll = () => {
|
|
19156
19672
|
if (isDisposed()) return;
|
|
@@ -19159,35 +19675,31 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
19159
19675
|
void broadcastSessions();
|
|
19160
19676
|
scheduleStatusPoll();
|
|
19161
19677
|
},
|
|
19162
|
-
|
|
19678
|
+
subscriptionLive ? 3e4 : 5e3
|
|
19163
19679
|
);
|
|
19164
19680
|
if (statusTimer.unref) statusTimer.unref();
|
|
19165
19681
|
};
|
|
19166
19682
|
disposers.push(() => {
|
|
19167
19683
|
if (statusTimer) clearTimeout(statusTimer);
|
|
19168
19684
|
});
|
|
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
|
-
}
|
|
19685
|
+
let eventDebounce;
|
|
19686
|
+
let unsubscribe;
|
|
19687
|
+
void import("@wrongstack/core/storage").then(async ({ getSessionRegistry: getSessionRegistry3 }) => {
|
|
19688
|
+
const registry = getSessionRegistry3(globalRoot);
|
|
19689
|
+
const projectSlug2 = wpaths?.projectSlug;
|
|
19690
|
+
if (!projectSlug2 || isDisposed()) return;
|
|
19691
|
+
unsubscribe = await registry.subscribeProject(projectSlug2, context.projectRoot, () => {
|
|
19692
|
+
if (eventDebounce) clearTimeout(eventDebounce);
|
|
19693
|
+
eventDebounce = setTimeout(() => void broadcastSessions(), 25);
|
|
19694
|
+
});
|
|
19695
|
+
subscriptionLive = true;
|
|
19696
|
+
}).catch(() => {
|
|
19697
|
+
subscriptionLive = false;
|
|
19698
|
+
});
|
|
19699
|
+
disposers.push(() => {
|
|
19700
|
+
if (eventDebounce) clearTimeout(eventDebounce);
|
|
19701
|
+
void unsubscribe?.();
|
|
19702
|
+
});
|
|
19191
19703
|
scheduleStatusPoll();
|
|
19192
19704
|
void broadcastSessions();
|
|
19193
19705
|
return () => {
|
|
@@ -19300,7 +19812,7 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
|
|
|
19300
19812
|
}
|
|
19301
19813
|
|
|
19302
19814
|
// src/server/setup-events-status-watcher.ts
|
|
19303
|
-
import { watch as
|
|
19815
|
+
import { watch as fsWatch } from "node:fs";
|
|
19304
19816
|
import * as fs18 from "node:fs/promises";
|
|
19305
19817
|
import * as path22 from "node:path";
|
|
19306
19818
|
|
|
@@ -19390,7 +19902,7 @@ function registerSetupEventsStatusWatcher(deps2) {
|
|
|
19390
19902
|
try {
|
|
19391
19903
|
await fs18.mkdir(projectsDir, { recursive: true });
|
|
19392
19904
|
if (isDisposed()) return;
|
|
19393
|
-
watcher =
|
|
19905
|
+
watcher = fsWatch(
|
|
19394
19906
|
projectsDir,
|
|
19395
19907
|
{ persistent: true, recursive: true },
|
|
19396
19908
|
async (eventType, filename) => {
|
|
@@ -20552,7 +21064,6 @@ import {
|
|
|
20552
21064
|
mailboxSessionTag,
|
|
20553
21065
|
ObservableBrainArbiter as ObservableBrainArbiterCtor
|
|
20554
21066
|
} from "@wrongstack/core/coordination";
|
|
20555
|
-
import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
|
|
20556
21067
|
import { installDesignStudioMiddleware } from "@wrongstack/core/design";
|
|
20557
21068
|
import {
|
|
20558
21069
|
AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
|
|
@@ -20564,6 +21075,7 @@ import {
|
|
|
20564
21075
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
20565
21076
|
import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
|
|
20566
21077
|
import {
|
|
21078
|
+
DEFAULT_TOOLS_CONFIG,
|
|
20567
21079
|
resolveContextWindowPolicy as resolveContextWindowPolicy2
|
|
20568
21080
|
} from "@wrongstack/core/types";
|
|
20569
21081
|
import {
|
|
@@ -21445,6 +21957,11 @@ async function createAgentServices(input) {
|
|
|
21445
21957
|
taskAware: config.Sage?.inject?.taskAware,
|
|
21446
21958
|
minScore: config.Sage?.inject?.minScore,
|
|
21447
21959
|
minImportance: config.Sage?.inject?.minImportance,
|
|
21960
|
+
// Forward the explicit relation floor so an operator-configured
|
|
21961
|
+
// `Sage.inject.relationFloor` is honored in WebUI sessions. Without
|
|
21962
|
+
// this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
|
|
21963
|
+
// is the CLI default but masks operator overrides.
|
|
21964
|
+
relationFloor: config.Sage?.inject?.relationFloor,
|
|
21448
21965
|
repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
|
|
21449
21966
|
verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
|
|
21450
21967
|
triggers: config.Sage?.inject?.triggers,
|
|
@@ -21466,6 +21983,10 @@ async function createAgentServices(input) {
|
|
|
21466
21983
|
maxMemories: config.Sage?.inject?.maxTurnMemories,
|
|
21467
21984
|
maxChars: config.Sage?.inject?.maxCharsPerTurn,
|
|
21468
21985
|
minScore: config.Sage?.inject?.minScore,
|
|
21986
|
+
// CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
|
|
21987
|
+
// value drives both runtimes instead of silently falling back to the
|
|
21988
|
+
// 0.3 default. The undefined case keeps the middleware's own default.
|
|
21989
|
+
metadataWeight: config.Sage?.retrieval?.metadataWeight,
|
|
21469
21990
|
getSessionId: getSageSessionId,
|
|
21470
21991
|
tracker: sageInjectionTracker
|
|
21471
21992
|
})
|
|
@@ -21490,25 +22011,29 @@ async function createAgentServices(input) {
|
|
|
21490
22011
|
strategy: config.context?.strategy,
|
|
21491
22012
|
preserveK: config.context?.preserveK ?? 10,
|
|
21492
22013
|
eliseThreshold: config.context?.eliseThreshold ?? 2e3,
|
|
22014
|
+
// Match the CLI/TUI runtime: keep corrections, errors and decisions
|
|
22015
|
+
// verbatim while collapsing routine assistant chatter/tool protocol.
|
|
22016
|
+
// Without this WebUI's hybrid strategy builds an ever-growing lossless
|
|
22017
|
+
// digest and eventually relies on blunt emergency head/tail trimming.
|
|
22018
|
+
smart: true,
|
|
21493
22019
|
summarizerModel: config.context?.summarizerModel,
|
|
21494
22020
|
llmSelector: config.context?.llmSelector
|
|
21495
22021
|
});
|
|
21496
22022
|
const initialContextPolicy = resolveContextWindowPolicy2(config.context);
|
|
21497
22023
|
let autoCompactor;
|
|
21498
22024
|
if (config.context?.autoCompact !== false) {
|
|
21499
|
-
let effectiveMaxContext =
|
|
21500
|
-
|
|
21501
|
-
|
|
21502
|
-
|
|
21503
|
-
|
|
21504
|
-
|
|
21505
|
-
|
|
21506
|
-
|
|
21507
|
-
|
|
21508
|
-
|
|
21509
|
-
} catch {
|
|
21510
|
-
}
|
|
22025
|
+
let effectiveMaxContext = 0;
|
|
22026
|
+
try {
|
|
22027
|
+
const m = await resolveProviderModelMetadata(
|
|
22028
|
+
modelsRegistry,
|
|
22029
|
+
config.provider,
|
|
22030
|
+
context.model,
|
|
22031
|
+
config.providers?.[config.provider]
|
|
22032
|
+
);
|
|
22033
|
+
effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
|
|
22034
|
+
} catch {
|
|
21511
22035
|
}
|
|
22036
|
+
if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
|
|
21512
22037
|
if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
|
|
21513
22038
|
autoCompactor = new AutoCompactionMiddlewareCtor(
|
|
21514
22039
|
compactor,
|
|
@@ -22029,6 +22554,7 @@ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES)
|
|
|
22029
22554
|
|
|
22030
22555
|
// src/server/message-dispatcher.ts
|
|
22031
22556
|
import path23 from "node:path";
|
|
22557
|
+
import { planTool, taskTool, todoTool } from "@wrongstack/tools";
|
|
22032
22558
|
function createMessageDispatcher(opts) {
|
|
22033
22559
|
const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
|
|
22034
22560
|
function makeWorklistContext() {
|
|
@@ -22040,7 +22566,20 @@ function createMessageDispatcher(opts) {
|
|
|
22040
22566
|
},
|
|
22041
22567
|
send: (w, m) => send(w, m),
|
|
22042
22568
|
broadcast: (m) => broadcast(state.getClients(), m),
|
|
22043
|
-
replaceTodos: (todos) => deps2.context.state.replaceTodos(todos)
|
|
22569
|
+
replaceTodos: (todos) => deps2.context.state.replaceTodos(todos),
|
|
22570
|
+
mutateTodos: async (todos) => {
|
|
22571
|
+
const result = await todoTool.execute({ todos }, deps2.context, {
|
|
22572
|
+
signal: AbortSignal.timeout(3e4)
|
|
22573
|
+
});
|
|
22574
|
+
return {
|
|
22575
|
+
todos: [...deps2.context.todos],
|
|
22576
|
+
...result.kanban_warnings ? { warnings: result.kanban_warnings } : {}
|
|
22577
|
+
};
|
|
22578
|
+
},
|
|
22579
|
+
mutateTaskStatus: async (id, status) => taskTool.execute({ action: "status", id, status }, deps2.context, {
|
|
22580
|
+
signal: AbortSignal.timeout(3e4)
|
|
22581
|
+
}),
|
|
22582
|
+
mutatePlan: async (operation) => planTool.execute(operation, deps2.context, { signal: AbortSignal.timeout(3e4) })
|
|
22044
22583
|
};
|
|
22045
22584
|
}
|
|
22046
22585
|
function makeSkillsContext() {
|
|
@@ -22130,16 +22669,24 @@ function createMessageDispatcher(opts) {
|
|
|
22130
22669
|
getAgent: () => deps2.agent,
|
|
22131
22670
|
getSessionId: () => state.getSession().id,
|
|
22132
22671
|
runControl: {
|
|
22133
|
-
begin: () => {
|
|
22672
|
+
begin: (_ws, sessionId) => {
|
|
22134
22673
|
if (runLock.get()) return void 0;
|
|
22135
22674
|
const controller = new AbortController();
|
|
22136
22675
|
runLock.set(controller);
|
|
22676
|
+
runLock.setSession(sessionId);
|
|
22137
22677
|
return controller;
|
|
22138
22678
|
},
|
|
22139
|
-
end: (_ws, controller) => {
|
|
22140
|
-
if (runLock.get() === controller)
|
|
22679
|
+
end: (_ws, _sessionId, controller) => {
|
|
22680
|
+
if (runLock.get() === controller) {
|
|
22681
|
+
runLock.set(null);
|
|
22682
|
+
runLock.setSession(null);
|
|
22683
|
+
}
|
|
22141
22684
|
},
|
|
22142
|
-
abort: () =>
|
|
22685
|
+
abort: (_ws, sessionId) => {
|
|
22686
|
+
if (runLock.getSession() === sessionId || !runLock.getSession()) {
|
|
22687
|
+
runLock.get()?.abort();
|
|
22688
|
+
}
|
|
22689
|
+
}
|
|
22143
22690
|
},
|
|
22144
22691
|
pendingConfirms,
|
|
22145
22692
|
send,
|
|
@@ -22161,10 +22708,20 @@ function createMessageDispatcher(opts) {
|
|
|
22161
22708
|
const goalSnapshotRoutes = {
|
|
22162
22709
|
getSnapshot: () => handleGoalGet(state.getProjectRoot(), (message) => broadcast(state.getClients(), message))
|
|
22163
22710
|
};
|
|
22711
|
+
const kanbanSupervisor = createKanbanSupervisor({
|
|
22712
|
+
projectRoot: () => state.getProjectRoot(),
|
|
22713
|
+
broadcast: (message) => broadcast(state.getClients(), message),
|
|
22714
|
+
log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
|
|
22715
|
+
});
|
|
22716
|
+
if (opts.onDispose) {
|
|
22717
|
+
const dispose = () => kanbanSupervisor.dispose();
|
|
22718
|
+
opts.onDispose(dispose);
|
|
22719
|
+
}
|
|
22164
22720
|
const kanbanContext = () => ({
|
|
22165
22721
|
projectRoot: state.getProjectRoot(),
|
|
22166
22722
|
context: deps2.context,
|
|
22167
|
-
broadcast: (message) => broadcast(state.getClients(), message)
|
|
22723
|
+
broadcast: (message) => broadcast(state.getClients(), message),
|
|
22724
|
+
supervisor: kanbanSupervisor
|
|
22168
22725
|
});
|
|
22169
22726
|
const kanbanHostRoutes = {
|
|
22170
22727
|
meta: async (ws) => {
|
|
@@ -22234,6 +22791,7 @@ function createMessageDispatcher(opts) {
|
|
|
22234
22791
|
agentRoster: {
|
|
22235
22792
|
rosterHandler: new AgentRosterWSHandler({
|
|
22236
22793
|
projectRoot: state.getProjectRoot,
|
|
22794
|
+
getAutoOptimizeSettings: () => state.getConfig().fleet?.learning?.autoOptimize,
|
|
22237
22795
|
getLlm: () => {
|
|
22238
22796
|
const ctx = deps2.agent.ctx;
|
|
22239
22797
|
return ctx.provider && ctx.model ? { provider: ctx.provider, model: ctx.model } : void 0;
|
|
@@ -22680,6 +23238,14 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
|
|
|
22680
23238
|
transition = transition.then(async () => {
|
|
22681
23239
|
if (stopped) return;
|
|
22682
23240
|
if (pendingClaim?.sessionId === sessionId) {
|
|
23241
|
+
await pendingClaim.claim.activate({
|
|
23242
|
+
sessionId,
|
|
23243
|
+
...target,
|
|
23244
|
+
clientType: "webui",
|
|
23245
|
+
pid: process.pid,
|
|
23246
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23247
|
+
agents: statusTracker.getAgents()
|
|
23248
|
+
});
|
|
22683
23249
|
pendingClaim = void 0;
|
|
22684
23250
|
} else {
|
|
22685
23251
|
await register(sessionId, true, target);
|
|
@@ -22703,14 +23269,35 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
|
|
|
22703
23269
|
}
|
|
22704
23270
|
if (sessionId === activeSessionId) return async () => {
|
|
22705
23271
|
};
|
|
22706
|
-
const previousSessionId = activeSessionId;
|
|
22707
|
-
const previousTarget = activeTarget;
|
|
22708
23272
|
const token = Symbol(sessionId);
|
|
22709
|
-
|
|
22710
|
-
|
|
23273
|
+
if ("reserveResume" in registry && typeof registry.reserveResume === "function") {
|
|
23274
|
+
const reservation = await registry.reserveResume({
|
|
23275
|
+
sessionId,
|
|
23276
|
+
projectSlug: target.projectSlug,
|
|
23277
|
+
projectRoot: target.projectRoot
|
|
23278
|
+
});
|
|
23279
|
+
pendingClaim = { sessionId, token, claim: reservation, target };
|
|
23280
|
+
} else {
|
|
23281
|
+
await register(sessionId, true, target);
|
|
23282
|
+
pendingClaim = {
|
|
23283
|
+
sessionId,
|
|
23284
|
+
token,
|
|
23285
|
+
target,
|
|
23286
|
+
claim: {
|
|
23287
|
+
reservation: {
|
|
23288
|
+
reservationId: "legacy",
|
|
23289
|
+
targetSessionId: sessionId,
|
|
23290
|
+
requesterInstanceId: "legacy",
|
|
23291
|
+
expiresAt: Number.MAX_SAFE_INTEGER
|
|
23292
|
+
},
|
|
23293
|
+
activate: async () => void 0,
|
|
23294
|
+
cancel: async () => register(activeSessionId, true, activeTarget)
|
|
23295
|
+
}
|
|
23296
|
+
};
|
|
23297
|
+
}
|
|
22711
23298
|
return async () => {
|
|
22712
23299
|
if (pendingClaim?.token !== token) return;
|
|
22713
|
-
await
|
|
23300
|
+
await pendingClaim.claim.cancel();
|
|
22714
23301
|
pendingClaim = void 0;
|
|
22715
23302
|
};
|
|
22716
23303
|
};
|
|
@@ -23415,11 +24002,11 @@ function buildRoutes(state, deps2, cb) {
|
|
|
23415
24002
|
}
|
|
23416
24003
|
|
|
23417
24004
|
// src/server/server-runtime.ts
|
|
23418
|
-
import * as path28 from "node:path";
|
|
23419
24005
|
import { createRequire as createRequire4 } from "node:module";
|
|
24006
|
+
import * as path28 from "node:path";
|
|
23420
24007
|
import { fileURLToPath } from "node:url";
|
|
23421
|
-
import { WebSocketServer } from "ws";
|
|
23422
24008
|
import { toErrorMessage as toErrorMessage12 } from "@wrongstack/core/utils";
|
|
24009
|
+
import { WebSocketServer } from "ws";
|
|
23423
24010
|
async function resolvePorts(opts) {
|
|
23424
24011
|
const surface = opts.surface ?? "webui";
|
|
23425
24012
|
const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
|
|
@@ -23637,10 +24224,11 @@ function startHttpServer(opts) {
|
|
|
23637
24224
|
return httpServer;
|
|
23638
24225
|
}
|
|
23639
24226
|
function registerShutdown(deps2) {
|
|
23640
|
-
registerShutdownHandlers({
|
|
24227
|
+
return registerShutdownHandlers({
|
|
23641
24228
|
flushSession: deps2.flushSession,
|
|
23642
24229
|
clients: deps2.clients,
|
|
23643
24230
|
servers: deps2.servers,
|
|
24231
|
+
onPreShutdown: deps2.onPreShutdown,
|
|
23644
24232
|
onShutdown: deps2.onShutdown
|
|
23645
24233
|
});
|
|
23646
24234
|
}
|
|
@@ -23955,10 +24543,15 @@ async function startWebUI(opts = {}) {
|
|
|
23955
24543
|
);
|
|
23956
24544
|
}
|
|
23957
24545
|
let _runLock = null;
|
|
24546
|
+
let _runLockSession = null;
|
|
23958
24547
|
const runLockControl = {
|
|
23959
24548
|
get: () => _runLock,
|
|
23960
24549
|
set: (ctrl) => {
|
|
23961
24550
|
_runLock = ctrl;
|
|
24551
|
+
},
|
|
24552
|
+
getSession: () => _runLockSession,
|
|
24553
|
+
setSession: (id) => {
|
|
24554
|
+
_runLockSession = id;
|
|
23962
24555
|
}
|
|
23963
24556
|
};
|
|
23964
24557
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
@@ -24083,6 +24676,7 @@ async function startWebUI(opts = {}) {
|
|
|
24083
24676
|
if (ctrl) {
|
|
24084
24677
|
ctrl.abort();
|
|
24085
24678
|
runLockControl.set(null);
|
|
24679
|
+
runLockControl.setSession(null);
|
|
24086
24680
|
}
|
|
24087
24681
|
},
|
|
24088
24682
|
isRunActive: () => runLockControl.get() !== null,
|
|
@@ -24232,6 +24826,7 @@ async function startWebUI(opts = {}) {
|
|
|
24232
24826
|
})
|
|
24233
24827
|
});
|
|
24234
24828
|
const routes = buildRoutes(state, deps2, cb);
|
|
24829
|
+
let kanbanSupervisorDispose = null;
|
|
24235
24830
|
const handleMessage = createMessageDispatcher({
|
|
24236
24831
|
state,
|
|
24237
24832
|
deps: deps2,
|
|
@@ -24239,7 +24834,10 @@ async function startWebUI(opts = {}) {
|
|
|
24239
24834
|
promptsCtx,
|
|
24240
24835
|
codebaseIndexing,
|
|
24241
24836
|
runLock: runLockControl,
|
|
24242
|
-
pendingConfirms
|
|
24837
|
+
pendingConfirms,
|
|
24838
|
+
onDispose: (dispose) => {
|
|
24839
|
+
kanbanSupervisorDispose = dispose;
|
|
24840
|
+
}
|
|
24243
24841
|
});
|
|
24244
24842
|
const mailbox = getSharedProjectMailbox4(
|
|
24245
24843
|
resolveProjectDir3(context.projectRoot, wstackGlobalRoot2()),
|
|
@@ -24273,7 +24871,14 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
24273
24871
|
priority: "high",
|
|
24274
24872
|
senderSessionId: session.id
|
|
24275
24873
|
}).catch((err) => {
|
|
24276
|
-
console.warn(
|
|
24874
|
+
console.warn(
|
|
24875
|
+
JSON.stringify({
|
|
24876
|
+
level: "warn",
|
|
24877
|
+
event: "webui.security_rejection_mailbox_note_failed",
|
|
24878
|
+
message: String(err),
|
|
24879
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
24880
|
+
})
|
|
24881
|
+
);
|
|
24277
24882
|
});
|
|
24278
24883
|
},
|
|
24279
24884
|
goalHandler,
|
|
@@ -24287,7 +24892,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
24287
24892
|
});
|
|
24288
24893
|
wssPrimary.on("connection", handleConnection);
|
|
24289
24894
|
if (wssSecondary) wssSecondary.on("connection", handleConnection);
|
|
24290
|
-
|
|
24895
|
+
let unregisterShutdown = () => {
|
|
24896
|
+
};
|
|
24897
|
+
unregisterShutdown = registerShutdown({
|
|
24291
24898
|
flushSession: async () => {
|
|
24292
24899
|
await session.append({
|
|
24293
24900
|
type: "session_end",
|
|
@@ -24303,7 +24910,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
24303
24910
|
wssPrimary,
|
|
24304
24911
|
...wssSecondary ? [wssSecondary] : []
|
|
24305
24912
|
],
|
|
24913
|
+
onPreShutdown: () => {
|
|
24914
|
+
kanbanSupervisorDispose?.();
|
|
24915
|
+
kanbanSupervisorDispose = null;
|
|
24916
|
+
},
|
|
24306
24917
|
onShutdown: async () => {
|
|
24918
|
+
unregisterShutdown();
|
|
24307
24919
|
await todosCheckpoint.detach();
|
|
24308
24920
|
await stopHeapWatchdog();
|
|
24309
24921
|
credentialWatcherClose?.();
|