praxis-agent 0.35.0 → 0.37.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 (111) hide show
  1. package/README.md +20 -2
  2. package/dist/application/background-agent-manager.d.ts +16 -6
  3. package/dist/application/background-agent-manager.js +139 -97
  4. package/dist/application/context-engine.d.ts +56 -0
  5. package/dist/application/context-engine.js +129 -0
  6. package/dist/application/durable-follow-up.d.ts +14 -0
  7. package/dist/application/durable-follow-up.js +52 -0
  8. package/dist/application/native-session-transcript.d.ts +72 -0
  9. package/dist/application/native-session-transcript.js +351 -0
  10. package/dist/application/native-sidechain-transcript.d.ts +34 -0
  11. package/dist/application/native-sidechain-transcript.js +156 -0
  12. package/dist/application/session-service.d.ts +23 -4
  13. package/dist/application/session-service.js +1130 -494
  14. package/dist/application/subagent-service.d.ts +5 -1
  15. package/dist/application/subagent-service.js +749 -320
  16. package/dist/application/team-agent-runtime.d.ts +29 -0
  17. package/dist/application/team-agent-runtime.js +169 -0
  18. package/dist/application/team-capability.d.ts +27 -0
  19. package/dist/application/team-capability.js +41 -0
  20. package/dist/application/team-lead-operations.d.ts +38 -0
  21. package/dist/application/team-lead-operations.js +155 -0
  22. package/dist/application/team-mailbox.d.ts +54 -0
  23. package/dist/application/team-mailbox.js +196 -0
  24. package/dist/application/team-manager.d.ts +113 -0
  25. package/dist/application/team-manager.js +681 -0
  26. package/dist/application/team-workspace.d.ts +29 -0
  27. package/dist/application/team-workspace.js +197 -0
  28. package/dist/application/top-level-agent-manager.d.ts +1 -0
  29. package/dist/application/top-level-agent-manager.js +1025 -403
  30. package/dist/application/transcript-projection.d.ts +35 -0
  31. package/dist/application/transcript-projection.js +139 -0
  32. package/dist/application/turn-lifecycle.d.ts +2 -0
  33. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  34. package/dist/application/turn-memory-coordinator.js +98 -0
  35. package/dist/cli/agents-dashboard.js +10 -5
  36. package/dist/cli/interactive.d.ts +7 -0
  37. package/dist/cli/interactive.js +645 -334
  38. package/dist/cli/tui/claude-style.d.ts +8 -25
  39. package/dist/cli/tui/claude-style.js +299 -277
  40. package/dist/cli/tui/decision-surface-model.d.ts +71 -0
  41. package/dist/cli/tui/decision-surface-model.js +131 -0
  42. package/dist/cli/tui/decision-surface.d.ts +7 -0
  43. package/dist/cli/tui/decision-surface.js +41 -0
  44. package/dist/cli/tui/help-surface-model.d.ts +68 -0
  45. package/dist/cli/tui/help-surface-model.js +131 -0
  46. package/dist/cli/tui/mcp-panel.js +1 -2
  47. package/dist/cli/tui/permission-surface-model.d.ts +195 -0
  48. package/dist/cli/tui/permission-surface-model.js +360 -0
  49. package/dist/cli/tui/permission-surface.d.ts +7 -0
  50. package/dist/cli/tui/permission-surface.js +69 -0
  51. package/dist/cli/tui/presentation-environment.d.ts +32 -0
  52. package/dist/cli/tui/presentation-environment.js +149 -0
  53. package/dist/cli/tui/task-panel.js +10 -1
  54. package/dist/cli/tui/theme.d.ts +46 -3
  55. package/dist/cli/tui/theme.js +414 -56
  56. package/dist/cli/tui/tool-permission.js +16 -8
  57. package/dist/cli/tui/transcript-presentation.d.ts +19 -1
  58. package/dist/cli/tui/transcript-presentation.js +26 -6
  59. package/dist/cli/tui/transcript-viewport.d.ts +18 -0
  60. package/dist/cli/tui/transcript-viewport.js +517 -111
  61. package/dist/cli/tui/transcript-window-model.d.ts +108 -0
  62. package/dist/cli/tui/transcript-window-model.js +772 -0
  63. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  64. package/dist/cli/tui/tui-interaction-router.js +127 -0
  65. package/dist/cli/tui/tui-screen-model.d.ts +114 -0
  66. package/dist/cli/tui/tui-screen-model.js +210 -0
  67. package/dist/cli/tui/tui-view-model.d.ts +2 -49
  68. package/dist/cli/tui/tui-view-model.js +1 -68
  69. package/dist/cli-runtime.d.ts +4 -2
  70. package/dist/cli-runtime.js +397 -32
  71. package/dist/compatibility/claude/transcript-codec.js +6 -0
  72. package/dist/core/agent-orchestration.d.ts +31 -0
  73. package/dist/core/agent-orchestration.js +235 -0
  74. package/dist/core/permission-policy.d.ts +4 -0
  75. package/dist/core/permission-policy.js +21 -0
  76. package/dist/core/runtime.d.ts +6 -0
  77. package/dist/core/runtime.js +20 -2
  78. package/dist/core/team-mailbox.d.ts +54 -0
  79. package/dist/core/team-mailbox.js +212 -0
  80. package/dist/core/team-ownership.d.ts +84 -0
  81. package/dist/core/team-ownership.js +505 -0
  82. package/dist/core/tool-execution-scheduler.js +6 -1
  83. package/dist/core/transcript-event.d.ts +5 -1
  84. package/dist/core/transcript-event.js +12 -0
  85. package/dist/persistence/claude-job-store.d.ts +58 -0
  86. package/dist/persistence/claude-job-store.js +483 -1
  87. package/dist/persistence/claude-transcript-store.d.ts +2 -1
  88. package/dist/persistence/claude-transcript-store.js +2 -10
  89. package/dist/persistence/native-team-mailbox.d.ts +68 -0
  90. package/dist/persistence/native-team-mailbox.js +542 -0
  91. package/dist/persistence/native-team-store.d.ts +34 -0
  92. package/dist/persistence/native-team-store.js +257 -0
  93. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  94. package/dist/persistence/native-transcript-reader.js +283 -0
  95. package/dist/persistence/native-transcript-store.d.ts +69 -0
  96. package/dist/persistence/native-transcript-store.js +321 -0
  97. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  98. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  99. package/dist/persistence/transcript-file-append.d.ts +2 -0
  100. package/dist/persistence/transcript-file-append.js +10 -0
  101. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  102. package/dist/platform/exclusive-file-lease.js +3 -0
  103. package/dist/tools/claude-capabilities.d.ts +4 -0
  104. package/dist/tools/claude-capabilities.js +18 -2
  105. package/dist/tools/team-lead-tools.d.ts +19 -0
  106. package/dist/tools/team-lead-tools.js +479 -0
  107. package/dist/tools/team-member-tools.d.ts +21 -0
  108. package/dist/tools/team-member-tools.js +259 -0
  109. package/package.json +1 -1
  110. package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
  111. package/dist/cli/tui/fullscreen-renderer.js +0 -13
@@ -12,17 +12,20 @@ import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/
12
12
  import { assembleContextSnapshot } from '../core/prompt-composer.js';
13
13
  import { ContextBudget } from '../core/context-budget.js';
14
14
  import { AgentRuntime, } from '../core/runtime.js';
15
+ import { composePermissionResolvers } from '../core/permission-policy.js';
15
16
  import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
16
17
  import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.js';
17
18
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
18
19
  import { ClaudeSidechainStore } from '../persistence/claude-sidechain-store.js';
19
20
  import { InMemorySidechainStore } from '../persistence/in-memory-sidechain-store.js';
20
- import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
21
+ import { SubagentLifecycleStore, } from '../persistence/subagent-lifecycle-store.js';
21
22
  import { resolveDataPlanePaths, } from '../persistence/data-plane.js';
22
23
  import { BackgroundAgentManager, BackgroundAgentRunError, BackgroundAgentShutdownError, backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
23
24
  import { isBackgroundBashTaskId } from './background-task-id.js';
24
25
  import { createManagedWorktree, restoreManagedWorktree, } from './managed-worktree.js';
25
26
  import { createWorkflowWorktree } from './workflow-worktree.js';
27
+ import { NativeSidechainTranscript, } from './native-sidechain-transcript.js';
28
+ import { DurableFollowUpTracker, } from './durable-follow-up.js';
26
29
  const DEFAULT_MAX_DEPTH = 4;
27
30
  const DEFAULT_MAX_CALLS = 16;
28
31
  const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
@@ -128,6 +131,48 @@ async function readSidechainMetadataName(metadataFile) {
128
131
  return undefined;
129
132
  }
130
133
  }
134
+ function projectNativeSidechainContinuationMessages(messages) {
135
+ const callCounts = new Map();
136
+ const resultCounts = new Map();
137
+ for (const message of messages) {
138
+ if (message.role === 'assistant') {
139
+ for (const call of message.toolCalls ?? [])
140
+ callCounts.set(call.id, (callCounts.get(call.id) ?? 0) + 1);
141
+ }
142
+ else if (message.role === 'tool') {
143
+ resultCounts.set(message.toolCallId, (resultCounts.get(message.toolCallId) ?? 0) + 1);
144
+ }
145
+ }
146
+ for (const [callId, count] of callCounts) {
147
+ if (count > 1)
148
+ throw new Error(`Native sidechain continuation has duplicate tool ID ${callId}`);
149
+ }
150
+ for (const [callId, count] of resultCounts) {
151
+ if (count > 1)
152
+ throw new Error(`Native sidechain continuation has duplicate tool result ${callId}`);
153
+ }
154
+ const resolved = new Set([...callCounts.keys()].filter((callId) => resultCounts.get(callId) === 1));
155
+ return messages.flatMap((message) => {
156
+ if (message.role === 'assistant') {
157
+ const toolCalls = (message.toolCalls ?? []).filter((call) => resolved.has(call.id));
158
+ if (message.content.trim() === '' && toolCalls.length === 0)
159
+ return [];
160
+ return [
161
+ {
162
+ role: 'assistant',
163
+ content: message.content,
164
+ ...(message.thinkingBlocks
165
+ ? { thinkingBlocks: message.thinkingBlocks }
166
+ : {}),
167
+ ...(toolCalls.length === 0 ? {} : { toolCalls }),
168
+ },
169
+ ];
170
+ }
171
+ if (message.role === 'tool' && !resolved.has(message.toolCallId))
172
+ return [];
173
+ return [message];
174
+ });
175
+ }
131
176
  export class StructuredOutputRegistry {
132
177
  base;
133
178
  schema;
@@ -459,8 +504,19 @@ export class ClaudeSubagentExecutor {
459
504
  }
460
505
  constructor(options) {
461
506
  this.options = options;
507
+ if (options.experimentalNativeTranscriptWrites === true &&
508
+ (options.dataPlane !== 'native' || options.persistence !== 'disk')) {
509
+ throw new Error('experimental native transcript writes require native dataPlane and disk persistence');
510
+ }
511
+ if (options.experimentalNativeTranscriptWrites === true &&
512
+ options.hooks !== undefined) {
513
+ throw new Error('experimental native sidechain writes do not support Claude hooks');
514
+ }
462
515
  this.schema = selectClaudeSchemaAdapter(options.claudeVersion);
463
516
  }
517
+ nativeSidechainWritesEnabled() {
518
+ return this.options.experimentalNativeTranscriptWrites === true;
519
+ }
464
520
  async close() {
465
521
  for (const agentId of [...this.foreground.keys()]) {
466
522
  this.backgroundForegroundTask(agentId);
@@ -716,25 +772,77 @@ export class ClaudeSubagentExecutor {
716
772
  const agentId = `a${randomBytes(8).toString('hex')}`;
717
773
  const parentCwd = context.cwd;
718
774
  const paths = this.sessionPaths(sessionId, parentCwd);
719
- const initialIsolation = input.isolation
720
- ? await this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd)
775
+ const sidechainPaths = resolveClaudeSidechainPaths(paths.projectRoot, sessionId, agentId);
776
+ const lifecycleStore = this.options.persistence === 'memory'
777
+ ? undefined
778
+ : new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
779
+ const initialExecution = lifecycleStore
780
+ ? await lifecycleStore.start()
721
781
  : undefined;
782
+ let initialIsolation;
783
+ const settleInitialSetupFailure = async (primary, isolation) => {
784
+ const secondary = [];
785
+ const message = primary instanceof Error ? primary.message : String(primary);
786
+ try {
787
+ await initialExecution?.finish('failed', {
788
+ text: message,
789
+ usage: { inputTokens: 0, outputTokens: 0 },
790
+ toolUseCount: 0,
791
+ durationMs: 0,
792
+ }, message);
793
+ }
794
+ catch (error) {
795
+ secondary.push(error);
796
+ }
797
+ try {
798
+ await initialExecution?.release();
799
+ }
800
+ catch (error) {
801
+ secondary.push(error);
802
+ }
803
+ try {
804
+ await isolation?.cleanup();
805
+ }
806
+ catch (error) {
807
+ secondary.push(error);
808
+ }
809
+ if (secondary.length > 0)
810
+ throw new AggregateError([primary, ...secondary], 'Background agent setup and cleanup failed');
811
+ throw primary;
812
+ };
813
+ try {
814
+ initialIsolation = input.isolation
815
+ ? await this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd)
816
+ : undefined;
817
+ }
818
+ catch (error) {
819
+ return settleInitialSetupFailure(error, initialIsolation);
820
+ }
722
821
  const agentCwd = initialIsolation?.cwd ?? parentCwd;
723
- const sidechainPaths = resolveClaudeSidechainPaths(paths.projectRoot, sessionId, agentId);
724
- const sidechain = this.sidechainStore(sessionId, agentId, sidechainPaths, paths.praxisRoot);
725
- const root = createClaudeSidechainRoot({
726
- sessionId,
727
- promptId,
728
- prompt: input.prompt,
729
- agentId,
730
- cwd: agentCwd,
731
- claudeVersion: this.options.claudeVersion,
732
- gitBranch: null,
733
- uuid: randomUUID(),
734
- timestamp: new Date().toISOString(),
735
- });
822
+ const nativeSidechain = this.nativeSidechainWritesEnabled()
823
+ ? new NativeSidechainTranscript({
824
+ ...sidechainPaths,
825
+ lockFile: join(paths.praxisRoot, 'locks', `${sessionId}-${agentId}.lock`),
826
+ })
827
+ : undefined;
828
+ const claudeSidechain = nativeSidechain
829
+ ? undefined
830
+ : this.sidechainStore(sessionId, agentId, sidechainPaths, paths.praxisRoot);
831
+ const root = nativeSidechain
832
+ ? undefined
833
+ : createClaudeSidechainRoot({
834
+ sessionId,
835
+ promptId,
836
+ prompt: input.prompt,
837
+ agentId,
838
+ cwd: agentCwd,
839
+ claudeVersion: this.options.claudeVersion,
840
+ gitBranch: null,
841
+ uuid: randomUUID(),
842
+ timestamp: new Date().toISOString(),
843
+ });
736
844
  try {
737
- await sidechain.create(root, {
845
+ const metadata = {
738
846
  agentType: input.subagentType,
739
847
  description: input.description,
740
848
  toolUseId: call.id,
@@ -746,39 +854,55 @@ export class ClaudeSubagentExecutor {
746
854
  ...(input.isolation ? { isolation: input.isolation } : {}),
747
855
  ...(parentAgentId ? { parentAgentId } : {}),
748
856
  ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
749
- });
857
+ };
858
+ if (nativeSidechain) {
859
+ await nativeSidechain.create(input.prompt, {
860
+ ...metadata,
861
+ cwd: agentCwd,
862
+ promptId,
863
+ });
864
+ }
865
+ else {
866
+ if (!claudeSidechain || !root)
867
+ throw new Error('Claude sidechain initialization is unavailable');
868
+ await claudeSidechain.create(root, metadata);
869
+ }
750
870
  }
751
871
  catch (error) {
752
- await initialIsolation?.cleanup();
753
- throw error;
872
+ return settleInitialSetupFailure(error, initialIsolation);
754
873
  }
755
874
  const provider = input.model
756
875
  ? (this.options.providerForModel?.(input.model) ?? this.options.provider)
757
876
  : this.options.provider;
758
- const lifecycleStore = this.options.persistence === 'memory'
759
- ? undefined
760
- : new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
761
877
  const backgroundRun = this.createBackgroundAgentRun({
762
878
  input,
763
879
  parentCwd,
764
880
  ...(lifecycleStore ? { lifecycle: lifecycleStore } : {}),
881
+ ...(initialExecution ? { initialExecution } : {}),
765
882
  ...(initialIsolation ? { initialIsolation } : {}),
766
883
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
767
- execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
768
- lease,
769
- root,
770
- input,
771
- provider,
772
- agentId,
773
- spawnDepth,
774
- promptId,
775
- toolUseId: call.id,
776
- transcriptPath: sidechainPaths.transcriptFile,
777
- toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
778
- ...(continuation ? { continuationMessage: message } : {}),
779
- signal,
780
- cwd,
781
- })),
884
+ execute: async (cwd, message, signal, continuation) => {
885
+ const run = (lease) => this.runSidechain({
886
+ ...lease,
887
+ sessionId,
888
+ input,
889
+ provider,
890
+ agentId,
891
+ spawnDepth,
892
+ promptId,
893
+ toolUseId: call.id,
894
+ transcriptPath: sidechainPaths.transcriptFile,
895
+ toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
896
+ ...(continuation ? { continuationMessage: message } : {}),
897
+ signal,
898
+ cwd,
899
+ });
900
+ if (nativeSidechain)
901
+ return nativeSidechain.withLease((nativeLease) => run({ nativeLease }));
902
+ if (!claudeSidechain)
903
+ throw new Error('Claude sidechain execution is unavailable');
904
+ return claudeSidechain.withLease((claudeLease) => run({ claudeLease }));
905
+ },
782
906
  });
783
907
  const resolvedModel = provider.model ?? 'praxis/provider';
784
908
  const spec = {
@@ -790,6 +914,7 @@ export class ClaudeSubagentExecutor {
790
914
  toolUseId: call.id,
791
915
  outputFile: sidechainPaths.transcriptFile,
792
916
  resolvedModel,
917
+ lifecycle: backgroundRun.lifecycle,
793
918
  run: backgroundRun.run,
794
919
  markBackground: backgroundRun.markBackground,
795
920
  ...(lifecycleStore
@@ -820,7 +945,18 @@ export class ClaudeSubagentExecutor {
820
945
  },
821
946
  });
822
947
  if (input.runInBackground) {
823
- this.background.launch(spec);
948
+ try {
949
+ this.background.launch(spec);
950
+ }
951
+ catch (error) {
952
+ try {
953
+ await backgroundRun.disposeBeforeStart();
954
+ }
955
+ catch (cleanupError) {
956
+ throw new AggregateError([error, cleanupError], 'Background agent launch and pre-start cleanup failed');
957
+ }
958
+ throw error;
959
+ }
824
960
  return asyncResult();
825
961
  }
826
962
  const controller = new AbortController();
@@ -947,183 +1083,288 @@ export class ClaudeSubagentExecutor {
947
1083
  let availableIsolation = options.initialIsolation;
948
1084
  let retainedIsolation;
949
1085
  let notificationExpected = options.input.runInBackground;
1086
+ let recoverNext = options.recover ?? false;
1087
+ let pendingExecution = options.initialExecution;
1088
+ let disposed = false;
1089
+ let lifecycleState = options.initialExecution?.snapshot.state ??
1090
+ options.initialLifecycleState ??
1091
+ 'queued';
1092
+ const lifecycleListeners = new Set();
1093
+ const publishLifecycle = (state) => {
1094
+ lifecycleState = state;
1095
+ if (state === 'orphaned')
1096
+ recoverNext = true;
1097
+ for (const listener of lifecycleListeners)
1098
+ listener(state);
1099
+ };
1100
+ const lifecycle = {
1101
+ current: () => lifecycleState,
1102
+ subscribe(listener) {
1103
+ lifecycleListeners.add(listener);
1104
+ return () => lifecycleListeners.delete(listener);
1105
+ },
1106
+ };
1107
+ const disposeBeforeStart = async () => {
1108
+ if (disposed)
1109
+ return;
1110
+ disposed = true;
1111
+ const execution = pendingExecution;
1112
+ pendingExecution = undefined;
1113
+ const isolation = availableIsolation;
1114
+ availableIsolation = undefined;
1115
+ const errors = [];
1116
+ if (execution) {
1117
+ try {
1118
+ const snapshot = await execution.finish('failed', {
1119
+ text: 'Background agent launch was rejected before execution started',
1120
+ usage: { inputTokens: 0, outputTokens: 0 },
1121
+ toolUseCount: 0,
1122
+ durationMs: 0,
1123
+ }, 'Background agent launch was rejected before execution started');
1124
+ publishLifecycle(snapshot.state);
1125
+ }
1126
+ catch (error) {
1127
+ errors.push(error);
1128
+ }
1129
+ finally {
1130
+ try {
1131
+ await execution.release();
1132
+ publishLifecycle(execution.snapshot.state);
1133
+ }
1134
+ catch (error) {
1135
+ errors.push(error);
1136
+ }
1137
+ }
1138
+ }
1139
+ if (isolation) {
1140
+ try {
1141
+ await isolation.cleanup();
1142
+ }
1143
+ catch (error) {
1144
+ errors.push(error);
1145
+ }
1146
+ }
1147
+ if (errors.length > 0) {
1148
+ throw new AggregateError(errors, 'Background agent pre-start cleanup failed');
1149
+ }
1150
+ };
950
1151
  const run = async (message, signal, continuation, currentToolUseId) => {
951
- await options.lifecycle?.write('running');
1152
+ let execution;
1153
+ let cancellation;
1154
+ let lifecycleTerminalized = false;
1155
+ let onAbort;
952
1156
  const startedAt = Date.now();
953
1157
  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;
992
1158
  let result;
993
1159
  let failure;
994
1160
  let cleanup;
1161
+ const finishLifecycle = async (state, result, detail, notification) => {
1162
+ if (execution) {
1163
+ const snapshot = await execution.finish(state, result, detail, notification);
1164
+ publishLifecycle(snapshot.state);
1165
+ }
1166
+ else
1167
+ publishLifecycle(state);
1168
+ };
995
1169
  try {
996
- result = await options.execute(isolation?.cwd ?? options.parentCwd, message, signal, continuation);
997
- }
998
- catch (error) {
999
- failure = error;
1000
- }
1001
- finally {
1170
+ if (pendingExecution) {
1171
+ execution = pendingExecution;
1172
+ pendingExecution = undefined;
1173
+ publishLifecycle(execution.snapshot.state);
1174
+ }
1175
+ else if (options.lifecycle) {
1176
+ execution = await options.lifecycle.acquire(recoverNext ? 'recover' : continuation ? 'continue' : 'start');
1177
+ publishLifecycle(execution.snapshot.state);
1178
+ recoverNext = false;
1179
+ }
1180
+ if (execution) {
1181
+ const snapshot = await execution.running();
1182
+ publishLifecycle(snapshot.state);
1183
+ }
1184
+ else {
1185
+ publishLifecycle('running');
1186
+ }
1187
+ onAbort = () => {
1188
+ if (cancellation)
1189
+ return;
1190
+ if (!execution) {
1191
+ cancellation = Promise.resolve(publishLifecycle('cancelling'));
1192
+ return;
1193
+ }
1194
+ const activeExecution = execution;
1195
+ const transition = activeExecution.beginCancellation();
1196
+ transition.catch(() => undefined);
1197
+ const observedCancellation = transition
1198
+ .then((snapshot) => publishLifecycle(snapshot.state))
1199
+ .finally(() => publishLifecycle(activeExecution.snapshot.state));
1200
+ observedCancellation.catch(() => undefined);
1201
+ cancellation = observedCancellation;
1202
+ };
1203
+ signal.addEventListener('abort', onAbort, { once: true });
1204
+ if (signal.aborted)
1205
+ onAbort();
1002
1206
  try {
1003
- cleanup = await isolation?.cleanup();
1207
+ isolation = options.input.isolation
1208
+ ? (retainedIsolation ??
1209
+ availableIsolation ??
1210
+ (await options.createIsolation()))
1211
+ : undefined;
1212
+ availableIsolation = undefined;
1213
+ result = await options.execute(isolation?.cwd ?? options.parentCwd, message, signal, continuation);
1004
1214
  }
1005
1215
  catch (error) {
1006
- failure ??= error;
1216
+ failure = error;
1007
1217
  }
1008
- retainedIsolation = cleanup?.retained ? isolation : undefined;
1009
- }
1010
- if (failure !== undefined) {
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
1218
+ finally {
1219
+ try {
1220
+ cleanup = await isolation?.cleanup();
1221
+ }
1222
+ catch (error) {
1223
+ failure ??= error;
1224
+ }
1225
+ retainedIsolation = cleanup?.retained ? isolation : undefined;
1226
+ }
1227
+ if (failure !== undefined) {
1228
+ const underlyingFailure = failure instanceof SubagentExecutionFailure
1229
+ ? failure.cause
1230
+ : failure;
1231
+ const failureMessage = underlyingFailure instanceof Error
1232
+ ? underlyingFailure.message
1233
+ : String(underlyingFailure);
1234
+ const detail = cleanup?.reason
1235
+ ? `${failureMessage}\n${cleanup.reason}`
1236
+ : failureMessage;
1237
+ const partialResult = failure instanceof SubagentExecutionFailure
1238
+ ? failure.result
1239
+ : undefined;
1240
+ const failureResult = {
1241
+ ...partialResult,
1242
+ text: failureMessage,
1243
+ usage: partialResult?.usage ?? {
1244
+ inputTokens: 0,
1245
+ outputTokens: 0,
1246
+ },
1247
+ toolUseCount: partialResult?.toolUseCount ?? 0,
1248
+ durationMs: Date.now() - startedAt,
1249
+ ...(isolation ? { isolationPath: isolation.cwd } : {}),
1250
+ ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1251
+ ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1252
+ };
1253
+ await cancellation;
1254
+ const status = signal.aborted ? 'killed' : 'failed';
1255
+ const notificationId = notificationExpected &&
1256
+ !(signal.reason instanceof BackgroundAgentShutdownError)
1257
+ ? randomUUID()
1258
+ : undefined;
1259
+ await finishLifecycle(signal.aborted ? 'cancelled' : 'failed', failureResult, detail, notificationId
1042
1260
  ? {
1043
- notification: {
1044
- id: notificationId,
1045
- status,
1046
- toolUseId: currentToolUseId,
1047
- error: detail,
1048
- },
1261
+ id: notificationId,
1262
+ status,
1263
+ toolUseId: currentToolUseId,
1264
+ error: detail,
1049
1265
  }
1050
- : {}),
1051
- });
1052
- if (!notificationExpected && !isolation && !cleanup?.reason) {
1053
- throw underlyingFailure;
1266
+ : undefined);
1267
+ lifecycleTerminalized = true;
1268
+ if (!notificationExpected && !isolation && !cleanup?.reason) {
1269
+ throw underlyingFailure;
1270
+ }
1271
+ throw new BackgroundAgentRunError(detail, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, underlyingFailure);
1054
1272
  }
1055
- throw new BackgroundAgentRunError(detail, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, underlyingFailure);
1056
- }
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,
1273
+ if (!result) {
1274
+ const error = new Error('Agent completed without a result');
1275
+ const failureResult = {
1276
+ text: error.message,
1277
+ usage: { inputTokens: 0, outputTokens: 0 },
1278
+ toolUseCount: 0,
1279
+ durationMs: Date.now() - startedAt,
1280
+ ...(isolation ? { isolationPath: isolation.cwd } : {}),
1281
+ ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1282
+ ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1283
+ };
1284
+ const notificationId = notificationExpected &&
1285
+ !(signal.reason instanceof BackgroundAgentShutdownError)
1286
+ ? randomUUID()
1287
+ : undefined;
1288
+ await cancellation;
1289
+ await finishLifecycle(signal.aborted ? 'cancelled' : 'failed', failureResult, error.message, notificationId
1290
+ ? {
1291
+ id: notificationId,
1292
+ status: signal.aborted ? 'killed' : 'failed',
1293
+ toolUseId: currentToolUseId,
1294
+ error: error.message,
1295
+ }
1296
+ : undefined);
1297
+ lifecycleTerminalized = true;
1298
+ if (!notificationExpected)
1299
+ throw error;
1300
+ throw new BackgroundAgentRunError(error.message, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, error);
1301
+ }
1302
+ const finalResult = {
1303
+ ...result,
1063
1304
  durationMs: Date.now() - startedAt,
1064
1305
  ...(isolation ? { isolationPath: isolation.cwd } : {}),
1065
1306
  ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1066
1307
  ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1067
1308
  };
1309
+ await cancellation;
1310
+ const status = signal.aborted ? 'killed' : 'completed';
1311
+ const detail = signal.aborted
1312
+ ? 'Agent was aborted before terminal cleanup'
1313
+ : undefined;
1068
1314
  const notificationId = notificationExpected &&
1069
1315
  !(signal.reason instanceof BackgroundAgentShutdownError)
1070
1316
  ? randomUUID()
1071
1317
  : 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 = {
1090
- ...result,
1091
- durationMs: Date.now() - startedAt,
1092
- ...(isolation ? { isolationPath: isolation.cwd } : {}),
1093
- ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1094
- ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
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
1318
+ await finishLifecycle(signal.aborted ? 'cancelled' : 'completed', finalResult, detail, notificationId
1107
1319
  ? {
1108
- notification: {
1109
- id: notificationId,
1110
- status,
1111
- toolUseId: currentToolUseId,
1112
- error: status === 'killed' ? (detail ?? null) : null,
1113
- },
1320
+ id: notificationId,
1321
+ status,
1322
+ toolUseId: currentToolUseId,
1323
+ error: status === 'killed' ? (detail ?? null) : null,
1114
1324
  }
1115
- : {}),
1116
- });
1117
- return {
1118
- ...finalResult,
1119
- ...(notificationId ? { notificationId } : {}),
1120
- };
1325
+ : undefined);
1326
+ lifecycleTerminalized = true;
1327
+ return {
1328
+ ...finalResult,
1329
+ ...(notificationId ? { notificationId } : {}),
1330
+ };
1331
+ }
1332
+ finally {
1333
+ if (onAbort)
1334
+ signal.removeEventListener('abort', onAbort);
1335
+ if (!lifecycleTerminalized) {
1336
+ try {
1337
+ await execution?.release();
1338
+ }
1339
+ catch (releaseError) {
1340
+ this.options.eventSink?.({
1341
+ type: 'warning',
1342
+ message: `Background agent execution cleanup could not release its durable owner; durable owner reconciliation is required: ${releaseError instanceof Error ? releaseError.message : String(releaseError)}`,
1343
+ });
1344
+ }
1345
+ finally {
1346
+ if (execution)
1347
+ publishLifecycle(execution.snapshot.state);
1348
+ }
1349
+ }
1350
+ else {
1351
+ try {
1352
+ await execution?.release();
1353
+ }
1354
+ finally {
1355
+ if (execution)
1356
+ publishLifecycle(execution.snapshot.state);
1357
+ }
1358
+ }
1359
+ }
1121
1360
  };
1122
1361
  return {
1123
1362
  run,
1124
1363
  markBackground: () => {
1125
1364
  notificationExpected = true;
1126
1365
  },
1366
+ disposeBeforeStart,
1367
+ lifecycle,
1127
1368
  };
1128
1369
  }
1129
1370
  async runWorkflowAgent(options) {
@@ -1158,7 +1399,13 @@ export class ClaudeSubagentExecutor {
1158
1399
  metadataFile: agentFiles.metadataFile,
1159
1400
  };
1160
1401
  const sessionPaths = this.sessionPaths(options.sessionId);
1161
- const sidechain = new ClaudeSidechainStore(paths, join(sessionPaths.praxisRoot, 'locks', `${options.sessionId}-${options.runId}-${options.agentId}.lock`), this.schema);
1402
+ const lockFile = join(sessionPaths.praxisRoot, 'locks', `${options.sessionId}-${options.runId}-${options.agentId}.lock`);
1403
+ const nativeSidechain = this.nativeSidechainWritesEnabled()
1404
+ ? new NativeSidechainTranscript({ ...paths, lockFile })
1405
+ : undefined;
1406
+ const claudeSidechain = nativeSidechain
1407
+ ? undefined
1408
+ : new ClaudeSidechainStore(paths, lockFile, this.schema);
1162
1409
  const isolation = options.isolation
1163
1410
  ? await createWorkflowWorktree({
1164
1411
  cwd: this.cwd(),
@@ -1168,29 +1415,47 @@ export class ClaudeSubagentExecutor {
1168
1415
  })
1169
1416
  : null;
1170
1417
  const agentCwd = isolation?.cwd ?? this.cwd();
1171
- const root = createClaudeSidechainRoot({
1172
- sessionId: options.sessionId,
1173
- promptId: options.promptId,
1174
- prompt: options.prompt,
1175
- agentId: options.agentId,
1176
- cwd: agentCwd,
1177
- claudeVersion: this.options.claudeVersion,
1178
- gitBranch: null,
1179
- uuid: randomUUID(),
1180
- timestamp: new Date().toISOString(),
1181
- });
1418
+ const root = nativeSidechain
1419
+ ? undefined
1420
+ : createClaudeSidechainRoot({
1421
+ sessionId: options.sessionId,
1422
+ promptId: options.promptId,
1423
+ prompt: options.prompt,
1424
+ agentId: options.agentId,
1425
+ cwd: agentCwd,
1426
+ claudeVersion: this.options.claudeVersion,
1427
+ gitBranch: null,
1428
+ uuid: randomUUID(),
1429
+ timestamp: new Date().toISOString(),
1430
+ });
1182
1431
  const startedAt = Date.now();
1183
1432
  const structured = { calls: 0, value: undefined };
1184
1433
  let run;
1185
1434
  let cleanup;
1186
1435
  try {
1187
- await sidechain.createWorkflow(root, {
1188
- agentType: options.agentType ?? 'workflow-subagent',
1189
- spawnDepth: 1,
1190
- });
1191
- run = await sidechain.withLease((lease) => this.runSidechain({
1192
- lease,
1193
- root,
1436
+ if (nativeSidechain) {
1437
+ await nativeSidechain.create(options.prompt, {
1438
+ agentType: options.agentType ?? 'workflow-subagent',
1439
+ description: input.description,
1440
+ toolUseId: `workflow:${options.runId}`,
1441
+ spawnDepth: 1,
1442
+ cwd: agentCwd,
1443
+ promptId: options.promptId,
1444
+ ...(options.isolation ? { isolation: options.isolation } : {}),
1445
+ ...(isolation ? { worktreePath: isolation.cwd } : {}),
1446
+ });
1447
+ }
1448
+ else {
1449
+ if (!claudeSidechain || !root)
1450
+ throw new Error('Claude workflow sidechain is unavailable');
1451
+ await claudeSidechain.createWorkflow(root, {
1452
+ agentType: options.agentType ?? 'workflow-subagent',
1453
+ spawnDepth: 1,
1454
+ });
1455
+ }
1456
+ const runWithLease = (lease) => this.runSidechain({
1457
+ ...lease,
1458
+ sessionId: options.sessionId,
1194
1459
  input,
1195
1460
  provider,
1196
1461
  agentId: options.agentId,
@@ -1204,7 +1469,15 @@ export class ClaudeSubagentExecutor {
1204
1469
  ...(options.effort ? { effort: options.effort } : {}),
1205
1470
  ...(options.signal ? { signal: options.signal } : {}),
1206
1471
  cwd: agentCwd,
1207
- }));
1472
+ });
1473
+ if (nativeSidechain) {
1474
+ run = await nativeSidechain.withLease((nativeLease) => runWithLease({ nativeLease }));
1475
+ }
1476
+ else {
1477
+ if (!claudeSidechain)
1478
+ throw new Error('Claude workflow sidechain is unavailable');
1479
+ run = await claudeSidechain.withLease((claudeLease) => runWithLease({ claudeLease }));
1480
+ }
1208
1481
  }
1209
1482
  finally {
1210
1483
  cleanup = await isolation?.cleanup();
@@ -1309,7 +1582,7 @@ export class ClaudeSubagentExecutor {
1309
1582
  }
1310
1583
  const agentId = String(call.input.to);
1311
1584
  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);
1585
+ const owned = await this.options.sendOwnedBackgroundAgent?.(sessionId, agentId, String(call.input.message), summary, call.id);
1313
1586
  if (owned !== undefined && owned !== null) {
1314
1587
  return { content: owned, isError: false };
1315
1588
  }
@@ -1372,40 +1645,74 @@ export class ClaudeSubagentExecutor {
1372
1645
  if (!sidechainPaths)
1373
1646
  return;
1374
1647
  const agentId = sidechainPaths.agentId;
1375
- const sidechain = new ClaudeSidechainStore(sidechainPaths, join(paths.praxisRoot, 'locks', `${sessionId}-${agentId}.lock`), this.schema);
1376
- let snapshot;
1648
+ const lockFile = join(paths.praxisRoot, 'locks', `${sessionId}-${agentId}.lock`);
1649
+ const nativeSidechain = this.nativeSidechainWritesEnabled()
1650
+ ? new NativeSidechainTranscript({ ...sidechainPaths, lockFile })
1651
+ : undefined;
1652
+ const claudeSidechain = nativeSidechain
1653
+ ? undefined
1654
+ : new ClaudeSidechainStore(sidechainPaths, lockFile, this.schema);
1655
+ let metadata = null;
1656
+ let root;
1657
+ let projected;
1377
1658
  try {
1378
- snapshot = await sidechain.loadReadOnly();
1659
+ if (nativeSidechain) {
1660
+ await nativeSidechain.loadReadOnly();
1661
+ metadata = await nativeSidechain.metadata();
1662
+ projected = await nativeSidechain.withLease(async (lease) => lease.activeMessages());
1663
+ }
1664
+ else {
1665
+ if (!claudeSidechain)
1666
+ throw new Error('Claude persisted sidechain is unavailable');
1667
+ const snapshot = await claudeSidechain.loadReadOnly();
1668
+ metadata = await claudeSidechain.metadata().catch((error) => {
1669
+ if (error.code === 'ENOENT')
1670
+ return null;
1671
+ throw error;
1672
+ });
1673
+ root = snapshot.entries[0];
1674
+ if (!root || root.type !== 'user') {
1675
+ throw new Error(`Background agent ${agentId} has no sidechain root`);
1676
+ }
1677
+ projected = projectClaudeModelMessages(snapshot.entries);
1678
+ }
1379
1679
  }
1380
1680
  catch (error) {
1381
1681
  if (error.code === 'ENOENT')
1382
1682
  return;
1383
1683
  throw error;
1384
1684
  }
1385
- let metadata = null;
1386
- try {
1387
- metadata = await sidechain.metadata();
1388
- }
1389
- catch (error) {
1390
- if (error.code !== 'ENOENT')
1391
- throw error;
1392
- }
1393
- const root = snapshot.entries[0];
1394
- if (!root || root.type !== 'user') {
1685
+ const rootMessage = projected[0];
1686
+ if (rootMessage?.role !== 'user' || rootMessage.content.trim() === '') {
1395
1687
  throw new Error(`Background agent ${agentId} has no sidechain root`);
1396
1688
  }
1397
- const prompt = typeof root.message === 'object' &&
1398
- root.message !== null &&
1399
- typeof root.message.content === 'string'
1400
- ? String(root.message.content)
1401
- : '';
1402
- const projected = projectClaudeModelMessages(snapshot.entries);
1689
+ const prompt = rootMessage.content;
1403
1690
  const lastAssistant = projected.at(-1);
1404
1691
  const completed = lastAssistant?.role === 'assistant' &&
1405
1692
  lastAssistant.content.trim().length > 0 &&
1406
1693
  (lastAssistant.toolCalls?.length ?? 0) === 0;
1407
1694
  const lifecycleStore = new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
1408
- const lifecycle = await lifecycleStore.read();
1695
+ const ownership = await lifecycleStore.reconcileOwnerLoss();
1696
+ if (ownership.owned)
1697
+ return;
1698
+ let lifecycle = await lifecycleStore.read();
1699
+ if (!lifecycle) {
1700
+ const seed = await lifecycleStore.start();
1701
+ await seed.running();
1702
+ if (completed) {
1703
+ await seed.finish('completed', {
1704
+ text: lastAssistant?.content ?? '',
1705
+ usage: { inputTokens: 0, outputTokens: 0 },
1706
+ toolUseCount: 0,
1707
+ durationMs: 0,
1708
+ });
1709
+ }
1710
+ await seed.release();
1711
+ lifecycle = await lifecycleStore.read();
1712
+ }
1713
+ if (!lifecycle) {
1714
+ throw new Error(`Unable to establish lifecycle state for background agent ${agentId}`);
1715
+ }
1409
1716
  const lifecycleIsCurrent = lifecycle !== null && (await lifecycleStore.matchesTranscript(lifecycle));
1410
1717
  if (lifecycleIsCurrent && lifecycle.status === 'completed' && !completed) {
1411
1718
  throw new Error(`Background agent ${agentId} has completed lifecycle state but an incomplete sidechain`);
@@ -1445,7 +1752,9 @@ export class ClaudeSubagentExecutor {
1445
1752
  }
1446
1753
  }
1447
1754
  else {
1448
- const persistedCwd = root.cwd;
1755
+ const persistedCwd = nativeSidechain
1756
+ ? metadata.cwd
1757
+ : root?.cwd;
1449
1758
  if (typeof persistedCwd === 'string' &&
1450
1759
  persistedCwd.length > 0 &&
1451
1760
  !persistedCwd.includes('\0') &&
@@ -1480,27 +1789,39 @@ export class ClaudeSubagentExecutor {
1480
1789
  runInBackground: true,
1481
1790
  };
1482
1791
  const provider = this.options.provider;
1792
+ const recoveredPromptId = nativeSidechain
1793
+ ? metadata.promptId
1794
+ : String(root?.promptId ?? randomUUID());
1483
1795
  const backgroundRun = this.createBackgroundAgentRun({
1484
1796
  input,
1485
1797
  parentCwd: restoredCwd,
1486
1798
  lifecycle: lifecycleStore,
1799
+ recover: lifecycle.lifecycle.state === 'orphaned',
1800
+ initialLifecycleState: lifecycle.lifecycle.state,
1487
1801
  ...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
1488
1802
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
1489
- execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
1490
- lease,
1491
- root,
1492
- input,
1493
- provider,
1494
- agentId,
1495
- spawnDepth,
1496
- promptId: String(root.promptId ?? randomUUID()),
1497
- toolUseId,
1498
- transcriptPath: sidechainPaths.transcriptFile,
1499
- toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
1500
- ...(continuation ? { continuationMessage: message } : {}),
1501
- signal,
1502
- cwd,
1503
- })),
1803
+ execute: async (cwd, message, signal, continuation) => {
1804
+ const run = (lease) => this.runSidechain({
1805
+ ...lease,
1806
+ sessionId,
1807
+ input,
1808
+ provider,
1809
+ agentId,
1810
+ spawnDepth,
1811
+ promptId: recoveredPromptId,
1812
+ toolUseId,
1813
+ transcriptPath: sidechainPaths.transcriptFile,
1814
+ toolResultDirectory: join(paths.projectRoot, sessionId, 'tool-results'),
1815
+ ...(continuation ? { continuationMessage: message } : {}),
1816
+ signal,
1817
+ cwd,
1818
+ });
1819
+ if (nativeSidechain)
1820
+ return nativeSidechain.withLease((nativeLease) => run({ nativeLease }));
1821
+ if (!claudeSidechain)
1822
+ throw new Error('Claude persisted sidechain is unavailable');
1823
+ return claudeSidechain.withLease((claudeLease) => run({ claudeLease }));
1824
+ },
1504
1825
  });
1505
1826
  const spec = {
1506
1827
  agentId,
@@ -1511,31 +1832,34 @@ export class ClaudeSubagentExecutor {
1511
1832
  toolUseId,
1512
1833
  outputFile: sidechainPaths.transcriptFile,
1513
1834
  resolvedModel: provider.model ?? 'praxis/provider',
1835
+ lifecycle: backgroundRun.lifecycle,
1514
1836
  run: backgroundRun.run,
1515
1837
  markBackground: backgroundRun.markBackground,
1516
1838
  acknowledgeNotification: (notificationId) => lifecycleStore.acknowledgeNotification(notificationId),
1517
1839
  prepareNotificationDetached: (notificationId, model) => lifecycleStore.prepareNotificationDetached(notificationId, model),
1518
1840
  confirmNotificationDetached: (notificationId) => lifecycleStore.confirmNotificationDetached(notificationId),
1519
1841
  };
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
- }
1842
+ this.background.registerPersisted(spec, {
1843
+ ...(lifecycleIsCurrent && lifecycle.result
1844
+ ? { result: lifecycle.result }
1845
+ : completed
1846
+ ? {
1847
+ result: {
1848
+ text: lastAssistant.content,
1849
+ usage: { inputTokens: 0, outputTokens: 0 },
1850
+ toolUseCount: 0,
1851
+ durationMs: 0,
1852
+ },
1853
+ }
1854
+ : {}),
1855
+ ...(lifecycleIsCurrent && lifecycle.status === 'failed'
1856
+ ? { error: lifecycle.detail ?? 'Persisted agent failed' }
1857
+ : lifecycleIsCurrent && lifecycle.status === 'killed'
1858
+ ? { error: lifecycle.detail ?? 'Persisted agent was killed' }
1859
+ : !completed && lifecycle.lifecycle.state === 'orphaned'
1860
+ ? { error: 'Persisted agent was interrupted before completion' }
1861
+ : {}),
1862
+ });
1539
1863
  for (const notification of lifecycle?.notifications ?? []) {
1540
1864
  if (notification.consumed)
1541
1865
  continue;
@@ -1553,7 +1877,7 @@ export class ClaudeSubagentExecutor {
1553
1877
  }
1554
1878
  this.background.registerPersistedNotification(agentId, {
1555
1879
  id: notification.id,
1556
- status: notification.status === 'killed' ? 'stopped' : notification.status,
1880
+ status: notification.status === 'killed' ? 'cancelled' : notification.status,
1557
1881
  result: notification.result,
1558
1882
  error: notification.error,
1559
1883
  toolUseId: notification.toolUseId,
@@ -1680,42 +2004,77 @@ export class ClaudeSubagentExecutor {
1680
2004
  async runSidechain(options) {
1681
2005
  const cwd = options.cwd ?? this.cwd();
1682
2006
  const permissions = options.input.permissionMode && options.input.permissionMode !== 'default'
1683
- ? this.options.permissionResolverForMode?.(options.input.permissionMode)
2007
+ ? (() => {
2008
+ const mode = options.input.permissionMode;
2009
+ if (!mode)
2010
+ return undefined;
2011
+ const child = this.options.permissionResolverForMode?.(mode);
2012
+ return child
2013
+ ? composePermissionResolvers(this.options.permissions, child)
2014
+ : undefined;
2015
+ })()
1684
2016
  : this.options.permissions;
1685
2017
  if (!permissions) {
1686
2018
  throw new Error('Agent permission mode overrides are unavailable');
1687
2019
  }
1688
- let snapshot = await options.lease.load();
2020
+ if ((options.claudeLease === undefined) ===
2021
+ (options.nativeLease === undefined))
2022
+ throw new Error('Exactly one sidechain transcript lease is required');
2023
+ const claudeLease = options.claudeLease;
2024
+ const nativeLease = options.nativeLease;
2025
+ let snapshot = claudeLease
2026
+ ? await claudeLease.load()
2027
+ : undefined;
2028
+ const requireClaudeSnapshot = () => {
2029
+ if (!snapshot)
2030
+ throw new Error('Claude sidechain snapshot is unavailable');
2031
+ return snapshot;
2032
+ };
2033
+ const nativeInterruption = nativeLease?.interruption();
2034
+ if (nativeInterruption?.kind === 'indeterminate-tools')
2035
+ throw new Error(`Native sidechain tool execution is indeterminate: ${nativeInterruption.callIds.join(', ')}`);
2036
+ const nativeRecoveryCalls = options.continuationMessage === undefined &&
2037
+ nativeInterruption?.kind === 'recoverable-tools'
2038
+ ? [...nativeInterruption.calls]
2039
+ : [];
1689
2040
  if (options.continuationMessage !== undefined) {
1690
2041
  // Validate ambiguous tool linkage before the append-only sidechain is
1691
2042
  // mutated. The projection is rebuilt after appending the one explicit
1692
2043
  // continuation prompt so that prompt is visible to the provider.
1693
- projectClaudeSidechainContinuationMessages(snapshot.entries);
1694
- const [entry] = translateProviderEvents([
1695
- {
1696
- type: 'user-text-block',
1697
- text: `The coordinator sent a message while you were working:\n${options.continuationMessage}\n\nAddress this before completing your current task.`,
1698
- },
1699
- ], {
1700
- sessionId: String(options.root.sessionId),
1701
- parentUuid: snapshot.tail.lastUuid,
1702
- cwd,
1703
- claudeVersion: this.options.claudeVersion,
1704
- gitBranch: null,
1705
- history: snapshot.entries,
1706
- });
1707
- if (!entry)
1708
- throw new Error('Could not translate subagent message');
1709
- const sidechainEntry = toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType);
1710
- const appendResult = await options.lease.append(snapshot.tail, sidechainEntry);
1711
- if (appendResult.status === 'conflict') {
1712
- throw new Error(`Claude sidechain append conflict: ${appendResult.reason}`);
2044
+ const continuation = `The coordinator sent a message while you were working:\n${options.continuationMessage}\n\nAddress this before completing your current task.`;
2045
+ if (nativeLease) {
2046
+ projectNativeSidechainContinuationMessages(nativeLease.activeMessages());
2047
+ await nativeLease.appendMessages({
2048
+ messages: [{ role: 'user', content: continuation }],
2049
+ });
2050
+ }
2051
+ else {
2052
+ const current = requireClaudeSnapshot();
2053
+ projectClaudeSidechainContinuationMessages(current.entries);
2054
+ const [entry] = translateProviderEvents([{ type: 'user-text-block', text: continuation }], {
2055
+ sessionId: options.sessionId,
2056
+ parentUuid: current.tail.lastUuid,
2057
+ cwd,
2058
+ claudeVersion: this.options.claudeVersion,
2059
+ gitBranch: null,
2060
+ history: current.entries,
2061
+ });
2062
+ if (!entry)
2063
+ throw new Error('Could not translate subagent message');
2064
+ const sidechainEntry = toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType);
2065
+ if (!claudeLease)
2066
+ throw new Error('Claude sidechain continuation is unavailable');
2067
+ const appendResult = await claudeLease.append(current.tail, sidechainEntry);
2068
+ if (appendResult.status === 'conflict') {
2069
+ throw new Error(`Claude sidechain append conflict: ${appendResult.reason}`);
2070
+ }
2071
+ snapshot = {
2072
+ entries: [...current.entries, sidechainEntry],
2073
+ tail: appendResult.tail,
2074
+ };
1713
2075
  }
1714
- snapshot = {
1715
- entries: [...snapshot.entries, sidechainEntry],
1716
- tail: appendResult.tail,
1717
- };
1718
2076
  }
2077
+ const durableFollowUps = new DurableFollowUpTracker();
1719
2078
  let toolUseCount = 0;
1720
2079
  let taskTokenTotal = 0;
1721
2080
  let recordedUsage = { inputTokens: 0, outputTokens: 0 };
@@ -1726,29 +2085,37 @@ export class ClaudeSubagentExecutor {
1726
2085
  let recordedApiDurationSeen = false;
1727
2086
  let lastToolName;
1728
2087
  const taskStartedAt = Date.now();
1729
- const append = async (entry) => {
1730
- const result = await options.lease.append(snapshot.tail, entry);
2088
+ const appendClaude = async (entry) => {
2089
+ if (!claudeLease)
2090
+ throw new Error('Claude sidechain append is unavailable');
2091
+ const current = requireClaudeSnapshot();
2092
+ const result = await claudeLease.append(current.tail, entry);
1731
2093
  if (result.status === 'conflict') {
1732
2094
  throw new Error(`Claude sidechain append conflict: ${result.reason}`);
1733
2095
  }
1734
- snapshot = { entries: [...snapshot.entries, entry], tail: result.tail };
2096
+ snapshot = { entries: [...current.entries, entry], tail: result.tail };
2097
+ };
2098
+ const translationContext = () => {
2099
+ const current = requireClaudeSnapshot();
2100
+ return {
2101
+ sessionId: options.sessionId,
2102
+ parentUuid: current.tail.lastUuid,
2103
+ cwd,
2104
+ claudeVersion: this.options.claudeVersion,
2105
+ gitBranch: null,
2106
+ history: current.entries,
2107
+ };
1735
2108
  };
1736
- const translationContext = () => ({
1737
- sessionId: String(options.root.sessionId),
1738
- parentUuid: snapshot.tail.lastUuid,
1739
- cwd,
1740
- claudeVersion: this.options.claudeVersion,
1741
- gitBranch: null,
1742
- history: snapshot.entries,
1743
- });
1744
2109
  const recordHookOutcome = async (outcome) => {
2110
+ if (nativeLease)
2111
+ throw new Error('Claude hooks are unavailable for native sidechains');
1745
2112
  for (const entry of createClaudeHookAttachmentEntries(outcome, translationContext())) {
1746
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2113
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
1747
2114
  }
1748
2115
  };
1749
2116
  const customAgent = this.agentDefinition(options.input);
1750
2117
  const hookSession = {
1751
- session_id: String(options.root.sessionId),
2118
+ session_id: options.sessionId,
1752
2119
  transcript_path: options.transcriptPath,
1753
2120
  cwd,
1754
2121
  permission_mode: options.input.permissionMode ?? 'default',
@@ -1798,7 +2165,7 @@ export class ClaudeSubagentExecutor {
1798
2165
  recordedDurationApiWithoutRetriesMs += withoutRetries;
1799
2166
  recordedApiDurationSeen = true;
1800
2167
  };
1801
- const nestedTools = this.registry(String(options.root.sessionId), options.spawnDepth, () => options.promptId, options.agentId);
2168
+ const nestedTools = this.registry(options.sessionId, options.spawnDepth, () => options.promptId, options.agentId);
1802
2169
  const agentMcp = customAgent?.mcpServers?.length
1803
2170
  ? await this.options.mcp?.connectAgent?.({
1804
2171
  specs: customAgent.mcpServers,
@@ -1893,8 +2260,29 @@ export class ClaudeSubagentExecutor {
1893
2260
  maxToolInputBytes: 1024 * 1024,
1894
2261
  });
1895
2262
  const observer = {
2263
+ toolExecutionStarted: async (call) => {
2264
+ await nativeLease?.beginToolExecution(call.id);
2265
+ },
1896
2266
  assistantCompleted: async (message) => {
1897
2267
  commitProviderUsage();
2268
+ if (nativeLease) {
2269
+ await nativeLease.appendMessages({
2270
+ messages: [
2271
+ {
2272
+ role: 'assistant',
2273
+ content: message.content,
2274
+ ...(message.thinkingBlocks
2275
+ ? { thinkingBlocks: message.thinkingBlocks }
2276
+ : {}),
2277
+ ...(message.toolCalls?.length
2278
+ ? { toolCalls: message.toolCalls }
2279
+ : {}),
2280
+ },
2281
+ ],
2282
+ model: options.provider.model ?? 'praxis/provider',
2283
+ });
2284
+ return;
2285
+ }
1898
2286
  const [entry] = translateProviderEvents([
1899
2287
  {
1900
2288
  type: 'assistant-message',
@@ -1909,7 +2297,7 @@ export class ClaudeSubagentExecutor {
1909
2297
  ], translationContext());
1910
2298
  if (!entry)
1911
2299
  throw new Error('Could not translate subagent response');
1912
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2300
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
1913
2301
  },
1914
2302
  toolCompleted: async (call, result) => {
1915
2303
  if (result.usage) {
@@ -1933,6 +2321,16 @@ export class ClaudeSubagentExecutor {
1933
2321
  nestedToolUseCount > 0) {
1934
2322
  toolUseCount += nestedToolUseCount;
1935
2323
  }
2324
+ if (nativeLease) {
2325
+ await nativeLease.appendToolCompletion({
2326
+ callId: call.id,
2327
+ result,
2328
+ ...(result.followUpUserMessages?.length
2329
+ ? { followUpUserMessages: result.followUpUserMessages }
2330
+ : {}),
2331
+ });
2332
+ return;
2333
+ }
1936
2334
  const [entry] = translateProviderEvents([
1937
2335
  {
1938
2336
  type: 'tool-result',
@@ -1954,16 +2352,19 @@ export class ClaudeSubagentExecutor {
1954
2352
  ], translationContext());
1955
2353
  if (!entry)
1956
2354
  throw new Error('Could not translate subagent tool result');
1957
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2355
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
1958
2356
  },
1959
2357
  followUpUserMessagesCompleted: async (messages) => {
1960
2358
  for (const text of messages) {
1961
- const [entry] = translateProviderEvents([{ type: 'user-text-block', text }], translationContext());
1962
- if (!entry)
1963
- throw new Error('Could not translate subagent follow-up');
1964
- await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2359
+ if (!nativeLease) {
2360
+ const [entry] = translateProviderEvents([{ type: 'user-text-block', text }], translationContext());
2361
+ if (!entry)
2362
+ throw new Error('Could not translate subagent follow-up');
2363
+ await appendClaude(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
2364
+ }
1965
2365
  await this.background.acknowledge([text]);
1966
2366
  }
2367
+ await durableFollowUps.followUpUserMessagesCompleted(messages);
1967
2368
  },
1968
2369
  };
1969
2370
  this.options.eventSink?.({
@@ -2031,11 +2432,16 @@ export class ClaudeSubagentExecutor {
2031
2432
  });
2032
2433
  const contextProjection = projectContextSnapshot(assembledContext);
2033
2434
  stableSystemMessageCount = contextProjection.stableSystemSectionCount;
2435
+ const transcriptMessages = nativeLease
2436
+ ? options.continuationMessage === undefined
2437
+ ? nativeLease.activeMessages()
2438
+ : projectNativeSidechainContinuationMessages(nativeLease.activeMessages())
2439
+ : options.continuationMessage === undefined
2440
+ ? projectClaudeModelMessages(requireClaudeSnapshot().entries)
2441
+ : projectClaudeSidechainContinuationMessages(requireClaudeSnapshot().entries);
2034
2442
  const messages = [
2035
2443
  ...contextProjection.systemMessages,
2036
- ...injectFirstUserMessageContext(options.continuationMessage === undefined
2037
- ? projectClaudeModelMessages(snapshot.entries)
2038
- : projectClaudeSidechainContinuationMessages(snapshot.entries), contextProjection.firstUserMessageContext),
2444
+ ...injectFirstUserMessageContext(transcriptMessages, contextProjection.firstUserMessageContext),
2039
2445
  ...preloadedSkills,
2040
2446
  ];
2041
2447
  observedMessages = messages;
@@ -2046,9 +2452,27 @@ export class ClaudeSubagentExecutor {
2046
2452
  };
2047
2453
  const configuredEffort = typeof customAgent?.effort === 'string' ? customAgent.effort : undefined;
2048
2454
  const effectiveEffort = options.effort ?? configuredEffort;
2455
+ if (nativeRecoveryCalls.length > 0) {
2456
+ if (!nativeLease)
2457
+ throw new Error('Native sidechain recovery lease is unavailable');
2458
+ await runtime.recoverToolCalls(nativeRecoveryCalls, {
2459
+ cwd,
2460
+ toolResultDirectory: options.toolResultDirectory,
2461
+ messages: nativeLease.activeMessages(),
2462
+ observer,
2463
+ ...(this.options.approveTool
2464
+ ? { approveTool: this.options.approveTool }
2465
+ : {}),
2466
+ permissionUpdates: this.options.permissionUpdates?.() ?? [],
2467
+ ...(this.options.onPermissionUpdates
2468
+ ? { onPermissionUpdates: this.options.onPermissionUpdates }
2469
+ : {}),
2470
+ ...(options.signal ? { signal: options.signal } : {}),
2471
+ });
2472
+ }
2049
2473
  const initialMessages = await assembleMessages();
2050
2474
  const runtimeRequest = {
2051
- sessionId: String(options.root.sessionId),
2475
+ sessionId: options.sessionId,
2052
2476
  messages: initialMessages,
2053
2477
  collectMetrics: true,
2054
2478
  stableSystemMessageCount,
@@ -2068,10 +2492,17 @@ export class ClaudeSubagentExecutor {
2068
2492
  ...(this.options.onPermissionUpdates
2069
2493
  ? { onPermissionUpdates: this.options.onPermissionUpdates }
2070
2494
  : {}),
2071
- ...(scopedHooks || this.options.backgroundTaskNotifications
2495
+ ...(scopedHooks ||
2496
+ this.options.backgroundTaskNotifications ||
2497
+ this.options.durableFollowUpSource
2072
2498
  ? {
2073
2499
  onStop: async (text) => {
2074
2500
  const messages = [];
2501
+ const durableBatch = await this.options.durableFollowUpSource?.();
2502
+ if (durableBatch) {
2503
+ durableFollowUps.register(durableBatch);
2504
+ messages.push(...durableBatch.messages);
2505
+ }
2075
2506
  const outcome = await scopedHooks?.run({
2076
2507
  ...hookSession,
2077
2508
  hook_event_name: 'SubagentStop',
@@ -2088,15 +2519,13 @@ export class ClaudeSubagentExecutor {
2088
2519
  messages.push(`SubagentStop hook error: ${outcome.blockedReason}`);
2089
2520
  }
2090
2521
  }
2091
- await this.hydratePersistedTasks(String(options.root.sessionId), options.cwd ?? this.cwd());
2522
+ await this.hydratePersistedTasks(options.sessionId, options.cwd ?? this.cwd());
2092
2523
  await this.reconcileDeliveredNotifications((notification) => {
2093
2524
  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
- });
2525
+ const source = JSON.stringify(nativeLease
2526
+ ? nativeLease.activeMessages()
2527
+ : requireClaudeSnapshot().entries);
2528
+ return markers.every((marker) => source.includes(marker));
2100
2529
  });
2101
2530
  const background = await this.background.notifications({
2102
2531
  waitForRunning: false,