@a-t-h-i/bot-lobby 0.6.53 → 0.6.55

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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/prompts/expert.md +17 -0
  4. package/prompts/goal-check.md +44 -0
  5. package/prompts/master.md +70 -32
  6. package/prompts/worker.md +25 -11
  7. package/src/desk/board-client.ts +65 -0
  8. package/src/desk/board.ts +200 -0
  9. package/src/desk/client-extension.ts +31 -1
  10. package/src/desk/ipc.ts +16 -4
  11. package/src/desk/session.ts +129 -1
  12. package/src/execution/agent-runner.ts +27 -132
  13. package/src/execution/delegation.ts +112 -38
  14. package/src/execution/live-run.ts +3 -14
  15. package/src/execution/pi-runner.ts +30 -163
  16. package/src/execution/worker-trees.ts +396 -0
  17. package/src/index.ts +6 -0
  18. package/src/lobby/feed.ts +1 -1
  19. package/src/lobby/host.ts +2 -0
  20. package/src/lobby/planner.ts +2 -2
  21. package/src/lobby/pr-review.ts +3 -3
  22. package/src/lobby/quickfix.ts +4 -3
  23. package/src/lobby/recovery.ts +2 -10
  24. package/src/lobby/runtime.ts +1 -1
  25. package/src/lobby/service.ts +3 -1
  26. package/src/master/goal-check.ts +114 -0
  27. package/src/master/master.ts +60 -24
  28. package/src/master/research.ts +3 -6
  29. package/src/pi/commands.ts +5 -45
  30. package/src/pi/delegation.ts +23 -4
  31. package/src/pi/events.ts +6 -19
  32. package/src/pi/grants.ts +89 -0
  33. package/src/pi/model-settings.ts +2 -4
  34. package/src/pi/model-support.ts +12 -5
  35. package/src/pi/route.ts +1 -1
  36. package/src/pi/run-summary.ts +3 -7
  37. package/src/pi/start-flags.ts +3 -3
  38. package/src/pi/start-task.ts +2 -8
  39. package/src/pi/tools.ts +10 -12
  40. package/src/roles/worker.ts +0 -18
  41. package/src/schemas/configuration.ts +81 -39
  42. package/src/schemas/findings.ts +0 -12
  43. package/src/schemas/task.ts +12 -1
  44. package/src/state/metrics.ts +1 -1
  45. package/src/state/work-clock.ts +140 -0
  46. package/src/state/work-time.ts +4 -7
  47. package/src/webui/api/index.ts +2 -1
  48. package/src/webui/api/settings.ts +6 -18
  49. package/src/webui/dev/fake-service.ts +14 -0
  50. package/src/webui/protocol.ts +23 -0
  51. package/src/workflow/workflow.ts +204 -344
  52. package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-CIfjkb5H.js} +1 -1
  53. package/webui/dist/assets/index-BkXhtB6A.css +2 -0
  54. package/webui/dist/assets/index-DIM2-FUQ.js +114 -0
  55. package/webui/dist/build.json +10 -6
  56. package/webui/dist/index.html +2 -2
  57. package/src/state/budget.ts +0 -301
  58. package/src/state/step-limits.ts +0 -41
  59. package/webui/dist/assets/index-Cca5uEVs.js +0 -115
  60. package/webui/dist/assets/index-DsVdXmh5.css +0 -2
@@ -7,16 +7,31 @@ import type { AgentRun } from "../schemas/findings.ts";
7
7
  import type { Domain, Role } from "../schemas/agent.ts";
8
8
  import { effortFor } from "../classifier/instance.ts";
9
9
  import { fellShort, profileLabel, routeLabel } from "../classifier/effort.ts";
10
- import { agentProfile } from "../schemas/configuration.ts";
10
+ import { agentProfile, expertProfile } from "../schemas/configuration.ts";
11
+ import { loadPrompt } from "../prompts/loader.ts";
11
12
  import { dataRoot, detectProjectRoot, loadConfig } from "../state/project.ts";
12
13
  import { EditLog } from "../state/changes.ts";
13
14
  import { createLiveRun } from "./live-run.ts";
14
15
  import { runPiAgent, type PiRunOptions, type ProcessRunner } from "./pi-runner.ts";
15
16
  import { startDeskServer } from "../desk/ipc.ts";
16
- import { DESK_ENV, DESK_TOOLS, DeskSession, deskSessionAt } from "../desk/session.ts";
17
+ import { DESK_ENV, DeskSession, deskSessionAt, PEER_TOOLS } from "../desk/session.ts";
18
+ import { BOARD_ENV } from "../desk/board.ts";
17
19
 
18
20
  export const MAX_CHILDREN = 5;
19
21
  export const DELEGATE_TOOL = "delegate_subtasks";
22
+ export const EXPERT_TOOL = "ask_expert";
23
+ /** Consults one step may make: advice on the hard part, not a second worker. */
24
+ export const MAX_CONSULTS = 2;
25
+ /** Longest an expert may take to answer. */
26
+ /** Marks an expert's run, so its host gives it an adviser's note rather than a child's. */
27
+ export const EXPERT_ENV = "BOT_LOBBY_EXPERT";
28
+
29
+ /** The worker's env without its desk and board: an expert only reads and answers. */
30
+ function withoutTeamEnv(env: Record<string, string> | undefined): Record<string, string> {
31
+ const { [DESK_ENV.address]: _address, [DESK_ENV.worker]: _worker, [BOARD_ENV]: _board, BOT_LOBBY_BOARD_FROM: _from, ...rest } = env ?? {};
32
+ return rest;
33
+ }
34
+ const EXPERT_TOOLS = ["read", "grep", "find", "ls"];
20
35
  export const AGENT_ENV = "BOT_LOBBY_AGENT_POLICY";
21
36
  export const DELEGATION_ENV = "BOT_LOBBY_DELEGATION";
22
37
 
@@ -27,6 +42,12 @@ export function canDelegate(agent: { role: Role; domain: Domain; depth?: number
27
42
  return agent.role === "reviewer" && agent.domain === "qa";
28
43
  }
29
44
 
45
+ /** A task's DESIGN/DEV workers (not their children, nor quick fixes) may consult a stronger model, when the setting is on. */
46
+ export function canConsult(agent: { runId: string; taskId: string; role: Role; domain: Domain; depth?: number }, enabled: boolean): boolean {
47
+ // A lobby job (a quick fix) runs as its own task: it is one agent on a small change, not a task's worker.
48
+ return enabled && agent.taskId !== agent.runId && (agent.depth ?? 1) === 1 && agent.role === "worker" && (agent.domain === "designer" || agent.domain === "backend");
49
+ }
50
+
30
51
  /** One shared child pool per task; parents waiting on children never hold its slots. */
31
52
  const pools = new Map<string, { active: number; users: number; waiters: Array<() => void> }>();
32
53
 
@@ -63,7 +84,7 @@ export interface AgentPolicy {
63
84
  depth: number;
64
85
  }
65
86
 
66
- /** The host owns quotas, deadlines, file identities and child lifetimes, outside model context. */
87
+ /** The host owns quotas, file identities and child lifetimes, outside model context. */
67
88
  export async function delegationHost(options: PiRunOptions, run: ProcessRunner) {
68
89
  const config = loadConfig();
69
90
  const id = options.agent?.runId ?? randomUUID();
@@ -72,26 +93,29 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
72
93
  const agent = options.agent ?? { runId: id, taskId: id, domain: "backend" as const, role: editable ? "worker" as const : "reviewer" as const };
73
94
  const depth = agent.depth ?? 1;
74
95
  const quota = options.delegationQuota ?? { used: 0 };
75
- const tools = [...new Set([...baseTools, "codemode", "tool_search", ...(canDelegate(agent, quota.used) ? [DELEGATE_TOOL] : [])])];
96
+ const consults = canConsult(agent, config.workflow.consultExpert);
97
+ const tools = [...new Set([...baseTools, "codemode", "tool_search", ...(canDelegate(agent, quota.used) ? [DELEGATE_TOOL] : []), ...(consults ? [EXPERT_TOOL] : [])])];
76
98
  const mcpTools = options.mcpTools ?? agentProfile(config, agent.domain, agent.role).mcpTools ?? [];
77
99
  const policy: AgentPolicy = { tools, mcpTools, readOnly: !editable, depth };
78
- if (depth > 1) return { tools, mcpTools, env: { ...options.env, [AGENT_ENV]: JSON.stringify(policy), [DELEGATION_ENV]: "" }, note: "You are a child agent. Finish only your assigned brief and report evidence to your parent; you cannot delegate further.", close: async () => {}, onStart: options.onStart, children: [] as AgentRun[] };
100
+ if (depth > 1) return { tools, mcpTools, env: { ...options.env, [AGENT_ENV]: JSON.stringify(policy), [DELEGATION_ENV]: "" }, note: options.env?.[EXPERT_ENV] ? "You are advising a worker: read what you need, answer its question, and change nothing." : "You are a child agent. Finish only your assigned brief and report evidence to your parent; you cannot delegate further.", close: async () => {}, onStart: options.onStart, children: [] as AgentRun[] };
79
101
 
80
102
  const address = options.env?.[DESK_ENV.address];
81
103
  let desk = address ? deskSessionAt(address) : undefined;
82
104
  if (address && !desk) throw new Error("The inherited file desk is unavailable.");
83
- const ownedDesk = editable && !desk ? new DeskSession({ cwd: options.cwd }) : undefined;
84
- if (ownedDesk) { await ownedDesk.open(); desk = ownedDesk; }
105
+ const board = options.env?.[BOARD_ENV];
106
+ // A worker with a checkout of its own (no claims on the batch desk) still needs claims among its children.
107
+ const ownedDesk = editable && (!desk || !desk.usesClaims) ? new DeskSession({ cwd: options.cwd, ...(board ? { boardPath: board } : {}) }) : undefined;
108
+ if (ownedDesk) await ownedDesk.open();
109
+ // Children claim files among themselves; the parent keeps the batch desk (and its peers) when it has one.
110
+ const childDesk = ownedDesk ?? desk;
111
+ desk ??= ownedDesk;
85
112
  const controller = new AbortController();
86
113
  const lifetime = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
87
- let timeController = new AbortController();
88
- const childSignal = () => AbortSignal.any([lifetime, timeController.signal]);
114
+ const childSignal = () => lifetime;
89
115
  const key = `${options.cwd}:${agent.taskId}`;
90
116
  const pool = pools.get(key) ?? { active: 0, users: 0, waiters: [] };
91
117
  pool.users += 1;
92
118
  pools.set(key, pool);
93
- let deadlineAt = Date.now() + (options.time?.upAtMs ?? options.timeoutMs);
94
- let deadline = setTimeout(() => timeController.abort(), Math.max(1, deadlineAt - Date.now()));
95
119
  const children = new Map<string, AgentRun>();
96
120
  const pending = new Set<Promise<AgentRun>>();
97
121
  const parentDeskId = options.env?.[DESK_ENV.worker] ?? id;
@@ -109,18 +133,16 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
109
133
 
110
134
  const childRun = async (brief: string, signal: AbortSignal): Promise<AgentRun> => {
111
135
  const childId = randomUUID();
112
- const base: AgentRun = { runId: childId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: agent.role, instruction: brief, stepInstruction: options.task, status: "running", endsAt: deadlineAt, output: "", attempts: 1, startedAt: new Date().toISOString(), ...(options.thinking ? { thinking: options.thinking } : {}) };
136
+ const base: AgentRun = { runId: childId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: agent.role, instruction: brief, stepInstruction: options.task, status: "running", output: "", attempts: 1, startedAt: new Date().toISOString(), ...(options.thinking ? { thinking: options.thinking } : {}) };
113
137
  const edits = new EditLog(options.cwd);
114
- const live = createLiveRun(base, { onUpdate: update, timeoutMs: options.timeoutMs }, edits);
138
+ const live = createLiveRun(base, { onUpdate: update }, edits);
115
139
  update(base);
116
140
  try {
117
141
  const configured = options.effortProfile ?? { model: options.model, thinking: options.thinking ?? "medium" };
118
142
  const effort = options.effort ?? effortFor();
119
143
  const route = await effort.childRoute?.(brief, configured, { context: options.task, signal }).catch(() => undefined);
120
144
  const attempt = async (model: string | undefined, thinking: string) => {
121
- const left = deadlineAt - Date.now();
122
145
  signal.throwIfAborted();
123
- if (left <= 0) throw new Error("The parent's time is up.");
124
146
  const routed = route && model === route.model && thinking === route.thinking;
125
147
  live.annotate({ model, thinking, route: routed ? routeLabel(route) : undefined, routedFrom: routed ? profileLabel(route.from) : undefined, note: routed ? routeLabel(route) : undefined, noteKind: "info" });
126
148
  return runPiAgent({
@@ -129,22 +151,19 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
129
151
  task: brief,
130
152
  model,
131
153
  thinking,
132
- tools: [...new Set([...tools.filter((tool) => tool !== DELEGATE_TOOL), ...(desk ? DESK_TOOLS : [])])],
154
+ tools: [...new Set([...tools.filter((tool) => tool !== DELEGATE_TOOL && tool !== EXPERT_TOOL), ...(childDesk ? childDesk.tools().filter((tool) => !PEER_TOOLS.includes(tool)) : [])])],
133
155
  mcpTools,
134
- timeoutMs: left,
135
156
  signal,
136
- time: undefined,
137
- wrapUpAtMs: Math.floor(left * 0.85),
138
157
  delegationQuota: undefined,
139
158
  onChildRun: undefined,
140
159
  onEvent: live.onEvent,
141
- env: { ...options.env, ...(desk ? desk.env(childId) : {}) },
142
- onStart: (handle) => desk?.attach(childId, { runId: childId, steer: handle.steer, annotate: live.annotate }),
160
+ env: { ...options.env, ...(childDesk ? childDesk.env(childId) : {}) },
161
+ onStart: (handle) => childDesk?.attach(childId, { runId: childId, steer: handle.steer, annotate: live.annotate }),
143
162
  }, run);
144
163
  };
145
164
  let result = await attempt(route?.model ?? configured.model, route?.thinking ?? configured.thinking);
146
165
  let retried = false;
147
- if (route && (route.level === "simple" || route.level === "trivial") && fellShort(result) && !signal.aborted && deadlineAt - Date.now() >= 60_000) {
166
+ if (route && (route.level === "simple" || route.level === "trivial") && fellShort(result) && !signal.aborted) {
148
167
  retried = true;
149
168
  result = await attempt(configured.model, configured.thinking);
150
169
  }
@@ -152,24 +171,88 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
152
171
  update(final);
153
172
  return final;
154
173
  } catch (error) {
155
- const final: AgentRun = { ...live.current(), edited: edits.list(), status: Date.now() >= deadlineAt ? "timeout" : signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
174
+ const final: AgentRun = { ...live.current(), edited: edits.list(), status: signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
156
175
  update(final);
157
176
  return final;
158
177
  } finally {
159
- desk?.release(childId, () => "Child finished; re-read the file and check its report before continuing.");
178
+ childDesk?.release(childId, () => "Child finished; re-read the file and check its report before continuing.");
160
179
  }
161
180
  };
162
181
 
182
+ // Kept on the quota, which a step's retries share, so a retry does not grant fresh consults.
183
+ quota.consults ??= 0;
184
+ const consultations = new Map<string, AbortController>();
185
+ const consult = async (question: string, signal: AbortSignal): Promise<{ ok: boolean; text: string }> => {
186
+ // The oracle resolves the expert (its session model when settings name none); settings alone, then the worker's own, without it.
187
+ const settled = options.expert ?? expertProfile(config);
188
+ const expert = { model: settled.model ?? options.model, thinking: settled.thinking };
189
+ const expertId = randomUUID();
190
+ const base: AgentRun = { runId: expertId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: "reviewer", instruction: question, stepInstruction: options.task, status: "running", output: "", attempts: 1, startedAt: new Date().toISOString(), thinking: expert.thinking, ...(expert.model ? { model: expert.model } : {}), note: "expert consult", noteKind: "info" };
191
+ const live = createLiveRun(base, { onUpdate: update }, new EditLog(options.cwd));
192
+ update(base);
193
+ let final: AgentRun;
194
+ try {
195
+ const result = await runPiAgent({
196
+ cwd: options.cwd,
197
+ agent: { ...agent, runId: expertId, role: "reviewer", depth: 2 },
198
+ task: [`A worker asks: ${question}`, `The worker's brief, for context:\n${options.task.slice(0, 8000)}`].join("\n\n"),
199
+ systemPrompt: loadPrompt("expert.md"),
200
+ tools: EXPERT_TOOLS,
201
+ mcpTools: [],
202
+ ...(expert.model ? { model: expert.model } : {}),
203
+ thinking: expert.thinking,
204
+ ...(options.stallTimeoutMs !== undefined ? { stallTimeoutMs: options.stallTimeoutMs } : {}),
205
+ ...(options.toolStallTimeoutMs !== undefined ? { toolStallTimeoutMs: options.toolStallTimeoutMs } : {}),
206
+ signal,
207
+ onEvent: live.onEvent,
208
+ // Its own env: the expert is not one of the worker's children, nor on its desk or board.
209
+ env: { ...withoutTeamEnv(options.env), [EXPERT_ENV]: "1" },
210
+ }, run);
211
+ final = { ...live.current(), ...result, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
212
+ } catch (error) {
213
+ final = { ...live.current(), status: signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
214
+ }
215
+ update(final);
216
+ if (final.status === "success" && final.output.trim()) return { ok: true, text: final.output.trim().slice(0, 8000) };
217
+ // An expert that never answered does not use up a consult.
218
+ quota.consults = Math.max(0, (quota.consults ?? 1) - 1);
219
+ return { ok: false, text: `The expert did not answer (${final.status}${final.error ? `: ${final.error}` : ""}); carry on with your best reading.` };
220
+ };
221
+
163
222
  const token = randomUUID();
164
223
  const server = await startDeskServer(async (request) => {
165
224
  if (request.worker !== token) return { ok: false, text: "Unknown parent agent." };
166
225
  if (request.op === "cancel_delegate") { controller.abort(); return { ok: true, text: "Children cancelled." }; }
226
+ if (request.op === "cancel_consult") {
227
+ consultations.get(request.consultId ?? "")?.abort();
228
+ return { ok: true, text: "Consult cancelled." };
229
+ }
230
+ if (request.op === "consult") {
231
+ if (!consults) return { ok: false, text: "Consulting is off for this agent." };
232
+ if ((quota.consults ?? 0) >= MAX_CONSULTS) return { ok: false, text: `You have used your ${MAX_CONSULTS} consults for this step; decide with what you have, or report the open question.` };
233
+ const question = (request.text ?? "").trim();
234
+ if (!question) return { ok: false, text: "ask_expert needs a question." };
235
+ quota.consults = (quota.consults ?? 0) + 1;
236
+ // Its own controller, so the worker abandoning its tool call stops this expert and nothing else.
237
+ const own = new AbortController();
238
+ const parent = childSignal();
239
+ const stop = () => own.abort();
240
+ parent.addEventListener("abort", stop, { once: true });
241
+ const consultId = request.consultId ?? randomUUID();
242
+ consultations.set(consultId, own);
243
+ try {
244
+ return await consult(question.slice(0, 6000), own.signal);
245
+ } finally {
246
+ consultations.delete(consultId);
247
+ parent.removeEventListener("abort", stop);
248
+ }
249
+ }
167
250
  if (request.op !== "delegate") return { ok: false, text: "Unknown delegation operation." };
168
251
  const signal = childSignal();
169
252
  signal.throwIfAborted();
170
253
  const briefs = reserveChildren(quota, request.briefs);
171
254
  // A coordinating parent must release claims before waiting for children that may need them.
172
- desk?.releaseFiles(parentDeskId, () => "Delegating this work; re-read before editing.");
255
+ for (const held of new Set([desk, childDesk])) held?.releaseFiles(parentDeskId, () => "Delegating this work; re-read before editing.");
173
256
  const runs = briefs.map((brief) => {
174
257
  const promise = inPool(key, Math.max(1, config.workflow.maxParallelWorkers), signal, () => childRun(brief, signal));
175
258
  pending.add(promise);
@@ -179,7 +262,6 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
179
262
  const results = await Promise.all(runs);
180
263
  return { ok: true, text: JSON.stringify(results.map((child) => ({ runId: child.runId, status: child.status, report: child.output.slice(0, 12_000), reportPath: join(dataRoot(detectProjectRoot(options.cwd, CONFIG_DIR_NAME), CONFIG_DIR_NAME), "delegations", `${child.runId}.json`), error: child.error, usage: child.usage }))) };
181
264
  }, () => controller.abort()).catch(async (error) => {
182
- clearTimeout(deadline);
183
265
  if (ownedDesk) await ownedDesk.close();
184
266
  pool.users -= 1;
185
267
  if (pool.users === 0) pools.delete(key);
@@ -188,30 +270,22 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
188
270
 
189
271
  let closed = false;
190
272
  return {
191
- tools: desk ? [...new Set([...tools, ...DESK_TOOLS])] : tools,
273
+ tools: desk ? [...new Set([...tools, ...desk.tools()])] : tools,
192
274
  mcpTools,
193
- env: { ...options.env, ...(desk ? desk.env(parentDeskId) : {}), [AGENT_ENV]: JSON.stringify({ ...policy, tools: desk ? [...new Set([...tools, ...DESK_TOOLS])] : tools }), [DELEGATION_ENV]: JSON.stringify({ address: server.address, token, timeoutMs: options.timeoutMs }) },
194
- note: `${canDelegate(agent, quota.used) ? `Work directly when you can finish the brief yourself. Use ${DELEGATE_TOOL} only for useful, separable work with disjoint files, at most five children total; children share your remaining time and cannot delegate further. Integrate and verify their work yourself. ` : ""}Use codemode for concise parallel calls, inspect failures, and return only useful evidence.${desk ? " Before edit/write, claim_file; handover_file when finished. Release files before delegating or waiting." : ""}`,
275
+ env: { ...options.env, ...(desk ? desk.env(parentDeskId) : {}), [AGENT_ENV]: JSON.stringify({ ...policy, tools: desk ? [...new Set([...tools, ...desk.tools()])] : tools }), [DELEGATION_ENV]: JSON.stringify({ address: server.address, token }) },
276
+ note: `${consults ? `When you are stuck on a hard step after a real attempt, or about to make a choice you are unsure of (a tricky algorithm, a subtle bug, an unfamiliar API), ${EXPERT_TOOL} asks a stronger model for advice, at most ${MAX_CONSULTS} times; it reads the repository and answers, you still do the work. ` : ""}${canDelegate(agent, quota.used) ? `Work directly when you can finish the brief yourself. Use ${DELEGATE_TOOL} only for useful, separable work with disjoint files, at most five children total; children cannot delegate further. Integrate and verify their work yourself. ` : ""}Use codemode for concise parallel calls, inspect failures, and return only useful evidence.${desk?.usesClaims ? " Before edit/write, claim_file; handover_file when finished. Release files before delegating or waiting." : ""}`,
195
277
  onStart: (handle: Parameters<NonNullable<PiRunOptions["onStart"]>>[0]) => {
196
278
  desk?.attach(parentDeskId, { runId: id, steer: handle.steer, annotate: () => {} });
197
279
  options.onStart?.(handle);
198
280
  },
199
281
  get children() { return [...children.values()]; },
200
- pause() { clearTimeout(deadline); },
201
- extend(ms: number, reset = false) {
202
- deadlineAt = reset ? Date.now() + ms : deadlineAt + ms;
203
- clearTimeout(deadline);
204
- if (timeController.signal.aborted) timeController = new AbortController();
205
- deadline = setTimeout(() => timeController.abort(), Math.max(1, deadlineAt - Date.now()));
206
- },
207
282
  async close() {
208
283
  if (closed) return;
209
284
  closed = true;
210
285
  controller.abort();
211
- clearTimeout(deadline);
212
286
  await Promise.allSettled([...pending]);
213
287
  await server.close();
214
- if (ownedDesk) { desk?.release(parentDeskId, () => "Parent finished; re-read before editing."); await ownedDesk.close(); }
288
+ if (ownedDesk) { ownedDesk.release(parentDeskId, () => "Parent finished; re-read before editing."); await ownedDesk.close(); }
215
289
  pool.users -= 1;
216
290
  if (pool.users === 0) pools.delete(key);
217
291
  },
@@ -3,7 +3,7 @@ import type { AgentRun } from "../schemas/findings.ts";
3
3
  import type { PiStreamEvent } from "./pi-runner.ts";
4
4
  import { EditLog } from "../state/changes.ts";
5
5
  import { activityDetail, activityWord, describeToolCall } from "../pi/activity.ts";
6
- import { shortDuration, truncate } from "../text.ts";
6
+ import { truncate } from "../text.ts";
7
7
 
8
8
  const THROTTLE_MS = 2000;
9
9
 
@@ -12,7 +12,7 @@ const THROTTLE_MS = 2000;
12
12
  * `onUpdate`: activity changes, retries and notes immediately, counters and
13
13
  * heartbeats at most every couple of seconds.
14
14
  */
15
- export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpdate" | "timeoutMs" | "time">, edits: EditLog) {
15
+ export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpdate">, edits: EditLog) {
16
16
  let state: AgentRun = base;
17
17
  let lastEmit = 0;
18
18
  const emit = (patch: Partial<AgentRun>, force: boolean) => {
@@ -55,22 +55,11 @@ export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpd
55
55
  case "compaction_end":
56
56
  emit({ note: event.error, noteKind: event.error ? "warning" : undefined }, true);
57
57
  return;
58
- case "wrap_up":
59
- emit({ note: `asked to wrap up (${shortDuration(request.timeoutMs)} limit)`, noteKind: "warning", wrappedUp: true }, true);
60
- return;
61
- case "time_up":
62
- emit({ note: request.time?.onTimeUp ? "out of time: reporting where it left off" : "out of time: reporting", noteKind: "warning" }, true);
63
- return;
64
- case "extended":
65
- emit({ endsAt: request.time?.endsAt, extendedMs: (state.extendedMs ?? 0) + event.ms }, true);
66
- return;
67
58
  case "asking":
68
59
  emit({ note: `waiting on you: ${event.questions} question${event.questions === 1 ? "" : "s"}`, noteKind: "info" }, true);
69
60
  return;
70
61
  case "answered":
71
- // Its time waited with it.
72
- if (request.time) request.time.endsAt += event.waitedMs;
73
- emit({ note: "you answered; carrying on", noteKind: "info", ...(request.time ? { endsAt: request.time.endsAt } : {}) }, true);
62
+ emit({ note: "you answered; carrying on", noteKind: "info" }, true);
74
63
  return;
75
64
  case "usage": {
76
65
  const usage = state.usage ?? { input: 0, output: 0, cost: 0, turns: 0 };