praxis-agent 0.35.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 (38) hide show
  1. package/README.md +5 -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 -4
  7. package/dist/application/session-service.js +528 -434
  8. package/dist/application/subagent-service.js +358 -177
  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 +2 -0
  14. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  15. package/dist/application/turn-memory-coordinator.js +98 -0
  16. package/dist/cli/agents-dashboard.js +10 -5
  17. package/dist/cli/interactive.js +264 -204
  18. package/dist/cli/tui/claude-style.js +154 -130
  19. package/dist/cli/tui/task-panel.js +10 -1
  20. package/dist/cli/tui/theme.d.ts +46 -3
  21. package/dist/cli/tui/theme.js +414 -56
  22. package/dist/cli/tui/tool-permission.js +9 -5
  23. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  24. package/dist/cli/tui/tui-interaction-router.js +127 -0
  25. package/dist/cli-runtime.d.ts +2 -2
  26. package/dist/core/agent-orchestration.d.ts +31 -0
  27. package/dist/core/agent-orchestration.js +235 -0
  28. package/dist/core/runtime.d.ts +5 -0
  29. package/dist/core/runtime.js +15 -1
  30. package/dist/persistence/claude-job-store.d.ts +58 -0
  31. package/dist/persistence/claude-job-store.js +483 -1
  32. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  33. package/dist/persistence/native-transcript-reader.js +283 -0
  34. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  35. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  36. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  37. package/dist/platform/exclusive-file-lease.js +3 -0
  38. package/package.json +1 -1
@@ -17,7 +17,7 @@ import { BUILTIN_STATUSLINE_AGENT_PATH, } from '../extensions/claude-extensions.
17
17
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
18
18
  import { ClaudeSidechainStore } from '../persistence/claude-sidechain-store.js';
19
19
  import { InMemorySidechainStore } from '../persistence/in-memory-sidechain-store.js';
20
- import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
20
+ import { SubagentLifecycleStore, } from '../persistence/subagent-lifecycle-store.js';
21
21
  import { resolveDataPlanePaths, } from '../persistence/data-plane.js';
22
22
  import { BackgroundAgentManager, BackgroundAgentRunError, BackgroundAgentShutdownError, backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
23
23
  import { isBackgroundBashTaskId } from './background-task-id.js';
@@ -716,11 +716,53 @@ export class ClaudeSubagentExecutor {
716
716
  const agentId = `a${randomBytes(8).toString('hex')}`;
717
717
  const parentCwd = context.cwd;
718
718
  const paths = this.sessionPaths(sessionId, parentCwd);
719
- const initialIsolation = input.isolation
720
- ? 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()
721
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
+ }
722
765
  const agentCwd = initialIsolation?.cwd ?? parentCwd;
723
- const sidechainPaths = resolveClaudeSidechainPaths(paths.projectRoot, sessionId, agentId);
724
766
  const sidechain = this.sidechainStore(sessionId, agentId, sidechainPaths, paths.praxisRoot);
725
767
  const root = createClaudeSidechainRoot({
726
768
  sessionId,
@@ -749,19 +791,16 @@ export class ClaudeSubagentExecutor {
749
791
  });
750
792
  }
751
793
  catch (error) {
752
- await initialIsolation?.cleanup();
753
- throw error;
794
+ return settleInitialSetupFailure(error, initialIsolation);
754
795
  }
755
796
  const provider = input.model
756
797
  ? (this.options.providerForModel?.(input.model) ?? this.options.provider)
757
798
  : this.options.provider;
758
- const lifecycleStore = this.options.persistence === 'memory'
759
- ? undefined
760
- : new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
761
799
  const backgroundRun = this.createBackgroundAgentRun({
762
800
  input,
763
801
  parentCwd,
764
802
  ...(lifecycleStore ? { lifecycle: lifecycleStore } : {}),
803
+ ...(initialExecution ? { initialExecution } : {}),
765
804
  ...(initialIsolation ? { initialIsolation } : {}),
766
805
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
767
806
  execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
@@ -790,6 +829,7 @@ export class ClaudeSubagentExecutor {
790
829
  toolUseId: call.id,
791
830
  outputFile: sidechainPaths.transcriptFile,
792
831
  resolvedModel,
832
+ lifecycle: backgroundRun.lifecycle,
793
833
  run: backgroundRun.run,
794
834
  markBackground: backgroundRun.markBackground,
795
835
  ...(lifecycleStore
@@ -820,7 +860,18 @@ export class ClaudeSubagentExecutor {
820
860
  },
821
861
  });
822
862
  if (input.runInBackground) {
823
- 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
+ }
824
875
  return asyncResult();
825
876
  }
826
877
  const controller = new AbortController();
@@ -947,183 +998,288 @@ export class ClaudeSubagentExecutor {
947
998
  let availableIsolation = options.initialIsolation;
948
999
  let retainedIsolation;
949
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
+ };
950
1066
  const run = async (message, signal, continuation, currentToolUseId) => {
951
- await options.lifecycle?.write('running');
1067
+ let execution;
1068
+ let cancellation;
1069
+ let lifecycleTerminalized = false;
1070
+ let onAbort;
952
1071
  const startedAt = Date.now();
953
1072
  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
1073
  let result;
993
1074
  let failure;
994
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
+ };
995
1084
  try {
996
- result = await options.execute(isolation?.cwd ?? options.parentCwd, message, signal, continuation);
997
- }
998
- catch (error) {
999
- failure = error;
1000
- }
1001
- 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();
1002
1121
  try {
1003
- 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);
1004
1129
  }
1005
1130
  catch (error) {
1006
- failure ??= error;
1131
+ failure = error;
1007
1132
  }
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
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
1042
1175
  ? {
1043
- notification: {
1044
- id: notificationId,
1045
- status,
1046
- toolUseId: currentToolUseId,
1047
- error: detail,
1048
- },
1176
+ id: notificationId,
1177
+ status,
1178
+ toolUseId: currentToolUseId,
1179
+ error: detail,
1049
1180
  }
1050
- : {}),
1051
- });
1052
- if (!notificationExpected && !isolation && !cleanup?.reason) {
1053
- 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);
1054
1187
  }
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,
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,
1063
1219
  durationMs: Date.now() - startedAt,
1064
1220
  ...(isolation ? { isolationPath: isolation.cwd } : {}),
1065
1221
  ...(cleanup ? { isolationRetained: cleanup.retained } : {}),
1066
1222
  ...(cleanup?.reason ? { isolationWarning: cleanup.reason } : {}),
1067
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;
1068
1229
  const notificationId = notificationExpected &&
1069
1230
  !(signal.reason instanceof BackgroundAgentShutdownError)
1070
1231
  ? randomUUID()
1071
1232
  : 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
1233
+ await finishLifecycle(signal.aborted ? 'cancelled' : 'completed', finalResult, detail, notificationId
1107
1234
  ? {
1108
- notification: {
1109
- id: notificationId,
1110
- status,
1111
- toolUseId: currentToolUseId,
1112
- error: status === 'killed' ? (detail ?? null) : null,
1113
- },
1235
+ id: notificationId,
1236
+ status,
1237
+ toolUseId: currentToolUseId,
1238
+ error: status === 'killed' ? (detail ?? null) : null,
1114
1239
  }
1115
- : {}),
1116
- });
1117
- return {
1118
- ...finalResult,
1119
- ...(notificationId ? { notificationId } : {}),
1120
- };
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
+ }
1121
1275
  };
1122
1276
  return {
1123
1277
  run,
1124
1278
  markBackground: () => {
1125
1279
  notificationExpected = true;
1126
1280
  },
1281
+ disposeBeforeStart,
1282
+ lifecycle,
1127
1283
  };
1128
1284
  }
1129
1285
  async runWorkflowAgent(options) {
@@ -1405,7 +1561,27 @@ export class ClaudeSubagentExecutor {
1405
1561
  lastAssistant.content.trim().length > 0 &&
1406
1562
  (lastAssistant.toolCalls?.length ?? 0) === 0;
1407
1563
  const lifecycleStore = new SubagentLifecycleStore(paths.praxisRoot, sessionId, agentId, sidechainPaths.transcriptFile);
1408
- 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
+ }
1409
1585
  const lifecycleIsCurrent = lifecycle !== null && (await lifecycleStore.matchesTranscript(lifecycle));
1410
1586
  if (lifecycleIsCurrent && lifecycle.status === 'completed' && !completed) {
1411
1587
  throw new Error(`Background agent ${agentId} has completed lifecycle state but an incomplete sidechain`);
@@ -1484,6 +1660,8 @@ export class ClaudeSubagentExecutor {
1484
1660
  input,
1485
1661
  parentCwd: restoredCwd,
1486
1662
  lifecycle: lifecycleStore,
1663
+ recover: lifecycle.lifecycle.state === 'orphaned',
1664
+ initialLifecycleState: lifecycle.lifecycle.state,
1487
1665
  ...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
1488
1666
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
1489
1667
  execute: (cwd, message, signal, continuation) => sidechain.withLease(async (lease) => this.runSidechain({
@@ -1511,31 +1689,34 @@ export class ClaudeSubagentExecutor {
1511
1689
  toolUseId,
1512
1690
  outputFile: sidechainPaths.transcriptFile,
1513
1691
  resolvedModel: provider.model ?? 'praxis/provider',
1692
+ lifecycle: backgroundRun.lifecycle,
1514
1693
  run: backgroundRun.run,
1515
1694
  markBackground: backgroundRun.markBackground,
1516
1695
  acknowledgeNotification: (notificationId) => lifecycleStore.acknowledgeNotification(notificationId),
1517
1696
  prepareNotificationDetached: (notificationId, model) => lifecycleStore.prepareNotificationDetached(notificationId, model),
1518
1697
  confirmNotificationDetached: (notificationId) => lifecycleStore.confirmNotificationDetached(notificationId),
1519
1698
  };
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
- }
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
+ });
1539
1720
  for (const notification of lifecycle?.notifications ?? []) {
1540
1721
  if (notification.consumed)
1541
1722
  continue;
@@ -1553,7 +1734,7 @@ export class ClaudeSubagentExecutor {
1553
1734
  }
1554
1735
  this.background.registerPersistedNotification(agentId, {
1555
1736
  id: notification.id,
1556
- status: notification.status === 'killed' ? 'stopped' : notification.status,
1737
+ status: notification.status === 'killed' ? 'cancelled' : notification.status,
1557
1738
  result: notification.result,
1558
1739
  error: notification.error,
1559
1740
  toolUseId: notification.toolUseId,
@@ -59,6 +59,7 @@ export declare class TopLevelAgentManager {
59
59
  cwd?: string;
60
60
  all: boolean;
61
61
  }): Promise<TopLevelAgentSummary[]>;
62
+ private readLifecycleView;
62
63
  private nativeSessions;
63
64
  logs(id: string): Promise<string>;
64
65
  review(agent: Pick<TopLevelAgentSummary, 'id' | 'cwd' | 'sessionId'>): Promise<string>;