@a-t-h-i/bot-lobby 0.6.45 → 0.6.46
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/package.json +1 -1
- package/src/execution/task-cancellation.ts +27 -0
- package/src/lobby/host.ts +4 -0
- package/src/lobby/prompts.ts +1 -2
- package/src/lobby/service.ts +77 -1
- package/src/state/persistence.ts +26 -1
- package/src/webui/api/index.ts +13 -6
- package/src/webui/api/tasks.ts +39 -2
- package/src/webui/palette-core.ts +233 -2
- package/src/webui/protocol.ts +6 -74
- package/src/workflow/workflow.ts +15 -3
- package/webui/dist/assets/index-BJ1fEle9.js +110 -0
- package/webui/dist/assets/index-BdtiIB9L.css +2 -0
- package/webui/dist/build.json +33 -61
- package/webui/dist/index.html +2 -2
- package/src/lobby/models/metrics.ts +0 -14
- package/src/webui/api/metrics.ts +0 -96
- package/webui/dist/assets/index-ClwTDiyo.css +0 -2
- package/webui/dist/assets/index-Ct47Kp7Q.js +0 -110
package/src/webui/protocol.ts
CHANGED
|
@@ -276,78 +276,6 @@ export interface PlannerSeedInfo {
|
|
|
276
276
|
body: string;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
/** The Metrics tab's KPI row as data: runs, success, average run, cost, tasks. */
|
|
280
|
-
export interface MetricsTiles {
|
|
281
|
-
runs: number;
|
|
282
|
-
successes: number;
|
|
283
|
-
stalls: number;
|
|
284
|
-
avgMs: number;
|
|
285
|
-
p90Ms: number;
|
|
286
|
-
cost: number;
|
|
287
|
-
completed: number;
|
|
288
|
-
active: number;
|
|
289
|
-
avgCompleteMs: number;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** One per-model group as `metrics.get` carries it. */
|
|
293
|
-
export interface MetricGroupInfo {
|
|
294
|
-
model: string;
|
|
295
|
-
thinking: string;
|
|
296
|
-
kinds: string[];
|
|
297
|
-
runs: number;
|
|
298
|
-
successes: number;
|
|
299
|
-
timeouts: number;
|
|
300
|
-
avgMs: number;
|
|
301
|
-
p50Ms: number;
|
|
302
|
-
p90Ms: number;
|
|
303
|
-
avgTurns: number;
|
|
304
|
-
avgTools: number;
|
|
305
|
-
avgTokens: number;
|
|
306
|
-
avgCost: number;
|
|
307
|
-
totalCost: number;
|
|
308
|
-
tokensPerSecond: number;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/** One agent's share of all run time. */
|
|
312
|
-
export interface MetricsAgentShare {
|
|
313
|
-
agent: string;
|
|
314
|
-
runs: number;
|
|
315
|
-
ms: number;
|
|
316
|
-
share: number;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Completed tasks' request-to-done time by the oracle's model and thinking. */
|
|
320
|
-
export interface MetricsTaskTime {
|
|
321
|
-
model: string;
|
|
322
|
-
thinking: string;
|
|
323
|
-
tasks: number;
|
|
324
|
-
avgMs: number;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** What the classifier did and spared, when it has run. */
|
|
328
|
-
export interface MetricsClassifier {
|
|
329
|
-
calls: number;
|
|
330
|
-
ok: number;
|
|
331
|
-
p50Ms: number;
|
|
332
|
-
p90Ms: number;
|
|
333
|
-
input: number;
|
|
334
|
-
byPurpose: Record<string, number>;
|
|
335
|
-
seatRunsSkipped: number;
|
|
336
|
-
questionsAnswered: number;
|
|
337
|
-
quickFixesHeld: number;
|
|
338
|
-
routed: number;
|
|
339
|
-
routedOk: number;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** `metrics.get`: the dashboard figures for one grouping and search. */
|
|
343
|
-
export interface MetricsData {
|
|
344
|
-
tiles: MetricsTiles;
|
|
345
|
-
groups: MetricGroupInfo[];
|
|
346
|
-
daily: Array<{ date: string; runs: number; successes: number; cost: number }>;
|
|
347
|
-
timeShare: { byAgent: MetricsAgentShare[]; taskTimes: MetricsTaskTime[] };
|
|
348
|
-
classifier?: MetricsClassifier;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
279
|
/** Who a knowledge file belongs to. */
|
|
352
280
|
export type KnowledgeAgentName = "master" | "designer" | "backend" | "qa";
|
|
353
281
|
|
|
@@ -395,6 +323,8 @@ export interface EntryRefInfo {
|
|
|
395
323
|
|
|
396
324
|
/** `tasks.get`: one task read whole, in the terminal's wording. */
|
|
397
325
|
export interface TaskDetail {
|
|
326
|
+
canAccept: boolean;
|
|
327
|
+
canForceClose: boolean;
|
|
398
328
|
timing?: TimingProjection;
|
|
399
329
|
delivery?: Delivery;
|
|
400
330
|
/** The full request, when it says more than the title. */
|
|
@@ -414,7 +344,7 @@ export interface TaskDetail {
|
|
|
414
344
|
work?: WorkClock;
|
|
415
345
|
amendments: string[];
|
|
416
346
|
/** Approvals waiting on you: `kind` plus `for <domain>: <detail>`. */
|
|
417
|
-
waiting: Array<{ kind: string; detail: string }>;
|
|
347
|
+
waiting: Array<{ id: string; domain: string; kind: string; detail: string }>;
|
|
418
348
|
blockers: Array<{ reason: string; need: string }>;
|
|
419
349
|
/** The last six runs, one line each (`✓ DEV worker · 3m 12s · …`). */
|
|
420
350
|
runs: string[];
|
|
@@ -589,6 +519,9 @@ export interface Api {
|
|
|
589
519
|
"tasks.get": { request: { taskId: string }; result: TaskDetail };
|
|
590
520
|
"tasks.open": { request: { taskId: string }; result: { sessionId?: string; key?: string; notice?: string } };
|
|
591
521
|
"tasks.resume": { request: { taskId: string }; result: { notice: string; key?: string } };
|
|
522
|
+
"tasks.accept": { request: { taskId: string }; result: { notice: string } };
|
|
523
|
+
"tasks.forceClose": { request: { taskId: string; title: string }; result: { notice: string } };
|
|
524
|
+
"tasks.resolveApproval": { request: { taskId: string; approvalId: string; decision: "approved" | "rejected"; note?: string }; result: { notice: string } };
|
|
592
525
|
"tasks.deliveryReview": { request: { taskId: string }; result: { delivery: Delivery } };
|
|
593
526
|
"tasks.deliveryDefer": { request: { taskId: string; reviewId: string }; result: { delivery: Delivery } };
|
|
594
527
|
"tasks.deliver": { request: { taskId: string; reviewId: string; action: DeliveryAction; confirmMain?: boolean }; result: { delivery: Delivery } };
|
|
@@ -624,7 +557,6 @@ export interface Api {
|
|
|
624
557
|
"quickfix.cancel": { request: { id: string }; result: { notice: string } };
|
|
625
558
|
"quickfix.runAnyway": { request: { id: string }; result: { notice: string } };
|
|
626
559
|
"quickfix.movedToTask": { request: { id: string }; result: { notice: string; key?: string } };
|
|
627
|
-
"metrics.get": { request: { groupBy: "model" | "model-kind"; query?: string; from?: string; to?: string; timeZone?: string }; result: MetricsData };
|
|
628
560
|
"projects.browse": { request: { path?: string }; result: FolderListing };
|
|
629
561
|
"projects.open": { request: { path: string }; result: { project: ProjectInfo } };
|
|
630
562
|
"knowledge.files": { request: Record<string, never>; result: { files: KnowledgeFileInfo[]; models?: Partial<Record<KnowledgeAgentName, string>> } };
|
package/src/workflow/workflow.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/
|
|
|
28
28
|
import { previewDir } from "../ask/relay.ts";
|
|
29
29
|
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
30
30
|
import { mapConcurrent } from "../execution/agent-runner.ts";
|
|
31
|
+
import { taskExecution } from "../execution/task-cancellation.ts";
|
|
31
32
|
import { parseWorkerResult } from "../roles/worker.ts";
|
|
32
33
|
import { autoNote, DESK_TOOLS, DeskSession } from "../desk/session.ts";
|
|
33
34
|
import type { Handover } from "../desk/desk.ts";
|
|
@@ -1171,7 +1172,7 @@ function handoverLog(handovers: readonly Handover[]): string {
|
|
|
1171
1172
|
return `File handovers:\n${lines.join("\n")}`;
|
|
1172
1173
|
}
|
|
1173
1174
|
|
|
1174
|
-
function handleResolveApproval(task: Task, params: OrchestrateParams): string {
|
|
1175
|
+
export function handleResolveApproval(task: Task, params: OrchestrateParams): string {
|
|
1175
1176
|
const id = params.approvalId?.trim();
|
|
1176
1177
|
const decision = params.decision;
|
|
1177
1178
|
if (!id || (decision !== "approved" && decision !== "rejected")) {
|
|
@@ -1557,7 +1558,9 @@ async function handleComplete(task: Task, params: OrchestrateParams, deps: Workf
|
|
|
1557
1558
|
// The gate refuses before any side effect, so a refusal leaves the task untouched.
|
|
1558
1559
|
const oversized = overThreshold(readDataRoots(deps.root, deps.configDir), deps.config.knowledge.compactionThreshold);
|
|
1559
1560
|
if (oversized.length > 0) throw new Error(`cannot complete: knowledge files over the compaction threshold: ${oversized.map((entry) => `${entry.agent}/${entry.file} (${entry.chars})`).join(", ")}. First record domain-relevant facts where they belong with action=knowledge (domain=designer|backend|qa), then rewrite each oversized file with action=compact, then call complete again.`);
|
|
1561
|
+
deps.signal?.throwIfAborted();
|
|
1560
1562
|
const committed = await commitTaskWork(task, params, deps);
|
|
1563
|
+
deps.signal?.throwIfAborted();
|
|
1561
1564
|
const summary = params.text?.trim() || task.proposal || task.title;
|
|
1562
1565
|
recordCompletion(deps, task, summary);
|
|
1563
1566
|
removeTaskScratchpads(deps.root, deps.configDir, task.id);
|
|
@@ -1598,6 +1601,7 @@ async function commitTaskWork(task: Task, params: OrchestrateParams, deps: Workf
|
|
|
1598
1601
|
}
|
|
1599
1602
|
let result: Awaited<ReturnType<typeof commitFiles>>;
|
|
1600
1603
|
try {
|
|
1604
|
+
deps.signal?.throwIfAborted();
|
|
1601
1605
|
result = await commitFiles(state.top, dirty, message);
|
|
1602
1606
|
} catch (error) {
|
|
1603
1607
|
throw new Error(`cannot complete: git refused the commit: ${(error as Error).message}`);
|
|
@@ -2098,14 +2102,19 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
2098
2102
|
}
|
|
2099
2103
|
const gone = missingWorktree(task);
|
|
2100
2104
|
if (gone && params.action !== "status") return { ok: false, taskId: task.id, state: task.state, message: `Rejected: ${gone}` };
|
|
2105
|
+
const execution = taskExecution(deps.root, deps.configDir, task.id, deps.signal);
|
|
2101
2106
|
const finished = new Map<string, AgentRun>();
|
|
2102
2107
|
const identity = { root: deps.root, configDir: deps.configDir, taskId: task.id, sessionId: task.ownerSessionId };
|
|
2103
|
-
const wait = <T>(run: () => Promise<T>) => {
|
|
2108
|
+
const wait = async <T>(run: () => Promise<T>) => {
|
|
2109
|
+
execution.signal.throwIfAborted();
|
|
2104
2110
|
saveTask(deps.root, deps.configDir, task);
|
|
2105
|
-
|
|
2111
|
+
const result = await withBlockingRequest(identity, run);
|
|
2112
|
+
execution.signal.throwIfAborted();
|
|
2113
|
+
return result;
|
|
2106
2114
|
};
|
|
2107
2115
|
const tracked: WorkflowDeps = {
|
|
2108
2116
|
...deps,
|
|
2117
|
+
signal: execution.signal,
|
|
2109
2118
|
choose: (...args) => wait(() => deps.choose(...args)),
|
|
2110
2119
|
ask: (...args) => wait(() => deps.ask(...args)),
|
|
2111
2120
|
...(deps.askQuestions ? { askQuestions: (...args: Parameters<NonNullable<WorkflowDeps["askQuestions"]>>) => wait(() => deps.askQuestions!(...args)) } : {}),
|
|
@@ -2113,6 +2122,7 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
2113
2122
|
cwd: taskCwd(task, deps.cwd),
|
|
2114
2123
|
// An agent can work for an hour: what the action has done so far (implementing, reviewing) is saved before it starts, so every window reads it.
|
|
2115
2124
|
runProcess: (args, options) => {
|
|
2125
|
+
execution.signal.throwIfAborted();
|
|
2116
2126
|
saveTask(deps.root, deps.configDir, task);
|
|
2117
2127
|
return (deps.runProcess ?? spawnPiProcess)(args, options);
|
|
2118
2128
|
},
|
|
@@ -2134,6 +2144,7 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
2134
2144
|
const working = new Set<string>();
|
|
2135
2145
|
try {
|
|
2136
2146
|
const message = await handler(task, params, tracked);
|
|
2147
|
+
execution.signal.throwIfAborted();
|
|
2137
2148
|
const runs = recordRunLog(task, finished, deps);
|
|
2138
2149
|
saveTask(deps.root, deps.configDir, task);
|
|
2139
2150
|
return { ok: true, taskId: task.id, state: task.state, message: `${message}${runsFooter(runs)}${budgetFooter(task, deps)}`, runs };
|
|
@@ -2142,6 +2153,7 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
2142
2153
|
saveTask(deps.root, deps.configDir, task);
|
|
2143
2154
|
return { ok: false, taskId: task.id, state: task.state, message: `Rejected: ${(error as Error).message}${budgetFooter(task, deps)}`, runs };
|
|
2144
2155
|
} finally {
|
|
2156
|
+
execution.release();
|
|
2145
2157
|
for (const runId of working) noteWorkerRun(deps.root, deps.configDir, task.id, { runId, startedAt: "", status: "cancelled" });
|
|
2146
2158
|
}
|
|
2147
2159
|
}
|