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