praxis-agent 0.33.0 → 0.35.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.
Files changed (66) hide show
  1. package/README.md +4 -0
  2. package/dist/application/background-agent-manager.d.ts +48 -3
  3. package/dist/application/background-agent-manager.js +249 -73
  4. package/dist/application/background-task-runtime.d.ts +20 -1
  5. package/dist/application/background-task-runtime.js +45 -6
  6. package/dist/application/managed-worktree.d.ts +8 -0
  7. package/dist/application/managed-worktree.js +54 -2
  8. package/dist/application/session-service.d.ts +12 -1
  9. package/dist/application/session-service.js +1849 -1628
  10. package/dist/application/subagent-service.d.ts +34 -7
  11. package/dist/application/subagent-service.js +720 -150
  12. package/dist/application/turn-lifecycle.d.ts +38 -0
  13. package/dist/application/turn-lifecycle.js +36 -0
  14. package/dist/cli/interactive.d.ts +1 -0
  15. package/dist/cli/interactive.js +43 -8
  16. package/dist/cli/protocol.js +5 -2
  17. package/dist/cli/tui/claude-style.d.ts +5 -48
  18. package/dist/cli/tui/claude-style.js +19 -73
  19. package/dist/cli/tui/task-panel.d.ts +1 -1
  20. package/dist/cli/tui/task-panel.js +2 -1
  21. package/dist/cli/tui/transcript-presentation.d.ts +89 -0
  22. package/dist/cli/tui/transcript-presentation.js +169 -0
  23. package/dist/cli/tui/transcript-viewport.d.ts +5 -34
  24. package/dist/cli/tui/transcript-viewport.js +639 -158
  25. package/dist/cli/tui/tui-view-model.d.ts +6 -2
  26. package/dist/cli/tui/tui-view-model.js +23 -6
  27. package/dist/compatibility/claude/context.d.ts +2 -2
  28. package/dist/compatibility/claude/context.js +2 -14
  29. package/dist/compatibility/claude/data-plane-adapter.d.ts +9 -0
  30. package/dist/compatibility/claude/data-plane-adapter.js +44 -0
  31. package/dist/compatibility/claude/paths.js +2 -2
  32. package/dist/compatibility/claude/projection.d.ts +4 -0
  33. package/dist/compatibility/claude/projection.js +82 -0
  34. package/dist/compatibility/claude/shared-resources.d.ts +7 -38
  35. package/dist/compatibility/claude/sidechain.d.ts +5 -0
  36. package/dist/compatibility/claude/sidechain.js +13 -4
  37. package/dist/compatibility/claude/transcript-codec.d.ts +21 -0
  38. package/dist/compatibility/claude/transcript-codec.js +512 -0
  39. package/dist/core/context.d.ts +25 -7
  40. package/dist/core/context.js +46 -0
  41. package/dist/core/prompt-composer.d.ts +8 -16
  42. package/dist/core/prompt-composer.js +65 -25
  43. package/dist/core/resources.d.ts +39 -0
  44. package/dist/core/resources.js +2 -0
  45. package/dist/core/runtime.d.ts +1 -1
  46. package/dist/core/session.d.ts +2 -0
  47. package/dist/core/session.js +5 -0
  48. package/dist/core/transcript-codec.d.ts +70 -0
  49. package/dist/core/transcript-codec.js +60 -0
  50. package/dist/core/transcript-event.d.ts +33 -0
  51. package/dist/core/transcript-event.js +198 -0
  52. package/dist/persistence/claude-sidechain-store.js +19 -3
  53. package/dist/persistence/data-plane-adapter.d.ts +32 -0
  54. package/dist/persistence/data-plane-adapter.js +2 -0
  55. package/dist/persistence/data-plane.d.ts +4 -20
  56. package/dist/persistence/data-plane.js +10 -47
  57. package/dist/persistence/native-data-plane-adapter.d.ts +9 -0
  58. package/dist/persistence/native-data-plane-adapter.js +39 -0
  59. package/dist/persistence/native-resources.d.ts +4 -4
  60. package/dist/persistence/native-transcript-codec.d.ts +13 -0
  61. package/dist/persistence/native-transcript-codec.js +102 -0
  62. package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
  63. package/dist/persistence/subagent-lifecycle-store.js +299 -0
  64. package/dist/tools/claude-user-message.d.ts +0 -1
  65. package/dist/tools/claude-user-message.js +0 -1
  66. package/package.json +6 -2
package/README.md CHANGED
@@ -236,6 +236,10 @@ npm ci
236
236
  npm run check
237
237
  ```
238
238
 
239
+ `npm run build:native` compiles the current Praxis-owned core, provider
240
+ adapters, and native data-plane slice without the Claude compatibility adapter.
241
+ `npm run check` also enforces the corresponding source dependency direction.
242
+
239
243
  Contributions use Conventional Commit pull-request titles and the protected
240
244
  squash-merge workflow. Read
241
245
  [CONTRIBUTING.md](https://github.com/Forest-Isle/Praxis/blob/main/CONTRIBUTING.md)
@@ -10,11 +10,15 @@ export interface BackgroundAgentRunResult {
10
10
  isolationPath?: string;
11
11
  isolationRetained?: boolean;
12
12
  isolationWarning?: string;
13
+ notificationId?: string;
13
14
  }
14
15
  export declare class BackgroundAgentRunError extends Error {
15
16
  readonly result?: BackgroundAgentRunResult | undefined;
16
17
  constructor(message: string, result?: BackgroundAgentRunResult | undefined, cause?: unknown);
17
18
  }
19
+ export declare class BackgroundAgentShutdownError extends Error {
20
+ constructor();
21
+ }
18
22
  export interface BackgroundAgentTaskSpec {
19
23
  agentId: string;
20
24
  name?: string;
@@ -24,9 +28,18 @@ export interface BackgroundAgentTaskSpec {
24
28
  toolUseId: string;
25
29
  outputFile: string;
26
30
  resolvedModel: string;
27
- run(message: string, signal: AbortSignal, continuation: boolean): Promise<BackgroundAgentRunResult>;
31
+ markBackground?(): void;
32
+ acknowledgeNotification?(notificationId: string): Promise<void>;
33
+ prepareNotificationDetached?(notificationId: string, model: string): Promise<void>;
34
+ confirmNotificationDetached?(notificationId: string): Promise<void>;
35
+ run(message: string, signal: AbortSignal, continuation: boolean, toolUseId: string): Promise<BackgroundAgentRunResult>;
36
+ }
37
+ type BackgroundAgentStatus = 'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
38
+ export interface BackgroundAgentNotificationIdentity {
39
+ agentId: string;
40
+ toolUseId: string;
41
+ status: 'completed' | 'failed' | 'killed';
28
42
  }
29
- type BackgroundAgentStatus = 'running' | 'completed' | 'failed' | 'stopped';
30
43
  export interface BackgroundAgentSnapshot {
31
44
  agentId: string;
32
45
  status: BackgroundAgentStatus;
@@ -38,13 +51,33 @@ export interface BackgroundAgentSnapshot {
38
51
  startedAt: number;
39
52
  durationMs: number | null;
40
53
  }
54
+ export declare function backgroundAgentNotificationMarkers(notification: BackgroundAgentNotificationIdentity): readonly string[];
41
55
  export declare class BackgroundAgentManager {
42
56
  private readonly tasks;
43
57
  private readonly names;
58
+ private readonly closedSignal;
59
+ private resolveClosed;
44
60
  private closed;
61
+ constructor();
45
62
  launch(spec: BackgroundAgentTaskSpec): BackgroundAgentSnapshot;
63
+ adopt(options: {
64
+ spec: BackgroundAgentTaskSpec;
65
+ controller: AbortController;
66
+ operation: Promise<BackgroundAgentRunResult>;
67
+ startedAt: number;
68
+ }): BackgroundAgentSnapshot;
46
69
  registerCompleted(spec: BackgroundAgentTaskSpec, result: BackgroundAgentRunResult): BackgroundAgentSnapshot;
70
+ registerInterrupted(spec: BackgroundAgentTaskSpec, error?: string): BackgroundAgentSnapshot;
71
+ registerTerminal(spec: BackgroundAgentTaskSpec, status: 'failed' | 'stopped', error: string): BackgroundAgentSnapshot;
72
+ registerPersistedNotification(agentId: string, notification: {
73
+ id: string;
74
+ status: 'completed' | 'failed' | 'stopped';
75
+ result: BackgroundAgentRunResult | null;
76
+ error: string | null;
77
+ toolUseId: string;
78
+ }): void;
47
79
  has(agentId: string): boolean;
80
+ notificationClaimAgentIds(): string[];
48
81
  snapshotById(agentId: string): BackgroundAgentSnapshot | null;
49
82
  snapshots(): readonly BackgroundAgentSnapshot[];
50
83
  output(agentId: string, options: {
@@ -52,11 +85,14 @@ export declare class BackgroundAgentManager {
52
85
  timeout: number;
53
86
  }): Promise<string>;
54
87
  stop(agentId: string): string;
55
- close(): Promise<void>;
88
+ stopAndWait(agentId: string, timeout?: number): Promise<string>;
89
+ stopAll(): readonly string[];
90
+ close(drainMilliseconds?: number): Promise<void>;
56
91
  send(agentId: string, message: string, summary: string | undefined, toolUseId: string): string;
57
92
  notifications(options: {
58
93
  waitForRunning: boolean;
59
94
  excludeAgentId?: string;
95
+ consume?: boolean;
60
96
  }): Promise<{
61
97
  messages: string[];
62
98
  usage: ModelUsage;
@@ -64,8 +100,17 @@ export declare class BackgroundAgentManager {
64
100
  durationApiMs?: number;
65
101
  durationApiWithoutRetriesMs?: number;
66
102
  }>;
103
+ acknowledge(messages: readonly string[]): Promise<void>;
104
+ prepareNotificationsDetached(messages: readonly string[]): Promise<void>;
105
+ confirmNotificationsDetached(messages: readonly string[]): Promise<void>;
106
+ private forEachMatchingNotification;
107
+ acknowledgeDelivered(delivered: (notification: BackgroundAgentNotificationIdentity) => boolean): Promise<void>;
108
+ acknowledgeDeliveredAsDetached(delivered: (notification: BackgroundAgentNotificationIdentity) => boolean): Promise<void>;
67
109
  private start;
110
+ private track;
111
+ private registerTask;
68
112
  private resolveOptional;
113
+ private stopTask;
69
114
  private assertNameAvailable;
70
115
  private finishStopped;
71
116
  private resolveRequired;
@@ -1,5 +1,7 @@
1
- const AGENT_ID_PATTERN = /^a[0-9a-f]{16}$/u;
1
+ import { randomUUID } from 'node:crypto';
2
+ import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
2
3
  const MAX_TIMEOUT_MS = 600_000;
4
+ const DEFAULT_CLOSE_DRAIN_MS = 5_000;
3
5
  export class BackgroundAgentRunError extends Error {
4
6
  result;
5
7
  constructor(message, result, cause) {
@@ -8,8 +10,14 @@ export class BackgroundAgentRunError extends Error {
8
10
  this.name = 'BackgroundAgentRunError';
9
11
  }
10
12
  }
13
+ export class BackgroundAgentShutdownError extends Error {
14
+ constructor() {
15
+ super('Background agent manager closed');
16
+ this.name = 'BackgroundAgentShutdownError';
17
+ }
18
+ }
11
19
  function assertAgentId(agentId) {
12
- if (!AGENT_ID_PATTERN.test(agentId)) {
20
+ if (!isClaudeAgentId(agentId)) {
13
21
  throw new Error(`Invalid background agent ID: ${agentId}`);
14
22
  }
15
23
  }
@@ -21,6 +29,13 @@ function escapeXml(value) {
21
29
  .replaceAll('"', '&quot;')
22
30
  .replaceAll("'", '&apos;');
23
31
  }
32
+ export function backgroundAgentNotificationMarkers(notification) {
33
+ return [
34
+ `<task-id>${notification.agentId}</task-id>`,
35
+ `<tool-use-id>${escapeXml(notification.toolUseId)}</tool-use-id>`,
36
+ `<status>${notification.status}</status>`,
37
+ ];
38
+ }
24
39
  function waitBounded(operation, milliseconds) {
25
40
  return new Promise((resolve, reject) => {
26
41
  const timer = setTimeout(resolve, milliseconds);
@@ -149,71 +164,95 @@ function mergeToolModelUsage(map, breakdown) {
149
164
  export class BackgroundAgentManager {
150
165
  tasks = new Map();
151
166
  names = new Map();
167
+ closedSignal;
168
+ resolveClosed;
152
169
  closed = false;
170
+ constructor() {
171
+ this.closedSignal = new Promise((resolve) => {
172
+ this.resolveClosed = resolve;
173
+ });
174
+ }
153
175
  launch(spec) {
154
- if (this.closed)
155
- throw new Error('Background agent manager is closed');
156
- assertAgentId(spec.agentId);
157
- if (spec.name !== undefined &&
158
- !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(spec.name)) {
159
- throw new Error(`Invalid background agent name: ${spec.name}`);
160
- }
161
- if (this.tasks.has(spec.agentId)) {
162
- throw new Error(`Background agent ${spec.agentId} already exists`);
163
- }
164
- this.assertNameAvailable(spec.name, spec.agentId);
165
- if (spec.name !== undefined)
166
- this.names.set(spec.name, spec.agentId);
167
- const task = {
168
- spec,
176
+ const { task } = this.registerTask(spec, {
169
177
  status: 'running',
170
178
  controller: null,
171
- promise: null,
172
179
  result: null,
173
180
  error: null,
174
- notifications: [],
175
181
  generation: 0,
176
- queuedMessages: [],
177
182
  startedAt: Date.now(),
178
183
  durationMs: null,
179
- };
180
- this.tasks.set(spec.agentId, task);
184
+ }, 'throw');
185
+ spec.markBackground?.();
181
186
  this.start(task, spec.prompt, false, spec.toolUseId);
182
187
  return this.snapshot(task);
183
188
  }
189
+ adopt(options) {
190
+ const { spec } = options;
191
+ const { task } = this.registerTask(spec, {
192
+ status: 'running',
193
+ controller: options.controller,
194
+ result: null,
195
+ error: null,
196
+ generation: 1,
197
+ startedAt: options.startedAt,
198
+ durationMs: null,
199
+ }, 'throw');
200
+ spec.markBackground?.();
201
+ this.track(task, options.operation, 1, spec.toolUseId);
202
+ return this.snapshot(task);
203
+ }
184
204
  registerCompleted(spec, result) {
185
- if (this.closed)
186
- throw new Error('Background agent manager is closed');
187
- assertAgentId(spec.agentId);
188
- if (spec.name !== undefined &&
189
- !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(spec.name)) {
190
- throw new Error(`Invalid background agent name: ${spec.name}`);
191
- }
192
- const existing = this.tasks.get(spec.agentId);
193
- if (existing)
194
- return this.snapshot(existing);
195
- this.assertNameAvailable(spec.name, spec.agentId);
196
- if (spec.name !== undefined)
197
- this.names.set(spec.name, spec.agentId);
198
- const task = {
199
- spec,
205
+ const { task } = this.registerTask(spec, {
200
206
  status: 'completed',
201
207
  controller: null,
202
- promise: null,
203
208
  result,
204
209
  error: null,
205
- notifications: [],
206
210
  generation: 0,
207
- queuedMessages: [],
208
211
  startedAt: Date.now() - result.durationMs,
209
212
  durationMs: result.durationMs,
210
- };
211
- this.tasks.set(spec.agentId, task);
213
+ }, 'return-existing');
214
+ return this.snapshot(task);
215
+ }
216
+ registerInterrupted(spec, error = 'Persisted agent was interrupted before completion') {
217
+ const { task } = this.registerTask(spec, {
218
+ status: 'interrupted',
219
+ controller: null,
220
+ result: null,
221
+ error,
222
+ generation: 0,
223
+ startedAt: Date.now(),
224
+ durationMs: 0,
225
+ }, 'return-existing');
212
226
  return this.snapshot(task);
213
227
  }
228
+ registerTerminal(spec, status, error) {
229
+ const { task } = this.registerTask(spec, {
230
+ status,
231
+ controller: null,
232
+ result: null,
233
+ error,
234
+ generation: 0,
235
+ startedAt: Date.now(),
236
+ durationMs: 0,
237
+ }, 'return-existing');
238
+ return this.snapshot(task);
239
+ }
240
+ registerPersistedNotification(agentId, notification) {
241
+ const task = this.tasks.get(agentId);
242
+ if (!task)
243
+ throw new Error(`No task found with ID: ${agentId}`);
244
+ if (task.notifications.some(({ id }) => id === notification.id))
245
+ return;
246
+ task.notifications.push(notification);
247
+ }
214
248
  has(agentId) {
215
249
  return this.tasks.has(this.resolveOptional(agentId) ?? agentId);
216
250
  }
251
+ notificationClaimAgentIds() {
252
+ return [...this.tasks.entries()]
253
+ .filter(([, task]) => task.status === 'running' || task.notifications.length > 0)
254
+ .map(([agentId]) => agentId);
255
+ }
217
256
  snapshotById(agentId) {
218
257
  const task = this.tasks.get(agentId);
219
258
  return task ? this.snapshot(task) : null;
@@ -254,32 +293,52 @@ export class BackgroundAgentManager {
254
293
  const task = this.tasks.get(agentId);
255
294
  if (!task)
256
295
  throw new Error(`No task found with ID: ${agentId}`);
257
- if (task.status !== 'running' || !task.controller) {
258
- throw new Error(`Task ${agentId} is not running (status: ${task.status})`);
259
- }
260
- task.status = 'stopped';
261
- task.error = 'Stopped by TaskStop';
262
- task.durationMs ??= Date.now() - task.startedAt;
263
- task.queuedMessages.length = 0;
264
- task.controller.abort();
296
+ this.stopTask(task, 'Stopped by TaskStop');
265
297
  return `Task ${agentId} stopped successfully`;
266
298
  }
267
- async close() {
299
+ async stopAndWait(agentId, timeout = DEFAULT_CLOSE_DRAIN_MS) {
300
+ const message = this.stop(agentId);
301
+ const task = this.tasks.get(agentId);
302
+ if (task?.promise)
303
+ await waitBounded(task.promise, timeout);
304
+ return message;
305
+ }
306
+ stopAll() {
307
+ const stopped = [];
308
+ for (const task of this.tasks.values()) {
309
+ if (task.status !== 'running' || !task.controller)
310
+ continue;
311
+ this.stopTask(task, 'Stopped by explicit bulk kill');
312
+ stopped.push(task.spec.agentId);
313
+ }
314
+ return stopped;
315
+ }
316
+ async close(drainMilliseconds = DEFAULT_CLOSE_DRAIN_MS) {
268
317
  if (this.closed)
269
318
  return;
319
+ if (!Number.isFinite(drainMilliseconds) ||
320
+ drainMilliseconds < 0 ||
321
+ drainMilliseconds > MAX_TIMEOUT_MS) {
322
+ throw new Error(`close drain must be between 0 and ${MAX_TIMEOUT_MS} milliseconds`);
323
+ }
270
324
  this.closed = true;
325
+ this.resolveClosed();
271
326
  const running = [];
272
327
  for (const task of this.tasks.values()) {
273
- if (task.status !== 'running' || !task.controller)
274
- continue;
275
- task.status = 'stopped';
276
- task.error = 'Stopped because the background agent manager closed';
277
- task.queuedMessages.length = 0;
278
- task.controller.abort();
328
+ if (task.status === 'running' && task.controller) {
329
+ task.status = 'stopped';
330
+ task.error = 'Stopped because the background agent manager closed';
331
+ task.suppressNotifications = true;
332
+ task.notifications.length = 0;
333
+ task.queuedMessages.length = 0;
334
+ task.controller.abort(new BackgroundAgentShutdownError());
335
+ }
279
336
  if (task.promise)
280
337
  running.push(task.promise);
281
338
  }
282
- await Promise.allSettled(running);
339
+ await waitBounded(Promise.allSettled(running).then(() => undefined), drainMilliseconds);
340
+ this.tasks.clear();
341
+ this.names.clear();
283
342
  }
284
343
  send(agentId, message, summary, toolUseId) {
285
344
  if (message.trim().length === 0)
@@ -317,8 +376,9 @@ export class BackgroundAgentManager {
317
376
  .filter((task) => task.status === 'running')
318
377
  .map((task) => task.promise)
319
378
  .filter((promise) => promise !== null);
320
- if (running.length > 0)
321
- await Promise.race(running);
379
+ if (running.length > 0) {
380
+ await Promise.race([Promise.race(running), this.closedSignal]);
381
+ }
322
382
  }
323
383
  const notifications = [];
324
384
  let usage = { inputTokens: 0, outputTokens: 0 };
@@ -352,16 +412,86 @@ export class BackgroundAgentManager {
352
412
  const modelUsage = modelUsageByModel.size === 0
353
413
  ? undefined
354
414
  : Object.fromEntries(modelUsageByModel);
415
+ if (options.consume !== false) {
416
+ for (const task of consumedTasks) {
417
+ if (!task.spec.acknowledgeNotification)
418
+ continue;
419
+ for (const notification of task.notifications) {
420
+ await task.spec.acknowledgeNotification(notification.id);
421
+ }
422
+ }
423
+ }
355
424
  const result = {
356
425
  messages: notifications,
357
426
  usage,
358
427
  ...(modelUsage === undefined ? {} : { modelUsage }),
359
428
  ...(durationSeen ? { durationApiMs, durationApiWithoutRetriesMs } : {}),
360
429
  };
361
- for (const task of consumedTasks)
362
- task.notifications.splice(0);
430
+ if (options.consume !== false) {
431
+ for (const task of consumedTasks)
432
+ task.notifications.splice(0);
433
+ }
363
434
  return result;
364
435
  }
436
+ async acknowledge(messages) {
437
+ await this.forEachMatchingNotification(messages, async (task, notification) => {
438
+ await task.spec.acknowledgeNotification?.(notification.id);
439
+ task.notifications.splice(task.notifications.indexOf(notification), 1);
440
+ });
441
+ }
442
+ async prepareNotificationsDetached(messages) {
443
+ await this.forEachMatchingNotification(messages, async (task, notification) => {
444
+ await task.spec.prepareNotificationDetached?.(notification.id, task.spec.resolvedModel);
445
+ });
446
+ }
447
+ async confirmNotificationsDetached(messages) {
448
+ await this.forEachMatchingNotification(messages, async (task, notification) => {
449
+ await task.spec.confirmNotificationDetached?.(notification.id);
450
+ });
451
+ }
452
+ async forEachMatchingNotification(messages, operation) {
453
+ const remaining = [...messages];
454
+ for (const task of this.tasks.values()) {
455
+ for (const notification of [...task.notifications]) {
456
+ const message = this.formatNotification(task, notification);
457
+ const index = remaining.indexOf(message);
458
+ if (index < 0)
459
+ continue;
460
+ await operation(task, notification);
461
+ remaining.splice(index, 1);
462
+ }
463
+ }
464
+ }
465
+ async acknowledgeDelivered(delivered) {
466
+ for (const [agentId, task] of this.tasks) {
467
+ for (const notification of [...task.notifications]) {
468
+ if (notification.status === 'interrupted')
469
+ continue;
470
+ const status = notification.status === 'stopped' ? 'killed' : notification.status;
471
+ if (!delivered({ agentId, toolUseId: notification.toolUseId, status })) {
472
+ continue;
473
+ }
474
+ await task.spec.acknowledgeNotification?.(notification.id);
475
+ task.notifications.splice(task.notifications.indexOf(notification), 1);
476
+ }
477
+ }
478
+ }
479
+ async acknowledgeDeliveredAsDetached(delivered) {
480
+ for (const [agentId, task] of this.tasks) {
481
+ for (const notification of [...task.notifications]) {
482
+ if (notification.status === 'interrupted')
483
+ continue;
484
+ const status = notification.status === 'stopped' ? 'killed' : notification.status;
485
+ if (!delivered({ agentId, toolUseId: notification.toolUseId, status })) {
486
+ continue;
487
+ }
488
+ await task.spec.prepareNotificationDetached?.(notification.id, task.spec.resolvedModel);
489
+ await task.spec.confirmNotificationDetached?.(notification.id);
490
+ await task.spec.acknowledgeNotification?.(notification.id);
491
+ task.notifications.splice(task.notifications.indexOf(notification), 1);
492
+ }
493
+ }
494
+ }
365
495
  start(task, message, continuation, toolUseId) {
366
496
  task.generation += 1;
367
497
  const generation = task.generation;
@@ -372,8 +502,10 @@ export class BackgroundAgentManager {
372
502
  task.controller = controller;
373
503
  task.result = null;
374
504
  task.error = null;
375
- task.promise = task.spec
376
- .run(message, controller.signal, continuation)
505
+ this.track(task, task.spec.run(message, controller.signal, continuation, toolUseId), generation, toolUseId);
506
+ }
507
+ track(task, operation, generation, toolUseId) {
508
+ task.promise = operation
377
509
  .then((result) => {
378
510
  if (task.generation !== generation)
379
511
  return;
@@ -386,6 +518,7 @@ export class BackgroundAgentManager {
386
518
  task.durationMs = result.durationMs;
387
519
  task.error = null;
388
520
  task.notifications.push({
521
+ id: result.notificationId ?? randomUUID(),
389
522
  status: 'completed',
390
523
  result,
391
524
  error: null,
@@ -406,8 +539,9 @@ export class BackgroundAgentManager {
406
539
  failedResult?.durationMs ?? Date.now() - task.startedAt;
407
540
  task.error = error instanceof Error ? error.message : String(error);
408
541
  task.notifications.push({
542
+ id: failedResult?.notificationId ?? randomUUID(),
409
543
  status: 'failed',
410
- result: null,
544
+ result: failedResult ?? null,
411
545
  error: task.error,
412
546
  toolUseId,
413
547
  });
@@ -423,11 +557,50 @@ export class BackgroundAgentManager {
423
557
  }
424
558
  });
425
559
  }
560
+ registerTask(spec, state, duplicate) {
561
+ if (this.closed)
562
+ throw new Error('Background agent manager is closed');
563
+ assertAgentId(spec.agentId);
564
+ if (spec.name !== undefined &&
565
+ !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(spec.name)) {
566
+ throw new Error(`Invalid background agent name: ${spec.name}`);
567
+ }
568
+ const existing = this.tasks.get(spec.agentId);
569
+ if (existing) {
570
+ if (duplicate === 'throw') {
571
+ throw new Error(`Background agent ${spec.agentId} already exists`);
572
+ }
573
+ return { task: existing, created: false };
574
+ }
575
+ this.assertNameAvailable(spec.name, spec.agentId);
576
+ if (spec.name !== undefined)
577
+ this.names.set(spec.name, spec.agentId);
578
+ const task = {
579
+ spec,
580
+ ...state,
581
+ promise: null,
582
+ notifications: [],
583
+ queuedMessages: [],
584
+ suppressNotifications: false,
585
+ };
586
+ this.tasks.set(spec.agentId, task);
587
+ return { task, created: true };
588
+ }
426
589
  resolveOptional(identifier) {
427
- if (AGENT_ID_PATTERN.test(identifier))
590
+ if (isClaudeAgentId(identifier))
428
591
  return identifier;
429
592
  return this.names.get(identifier);
430
593
  }
594
+ stopTask(task, reason) {
595
+ if (task.status !== 'running' || !task.controller) {
596
+ throw new Error(`Task ${task.spec.agentId} is not running (status: ${task.status})`);
597
+ }
598
+ task.status = 'stopped';
599
+ task.error = reason;
600
+ task.durationMs ??= Date.now() - task.startedAt;
601
+ task.queuedMessages.length = 0;
602
+ task.controller.abort();
603
+ }
431
604
  assertNameAvailable(name, agentId) {
432
605
  if (name === undefined)
433
606
  return;
@@ -445,16 +618,19 @@ export class BackgroundAgentManager {
445
618
  };
446
619
  task.result = stoppedResult;
447
620
  task.durationMs ??= stoppedResult.durationMs || Date.now() - task.startedAt;
448
- task.notifications.push({
449
- status: 'stopped',
450
- result: stoppedResult,
451
- error: task.error,
452
- toolUseId: task.spec.toolUseId,
453
- });
621
+ if (!task.suppressNotifications) {
622
+ task.notifications.push({
623
+ id: stoppedResult.notificationId ?? randomUUID(),
624
+ status: 'stopped',
625
+ result: stoppedResult,
626
+ error: task.error,
627
+ toolUseId: task.spec.toolUseId,
628
+ });
629
+ }
454
630
  }
455
631
  resolveRequired(identifier) {
456
632
  const resolved = this.resolveOptional(identifier);
457
- if (!AGENT_ID_PATTERN.test(identifier) &&
633
+ if (!isClaudeAgentId(identifier) &&
458
634
  !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(identifier)) {
459
635
  assertAgentId(identifier);
460
636
  }
@@ -12,7 +12,14 @@ export interface BackgroundBashTaskSource {
12
12
  }
13
13
  export interface BackgroundAgentTaskSource {
14
14
  backgroundSnapshots(): readonly BackgroundAgentSnapshot[];
15
- stopBackgroundTask(taskId: string): string;
15
+ outputBackgroundTask(taskId: string, options: {
16
+ block: boolean;
17
+ timeout: number;
18
+ }): Promise<string>;
19
+ stopBackgroundTask(taskId: string): Promise<string>;
20
+ sendBackgroundMessage(agentId: string, message: string, summary: string | undefined, toolUseId: string): string;
21
+ hasForegroundTask(): boolean;
22
+ backgroundForegroundTask(): BackgroundAgentSnapshot;
16
23
  }
17
24
  export interface WorkflowTaskSource {
18
25
  list(sessionId?: string): readonly WorkflowTaskSnapshot[];
@@ -29,6 +36,18 @@ export declare class BackgroundTaskRuntime {
29
36
  registerAgents(sessionId: string, source: BackgroundAgentTaskSource): void;
30
37
  snapshot(sessionId: string): Promise<BackgroundTaskSnapshot>;
31
38
  stop(sessionId: string, taskId: string): Promise<void>;
39
+ outputAgent(sessionId: string, agentId: string, options: {
40
+ block: boolean;
41
+ timeout: number;
42
+ }): Promise<string | null>;
43
+ stopAgent(sessionId: string, agentId: string): Promise<string | null>;
44
+ sendAgent(sessionId: string, agentId: string, message: string, summary: string | undefined, toolUseId: string): string | null;
45
+ sendAgentWithOwner(sessionId: string, agentId: string, message: string, summary: string | undefined, toolUseId: string): {
46
+ content: string;
47
+ owner: BackgroundAgentTaskSource;
48
+ } | null;
49
+ backgroundForeground(sessionId: string): BackgroundAgentSnapshot;
32
50
  clear(): void;
51
+ private agentSource;
33
52
  }
34
53
  //# sourceMappingURL=background-task-runtime.d.ts.map
@@ -38,22 +38,61 @@ export class BackgroundTaskRuntime {
38
38
  await source.stopBackgroundTask(taskId);
39
39
  return;
40
40
  }
41
- for (const source of this.agentSources.get(sessionId) ?? []) {
42
- if (!source.backgroundSnapshots().some(({ agentId }) => agentId === taskId)) {
43
- continue;
44
- }
45
- source.stopBackgroundTask(taskId);
41
+ if ((await this.stopAgent(sessionId, taskId)) !== null)
46
42
  return;
47
- }
48
43
  if (this.workflows?.hasForSession(sessionId, taskId)) {
49
44
  await this.workflows.stopAndWait(taskId);
50
45
  return;
51
46
  }
52
47
  throw new Error(`No task found with ID: ${taskId}`);
53
48
  }
49
+ async outputAgent(sessionId, agentId, options) {
50
+ const source = this.agentSource(sessionId, agentId);
51
+ return source ? source.outputBackgroundTask(agentId, options) : null;
52
+ }
53
+ async stopAgent(sessionId, agentId) {
54
+ const source = this.agentSource(sessionId, agentId);
55
+ return source ? source.stopBackgroundTask(agentId) : null;
56
+ }
57
+ sendAgent(sessionId, agentId, message, summary, toolUseId) {
58
+ return (this.sendAgentWithOwner(sessionId, agentId, message, summary, toolUseId)
59
+ ?.content ?? null);
60
+ }
61
+ sendAgentWithOwner(sessionId, agentId, message, summary, toolUseId) {
62
+ const source = this.agentSource(sessionId, agentId);
63
+ return source
64
+ ? {
65
+ content: source.sendBackgroundMessage(agentId, message, summary, toolUseId),
66
+ owner: source,
67
+ }
68
+ : null;
69
+ }
70
+ backgroundForeground(sessionId) {
71
+ const sources = [...(this.agentSources.get(sessionId) ?? [])].reverse();
72
+ const source = sources.find((candidate) => candidate.hasForegroundTask());
73
+ if (!source)
74
+ throw new Error('No foreground agent is running');
75
+ return source.backgroundForegroundTask();
76
+ }
54
77
  clear() {
55
78
  this.bashSources.clear();
56
79
  this.agentSources.clear();
57
80
  }
81
+ agentSource(sessionId, identifier) {
82
+ const sources = [...(this.agentSources.get(sessionId) ?? [])];
83
+ const idMatches = sources.filter((source) => source
84
+ .backgroundSnapshots()
85
+ .some(({ agentId }) => agentId === identifier));
86
+ if (idMatches.length > 1) {
87
+ throw new Error(`Multiple background agent owners claim ID: ${identifier}`);
88
+ }
89
+ if (idMatches[0])
90
+ return idMatches[0];
91
+ const nameMatches = sources.filter((source) => source.backgroundSnapshots().some(({ name }) => name === identifier));
92
+ if (nameMatches.length > 1) {
93
+ throw new Error(`Multiple live background agents are named '${identifier}'`);
94
+ }
95
+ return nameMatches[0] ?? null;
96
+ }
58
97
  }
59
98
  //# sourceMappingURL=background-task-runtime.js.map
@@ -12,4 +12,12 @@ export declare function createManagedWorktree(options: {
12
12
  directoryName: string;
13
13
  label: 'Agent' | 'Workflow';
14
14
  }): Promise<ManagedWorktree>;
15
+ /** Reattaches only to a real, registered Git worktree. Persisted metadata is
16
+ * untrusted input, so an arbitrary directory can never become an execution
17
+ * cwd merely because it exists. Restored worktrees are retained for audit. */
18
+ export declare function restoreManagedWorktree(options: {
19
+ cwd: string;
20
+ path: string;
21
+ label: 'Agent' | 'Workflow';
22
+ }): Promise<ManagedWorktree>;
15
23
  //# sourceMappingURL=managed-worktree.d.ts.map