macroclaw 0.8.0 → 0.9.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.
@@ -1,16 +1,22 @@
1
1
  import { z } from "zod/v4";
2
- import { Claude, type ClaudeDeferredResult, ClaudeParseError, ClaudeProcessError, type ClaudeResult, isDeferred } from "./claude";
3
- import { logPrompt, logResult } from "./history";
2
+ import {
3
+ Claude,
4
+ QueryParseError,
5
+ QueryProcessError,
6
+ type QueryResult,
7
+ QueryValidationError,
8
+ type RunningQuery
9
+ } from "./claude";
10
+ import { writeHistoryPrompt, writeHistoryResult } from "./history";
4
11
  import { createLogger } from "./logger";
5
- import { BG_TIMEOUT, CRON_TIMEOUT, MAIN_TIMEOUT, SYSTEM_PROMPT } from "./prompts";
12
+ import { CRON_TIMEOUT, MAIN_TIMEOUT, SYSTEM_PROMPT } from "./prompts";
6
13
  import { Queue } from "./queue";
7
- import { loadSessions, newSessionId, type Sessions, saveSessions } from "./sessions";
14
+ import { loadSessions, saveSessions } from "./sessions";
8
15
  import type { ButtonSpec } from "./telegram";
9
16
 
10
17
  const log = createLogger("orchestrator");
11
18
 
12
- // --- Response schema (owned by orchestrator) ---
13
- // Flat object (no oneOf/discriminatedUnion) — Claude CLI --json-schema requires a single top-level object.
19
+ // --- Response schema ---
14
20
 
15
21
  const backgroundAgentSchema = z.object({
16
22
  name: z.string().describe("Label for the background agent"),
@@ -18,7 +24,7 @@ const backgroundAgentSchema = z.object({
18
24
  model: z.enum(["haiku", "sonnet", "opus"]).describe("Model to use for the background agent").optional(),
19
25
  });
20
26
 
21
- const claudeResponseSchema = z.object({
27
+ const agentOutputSchema = z.object({
22
28
  action: z.enum(["send", "silent"]).describe("'send' to reply to the user, 'silent' to do nothing"),
23
29
  actionReason: z.string().describe("Why the agent chose this action (logged, not sent)"),
24
30
  message: z.string().describe("The message to send to Telegram (required when action is 'send')").optional(),
@@ -27,9 +33,11 @@ const claudeResponseSchema = z.object({
27
33
  backgroundAgents: z.array(backgroundAgentSchema).describe("Background agents to spawn alongside this response").optional(),
28
34
  });
29
35
 
30
- type ClaudeResponseInternal = z.infer<typeof claudeResponseSchema>;
36
+ type AgentOutput = z.infer<typeof agentOutputSchema>;
31
37
 
32
- const jsonSchema = JSON.stringify(z.toJSONSchema(claudeResponseSchema, { target: "jsonSchema7" }));
38
+ const responseResultType = { type: "object" as const, schema: agentOutputSchema };
39
+
40
+ const textResultType = { type: "text" } as const;
33
41
 
34
42
  // --- Public response type ---
35
43
 
@@ -46,8 +54,7 @@ export interface OrchestratorResponse {
46
54
  type OrchestratorRequest =
47
55
  | { type: "user"; message: string; files?: string[] }
48
56
  | { type: "cron"; name: string; prompt: string; model?: string }
49
- | { type: "background-agent-result"; name: string; response: ClaudeResponseInternal; sessionId?: string }
50
- | { type: "background-agent"; name: string; prompt: string; model?: string }
57
+ | { type: "background-agent-result"; name: string; response: AgentOutput; sessionId?: string }
51
58
  | { type: "button"; label: string };
52
59
 
53
60
  function escapeHtml(text: string): string {
@@ -62,8 +69,6 @@ interface BackgroundInfo {
62
69
  startTime: Date;
63
70
  }
64
71
 
65
- // --- Orchestrator ---
66
-
67
72
  export interface OrchestratorConfig {
68
73
  model?: string;
69
74
  workspace: string;
@@ -72,45 +77,21 @@ export interface OrchestratorConfig {
72
77
  claude?: Claude;
73
78
  }
74
79
 
75
- interface BuiltRequest {
76
- prompt: string;
77
- model: string | undefined;
78
- timeout: number;
79
- files?: string[];
80
- useMainSession: boolean;
81
- }
82
-
83
- interface CallResult {
84
- response: ClaudeResponseInternal;
85
- sessionId: string;
86
- }
87
-
88
80
  export class Orchestrator {
81
+ #config: Omit<OrchestratorConfig , 'claude'>;
89
82
  #claude: Claude;
90
- #sessions: Sessions;
91
- #sessionId: string;
92
- #resumeSession: boolean;
93
- #sessionResolved = false;
94
- #config: OrchestratorConfig;
95
- #active = new Map<string, BackgroundInfo>();
83
+
84
+ #mainSessionId: string | undefined;
85
+ #backgroundAgents = new Map<string, BackgroundInfo>();
96
86
  #queue: Queue<OrchestratorRequest>;
97
87
 
98
88
  constructor(config: OrchestratorConfig) {
99
89
  this.#config = config;
100
- this.#claude = config.claude ?? new Claude({ workspace: config.workspace, jsonSchema });
101
- this.#sessions = loadSessions(config.settingsDir);
90
+ this.#claude = config.claude ?? new Claude({ workspace: config.workspace, systemPrompt: SYSTEM_PROMPT });
102
91
  this.#queue = new Queue<OrchestratorRequest>();
103
92
  this.#queue.setHandler((request) => this.#handleRequest(request));
104
93
 
105
- if (this.#sessions.mainSessionId) {
106
- this.#sessionId = this.#sessions.mainSessionId;
107
- this.#resumeSession = true;
108
- } else {
109
- this.#sessionId = newSessionId();
110
- this.#resumeSession = false;
111
- saveSessions({ mainSessionId: this.#sessionId }, config.settingsDir);
112
- log.info({ sessionId: this.#sessionId }, "Created new session");
113
- }
94
+ this.#mainSessionId = loadSessions(config.settingsDir).mainSessionId;
114
95
  }
115
96
 
116
97
  // --- Public handle methods ---
@@ -134,7 +115,7 @@ export class Orchestrator {
134
115
  }
135
116
 
136
117
  handleBackgroundList(): void {
137
- const agents = [...this.#active.values()];
118
+ const agents = [...this.#backgroundAgents.values()];
138
119
  if (agents.length === 0) {
139
120
  this.#callOnResponse({ message: "No background agents running." });
140
121
  return;
@@ -148,12 +129,12 @@ export class Orchestrator {
148
129
  const text = `${a.name} (${elapsed}s)`.slice(0, 27);
149
130
  return { text, data: `peek:${a.sessionId}` };
150
131
  });
151
- buttons.push("_dismiss");
132
+ buttons.push({text: "Dismiss", data: "_dismiss"});
152
133
  this.#callOnResponse({ message: lines.join("\n"), buttons });
153
134
  }
154
135
 
155
136
  async handlePeek(sessionId: string): Promise<void> {
156
- const agent = this.#active.get(sessionId);
137
+ const agent = this.#backgroundAgents.get(sessionId);
157
138
  if (!agent) {
158
139
  this.#callOnResponse({ message: "Agent not found or already finished." });
159
140
  return;
@@ -162,24 +143,21 @@ export class Orchestrator {
162
143
  this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(agent.name)}</b>...` });
163
144
 
164
145
  try {
165
- const result = await this.#claude.run({
166
- resume: true,
146
+ const query = this.#claude.forkSession(
167
147
  sessionId,
168
- forkSession: true,
169
- model: "haiku",
170
- plainText: true,
171
- prompt: "Give a brief status update: what has been done so far, what's currently happening, and what's remaining. 2-3 sentences max.",
172
- });
173
-
174
- const text = isDeferred(result) ? "Agent is still working..." : (result.result ?? "[No output]");
175
- this.#callOnResponse({ message: `<b>[${escapeHtml(agent.name)}]</b> ${text}` });
148
+ "Give a brief status update: what has been done so far, what's currently happening, and what's remaining. 2-3 sentences max.",
149
+ textResultType,
150
+ { model: "haiku" },
151
+ );
152
+ const { value } = await query.result;
153
+ this.#callOnResponse({ message: `<b>[${escapeHtml(agent.name)}]</b> ${value || "[No output]"}` });
176
154
  } catch (err) {
177
155
  this.#callOnResponse({ message: `Couldn't peek at ${escapeHtml(agent.name)}: ${err}` });
178
156
  }
179
157
  }
180
158
 
181
159
  handleSessionCommand(): void {
182
- this.#callOnResponse({ message: `Session: <code>${this.#sessionId}</code>` });
160
+ this.#callOnResponse({ message: `Session: <code>${this.#mainSessionId ?? "none"}</code>` });
183
161
  }
184
162
 
185
163
  // --- Internal queue handler ---
@@ -187,39 +165,32 @@ export class Orchestrator {
187
165
  async #handleRequest(request: OrchestratorRequest): Promise<void> {
188
166
  log.debug({ type: request.type }, "Incoming request");
189
167
 
190
- // Background result with matching session ID: deliver directly without Claude round-trip
191
- if (request.type === "background-agent-result" && "sessionId" in request && request.sessionId === this.#sessionId) {
168
+ // Background result with matching session ID: deliver directly
169
+ if (request.type === "background-agent-result" && request.sessionId === this.#mainSessionId) {
192
170
  log.debug({ name: request.name }, "Background result on current session, applying directly");
193
- await this.#deliverClaudeResponse(request.response);
171
+ await this.#deliverResponse(request.response);
194
172
  return;
195
173
  }
196
174
 
197
- // Fork session if a backgrounded task is running on the main session
198
- const needsFork = (request.type === "user" || request.type === "button") && this.#active.has(this.#sessionId);
199
-
200
- const startTime = new Date();
201
- const rawResponse = await this.#processRequest(request, needsFork ? { forkSession: true } : undefined);
202
- if (isDeferred(rawResponse)) {
203
- const name = request.type === "user" ? request.message.slice(0, 30).replace(/\s+/g, "-")
204
- : request.type === "cron" ? `cron-${request.name}`
205
- : "task";
206
- log.info({ name, sessionId: rawResponse.sessionId }, "Request backgrounded due to timeout");
207
- this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
208
- this.#adoptBackground(name, rawResponse.sessionId, startTime, rawResponse.completion.then(
209
- (r) => {
210
- const msg = r.structuredOutput ? String((r.structuredOutput as Record<string, unknown>).message ?? "") : (r.result ?? "");
211
- return { action: "send" as const, message: msg, actionReason: "deferred-completed" };
212
- },
213
- (err) => ({ action: "send" as const, message: `[Error] ${err}`, actionReason: "deferred-failed" }),
214
- ));
215
- return;
175
+ await writeHistoryPrompt(request);
176
+
177
+ const result = await this.#queryWithRetry(request);
178
+ if (!result) return;
179
+
180
+ // Update session ID (important after fork or new session)
181
+ if (result.sessionId !== this.#mainSessionId) {
182
+ log.info({ oldSessionId: this.#mainSessionId, newSessionId: result.sessionId }, "Session updated");
183
+ this.#mainSessionId = result.sessionId;
184
+ saveSessions({ mainSessionId: this.#mainSessionId }, this.#config.settingsDir);
216
185
  }
217
186
 
218
- log.debug({ action: rawResponse.action, actionReason: rawResponse.actionReason }, "Response");
219
- await this.#deliverClaudeResponse(rawResponse);
187
+ await writeHistoryResult(result.value);
188
+ await this.#deliverResponse(result.value);
220
189
  }
221
190
 
222
- async #deliverClaudeResponse(response: ClaudeResponseInternal): Promise<void> {
191
+ // --- Response delivery ---
192
+
193
+ async #deliverResponse(response: AgentOutput): Promise<void> {
223
194
  if (response.action === "send") {
224
195
  this.#callOnResponse({
225
196
  message: response.message || "[No output]",
@@ -245,187 +216,151 @@ export class Orchestrator {
245
216
  });
246
217
  }
247
218
 
248
- // --- Internal background management ---
219
+ // --- Claude calls ---
249
220
 
250
- #spawnBackground(name: string, prompt: string, model: string | undefined) {
251
- const sessionId = newSessionId();
252
- const info: BackgroundInfo = { name, sessionId, startTime: new Date() };
253
- this.#active.set(sessionId, info);
254
-
255
- log.debug({ name, sessionId }, "Starting background agent");
221
+ async #queryWithRetry(request: OrchestratorRequest): Promise<QueryResult<AgentOutput> | null> {
222
+ const timeout = request.type === "cron" ? CRON_TIMEOUT : MAIN_TIMEOUT;
223
+ const query = this.#query(request);
256
224
 
257
- this.#processRequest({ type: "background-agent", name, prompt, model }).then(
258
- async (rawResponse) => {
259
- let response: ClaudeResponseInternal;
260
- if (isDeferred(rawResponse)) {
261
- try {
262
- const r = await rawResponse.completion;
263
- response = { action: "send", message: String(r.structuredOutput ?? r.result ?? ""), actionReason: "deferred-completed" };
264
- } catch (err) {
265
- response = { action: "send", message: `[Error] ${err}`, actionReason: "deferred-failed" };
266
- }
267
- } else {
268
- response = rawResponse;
225
+ try {
226
+ const result = await this.#awaitOrBackground(query, request, timeout);
227
+ if (!result) return null;
228
+ return result;
229
+ } catch (err) {
230
+ // Resume failed retry with a fresh session
231
+ if (err instanceof QueryProcessError && this.#mainSessionId) {
232
+ log.info("Resume failed, retrying with new session");
233
+ this.#mainSessionId = undefined;
234
+ const retryQuery = this.#query(request);
235
+
236
+ try {
237
+ const retryResult = await this.#awaitOrBackground(retryQuery, request, timeout);
238
+ if (!retryResult) return null;
239
+ return retryResult;
240
+ } catch (retryErr) {
241
+ return { value: this.#errorResponse(retryErr), sessionId: retryQuery.sessionId };
269
242
  }
270
- this.#active.delete(sessionId);
271
- log.debug({ name, message: response.message }, "Background agent finished");
272
- this.#queue.push({ type: "background-agent-result", name, response });
273
- },
274
- (err) => {
275
- this.#active.delete(sessionId);
276
- log.error({ name, err }, "Background agent failed");
277
- this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
278
- },
279
- );
280
- }
281
-
282
- #adoptBackground(name: string, sessionId: string, startTime: Date, completion: Promise<ClaudeResponseInternal>) {
283
- const info: BackgroundInfo = { name, sessionId, startTime };
284
- this.#active.set(sessionId, info);
285
-
286
- log.debug({ name, sessionId }, "Adopting backgrounded task");
287
-
288
- // completion is pre-wrapped with .then(ok, err) at the call site, so it never rejects
289
- completion.then((response) => {
290
- this.#active.delete(sessionId);
291
- log.debug({ name, message: response.message }, "Adopted task finished");
292
- this.#queue.push({ type: "background-agent-result", name, response, sessionId });
293
- });
294
- }
295
-
296
- // --- Core Claude processing ---
297
-
298
- async #processRequest(request: OrchestratorRequest, options?: { forkSession?: boolean }): Promise<ClaudeResponseInternal | ClaudeDeferredResult> {
299
- const built = this.#buildRequest(request);
300
- await logPrompt(request);
301
-
302
- if (built.useMainSession) {
303
- let result = await this.#callClaude(built, this.#resumeSession, this.#sessionId, options?.forkSession);
304
-
305
- // Session resolution: if resume failed on first call, create new session
306
- if (!isDeferred(result) && !this.#sessionResolved && this.#resumeSession && result.response.actionReason === "process-error") {
307
- this.#sessionId = newSessionId();
308
- log.info({ sessionId: this.#sessionId }, "Resume failed, created new session");
309
- this.#resumeSession = false;
310
- saveSessions({ mainSessionId: this.#sessionId }, this.#config.settingsDir);
311
- result = await this.#callClaude(built, this.#resumeSession, this.#sessionId);
312
243
  }
313
244
 
314
- if (isDeferred(result)) return result;
245
+ return { value: this.#errorResponse(err), sessionId: this.#mainSessionId ?? "" };
246
+ }
247
+ }
315
248
 
316
- // Update session ID from response (important after fork)
317
- if (result.sessionId && result.sessionId !== this.#sessionId) {
318
- log.info({ oldSessionId: this.#sessionId, newSessionId: result.sessionId }, "Session forked, updating session ID");
319
- this.#sessionId = result.sessionId;
320
- saveSessions({ mainSessionId: this.#sessionId }, this.#config.settingsDir);
321
- }
249
+ #query(request: OrchestratorRequest) {
250
+ const prompt = this.#formatPrompt(request);
251
+ const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
252
+ const opts = { model };
322
253
 
323
- // Mark resolved on first success
324
- if (!this.#sessionResolved && result.response.actionReason !== "process-error") {
325
- this.#sessionResolved = true;
326
- this.#resumeSession = true;
327
- }
254
+ // Fork if a background agent is running on the main session
255
+ if (this.#mainSessionId && this.#backgroundAgents.has(this.#mainSessionId)) {
256
+ return this.#claude.forkSession(this.#mainSessionId, prompt, responseResultType, opts);
257
+ }
328
258
 
329
- await logResult(result.response);
330
- return result.response;
259
+ // Resume existing session
260
+ if (this.#mainSessionId) {
261
+ return this.#claude.resumeSession(this.#mainSessionId, prompt, responseResultType, opts);
331
262
  }
332
263
 
333
- // background-agent: fork from main session for full context
334
- log.debug({ name: (request as { name: string }).name }, "Processing background-agent (forked session)");
335
- const bgResult = await this.#callClaude(built, true, this.#sessionId, true);
336
- if (isDeferred(bgResult)) return bgResult;
337
- await logResult(bgResult.response);
338
- return bgResult.response;
264
+ // Start fresh
265
+ return this.#claude.newSession(prompt, responseResultType, opts);
339
266
  }
340
267
 
341
- #buildRequest(request: OrchestratorRequest): BuiltRequest {
268
+ #formatPrompt(request: OrchestratorRequest): string {
342
269
  switch (request.type) {
343
- case "user":
344
- return {
345
- prompt: this.#buildPromptWithFiles(request.message, request.files),
346
- model: this.#config.model,
347
- timeout: MAIN_TIMEOUT,
348
- useMainSession: true,
349
- };
270
+ case "user": {
271
+ if (!request.files?.length) return request.message;
272
+ const prefix = request.files.map((f) => `[File: ${f}]`).join("\n");
273
+ return request.message ? `${prefix}\n${request.message}` : prefix;
274
+ }
350
275
  case "cron":
351
- return {
352
- prompt: `[Context: cron/${request.name}] ${request.prompt}`,
353
- model: request.model ?? this.#config.model,
354
- timeout: CRON_TIMEOUT,
355
- useMainSession: true,
356
- };
276
+ return `[Context: cron/${request.name}] ${request.prompt}`;
357
277
  case "background-agent-result":
358
- return {
359
- prompt: `[Context: background-result/${request.name}] ${request.response.message || "[No output]"}`,
360
- model: this.#config.model,
361
- timeout: MAIN_TIMEOUT,
362
- useMainSession: true,
363
- };
278
+ return `[Context: background-result/${request.name}] ${request.response.message || "[No output]"}`;
364
279
  case "button":
365
- return {
366
- prompt: `[Context: button-click] User tapped "${request.label}"`,
367
- model: this.#config.model,
368
- timeout: MAIN_TIMEOUT,
369
- useMainSession: true,
370
- };
371
- case "background-agent":
372
- return {
373
- prompt: `[Context: background-agent/${request.name}] ${request.prompt}`,
374
- model: request.model ?? this.#config.model,
375
- timeout: BG_TIMEOUT,
376
- useMainSession: false,
377
- };
280
+ return `[Context: button-click] User tapped "${request.label}"`;
378
281
  }
379
282
  }
380
283
 
381
- #buildPromptWithFiles(message: string, files?: string[]): string {
382
- if (!files?.length) return message;
383
- const prefix = files.map((f) => `[File: ${f}]`).join("\n");
384
- return message ? `${prefix}\n${message}` : prefix;
284
+ async #awaitOrBackground(
285
+ query: RunningQuery<AgentOutput>,
286
+ request: OrchestratorRequest,
287
+ timeoutMs: number,
288
+ ): Promise<QueryResult<AgentOutput> | null> {
289
+ const result = await Promise.race([
290
+ query.result,
291
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),
292
+ ]);
293
+
294
+ if (result !== null) return result;
295
+
296
+ const name = request.type === "user" ? request.message.slice(0, 30).replace(/\s+/g, "-")
297
+ : request.type === "cron" ? `cron-${request.name}`
298
+ : "task";
299
+ log.info({ name, sessionId: query.sessionId }, "Request backgrounded due to timeout");
300
+ this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
301
+ this.#adoptBackground(name, query.sessionId, query.startedAt, query.result);
302
+ return null;
385
303
  }
386
304
 
387
- #validateResponse(raw: unknown): ClaudeResponseInternal {
388
- const parsed = claudeResponseSchema.safeParse(raw);
389
- if (parsed.success) return parsed.data;
390
- log.warn({ error: parsed.error.message }, "structured_output failed validation");
391
- const rawObj = raw as Record<string, unknown> | null;
392
- const msg = typeof rawObj?.message === "string" ? rawObj.message : JSON.stringify(raw);
393
- return { action: "send", message: msg, actionReason: "validation-failed" };
305
+ #errorResponse(err: unknown): AgentOutput {
306
+ if (err instanceof QueryProcessError) {
307
+ return { action: "send", message: `[Error] Claude exited with code ${err.exitCode}:\n${err.stderr}`, actionReason: "process-error" };
308
+ }
309
+ if (err instanceof QueryParseError) {
310
+ return { action: "send", message: `[JSON Error] ${err.raw}`, actionReason: "json-parse-failed" };
311
+ }
312
+ if (err instanceof QueryValidationError) {
313
+ const rawObj = err.raw as Record<string, unknown> | null;
314
+ const msg = typeof rawObj?.message === "string" ? rawObj.message : JSON.stringify(err.raw);
315
+ return { action: "send", message: msg, actionReason: "validation-failed" };
316
+ }
317
+ throw err;
394
318
  }
395
319
 
396
- #resultToCallResult(result: ClaudeResult): CallResult {
397
- if (result.structuredOutput) {
398
- return { response: this.#validateResponse(result.structuredOutput), sessionId: result.sessionId };
399
- }
400
- log.error({ hasResult: !!result.result }, "No structured_output in response");
401
- const raw = result.result ? escapeHtml(result.result) : "";
402
- const msg = raw ? `[No structured output] ${raw}` : "[No output]";
403
- return { response: { action: "send", message: msg, actionReason: "no-structured-output" }, sessionId: result.sessionId };
320
+ // --- Background management ---
321
+
322
+ #spawnBackground(name: string, prompt: string, model: string | undefined) {
323
+ const bgPrompt = `[Context: background-agent/${name}] ${prompt}`;
324
+ const query = this.#mainSessionId
325
+ ? this.#claude.forkSession(this.#mainSessionId, bgPrompt, responseResultType, { model })
326
+ : this.#claude.newSession(bgPrompt, responseResultType, { model });
327
+ const sessionId = query.sessionId;
328
+ const info: BackgroundInfo = { name, sessionId, startTime: query.startedAt };
329
+ this.#backgroundAgents.set(sessionId, info);
330
+
331
+ log.debug({ name, sessionId }, "Starting background agent");
332
+
333
+ query.result.then(
334
+ async ({ value: response }) => {
335
+ this.#backgroundAgents.delete(sessionId);
336
+ log.debug({ name, message: response.message }, "Background agent finished");
337
+ this.#queue.push({ type: "background-agent-result", name, response });
338
+ },
339
+ (err) => {
340
+ this.#backgroundAgents.delete(sessionId);
341
+ log.error({ name, err }, "Background agent failed");
342
+ this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
343
+ },
344
+ );
404
345
  }
405
346
 
406
- async #callClaude(built: BuiltRequest, resume: boolean, sid: string, forkSession?: boolean): Promise<CallResult | ClaudeDeferredResult> {
407
- try {
408
- const result = await this.#claude.run({
409
- prompt: built.prompt,
410
- resume,
411
- sessionId: sid,
412
- forkSession,
413
- model: built.model,
414
- systemPrompt: SYSTEM_PROMPT,
415
- timeoutMs: built.timeout,
416
- });
347
+ #adoptBackground(name: string, sessionId: string, startTime: Date, completion: Promise<QueryResult<AgentOutput>>) {
348
+ const info: BackgroundInfo = { name, sessionId, startTime };
349
+ this.#backgroundAgents.set(sessionId, info);
417
350
 
418
- if (isDeferred(result)) return result;
351
+ log.debug({ name, sessionId }, "Adopting backgrounded task");
419
352
 
420
- return this.#resultToCallResult(result);
421
- } catch (err) {
422
- if (err instanceof ClaudeProcessError) {
423
- return { response: { action: "send", message: `[Error] Claude exited with code ${err.exitCode}:\n${err.stderr}`, actionReason: "process-error" }, sessionId: sid };
424
- }
425
- if (err instanceof ClaudeParseError) {
426
- return { response: { action: "send", message: `[JSON Error] ${err.raw}`, actionReason: "json-parse-failed" }, sessionId: sid };
427
- }
428
- throw err;
429
- }
353
+ completion.then(
354
+ ({ value: response }) => {
355
+ this.#backgroundAgents.delete(sessionId);
356
+ log.debug({ name }, "Adopted task finished");
357
+ this.#queue.push({ type: "background-agent-result", name, response, sessionId });
358
+ },
359
+ (err) => {
360
+ this.#backgroundAgents.delete(sessionId);
361
+ log.error({ name, err }, "Adopted task failed");
362
+ this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "deferred-failed" }, sessionId });
363
+ },
364
+ );
430
365
  }
431
366
  }