praxis-agent 0.34.0 → 0.36.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 (78) hide show
  1. package/README.md +9 -1
  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/session-service.d.ts +8 -5
  7. package/dist/application/session-service.js +1824 -1728
  8. package/dist/application/subagent-service.js +385 -193
  9. package/dist/application/top-level-agent-manager.d.ts +1 -0
  10. package/dist/application/top-level-agent-manager.js +1025 -403
  11. package/dist/application/transcript-projection.d.ts +33 -0
  12. package/dist/application/transcript-projection.js +110 -0
  13. package/dist/application/turn-lifecycle.d.ts +40 -0
  14. package/dist/application/turn-lifecycle.js +36 -0
  15. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  16. package/dist/application/turn-memory-coordinator.js +98 -0
  17. package/dist/cli/agents-dashboard.js +10 -5
  18. package/dist/cli/interactive.js +279 -209
  19. package/dist/cli/protocol.js +5 -2
  20. package/dist/cli/tui/claude-style.d.ts +5 -48
  21. package/dist/cli/tui/claude-style.js +167 -197
  22. package/dist/cli/tui/task-panel.js +10 -1
  23. package/dist/cli/tui/theme.d.ts +46 -3
  24. package/dist/cli/tui/theme.js +414 -56
  25. package/dist/cli/tui/tool-permission.js +9 -5
  26. package/dist/cli/tui/transcript-presentation.d.ts +89 -0
  27. package/dist/cli/tui/transcript-presentation.js +169 -0
  28. package/dist/cli/tui/transcript-viewport.d.ts +5 -34
  29. package/dist/cli/tui/transcript-viewport.js +639 -158
  30. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  31. package/dist/cli/tui/tui-interaction-router.js +127 -0
  32. package/dist/cli/tui/tui-view-model.d.ts +6 -2
  33. package/dist/cli/tui/tui-view-model.js +23 -6
  34. package/dist/cli-runtime.d.ts +2 -2
  35. package/dist/compatibility/claude/context.d.ts +2 -2
  36. package/dist/compatibility/claude/context.js +2 -14
  37. package/dist/compatibility/claude/data-plane-adapter.d.ts +9 -0
  38. package/dist/compatibility/claude/data-plane-adapter.js +44 -0
  39. package/dist/compatibility/claude/paths.js +2 -2
  40. package/dist/compatibility/claude/shared-resources.d.ts +7 -38
  41. package/dist/compatibility/claude/transcript-codec.d.ts +21 -0
  42. package/dist/compatibility/claude/transcript-codec.js +512 -0
  43. package/dist/core/agent-orchestration.d.ts +31 -0
  44. package/dist/core/agent-orchestration.js +235 -0
  45. package/dist/core/context.d.ts +25 -7
  46. package/dist/core/context.js +46 -0
  47. package/dist/core/prompt-composer.d.ts +8 -16
  48. package/dist/core/prompt-composer.js +65 -25
  49. package/dist/core/resources.d.ts +39 -0
  50. package/dist/core/resources.js +2 -0
  51. package/dist/core/runtime.d.ts +6 -1
  52. package/dist/core/runtime.js +15 -1
  53. package/dist/core/session.d.ts +2 -0
  54. package/dist/core/session.js +5 -0
  55. package/dist/core/transcript-codec.d.ts +70 -0
  56. package/dist/core/transcript-codec.js +60 -0
  57. package/dist/core/transcript-event.d.ts +33 -0
  58. package/dist/core/transcript-event.js +198 -0
  59. package/dist/persistence/claude-job-store.d.ts +58 -0
  60. package/dist/persistence/claude-job-store.js +483 -1
  61. package/dist/persistence/data-plane-adapter.d.ts +32 -0
  62. package/dist/persistence/data-plane-adapter.js +2 -0
  63. package/dist/persistence/data-plane.d.ts +4 -20
  64. package/dist/persistence/data-plane.js +10 -47
  65. package/dist/persistence/native-data-plane-adapter.d.ts +9 -0
  66. package/dist/persistence/native-data-plane-adapter.js +39 -0
  67. package/dist/persistence/native-resources.d.ts +4 -4
  68. package/dist/persistence/native-transcript-codec.d.ts +13 -0
  69. package/dist/persistence/native-transcript-codec.js +102 -0
  70. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  71. package/dist/persistence/native-transcript-reader.js +283 -0
  72. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  73. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  74. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  75. package/dist/platform/exclusive-file-lease.js +3 -0
  76. package/dist/tools/claude-user-message.d.ts +0 -1
  77. package/dist/tools/claude-user-message.js +0 -1
  78. package/package.json +6 -2
@@ -8,7 +8,8 @@ import { createClaudeAsyncAgentToolUseResult, createClaudeAgentToolUseResult, cr
8
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,7 +17,7 @@ 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';
19
- import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
20
+ import { SubagentLifecycleStore, } from '../persistence/subagent-lifecycle-store.js';
20
21
  import { resolveDataPlanePaths, } from '../persistence/data-plane.js';
21
22
  import { BackgroundAgentManager, BackgroundAgentRunError, BackgroundAgentShutdownError, backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
22
23
  import { isBackgroundBashTaskId } from './background-task-id.js';
@@ -715,11 +716,53 @@ export class ClaudeSubagentExecutor {
715
716
  const agentId = `a${randomBytes(8).toString('hex')}`;
716
717
  const parentCwd = context.cwd;
717
718
  const paths = this.sessionPaths(sessionId, parentCwd);
718
- const initialIsolation = input.isolation
719
- ? await this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd)
719
+ const sidechainPaths = resolveClaudeSidechainPaths(paths.projectRoot, sessionId, agentId);
720
+ const lifecycleStore = this.options.persistence === 'memory'
721
+ ? undefined
722
+ : new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
723
+ const initialExecution = lifecycleStore
724
+ ? await lifecycleStore.start()
720
725
  : undefined;
726
+ let initialIsolation;
727
+ const settleInitialSetupFailure = async (primary, isolation) => {
728
+ const secondary = [];
729
+ const message = primary instanceof Error ? primary.message : String(primary);
730
+ try {
731
+ await initialExecution?.finish('failed', {
732
+ text: message,
733
+ usage: { inputTokens: 0, outputTokens: 0 },
734
+ toolUseCount: 0,
735
+ durationMs: 0,
736
+ }, message);
737
+ }
738
+ catch (error) {
739
+ secondary.push(error);
740
+ }
741
+ try {
742
+ await initialExecution?.release();
743
+ }
744
+ catch (error) {
745
+ secondary.push(error);
746
+ }
747
+ try {
748
+ await isolation?.cleanup();
749
+ }
750
+ catch (error) {
751
+ secondary.push(error);
752
+ }
753
+ if (secondary.length > 0)
754
+ throw new AggregateError([primary, ...secondary], 'Background agent setup and cleanup failed');
755
+ throw primary;
756
+ };
757
+ try {
758
+ initialIsolation = input.isolation
759
+ ? await this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd)
760
+ : undefined;
761
+ }
762
+ catch (error) {
763
+ return settleInitialSetupFailure(error, initialIsolation);
764
+ }
721
765
  const agentCwd = initialIsolation?.cwd ?? parentCwd;
722
- const sidechainPaths = resolveClaudeSidechainPaths(paths.projectRoot, sessionId, agentId);
723
766
  const sidechain = this.sidechainStore(sessionId, agentId, sidechainPaths, paths.praxisRoot);
724
767
  const root = createClaudeSidechainRoot({
725
768
  sessionId,
@@ -748,19 +791,16 @@ export class ClaudeSubagentExecutor {
748
791
  });
749
792
  }
750
793
  catch (error) {
751
- await initialIsolation?.cleanup();
752
- throw error;
794
+ return settleInitialSetupFailure(error, initialIsolation);
753
795
  }
754
796
  const provider = input.model
755
797
  ? (this.options.providerForModel?.(input.model) ?? this.options.provider)
756
798
  : this.options.provider;
757
- const lifecycleStore = this.options.persistence === 'memory'
758
- ? undefined
759
- : new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
760
799
  const backgroundRun = this.createBackgroundAgentRun({
761
800
  input,
762
801
  parentCwd,
763
802
  ...(lifecycleStore ? { lifecycle: lifecycleStore } : {}),
803
+ ...(initialExecution ? { initialExecution } : {}),
764
804
  ...(initialIsolation ? { initialIsolation } : {}),
765
805
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
766
806
  execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
@@ -789,6 +829,7 @@ export class ClaudeSubagentExecutor {
789
829
  toolUseId: call.id,
790
830
  outputFile: sidechainPaths.transcriptFile,
791
831
  resolvedModel,
832
+ lifecycle: backgroundRun.lifecycle,
792
833
  run: backgroundRun.run,
793
834
  markBackground: backgroundRun.markBackground,
794
835
  ...(lifecycleStore
@@ -819,7 +860,18 @@ export class ClaudeSubagentExecutor {
819
860
  },
820
861
  });
821
862
  if (input.runInBackground) {
822
- this.background.launch(spec);
863
+ try {
864
+ this.background.launch(spec);
865
+ }
866
+ catch (error) {
867
+ try {
868
+ await backgroundRun.disposeBeforeStart();
869
+ }
870
+ catch (cleanupError) {
871
+ throw new AggregateError([error, cleanupError], 'Background agent launch and pre-start cleanup failed');
872
+ }
873
+ throw error;
874
+ }
823
875
  return asyncResult();
824
876
  }
825
877
  const controller = new AbortController();
@@ -946,183 +998,288 @@ export class ClaudeSubagentExecutor {
946
998
  let availableIsolation = options.initialIsolation;
947
999
  let retainedIsolation;
948
1000
  let notificationExpected = options.input.runInBackground;
1001
+ let recoverNext = options.recover ?? false;
1002
+ let pendingExecution = options.initialExecution;
1003
+ let disposed = false;
1004
+ let lifecycleState = options.initialExecution?.snapshot.state ??
1005
+ options.initialLifecycleState ??
1006
+ 'queued';
1007
+ const lifecycleListeners = new Set();
1008
+ const publishLifecycle = (state) => {
1009
+ lifecycleState = state;
1010
+ if (state === 'orphaned')
1011
+ recoverNext = true;
1012
+ for (const listener of lifecycleListeners)
1013
+ listener(state);
1014
+ };
1015
+ const lifecycle = {
1016
+ current: () => lifecycleState,
1017
+ subscribe(listener) {
1018
+ lifecycleListeners.add(listener);
1019
+ return () => lifecycleListeners.delete(listener);
1020
+ },
1021
+ };
1022
+ const disposeBeforeStart = async () => {
1023
+ if (disposed)
1024
+ return;
1025
+ disposed = true;
1026
+ const execution = pendingExecution;
1027
+ pendingExecution = undefined;
1028
+ const isolation = availableIsolation;
1029
+ availableIsolation = undefined;
1030
+ const errors = [];
1031
+ if (execution) {
1032
+ try {
1033
+ const snapshot = await execution.finish('failed', {
1034
+ text: 'Background agent launch was rejected before execution started',
1035
+ usage: { inputTokens: 0, outputTokens: 0 },
1036
+ toolUseCount: 0,
1037
+ durationMs: 0,
1038
+ }, 'Background agent launch was rejected before execution started');
1039
+ publishLifecycle(snapshot.state);
1040
+ }
1041
+ catch (error) {
1042
+ errors.push(error);
1043
+ }
1044
+ finally {
1045
+ try {
1046
+ await execution.release();
1047
+ publishLifecycle(execution.snapshot.state);
1048
+ }
1049
+ catch (error) {
1050
+ errors.push(error);
1051
+ }
1052
+ }
1053
+ }
1054
+ if (isolation) {
1055
+ try {
1056
+ await isolation.cleanup();
1057
+ }
1058
+ catch (error) {
1059
+ errors.push(error);
1060
+ }
1061
+ }
1062
+ if (errors.length > 0) {
1063
+ throw new AggregateError(errors, 'Background agent pre-start cleanup failed');
1064
+ }
1065
+ };
949
1066
  const run = async (message, signal, continuation, currentToolUseId) => {
950
- await options.lifecycle?.write('running');
1067
+ let execution;
1068
+ let cancellation;
1069
+ let lifecycleTerminalized = false;
1070
+ let onAbort;
951
1071
  const startedAt = Date.now();
952
1072
  let isolation;
953
- try {
954
- isolation = options.input.isolation
955
- ? (retainedIsolation ??
956
- availableIsolation ??
957
- (await options.createIsolation()))
958
- : undefined;
959
- }
960
- catch (error) {
961
- const failureMessage = error instanceof Error ? error.message : String(error);
962
- const failureResult = {
963
- text: failureMessage,
964
- usage: { inputTokens: 0, outputTokens: 0 },
965
- toolUseCount: 0,
966
- durationMs: Date.now() - startedAt,
967
- };
968
- const status = signal.aborted ? 'killed' : 'failed';
969
- const notificationId = notificationExpected &&
970
- !(signal.reason instanceof BackgroundAgentShutdownError)
971
- ? randomUUID()
972
- : undefined;
973
- await options.lifecycle?.write(status, failureMessage, {
974
- result: failureResult,
975
- ...(notificationId
976
- ? {
977
- notification: {
978
- id: notificationId,
979
- status,
980
- toolUseId: currentToolUseId,
981
- error: failureMessage,
982
- },
983
- }
984
- : {}),
985
- });
986
- if (!notificationExpected)
987
- throw error;
988
- throw new BackgroundAgentRunError(failureMessage, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, error);
989
- }
990
- availableIsolation = undefined;
991
1073
  let result;
992
1074
  let failure;
993
1075
  let cleanup;
1076
+ const finishLifecycle = async (state, result, detail, notification) => {
1077
+ if (execution) {
1078
+ const snapshot = await execution.finish(state, result, detail, notification);
1079
+ publishLifecycle(snapshot.state);
1080
+ }
1081
+ else
1082
+ publishLifecycle(state);
1083
+ };
994
1084
  try {
995
- result = await options.execute(isolation?.cwd ?? options.parentCwd, message, signal, continuation);
996
- }
997
- catch (error) {
998
- failure = error;
999
- }
1000
- finally {
1085
+ if (pendingExecution) {
1086
+ execution = pendingExecution;
1087
+ pendingExecution = undefined;
1088
+ publishLifecycle(execution.snapshot.state);
1089
+ }
1090
+ else if (options.lifecycle) {
1091
+ execution = await options.lifecycle.acquire(recoverNext ? 'recover' : continuation ? 'continue' : 'start');
1092
+ publishLifecycle(execution.snapshot.state);
1093
+ recoverNext = false;
1094
+ }
1095
+ if (execution) {
1096
+ const snapshot = await execution.running();
1097
+ publishLifecycle(snapshot.state);
1098
+ }
1099
+ else {
1100
+ publishLifecycle('running');
1101
+ }
1102
+ onAbort = () => {
1103
+ if (cancellation)
1104
+ return;
1105
+ if (!execution) {
1106
+ cancellation = Promise.resolve(publishLifecycle('cancelling'));
1107
+ return;
1108
+ }
1109
+ const activeExecution = execution;
1110
+ const transition = activeExecution.beginCancellation();
1111
+ transition.catch(() => undefined);
1112
+ const observedCancellation = transition
1113
+ .then((snapshot) => publishLifecycle(snapshot.state))
1114
+ .finally(() => publishLifecycle(activeExecution.snapshot.state));
1115
+ observedCancellation.catch(() => undefined);
1116
+ cancellation = observedCancellation;
1117
+ };
1118
+ signal.addEventListener('abort', onAbort, { once: true });
1119
+ if (signal.aborted)
1120
+ onAbort();
1001
1121
  try {
1002
- cleanup = await isolation?.cleanup();
1122
+ isolation = options.input.isolation
1123
+ ? (retainedIsolation ??
1124
+ availableIsolation ??
1125
+ (await options.createIsolation()))
1126
+ : undefined;
1127
+ availableIsolation = undefined;
1128
+ result = await options.execute(isolation?.cwd ?? options.parentCwd, message, signal, continuation);
1003
1129
  }
1004
1130
  catch (error) {
1005
- failure ??= error;
1131
+ failure = error;
1006
1132
  }
1007
- retainedIsolation = cleanup?.retained ? isolation : undefined;
1008
- }
1009
- if (failure !== undefined) {
1010
- const underlyingFailure = failure instanceof SubagentExecutionFailure ? failure.cause : failure;
1011
- const failureMessage = underlyingFailure instanceof Error
1012
- ? underlyingFailure.message
1013
- : String(underlyingFailure);
1014
- const detail = cleanup?.reason
1015
- ? `${failureMessage}\n${cleanup.reason}`
1016
- : failureMessage;
1017
- const partialResult = failure instanceof SubagentExecutionFailure
1018
- ? failure.result
1019
- : undefined;
1020
- const failureResult = {
1021
- ...partialResult,
1022
- text: failureMessage,
1023
- usage: partialResult?.usage ?? {
1024
- inputTokens: 0,
1025
- outputTokens: 0,
1026
- },
1027
- toolUseCount: partialResult?.toolUseCount ?? 0,
1028
- durationMs: Date.now() - startedAt,
1029
- ...(isolation ? { isolationPath: isolation.cwd } : {}),
1030
- ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1031
- ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1032
- };
1033
- const status = signal.aborted ? 'killed' : 'failed';
1034
- const notificationId = notificationExpected &&
1035
- !(signal.reason instanceof BackgroundAgentShutdownError)
1036
- ? randomUUID()
1037
- : undefined;
1038
- await options.lifecycle?.write(status, detail, {
1039
- result: failureResult,
1040
- ...(notificationId
1133
+ finally {
1134
+ try {
1135
+ cleanup = await isolation?.cleanup();
1136
+ }
1137
+ catch (error) {
1138
+ failure ??= error;
1139
+ }
1140
+ retainedIsolation = cleanup?.retained ? isolation : undefined;
1141
+ }
1142
+ if (failure !== undefined) {
1143
+ const underlyingFailure = failure instanceof SubagentExecutionFailure
1144
+ ? failure.cause
1145
+ : failure;
1146
+ const failureMessage = underlyingFailure instanceof Error
1147
+ ? underlyingFailure.message
1148
+ : String(underlyingFailure);
1149
+ const detail = cleanup?.reason
1150
+ ? `${failureMessage}\n${cleanup.reason}`
1151
+ : failureMessage;
1152
+ const partialResult = failure instanceof SubagentExecutionFailure
1153
+ ? failure.result
1154
+ : undefined;
1155
+ const failureResult = {
1156
+ ...partialResult,
1157
+ text: failureMessage,
1158
+ usage: partialResult?.usage ?? {
1159
+ inputTokens: 0,
1160
+ outputTokens: 0,
1161
+ },
1162
+ toolUseCount: partialResult?.toolUseCount ?? 0,
1163
+ durationMs: Date.now() - startedAt,
1164
+ ...(isolation ? { isolationPath: isolation.cwd } : {}),
1165
+ ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1166
+ ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1167
+ };
1168
+ await cancellation;
1169
+ const status = signal.aborted ? 'killed' : 'failed';
1170
+ const notificationId = notificationExpected &&
1171
+ !(signal.reason instanceof BackgroundAgentShutdownError)
1172
+ ? randomUUID()
1173
+ : undefined;
1174
+ await finishLifecycle(signal.aborted ? 'cancelled' : 'failed', failureResult, detail, notificationId
1041
1175
  ? {
1042
- notification: {
1043
- id: notificationId,
1044
- status,
1045
- toolUseId: currentToolUseId,
1046
- error: detail,
1047
- },
1176
+ id: notificationId,
1177
+ status,
1178
+ toolUseId: currentToolUseId,
1179
+ error: detail,
1048
1180
  }
1049
- : {}),
1050
- });
1051
- if (!notificationExpected && !isolation && !cleanup?.reason) {
1052
- throw underlyingFailure;
1181
+ : undefined);
1182
+ lifecycleTerminalized = true;
1183
+ if (!notificationExpected && !isolation && !cleanup?.reason) {
1184
+ throw underlyingFailure;
1185
+ }
1186
+ throw new BackgroundAgentRunError(detail, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, underlyingFailure);
1053
1187
  }
1054
- throw new BackgroundAgentRunError(detail, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, underlyingFailure);
1055
- }
1056
- if (!result) {
1057
- const error = new Error('Agent completed without a result');
1058
- const failureResult = {
1059
- text: error.message,
1060
- usage: { inputTokens: 0, outputTokens: 0 },
1061
- toolUseCount: 0,
1188
+ if (!result) {
1189
+ const error = new Error('Agent completed without a result');
1190
+ const failureResult = {
1191
+ text: error.message,
1192
+ usage: { inputTokens: 0, outputTokens: 0 },
1193
+ toolUseCount: 0,
1194
+ durationMs: Date.now() - startedAt,
1195
+ ...(isolation ? { isolationPath: isolation.cwd } : {}),
1196
+ ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1197
+ ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1198
+ };
1199
+ const notificationId = notificationExpected &&
1200
+ !(signal.reason instanceof BackgroundAgentShutdownError)
1201
+ ? randomUUID()
1202
+ : undefined;
1203
+ await cancellation;
1204
+ await finishLifecycle(signal.aborted ? 'cancelled' : 'failed', failureResult, error.message, notificationId
1205
+ ? {
1206
+ id: notificationId,
1207
+ status: signal.aborted ? 'killed' : 'failed',
1208
+ toolUseId: currentToolUseId,
1209
+ error: error.message,
1210
+ }
1211
+ : undefined);
1212
+ lifecycleTerminalized = true;
1213
+ if (!notificationExpected)
1214
+ throw error;
1215
+ throw new BackgroundAgentRunError(error.message, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, error);
1216
+ }
1217
+ const finalResult = {
1218
+ ...result,
1062
1219
  durationMs: Date.now() - startedAt,
1063
1220
  ...(isolation ? { isolationPath: isolation.cwd } : {}),
1064
1221
  ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1065
1222
  ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1066
1223
  };
1224
+ await cancellation;
1225
+ const status = signal.aborted ? 'killed' : 'completed';
1226
+ const detail = signal.aborted
1227
+ ? 'Agent was aborted before terminal cleanup'
1228
+ : undefined;
1067
1229
  const notificationId = notificationExpected &&
1068
1230
  !(signal.reason instanceof BackgroundAgentShutdownError)
1069
1231
  ? randomUUID()
1070
1232
  : undefined;
1071
- await options.lifecycle?.write('failed', error.message, {
1072
- result: failureResult,
1073
- ...(notificationId
1074
- ? {
1075
- notification: {
1076
- id: notificationId,
1077
- status: 'failed',
1078
- toolUseId: currentToolUseId,
1079
- error: error.message,
1080
- },
1081
- }
1082
- : {}),
1083
- });
1084
- if (!notificationExpected)
1085
- throw error;
1086
- throw new BackgroundAgentRunError(error.message, { ...failureResult, ...(notificationId ? { notificationId } : {}) }, error);
1087
- }
1088
- const finalResult = {
1089
- ...result,
1090
- durationMs: Date.now() - startedAt,
1091
- ...(isolation ? { isolationPath: isolation.cwd } : {}),
1092
- ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1093
- ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1094
- };
1095
- const status = signal.aborted ? 'killed' : 'completed';
1096
- const detail = signal.aborted
1097
- ? 'Agent was aborted before terminal cleanup'
1098
- : undefined;
1099
- const notificationId = notificationExpected &&
1100
- !(signal.reason instanceof BackgroundAgentShutdownError)
1101
- ? randomUUID()
1102
- : undefined;
1103
- await options.lifecycle?.write(status, detail, {
1104
- result: finalResult,
1105
- ...(notificationId
1233
+ await finishLifecycle(signal.aborted ? 'cancelled' : 'completed', finalResult, detail, notificationId
1106
1234
  ? {
1107
- notification: {
1108
- id: notificationId,
1109
- status,
1110
- toolUseId: currentToolUseId,
1111
- error: status === 'killed' ? (detail ?? null) : null,
1112
- },
1235
+ id: notificationId,
1236
+ status,
1237
+ toolUseId: currentToolUseId,
1238
+ error: status === 'killed' ? (detail ?? null) : null,
1113
1239
  }
1114
- : {}),
1115
- });
1116
- return {
1117
- ...finalResult,
1118
- ...(notificationId ? { notificationId } : {}),
1119
- };
1240
+ : undefined);
1241
+ lifecycleTerminalized = true;
1242
+ return {
1243
+ ...finalResult,
1244
+ ...(notificationId ? { notificationId } : {}),
1245
+ };
1246
+ }
1247
+ finally {
1248
+ if (onAbort)
1249
+ signal.removeEventListener('abort', onAbort);
1250
+ if (!lifecycleTerminalized) {
1251
+ try {
1252
+ await execution?.release();
1253
+ }
1254
+ catch (releaseError) {
1255
+ this.options.eventSink?.({
1256
+ type: 'warning',
1257
+ message: `Background agent execution cleanup could not release its durable owner; durable owner reconciliation is required: ${releaseError instanceof Error ? releaseError.message : String(releaseError)}`,
1258
+ });
1259
+ }
1260
+ finally {
1261
+ if (execution)
1262
+ publishLifecycle(execution.snapshot.state);
1263
+ }
1264
+ }
1265
+ else {
1266
+ try {
1267
+ await execution?.release();
1268
+ }
1269
+ finally {
1270
+ if (execution)
1271
+ publishLifecycle(execution.snapshot.state);
1272
+ }
1273
+ }
1274
+ }
1120
1275
  };
1121
1276
  return {
1122
1277
  run,
1123
1278
  markBackground: () => {
1124
1279
  notificationExpected = true;
1125
1280
  },
1281
+ disposeBeforeStart,
1282
+ lifecycle,
1126
1283
  };
1127
1284
  }
1128
1285
  async runWorkflowAgent(options) {
@@ -1404,7 +1561,27 @@ export class ClaudeSubagentExecutor {
1404
1561
  lastAssistant.content.trim().length > 0 &&
1405
1562
  (lastAssistant.toolCalls?.length ?? 0) === 0;
1406
1563
  const lifecycleStore = new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
1407
- const lifecycle = await lifecycleStore.read();
1564
+ const ownership = await lifecycleStore.reconcileOwnerLoss();
1565
+ if (ownership.owned)
1566
+ return;
1567
+ let lifecycle = await lifecycleStore.read();
1568
+ if (!lifecycle) {
1569
+ const seed = await lifecycleStore.start();
1570
+ await seed.running();
1571
+ if (completed) {
1572
+ await seed.finish('completed', {
1573
+ text: lastAssistant?.content ?? '',
1574
+ usage: { inputTokens: 0, outputTokens: 0 },
1575
+ toolUseCount: 0,
1576
+ durationMs: 0,
1577
+ });
1578
+ }
1579
+ await seed.release();
1580
+ lifecycle = await lifecycleStore.read();
1581
+ }
1582
+ if (!lifecycle) {
1583
+ throw new Error(`Unable to establish lifecycle state for background agent ${agentId}`);
1584
+ }
1408
1585
  const lifecycleIsCurrent = lifecycle !== null && (await lifecycleStore.matchesTranscript(lifecycle));
1409
1586
  if (lifecycleIsCurrent && lifecycle.status === 'completed' && !completed) {
1410
1587
  throw new Error(`Background agent ${agentId} has completed lifecycle state but an incomplete sidechain`);
@@ -1483,6 +1660,8 @@ export class ClaudeSubagentExecutor {
1483
1660
  input,
1484
1661
  parentCwd: restoredCwd,
1485
1662
  lifecycle: lifecycleStore,
1663
+ recover: lifecycle.lifecycle.state === 'orphaned',
1664
+ initialLifecycleState: lifecycle.lifecycle.state,
1486
1665
  ...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
1487
1666
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
1488
1667
  execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
@@ -1510,31 +1689,34 @@ export class ClaudeSubagentExecutor {
1510
1689
  toolUseId,
1511
1690
  outputFile: sidechainPaths.transcriptFile,
1512
1691
  resolvedModel: provider.model ?? 'praxis/provider',
1692
+ lifecycle: backgroundRun.lifecycle,
1513
1693
  run: backgroundRun.run,
1514
1694
  markBackground: backgroundRun.markBackground,
1515
1695
  acknowledgeNotification: (notificationId) => lifecycleStore.acknowledgeNotification(notificationId),
1516
1696
  prepareNotificationDetached: (notificationId, model) => lifecycleStore.prepareNotificationDetached(notificationId, model),
1517
1697
  confirmNotificationDetached: (notificationId) => lifecycleStore.confirmNotificationDetached(notificationId),
1518
1698
  };
1519
- if (lifecycleIsCurrent && lifecycle.status === 'failed') {
1520
- this.background.registerTerminal(spec, 'failed', lifecycle.detail ?? 'Persisted agent failed');
1521
- }
1522
- else if (lifecycleIsCurrent && lifecycle.status === 'killed') {
1523
- this.background.registerTerminal(spec, 'stopped', lifecycle.detail ?? 'Persisted agent was killed');
1524
- }
1525
- else if (completed) {
1526
- this.background.registerCompleted(spec, lifecycleIsCurrent && lifecycle.result
1527
- ? lifecycle.result
1528
- : {
1529
- text: lastAssistant.content,
1530
- usage: { inputTokens: 0, outputTokens: 0 },
1531
- toolUseCount: 0,
1532
- durationMs: 0,
1533
- });
1534
- }
1535
- else {
1536
- this.background.registerInterrupted(spec);
1537
- }
1699
+ this.background.registerPersisted(spec, {
1700
+ ...(lifecycleIsCurrent && lifecycle.result
1701
+ ? { result: lifecycle.result }
1702
+ : completed
1703
+ ? {
1704
+ result: {
1705
+ text: lastAssistant.content,
1706
+ usage: { inputTokens: 0, outputTokens: 0 },
1707
+ toolUseCount: 0,
1708
+ durationMs: 0,
1709
+ },
1710
+ }
1711
+ : {}),
1712
+ ...(lifecycleIsCurrent && lifecycle.status === 'failed'
1713
+ ? { error: lifecycle.detail ?? 'Persisted agent failed' }
1714
+ : lifecycleIsCurrent && lifecycle.status === 'killed'
1715
+ ? { error: lifecycle.detail ?? 'Persisted agent was killed' }
1716
+ : !completed && lifecycle.lifecycle.state === 'orphaned'
1717
+ ? { error: 'Persisted agent was interrupted before completion' }
1718
+ : {}),
1719
+ });
1538
1720
  for (const notification of lifecycle?.notifications ?? []) {
1539
1721
  if (notification.consumed)
1540
1722
  continue;
@@ -1552,7 +1734,7 @@ export class ClaudeSubagentExecutor {
1552
1734
  }
1553
1735
  this.background.registerPersistedNotification(agentId, {
1554
1736
  id: notification.id,
1555
- status: notification.status === 'killed' ? 'stopped' : notification.status,
1737
+ status: notification.status === 'killed' ? 'cancelled' : notification.status,
1556
1738
  result: notification.result,
1557
1739
  error: notification.error,
1558
1740
  toolUseId: notification.toolUseId,
@@ -1995,9 +2177,6 @@ export class ClaudeSubagentExecutor {
1995
2177
  const agentSystem = memoryPrompt
1996
2178
  ? `${baseSystem}\n\n${memoryPrompt}`
1997
2179
  : baseSystem;
1998
- const system = options.outputSchema
1999
- ? `${agentSystem}\n\nYou MUST call StructuredOutput exactly once at the end with a value matching its schema.`
2000
- : agentSystem;
2001
2180
  const preloadedSkills = agentSkillMessages(this.options.extensions, customAgent);
2002
2181
  let stopHookActive = false;
2003
2182
  const contextBudget = options.provider.capabilities.contextWindowTokens
@@ -2016,22 +2195,28 @@ export class ClaudeSubagentExecutor {
2016
2195
  ? runtimeTools.definitions()
2017
2196
  : [];
2018
2197
  let observedMessages;
2198
+ let stableSystemMessageCount = 0;
2019
2199
  const assembleMessages = async () => {
2020
- const assembledContext = await this.options.contextAssembler?.assemble({
2200
+ const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
2021
2201
  cwd,
2022
2202
  lifecycleId: options.agentId,
2023
2203
  mode: 'subagent',
2024
- baseSystemPrompt: system,
2204
+ baseSystemPrompt: agentSystem,
2205
+ ...(options.outputSchema
2206
+ ? {
2207
+ turn: {
2208
+ structuredOutput: true,
2209
+ },
2210
+ }
2211
+ : {}),
2025
2212
  });
2026
- const composedSubagentPolicy = assembledContext?.promptSections?.some((section) => section.id === 'subagent-policy');
2213
+ const contextProjection = projectContextSnapshot(assembledContext);
2214
+ stableSystemMessageCount = contextProjection.stableSystemSectionCount;
2027
2215
  const messages = [
2028
- ...(assembledContext?.systemMessages ?? []),
2029
- ...(composedSubagentPolicy
2030
- ? []
2031
- : [{ role: 'system', content: system }]),
2216
+ ...contextProjection.systemMessages,
2032
2217
  ...injectFirstUserMessageContext(options.continuationMessage === undefined
2033
2218
  ? projectClaudeModelMessages(snapshot.entries)
2034
- : projectClaudeSidechainContinuationMessages(snapshot.entries), assembledContext?.firstUserMessageContext),
2219
+ : projectClaudeSidechainContinuationMessages(snapshot.entries), contextProjection.firstUserMessageContext),
2035
2220
  ...preloadedSkills,
2036
2221
  ];
2037
2222
  observedMessages = messages;
@@ -2042,11 +2227,17 @@ export class ClaudeSubagentExecutor {
2042
2227
  };
2043
2228
  const configuredEffort = typeof customAgent?.effort === 'string' ? customAgent.effort : undefined;
2044
2229
  const effectiveEffort = options.effort ?? configuredEffort;
2045
- const result = await runtime.run({
2230
+ const initialMessages = await assembleMessages();
2231
+ const runtimeRequest = {
2046
2232
  sessionId: String(options.root.sessionId),
2047
- messages: await assembleMessages(),
2233
+ messages: initialMessages,
2048
2234
  collectMetrics: true,
2049
- reloadMessages: assembleMessages,
2235
+ stableSystemMessageCount,
2236
+ reloadMessages: async () => {
2237
+ const messages = await assembleMessages();
2238
+ runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
2239
+ return messages;
2240
+ },
2050
2241
  cwd,
2051
2242
  ...(effectiveEffort ? { effort: effectiveEffort } : {}),
2052
2243
  toolResultDirectory: options.toolResultDirectory,
@@ -2119,7 +2310,8 @@ export class ClaudeSubagentExecutor {
2119
2310
  }
2120
2311
  : {}),
2121
2312
  ...(options.signal ? { signal: options.signal } : {}),
2122
- });
2313
+ };
2314
+ const result = await runtime.run(runtimeRequest);
2123
2315
  contextBudget?.observeUsage(result.usage, observedMessages ?? [], definitions);
2124
2316
  this.options.eventSink?.({
2125
2317
  type: 'task-progress',