praxis-agent 0.32.0 → 0.34.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 (36) hide show
  1. package/dist/application/background-agent-manager.d.ts +48 -3
  2. package/dist/application/background-agent-manager.js +249 -73
  3. package/dist/application/background-task-runtime.d.ts +20 -1
  4. package/dist/application/background-task-runtime.js +45 -6
  5. package/dist/application/managed-worktree.d.ts +8 -0
  6. package/dist/application/managed-worktree.js +54 -2
  7. package/dist/application/session-service.d.ts +38 -0
  8. package/dist/application/session-service.js +590 -70
  9. package/dist/application/subagent-service.d.ts +34 -7
  10. package/dist/application/subagent-service.js +694 -135
  11. package/dist/cli/interactive.d.ts +1 -0
  12. package/dist/cli/interactive.js +26 -1
  13. package/dist/cli/tui/task-panel.d.ts +1 -1
  14. package/dist/cli/tui/task-panel.js +2 -1
  15. package/dist/cli-runtime.d.ts +1 -0
  16. package/dist/cli-runtime.js +111 -31
  17. package/dist/compatibility/claude/history.d.ts +8 -0
  18. package/dist/compatibility/claude/history.js +54 -0
  19. package/dist/compatibility/claude/interruption.d.ts +16 -0
  20. package/dist/compatibility/claude/interruption.js +147 -0
  21. package/dist/compatibility/claude/projection.d.ts +4 -0
  22. package/dist/compatibility/claude/projection.js +82 -0
  23. package/dist/compatibility/claude/schema.js +22 -0
  24. package/dist/compatibility/claude/session-metadata.d.ts +44 -0
  25. package/dist/compatibility/claude/session-metadata.js +180 -0
  26. package/dist/compatibility/claude/sidechain.d.ts +5 -0
  27. package/dist/compatibility/claude/sidechain.js +13 -4
  28. package/dist/persistence/claude-session-index.d.ts +39 -0
  29. package/dist/persistence/claude-session-index.js +175 -0
  30. package/dist/persistence/claude-sidechain-store.js +19 -3
  31. package/dist/persistence/claude-transcript-store.d.ts +6 -2
  32. package/dist/persistence/claude-transcript-store.js +78 -3
  33. package/dist/persistence/in-memory-transcript-store.js +1 -0
  34. package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
  35. package/dist/persistence/subagent-lifecycle-store.js +299 -0
  36. package/package.json +2 -1
@@ -1,11 +1,11 @@
1
1
  import { randomBytes, randomUUID } from 'node:crypto';
2
2
  import { lstat, mkdir, readFile, readdir } from 'node:fs/promises';
3
- import { join, relative } from 'node:path';
3
+ import { isAbsolute, join, relative } from 'node:path';
4
4
  import { Ajv2020 } from 'ajv/dist/2020.js';
5
5
  import { resolveClaudePaths } from '../compatibility/claude/paths.js';
6
6
  import { workflowAgentFiles } from '../compatibility/claude/workflow.js';
7
- import { createClaudeAsyncAgentToolUseResult, createClaudeAgentToolUseResult, createClaudeSidechainRoot, resolveClaudeSidechainPaths, toClaudeSidechainEntry, } from '../compatibility/claude/sidechain.js';
8
- import { projectClaudeModelMessages } from '../compatibility/claude/projection.js';
7
+ import { createClaudeAsyncAgentToolUseResult, createClaudeAgentToolUseResult, createClaudeSidechainRoot, isClaudeAgentId, resolveClaudeSidechainPaths, toClaudeSidechainEntry, } from '../compatibility/claude/sidechain.js';
8
+ import { projectClaudeModelMessages, projectClaudeSidechainContinuationMessages, } from '../compatibility/claude/projection.js';
9
9
  import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
10
10
  import { createClaudeHookAttachmentEntries, translateProviderEvents, } from '../compatibility/claude/translation.js';
11
11
  import { injectFirstUserMessageContext, } from '../core/context.js';
@@ -16,21 +16,105 @@ import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.
16
16
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
17
17
  import { ClaudeSidechainStore } from '../persistence/claude-sidechain-store.js';
18
18
  import { InMemorySidechainStore } from '../persistence/in-memory-sidechain-store.js';
19
+ import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
19
20
  import { resolveDataPlanePaths, } from '../persistence/data-plane.js';
20
- import { BackgroundAgentManager, BackgroundAgentRunError, } from './background-agent-manager.js';
21
+ import { BackgroundAgentManager, BackgroundAgentRunError, BackgroundAgentShutdownError, backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
21
22
  import { isBackgroundBashTaskId } from './background-task-id.js';
22
- import { createManagedWorktree, } from './managed-worktree.js';
23
+ import { createManagedWorktree, restoreManagedWorktree, } from './managed-worktree.js';
23
24
  import { createWorkflowWorktree } from './workflow-worktree.js';
24
25
  const DEFAULT_MAX_DEPTH = 4;
25
26
  const DEFAULT_MAX_CALLS = 16;
26
27
  const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
27
28
  const SIDECHAIN_DISCOVERY_MAX_DEPTH = 4;
29
+ function mergeSubagentUsage(left, right) {
30
+ for (const usage of [left, right]) {
31
+ for (const field of [
32
+ 'inputTokens',
33
+ 'outputTokens',
34
+ 'cacheReadInputTokens',
35
+ 'cacheCreationInputTokens',
36
+ 'webSearchRequests',
37
+ ]) {
38
+ const value = usage[field];
39
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
40
+ throw new Error(`Subagent usage has an invalid ${field}`);
41
+ }
42
+ }
43
+ for (const field of ['contextWindow', 'maxOutputTokens']) {
44
+ const value = usage[field];
45
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
46
+ throw new Error(`Subagent usage has an invalid ${field}`);
47
+ }
48
+ }
49
+ }
50
+ const counters = {
51
+ inputTokens: left.inputTokens + right.inputTokens,
52
+ outputTokens: left.outputTokens + right.outputTokens,
53
+ cacheReadInputTokens: (left.cacheReadInputTokens ?? 0) + (right.cacheReadInputTokens ?? 0),
54
+ cacheCreationInputTokens: (left.cacheCreationInputTokens ?? 0) +
55
+ (right.cacheCreationInputTokens ?? 0),
56
+ webSearchRequests: (left.webSearchRequests ?? 0) + (right.webSearchRequests ?? 0),
57
+ };
58
+ if (Object.values(counters).some((value) => !Number.isSafeInteger(value))) {
59
+ throw new Error('Subagent usage total overflow');
60
+ }
61
+ const metadata = (field) => {
62
+ const leftValue = left[field];
63
+ const rightValue = right[field];
64
+ if (leftValue !== undefined &&
65
+ rightValue !== undefined &&
66
+ leftValue !== rightValue) {
67
+ throw new Error(`Subagent usage has conflicting ${field}`);
68
+ }
69
+ return leftValue ?? rightValue;
70
+ };
71
+ const contextWindow = metadata('contextWindow');
72
+ const maxOutputTokens = metadata('maxOutputTokens');
73
+ return {
74
+ inputTokens: counters.inputTokens,
75
+ outputTokens: counters.outputTokens,
76
+ ...(counters.cacheReadInputTokens === 0
77
+ ? {}
78
+ : { cacheReadInputTokens: counters.cacheReadInputTokens }),
79
+ ...(counters.cacheCreationInputTokens === 0
80
+ ? {}
81
+ : { cacheCreationInputTokens: counters.cacheCreationInputTokens }),
82
+ ...(counters.webSearchRequests === 0
83
+ ? {}
84
+ : { webSearchRequests: counters.webSearchRequests }),
85
+ ...(contextWindow === undefined ? {} : { contextWindow }),
86
+ ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
87
+ };
88
+ }
89
+ function mergeSubagentModelUsage(target, source) {
90
+ for (const [model, usage] of Object.entries(source)) {
91
+ if (model.trim().length === 0) {
92
+ throw new Error('Subagent model usage has a blank model name');
93
+ }
94
+ target.set(model, mergeSubagentUsage(target.get(model) ?? { inputTokens: 0, outputTokens: 0 }, usage));
95
+ }
96
+ }
97
+ class SubagentExecutionFailure extends Error {
98
+ result;
99
+ name = 'SubagentExecutionFailure';
100
+ constructor(cause, result) {
101
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
102
+ this.result = result;
103
+ }
104
+ }
28
105
  const structuredOnlyTools = {
29
106
  definitions: () => [],
30
107
  schedulingPolicy: () => ({ concurrency: 'exclusive' }),
31
108
  prepare: async (call) => call,
32
109
  execute: async () => ({ content: '', isError: false }),
33
110
  };
111
+ function sidechainAgentIdFromFile(fileName, suffix) {
112
+ if (!fileName.startsWith('agent-') || !fileName.endsWith(suffix)) {
113
+ return undefined;
114
+ }
115
+ const agentId = fileName.slice('agent-'.length, -suffix.length);
116
+ return isClaudeAgentId(agentId) ? agentId : undefined;
117
+ }
34
118
  async function readSidechainMetadataName(metadataFile) {
35
119
  try {
36
120
  const value = JSON.parse(await readFile(metadataFile, 'utf8'));
@@ -368,6 +452,7 @@ export class ClaudeSubagentExecutor {
368
452
  calls = 0;
369
453
  background = new BackgroundAgentManager();
370
454
  ephemeralSidechains = new Map();
455
+ foreground = new Map();
371
456
  isEnabled(name) {
372
457
  return this.options.toolNames?.includes(name) ?? true;
373
458
  }
@@ -376,14 +461,55 @@ export class ClaudeSubagentExecutor {
376
461
  this.schema = selectClaudeSchemaAdapter(options.claudeVersion);
377
462
  }
378
463
  async close() {
464
+ for (const agentId of [...this.foreground.keys()]) {
465
+ this.backgroundForegroundTask(agentId);
466
+ }
379
467
  await this.background.close();
468
+ this.foreground.clear();
380
469
  this.ephemeralSidechains.clear();
381
470
  }
382
471
  backgroundSnapshots() {
383
472
  return this.background.snapshots();
384
473
  }
474
+ hasForegroundTask() {
475
+ return this.foreground.size > 0;
476
+ }
477
+ backgroundForegroundTask(identifier) {
478
+ const candidates = [...this.foreground.entries()];
479
+ const selected = identifier === undefined
480
+ ? candidates.at(-1)
481
+ : candidates.find(([agentId, task]) => agentId === identifier || task.spec.name === identifier);
482
+ if (!selected) {
483
+ throw new Error(identifier === undefined
484
+ ? 'No foreground agent is running'
485
+ : `No foreground agent found with ID or name: ${identifier}`);
486
+ }
487
+ const [, task] = selected;
488
+ if (task.backgrounded) {
489
+ throw new Error(`Agent ${task.spec.agentId} is already backgrounded`);
490
+ }
491
+ const snapshot = this.background.adopt({
492
+ spec: task.spec,
493
+ controller: task.controller,
494
+ operation: task.operation,
495
+ startedAt: task.startedAt,
496
+ });
497
+ task.backgrounded = true;
498
+ task.detachParentSignal();
499
+ task.resolveHandoff();
500
+ return snapshot;
501
+ }
385
502
  stopBackgroundTask(taskId) {
386
- return this.background.stop(taskId);
503
+ return this.background.stopAndWait(taskId);
504
+ }
505
+ outputBackgroundTask(taskId, options) {
506
+ return this.background.output(taskId, options);
507
+ }
508
+ sendBackgroundMessage(agentId, message, summary, toolUseId) {
509
+ return this.background.send(agentId, message, summary, toolUseId);
510
+ }
511
+ stopAllBackgroundTasks() {
512
+ return this.background.stopAll();
387
513
  }
388
514
  cwd() {
389
515
  return this.options.cwdProvider?.() ?? this.options.cwd;
@@ -398,7 +524,7 @@ export class ClaudeSubagentExecutor {
398
524
  ? definition.model
399
525
  : undefined;
400
526
  const model = environmentModel || input.model || configuredModel;
401
- const parentPermissionMode = this.options.parentPermissionMode?.();
527
+ const parentPermissionMode = this.options.parentPermissionMode?.() ?? 'default';
402
528
  const definitionCanOverridePermissionMode = ![
403
529
  'acceptEdits',
404
530
  'auto',
@@ -406,7 +532,7 @@ export class ClaudeSubagentExecutor {
406
532
  ].includes(parentPermissionMode ?? 'default');
407
533
  const permissionMode = input.permissionMode ??
408
534
  (definitionCanOverridePermissionMode
409
- ? definition?.permissionMode
535
+ ? (definition?.permissionMode ?? parentPermissionMode)
410
536
  : parentPermissionMode);
411
537
  const isolation = input.isolation ?? definition?.isolation;
412
538
  return {
@@ -417,8 +543,8 @@ export class ClaudeSubagentExecutor {
417
543
  runInBackground: input.runInBackground || definition?.background === true,
418
544
  };
419
545
  }
420
- registry(sessionId, depth, promptIdForCall) {
421
- return new ClaudeSubagentToolRegistry(this.options.baseTools, this, sessionId, depth, promptIdForCall);
546
+ registry(sessionId, depth, promptIdForCall, parentAgentId) {
547
+ return new ClaudeSubagentToolRegistry(this.options.baseTools, this, sessionId, depth, promptIdForCall, parentAgentId);
422
548
  }
423
549
  definitions() {
424
550
  return {
@@ -558,7 +684,9 @@ export class ClaudeSubagentExecutor {
558
684
  if (input.model && !this.options.providerForModel) {
559
685
  throw new Error('Agent model overrides are unavailable for this provider');
560
686
  }
561
- if (input.permissionMode && !this.options.permissionResolverForMode) {
687
+ if (input.permissionMode &&
688
+ input.permissionMode !== 'default' &&
689
+ !this.options.permissionResolverForMode) {
562
690
  throw new Error('Agent permission mode overrides are unavailable');
563
691
  }
564
692
  return {
@@ -576,7 +704,7 @@ export class ClaudeSubagentExecutor {
576
704
  },
577
705
  };
578
706
  }
579
- async execute(call, sessionId, depth, promptId, context) {
707
+ async execute(call, sessionId, depth, promptId, context, parentAgentId) {
580
708
  this.calls += 1;
581
709
  const maxCalls = this.options.maxCalls ?? DEFAULT_MAX_CALLS;
582
710
  if (this.calls > maxCalls) {
@@ -585,11 +713,12 @@ export class ClaudeSubagentExecutor {
585
713
  const input = this.resolveAgentInput(parseAgentInput(call));
586
714
  const spawnDepth = depth + 1;
587
715
  const agentId = `a${randomBytes(8).toString('hex')}`;
588
- const paths = this.sessionPaths(sessionId);
716
+ const parentCwd = context.cwd;
717
+ const paths = this.sessionPaths(sessionId, parentCwd);
589
718
  const initialIsolation = input.isolation
590
- ? await this.createAgentWorktree(paths.praxisRoot, sessionId, agentId)
719
+ ? await this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd)
591
720
  : undefined;
592
- const agentCwd = initialIsolation?.cwd ?? this.cwd();
721
+ const agentCwd = initialIsolation?.cwd ?? parentCwd;
593
722
  const sidechainPaths = resolveClaudeSidechainPaths(paths.projectRoot, sessionId, agentId);
594
723
  const sidechain = this.sidechainStore(sessionId, agentId, sidechainPaths, paths.praxisRoot);
595
724
  const root = createClaudeSidechainRoot({
@@ -614,6 +743,8 @@ export class ClaudeSubagentExecutor {
614
743
  ? { permissionMode: input.permissionMode }
615
744
  : {}),
616
745
  ...(input.isolation ? { isolation: input.isolation } : {}),
746
+ ...(parentAgentId ? { parentAgentId } : {}),
747
+ ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
617
748
  });
618
749
  }
619
750
  catch (error) {
@@ -623,10 +754,15 @@ export class ClaudeSubagentExecutor {
623
754
  const provider = input.model
624
755
  ? (this.options.providerForModel?.(input.model) ?? this.options.provider)
625
756
  : this.options.provider;
626
- const run = this.createBackgroundAgentRun({
757
+ const lifecycleStore = this.options.persistence === 'memory'
758
+ ? undefined
759
+ : new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
760
+ const backgroundRun = this.createBackgroundAgentRun({
627
761
  input,
762
+ parentCwd,
763
+ ...(lifecycleStore ? { lifecycle: lifecycleStore } : {}),
628
764
  ...(initialIsolation ? { initialIsolation } : {}),
629
- createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId),
765
+ createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
630
766
  execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
631
767
  lease,
632
768
  root,
@@ -643,48 +779,82 @@ export class ClaudeSubagentExecutor {
643
779
  cwd,
644
780
  })),
645
781
  });
646
- if (input.runInBackground) {
647
- const resolvedModel = provider.model ?? 'praxis/provider';
648
- this.background.launch({
782
+ const resolvedModel = provider.model ?? 'praxis/provider';
783
+ const spec = {
784
+ agentId,
785
+ ...(input.name ? { name: input.name } : {}),
786
+ agentType: input.subagentType,
787
+ description: input.description,
788
+ prompt: input.prompt,
789
+ toolUseId: call.id,
790
+ outputFile: sidechainPaths.transcriptFile,
791
+ resolvedModel,
792
+ run: backgroundRun.run,
793
+ markBackground: backgroundRun.markBackground,
794
+ ...(lifecycleStore
795
+ ? {
796
+ acknowledgeNotification: (notificationId) => lifecycleStore.acknowledgeNotification(notificationId),
797
+ prepareNotificationDetached: (notificationId, model) => lifecycleStore.prepareNotificationDetached(notificationId, model),
798
+ confirmNotificationDetached: (notificationId) => lifecycleStore.confirmNotificationDetached(notificationId),
799
+ }
800
+ : {}),
801
+ };
802
+ const asyncResult = () => ({
803
+ content: this.asyncLaunchResult({
649
804
  agentId,
650
- ...(input.name ? { name: input.name } : {}),
651
- agentType: input.subagentType,
652
805
  description: input.description,
653
- prompt: input.prompt,
654
- toolUseId: call.id,
655
806
  outputFile: sidechainPaths.transcriptFile,
656
- resolvedModel,
657
- run,
658
- });
659
- return {
660
- content: this.asyncLaunchResult({
807
+ ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
808
+ }),
809
+ isError: false,
810
+ nativeToolUseResult: {
811
+ ...createClaudeAsyncAgentToolUseResult({
812
+ prompt: input.prompt,
661
813
  agentId,
662
814
  description: input.description,
815
+ resolvedModel,
663
816
  outputFile: sidechainPaths.transcriptFile,
664
- ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
665
817
  }),
666
- isError: false,
667
- nativeToolUseResult: {
668
- ...createClaudeAsyncAgentToolUseResult({
669
- prompt: input.prompt,
670
- agentId,
671
- description: input.description,
672
- resolvedModel,
673
- outputFile: sidechainPaths.transcriptFile,
674
- }),
675
- ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
676
- },
677
- };
818
+ ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
819
+ },
820
+ });
821
+ if (input.runInBackground) {
822
+ this.background.launch(spec);
823
+ return asyncResult();
678
824
  }
679
825
  const controller = new AbortController();
680
- const abort = () => controller.abort();
826
+ const abort = () => controller.abort(context.signal?.reason);
681
827
  context.signal?.addEventListener('abort', abort, { once: true });
828
+ if (context.signal?.aborted)
829
+ abort();
830
+ const operation = backgroundRun.run(input.prompt, controller.signal, false, call.id);
831
+ let resolveHandoff;
832
+ const handoff = new Promise((resolve) => {
833
+ resolveHandoff = resolve;
834
+ });
835
+ const foreground = {
836
+ spec,
837
+ controller,
838
+ operation,
839
+ startedAt: Date.now(),
840
+ detachParentSignal: () => context.signal?.removeEventListener('abort', abort),
841
+ resolveHandoff,
842
+ backgrounded: false,
843
+ };
844
+ this.foreground.set(agentId, foreground);
682
845
  let result;
683
846
  try {
684
- result = await run(input.prompt, controller.signal, false);
847
+ const outcome = await Promise.race([
848
+ operation.then((value) => ({ kind: 'completed', value })),
849
+ handoff.then(() => ({ kind: 'backgrounded' })),
850
+ ]);
851
+ if (outcome.kind === 'backgrounded')
852
+ return asyncResult();
853
+ result = outcome.value;
685
854
  }
686
855
  finally {
687
- context.signal?.removeEventListener('abort', abort);
856
+ foreground.detachParentSignal();
857
+ this.foreground.delete(agentId);
688
858
  }
689
859
  const maxOutputBytes = this.options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
690
860
  if (Buffer.byteLength(result.text) > maxOutputBytes) {
@@ -721,7 +891,7 @@ export class ClaudeSubagentExecutor {
721
891
  agentId,
722
892
  agentType: input.subagentType,
723
893
  text: result.text,
724
- resolvedModel: provider.model ?? 'praxis/provider',
894
+ resolvedModel,
725
895
  durationMs: result.durationMs,
726
896
  usage: result.usage,
727
897
  toolUseCount: result.toolUseCount,
@@ -750,23 +920,23 @@ export class ClaudeSubagentExecutor {
750
920
  }
751
921
  return store;
752
922
  }
753
- sessionPaths(sessionId) {
923
+ sessionPaths(sessionId, cwd = this.cwd()) {
754
924
  return this.options.dataPlane === 'native'
755
925
  ? resolveDataPlanePaths({
756
926
  dataPlane: 'native',
757
927
  root: this.options.configRoot,
758
- cwd: this.cwd(),
928
+ cwd,
759
929
  sessionId,
760
930
  })
761
931
  : resolveClaudePaths({
762
932
  configDir: this.options.configRoot,
763
- cwd: this.cwd(),
933
+ cwd,
764
934
  sessionId,
765
935
  });
766
936
  }
767
- createAgentWorktree(praxisRoot, sessionId, agentId) {
937
+ createAgentWorktree(praxisRoot, sessionId, agentId, cwd = this.cwd()) {
768
938
  return createManagedWorktree({
769
- cwd: this.cwd(),
939
+ cwd,
770
940
  parentDirectory: join(praxisRoot, 'agent-worktrees'),
771
941
  directoryName: `${sessionId}-${agentId}`,
772
942
  label: 'Agent',
@@ -775,55 +945,184 @@ export class ClaudeSubagentExecutor {
775
945
  createBackgroundAgentRun(options) {
776
946
  let availableIsolation = options.initialIsolation;
777
947
  let retainedIsolation;
778
- return async (message, signal, continuation) => {
779
- const isolation = options.input.isolation
780
- ? (retainedIsolation ??
781
- availableIsolation ??
782
- (await options.createIsolation()))
783
- : undefined;
784
- availableIsolation = undefined;
948
+ let notificationExpected = options.input.runInBackground;
949
+ const run = async (message, signal, continuation, currentToolUseId) => {
950
+ await options.lifecycle?.write('running');
785
951
  const startedAt = Date.now();
952
+ let isolation;
953
+ try {
954
+ isolation = options.input.isolation
955
+ ? (retainedIsolation ??
956
+ availableIsolation ??
957
+ (await options.createIsolation()))
958
+ : undefined;
959
+ }
960
+ catch (error) {
961
+ const failureMessage = error instanceof Error ? error.message : String(error);
962
+ const failureResult = {
963
+ text: failureMessage,
964
+ usage: { inputTokens: 0, outputTokens: 0 },
965
+ toolUseCount: 0,
966
+ durationMs: Date.now() - startedAt,
967
+ };
968
+ const status = signal.aborted ? 'killed' : 'failed';
969
+ const notificationId = notificationExpected &&
970
+ !(signal.reason instanceof BackgroundAgentShutdownError)
971
+ ? randomUUID()
972
+ : undefined;
973
+ await options.lifecycle?.write(status, failureMessage, {
974
+ result: failureResult,
975
+ ...(notificationId
976
+ ? {
977
+ notification: {
978
+ id: notificationId,
979
+ status,
980
+ toolUseId: currentToolUseId,
981
+ error: failureMessage,
982
+ },
983
+ }
984
+ : {}),
985
+ });
986
+ if (!notificationExpected)
987
+ throw error;
988
+ throw new BackgroundAgentRunError(failureMessage, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, error);
989
+ }
990
+ availableIsolation = undefined;
786
991
  let result;
787
992
  let failure;
788
993
  let cleanup;
789
994
  try {
790
- result = await options.execute(isolation?.cwd ?? this.cwd(), message, signal, continuation);
995
+ result = await options.execute(isolation?.cwd ?? options.parentCwd, message, signal, continuation);
791
996
  }
792
997
  catch (error) {
793
998
  failure = error;
794
999
  }
795
1000
  finally {
796
- cleanup = await isolation?.cleanup();
1001
+ try {
1002
+ cleanup = await isolation?.cleanup();
1003
+ }
1004
+ catch (error) {
1005
+ failure ??= error;
1006
+ }
797
1007
  retainedIsolation = cleanup?.retained ? isolation : undefined;
798
1008
  }
799
1009
  if (failure !== undefined) {
800
- if (isolation && cleanup) {
801
- const message = failure instanceof Error ? failure.message : String(failure);
802
- const result = {
803
- text: message,
804
- usage: { inputTokens: 0, outputTokens: 0 },
805
- toolUseCount: 0,
806
- durationMs: Date.now() - startedAt,
807
- isolationPath: isolation.cwd,
808
- isolationRetained: cleanup.retained,
809
- ...(cleanup.reason ? { isolationWarning: cleanup.reason } : {}),
810
- };
811
- throw new BackgroundAgentRunError(cleanup.reason ? `${message}\n${cleanup.reason}` : message, result, failure);
812
- }
813
- if (cleanup?.reason) {
814
- throw new Error(`${failure instanceof Error ? failure.message : String(failure)}\n${cleanup.reason}`, { cause: failure });
1010
+ const underlyingFailure = failure instanceof SubagentExecutionFailure ? failure.cause : failure;
1011
+ const failureMessage = underlyingFailure instanceof Error
1012
+ ? underlyingFailure.message
1013
+ : String(underlyingFailure);
1014
+ const detail = cleanup?.reason
1015
+ ? `${failureMessage}\n${cleanup.reason}`
1016
+ : failureMessage;
1017
+ const partialResult = failure instanceof SubagentExecutionFailure
1018
+ ? failure.result
1019
+ : undefined;
1020
+ const failureResult = {
1021
+ ...partialResult,
1022
+ text: failureMessage,
1023
+ usage: partialResult?.usage ?? {
1024
+ inputTokens: 0,
1025
+ outputTokens: 0,
1026
+ },
1027
+ toolUseCount: partialResult?.toolUseCount ?? 0,
1028
+ durationMs: Date.now() - startedAt,
1029
+ ...(isolation ? { isolationPath: isolation.cwd } : {}),
1030
+ ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1031
+ ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1032
+ };
1033
+ const status = signal.aborted ? 'killed' : 'failed';
1034
+ const notificationId = notificationExpected &&
1035
+ !(signal.reason instanceof BackgroundAgentShutdownError)
1036
+ ? randomUUID()
1037
+ : undefined;
1038
+ await options.lifecycle?.write(status, detail, {
1039
+ result: failureResult,
1040
+ ...(notificationId
1041
+ ? {
1042
+ notification: {
1043
+ id: notificationId,
1044
+ status,
1045
+ toolUseId: currentToolUseId,
1046
+ error: detail,
1047
+ },
1048
+ }
1049
+ : {}),
1050
+ });
1051
+ if (!notificationExpected && !isolation && !cleanup?.reason) {
1052
+ throw underlyingFailure;
815
1053
  }
816
- throw failure;
1054
+ throw new BackgroundAgentRunError(detail, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, underlyingFailure);
817
1055
  }
818
- if (!result)
819
- throw new Error('Agent completed without a result');
820
- return {
1056
+ if (!result) {
1057
+ const error = new Error('Agent completed without a result');
1058
+ const failureResult = {
1059
+ text: error.message,
1060
+ usage: { inputTokens: 0, outputTokens: 0 },
1061
+ toolUseCount: 0,
1062
+ durationMs: Date.now() - startedAt,
1063
+ ...(isolation ? { isolationPath: isolation.cwd } : {}),
1064
+ ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1065
+ ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1066
+ };
1067
+ const notificationId = notificationExpected &&
1068
+ !(signal.reason instanceof BackgroundAgentShutdownError)
1069
+ ? randomUUID()
1070
+ : undefined;
1071
+ await options.lifecycle?.write('failed', error.message, {
1072
+ result: failureResult,
1073
+ ...(notificationId
1074
+ ? {
1075
+ notification: {
1076
+ id: notificationId,
1077
+ status: 'failed',
1078
+ toolUseId: currentToolUseId,
1079
+ error: error.message,
1080
+ },
1081
+ }
1082
+ : {}),
1083
+ });
1084
+ if (!notificationExpected)
1085
+ throw error;
1086
+ throw new BackgroundAgentRunError(error.message, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, error);
1087
+ }
1088
+ const finalResult = {
821
1089
  ...result,
822
1090
  durationMs: Date.now() - startedAt,
823
1091
  ...(isolation ? { isolationPath: isolation.cwd } : {}),
824
1092
  ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
825
1093
  ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
826
1094
  };
1095
+ const status = signal.aborted ? 'killed' : 'completed';
1096
+ const detail = signal.aborted
1097
+ ? 'Agent was aborted before terminal cleanup'
1098
+ : undefined;
1099
+ const notificationId = notificationExpected &&
1100
+ !(signal.reason instanceof BackgroundAgentShutdownError)
1101
+ ? randomUUID()
1102
+ : undefined;
1103
+ await options.lifecycle?.write(status, detail, {
1104
+ result: finalResult,
1105
+ ...(notificationId
1106
+ ? {
1107
+ notification: {
1108
+ id: notificationId,
1109
+ status,
1110
+ toolUseId: currentToolUseId,
1111
+ error: status === 'killed' ? (detail ?? null) : null,
1112
+ },
1113
+ }
1114
+ : {}),
1115
+ });
1116
+ return {
1117
+ ...finalResult,
1118
+ ...(notificationId ? { notificationId } : {}),
1119
+ };
1120
+ };
1121
+ return {
1122
+ run,
1123
+ markBackground: () => {
1124
+ notificationExpected = true;
1125
+ },
827
1126
  };
828
1127
  }
829
1128
  async runWorkflowAgent(options) {
@@ -976,10 +1275,17 @@ export class ClaudeSubagentExecutor {
976
1275
  input: { to, message, ...(summary === undefined ? {} : { summary }) },
977
1276
  };
978
1277
  }
979
- async executeManagement(call, sessionId) {
1278
+ async executeManagement(call, sessionId, context) {
980
1279
  if (call.name === 'TaskOutput') {
981
1280
  const taskId = String(call.input.task_id);
982
- await this.hydrateCompletedTask(sessionId, taskId);
1281
+ const owned = await this.options.outputOwnedBackgroundAgent?.(sessionId, taskId, {
1282
+ block: Boolean(call.input.block),
1283
+ timeout: Number(call.input.timeout),
1284
+ });
1285
+ if (owned !== undefined && owned !== null) {
1286
+ return { content: owned, isError: false };
1287
+ }
1288
+ await this.hydratePersistedTask(sessionId, taskId, context.cwd);
983
1289
  return {
984
1290
  content: await this.background.output(taskId, {
985
1291
  block: Boolean(call.input.block),
@@ -990,23 +1296,77 @@ export class ClaudeSubagentExecutor {
990
1296
  }
991
1297
  if (call.name === 'TaskStop') {
992
1298
  const taskId = String(call.input.task_id);
993
- await this.hydrateCompletedTask(sessionId, taskId);
994
- return { content: this.background.stop(taskId), isError: false };
1299
+ const owned = await this.options.stopOwnedBackgroundAgent?.(sessionId, taskId);
1300
+ if (owned !== undefined && owned !== null) {
1301
+ return { content: owned, isError: false };
1302
+ }
1303
+ await this.hydratePersistedTask(sessionId, taskId, context.cwd);
1304
+ return {
1305
+ content: await this.background.stopAndWait(taskId),
1306
+ isError: false,
1307
+ };
995
1308
  }
996
1309
  const agentId = String(call.input.to);
997
- await this.hydrateCompletedTask(sessionId, agentId);
1310
+ const summary = typeof call.input.summary === 'string' ? call.input.summary : undefined;
1311
+ const owned = this.options.sendOwnedBackgroundAgent?.(sessionId, agentId, String(call.input.message), summary, call.id);
1312
+ if (owned !== undefined && owned !== null) {
1313
+ return { content: owned, isError: false };
1314
+ }
1315
+ await this.hydratePersistedTask(sessionId, agentId, context.cwd);
998
1316
  return {
999
- content: this.background.send(agentId, String(call.input.message), typeof call.input.summary === 'string' ? call.input.summary : undefined, call.id),
1317
+ content: this.background.send(agentId, String(call.input.message), summary, call.id),
1000
1318
  isError: false,
1001
1319
  };
1002
1320
  }
1003
- notifications(waitForRunning) {
1004
- return this.background.notifications({ waitForRunning });
1321
+ notifications(waitForRunning, consume = true) {
1322
+ return this.background.notifications({ waitForRunning, consume });
1323
+ }
1324
+ acknowledgeNotifications(messages) {
1325
+ return this.background.acknowledge(messages);
1326
+ }
1327
+ prepareNotificationsDetached(messages) {
1328
+ return this.background.prepareNotificationsDetached(messages);
1329
+ }
1330
+ confirmNotificationsDetached(messages) {
1331
+ return this.background.confirmNotificationsDetached(messages);
1332
+ }
1333
+ reconcileDeliveredNotifications(delivered = this.options.notificationDelivered ?? (() => false)) {
1334
+ return this.background.acknowledgeDelivered(delivered);
1335
+ }
1336
+ reconcileDetachedNotifications(delivered) {
1337
+ return this.background.acknowledgeDeliveredAsDetached(delivered);
1338
+ }
1339
+ notificationClaimAgentIds() {
1340
+ return this.background.notificationClaimAgentIds();
1341
+ }
1342
+ async hydratePersistedTasks(sessionId, parentCwd, excludedAgentIds = new Set()) {
1343
+ if (this.options.persistence === 'memory')
1344
+ return;
1345
+ const paths = this.sessionPaths(sessionId, parentCwd);
1346
+ const candidates = await this.discoverSidechainCandidates(paths.projectRoot, sessionId);
1347
+ for (const agentId of new Set(candidates.map((candidate) => candidate.agentId))) {
1348
+ if (excludedAgentIds.has(agentId) || this.background.has(agentId)) {
1349
+ continue;
1350
+ }
1351
+ try {
1352
+ const lifecycle = await new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId).read();
1353
+ if (!lifecycle?.notifications?.some((notification) => !notification.consumed)) {
1354
+ continue;
1355
+ }
1356
+ await this.hydratePersistedTask(sessionId, agentId, parentCwd);
1357
+ }
1358
+ catch (error) {
1359
+ this.options.eventSink?.({
1360
+ type: 'warning',
1361
+ message: `Background agent ${agentId} could not be recovered automatically: ${error instanceof Error ? error.message : String(error)}`,
1362
+ });
1363
+ }
1364
+ }
1005
1365
  }
1006
- async hydrateCompletedTask(sessionId, identifier) {
1366
+ async hydratePersistedTask(sessionId, identifier, parentCwd) {
1007
1367
  if (this.background.has(identifier))
1008
1368
  return;
1009
- const paths = this.sessionPaths(sessionId);
1369
+ const paths = this.sessionPaths(sessionId, parentCwd);
1010
1370
  const sidechainPaths = await this.resolvePersistedSidechain(paths.projectRoot, sessionId, identifier);
1011
1371
  if (!sidechainPaths)
1012
1372
  return;
@@ -1040,10 +1400,14 @@ export class ClaudeSubagentExecutor {
1040
1400
  : '';
1041
1401
  const projected = projectClaudeModelMessages(snapshot.entries);
1042
1402
  const lastAssistant = projected.at(-1);
1043
- if (!lastAssistant ||
1044
- lastAssistant.role !== 'assistant' ||
1045
- (lastAssistant.toolCalls?.length ?? 0) > 0) {
1046
- throw new Error(`Background agent ${agentId} is not completed`);
1403
+ const completed = lastAssistant?.role === 'assistant' &&
1404
+ lastAssistant.content.trim().length > 0 &&
1405
+ (lastAssistant.toolCalls?.length ?? 0) === 0;
1406
+ const lifecycleStore = new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
1407
+ const lifecycle = await lifecycleStore.read();
1408
+ const lifecycleIsCurrent = lifecycle !== null && (await lifecycleStore.matchesTranscript(lifecycle));
1409
+ if (lifecycleIsCurrent && lifecycle.status === 'completed' && !completed) {
1410
+ throw new Error(`Background agent ${agentId} has completed lifecycle state but an incomplete sidechain`);
1047
1411
  }
1048
1412
  const agentType = metadata?.agentType ?? 'general-purpose';
1049
1413
  const description = metadata?.description ?? 'Recovered Claude sidechain';
@@ -1051,7 +1415,60 @@ export class ClaudeSubagentExecutor {
1051
1415
  const spawnDepth = metadata?.spawnDepth ?? 1;
1052
1416
  const name = metadata?.name;
1053
1417
  const permissionMode = metadata?.permissionMode;
1054
- const isolation = metadata?.isolation;
1418
+ let isolation = metadata?.isolation;
1419
+ let restoredIsolation;
1420
+ let restoredCwd = parentCwd;
1421
+ if (isolation === 'worktree') {
1422
+ if (metadata?.worktreePath === undefined) {
1423
+ this.options.eventSink?.({
1424
+ type: 'warning',
1425
+ message: `Background agent ${agentId} has no retained worktree path; falling back to parent cwd ${parentCwd}`,
1426
+ });
1427
+ isolation = undefined;
1428
+ }
1429
+ else {
1430
+ try {
1431
+ restoredIsolation = await restoreManagedWorktree({
1432
+ cwd: parentCwd,
1433
+ path: metadata.worktreePath,
1434
+ label: 'Agent',
1435
+ });
1436
+ }
1437
+ catch (error) {
1438
+ this.options.eventSink?.({
1439
+ type: 'warning',
1440
+ message: `Background agent ${agentId} could not restore its retained worktree; falling back to parent cwd ${parentCwd}: ${error instanceof Error ? error.message : String(error)}`,
1441
+ });
1442
+ isolation = undefined;
1443
+ }
1444
+ }
1445
+ }
1446
+ else {
1447
+ const persistedCwd = root.cwd;
1448
+ if (typeof persistedCwd === 'string' &&
1449
+ persistedCwd.length > 0 &&
1450
+ !persistedCwd.includes('\0') &&
1451
+ isAbsolute(persistedCwd)) {
1452
+ try {
1453
+ if (!(await lstat(persistedCwd)).isDirectory()) {
1454
+ throw new Error('persisted cwd is not a directory');
1455
+ }
1456
+ restoredCwd = persistedCwd;
1457
+ }
1458
+ catch (error) {
1459
+ this.options.eventSink?.({
1460
+ type: 'warning',
1461
+ message: `Background agent ${agentId} could not restore its persisted cwd; falling back to parent cwd ${parentCwd}: ${error instanceof Error ? error.message : String(error)}`,
1462
+ });
1463
+ }
1464
+ }
1465
+ else {
1466
+ this.options.eventSink?.({
1467
+ type: 'warning',
1468
+ message: `Background agent ${agentId} has an invalid persisted cwd; falling back to parent cwd ${parentCwd}`,
1469
+ });
1470
+ }
1471
+ }
1055
1472
  const input = {
1056
1473
  description,
1057
1474
  prompt,
@@ -1062,9 +1479,12 @@ export class ClaudeSubagentExecutor {
1062
1479
  runInBackground: true,
1063
1480
  };
1064
1481
  const provider = this.options.provider;
1065
- const run = this.createBackgroundAgentRun({
1482
+ const backgroundRun = this.createBackgroundAgentRun({
1066
1483
  input,
1067
- createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId),
1484
+ parentCwd: restoredCwd,
1485
+ lifecycle: lifecycleStore,
1486
+ ...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
1487
+ createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
1068
1488
  execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
1069
1489
  lease,
1070
1490
  root,
@@ -1081,7 +1501,7 @@ export class ClaudeSubagentExecutor {
1081
1501
  cwd,
1082
1502
  })),
1083
1503
  });
1084
- this.background.registerCompleted({
1504
+ const spec = {
1085
1505
  agentId,
1086
1506
  ...(name ? { name } : {}),
1087
1507
  agentType,
@@ -1090,16 +1510,57 @@ export class ClaudeSubagentExecutor {
1090
1510
  toolUseId,
1091
1511
  outputFile: sidechainPaths.transcriptFile,
1092
1512
  resolvedModel: provider.model ?? 'praxis/provider',
1093
- run,
1094
- }, {
1095
- text: lastAssistant.content,
1096
- usage: { inputTokens: 0, outputTokens: 0 },
1097
- toolUseCount: 0,
1098
- durationMs: 0,
1099
- });
1513
+ run: backgroundRun.run,
1514
+ markBackground: backgroundRun.markBackground,
1515
+ acknowledgeNotification: (notificationId) => lifecycleStore.acknowledgeNotification(notificationId),
1516
+ prepareNotificationDetached: (notificationId, model) => lifecycleStore.prepareNotificationDetached(notificationId, model),
1517
+ confirmNotificationDetached: (notificationId) => lifecycleStore.confirmNotificationDetached(notificationId),
1518
+ };
1519
+ if (lifecycleIsCurrent && lifecycle.status === 'failed') {
1520
+ this.background.registerTerminal(spec, 'failed', lifecycle.detail ?? 'Persisted agent failed');
1521
+ }
1522
+ else if (lifecycleIsCurrent && lifecycle.status === 'killed') {
1523
+ this.background.registerTerminal(spec, 'stopped', lifecycle.detail ?? 'Persisted agent was killed');
1524
+ }
1525
+ else if (completed) {
1526
+ this.background.registerCompleted(spec, lifecycleIsCurrent && lifecycle.result
1527
+ ? lifecycle.result
1528
+ : {
1529
+ text: lastAssistant.content,
1530
+ usage: { inputTokens: 0, outputTokens: 0 },
1531
+ toolUseCount: 0,
1532
+ durationMs: 0,
1533
+ });
1534
+ }
1535
+ else {
1536
+ this.background.registerInterrupted(spec);
1537
+ }
1538
+ for (const notification of lifecycle?.notifications ?? []) {
1539
+ if (notification.consumed)
1540
+ continue;
1541
+ if (this.options.notificationDelivered?.({
1542
+ agentId,
1543
+ toolUseId: notification.toolUseId,
1544
+ status: notification.status,
1545
+ })) {
1546
+ if (notification.accounting?.kind === 'detached' &&
1547
+ !notification.accounting.delivered) {
1548
+ await lifecycleStore.confirmNotificationDetached(notification.id);
1549
+ }
1550
+ await lifecycleStore.acknowledgeNotification(notification.id);
1551
+ continue;
1552
+ }
1553
+ this.background.registerPersistedNotification(agentId, {
1554
+ id: notification.id,
1555
+ status: notification.status === 'killed' ? 'stopped' : notification.status,
1556
+ result: notification.result,
1557
+ error: notification.error,
1558
+ toolUseId: notification.toolUseId,
1559
+ });
1560
+ }
1100
1561
  }
1101
1562
  async resolvePersistedSidechain(projectRoot, sessionId, identifier) {
1102
- const isAgentId = /^a[0-9a-f]{16}$/u.test(identifier);
1563
+ const isAgentId = isClaudeAgentId(identifier);
1103
1564
  const isName = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(identifier);
1104
1565
  if (!isAgentId && !isName)
1105
1566
  return null;
@@ -1107,6 +1568,9 @@ export class ClaudeSubagentExecutor {
1107
1568
  const matches = candidates.filter((candidate) => isAgentId
1108
1569
  ? candidate.agentId === identifier
1109
1570
  : candidate.name === identifier);
1571
+ if (matches.length > 1) {
1572
+ throw new Error(`Ambiguous persisted background agent ${identifier}: ${matches.length} sidechains match`);
1573
+ }
1110
1574
  matches.sort((left, right) => (right.metadataModifiedAt ?? -Infinity) -
1111
1575
  (left.metadataModifiedAt ?? -Infinity) ||
1112
1576
  left.agentId.localeCompare(right.agentId) ||
@@ -1151,11 +1615,9 @@ export class ClaudeSubagentExecutor {
1151
1615
  }
1152
1616
  if (!entry.isFile())
1153
1617
  return;
1154
- const metadataMatch = /^agent-(a[0-9a-f]{16})\.meta\.json$/u.exec(entry.name);
1155
- if (metadataMatch) {
1156
- const agentId = metadataMatch[1];
1157
- if (agentId === undefined)
1158
- return;
1618
+ const metadataAgentId = sidechainAgentIdFromFile(entry.name, '.meta.json');
1619
+ if (metadataAgentId !== undefined) {
1620
+ const agentId = metadataAgentId;
1159
1621
  const transcriptEntry = byName.get(`agent-${agentId}.jsonl`);
1160
1622
  if (!transcriptEntry ||
1161
1623
  transcriptEntry.isSymbolicLink() ||
@@ -1183,12 +1645,10 @@ export class ClaudeSubagentExecutor {
1183
1645
  });
1184
1646
  return;
1185
1647
  }
1186
- const transcriptMatch = /^agent-(a[0-9a-f]{16})\.jsonl$/u.exec(entry.name);
1187
- if (!transcriptMatch)
1188
- return;
1189
- const agentId = transcriptMatch[1];
1190
- if (agentId === undefined)
1648
+ const transcriptAgentId = sidechainAgentIdFromFile(entry.name, '.jsonl');
1649
+ if (transcriptAgentId === undefined)
1191
1650
  return;
1651
+ const agentId = transcriptAgentId;
1192
1652
  // A metadata companion (even an invalid one) is owned by the
1193
1653
  // metadata branch; only a bare transcript without metadata is a
1194
1654
  // legacy candidate.
@@ -1218,7 +1678,7 @@ export class ClaudeSubagentExecutor {
1218
1678
  }
1219
1679
  async runSidechain(options) {
1220
1680
  const cwd = options.cwd ?? this.cwd();
1221
- const permissions = options.input.permissionMode
1681
+ const permissions = options.input.permissionMode && options.input.permissionMode !== 'default'
1222
1682
  ? this.options.permissionResolverForMode?.(options.input.permissionMode)
1223
1683
  : this.options.permissions;
1224
1684
  if (!permissions) {
@@ -1226,6 +1686,10 @@ export class ClaudeSubagentExecutor {
1226
1686
  }
1227
1687
  let snapshot = await options.lease.load();
1228
1688
  if (options.continuationMessage !== undefined) {
1689
+ // Validate ambiguous tool linkage before the append-only sidechain is
1690
+ // mutated. The projection is rebuilt after appending the one explicit
1691
+ // continuation prompt so that prompt is visible to the provider.
1692
+ projectClaudeSidechainContinuationMessages(snapshot.entries);
1229
1693
  const [entry] = translateProviderEvents([
1230
1694
  {
1231
1695
  type: 'user-text-block',
@@ -1253,6 +1717,12 @@ export class ClaudeSubagentExecutor {
1253
1717
  }
1254
1718
  let toolUseCount = 0;
1255
1719
  let taskTokenTotal = 0;
1720
+ let recordedUsage = { inputTokens: 0, outputTokens: 0 };
1721
+ let pendingProviderUsage;
1722
+ const recordedModelUsage = new Map();
1723
+ let recordedDurationApiMs = 0;
1724
+ let recordedDurationApiWithoutRetriesMs = 0;
1725
+ let recordedApiDurationSeen = false;
1256
1726
  let lastToolName;
1257
1727
  const taskStartedAt = Date.now();
1258
1728
  const append = async (entry) => {
@@ -1286,7 +1756,48 @@ export class ClaudeSubagentExecutor {
1286
1756
  const scopedHooks = scopedHookSettings.length > 0
1287
1757
  ? this.options.hooks?.withAdditionalSettings(scopedHookSettings)
1288
1758
  : this.options.hooks;
1289
- const nestedTools = this.registry(String(options.root.sessionId), options.spawnDepth, () => options.promptId);
1759
+ const commitProviderUsage = () => {
1760
+ if (!pendingProviderUsage)
1761
+ return;
1762
+ recordedUsage = mergeSubagentUsage(recordedUsage, pendingProviderUsage);
1763
+ const model = options.provider.model?.trim();
1764
+ if (model) {
1765
+ mergeSubagentModelUsage(recordedModelUsage, {
1766
+ [model]: {
1767
+ ...pendingProviderUsage,
1768
+ ...(options.provider.capabilities.contextWindowTokens === undefined
1769
+ ? {}
1770
+ : {
1771
+ contextWindow: options.provider.capabilities.contextWindowTokens,
1772
+ }),
1773
+ ...(options.provider.capabilities.maxOutputTokens === undefined
1774
+ ? {}
1775
+ : {
1776
+ maxOutputTokens: options.provider.capabilities.maxOutputTokens,
1777
+ }),
1778
+ },
1779
+ });
1780
+ }
1781
+ pendingProviderUsage = undefined;
1782
+ };
1783
+ const recordApiDuration = (options) => {
1784
+ if (options.durationApiMs === undefined &&
1785
+ options.durationApiWithoutRetriesMs === undefined) {
1786
+ return;
1787
+ }
1788
+ const total = options.durationApiMs ?? 0;
1789
+ const withoutRetries = options.durationApiWithoutRetriesMs ?? total;
1790
+ if (!Number.isFinite(total) ||
1791
+ total < 0 ||
1792
+ !Number.isFinite(withoutRetries) ||
1793
+ withoutRetries < 0) {
1794
+ throw new Error('Subagent API duration must be finite and nonnegative');
1795
+ }
1796
+ recordedDurationApiMs += total;
1797
+ recordedDurationApiWithoutRetriesMs += withoutRetries;
1798
+ recordedApiDurationSeen = true;
1799
+ };
1800
+ const nestedTools = this.registry(String(options.root.sessionId), options.spawnDepth, () => options.promptId, options.agentId);
1290
1801
  const agentMcp = customAgent?.mcpServers?.length
1291
1802
  ? await this.options.mcp?.connectAgent?.({
1292
1803
  specs: customAgent.mcpServers,
@@ -1330,6 +1841,7 @@ export class ClaudeSubagentExecutor {
1330
1841
  : permissions;
1331
1842
  const emit = (event) => {
1332
1843
  if (event.type === 'usage') {
1844
+ pendingProviderUsage = event.usage;
1333
1845
  taskTokenTotal += event.usage.inputTokens + event.usage.outputTokens;
1334
1846
  this.options.eventSink?.({
1335
1847
  type: 'task-progress',
@@ -1381,6 +1893,7 @@ export class ClaudeSubagentExecutor {
1381
1893
  });
1382
1894
  const observer = {
1383
1895
  assistantCompleted: async (message) => {
1896
+ commitProviderUsage();
1384
1897
  const [entry] = translateProviderEvents([
1385
1898
  {
1386
1899
  type: 'assistant-message',
@@ -1398,6 +1911,13 @@ export class ClaudeSubagentExecutor {
1398
1911
  await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
1399
1912
  },
1400
1913
  toolCompleted: async (call, result) => {
1914
+ if (result.usage) {
1915
+ recordedUsage = mergeSubagentUsage(recordedUsage, result.usage);
1916
+ }
1917
+ if (result.isError === false && result.modelUsage) {
1918
+ mergeSubagentModelUsage(recordedModelUsage, result.modelUsage);
1919
+ }
1920
+ recordApiDuration(result);
1401
1921
  if (result.isError === false) {
1402
1922
  const linesAdded = result.linesAdded ?? 0;
1403
1923
  const linesRemoved = result.linesRemoved ?? 0;
@@ -1441,6 +1961,7 @@ export class ClaudeSubagentExecutor {
1441
1961
  if (!entry)
1442
1962
  throw new Error('Could not translate subagent follow-up');
1443
1963
  await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
1964
+ await this.background.acknowledge([text]);
1444
1965
  }
1445
1966
  },
1446
1967
  };
@@ -1508,7 +2029,9 @@ export class ClaudeSubagentExecutor {
1508
2029
  ...(composedSubagentPolicy
1509
2030
  ? []
1510
2031
  : [{ role: 'system', content: system }]),
1511
- ...injectFirstUserMessageContext(projectClaudeModelMessages(snapshot.entries), assembledContext?.firstUserMessageContext),
2032
+ ...injectFirstUserMessageContext(options.continuationMessage === undefined
2033
+ ? projectClaudeModelMessages(snapshot.entries)
2034
+ : projectClaudeSidechainContinuationMessages(snapshot.entries), assembledContext?.firstUserMessageContext),
1512
2035
  ...preloadedSkills,
1513
2036
  ];
1514
2037
  observedMessages = messages;
@@ -1555,10 +2078,26 @@ export class ClaudeSubagentExecutor {
1555
2078
  messages.push(`SubagentStop hook error: ${outcome.blockedReason}`);
1556
2079
  }
1557
2080
  }
2081
+ await this.hydratePersistedTasks(String(options.root.sessionId), options.cwd ?? this.cwd());
2082
+ await this.reconcileDeliveredNotifications((notification) => {
2083
+ const markers = backgroundAgentNotificationMarkers(notification);
2084
+ return snapshot.entries.some((entry) => {
2085
+ if (entry.type !== 'user')
2086
+ return false;
2087
+ const source = JSON.stringify(entry.message);
2088
+ return markers.every((marker) => source.includes(marker));
2089
+ });
2090
+ });
1558
2091
  const background = await this.background.notifications({
1559
2092
  waitForRunning: false,
1560
2093
  excludeAgentId: options.agentId,
2094
+ consume: false,
1561
2095
  });
2096
+ recordedUsage = mergeSubagentUsage(recordedUsage, background.usage);
2097
+ if (background.modelUsage) {
2098
+ mergeSubagentModelUsage(recordedModelUsage, background.modelUsage);
2099
+ }
2100
+ recordApiDuration(background);
1562
2101
  messages.push(...background.messages);
1563
2102
  messages.push(...((await this.options.backgroundTaskNotifications?.(true)) ?? []));
1564
2103
  return {
@@ -1614,20 +2153,38 @@ export class ClaudeSubagentExecutor {
1614
2153
  };
1615
2154
  }
1616
2155
  catch (error) {
1617
- this.options.eventSink?.({
1618
- type: 'task-notification',
1619
- taskId: options.agentId,
1620
- ...(options.toolUseId ? { toolUseId: options.toolUseId } : {}),
1621
- status: options.signal?.aborted ? 'stopped' : 'failed',
1622
- outputFile: options.transcriptPath,
1623
- summary: error instanceof Error ? error.message : String(error),
1624
- usage: {
1625
- totalTokens: 0,
1626
- toolUses: toolUseCount,
1627
- durationMs: Date.now() - taskStartedAt,
1628
- },
2156
+ commitProviderUsage();
2157
+ const partialModelUsage = recordedModelUsage.size === 0
2158
+ ? undefined
2159
+ : Object.fromEntries(recordedModelUsage);
2160
+ if (!(options.signal?.reason instanceof BackgroundAgentShutdownError)) {
2161
+ this.options.eventSink?.({
2162
+ type: 'task-notification',
2163
+ taskId: options.agentId,
2164
+ ...(options.toolUseId ? { toolUseId: options.toolUseId } : {}),
2165
+ status: options.signal?.aborted ? 'stopped' : 'failed',
2166
+ outputFile: options.transcriptPath,
2167
+ summary: error instanceof Error ? error.message : String(error),
2168
+ usage: {
2169
+ totalTokens: recordedUsage.inputTokens + recordedUsage.outputTokens,
2170
+ toolUses: toolUseCount,
2171
+ durationMs: Date.now() - taskStartedAt,
2172
+ },
2173
+ });
2174
+ }
2175
+ throw new SubagentExecutionFailure(error, {
2176
+ text: error instanceof Error ? error.message : String(error),
2177
+ usage: recordedUsage,
2178
+ ...(partialModelUsage ? { modelUsage: partialModelUsage } : {}),
2179
+ ...(recordedApiDurationSeen
2180
+ ? {
2181
+ durationApiMs: recordedDurationApiMs,
2182
+ durationApiWithoutRetriesMs: recordedDurationApiWithoutRetriesMs,
2183
+ }
2184
+ : {}),
2185
+ toolUseCount,
2186
+ durationMs: Date.now() - taskStartedAt,
1629
2187
  });
1630
- throw error;
1631
2188
  }
1632
2189
  finally {
1633
2190
  await agentMcp?.close().catch((error) => {
@@ -1645,12 +2202,14 @@ class ClaudeSubagentToolRegistry {
1645
2202
  sessionId;
1646
2203
  depth;
1647
2204
  promptIdForCall;
1648
- constructor(base, executor, sessionId, depth, promptIdForCall) {
2205
+ parentAgentId;
2206
+ constructor(base, executor, sessionId, depth, promptIdForCall, parentAgentId) {
1649
2207
  this.base = base;
1650
2208
  this.executor = executor;
1651
2209
  this.sessionId = sessionId;
1652
2210
  this.depth = depth;
1653
2211
  this.promptIdForCall = promptIdForCall;
2212
+ this.parentAgentId = parentAgentId;
1654
2213
  }
1655
2214
  definitions() {
1656
2215
  const base = this.base.definitions();
@@ -1698,21 +2257,21 @@ class ClaudeSubagentToolRegistry {
1698
2257
  }
1699
2258
  async execute(call, context) {
1700
2259
  if (call.name === 'SendMessage') {
1701
- return this.executor.executeManagement(call, this.sessionId);
2260
+ return this.executor.executeManagement(call, this.sessionId, context);
1702
2261
  }
1703
2262
  if (call.name === 'TaskOutput' || call.name === 'TaskStop') {
1704
2263
  const taskId = call.input.task_id ?? call.input.shell_id;
1705
2264
  if (typeof taskId === 'string' && isBackgroundBashTaskId(taskId)) {
1706
2265
  return this.base.execute(call, context);
1707
2266
  }
1708
- return this.executor.executeManagement(call, this.sessionId);
2267
+ return this.executor.executeManagement(call, this.sessionId, context);
1709
2268
  }
1710
2269
  if (call.name !== 'Agent')
1711
2270
  return this.base.execute(call, context);
1712
2271
  const promptId = this.promptIdForCall(call.id);
1713
2272
  if (!promptId)
1714
2273
  throw new Error(`Could not locate prompt for Agent ${call.id}`);
1715
- return this.executor.execute(call, this.sessionId, this.depth, promptId, context);
2274
+ return this.executor.execute(call, this.sessionId, this.depth, promptId, context, this.parentAgentId);
1716
2275
  }
1717
2276
  }
1718
2277
  //# sourceMappingURL=subagent-service.js.map