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.
- package/README.md +5 -1
- package/dist/application/background-agent-manager.d.ts +16 -6
- package/dist/application/background-agent-manager.js +139 -97
- package/dist/application/context-engine.d.ts +56 -0
- package/dist/application/context-engine.js +129 -0
- package/dist/application/session-service.d.ts +8 -4
- package/dist/application/session-service.js +528 -434
- package/dist/application/subagent-service.js +358 -177
- package/dist/application/top-level-agent-manager.d.ts +1 -0
- package/dist/application/top-level-agent-manager.js +1025 -403
- package/dist/application/transcript-projection.d.ts +33 -0
- package/dist/application/transcript-projection.js +110 -0
- package/dist/application/turn-lifecycle.d.ts +2 -0
- package/dist/application/turn-memory-coordinator.d.ts +43 -0
- package/dist/application/turn-memory-coordinator.js +98 -0
- package/dist/cli/agents-dashboard.js +10 -5
- package/dist/cli/interactive.js +264 -204
- package/dist/cli/tui/claude-style.js +154 -130
- package/dist/cli/tui/task-panel.js +10 -1
- package/dist/cli/tui/theme.d.ts +46 -3
- package/dist/cli/tui/theme.js +414 -56
- package/dist/cli/tui/tool-permission.js +9 -5
- package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
- package/dist/cli/tui/tui-interaction-router.js +127 -0
- package/dist/cli-runtime.d.ts +2 -2
- package/dist/core/agent-orchestration.d.ts +31 -0
- package/dist/core/agent-orchestration.js +235 -0
- package/dist/core/runtime.d.ts +5 -0
- package/dist/core/runtime.js +15 -1
- package/dist/persistence/claude-job-store.d.ts +58 -0
- package/dist/persistence/claude-job-store.js +483 -1
- package/dist/persistence/native-transcript-reader.d.ts +53 -0
- package/dist/persistence/native-transcript-reader.js +283 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
- package/dist/persistence/subagent-lifecycle-store.js +373 -146
- package/dist/platform/exclusive-file-lease.d.ts +3 -0
- package/dist/platform/exclusive-file-lease.js +3 -0
- 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
|
|
720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
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
|
|
1131
|
+
failure = error;
|
|
1007
1132
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
error: detail,
|
|
1048
|
-
},
|
|
1176
|
+
id: notificationId,
|
|
1177
|
+
status,
|
|
1178
|
+
toolUseId: currentToolUseId,
|
|
1179
|
+
error: detail,
|
|
1049
1180
|
}
|
|
1050
|
-
:
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
|
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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
|
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
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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' ? '
|
|
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>;
|