macroclaw 0.26.0 → 0.28.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.
@@ -3,13 +3,13 @@ import {
3
3
  Claude,
4
4
  QueryParseError,
5
5
  QueryProcessError,
6
- type QueryResult,
7
6
  QueryValidationError,
8
7
  type RunningQuery
9
8
  } from "./claude";
10
9
  import { writeHistoryPrompt, writeHistoryResult } from "./history";
11
10
  import { createLogger } from "./logger";
12
- import { CRON_TIMEOUT, MAIN_TIMEOUT, SYSTEM_PROMPT } from "./prompts";
11
+ import { generateName } from "./naming";
12
+ import { buildEvent, type EventInput, SYSTEM_PROMPT } from "./prompts";
13
13
  import { Queue } from "./queue";
14
14
  import { loadSessions, saveSessions } from "./sessions";
15
15
 
@@ -17,6 +17,10 @@ type ButtonSpec = string | { text: string; data: string };
17
17
 
18
18
  const log = createLogger("orchestrator");
19
19
 
20
+ // --- Constants ---
21
+
22
+ const WAIT_THRESHOLD = 60_000;
23
+
20
24
  // --- Response schema ---
21
25
 
22
26
  const backgroundAgentSchema = z.object({
@@ -55,21 +59,21 @@ export interface OrchestratorResponse {
55
59
 
56
60
  type OrchestratorRequest =
57
61
  | { type: "user"; message: string; files?: string[] }
58
- | { type: "cron"; name: string; prompt: string; model?: string }
59
- | { type: "background-agent-result"; name: string; response: AgentOutput; sessionId?: string }
62
+ | { type: "background-agent-result"; name: string; response: AgentOutput }
60
63
  | { type: "button"; label: string };
61
64
 
62
65
  function escapeHtml(text: string): string {
63
66
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
64
67
  }
65
68
 
66
- // --- Background tracking ---
69
+ // --- Session tracking ---
67
70
 
68
- interface BackgroundInfo {
71
+ interface SessionInfo {
69
72
  name: string;
70
73
  prompt: string;
71
74
  model?: string;
72
75
  query: RunningQuery<AgentOutput>;
76
+ lastMessageAt: Date;
73
77
  }
74
78
 
75
79
  export interface OrchestratorConfig {
@@ -78,19 +82,23 @@ export interface OrchestratorConfig {
78
82
  settingsDir?: string;
79
83
  onResponse: (response: OrchestratorResponse) => Promise<void>;
80
84
  claude?: Claude;
85
+ /** How long to wait for a running main session before demoting it (ms). Default: 60000 */
86
+ waitThreshold?: number;
81
87
  }
82
88
 
83
89
  export class Orchestrator {
84
90
  #config: Omit<OrchestratorConfig , 'claude'>;
85
91
  #claude: Claude;
92
+ #waitThreshold: number;
86
93
 
87
94
  #mainSessionId: string | undefined;
88
- #backgroundAgents = new Map<string, BackgroundInfo>();
95
+ #runningSessions = new Map<string, SessionInfo>();
89
96
  #queue: Queue<OrchestratorRequest>;
90
97
 
91
98
  constructor(config: OrchestratorConfig) {
92
99
  this.#config = config;
93
100
  this.#claude = config.claude ?? new Claude({ workspace: config.workspace, systemPrompt: SYSTEM_PROMPT });
101
+ this.#waitThreshold = config.waitThreshold ?? WAIT_THRESHOLD;
94
102
  this.#queue = new Queue<OrchestratorRequest>();
95
103
  this.#queue.setHandler((request) => this.#handleRequest(request));
96
104
 
@@ -107,48 +115,60 @@ export class Orchestrator {
107
115
  this.#queue.push({ type: "button", label });
108
116
  }
109
117
 
110
- handleCron(name: string, prompt: string, model?: string): void {
111
- this.#queue.push({ type: "cron", name, prompt, model });
118
+ handleCron(name: string, prompt: string, model?: string, missed?: { missedBy: string; scheduledAt: string }): void {
119
+ const cronName = `cron-${name}`;
120
+ const formatted = buildEvent({
121
+ name: cronName,
122
+ type: "schedule-trigger",
123
+ session: "background",
124
+ schedule: { name, missedBy: missed?.missedBy, scheduledAt: missed?.scheduledAt },
125
+ text: prompt,
126
+ });
127
+ this.#spawnBackgroundRaw(cronName, prompt, formatted, model ?? this.#config.model);
112
128
  }
113
129
 
114
130
  handleBackgroundCommand(prompt: string): void {
115
- const name = prompt.slice(0, 30).replace(/\s+/g, "-");
131
+ const name = generateName(prompt);
116
132
  this.#spawnBackground(name, prompt, this.#config.model);
117
133
  this.#callOnResponse({ message: `Background agent "${escapeHtml(name)}" started.` });
118
134
  }
119
135
 
120
- handleBackgroundList(): void {
121
- const agents = [...this.#backgroundAgents.values()];
122
- if (agents.length === 0) {
123
- this.#callOnResponse({ message: "No background agents running." });
136
+ handleSessions(): void {
137
+ const sessions = [...this.#runningSessions.entries()];
138
+ if (sessions.length === 0) {
139
+ this.#callOnResponse({ message: "No running sessions." });
124
140
  return;
125
141
  }
126
- const lines = agents.map((a) => {
127
- const elapsed = Math.round((Date.now() - a.query.startedAt.getTime()) / 1000);
128
- return `- ${escapeHtml(a.name)} (${elapsed}s)`;
142
+ const lines = sessions.map(([sid, s]) => {
143
+ const elapsed = Math.round((Date.now() - s.query.startedAt.getTime()) / 1000);
144
+ const isMain = sid === this.#mainSessionId;
145
+ return isMain
146
+ ? `▶ ${escapeHtml(s.name)} (${elapsed}s) [main]`
147
+ : `- ${escapeHtml(s.name)} (${elapsed}s)`;
129
148
  });
130
- const buttons: ButtonSpec[] = agents.map((a) => {
131
- const elapsed = Math.round((Date.now() - a.query.startedAt.getTime()) / 1000);
132
- const text = `${a.name} (${elapsed}s)`.slice(0, 27);
133
- return { text, data: `detail:${a.query.sessionId}` };
149
+ const buttons: ButtonSpec[] = sessions.map(([sid, s]) => {
150
+ const elapsed = Math.round((Date.now() - s.query.startedAt.getTime()) / 1000);
151
+ const text = `${s.name} (${elapsed}s)`.slice(0, 27);
152
+ return { text, data: `detail:${sid}` };
134
153
  });
135
- buttons.push({text: "Dismiss", data: "_dismiss"});
154
+ buttons.push({ text: "Dismiss", data: "_dismiss" });
136
155
  this.#callOnResponse({ message: lines.join("\n"), buttons });
137
156
  }
138
157
 
139
158
  handleDetail(sessionId: string): void {
140
- const agent = this.#backgroundAgents.get(sessionId);
141
- if (!agent) {
142
- this.#callOnResponse({ message: "Agent not found or already finished." });
159
+ const session = this.#runningSessions.get(sessionId);
160
+ if (!session) {
161
+ this.#callOnResponse({ message: "Session not found or already finished." });
143
162
  return;
144
163
  }
145
164
 
146
- const elapsed = Math.round((Date.now() - agent.query.startedAt.getTime()) / 1000);
147
- const truncatedPrompt = agent.prompt.length > 300 ? `${agent.prompt.slice(0, 300)}…` : agent.prompt;
165
+ const elapsed = Math.round((Date.now() - session.query.startedAt.getTime()) / 1000);
166
+ const truncatedPrompt = session.prompt.length > 300 ? `${session.prompt.slice(0, 300)}…` : session.prompt;
167
+ const isMain = sessionId === this.#mainSessionId;
148
168
  const lines = [
149
- `<b>${escapeHtml(agent.name)}</b>`,
169
+ `<b>${escapeHtml(session.name)}</b>${isMain ? " [main]" : ""}`,
150
170
  `Prompt: ${escapeHtml(truncatedPrompt)}`,
151
- `Model: ${agent.model ?? "default"}`,
171
+ `Model: ${session.model ?? "default"}`,
152
172
  `Elapsed: ${elapsed}s`,
153
173
  "Status: running",
154
174
  ];
@@ -161,82 +181,98 @@ export class Orchestrator {
161
181
  }
162
182
 
163
183
  async handlePeek(sessionId: string): Promise<void> {
164
- const agent = this.#backgroundAgents.get(sessionId);
165
- if (!agent) {
166
- this.#callOnResponse({ message: "Agent not found or already finished." });
184
+ const session = this.#runningSessions.get(sessionId);
185
+ if (!session) {
186
+ this.#callOnResponse({ message: "Session not found or already finished." });
167
187
  return;
168
188
  }
169
189
 
170
- this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(agent.name)}</b>...` });
190
+ this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(session.name)}</b>...` });
171
191
 
172
192
  try {
173
- const startedAt = agent.query.startedAt.toISOString();
193
+ const prompt = buildEvent({
194
+ name: `peek-${session.name}`,
195
+ type: "peek",
196
+ session: "background",
197
+ targetEvent: session.name,
198
+ instructions: `Only consider progress since the "${session.name}" event. Brief status update: done, in progress, remaining. 2-3 sentences max, plain text.`,
199
+ });
174
200
  const query = this.#claude.forkSession(
175
201
  sessionId,
176
- `This session started at ${startedAt}. Only consider events after that time. Give a brief status update: what has been done so far, what's currently happening, and what's remaining. 2-3 sentences max.`,
202
+ prompt,
177
203
  textResultType,
178
204
  { model: "haiku" },
179
205
  );
180
206
  const { value } = await query.result;
181
- this.#callOnResponse({ message: `<b>[${escapeHtml(agent.name)}]</b> ${value || "[No output]"}` });
207
+ this.#callOnResponse({ message: `<b>[${escapeHtml(session.name)}]</b> ${value || "[No output]"}` });
182
208
  } catch (err) {
183
- this.#callOnResponse({ message: `Couldn't peek at ${escapeHtml(agent.name)}: ${err}` });
209
+ this.#callOnResponse({ message: `Couldn't peek at ${escapeHtml(session.name)}: ${err}` });
184
210
  }
185
211
  }
186
212
 
187
213
  async handleKill(sessionId: string): Promise<void> {
188
- const agent = this.#backgroundAgents.get(sessionId);
189
- if (!agent) {
190
- this.#callOnResponse({ message: "Agent not found or already finished." });
214
+ const session = this.#runningSessions.get(sessionId);
215
+ if (!session) {
216
+ this.#callOnResponse({ message: "Session not found or already finished." });
191
217
  return;
192
218
  }
193
219
 
194
- this.#backgroundAgents.delete(sessionId);
220
+ this.#runningSessions.delete(sessionId);
195
221
 
196
222
  try {
197
- await agent.query.kill();
223
+ await session.query.kill();
198
224
  } catch (err) {
199
- log.error({ err, name: agent.name }, "Kill failed");
225
+ log.error({ err, name: session.name }, "Kill failed");
200
226
  }
201
227
 
202
- this.#callOnResponse({ message: `Killed <b>${escapeHtml(agent.name)}</b>.` });
228
+ this.#callOnResponse({ message: `Killed <b>${escapeHtml(session.name)}</b>.` });
203
229
  }
204
230
 
205
- handleSessionCommand(): void {
206
- this.#callOnResponse({ message: `Session: <code>${this.#mainSessionId ?? "none"}</code>` });
207
- }
208
231
 
209
232
  // --- Internal queue handler ---
210
233
 
211
234
  async #handleRequest(request: OrchestratorRequest): Promise<void> {
212
235
  log.debug({ type: request.type }, "Incoming request");
213
236
 
214
- // Background result with matching session ID: deliver directly
215
- if (request.type === "background-agent-result" && request.sessionId === this.#mainSessionId) {
216
- log.debug({ name: request.name }, "Background result on current session, applying directly");
217
- await this.#deliverResponse(request.response);
218
- return;
237
+ const mainInfo = this.#mainSessionId ? this.#runningSessions.get(this.#mainSessionId) : undefined;
238
+ let movedToBackground: string | undefined;
239
+
240
+ if (mainInfo) {
241
+ const elapsed = Date.now() - mainInfo.lastMessageAt.getTime();
242
+ if (elapsed >= this.#waitThreshold) {
243
+ // Main has been running too long — move to background immediately
244
+ log.info({ name: mainInfo.name, sessionId: mainInfo.query.sessionId }, "Moving main session to background (exceeded threshold)");
245
+ movedToBackground = mainInfo.prompt;
246
+ } else {
247
+ // Main started recently — wait for it to finish or threshold
248
+ const remaining = this.#waitThreshold - elapsed;
249
+ const finished = await Promise.race([
250
+ mainInfo.query.result.then(() => true as const, () => true as const),
251
+ new Promise<false>((r) => setTimeout(() => r(false), remaining)),
252
+ ]);
253
+
254
+ if (!finished) {
255
+ log.info({ name: mainInfo.name, sessionId: mainInfo.query.sessionId }, "Moving main session to background (wait timed out)");
256
+ movedToBackground = mainInfo.prompt;
257
+ }
258
+ // If finished: completion handler already delivered the result and removed from map.
259
+ }
219
260
  }
220
261
 
221
262
  await writeHistoryPrompt(request);
222
263
 
223
- const result = await this.#queryWithRetry(request);
224
- if (!result) return;
225
-
226
- // Update session ID (important after fork or new session)
227
- if (result.sessionId !== this.#mainSessionId) {
228
- log.info({ oldSessionId: this.#mainSessionId, newSessionId: result.sessionId }, "Session updated");
229
- this.#mainSessionId = result.sessionId;
230
- saveSessions({ mainSessionId: this.#mainSessionId }, this.#config.settingsDir);
231
- }
264
+ const label = Orchestrator.#requestLabel(request);
265
+ const name = generateName(label);
266
+ const backgroundedName = movedToBackground ? mainInfo?.name : undefined;
267
+ const prompt = this.#formatPrompt(request, name, backgroundedName);
232
268
 
233
- await writeHistoryResult(result.value);
234
- await this.#deliverResponse(result.value);
269
+ this.#startMainQuery(name, prompt, this.#config.model);
235
270
  }
236
271
 
237
272
  // --- Response delivery ---
238
273
 
239
274
  async #deliverResponse(response: AgentOutput): Promise<void> {
275
+ await writeHistoryResult(response);
240
276
  if (response.action === "send") {
241
277
  this.#callOnResponse({
242
278
  message: response.message || "[No output]",
@@ -262,92 +298,111 @@ export class Orchestrator {
262
298
  });
263
299
  }
264
300
 
265
- // --- Claude calls ---
266
-
267
- async #queryWithRetry(request: OrchestratorRequest): Promise<QueryResult<AgentOutput> | null> {
268
- const timeout = request.type === "cron" ? CRON_TIMEOUT : MAIN_TIMEOUT;
269
- const query = this.#query(request);
301
+ // --- Main session query ---
270
302
 
271
- try {
272
- const result = await this.#awaitOrBackground(query, request, timeout);
273
- if (!result) return null;
274
- return result;
275
- } catch (err) {
276
- // Resume failed — retry with a fresh session
277
- if (err instanceof QueryProcessError && this.#mainSessionId) {
278
- log.info("Resume failed, retrying with new session");
279
- this.#mainSessionId = undefined;
280
- const retryQuery = this.#query(request);
281
-
282
- try {
283
- const retryResult = await this.#awaitOrBackground(retryQuery, request, timeout);
284
- if (!retryResult) return null;
285
- return retryResult;
286
- } catch (retryErr) {
287
- return { value: this.#errorResponse(retryErr), sessionId: retryQuery.sessionId };
288
- }
289
- }
303
+ #startMainQuery(name: string, prompt: string, model: string | undefined): void {
304
+ const opts = { model };
305
+ let query: RunningQuery<AgentOutput>;
290
306
 
291
- return { value: this.#errorResponse(err), sessionId: this.#mainSessionId ?? "" };
307
+ if (this.#mainSessionId && this.#runningSessions.has(this.#mainSessionId)) {
308
+ query = this.#claude.forkSession(this.#mainSessionId, prompt, responseResultType, opts);
309
+ } else if (this.#mainSessionId) {
310
+ query = this.#claude.resumeSession(this.#mainSessionId, prompt, responseResultType, opts);
311
+ } else {
312
+ query = this.#claude.newSession(prompt, responseResultType, opts);
292
313
  }
293
- }
294
314
 
295
- #query(request: OrchestratorRequest) {
296
- const prompt = this.#formatPrompt(request);
297
- const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
298
- const opts = { model };
315
+ const sid = query.sessionId;
316
+ this.#runningSessions.set(sid, { name, prompt, model, query, lastMessageAt: new Date() });
299
317
 
300
- // Fork if a background agent is running on the main session
301
- if (this.#mainSessionId && this.#backgroundAgents.has(this.#mainSessionId)) {
302
- return this.#claude.forkSession(this.#mainSessionId, prompt, responseResultType, opts);
318
+ if (sid !== this.#mainSessionId) {
319
+ log.info({ oldSessionId: this.#mainSessionId, newSessionId: sid }, "Session updated");
320
+ this.#mainSessionId = sid;
321
+ saveSessions({ mainSessionId: sid }, this.#config.settingsDir);
303
322
  }
304
323
 
305
- // Resume existing session
306
- if (this.#mainSessionId) {
307
- return this.#claude.resumeSession(this.#mainSessionId, prompt, responseResultType, opts);
308
- }
324
+ log.debug({ name, sessionId: sid }, "Main query started");
309
325
 
310
- // Start fresh
311
- return this.#claude.newSession(prompt, responseResultType, opts);
326
+ query.result.then(
327
+ async ({ value: response }) => {
328
+ if (!this.#runningSessions.has(sid)) {
329
+ log.error({ name, sessionId: sid }, "Completed session not in runningSessions — delivering anyway");
330
+ await this.#deliverResponse(response);
331
+ return;
332
+ }
333
+ this.#runningSessions.delete(sid);
334
+
335
+ if (sid === this.#mainSessionId) {
336
+ log.debug({ name, sessionId: sid }, "Main query finished, delivering directly");
337
+ await this.#deliverResponse(response);
338
+ } else {
339
+ log.debug({ name, sessionId: sid }, "Non-main query finished, feeding to main session");
340
+ this.#queue.push({ type: "background-agent-result", name, response });
341
+ }
342
+ },
343
+ async (err) => {
344
+ if (!this.#runningSessions.has(sid)) {
345
+ log.error({ name, sessionId: sid, err }, "Failed session not in runningSessions — delivering error");
346
+ } else {
347
+ this.#runningSessions.delete(sid);
348
+ log.error({ name, sessionId: sid, err }, "Main query failed");
349
+ }
350
+ await this.#deliverResponse(this.#errorResponse(err));
351
+ },
352
+ );
312
353
  }
313
354
 
314
- #formatPrompt(request: OrchestratorRequest): string {
355
+ #formatPrompt(request: OrchestratorRequest, name: string, backgroundedEvent?: string): string {
356
+ let input: EventInput;
357
+
315
358
  switch (request.type) {
316
- case "user": {
317
- if (!request.files?.length) return request.message;
318
- const prefix = request.files.map((f) => `[File: ${f}]`).join("\n");
319
- return request.message ? `${prefix}\n${request.message}` : prefix;
320
- }
321
- case "cron":
322
- return `[Context: cron/${request.name}] ${request.prompt}`;
359
+ case "user":
360
+ input = {
361
+ name,
362
+ type: "user-message",
363
+ session: "main",
364
+ text: request.message || undefined,
365
+ files: request.files,
366
+ backgroundedEvent,
367
+ };
368
+ break;
323
369
  case "background-agent-result":
324
- return `[Context: background-result/${request.name}] ${request.response.message || "[No output]"}`;
370
+ input = {
371
+ name,
372
+ type: "background-agent-result",
373
+ session: "main",
374
+ originalEvent: request.name,
375
+ result: {
376
+ text: request.response.message || "[No output]",
377
+ files: request.response.files,
378
+ },
379
+ backgroundedEvent,
380
+ instructions: "Forward this result to the user (action=\"send\"). Summarize or add context from the conversation as appropriate.",
381
+ };
382
+ break;
325
383
  case "button":
326
- return `[Context: button-click] User tapped "${request.label}"`;
384
+ input = {
385
+ name,
386
+ type: "button-click",
387
+ session: "main",
388
+ button: request.label,
389
+ backgroundedEvent,
390
+ };
391
+ break;
327
392
  }
393
+
394
+ return buildEvent(input);
328
395
  }
329
396
 
330
- async #awaitOrBackground(
331
- query: RunningQuery<AgentOutput>,
332
- request: OrchestratorRequest,
333
- timeoutMs: number,
334
- ): Promise<QueryResult<AgentOutput> | null> {
335
- const result = await Promise.race([
336
- query.result,
337
- new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),
338
- ]);
339
-
340
- if (result !== null) return result;
341
-
342
- const name = request.type === "user" ? request.message.slice(0, 30).replace(/\s+/g, "-")
343
- : request.type === "cron" ? `cron-${request.name}`
344
- : "task";
345
- const prompt = this.#formatPrompt(request);
346
- const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
347
- log.info({ name, sessionId: query.sessionId }, "Request backgrounded due to timeout");
348
- this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
349
- this.#adoptBackground(name, prompt, model, query);
350
- return null;
397
+ static #requestLabel(request: OrchestratorRequest): string {
398
+ switch (request.type) {
399
+ case "user":
400
+ return request.message;
401
+ case "background-agent-result":
402
+ return `bg:${request.name}`;
403
+ case "button":
404
+ return `btn:${request.label}`;
405
+ }
351
406
  }
352
407
 
353
408
  #errorResponse(err: unknown): AgentOutput {
@@ -368,51 +423,40 @@ export class Orchestrator {
368
423
  // --- Background management ---
369
424
 
370
425
  #spawnBackground(name: string, prompt: string, model: string | undefined) {
371
- const bgPrompt = `[Context: background-agent/${name}] ${prompt}`;
372
- const query = this.#mainSessionId
373
- ? this.#claude.forkSession(this.#mainSessionId, bgPrompt, responseResultType, { model })
374
- : this.#claude.newSession(bgPrompt, responseResultType, { model });
375
- const sessionId = query.sessionId;
376
- const info: BackgroundInfo = { name, prompt, model, query };
377
- this.#backgroundAgents.set(sessionId, info);
378
-
379
- log.debug({ name, sessionId }, "Starting background agent");
426
+ const formatted = buildEvent({
427
+ name,
428
+ type: "background-agent-start",
429
+ session: "background",
430
+ text: prompt,
431
+ });
432
+ this.#spawnBackgroundRaw(name, prompt, formatted, model);
433
+ }
380
434
 
381
- query.result.then(
382
- async ({ value: response }) => {
383
- if (!this.#backgroundAgents.has(sessionId)) return;
384
- this.#backgroundAgents.delete(sessionId);
385
- log.debug({ name, message: response.message }, "Background agent finished");
386
- this.#queue.push({ type: "background-agent-result", name, response });
387
- },
388
- (err) => {
389
- if (!this.#backgroundAgents.has(sessionId)) return;
390
- this.#backgroundAgents.delete(sessionId);
391
- log.error({ name, err }, "Background agent failed");
392
- this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
393
- },
394
- );
435
+ #spawnBackgroundRaw(name: string, prompt: string, formatted: string, model: string | undefined) {
436
+ const query = this.#mainSessionId
437
+ ? this.#claude.forkSession(this.#mainSessionId, formatted, responseResultType, { model })
438
+ : this.#claude.newSession(formatted, responseResultType, { model });
439
+ this.#registerBackground(name, prompt, model, query);
395
440
  }
396
441
 
397
- #adoptBackground(name: string, prompt: string, model: string | undefined, query: RunningQuery<AgentOutput>) {
398
- const sessionId = query.sessionId;
399
- const info: BackgroundInfo = { name, prompt, model, query };
400
- this.#backgroundAgents.set(sessionId, info);
442
+ #registerBackground(name: string, prompt: string, model: string | undefined, query: RunningQuery<AgentOutput>) {
443
+ const sid = query.sessionId;
444
+ this.#runningSessions.set(sid, { name, prompt, model, query, lastMessageAt: new Date() });
401
445
 
402
- log.debug({ name, sessionId }, "Adopting backgrounded task");
446
+ log.debug({ name, sessionId: sid }, "Background session registered");
403
447
 
404
448
  query.result.then(
405
449
  ({ value: response }) => {
406
- if (!this.#backgroundAgents.has(sessionId)) return;
407
- this.#backgroundAgents.delete(sessionId);
408
- log.debug({ name }, "Adopted task finished");
409
- this.#queue.push({ type: "background-agent-result", name, response, sessionId });
450
+ if (!this.#runningSessions.has(sid)) return;
451
+ this.#runningSessions.delete(sid);
452
+ log.debug({ name, message: response.message }, "Background session finished");
453
+ this.#queue.push({ type: "background-agent-result", name, response });
410
454
  },
411
455
  (err) => {
412
- if (!this.#backgroundAgents.has(sessionId)) return;
413
- this.#backgroundAgents.delete(sessionId);
414
- log.error({ name, err }, "Adopted task failed");
415
- this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "deferred-failed" }, sessionId });
456
+ if (!this.#runningSessions.has(sid)) return;
457
+ this.#runningSessions.delete(sid);
458
+ log.error({ name, err }, "Background session failed");
459
+ this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-failed" } });
416
460
  },
417
461
  );
418
462
  }