praxis-agent 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/application/background-agent-manager.d.ts +48 -3
- package/dist/application/background-agent-manager.js +249 -73
- package/dist/application/background-task-runtime.d.ts +20 -1
- package/dist/application/background-task-runtime.js +45 -6
- package/dist/application/managed-worktree.d.ts +8 -0
- package/dist/application/managed-worktree.js +54 -2
- package/dist/application/session-service.d.ts +12 -1
- package/dist/application/session-service.js +1849 -1628
- package/dist/application/subagent-service.d.ts +34 -7
- package/dist/application/subagent-service.js +720 -150
- package/dist/application/turn-lifecycle.d.ts +38 -0
- package/dist/application/turn-lifecycle.js +36 -0
- package/dist/cli/interactive.d.ts +1 -0
- package/dist/cli/interactive.js +43 -8
- package/dist/cli/protocol.js +5 -2
- package/dist/cli/tui/claude-style.d.ts +5 -48
- package/dist/cli/tui/claude-style.js +19 -73
- package/dist/cli/tui/task-panel.d.ts +1 -1
- package/dist/cli/tui/task-panel.js +2 -1
- package/dist/cli/tui/transcript-presentation.d.ts +89 -0
- package/dist/cli/tui/transcript-presentation.js +169 -0
- package/dist/cli/tui/transcript-viewport.d.ts +5 -34
- package/dist/cli/tui/transcript-viewport.js +639 -158
- package/dist/cli/tui/tui-view-model.d.ts +6 -2
- package/dist/cli/tui/tui-view-model.js +23 -6
- package/dist/compatibility/claude/context.d.ts +2 -2
- package/dist/compatibility/claude/context.js +2 -14
- package/dist/compatibility/claude/data-plane-adapter.d.ts +9 -0
- package/dist/compatibility/claude/data-plane-adapter.js +44 -0
- package/dist/compatibility/claude/paths.js +2 -2
- package/dist/compatibility/claude/projection.d.ts +4 -0
- package/dist/compatibility/claude/projection.js +82 -0
- package/dist/compatibility/claude/shared-resources.d.ts +7 -38
- package/dist/compatibility/claude/sidechain.d.ts +5 -0
- package/dist/compatibility/claude/sidechain.js +13 -4
- package/dist/compatibility/claude/transcript-codec.d.ts +21 -0
- package/dist/compatibility/claude/transcript-codec.js +512 -0
- package/dist/core/context.d.ts +25 -7
- package/dist/core/context.js +46 -0
- package/dist/core/prompt-composer.d.ts +8 -16
- package/dist/core/prompt-composer.js +65 -25
- package/dist/core/resources.d.ts +39 -0
- package/dist/core/resources.js +2 -0
- package/dist/core/runtime.d.ts +1 -1
- package/dist/core/session.d.ts +2 -0
- package/dist/core/session.js +5 -0
- package/dist/core/transcript-codec.d.ts +70 -0
- package/dist/core/transcript-codec.js +60 -0
- package/dist/core/transcript-event.d.ts +33 -0
- package/dist/core/transcript-event.js +198 -0
- package/dist/persistence/claude-sidechain-store.js +19 -3
- package/dist/persistence/data-plane-adapter.d.ts +32 -0
- package/dist/persistence/data-plane-adapter.js +2 -0
- package/dist/persistence/data-plane.d.ts +4 -20
- package/dist/persistence/data-plane.js +10 -47
- package/dist/persistence/native-data-plane-adapter.d.ts +9 -0
- package/dist/persistence/native-data-plane-adapter.js +39 -0
- package/dist/persistence/native-resources.d.ts +4 -4
- package/dist/persistence/native-transcript-codec.d.ts +13 -0
- package/dist/persistence/native-transcript-codec.js +102 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
- package/dist/persistence/subagent-lifecycle-store.js +299 -0
- package/dist/tools/claude-user-message.d.ts +0 -1
- package/dist/tools/claude-user-message.js +0 -1
- package/package.json +6 -2
|
@@ -6,6 +6,7 @@ import { isPathWithin } from '../platform/path-containment.js';
|
|
|
6
6
|
import { AGENT_COLOR_DEFAULT, agentColorMessage, getClaudeEffectiveAgentColor, } from '../compatibility/claude/agent-color.js';
|
|
7
7
|
import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
|
|
8
8
|
import { discoverClaudeProjectRoot, isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
|
|
9
|
+
import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
|
|
9
10
|
import { resolveDataPlanePaths, resolveScheduledTaskFile, } from '../persistence/data-plane.js';
|
|
10
11
|
import { downloadClaudeFileResources, } from '../compatibility/claude/file-resources.js';
|
|
11
12
|
import { createClaudeNativeFork } from '../compatibility/claude/fork.js';
|
|
@@ -20,17 +21,21 @@ import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, creat
|
|
|
20
21
|
import { AgentRunCancelledError, AgentRuntime, ModelProviderError, } from '../core/runtime.js';
|
|
21
22
|
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
22
23
|
import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
24
|
+
import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
|
|
23
25
|
import { usageCostUsd } from '../core/usage.js';
|
|
24
26
|
import { ContextBudget, ContextRecoveryPlanner, contextRecoveryMadeProgress, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
25
|
-
import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
27
|
+
import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
|
|
28
|
+
import { assembleContextSnapshot } from '../core/prompt-composer.js';
|
|
26
29
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
27
30
|
import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
|
|
28
31
|
import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
|
|
29
32
|
import { ClaudeSessionIndexCandidateError, readClaudeSessionIndexes, } from '../persistence/claude-session-index.js';
|
|
30
33
|
import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
|
|
34
|
+
import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
|
|
31
35
|
import { ModelCompactor } from './model-compactor.js';
|
|
32
36
|
import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
|
|
33
37
|
import { ScheduledPromptManager, } from './scheduled-prompt-manager.js';
|
|
38
|
+
import { TurnTerminalController } from './turn-lifecycle.js';
|
|
34
39
|
import { ClaudeScheduledToolRegistry } from '../tools/claude-scheduled-tools.js';
|
|
35
40
|
import { ClaudeTaskToolRegistry } from '../tools/claude-task-tools.js';
|
|
36
41
|
import { ClaudeWorkflowToolRegistry } from '../tools/claude-workflow-tools.js';
|
|
@@ -43,7 +48,7 @@ import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, }
|
|
|
43
48
|
import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
|
|
44
49
|
import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
|
|
45
50
|
import { generateToolUseSummary } from './tool-use-summary.js';
|
|
46
|
-
import { ClaudeUserMessageToolRegistry,
|
|
51
|
+
import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
|
|
47
52
|
function agentPermissionMode(mode) {
|
|
48
53
|
return mode === undefined || mode === 'manual' ? 'default' : mode;
|
|
49
54
|
}
|
|
@@ -51,6 +56,15 @@ function agentToolName(rule) {
|
|
|
51
56
|
const opening = rule.indexOf('(');
|
|
52
57
|
return (opening < 0 ? rule : rule.slice(0, opening)).trim();
|
|
53
58
|
}
|
|
59
|
+
function transcriptContainsBackgroundAgentNotification(entries, notification) {
|
|
60
|
+
const markers = backgroundAgentNotificationMarkers(notification);
|
|
61
|
+
return entries.some((entry) => {
|
|
62
|
+
if (entry.type !== 'user')
|
|
63
|
+
return false;
|
|
64
|
+
const source = JSON.stringify(entry.message);
|
|
65
|
+
return markers.every((marker) => source.includes(marker));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
54
68
|
function mainAgentToolNames(tools, agent) {
|
|
55
69
|
const requested = agent.tools ? new Set(agent.tools.map(agentToolName)) : null;
|
|
56
70
|
if (requested && agent.memory) {
|
|
@@ -636,8 +650,14 @@ export class ClaudeSessionService {
|
|
|
636
650
|
explicitResumeLeafUuids = new Map();
|
|
637
651
|
sessionPermissionUpdates = new Map();
|
|
638
652
|
hostedSubagents = new Set();
|
|
653
|
+
subagentExecutors = new Set();
|
|
639
654
|
hostedSubagentsByRegistry = new WeakMap();
|
|
655
|
+
subagentExecutorSessions = new WeakMap();
|
|
656
|
+
hostedSubagentNotificationPumps = new WeakMap();
|
|
657
|
+
hostedSubagentPumpPromises = new Set();
|
|
658
|
+
hostedSubagentNotificationReservations = new WeakSet();
|
|
640
659
|
backgroundNotificationWrites = new Map();
|
|
660
|
+
closing = false;
|
|
641
661
|
downloadedFileResourceSessions = new Set();
|
|
642
662
|
detachedHookRuns = new Map();
|
|
643
663
|
activeProvider;
|
|
@@ -795,6 +815,56 @@ export class ClaudeSessionService {
|
|
|
795
815
|
stopTask(sessionId, taskId) {
|
|
796
816
|
return this.backgroundTasks.stop(sessionId, taskId);
|
|
797
817
|
}
|
|
818
|
+
backgroundForegroundTask(sessionId) {
|
|
819
|
+
return this.backgroundTasks.backgroundForeground(sessionId);
|
|
820
|
+
}
|
|
821
|
+
sendOwnedBackgroundAgent(sessionId, agentId, message, summary, toolUseId) {
|
|
822
|
+
const sent = this.backgroundTasks.sendAgentWithOwner(sessionId, agentId, message, summary, toolUseId);
|
|
823
|
+
if (!sent)
|
|
824
|
+
return null;
|
|
825
|
+
const hosted = [...this.hostedSubagents].find((executor) => executor === sent.owner);
|
|
826
|
+
if (hosted)
|
|
827
|
+
this.startHostedSubagentNotificationPump(sessionId, hosted);
|
|
828
|
+
return sent.content;
|
|
829
|
+
}
|
|
830
|
+
sessionSubagentExecutors(sessionId, includeHosted = false) {
|
|
831
|
+
return [...this.subagentExecutors].filter((executor) => this.subagentExecutorSessions.get(executor) === sessionId &&
|
|
832
|
+
(includeHosted ||
|
|
833
|
+
(!this.hostedSubagentNotificationPumps.has(executor) &&
|
|
834
|
+
!this.hostedSubagentNotificationReservations.has(executor))));
|
|
835
|
+
}
|
|
836
|
+
async collectSubagentNotifications(sessionId, waitForExecutor) {
|
|
837
|
+
const executors = this.sessionSubagentExecutors(sessionId);
|
|
838
|
+
if (executors.length === 0)
|
|
839
|
+
return null;
|
|
840
|
+
const poll = () => Promise.all(executors.map((executor) => executor.notifications(false, false)));
|
|
841
|
+
let batches = await poll();
|
|
842
|
+
if (batches.every(({ messages }) => messages.length === 0)) {
|
|
843
|
+
if (waitForExecutor &&
|
|
844
|
+
executors.includes(waitForExecutor) &&
|
|
845
|
+
waitForExecutor.notificationClaimAgentIds().length > 0) {
|
|
846
|
+
await waitForExecutor.notifications(true, false);
|
|
847
|
+
batches = await poll();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const durationSeen = batches.some(({ durationApiMs, durationApiWithoutRetriesMs }) => durationApiMs !== undefined ||
|
|
851
|
+
durationApiWithoutRetriesMs !== undefined);
|
|
852
|
+
const modelUsage = mergeSessionRawModelUsage(...batches.map((batch) => batch.modelUsage));
|
|
853
|
+
return {
|
|
854
|
+
messages: batches.flatMap(({ messages }) => messages),
|
|
855
|
+
usage: batches.reduce((total, { usage }) => mergeUsage(total, usage), {
|
|
856
|
+
inputTokens: 0,
|
|
857
|
+
outputTokens: 0,
|
|
858
|
+
}),
|
|
859
|
+
...(modelUsage ? { modelUsage } : {}),
|
|
860
|
+
...(durationSeen
|
|
861
|
+
? {
|
|
862
|
+
durationApiMs: batches.reduce((total, batch) => addApiDuration(batch.durationApiMs, total, 'durationApiMs'), 0),
|
|
863
|
+
durationApiWithoutRetriesMs: batches.reduce((total, batch) => addApiDuration(batch.durationApiWithoutRetriesMs ?? batch.durationApiMs, total, 'durationApiWithoutRetriesMs'), 0),
|
|
864
|
+
}
|
|
865
|
+
: {}),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
798
868
|
async applyPermissionUpdates(sessionId, updates) {
|
|
799
869
|
if (updates.length === 0)
|
|
800
870
|
return;
|
|
@@ -808,16 +878,19 @@ export class ClaudeSessionService {
|
|
|
808
878
|
}
|
|
809
879
|
}
|
|
810
880
|
async close() {
|
|
881
|
+
this.closing = true;
|
|
811
882
|
await this.fileChangeWatcher?.close(5_000);
|
|
812
883
|
await this.hookLifecycle.close();
|
|
813
884
|
await this.drainDetachedHookRuns(5_000);
|
|
814
885
|
await this.options.hooks?.drainAsync(5_000);
|
|
815
886
|
this.scheduledPrompts?.close();
|
|
816
887
|
await this.options.projectMemoryExtraction?.close(5_000);
|
|
817
|
-
await Promise.all([...this.
|
|
888
|
+
await Promise.all([...this.subagentExecutors].map((executor) => executor.close()));
|
|
889
|
+
await Promise.allSettled([...this.hostedSubagentPumpPromises]);
|
|
818
890
|
await Promise.resolve();
|
|
819
891
|
await Promise.all([...this.backgroundNotificationWrites.values()]);
|
|
820
892
|
this.hostedSubagents.clear();
|
|
893
|
+
this.subagentExecutors.clear();
|
|
821
894
|
this.backgroundTasks.clear();
|
|
822
895
|
await Promise.all([...this.sessionMemoryControllers.values()].map((controller) => controller.close()));
|
|
823
896
|
this.sessionMemoryControllers.clear();
|
|
@@ -1050,11 +1123,15 @@ export class ClaudeSessionService {
|
|
|
1050
1123
|
backgroundTaskNotifications: (waitForRunning) => taskTools.notifications(waitForRunning),
|
|
1051
1124
|
}
|
|
1052
1125
|
: {}),
|
|
1126
|
+
stopOwnedBackgroundAgent: (ownerSessionId, agentId) => this.backgroundTasks.stopAgent(ownerSessionId, agentId),
|
|
1127
|
+
outputOwnedBackgroundAgent: (ownerSessionId, agentId, options) => this.backgroundTasks.outputAgent(ownerSessionId, agentId, options),
|
|
1128
|
+
sendOwnedBackgroundAgent: (ownerSessionId, agentId, message, summary, toolUseId) => this.sendOwnedBackgroundAgent(ownerSessionId, agentId, message, summary, toolUseId),
|
|
1053
1129
|
})
|
|
1054
1130
|
: null;
|
|
1055
|
-
if (subagentExecutor)
|
|
1056
|
-
this.hostedSubagents.add(subagentExecutor);
|
|
1057
1131
|
if (subagentExecutor) {
|
|
1132
|
+
this.hostedSubagents.add(subagentExecutor);
|
|
1133
|
+
this.subagentExecutors.add(subagentExecutor);
|
|
1134
|
+
this.subagentExecutorSessions.set(subagentExecutor, sessionId);
|
|
1058
1135
|
this.backgroundTasks.registerAgents(sessionId, subagentExecutor);
|
|
1059
1136
|
}
|
|
1060
1137
|
const agentTools = subagentExecutor
|
|
@@ -1157,19 +1234,67 @@ export class ClaudeSessionService {
|
|
|
1157
1234
|
}
|
|
1158
1235
|
async run(prompt, signal, sessionId = randomUUID(), name, images, documents) {
|
|
1159
1236
|
this.worktreeManager?.bindSession(sessionId);
|
|
1160
|
-
return this.executeTurn(
|
|
1237
|
+
return this.executeTurn({
|
|
1238
|
+
activation: {
|
|
1239
|
+
kind: 'start',
|
|
1240
|
+
sessionId,
|
|
1241
|
+
...(name !== undefined ? { name } : {}),
|
|
1242
|
+
},
|
|
1243
|
+
submission: {
|
|
1244
|
+
kind: 'prompt',
|
|
1245
|
+
text: prompt,
|
|
1246
|
+
...(images ? { images } : {}),
|
|
1247
|
+
...(documents ? { documents } : {}),
|
|
1248
|
+
},
|
|
1249
|
+
...(signal ? { signal } : {}),
|
|
1250
|
+
});
|
|
1161
1251
|
}
|
|
1162
1252
|
async runShell(command, signal, sessionId = randomUUID(), name) {
|
|
1163
1253
|
this.worktreeManager?.bindSession(sessionId);
|
|
1164
|
-
return this.executeTurn(
|
|
1254
|
+
return this.executeTurn({
|
|
1255
|
+
activation: {
|
|
1256
|
+
kind: 'start',
|
|
1257
|
+
sessionId,
|
|
1258
|
+
...(name !== undefined ? { name } : {}),
|
|
1259
|
+
},
|
|
1260
|
+
submission: { kind: 'shell', command },
|
|
1261
|
+
...(signal ? { signal } : {}),
|
|
1262
|
+
});
|
|
1165
1263
|
}
|
|
1166
1264
|
async resume(sessionId, prompt, signal, name, images, documents, resumeSessionAt) {
|
|
1167
1265
|
this.worktreeManager?.bindSession(sessionId);
|
|
1168
|
-
return this.executeTurn(
|
|
1266
|
+
return this.executeTurn({
|
|
1267
|
+
activation: {
|
|
1268
|
+
kind: 'resume',
|
|
1269
|
+
sessionId,
|
|
1270
|
+
...(name !== undefined ? { name } : {}),
|
|
1271
|
+
...(resumeSessionAt !== undefined
|
|
1272
|
+
? { atMessageId: resumeSessionAt }
|
|
1273
|
+
: {}),
|
|
1274
|
+
},
|
|
1275
|
+
submission: {
|
|
1276
|
+
kind: 'prompt',
|
|
1277
|
+
text: prompt,
|
|
1278
|
+
...(images ? { images } : {}),
|
|
1279
|
+
...(documents ? { documents } : {}),
|
|
1280
|
+
},
|
|
1281
|
+
...(signal ? { signal } : {}),
|
|
1282
|
+
});
|
|
1169
1283
|
}
|
|
1170
1284
|
async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
|
|
1171
1285
|
this.worktreeManager?.bindSession(sessionId);
|
|
1172
|
-
return this.executeTurn(
|
|
1286
|
+
return this.executeTurn({
|
|
1287
|
+
activation: {
|
|
1288
|
+
kind: 'resume',
|
|
1289
|
+
sessionId,
|
|
1290
|
+
...(name !== undefined ? { name } : {}),
|
|
1291
|
+
...(resumeSessionAt !== undefined
|
|
1292
|
+
? { atMessageId: resumeSessionAt }
|
|
1293
|
+
: {}),
|
|
1294
|
+
},
|
|
1295
|
+
submission: { kind: 'shell', command },
|
|
1296
|
+
...(signal ? { signal } : {}),
|
|
1297
|
+
});
|
|
1173
1298
|
}
|
|
1174
1299
|
async answerSideQuestion(sessionId, question, signal, onDelta, permissionMode = 'default') {
|
|
1175
1300
|
const prompt = question.trim();
|
|
@@ -1195,16 +1320,17 @@ export class ClaudeSessionService {
|
|
|
1195
1320
|
entries = loaded.value.entries;
|
|
1196
1321
|
this.restoreWorktree(entries);
|
|
1197
1322
|
}
|
|
1198
|
-
const assembledContext = await this.options.contextAssembler
|
|
1323
|
+
const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
|
|
1199
1324
|
cwd: this.activeCwd(),
|
|
1200
1325
|
lifecycleId: activeSessionId,
|
|
1201
1326
|
});
|
|
1327
|
+
const contextProjection = projectContextSnapshot(assembledContext);
|
|
1202
1328
|
const messages = [
|
|
1203
|
-
...
|
|
1329
|
+
...contextProjection.systemMessages,
|
|
1204
1330
|
...injectFirstUserMessageContext([
|
|
1205
1331
|
...projectClaudeModelMessages(entries),
|
|
1206
1332
|
{ role: 'user', content: prompt },
|
|
1207
|
-
],
|
|
1333
|
+
], contextProjection.firstUserMessageContext),
|
|
1208
1334
|
];
|
|
1209
1335
|
const budget = this.contextBudget(provider);
|
|
1210
1336
|
if (budget)
|
|
@@ -1215,6 +1341,7 @@ export class ClaudeSessionService {
|
|
|
1215
1341
|
: 'praxis/provider';
|
|
1216
1342
|
const metrics = await completeMeteredModelRequest(provider, {
|
|
1217
1343
|
messages,
|
|
1344
|
+
stableSystemMessageCount: contextProjection.stableSystemSectionCount,
|
|
1218
1345
|
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
1219
1346
|
...(signal ? { signal } : {}),
|
|
1220
1347
|
}, {
|
|
@@ -1257,6 +1384,9 @@ export class ClaudeSessionService {
|
|
|
1257
1384
|
.join('-')
|
|
1258
1385
|
.slice(0, 64) || 'side-question';
|
|
1259
1386
|
const registry = this.createHostedToolRegistry(sessionId);
|
|
1387
|
+
const executor = this.hostedSubagentsByRegistry.get(registry);
|
|
1388
|
+
if (executor)
|
|
1389
|
+
this.hostedSubagentNotificationReservations.add(executor);
|
|
1260
1390
|
const call = {
|
|
1261
1391
|
id: randomUUID(),
|
|
1262
1392
|
name: 'Agent',
|
|
@@ -1272,26 +1402,69 @@ export class ClaudeSessionService {
|
|
|
1272
1402
|
cwd: this.activeCwd(),
|
|
1273
1403
|
...(signal ? { signal } : {}),
|
|
1274
1404
|
};
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1405
|
+
try {
|
|
1406
|
+
const prepared = await registry.prepare(call, context);
|
|
1407
|
+
const result = await registry.execute(prepared, context);
|
|
1408
|
+
const agentId = result.nativeToolUseResult?.agentId;
|
|
1409
|
+
if (result.isError || typeof agentId !== 'string') {
|
|
1410
|
+
throw new Error(result.content || 'Could not fork side question');
|
|
1411
|
+
}
|
|
1412
|
+
await this.appendSystemLocalCommand(sessionId, 'btw', prompt, `⑂ forked ${name} (${agentId.slice(-4)})`);
|
|
1413
|
+
if (executor) {
|
|
1414
|
+
this.startHostedSubagentNotificationPump(sessionId, executor);
|
|
1415
|
+
}
|
|
1416
|
+
return { agentId, name };
|
|
1280
1417
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
.then(({ messages }) => this.enqueueBackgroundNotifications(sessionId, messages))
|
|
1287
|
-
.catch((error) => this.options.eventSink?.({
|
|
1288
|
-
type: 'warning',
|
|
1289
|
-
message: error instanceof Error
|
|
1290
|
-
? error.message
|
|
1291
|
-
: `Could not persist background notification: ${String(error)}`,
|
|
1292
|
-
}));
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
if (executor) {
|
|
1420
|
+
this.hostedSubagentNotificationReservations.delete(executor);
|
|
1421
|
+
}
|
|
1422
|
+
throw error;
|
|
1293
1423
|
}
|
|
1294
|
-
|
|
1424
|
+
}
|
|
1425
|
+
startHostedSubagentNotificationPump(sessionId, executor) {
|
|
1426
|
+
this.hostedSubagentNotificationReservations.delete(executor);
|
|
1427
|
+
if (this.hostedSubagentNotificationPumps.has(executor))
|
|
1428
|
+
return;
|
|
1429
|
+
const pump = (async () => {
|
|
1430
|
+
while (!this.closing) {
|
|
1431
|
+
const reconciled = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
1432
|
+
const snapshot = await lease.load();
|
|
1433
|
+
await executor.reconcileDetachedNotifications((notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification));
|
|
1434
|
+
});
|
|
1435
|
+
if (reconciled.status !== 'completed') {
|
|
1436
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const { messages } = await executor.notifications(true, false);
|
|
1440
|
+
if (messages.length === 0)
|
|
1441
|
+
return;
|
|
1442
|
+
await executor.prepareNotificationsDetached(messages);
|
|
1443
|
+
if (!(await this.enqueueBackgroundNotifications(sessionId, messages, async (message) => {
|
|
1444
|
+
await executor.confirmNotificationsDetached([message]);
|
|
1445
|
+
await executor.acknowledgeNotifications([message]);
|
|
1446
|
+
}))) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (executor.notificationClaimAgentIds().length === 0)
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
})()
|
|
1453
|
+
.catch((error) => this.options.eventSink?.({
|
|
1454
|
+
type: 'warning',
|
|
1455
|
+
message: error instanceof Error
|
|
1456
|
+
? error.message
|
|
1457
|
+
: `Could not persist background notification: ${String(error)}`,
|
|
1458
|
+
}))
|
|
1459
|
+
.finally(() => {
|
|
1460
|
+
if (this.hostedSubagentNotificationPumps.get(executor) === pump) {
|
|
1461
|
+
this.hostedSubagentNotificationPumps.delete(executor);
|
|
1462
|
+
}
|
|
1463
|
+
this.hostedSubagentPumpPromises.delete(pump);
|
|
1464
|
+
});
|
|
1465
|
+
this.hostedSubagentNotificationPumps.set(executor, pump);
|
|
1466
|
+
this.hostedSubagentPumpPromises.add(pump);
|
|
1467
|
+
void pump;
|
|
1295
1468
|
}
|
|
1296
1469
|
async promptSuggestion(sessionId, signal) {
|
|
1297
1470
|
const loaded = this.options.sessionPersistence === false
|
|
@@ -1309,27 +1482,21 @@ export class ClaudeSessionService {
|
|
|
1309
1482
|
const provider = this.providerForAgent(agent);
|
|
1310
1483
|
this.activeProvider = provider;
|
|
1311
1484
|
const agentSystem = await this.mainAgentSystemPrompt(agent);
|
|
1312
|
-
const assembledContext = await this.options.contextAssembler
|
|
1485
|
+
const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
|
|
1313
1486
|
cwd: this.activeCwd(),
|
|
1314
1487
|
lifecycleId: sessionId,
|
|
1315
|
-
...(agentSystem
|
|
1488
|
+
...(agentSystem
|
|
1489
|
+
? { mode: 'agent', baseSystemPrompt: agentSystem }
|
|
1490
|
+
: {}),
|
|
1316
1491
|
});
|
|
1317
|
-
const
|
|
1318
|
-
const
|
|
1319
|
-
? (assembledContext?.systemMessages ?? [])
|
|
1320
|
-
: this.assembledSystemMessages(agent, assembledContext?.systemMessages ?? []);
|
|
1321
|
-
const contextMessages = [
|
|
1322
|
-
...(!hasPromptManifest && agentSystem
|
|
1323
|
-
? [{ role: 'system', content: agentSystem }]
|
|
1324
|
-
: []),
|
|
1325
|
-
...assembledSystemMessages,
|
|
1326
|
-
];
|
|
1492
|
+
const contextProjection = projectContextSnapshot(assembledContext);
|
|
1493
|
+
const contextMessages = [...contextProjection.systemMessages];
|
|
1327
1494
|
const messages = [
|
|
1328
1495
|
...contextMessages,
|
|
1329
1496
|
...injectFirstUserMessageContext([
|
|
1330
1497
|
...projectClaudeModelMessages(entries.entries),
|
|
1331
1498
|
{ role: 'user', content: PROMPT_SUGGESTION_INSTRUCTION },
|
|
1332
|
-
],
|
|
1499
|
+
], contextProjection.firstUserMessageContext),
|
|
1333
1500
|
];
|
|
1334
1501
|
const suggestionTools = agent && this.options.tools
|
|
1335
1502
|
? new FilteredToolRegistry(this.options.tools, {
|
|
@@ -1339,6 +1506,7 @@ export class ClaudeSessionService {
|
|
|
1339
1506
|
await this.activateSessionCostTracker(sessionId);
|
|
1340
1507
|
const metrics = await completeMeteredModelRequest(provider, {
|
|
1341
1508
|
messages,
|
|
1509
|
+
stableSystemMessageCount: contextProjection.stableSystemSectionCount,
|
|
1342
1510
|
...(provider.capabilities.tools
|
|
1343
1511
|
? { tools: suggestionTools?.definitions() ?? [] }
|
|
1344
1512
|
: {}),
|
|
@@ -1866,7 +2034,11 @@ export class ClaudeSessionService {
|
|
|
1866
2034
|
}
|
|
1867
2035
|
async retryRecentlyDenied(sessionId, display, signal) {
|
|
1868
2036
|
await this.appendPermissionGrant(sessionId, display, true);
|
|
1869
|
-
return this.executeTurn(
|
|
2037
|
+
return this.executeTurn({
|
|
2038
|
+
activation: { kind: 'resume', sessionId },
|
|
2039
|
+
submission: { kind: 'retry', prompt: '/permissions' },
|
|
2040
|
+
...(signal ? { signal } : {}),
|
|
2041
|
+
});
|
|
1870
2042
|
}
|
|
1871
2043
|
async recordBtwUsage(sessionId, permissionMode = 'default') {
|
|
1872
2044
|
const activeSessionId = await this.ensureLocalSession(sessionId, permissionMode);
|
|
@@ -2428,8 +2600,9 @@ export class ClaudeSessionService {
|
|
|
2428
2600
|
}
|
|
2429
2601
|
async costSnapshot(sessionId) {
|
|
2430
2602
|
const existing = this.sessionCostTrackers.get(sessionId);
|
|
2431
|
-
if (existing)
|
|
2432
|
-
return existing.snapshot();
|
|
2603
|
+
if (existing) {
|
|
2604
|
+
return this.withDetachedSubagentUsage(sessionId, existing.snapshot());
|
|
2605
|
+
}
|
|
2433
2606
|
const store = this.options.costStateStore;
|
|
2434
2607
|
const loaded = store ? await store.load(sessionId) : null;
|
|
2435
2608
|
const tracker = new ClaudeSessionCostTracker({
|
|
@@ -2437,1679 +2610,1727 @@ export class ClaudeSessionService {
|
|
|
2437
2610
|
...(loaded ? { restored: loaded } : {}),
|
|
2438
2611
|
});
|
|
2439
2612
|
this.sessionCostTrackers.set(sessionId, tracker);
|
|
2440
|
-
return tracker.snapshot();
|
|
2613
|
+
return this.withDetachedSubagentUsage(sessionId, tracker.snapshot());
|
|
2441
2614
|
}
|
|
2442
|
-
async
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2615
|
+
async withDetachedSubagentUsage(sessionId, base) {
|
|
2616
|
+
const tracker = new ClaudeSessionCostTracker({
|
|
2617
|
+
sessionId,
|
|
2618
|
+
restored: base,
|
|
2619
|
+
});
|
|
2620
|
+
const directory = join(this.paths(sessionId).praxisRoot, 'subagent-lifecycle', sessionId);
|
|
2621
|
+
let entries;
|
|
2622
|
+
try {
|
|
2623
|
+
entries = await readdir(directory);
|
|
2451
2624
|
}
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2625
|
+
catch (error) {
|
|
2626
|
+
if (error.code === 'ENOENT')
|
|
2627
|
+
return base;
|
|
2628
|
+
throw error;
|
|
2455
2629
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2630
|
+
for (const file of entries) {
|
|
2631
|
+
const agentId = file.endsWith('.json') ? file.slice(0, -5) : '';
|
|
2632
|
+
if (!isClaudeAgentId(agentId))
|
|
2633
|
+
continue;
|
|
2634
|
+
try {
|
|
2635
|
+
const lifecycle = await new SubagentLifecycleStore(this.paths(sessionId).praxisRoot, sessionId, agentId).read();
|
|
2636
|
+
for (const notification of lifecycle?.notifications ?? []) {
|
|
2637
|
+
if (notification.accounting?.kind !== 'detached' ||
|
|
2638
|
+
!notification.accounting.delivered ||
|
|
2639
|
+
notification.result === null) {
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
const rows = Object.entries(notification.result.modelUsage ?? {
|
|
2643
|
+
[notification.accounting.model]: notification.result.usage,
|
|
2644
|
+
});
|
|
2645
|
+
if (rows.length === 0) {
|
|
2646
|
+
tracker.recordDurations({
|
|
2647
|
+
...(notification.result.durationApiMs === undefined
|
|
2648
|
+
? {}
|
|
2649
|
+
: { apiDurationMs: notification.result.durationApiMs }),
|
|
2650
|
+
...(notification.result.durationApiWithoutRetriesMs === undefined
|
|
2651
|
+
? {}
|
|
2652
|
+
: {
|
|
2653
|
+
durationApiWithoutRetriesMs: notification.result.durationApiWithoutRetriesMs,
|
|
2654
|
+
}),
|
|
2655
|
+
});
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
for (const [index, [model, usage]] of rows.entries()) {
|
|
2659
|
+
const pricing = this.options.pricing?.resolve(model);
|
|
2660
|
+
const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
2661
|
+
tracker.recordTurn({
|
|
2662
|
+
model,
|
|
2663
|
+
usage,
|
|
2664
|
+
...(costUsd === undefined ? {} : { costUsd }),
|
|
2665
|
+
...(usage.webSearchRequests === undefined
|
|
2666
|
+
? {}
|
|
2667
|
+
: { webSearchRequests: usage.webSearchRequests }),
|
|
2668
|
+
...(index === 0
|
|
2669
|
+
? {
|
|
2670
|
+
apiDurationMs: notification.result.durationApiMs,
|
|
2671
|
+
apiDurationWithoutRetriesMs: notification.result.durationApiWithoutRetriesMs,
|
|
2672
|
+
}
|
|
2673
|
+
: {}),
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2480
2676
|
}
|
|
2481
2677
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
const persisted = await this.store(sessionId).load();
|
|
2488
|
-
if (persisted.entries.length > 0) {
|
|
2489
|
-
const imported = await store.create(persisted.entries);
|
|
2490
|
-
if (imported.status === 'created')
|
|
2491
|
-
snapshot = await lease.load();
|
|
2492
|
-
}
|
|
2678
|
+
catch (error) {
|
|
2679
|
+
this.options.eventSink?.({
|
|
2680
|
+
type: 'warning',
|
|
2681
|
+
message: `Could not account detached subagent ${agentId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2682
|
+
});
|
|
2493
2683
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2684
|
+
}
|
|
2685
|
+
return tracker.snapshot();
|
|
2686
|
+
}
|
|
2687
|
+
async executeTurn(request) {
|
|
2688
|
+
const { activation, submission, signal } = request;
|
|
2689
|
+
const sessionId = activation.sessionId;
|
|
2690
|
+
const requireExisting = activation.kind === 'resume';
|
|
2691
|
+
const name = activation.name;
|
|
2692
|
+
const resumeSessionAt = activation.kind === 'resume' ? activation.atMessageId : undefined;
|
|
2693
|
+
const prompt = submission.kind === 'shell'
|
|
2694
|
+
? `! ${submission.command}`
|
|
2695
|
+
: submission.kind === 'retry'
|
|
2696
|
+
? submission.prompt
|
|
2697
|
+
: submission.text;
|
|
2698
|
+
const images = submission.kind === 'prompt' ? (submission.images ?? []) : [];
|
|
2699
|
+
const documents = submission.kind === 'prompt' ? (submission.documents ?? []) : [];
|
|
2700
|
+
const shellCommand = submission.kind === 'shell' ? submission.command : undefined;
|
|
2701
|
+
const skipUserPrompt = submission.kind === 'retry';
|
|
2702
|
+
const controller = new TurnTerminalController(this.options.eventSink ?? (() => undefined));
|
|
2703
|
+
try {
|
|
2704
|
+
this.assertWritable();
|
|
2705
|
+
if (prompt.length === 0 && images.length === 0 && documents.length === 0)
|
|
2706
|
+
throw new Error('Prompt must not be empty');
|
|
2707
|
+
if (name !== undefined && name.length === 0) {
|
|
2708
|
+
throw new Error('Session name must not be empty');
|
|
2496
2709
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
snapshot = {
|
|
2500
|
-
entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
|
|
2501
|
-
tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
|
|
2502
|
-
};
|
|
2710
|
+
if (shellCommand !== undefined && shellCommand.trim().length === 0) {
|
|
2711
|
+
throw new Error('Shell command must not be empty');
|
|
2503
2712
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
this.
|
|
2507
|
-
snapshot = {
|
|
2508
|
-
entries: selected.entries,
|
|
2509
|
-
tail: { ...snapshot.tail, branchParentUuid: selected.leafUuid },
|
|
2510
|
-
};
|
|
2713
|
+
await this.activateSessionCostTracker(sessionId);
|
|
2714
|
+
if (this.options.sessionPersistence !== false) {
|
|
2715
|
+
this.durableMetadataSessions.add(sessionId);
|
|
2511
2716
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
!skipUserPrompt &&
|
|
2517
|
-
!(this.options.approveRecovery !== undefined &&
|
|
2518
|
-
findUnresolvedClaudeToolCalls(snapshot.entries).length > 0)) {
|
|
2519
|
-
const interruption = classifyClaudeInterruption(snapshot.entries);
|
|
2520
|
-
if ((interruption.kind === 'interrupted-prompt' ||
|
|
2521
|
-
interruption.kind === 'interrupted-turn') &&
|
|
2522
|
-
interruption.prompt !== undefined &&
|
|
2523
|
-
interruption.replayEntries !== undefined) {
|
|
2524
|
-
automaticReplayPrompt = interruption.prompt;
|
|
2525
|
-
snapshot = {
|
|
2526
|
-
entries: interruption.replayEntries,
|
|
2527
|
-
tail: {
|
|
2528
|
-
...snapshot.tail,
|
|
2529
|
-
branchParentUuid: interruption.replayParentUuid ?? null,
|
|
2530
|
-
},
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2717
|
+
await this.ensureFileResources(sessionId, signal);
|
|
2718
|
+
this.worktreeManager?.bindSession(sessionId);
|
|
2719
|
+
if (this.options.initialWorktree) {
|
|
2720
|
+
await this.worktreeManager?.ensureInitial(this.options.initialWorktreeName);
|
|
2533
2721
|
}
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
worktreeSession: initialTransition.state,
|
|
2539
|
-
sessionId,
|
|
2540
|
-
};
|
|
2541
|
-
const stateTail = await this.append(lease, snapshot.tail, stateEntry);
|
|
2542
|
-
snapshot = {
|
|
2543
|
-
entries: [...snapshot.entries, stateEntry],
|
|
2544
|
-
tail: stateTail,
|
|
2545
|
-
};
|
|
2546
|
-
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2722
|
+
const pinnedCwd = this.sessionCwds.get(sessionId) ?? this.activeCwd();
|
|
2723
|
+
this.sessionCwds.set(sessionId, pinnedCwd);
|
|
2724
|
+
if (this.options.workspace?.cwd() !== pinnedCwd) {
|
|
2725
|
+
this.options.workspace?.setCwd(pinnedCwd);
|
|
2547
2726
|
}
|
|
2548
|
-
this.
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2727
|
+
this.runtimeCwd = pinnedCwd;
|
|
2728
|
+
if (requireExisting) {
|
|
2729
|
+
await this.discoverProjectRoot(sessionId);
|
|
2730
|
+
}
|
|
2731
|
+
const sessionPaths = this.paths(sessionId);
|
|
2732
|
+
const toolResultDirectory = join(sessionPaths.projectRoot, sessionId, 'tool-results');
|
|
2733
|
+
const store = this.turnStore(sessionId);
|
|
2734
|
+
const leaseResult = await store.withLease(async (lease) => {
|
|
2735
|
+
if (!requireExisting) {
|
|
2736
|
+
const initialization = name === undefined
|
|
2737
|
+
? await store.reserve()
|
|
2738
|
+
: await store.create(this.sessionNameEntries(sessionId, name));
|
|
2739
|
+
if (initialization.status === 'conflict') {
|
|
2740
|
+
throw new Error(`Session ID ${sessionId} is already in use`);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
let snapshot = await lease.load();
|
|
2744
|
+
let automaticReplayPrompt;
|
|
2745
|
+
if (requireExisting &&
|
|
2746
|
+
this.options.sessionPersistence === false &&
|
|
2747
|
+
snapshot.entries.length === 0) {
|
|
2748
|
+
const persisted = await this.store(sessionId).load();
|
|
2749
|
+
if (persisted.entries.length > 0) {
|
|
2750
|
+
const imported = await store.create(persisted.entries);
|
|
2751
|
+
if (imported.status === 'created')
|
|
2752
|
+
snapshot = await lease.load();
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
if (requireExisting && snapshot.entries.length === 0) {
|
|
2756
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
2557
2757
|
}
|
|
2558
|
-
snapshot = {
|
|
2559
|
-
entries: [...snapshot.entries, ...entries],
|
|
2560
|
-
tail: appendResult.tail,
|
|
2561
|
-
};
|
|
2562
2758
|
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
(
|
|
2570
|
-
|
|
2571
|
-
|
|
2759
|
+
if (resumeSessionAt !== undefined) {
|
|
2760
|
+
snapshot = {
|
|
2761
|
+
entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
|
|
2762
|
+
tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
else if (this.explicitResumeLeafUuids.has(sessionId)) {
|
|
2766
|
+
const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
|
|
2767
|
+
this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
|
|
2768
|
+
snapshot = {
|
|
2769
|
+
entries: selected.entries,
|
|
2770
|
+
tail: { ...snapshot.tail, branchParentUuid: selected.leafUuid },
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
if (requireExisting &&
|
|
2774
|
+
resumeSessionAt === undefined &&
|
|
2775
|
+
this.options.resumeInterruptedTurn === true &&
|
|
2572
2776
|
shellCommand === undefined &&
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
let lastAssistantUuid = null;
|
|
2591
|
-
const fileHistory = this.options.fileCheckpointing &&
|
|
2592
|
-
this.options.sessionPersistence !== false
|
|
2593
|
-
? new ClaudeFileHistory(this.options.configRoot, sessionId, [
|
|
2594
|
-
this.activeCwd(),
|
|
2595
|
-
...(this.options.fileRewindRoots ?? []),
|
|
2596
|
-
])
|
|
2597
|
-
: null;
|
|
2598
|
-
const unresolvedToolCalls = findUnresolvedClaudeToolCalls(snapshot.entries);
|
|
2599
|
-
const pendingRecoveryToolCallIds = new Set(unresolvedToolCalls.map((call) => call.id));
|
|
2600
|
-
const pendingRecoveryHookOutcomes = [];
|
|
2601
|
-
const currentHookCwd = () => this.activeCwd();
|
|
2602
|
-
const hookSession = {
|
|
2603
|
-
session_id: sessionId,
|
|
2604
|
-
transcript_path: sessionPaths.sessionFile,
|
|
2605
|
-
get cwd() {
|
|
2606
|
-
return currentHookCwd();
|
|
2607
|
-
},
|
|
2608
|
-
permission_mode: 'default',
|
|
2609
|
-
};
|
|
2610
|
-
const appendHookOutcome = async (outcome) => {
|
|
2611
|
-
for (const entry of createClaudeHookAttachmentEntries(outcome, this.translationContext(sessionId, snapshot))) {
|
|
2612
|
-
const tail = await this.append(lease, snapshot.tail, entry);
|
|
2613
|
-
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
2777
|
+
!skipUserPrompt &&
|
|
2778
|
+
!(this.options.approveRecovery !== undefined &&
|
|
2779
|
+
findUnresolvedClaudeToolCalls(snapshot.entries).length > 0)) {
|
|
2780
|
+
const interruption = classifyClaudeInterruption(snapshot.entries);
|
|
2781
|
+
if ((interruption.kind === 'interrupted-prompt' ||
|
|
2782
|
+
interruption.kind === 'interrupted-turn') &&
|
|
2783
|
+
interruption.prompt !== undefined &&
|
|
2784
|
+
interruption.replayEntries !== undefined) {
|
|
2785
|
+
automaticReplayPrompt = interruption.prompt;
|
|
2786
|
+
snapshot = {
|
|
2787
|
+
entries: interruption.replayEntries,
|
|
2788
|
+
tail: {
|
|
2789
|
+
...snapshot.tail,
|
|
2790
|
+
branchParentUuid: interruption.replayParentUuid ?? null,
|
|
2791
|
+
},
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2614
2794
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2795
|
+
const initialTransition = this.worktreeManager?.consumeTransition('__initial__');
|
|
2796
|
+
if (initialTransition && snapshot.entries.length === 0) {
|
|
2797
|
+
const stateEntry = {
|
|
2798
|
+
type: 'worktree-state',
|
|
2799
|
+
worktreeSession: initialTransition.state,
|
|
2800
|
+
sessionId,
|
|
2801
|
+
};
|
|
2802
|
+
const stateTail = await this.append(lease, snapshot.tail, stateEntry);
|
|
2803
|
+
snapshot = {
|
|
2804
|
+
entries: [...snapshot.entries, stateEntry],
|
|
2805
|
+
tail: stateTail,
|
|
2806
|
+
};
|
|
2807
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2620
2808
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
if (typeof lastEntry?.uuid === 'string')
|
|
2637
|
-
parentUuid = lastEntry.uuid;
|
|
2809
|
+
this.restoreWorktree(snapshot.entries);
|
|
2810
|
+
this.options.interactiveTools?.restore(sessionId, snapshot.entries);
|
|
2811
|
+
if (requireExisting &&
|
|
2812
|
+
name !== undefined &&
|
|
2813
|
+
!this.hasSessionName(snapshot.entries, name)) {
|
|
2814
|
+
const entries = this.sessionNameEntries(sessionId, name);
|
|
2815
|
+
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
2816
|
+
if (appendResult.status === 'conflict') {
|
|
2817
|
+
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
2818
|
+
}
|
|
2819
|
+
snapshot = {
|
|
2820
|
+
entries: [...snapshot.entries, ...entries],
|
|
2821
|
+
tail: appendResult.tail,
|
|
2822
|
+
};
|
|
2823
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2638
2824
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2825
|
+
const agentName = this.options.agent ?? getClaudeAgentSetting(snapshot.entries);
|
|
2826
|
+
const agent = this.resolveAgent(agentName);
|
|
2827
|
+
const provider = this.providerForAgent(agent);
|
|
2828
|
+
this.activeProvider = provider;
|
|
2829
|
+
const effectivePrompt = automaticReplayPrompt ??
|
|
2830
|
+
(!requireExisting &&
|
|
2831
|
+
!skipUserPrompt &&
|
|
2832
|
+
!this.options.agentInitialPromptHandledExternally &&
|
|
2833
|
+
shellCommand === undefined &&
|
|
2834
|
+
agent?.initialPrompt
|
|
2835
|
+
? `${agent.initialPrompt}\n\n${prompt}`
|
|
2836
|
+
: prompt);
|
|
2837
|
+
const projectMemoryRecallTurn = !skipUserPrompt && shellCommand === undefined
|
|
2838
|
+
? this.options.projectMemoryRecall?.prefetch({
|
|
2839
|
+
sessionId,
|
|
2840
|
+
turnId: randomUUID(),
|
|
2841
|
+
prompt: effectivePrompt,
|
|
2842
|
+
...(signal ? { signal } : {}),
|
|
2843
|
+
})
|
|
2844
|
+
: undefined;
|
|
2845
|
+
const initialPricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
|
|
2846
|
+
if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
|
|
2847
|
+
throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
|
|
2642
2848
|
}
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
2849
|
+
const shellInputUuid = shellCommand === undefined ? null : randomUUID();
|
|
2850
|
+
let currentPromptId = shellInputUuid;
|
|
2851
|
+
let lastAssistantUuid = null;
|
|
2852
|
+
const fileHistory = this.options.fileCheckpointing &&
|
|
2853
|
+
this.options.sessionPersistence !== false
|
|
2854
|
+
? new ClaudeFileHistory(this.options.configRoot, sessionId, [
|
|
2855
|
+
this.activeCwd(),
|
|
2856
|
+
...(this.options.fileRewindRoots ?? []),
|
|
2857
|
+
])
|
|
2858
|
+
: null;
|
|
2859
|
+
const unresolvedToolCalls = findUnresolvedClaudeToolCalls(snapshot.entries);
|
|
2860
|
+
const pendingRecoveryToolCallIds = new Set(unresolvedToolCalls.map((call) => call.id));
|
|
2861
|
+
const pendingRecoveryHookOutcomes = [];
|
|
2862
|
+
const currentHookCwd = () => this.activeCwd();
|
|
2863
|
+
const hookSession = {
|
|
2864
|
+
session_id: sessionId,
|
|
2865
|
+
transcript_path: sessionPaths.sessionFile,
|
|
2866
|
+
get cwd() {
|
|
2867
|
+
return currentHookCwd();
|
|
2868
|
+
},
|
|
2869
|
+
permission_mode: 'default',
|
|
2870
|
+
};
|
|
2871
|
+
const appendHookOutcome = async (outcome) => {
|
|
2872
|
+
for (const entry of createClaudeHookAttachmentEntries(outcome, this.translationContext(sessionId, snapshot))) {
|
|
2873
|
+
const tail = await this.append(lease, snapshot.tail, entry);
|
|
2874
|
+
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
const recordHookOutcome = async (outcome, deferUntilApproval = false) => {
|
|
2878
|
+
if (deferUntilApproval) {
|
|
2879
|
+
pendingRecoveryHookOutcomes.push(outcome);
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
await appendHookOutcome(outcome);
|
|
2883
|
+
};
|
|
2884
|
+
const flushRecoveryHookOutcomes = async () => {
|
|
2885
|
+
const entries = [];
|
|
2886
|
+
let history = snapshot.entries;
|
|
2887
|
+
let parentUuid = this.logicalTailUuid(snapshot.tail);
|
|
2888
|
+
for (const outcome of pendingRecoveryHookOutcomes) {
|
|
2889
|
+
const outcomeEntries = createClaudeHookAttachmentEntries(outcome, {
|
|
2890
|
+
...this.translationContext(sessionId, snapshot),
|
|
2891
|
+
parentUuid,
|
|
2892
|
+
history,
|
|
2893
|
+
});
|
|
2894
|
+
entries.push(...outcomeEntries);
|
|
2895
|
+
history = [...history, ...outcomeEntries];
|
|
2896
|
+
const lastEntry = outcomeEntries.at(-1);
|
|
2897
|
+
if (typeof lastEntry?.uuid === 'string')
|
|
2898
|
+
parentUuid = lastEntry.uuid;
|
|
2899
|
+
}
|
|
2900
|
+
if (entries.length === 0) {
|
|
2901
|
+
pendingRecoveryHookOutcomes.length = 0;
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
2905
|
+
if (appendResult.status === 'conflict') {
|
|
2906
|
+
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
2907
|
+
}
|
|
2908
|
+
snapshot = { entries: history, tail: appendResult.tail };
|
|
2909
|
+
pendingRecoveryHookOutcomes.length = 0;
|
|
2910
|
+
};
|
|
2911
|
+
const capabilities = this.toolCapabilities();
|
|
2912
|
+
const taskToolNames = this.capabilityToolNames(this.options.taskToolNames, capabilities);
|
|
2913
|
+
const scheduledToolNames = this.capabilityToolNames(this.options.scheduledToolNames, capabilities);
|
|
2914
|
+
const taskTools = this.options.tools && taskToolNames.length > 0
|
|
2915
|
+
? new ClaudeTaskToolRegistry({
|
|
2916
|
+
base: this.options.tools,
|
|
2917
|
+
cwd: this.activeCwd(),
|
|
2918
|
+
cwdProvider: () => this.activeCwd(),
|
|
2919
|
+
praxisRoot: sessionPaths.praxisRoot,
|
|
2920
|
+
sessionId,
|
|
2921
|
+
taskRoot: sessionPaths.taskRoot,
|
|
2922
|
+
...(this.options.eventSink
|
|
2923
|
+
? { eventSink: this.options.eventSink }
|
|
2924
|
+
: {}),
|
|
2925
|
+
enabledTools: taskToolNames,
|
|
2926
|
+
...(this.options.hooks
|
|
2927
|
+
? {
|
|
2928
|
+
taskHooks: {
|
|
2929
|
+
created: async (task, taskSignal) => {
|
|
2930
|
+
const outcome = await this.options.hooks?.run({
|
|
2931
|
+
...hookSession,
|
|
2932
|
+
hook_event_name: 'TaskCreated',
|
|
2933
|
+
task_id: task.id,
|
|
2934
|
+
task_subject: task.subject,
|
|
2935
|
+
task_description: task.description,
|
|
2936
|
+
}, undefined, taskSignal);
|
|
2937
|
+
if (!outcome)
|
|
2938
|
+
return;
|
|
2939
|
+
await recordHookOutcome(outcome);
|
|
2940
|
+
if (outcome.blockedReason) {
|
|
2941
|
+
throw new Error(`TaskCreated hook error: ${outcome.blockedReason}`);
|
|
2942
|
+
}
|
|
2943
|
+
},
|
|
2944
|
+
completed: async (task, taskSignal) => {
|
|
2945
|
+
const outcome = await this.options.hooks?.run({
|
|
2946
|
+
...hookSession,
|
|
2947
|
+
hook_event_name: 'TaskCompleted',
|
|
2948
|
+
task_id: task.id,
|
|
2949
|
+
task_subject: task.subject,
|
|
2950
|
+
task_description: task.description,
|
|
2951
|
+
}, undefined, taskSignal);
|
|
2952
|
+
if (!outcome)
|
|
2953
|
+
return;
|
|
2954
|
+
await recordHookOutcome(outcome);
|
|
2955
|
+
if (outcome.blockedReason) {
|
|
2956
|
+
throw new Error(`TaskCompleted hook error: ${outcome.blockedReason}`);
|
|
2957
|
+
}
|
|
2958
|
+
},
|
|
2959
|
+
},
|
|
2960
|
+
}
|
|
2961
|
+
: {}),
|
|
2962
|
+
})
|
|
2963
|
+
: null;
|
|
2964
|
+
const scheduledTools = this.scheduledPrompts &&
|
|
2965
|
+
this.options.tools &&
|
|
2966
|
+
scheduledToolNames.length > 0
|
|
2967
|
+
? new ClaudeScheduledToolRegistry({
|
|
2968
|
+
base: taskTools ?? this.options.tools,
|
|
2969
|
+
manager: this.scheduledPrompts,
|
|
2970
|
+
sessionId,
|
|
2971
|
+
enabledTools: scheduledToolNames,
|
|
2972
|
+
dataPlane: this.options.dataPlane ?? 'claude',
|
|
2973
|
+
})
|
|
2974
|
+
: null;
|
|
2975
|
+
const baseTools = scheduledTools ?? taskTools ?? this.options.tools;
|
|
2976
|
+
const turnPermissions = this.options.interactiveTools?.permissions(sessionId) ??
|
|
2977
|
+
this.options.permissions;
|
|
2978
|
+
const subagentExecutor = (this.options.enableSubagents || this.options.enableWorkflows) &&
|
|
2979
|
+
baseTools &&
|
|
2980
|
+
turnPermissions
|
|
2981
|
+
? new ClaudeSubagentExecutor({
|
|
2982
|
+
configRoot: this.options.configRoot,
|
|
2983
|
+
dataPlane: this.options.dataPlane ?? 'claude',
|
|
2984
|
+
cwd: this.activeCwd(),
|
|
2985
|
+
cwdProvider: () => this.activeCwd(),
|
|
2986
|
+
claudeVersion: this.options.claudeVersion,
|
|
2987
|
+
provider,
|
|
2988
|
+
persistence: this.options.sessionPersistence === false ? 'memory' : 'disk',
|
|
2989
|
+
...(this.options.providerForModel
|
|
2990
|
+
? { providerForModel: this.options.providerForModel }
|
|
2991
|
+
: {}),
|
|
2992
|
+
baseTools,
|
|
2993
|
+
permissions: turnPermissions,
|
|
2994
|
+
...(this.options.permissionResolverForMode
|
|
2995
|
+
? {
|
|
2996
|
+
permissionResolverForMode: this.options.permissionResolverForMode,
|
|
2997
|
+
}
|
|
2998
|
+
: {}),
|
|
2999
|
+
parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
|
|
3000
|
+
this.options.permissionMode),
|
|
3001
|
+
...(this.options.subagentToolNames
|
|
3002
|
+
? {
|
|
3003
|
+
toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
|
|
3004
|
+
}
|
|
3005
|
+
: {}),
|
|
3006
|
+
...(this.options.extensions
|
|
3007
|
+
? { extensions: this.options.extensions }
|
|
3008
|
+
: {}),
|
|
3009
|
+
...(this.options.mcp ? { mcp: this.options.mcp } : {}),
|
|
3010
|
+
...(this.options.hooks ? { hooks: this.options.hooks } : {}),
|
|
3011
|
+
...(this.options.contextAssembler
|
|
3012
|
+
? { contextAssembler: this.options.contextAssembler }
|
|
3013
|
+
: {}),
|
|
3014
|
+
...((this.options.contextBudget?.reserveTokens ??
|
|
3015
|
+
this.options.contextReserveTokens) === undefined
|
|
3016
|
+
? {}
|
|
3017
|
+
: {
|
|
3018
|
+
contextReserveTokens: this.options.contextBudget?.reserveTokens ??
|
|
3019
|
+
this.options.contextReserveTokens,
|
|
3020
|
+
}),
|
|
3021
|
+
...(this.options.approveTool
|
|
3022
|
+
? { approveTool: this.options.approveTool }
|
|
3023
|
+
: {}),
|
|
3024
|
+
permissionUpdates: () => this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
3025
|
+
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
3026
|
+
onLineChanges: (changes) => {
|
|
3027
|
+
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
3028
|
+
if (!tracker) {
|
|
3029
|
+
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
3030
|
+
}
|
|
3031
|
+
tracker.recordLineChanges(changes);
|
|
3032
|
+
},
|
|
3033
|
+
...(this.options.eventSink
|
|
3034
|
+
? { eventSink: this.options.eventSink }
|
|
3035
|
+
: {}),
|
|
3036
|
+
...(taskTools
|
|
3037
|
+
? {
|
|
3038
|
+
backgroundTaskNotifications: (waitForRunning) => taskTools.notifications(waitForRunning),
|
|
3039
|
+
}
|
|
3040
|
+
: {}),
|
|
3041
|
+
notificationDelivered: (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification),
|
|
3042
|
+
stopOwnedBackgroundAgent: (ownerSessionId, agentId) => this.backgroundTasks.stopAgent(ownerSessionId, agentId),
|
|
3043
|
+
outputOwnedBackgroundAgent: (ownerSessionId, agentId, options) => this.backgroundTasks.outputAgent(ownerSessionId, agentId, options),
|
|
3044
|
+
sendOwnedBackgroundAgent: (ownerSessionId, agentId, message, summary, toolUseId) => this.sendOwnedBackgroundAgent(ownerSessionId, agentId, message, summary, toolUseId),
|
|
3045
|
+
})
|
|
3046
|
+
: null;
|
|
3047
|
+
if (subagentExecutor) {
|
|
3048
|
+
this.subagentExecutors.add(subagentExecutor);
|
|
3049
|
+
this.subagentExecutorSessions.set(subagentExecutor, sessionId);
|
|
3050
|
+
this.backgroundTasks.registerAgents(sessionId, subagentExecutor);
|
|
2646
3051
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
3052
|
+
const agentTools = subagentExecutor
|
|
3053
|
+
? subagentExecutor.registry(sessionId, 0, (callId) => currentPromptId ??
|
|
3054
|
+
this.promptIdForToolCall(snapshot.entries, callId))
|
|
3055
|
+
: baseTools;
|
|
3056
|
+
const turnTools = this.workflowManager && subagentExecutor && agentTools
|
|
3057
|
+
? new ClaudeWorkflowToolRegistry({
|
|
3058
|
+
base: agentTools,
|
|
3059
|
+
manager: this.workflowManager,
|
|
3060
|
+
executor: subagentExecutor,
|
|
3061
|
+
cwd: this.activeCwd(),
|
|
3062
|
+
cwdProvider: () => this.activeCwd(),
|
|
3063
|
+
configRoot: this.options.configRoot,
|
|
3064
|
+
sessionId,
|
|
3065
|
+
promptIdForCall: (callId) => currentPromptId ??
|
|
3066
|
+
this.promptIdForToolCall(snapshot.entries, callId),
|
|
3067
|
+
defaultModel: provider.model ?? 'praxis/provider',
|
|
3068
|
+
tokenBudget: workflowTokenTarget(effectivePrompt),
|
|
3069
|
+
enabled: capabilities.has('Workflow'),
|
|
3070
|
+
dataPlane: this.options.dataPlane ?? 'claude',
|
|
3071
|
+
})
|
|
3072
|
+
: agentTools;
|
|
3073
|
+
const workspaceTools = this.worktreeManager &&
|
|
3074
|
+
turnTools &&
|
|
3075
|
+
this.options.workspace &&
|
|
3076
|
+
(this.options.worktreeToolNames?.length ?? 0) > 0
|
|
3077
|
+
? new ClaudeWorktreeToolRegistry({
|
|
3078
|
+
base: turnTools,
|
|
3079
|
+
manager: this.worktreeManager,
|
|
3080
|
+
workspace: this.options.workspace,
|
|
3081
|
+
enabledTools: this.options.worktreeToolNames ?? [],
|
|
3082
|
+
dataPlane: this.options.dataPlane ?? 'claude',
|
|
3083
|
+
})
|
|
3084
|
+
: turnTools;
|
|
3085
|
+
const messageTools = this.options.brief && workspaceTools
|
|
3086
|
+
? new ClaudeUserMessageToolRegistry(workspaceTools, (message) => this.options.eventSink?.({
|
|
3087
|
+
type: 'user-message',
|
|
3088
|
+
message: message.message,
|
|
3089
|
+
status: message.status,
|
|
3090
|
+
...(message.attachments.length
|
|
3091
|
+
? { attachments: message.attachments }
|
|
3092
|
+
: {}),
|
|
3093
|
+
}))
|
|
3094
|
+
: workspaceTools;
|
|
3095
|
+
const interactiveMessageTools = this.options.interactiveTools && messageTools
|
|
3096
|
+
? this.options.interactiveTools.registry(messageTools, sessionId)
|
|
3097
|
+
: messageTools;
|
|
3098
|
+
const fileHistoryTools = fileHistory && interactiveMessageTools
|
|
3099
|
+
? {
|
|
3100
|
+
definitions: () => interactiveMessageTools.definitions(),
|
|
3101
|
+
schedulingPolicy: (call) => ({
|
|
3102
|
+
...resolveToolSchedulingPolicy(interactiveMessageTools, call),
|
|
3103
|
+
startAfterAssistant: true,
|
|
3104
|
+
}),
|
|
3105
|
+
prepare: (call, context) => interactiveMessageTools.prepare(call, context),
|
|
3106
|
+
execute: async (call, context) => {
|
|
3107
|
+
const path = call.name === 'Write' || call.name === 'Edit'
|
|
3108
|
+
? call.input.file_path
|
|
3109
|
+
: call.name === 'NotebookEdit'
|
|
3110
|
+
? call.input.notebook_path
|
|
3111
|
+
: undefined;
|
|
3112
|
+
if (typeof path !== 'string') {
|
|
3113
|
+
return interactiveMessageTools.execute(call, context);
|
|
3114
|
+
}
|
|
3115
|
+
if ((call.name === 'Write' || call.name === 'Edit') &&
|
|
3116
|
+
(await this.options.interactiveTools?.isPlanFile(sessionId, path))) {
|
|
3117
|
+
return interactiveMessageTools.execute(call, context);
|
|
3118
|
+
}
|
|
3119
|
+
const snapshotMessageId = currentPromptId ??
|
|
3120
|
+
this.promptIdForToolCall(snapshot.entries, call.id);
|
|
3121
|
+
const assistantMessageId = lastAssistantUuid ??
|
|
3122
|
+
this.assistantIdForToolCall(snapshot.entries, call.id);
|
|
3123
|
+
if (!snapshotMessageId || !assistantMessageId) {
|
|
3124
|
+
throw new Error('Claude file history could not link tool call');
|
|
3125
|
+
}
|
|
3126
|
+
const prepared = await fileHistory.prepareMutation(snapshot.entries, snapshotMessageId, path);
|
|
3127
|
+
let result;
|
|
3128
|
+
try {
|
|
3129
|
+
result = await interactiveMessageTools.execute(call, context);
|
|
3130
|
+
}
|
|
3131
|
+
catch (error) {
|
|
3132
|
+
await prepared.rollback();
|
|
3133
|
+
throw error;
|
|
3134
|
+
}
|
|
3135
|
+
if (result.isError) {
|
|
3136
|
+
await prepared.rollback();
|
|
3137
|
+
return result;
|
|
3138
|
+
}
|
|
3139
|
+
const entry = prepared.commit(assistantMessageId);
|
|
3140
|
+
if (entry) {
|
|
3141
|
+
const tail = await this.append(lease, snapshot.tail, entry);
|
|
3142
|
+
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
3143
|
+
}
|
|
3144
|
+
return result;
|
|
3145
|
+
},
|
|
3146
|
+
}
|
|
3147
|
+
: interactiveMessageTools;
|
|
3148
|
+
const capabilityTools = fileHistoryTools
|
|
3149
|
+
? new ClaudeCapabilityToolRegistry(fileHistoryTools, capabilities)
|
|
3150
|
+
: undefined;
|
|
3151
|
+
const structuredCapture = this.options.structuredOutputSchema
|
|
3152
|
+
? { calls: 0, value: undefined }
|
|
3153
|
+
: undefined;
|
|
3154
|
+
const agentScopedTools = agent && capabilityTools
|
|
3155
|
+
? new FilteredToolRegistry(capabilityTools, {
|
|
3156
|
+
tools: mainAgentToolNames(capabilityTools, agent),
|
|
3157
|
+
})
|
|
3158
|
+
: capabilityTools;
|
|
3159
|
+
const structuredTools = this.options.structuredOutputSchema && structuredCapture
|
|
3160
|
+
? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
|
|
3161
|
+
: agentScopedTools;
|
|
3162
|
+
const hookTools = this.options.hooks && structuredTools && turnPermissions
|
|
3163
|
+
? new ClaudeHookToolCoordinator({
|
|
3164
|
+
tools: structuredTools,
|
|
3165
|
+
permissions: turnPermissions,
|
|
3166
|
+
hooks: this.options.hooks,
|
|
3167
|
+
session: hookSession,
|
|
3168
|
+
recordOutcome: recordHookOutcome,
|
|
3169
|
+
...(this.options.eventSink
|
|
3170
|
+
? {
|
|
3171
|
+
warn: (message) => this.options.eventSink?.({
|
|
3172
|
+
type: 'warning',
|
|
3173
|
+
message,
|
|
3174
|
+
}),
|
|
3175
|
+
}
|
|
3176
|
+
: {}),
|
|
3177
|
+
deferPreToolUseOutcome: (call) => pendingRecoveryToolCallIds.has(call.id),
|
|
3178
|
+
})
|
|
3179
|
+
: null;
|
|
3180
|
+
const runtime = new AgentRuntime(provider, controller.emit, {
|
|
3181
|
+
emitInitialContextState: false,
|
|
3182
|
+
...(this.options.emitToolUseSummaries
|
|
3183
|
+
? {
|
|
3184
|
+
generateToolUseSummary: ({ tools, lastAssistantText, signal: summarySignal, }) => generateToolUseSummary(provider, tools, summarySignal, lastAssistantText, (metrics) => this.recordAuxiliaryMetrics(sessionId, metrics)),
|
|
3185
|
+
}
|
|
2663
3186
|
: {}),
|
|
2664
|
-
|
|
2665
|
-
...(this.options.hooks
|
|
3187
|
+
...(this.options.pricing
|
|
2666
3188
|
? {
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
...hookSession,
|
|
2671
|
-
hook_event_name: 'TaskCreated',
|
|
2672
|
-
task_id: task.id,
|
|
2673
|
-
task_subject: task.subject,
|
|
2674
|
-
task_description: task.description,
|
|
2675
|
-
}, undefined, taskSignal);
|
|
2676
|
-
if (!outcome)
|
|
2677
|
-
return;
|
|
2678
|
-
await recordHookOutcome(outcome);
|
|
2679
|
-
if (outcome.blockedReason) {
|
|
2680
|
-
throw new Error(`TaskCreated hook error: ${outcome.blockedReason}`);
|
|
2681
|
-
}
|
|
2682
|
-
},
|
|
2683
|
-
completed: async (task, taskSignal) => {
|
|
2684
|
-
const outcome = await this.options.hooks?.run({
|
|
2685
|
-
...hookSession,
|
|
2686
|
-
hook_event_name: 'TaskCompleted',
|
|
2687
|
-
task_id: task.id,
|
|
2688
|
-
task_subject: task.subject,
|
|
2689
|
-
task_description: task.description,
|
|
2690
|
-
}, undefined, taskSignal);
|
|
2691
|
-
if (!outcome)
|
|
2692
|
-
return;
|
|
2693
|
-
await recordHookOutcome(outcome);
|
|
2694
|
-
if (outcome.blockedReason) {
|
|
2695
|
-
throw new Error(`TaskCompleted hook error: ${outcome.blockedReason}`);
|
|
2696
|
-
}
|
|
2697
|
-
},
|
|
3189
|
+
costUsd: (usage) => {
|
|
3190
|
+
const pricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
|
|
3191
|
+
return pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
2698
3192
|
},
|
|
2699
3193
|
}
|
|
2700
3194
|
: {}),
|
|
2701
|
-
|
|
2702
|
-
: null;
|
|
2703
|
-
const scheduledTools = this.scheduledPrompts &&
|
|
2704
|
-
this.options.tools &&
|
|
2705
|
-
scheduledToolNames.length > 0
|
|
2706
|
-
? new ClaudeScheduledToolRegistry({
|
|
2707
|
-
base: taskTools ?? this.options.tools,
|
|
2708
|
-
manager: this.scheduledPrompts,
|
|
2709
|
-
sessionId,
|
|
2710
|
-
enabledTools: scheduledToolNames,
|
|
2711
|
-
dataPlane: this.options.dataPlane ?? 'claude',
|
|
2712
|
-
})
|
|
2713
|
-
: null;
|
|
2714
|
-
const baseTools = scheduledTools ?? taskTools ?? this.options.tools;
|
|
2715
|
-
const turnPermissions = this.options.interactiveTools?.permissions(sessionId) ??
|
|
2716
|
-
this.options.permissions;
|
|
2717
|
-
const subagentExecutor = (this.options.enableSubagents || this.options.enableWorkflows) &&
|
|
2718
|
-
baseTools &&
|
|
2719
|
-
turnPermissions
|
|
2720
|
-
? new ClaudeSubagentExecutor({
|
|
2721
|
-
configRoot: this.options.configRoot,
|
|
2722
|
-
dataPlane: this.options.dataPlane ?? 'claude',
|
|
2723
|
-
cwd: this.activeCwd(),
|
|
2724
|
-
cwdProvider: () => this.activeCwd(),
|
|
2725
|
-
claudeVersion: this.options.claudeVersion,
|
|
2726
|
-
provider,
|
|
2727
|
-
persistence: this.options.sessionPersistence === false ? 'memory' : 'disk',
|
|
2728
|
-
...(this.options.providerForModel
|
|
2729
|
-
? { providerForModel: this.options.providerForModel }
|
|
2730
|
-
: {}),
|
|
2731
|
-
baseTools,
|
|
2732
|
-
permissions: turnPermissions,
|
|
2733
|
-
...(this.options.permissionResolverForMode
|
|
2734
|
-
? {
|
|
2735
|
-
permissionResolverForMode: this.options.permissionResolverForMode,
|
|
2736
|
-
}
|
|
2737
|
-
: {}),
|
|
2738
|
-
parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
|
|
2739
|
-
this.options.permissionMode),
|
|
2740
|
-
...(this.options.subagentToolNames
|
|
2741
|
-
? {
|
|
2742
|
-
toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
|
|
2743
|
-
}
|
|
2744
|
-
: {}),
|
|
2745
|
-
...(this.options.extensions
|
|
2746
|
-
? { extensions: this.options.extensions }
|
|
2747
|
-
: {}),
|
|
2748
|
-
...(this.options.mcp ? { mcp: this.options.mcp } : {}),
|
|
2749
|
-
...(this.options.hooks ? { hooks: this.options.hooks } : {}),
|
|
2750
|
-
...(this.options.contextAssembler
|
|
2751
|
-
? { contextAssembler: this.options.contextAssembler }
|
|
2752
|
-
: {}),
|
|
2753
|
-
...((this.options.contextBudget?.reserveTokens ??
|
|
2754
|
-
this.options.contextReserveTokens) === undefined
|
|
3195
|
+
...(this.options.maxBudgetUsd === undefined
|
|
2755
3196
|
? {}
|
|
3197
|
+
: { maxBudgetUsd: this.options.maxBudgetUsd }),
|
|
3198
|
+
...(hookTools
|
|
3199
|
+
? { tools: hookTools, permissions: hookTools }
|
|
2756
3200
|
: {
|
|
2757
|
-
|
|
2758
|
-
|
|
3201
|
+
...(structuredTools ? { tools: structuredTools } : {}),
|
|
3202
|
+
...(turnPermissions ? { permissions: turnPermissions } : {}),
|
|
2759
3203
|
}),
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
3204
|
+
});
|
|
3205
|
+
let currentTurnUserMessages = null;
|
|
3206
|
+
let currentTurnToolCalls = 0;
|
|
3207
|
+
let projectMemoryMaintained = false;
|
|
3208
|
+
let projectMemoryRecallMessages = [];
|
|
3209
|
+
const observer = {
|
|
3210
|
+
assistantCompleted: async (message) => {
|
|
3211
|
+
const [entry] = translateProviderEvents([
|
|
3212
|
+
{
|
|
3213
|
+
type: 'assistant-message',
|
|
3214
|
+
text: message.content,
|
|
3215
|
+
...(message.thinkingBlocks
|
|
3216
|
+
? { thinkingBlocks: message.thinkingBlocks }
|
|
3217
|
+
: {}),
|
|
3218
|
+
toolCalls: message.toolCalls ?? [],
|
|
3219
|
+
providerMessageId: `msg_${randomUUID().replaceAll('-', '')}`,
|
|
3220
|
+
model: provider.model ?? 'praxis/provider',
|
|
3221
|
+
},
|
|
3222
|
+
], this.translationContext(sessionId, snapshot));
|
|
3223
|
+
if (!entry)
|
|
3224
|
+
throw new Error('Could not translate assistant response');
|
|
3225
|
+
const tail = await this.append(lease, snapshot.tail, entry);
|
|
3226
|
+
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
3227
|
+
lastAssistantUuid =
|
|
3228
|
+
typeof entry.uuid === 'string' ? entry.uuid : lastAssistantUuid;
|
|
2771
3229
|
},
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
executor: subagentExecutor,
|
|
2791
|
-
cwd: this.activeCwd(),
|
|
2792
|
-
cwdProvider: () => this.activeCwd(),
|
|
2793
|
-
configRoot: this.options.configRoot,
|
|
2794
|
-
sessionId,
|
|
2795
|
-
promptIdForCall: (callId) => currentPromptId ??
|
|
2796
|
-
this.promptIdForToolCall(snapshot.entries, callId),
|
|
2797
|
-
defaultModel: provider.model ?? 'praxis/provider',
|
|
2798
|
-
tokenBudget: workflowTokenTarget(effectivePrompt),
|
|
2799
|
-
enabled: capabilities.has('Workflow'),
|
|
2800
|
-
dataPlane: this.options.dataPlane ?? 'claude',
|
|
2801
|
-
})
|
|
2802
|
-
: agentTools;
|
|
2803
|
-
const workspaceTools = this.worktreeManager &&
|
|
2804
|
-
turnTools &&
|
|
2805
|
-
this.options.workspace &&
|
|
2806
|
-
(this.options.worktreeToolNames?.length ?? 0) > 0
|
|
2807
|
-
? new ClaudeWorktreeToolRegistry({
|
|
2808
|
-
base: turnTools,
|
|
2809
|
-
manager: this.worktreeManager,
|
|
2810
|
-
workspace: this.options.workspace,
|
|
2811
|
-
enabledTools: this.options.worktreeToolNames ?? [],
|
|
2812
|
-
dataPlane: this.options.dataPlane ?? 'claude',
|
|
2813
|
-
})
|
|
2814
|
-
: turnTools;
|
|
2815
|
-
const messageTools = this.options.brief && workspaceTools
|
|
2816
|
-
? new ClaudeUserMessageToolRegistry(workspaceTools, (message) => this.options.eventSink?.({
|
|
2817
|
-
type: 'user-message',
|
|
2818
|
-
message: message.message,
|
|
2819
|
-
status: message.status,
|
|
2820
|
-
...(message.attachments.length
|
|
2821
|
-
? { attachments: message.attachments }
|
|
2822
|
-
: {}),
|
|
2823
|
-
}))
|
|
2824
|
-
: workspaceTools;
|
|
2825
|
-
const interactiveMessageTools = this.options.interactiveTools && messageTools
|
|
2826
|
-
? this.options.interactiveTools.registry(messageTools, sessionId)
|
|
2827
|
-
: messageTools;
|
|
2828
|
-
const fileHistoryTools = fileHistory && interactiveMessageTools
|
|
2829
|
-
? {
|
|
2830
|
-
definitions: () => interactiveMessageTools.definitions(),
|
|
2831
|
-
schedulingPolicy: (call) => ({
|
|
2832
|
-
...resolveToolSchedulingPolicy(interactiveMessageTools, call),
|
|
2833
|
-
startAfterAssistant: true,
|
|
2834
|
-
}),
|
|
2835
|
-
prepare: (call, context) => interactiveMessageTools.prepare(call, context),
|
|
2836
|
-
execute: async (call, context) => {
|
|
2837
|
-
const path = call.name === 'Write' || call.name === 'Edit'
|
|
2838
|
-
? call.input.file_path
|
|
2839
|
-
: call.name === 'NotebookEdit'
|
|
2840
|
-
? call.input.notebook_path
|
|
2841
|
-
: undefined;
|
|
2842
|
-
if (typeof path !== 'string') {
|
|
2843
|
-
return interactiveMessageTools.execute(call, context);
|
|
2844
|
-
}
|
|
2845
|
-
if ((call.name === 'Write' || call.name === 'Edit') &&
|
|
2846
|
-
(await this.options.interactiveTools?.isPlanFile(sessionId, path))) {
|
|
2847
|
-
return interactiveMessageTools.execute(call, context);
|
|
2848
|
-
}
|
|
2849
|
-
const snapshotMessageId = currentPromptId ??
|
|
2850
|
-
this.promptIdForToolCall(snapshot.entries, call.id);
|
|
2851
|
-
const assistantMessageId = lastAssistantUuid ??
|
|
2852
|
-
this.assistantIdForToolCall(snapshot.entries, call.id);
|
|
2853
|
-
if (!snapshotMessageId || !assistantMessageId) {
|
|
2854
|
-
throw new Error('Claude file history could not link tool call');
|
|
2855
|
-
}
|
|
2856
|
-
const prepared = await fileHistory.prepareMutation(snapshot.entries, snapshotMessageId, path);
|
|
2857
|
-
let result;
|
|
2858
|
-
try {
|
|
2859
|
-
result = await interactiveMessageTools.execute(call, context);
|
|
2860
|
-
}
|
|
2861
|
-
catch (error) {
|
|
2862
|
-
await prepared.rollback();
|
|
2863
|
-
throw error;
|
|
2864
|
-
}
|
|
2865
|
-
if (result.isError) {
|
|
2866
|
-
await prepared.rollback();
|
|
2867
|
-
return result;
|
|
2868
|
-
}
|
|
2869
|
-
const entry = prepared.commit(assistantMessageId);
|
|
2870
|
-
if (entry) {
|
|
2871
|
-
const tail = await this.append(lease, snapshot.tail, entry);
|
|
2872
|
-
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
3230
|
+
toolCompleted: async (call, toolResult) => {
|
|
3231
|
+
currentTurnToolCalls += 1;
|
|
3232
|
+
const transition = this.worktreeManager?.consumeTransition(call.id);
|
|
3233
|
+
if (transition) {
|
|
3234
|
+
this.options.contextAssembler?.invalidate?.({
|
|
3235
|
+
lifecycleId: sessionId,
|
|
3236
|
+
reason: 'worktree',
|
|
3237
|
+
});
|
|
3238
|
+
const stateEntry = {
|
|
3239
|
+
type: 'worktree-state',
|
|
3240
|
+
worktreeSession: transition.state,
|
|
3241
|
+
sessionId,
|
|
3242
|
+
};
|
|
3243
|
+
const stateTail = await this.append(lease, snapshot.tail, stateEntry);
|
|
3244
|
+
snapshot = {
|
|
3245
|
+
entries: [...snapshot.entries, stateEntry],
|
|
3246
|
+
tail: stateTail,
|
|
3247
|
+
};
|
|
2873
3248
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
tools: mainAgentToolNames(capabilityTools, agent),
|
|
2887
|
-
})
|
|
2888
|
-
: capabilityTools;
|
|
2889
|
-
const structuredTools = this.options.structuredOutputSchema && structuredCapture
|
|
2890
|
-
? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
|
|
2891
|
-
: agentScopedTools;
|
|
2892
|
-
const hookTools = this.options.hooks && structuredTools && turnPermissions
|
|
2893
|
-
? new ClaudeHookToolCoordinator({
|
|
2894
|
-
tools: structuredTools,
|
|
2895
|
-
permissions: turnPermissions,
|
|
2896
|
-
hooks: this.options.hooks,
|
|
2897
|
-
session: hookSession,
|
|
2898
|
-
recordOutcome: recordHookOutcome,
|
|
2899
|
-
...(this.options.eventSink
|
|
2900
|
-
? {
|
|
2901
|
-
warn: (message) => this.options.eventSink?.({ type: 'warning', message }),
|
|
3249
|
+
const permissionMode = this.options.interactiveTools?.consumeTransition(call.id);
|
|
3250
|
+
if (permissionMode) {
|
|
3251
|
+
const modeEntry = {
|
|
3252
|
+
type: 'permission-mode',
|
|
3253
|
+
permissionMode,
|
|
3254
|
+
sessionId,
|
|
3255
|
+
};
|
|
3256
|
+
const modeTail = await this.append(lease, snapshot.tail, modeEntry);
|
|
3257
|
+
snapshot = {
|
|
3258
|
+
entries: [...snapshot.entries, modeEntry],
|
|
3259
|
+
tail: modeTail,
|
|
3260
|
+
};
|
|
2902
3261
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
text: message.content,
|
|
2942
|
-
...(message.thinkingBlocks
|
|
2943
|
-
? { thinkingBlocks: message.thinkingBlocks }
|
|
2944
|
-
: {}),
|
|
2945
|
-
toolCalls: message.toolCalls ?? [],
|
|
2946
|
-
providerMessageId: `msg_${randomUUID().replaceAll('-', '')}`,
|
|
2947
|
-
model: provider.model ?? 'praxis/provider',
|
|
2948
|
-
},
|
|
2949
|
-
], this.translationContext(sessionId, snapshot));
|
|
2950
|
-
if (!entry)
|
|
2951
|
-
throw new Error('Could not translate assistant response');
|
|
2952
|
-
const tail = await this.append(lease, snapshot.tail, entry);
|
|
2953
|
-
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
2954
|
-
lastAssistantUuid =
|
|
2955
|
-
typeof entry.uuid === 'string' ? entry.uuid : lastAssistantUuid;
|
|
2956
|
-
},
|
|
2957
|
-
toolCompleted: async (call, toolResult) => {
|
|
2958
|
-
currentTurnToolCalls += 1;
|
|
2959
|
-
const transition = this.worktreeManager?.consumeTransition(call.id);
|
|
2960
|
-
if (transition) {
|
|
2961
|
-
this.options.contextAssembler?.invalidate?.({
|
|
2962
|
-
lifecycleId: sessionId,
|
|
2963
|
-
reason: 'worktree',
|
|
2964
|
-
});
|
|
2965
|
-
const stateEntry = {
|
|
2966
|
-
type: 'worktree-state',
|
|
2967
|
-
worktreeSession: transition.state,
|
|
2968
|
-
sessionId,
|
|
2969
|
-
};
|
|
2970
|
-
const stateTail = await this.append(lease, snapshot.tail, stateEntry);
|
|
2971
|
-
snapshot = {
|
|
2972
|
-
entries: [...snapshot.entries, stateEntry],
|
|
2973
|
-
tail: stateTail,
|
|
2974
|
-
};
|
|
2975
|
-
}
|
|
2976
|
-
const permissionMode = this.options.interactiveTools?.consumeTransition(call.id);
|
|
2977
|
-
if (permissionMode) {
|
|
2978
|
-
const modeEntry = {
|
|
2979
|
-
type: 'permission-mode',
|
|
2980
|
-
permissionMode,
|
|
2981
|
-
sessionId,
|
|
2982
|
-
};
|
|
2983
|
-
const modeTail = await this.append(lease, snapshot.tail, modeEntry);
|
|
2984
|
-
snapshot = {
|
|
2985
|
-
entries: [...snapshot.entries, modeEntry],
|
|
2986
|
-
tail: modeTail,
|
|
2987
|
-
};
|
|
2988
|
-
}
|
|
2989
|
-
const [entry] = translateProviderEvents([
|
|
2990
|
-
{
|
|
2991
|
-
type: 'tool-result',
|
|
2992
|
-
toolCallId: call.id,
|
|
2993
|
-
content: toolResult.content,
|
|
2994
|
-
...(toolResult.contentBlocks
|
|
2995
|
-
? { contentBlocks: toolResult.contentBlocks }
|
|
2996
|
-
: {}),
|
|
2997
|
-
...(toolResult.images ? { images: toolResult.images } : {}),
|
|
2998
|
-
...(toolResult.documents
|
|
2999
|
-
? { documents: toolResult.documents }
|
|
3000
|
-
: {}),
|
|
3001
|
-
isError: toolResult.isError,
|
|
3002
|
-
...(toolResult.nativeToolUseResult
|
|
3003
|
-
? { nativeToolUseResult: toolResult.nativeToolUseResult }
|
|
3004
|
-
: {}),
|
|
3005
|
-
...(toolResult.nativeMcpMeta
|
|
3006
|
-
? { nativeMcpMeta: toolResult.nativeMcpMeta }
|
|
3007
|
-
: {}),
|
|
3008
|
-
},
|
|
3009
|
-
], this.translationContext(sessionId, snapshot));
|
|
3010
|
-
if (!entry)
|
|
3011
|
-
throw new Error('Could not translate tool result');
|
|
3012
|
-
const tail = await this.append(lease, snapshot.tail, entry);
|
|
3013
|
-
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
3014
|
-
if (!toolResult.isError && this.options.projectMemoryDirectory) {
|
|
3015
|
-
const pathValue = call.input.file_path;
|
|
3016
|
-
const path = typeof pathValue === 'string'
|
|
3017
|
-
? resolve(this.activeCwd(), pathValue)
|
|
3018
|
-
: null;
|
|
3019
|
-
if (path &&
|
|
3020
|
-
isPathWithin(this.options.projectMemoryDirectory, path)) {
|
|
3021
|
-
if (call.name === 'Read') {
|
|
3022
|
-
this.options.projectMemoryRecall?.recordRead(sessionId, path);
|
|
3262
|
+
const [entry] = translateProviderEvents([
|
|
3263
|
+
{
|
|
3264
|
+
type: 'tool-result',
|
|
3265
|
+
toolCallId: call.id,
|
|
3266
|
+
content: toolResult.content,
|
|
3267
|
+
...(toolResult.contentBlocks
|
|
3268
|
+
? { contentBlocks: toolResult.contentBlocks }
|
|
3269
|
+
: {}),
|
|
3270
|
+
...(toolResult.images ? { images: toolResult.images } : {}),
|
|
3271
|
+
...(toolResult.documents
|
|
3272
|
+
? { documents: toolResult.documents }
|
|
3273
|
+
: {}),
|
|
3274
|
+
isError: toolResult.isError,
|
|
3275
|
+
...(toolResult.nativeToolUseResult
|
|
3276
|
+
? { nativeToolUseResult: toolResult.nativeToolUseResult }
|
|
3277
|
+
: {}),
|
|
3278
|
+
...(toolResult.nativeMcpMeta
|
|
3279
|
+
? { nativeMcpMeta: toolResult.nativeMcpMeta }
|
|
3280
|
+
: {}),
|
|
3281
|
+
},
|
|
3282
|
+
], this.translationContext(sessionId, snapshot));
|
|
3283
|
+
if (!entry)
|
|
3284
|
+
throw new Error('Could not translate tool result');
|
|
3285
|
+
const tail = await this.append(lease, snapshot.tail, entry);
|
|
3286
|
+
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
3287
|
+
if (!toolResult.isError && this.options.projectMemoryDirectory) {
|
|
3288
|
+
const pathValue = call.input.file_path;
|
|
3289
|
+
const path = typeof pathValue === 'string'
|
|
3290
|
+
? resolve(this.activeCwd(), pathValue)
|
|
3291
|
+
: null;
|
|
3292
|
+
if (path &&
|
|
3293
|
+
isPathWithin(this.options.projectMemoryDirectory, path)) {
|
|
3294
|
+
if (call.name === 'Read') {
|
|
3295
|
+
this.options.projectMemoryRecall?.recordRead(sessionId, path);
|
|
3296
|
+
}
|
|
3297
|
+
else if (call.name === 'Write' || call.name === 'Edit') {
|
|
3298
|
+
projectMemoryMaintained = true;
|
|
3299
|
+
}
|
|
3023
3300
|
}
|
|
3024
|
-
|
|
3025
|
-
|
|
3301
|
+
if (call.name === 'Read') {
|
|
3302
|
+
for (const accessedPath of toolResult.accessedPaths ?? []) {
|
|
3303
|
+
const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
|
|
3304
|
+
if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
|
|
3305
|
+
this.options.projectMemoryRecall?.recordRead(sessionId, resolvedAccessedPath);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3026
3308
|
}
|
|
3027
3309
|
}
|
|
3028
|
-
if (
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3310
|
+
if (toolResult.isError ||
|
|
3311
|
+
call.name !== 'Read' ||
|
|
3312
|
+
!this.options.conditionalRuleResolver ||
|
|
3313
|
+
!toolResult.accessedPaths) {
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
const attachedRulePaths = this.attachedRulePaths(snapshot.entries);
|
|
3317
|
+
for (const filePath of toolResult.accessedPaths) {
|
|
3318
|
+
const rules = await this.options.conditionalRuleResolver.resolve(filePath, [...attachedRulePaths]);
|
|
3319
|
+
for (const rule of rules) {
|
|
3320
|
+
const attachment = createClaudeRuleAttachmentEntry(rule, this.displayRulePath(rule.path), this.translationContext(sessionId, snapshot));
|
|
3321
|
+
const attachmentTail = await this.append(lease, snapshot.tail, attachment);
|
|
3322
|
+
snapshot = {
|
|
3323
|
+
entries: [...snapshot.entries, attachment],
|
|
3324
|
+
tail: attachmentTail,
|
|
3325
|
+
};
|
|
3326
|
+
attachedRulePaths.add(rule.path);
|
|
3327
|
+
await this.instructionLoaded(sessionId, {
|
|
3328
|
+
path: rule.path,
|
|
3329
|
+
memoryType: rule.scope === 'user'
|
|
3330
|
+
? 'User'
|
|
3331
|
+
: rule.scope === 'local'
|
|
3332
|
+
? 'Local'
|
|
3333
|
+
: 'Project',
|
|
3334
|
+
globs: rule.globs,
|
|
3335
|
+
triggerFilePath: filePath,
|
|
3336
|
+
...(rule.importedFrom === undefined
|
|
3337
|
+
? {}
|
|
3338
|
+
: { parentFilePath: rule.importedFrom }),
|
|
3339
|
+
}, 'path_glob_match');
|
|
3034
3340
|
}
|
|
3035
3341
|
}
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
for (const filePath of toolResult.accessedPaths) {
|
|
3045
|
-
const rules = await this.options.conditionalRuleResolver.resolve(filePath, [...attachedRulePaths]);
|
|
3046
|
-
for (const rule of rules) {
|
|
3047
|
-
const attachment = createClaudeRuleAttachmentEntry(rule, this.displayRulePath(rule.path), this.translationContext(sessionId, snapshot));
|
|
3048
|
-
const attachmentTail = await this.append(lease, snapshot.tail, attachment);
|
|
3342
|
+
},
|
|
3343
|
+
followUpUserMessagesCompleted: async (messages) => {
|
|
3344
|
+
for (const content of messages) {
|
|
3345
|
+
const [followUpEntry] = translateProviderEvents([{ type: 'user-text-block', text: content }], this.translationContext(sessionId, snapshot));
|
|
3346
|
+
if (!followUpEntry) {
|
|
3347
|
+
throw new Error('Could not translate tool follow-up message');
|
|
3348
|
+
}
|
|
3349
|
+
const followUpTail = await this.append(lease, snapshot.tail, followUpEntry);
|
|
3049
3350
|
snapshot = {
|
|
3050
|
-
entries: [...snapshot.entries,
|
|
3051
|
-
tail:
|
|
3351
|
+
entries: [...snapshot.entries, followUpEntry],
|
|
3352
|
+
tail: followUpTail,
|
|
3052
3353
|
};
|
|
3053
|
-
|
|
3054
|
-
await this.
|
|
3055
|
-
path: rule.path,
|
|
3056
|
-
memoryType: rule.scope === 'user'
|
|
3057
|
-
? 'User'
|
|
3058
|
-
: rule.scope === 'local'
|
|
3059
|
-
? 'Local'
|
|
3060
|
-
: 'Project',
|
|
3061
|
-
globs: rule.globs,
|
|
3062
|
-
triggerFilePath: filePath,
|
|
3063
|
-
...(rule.importedFrom === undefined
|
|
3064
|
-
? {}
|
|
3065
|
-
: { parentFilePath: rule.importedFrom }),
|
|
3066
|
-
}, 'path_glob_match');
|
|
3354
|
+
currentTurnUserMessages?.push(content);
|
|
3355
|
+
await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => executor.acknowledgeNotifications([content])));
|
|
3067
3356
|
}
|
|
3357
|
+
},
|
|
3358
|
+
};
|
|
3359
|
+
let turnCompleted = false;
|
|
3360
|
+
try {
|
|
3361
|
+
const outcome = await this.hookLifecycle.start(sessionId, hookSession, requireExisting ? 'resume' : 'startup', signal);
|
|
3362
|
+
if (outcome) {
|
|
3363
|
+
await recordHookOutcome(outcome, pendingRecoveryToolCallIds.size > 0);
|
|
3068
3364
|
}
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3365
|
+
const approveRecovery = this.options.approveRecovery;
|
|
3366
|
+
const recoveryRequest = {
|
|
3367
|
+
cwd: this.activeCwd(),
|
|
3368
|
+
toolResultDirectory,
|
|
3369
|
+
messages: projectClaudeModelMessages(snapshot.entries),
|
|
3370
|
+
observer,
|
|
3371
|
+
permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
3372
|
+
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
3373
|
+
...(signal ? { signal } : {}),
|
|
3374
|
+
...(approveRecovery
|
|
3375
|
+
? {
|
|
3376
|
+
approveRecovery: async (call) => {
|
|
3377
|
+
if (!(await approveRecovery(call))) {
|
|
3378
|
+
throw new Error(`Claude session tool call ${call.id} recovery was declined`);
|
|
3379
|
+
}
|
|
3380
|
+
if (signal?.aborted)
|
|
3381
|
+
throw new AgentRunCancelledError();
|
|
3382
|
+
await flushRecoveryHookOutcomes();
|
|
3383
|
+
pendingRecoveryToolCallIds.delete(call.id);
|
|
3384
|
+
return true;
|
|
3385
|
+
},
|
|
3386
|
+
approveTool: () => true,
|
|
3387
|
+
}
|
|
3388
|
+
: {}),
|
|
3389
|
+
};
|
|
3390
|
+
const unresolvedToolCall = unresolvedToolCalls[0];
|
|
3391
|
+
if (unresolvedToolCall && !approveRecovery) {
|
|
3392
|
+
throw new Error(`Claude session tool call ${unresolvedToolCall.id} requires explicit recovery approval`);
|
|
3393
|
+
}
|
|
3394
|
+
const recoveryResults = await runtime.recoverToolCalls(unresolvedToolCalls, recoveryRequest);
|
|
3395
|
+
const recoveryUsage = recoveryResults.reduce((usage, result) => result.usage ? mergeUsage(usage, result.usage) : usage, { inputTokens: 0, outputTokens: 0 });
|
|
3396
|
+
const recoveryModelUsage = mergeSessionRawModelUsage(...recoveryResults.map((result) => result.isError ? undefined : result.modelUsage));
|
|
3397
|
+
const foregroundLineChanges = createLineCountAccumulator();
|
|
3398
|
+
for (const result of recoveryResults) {
|
|
3399
|
+
foregroundLineChanges.add(result);
|
|
3400
|
+
}
|
|
3401
|
+
if (this.options.agent &&
|
|
3402
|
+
agent &&
|
|
3403
|
+
getClaudeAgentSetting(snapshot.entries) !== this.options.agent) {
|
|
3404
|
+
const agentSetting = createClaudeAgentSettingEntry(sessionId, this.options.agent);
|
|
3405
|
+
const settingTail = await this.append(lease, snapshot.tail, agentSetting);
|
|
3077
3406
|
snapshot = {
|
|
3078
|
-
entries: [...snapshot.entries,
|
|
3079
|
-
tail:
|
|
3407
|
+
entries: [...snapshot.entries, agentSetting],
|
|
3408
|
+
tail: settingTail,
|
|
3080
3409
|
};
|
|
3081
|
-
currentTurnUserMessages?.push(content);
|
|
3082
3410
|
}
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3411
|
+
const sessionMemory = this.sessionMemoryController(sessionId);
|
|
3412
|
+
let agentSystem = null;
|
|
3413
|
+
let planModeMessage;
|
|
3414
|
+
let sessionMemoryMessage = null;
|
|
3415
|
+
let contextMessages = [];
|
|
3416
|
+
let contextProjection = {
|
|
3417
|
+
systemMessages: [],
|
|
3418
|
+
stableSystemSectionCount: 0,
|
|
3419
|
+
};
|
|
3420
|
+
let stableSystemMessageCount = 0;
|
|
3421
|
+
const refreshRuntimeContext = async () => {
|
|
3422
|
+
agentSystem = await this.mainAgentSystemPrompt(agent);
|
|
3423
|
+
planModeMessage =
|
|
3424
|
+
this.options.interactiveTools?.contextMessage(sessionId);
|
|
3425
|
+
sessionMemoryMessage = sessionMemory
|
|
3426
|
+
? this.sessionMemoryMessage(await sessionMemory.summary())
|
|
3427
|
+
: null;
|
|
3428
|
+
const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
|
|
3429
|
+
cwd: this.activeCwd(),
|
|
3430
|
+
lifecycleId: sessionId,
|
|
3431
|
+
...(agentSystem
|
|
3432
|
+
? { mode: 'agent', baseSystemPrompt: agentSystem }
|
|
3433
|
+
: {}),
|
|
3434
|
+
turn: {
|
|
3435
|
+
...(planModeMessage ? { planMode: planModeMessage } : {}),
|
|
3436
|
+
...(sessionMemoryMessage
|
|
3437
|
+
? { sessionMemory: sessionMemoryMessage }
|
|
3438
|
+
: {}),
|
|
3439
|
+
...(this.options.brief ? { briefOutput: true } : {}),
|
|
3440
|
+
...(this.options.structuredOutputSchema
|
|
3441
|
+
? { structuredOutput: true }
|
|
3442
|
+
: {}),
|
|
3111
3443
|
},
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
if (unresolvedToolCall && !approveRecovery) {
|
|
3118
|
-
throw new Error(`Claude session tool call ${unresolvedToolCall.id} requires explicit recovery approval`);
|
|
3119
|
-
}
|
|
3120
|
-
const recoveryResults = await runtime.recoverToolCalls(unresolvedToolCalls, recoveryRequest);
|
|
3121
|
-
const recoveryUsage = recoveryResults.reduce((usage, result) => result.usage ? mergeUsage(usage, result.usage) : usage, { inputTokens: 0, outputTokens: 0 });
|
|
3122
|
-
const recoveryModelUsage = mergeSessionRawModelUsage(...recoveryResults.map((result) => result.isError ? undefined : result.modelUsage));
|
|
3123
|
-
const foregroundLineChanges = createLineCountAccumulator();
|
|
3124
|
-
for (const result of recoveryResults) {
|
|
3125
|
-
foregroundLineChanges.add(result);
|
|
3126
|
-
}
|
|
3127
|
-
if (this.options.agent &&
|
|
3128
|
-
agent &&
|
|
3129
|
-
getClaudeAgentSetting(snapshot.entries) !== this.options.agent) {
|
|
3130
|
-
const agentSetting = createClaudeAgentSettingEntry(sessionId, this.options.agent);
|
|
3131
|
-
const settingTail = await this.append(lease, snapshot.tail, agentSetting);
|
|
3132
|
-
snapshot = {
|
|
3133
|
-
entries: [...snapshot.entries, agentSetting],
|
|
3134
|
-
tail: settingTail,
|
|
3444
|
+
});
|
|
3445
|
+
contextProjection = projectContextSnapshot(assembledContext);
|
|
3446
|
+
stableSystemMessageCount =
|
|
3447
|
+
contextProjection.stableSystemSectionCount;
|
|
3448
|
+
contextMessages = [...contextProjection.systemMessages];
|
|
3135
3449
|
};
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
placement: 'system',
|
|
3157
|
-
stability: 'volatile',
|
|
3158
|
-
content: planModeMessage,
|
|
3159
|
-
},
|
|
3160
|
-
]
|
|
3161
|
-
: []),
|
|
3162
|
-
...(sessionMemoryMessage
|
|
3163
|
-
? [
|
|
3164
|
-
{
|
|
3165
|
-
id: 'session-memory',
|
|
3166
|
-
placement: 'system',
|
|
3167
|
-
stability: 'volatile',
|
|
3168
|
-
content: sessionMemoryMessage,
|
|
3169
|
-
},
|
|
3170
|
-
]
|
|
3171
|
-
: []),
|
|
3172
|
-
...(this.options.brief
|
|
3173
|
-
? [
|
|
3174
|
-
{
|
|
3175
|
-
id: 'brief-output',
|
|
3176
|
-
placement: 'system',
|
|
3177
|
-
stability: 'volatile',
|
|
3178
|
-
content: CLAUDE_USER_MESSAGE_PROMPT,
|
|
3179
|
-
},
|
|
3180
|
-
]
|
|
3181
|
-
: []),
|
|
3182
|
-
...(this.options.structuredOutputSchema
|
|
3183
|
-
? [
|
|
3184
|
-
{
|
|
3185
|
-
id: 'structured-output',
|
|
3186
|
-
placement: 'system',
|
|
3187
|
-
stability: 'volatile',
|
|
3188
|
-
content: 'You MUST call StructuredOutput exactly once at the end with a value matching the requested JSON Schema.',
|
|
3189
|
-
},
|
|
3190
|
-
]
|
|
3191
|
-
: []),
|
|
3192
|
-
];
|
|
3193
|
-
assembledContext = await this.options.contextAssembler?.assemble({
|
|
3194
|
-
cwd: this.activeCwd(),
|
|
3195
|
-
lifecycleId: sessionId,
|
|
3196
|
-
...(agentSystem
|
|
3197
|
-
? { mode: 'agent', baseSystemPrompt: agentSystem }
|
|
3198
|
-
: {}),
|
|
3199
|
-
additionalSections,
|
|
3450
|
+
await refreshRuntimeContext();
|
|
3451
|
+
const expansion = skipUserPrompt
|
|
3452
|
+
? { userMessages: [] }
|
|
3453
|
+
: shellCommand === undefined
|
|
3454
|
+
? this.options.extensions
|
|
3455
|
+
? await this.options.extensions.expandPromptAsync(effectivePrompt, signal, toolResultDirectory)
|
|
3456
|
+
: { userMessages: [effectivePrompt] }
|
|
3457
|
+
: {
|
|
3458
|
+
userMessages: [`<bash-input>${shellCommand}</bash-input>`],
|
|
3459
|
+
};
|
|
3460
|
+
const expansionMessages = 'messages' in expansion ? expansion.messages : undefined;
|
|
3461
|
+
const userMessages = 'userMessages' in expansion ? expansion.userMessages : [];
|
|
3462
|
+
const expandedMessages = expansionMessages ?? userMessages.map((text) => ({ text }));
|
|
3463
|
+
const attachmentIndex = expansionMessages
|
|
3464
|
+
? expandedMessages.length - 1
|
|
3465
|
+
: 0;
|
|
3466
|
+
currentTurnUserMessages = [...expansion.userMessages];
|
|
3467
|
+
this.options.eventSink?.({
|
|
3468
|
+
type: 'state',
|
|
3469
|
+
state: 'assembling-context',
|
|
3200
3470
|
});
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
...(
|
|
3217
|
-
?
|
|
3218
|
-
:
|
|
3219
|
-
...(
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
? expandedMessages.length - 1
|
|
3250
|
-
: 0;
|
|
3251
|
-
currentTurnUserMessages = [...expansion.userMessages];
|
|
3252
|
-
this.options.eventSink?.({
|
|
3253
|
-
type: 'state',
|
|
3254
|
-
state: 'assembling-context',
|
|
3255
|
-
});
|
|
3256
|
-
let compactionUsage = {
|
|
3257
|
-
inputTokens: 0,
|
|
3258
|
-
outputTokens: 0,
|
|
3259
|
-
};
|
|
3260
|
-
let compactionDurationMs;
|
|
3261
|
-
let compactionDurationWithoutRetriesMs;
|
|
3262
|
-
let compactionModelUsage;
|
|
3263
|
-
const definitions = provider.capabilities.tools
|
|
3264
|
-
? (structuredTools?.definitions() ?? [])
|
|
3265
|
-
: [];
|
|
3266
|
-
const budget = this.contextBudget(provider);
|
|
3267
|
-
const recoveryPlanner = new ContextRecoveryPlanner();
|
|
3268
|
-
const pendingUserMessages = expandedMessages.map((message, index) => ({
|
|
3269
|
-
role: 'user',
|
|
3270
|
-
content: message.text,
|
|
3271
|
-
...(message.contentBlocks?.length
|
|
3272
|
-
? { contentBlocks: message.contentBlocks }
|
|
3273
|
-
: {}),
|
|
3274
|
-
...((index === attachmentIndex && images.length > 0) ||
|
|
3275
|
-
message.images?.length
|
|
3276
|
-
? {
|
|
3277
|
-
images: [
|
|
3278
|
-
...(index === attachmentIndex ? images : []),
|
|
3279
|
-
...(message.images ?? []),
|
|
3280
|
-
],
|
|
3281
|
-
}
|
|
3282
|
-
: {}),
|
|
3283
|
-
...(index === attachmentIndex && documents.length > 0
|
|
3284
|
-
? { documents }
|
|
3285
|
-
: {}),
|
|
3286
|
-
}));
|
|
3287
|
-
const agentMentionMessages = shellCommand === undefined && !skipUserPrompt
|
|
3288
|
-
? (this.options.extensions?.agentMentionMessages(effectivePrompt) ??
|
|
3289
|
-
[])
|
|
3290
|
-
: [];
|
|
3291
|
-
const injectAgentMentionContext = (messages) => {
|
|
3292
|
-
if (agentMentionMessages.length === 0)
|
|
3293
|
-
return [...messages];
|
|
3294
|
-
let insertionIndex = messages.length;
|
|
3295
|
-
let foundPrompt = false;
|
|
3296
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
3297
|
-
const message = messages[index];
|
|
3298
|
-
if (message?.role === 'user' &&
|
|
3299
|
-
typeof message.content === 'string' &&
|
|
3300
|
-
message.content.endsWith(effectivePrompt)) {
|
|
3301
|
-
insertionIndex = index;
|
|
3302
|
-
foundPrompt = true;
|
|
3303
|
-
break;
|
|
3471
|
+
let compactionUsage = {
|
|
3472
|
+
inputTokens: 0,
|
|
3473
|
+
outputTokens: 0,
|
|
3474
|
+
};
|
|
3475
|
+
let compactionDurationMs;
|
|
3476
|
+
let compactionDurationWithoutRetriesMs;
|
|
3477
|
+
let compactionModelUsage;
|
|
3478
|
+
const definitions = provider.capabilities.tools
|
|
3479
|
+
? (structuredTools?.definitions() ?? [])
|
|
3480
|
+
: [];
|
|
3481
|
+
const budget = this.contextBudget(provider);
|
|
3482
|
+
const recoveryPlanner = new ContextRecoveryPlanner();
|
|
3483
|
+
const pendingUserMessages = expandedMessages.map((message, index) => ({
|
|
3484
|
+
role: 'user',
|
|
3485
|
+
content: message.text,
|
|
3486
|
+
...(message.contentBlocks?.length
|
|
3487
|
+
? { contentBlocks: message.contentBlocks }
|
|
3488
|
+
: {}),
|
|
3489
|
+
...((index === attachmentIndex && images.length > 0) ||
|
|
3490
|
+
message.images?.length
|
|
3491
|
+
? {
|
|
3492
|
+
images: [
|
|
3493
|
+
...(index === attachmentIndex ? images : []),
|
|
3494
|
+
...(message.images ?? []),
|
|
3495
|
+
],
|
|
3496
|
+
}
|
|
3497
|
+
: {}),
|
|
3498
|
+
...(index === attachmentIndex && documents.length > 0
|
|
3499
|
+
? { documents }
|
|
3500
|
+
: {}),
|
|
3501
|
+
}));
|
|
3502
|
+
const agentMentionMessages = shellCommand === undefined && !skipUserPrompt
|
|
3503
|
+
? (this.options.extensions?.agentMentionMessages(effectivePrompt) ?? [])
|
|
3504
|
+
: [];
|
|
3505
|
+
const injectAgentMentionContext = (messages) => {
|
|
3506
|
+
if (agentMentionMessages.length === 0)
|
|
3507
|
+
return [...messages];
|
|
3508
|
+
let insertionIndex = messages.length;
|
|
3509
|
+
let foundPrompt = false;
|
|
3510
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
3511
|
+
const message = messages[index];
|
|
3512
|
+
if (message?.role === 'user' &&
|
|
3513
|
+
typeof message.content === 'string' &&
|
|
3514
|
+
message.content.endsWith(effectivePrompt)) {
|
|
3515
|
+
insertionIndex = index;
|
|
3516
|
+
foundPrompt = true;
|
|
3517
|
+
break;
|
|
3518
|
+
}
|
|
3304
3519
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
return [
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
...agentMentionMessages.map((content) => ({
|
|
3311
|
-
role: 'user',
|
|
3312
|
-
content,
|
|
3313
|
-
})),
|
|
3314
|
-
...messages.slice(insertionIndex),
|
|
3315
|
-
];
|
|
3316
|
-
};
|
|
3317
|
-
const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, assembledContext?.firstUserMessageContext);
|
|
3318
|
-
const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
|
|
3319
|
-
let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
|
|
3320
|
-
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
|
|
3321
|
-
if (!budget || this.options.autoCompact === false)
|
|
3322
|
-
return;
|
|
3323
|
-
const historyMessages = [
|
|
3324
|
-
...projectClaudeModelMessages(snapshot.entries),
|
|
3325
|
-
...projectMemoryRecallMessages,
|
|
3326
|
-
];
|
|
3327
|
-
const predicted = budget.evaluate([
|
|
3328
|
-
...contextMessages,
|
|
3329
|
-
...injectTurnContext([...historyMessages, ...pendingMessages]),
|
|
3330
|
-
], definitions, options);
|
|
3331
|
-
if (!predicted.shouldCompact)
|
|
3332
|
-
return;
|
|
3333
|
-
const irreducibleMessages = [
|
|
3334
|
-
...contextMessages,
|
|
3335
|
-
...injectTurnContext([
|
|
3336
|
-
...pendingMessages,
|
|
3337
|
-
...preservedUserMessages.map((content) => ({
|
|
3520
|
+
if (!foundPrompt)
|
|
3521
|
+
return [...messages];
|
|
3522
|
+
return [
|
|
3523
|
+
...messages.slice(0, insertionIndex),
|
|
3524
|
+
...agentMentionMessages.map((content) => ({
|
|
3338
3525
|
role: 'user',
|
|
3339
3526
|
content,
|
|
3340
3527
|
})),
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
{
|
|
3528
|
+
...messages.slice(insertionIndex),
|
|
3529
|
+
];
|
|
3530
|
+
};
|
|
3531
|
+
const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, contextProjection.firstUserMessageContext);
|
|
3532
|
+
const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
|
|
3533
|
+
let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
|
|
3534
|
+
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
|
|
3535
|
+
if (!budget || this.options.autoCompact === false)
|
|
3536
|
+
return;
|
|
3537
|
+
const historyMessages = [
|
|
3538
|
+
...projectClaudeModelMessages(snapshot.entries),
|
|
3539
|
+
...projectMemoryRecallMessages,
|
|
3540
|
+
];
|
|
3541
|
+
const predicted = budget.evaluate([
|
|
3542
|
+
...contextMessages,
|
|
3543
|
+
...injectTurnContext([...historyMessages, ...pendingMessages]),
|
|
3544
|
+
], definitions, options);
|
|
3545
|
+
if (!predicted.shouldCompact)
|
|
3546
|
+
return;
|
|
3547
|
+
const irreducibleMessages = [
|
|
3548
|
+
...contextMessages,
|
|
3549
|
+
...injectTurnContext([
|
|
3550
|
+
...pendingMessages,
|
|
3551
|
+
...preservedUserMessages.map((content) => ({
|
|
3552
|
+
role: 'user',
|
|
3553
|
+
content,
|
|
3554
|
+
})),
|
|
3555
|
+
]),
|
|
3556
|
+
];
|
|
3557
|
+
const irreducible = budget.evaluate(irreducibleMessages, definitions);
|
|
3558
|
+
budget.assertFits(irreducible);
|
|
3559
|
+
let logicalParentUuid = compactionAnchorUuid;
|
|
3560
|
+
if (!logicalParentUuid || historyMessages.length === 0) {
|
|
3561
|
+
budget.assertFits(predicted);
|
|
3562
|
+
throw new Error('Cannot compact an empty Claude transcript');
|
|
3563
|
+
}
|
|
3564
|
+
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
3565
|
+
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
3566
|
+
}
|
|
3567
|
+
const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', { trigger: 'auto', custom_instructions: null }, 'auto', signal);
|
|
3568
|
+
const memorySelection = sessionMemory
|
|
3569
|
+
? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries))
|
|
3570
|
+
: null;
|
|
3571
|
+
const compactorMessages = memorySelection
|
|
3572
|
+
? [
|
|
3573
|
+
memorySelection.memoryMessage,
|
|
3574
|
+
...projectClaudeModelMessages(memorySelection.compactedEntries),
|
|
3575
|
+
]
|
|
3576
|
+
: historyMessages;
|
|
3577
|
+
compactorMessages.push(...successfulHookOutput(preCompact).map((content) => ({
|
|
3374
3578
|
role: 'user',
|
|
3375
|
-
content:
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3579
|
+
content: `Additional summarization context: ${content}`,
|
|
3580
|
+
})));
|
|
3581
|
+
if (memorySelection) {
|
|
3582
|
+
logicalParentUuid = memorySelection.logicalParentUuid;
|
|
3583
|
+
}
|
|
3584
|
+
this.options.eventSink?.({ type: 'state', state: 'compacting' });
|
|
3585
|
+
const compactEnvelope = budget.evaluate([
|
|
3381
3586
|
...irreducibleMessages,
|
|
3382
3587
|
{
|
|
3383
3588
|
role: 'user',
|
|
3384
|
-
content: formatClaudeCompactSummary('
|
|
3589
|
+
content: formatClaudeCompactSummary(''),
|
|
3385
3590
|
},
|
|
3386
|
-
], definitions)
|
|
3387
|
-
targetTokens =
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3591
|
+
], definitions);
|
|
3592
|
+
let targetTokens = Math.min(8192, compactEnvelope.availableTokens - compactEnvelope.estimatedTokens);
|
|
3593
|
+
if (targetTokens < 1) {
|
|
3594
|
+
budget.assertFits(budget.evaluate([
|
|
3595
|
+
...irreducibleMessages,
|
|
3596
|
+
{
|
|
3597
|
+
role: 'user',
|
|
3598
|
+
content: formatClaudeCompactSummary('a'),
|
|
3599
|
+
},
|
|
3600
|
+
], definitions));
|
|
3601
|
+
targetTokens = 1;
|
|
3602
|
+
}
|
|
3603
|
+
const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
|
|
3604
|
+
messages: compactorMessages,
|
|
3605
|
+
targetTokens,
|
|
3606
|
+
contextWindowTokens: budget.contextWindowTokens,
|
|
3607
|
+
...(signal ? { signal } : {}),
|
|
3608
|
+
});
|
|
3609
|
+
const { durationMs: compactedDurationMs, durationWithoutRetriesMs: compactedDurationWithoutRetriesMs, } = requireCompactionDurations(compacted);
|
|
3610
|
+
const proposedCompactionDurationMs = (compactionDurationMs ?? 0) + compactedDurationMs;
|
|
3611
|
+
if (!Number.isFinite(proposedCompactionDurationMs)) {
|
|
3612
|
+
throw new TypeError('compaction durationMs total overflow');
|
|
3613
|
+
}
|
|
3614
|
+
const proposedCompactionDurationWithoutRetriesMs = (compactionDurationWithoutRetriesMs ?? 0) +
|
|
3615
|
+
compactedDurationWithoutRetriesMs;
|
|
3616
|
+
if (!Number.isFinite(proposedCompactionDurationWithoutRetriesMs)) {
|
|
3617
|
+
throw new TypeError('compaction durationWithoutRetriesMs total overflow');
|
|
3618
|
+
}
|
|
3619
|
+
const boundaryUuid = randomUUID();
|
|
3620
|
+
const summaryUuid = randomUUID();
|
|
3621
|
+
const timestamp = new Date().toISOString();
|
|
3622
|
+
const preTokens = budget.evaluate([...contextMessages, ...injectTurnContext(historyMessages)], definitions).estimatedTokens;
|
|
3623
|
+
const compactEntries = (postTokens) => {
|
|
3624
|
+
const uuids = [boundaryUuid, summaryUuid];
|
|
3625
|
+
return createClaudeCompactEntries({
|
|
3626
|
+
sessionId,
|
|
3627
|
+
logicalParentUuid,
|
|
3628
|
+
summary: compacted.summary,
|
|
3629
|
+
preTokens,
|
|
3630
|
+
postTokens,
|
|
3631
|
+
previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
|
|
3632
|
+
durationMs: compactedDurationMs,
|
|
3633
|
+
cwd: this.activeCwd(),
|
|
3634
|
+
claudeVersion: this.options.claudeVersion,
|
|
3635
|
+
gitBranch: null,
|
|
3636
|
+
...(memorySelection
|
|
3637
|
+
? {
|
|
3638
|
+
preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
|
|
3639
|
+
}
|
|
3640
|
+
: {}),
|
|
3641
|
+
createUuid: () => uuids.shift() ?? randomUUID(),
|
|
3642
|
+
now: () => timestamp,
|
|
3643
|
+
});
|
|
3644
|
+
};
|
|
3645
|
+
const provisionalEntries = compactEntries(0);
|
|
3646
|
+
const compactSummaryUuid = provisionalEntries.at(-1)?.uuid;
|
|
3647
|
+
if (typeof compactSummaryUuid !== 'string') {
|
|
3648
|
+
throw new Error('Could not create Claude compact summary');
|
|
3649
|
+
}
|
|
3650
|
+
const replayUuids = preservedUserMessages.map(() => randomUUID());
|
|
3651
|
+
const replayEntries = translateProviderEvents(preservedUserMessages.map((text, index) => index === 0
|
|
3652
|
+
? { type: 'user-text', text }
|
|
3653
|
+
: { type: 'user-text-block', text }), {
|
|
3412
3654
|
sessionId,
|
|
3413
|
-
|
|
3414
|
-
summary: compacted.summary,
|
|
3415
|
-
preTokens,
|
|
3416
|
-
postTokens,
|
|
3417
|
-
previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
|
|
3418
|
-
durationMs: compactedDurationMs,
|
|
3655
|
+
parentUuid: compactSummaryUuid,
|
|
3419
3656
|
cwd: this.activeCwd(),
|
|
3420
3657
|
claudeVersion: this.options.claudeVersion,
|
|
3421
3658
|
gitBranch: null,
|
|
3422
|
-
...
|
|
3423
|
-
|
|
3424
|
-
preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
|
|
3425
|
-
}
|
|
3426
|
-
: {}),
|
|
3427
|
-
createUuid: () => uuids.shift() ?? randomUUID(),
|
|
3659
|
+
history: [...snapshot.entries, ...provisionalEntries],
|
|
3660
|
+
createUuid: () => replayUuids.shift() ?? randomUUID(),
|
|
3428
3661
|
now: () => timestamp,
|
|
3429
3662
|
});
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
...
|
|
3663
|
+
const compactedHistory = [
|
|
3664
|
+
...projectClaudeModelMessages([
|
|
3665
|
+
...snapshot.entries,
|
|
3666
|
+
...provisionalEntries,
|
|
3667
|
+
...replayEntries,
|
|
3668
|
+
]),
|
|
3669
|
+
...projectMemoryRecallMessages,
|
|
3670
|
+
];
|
|
3671
|
+
const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
|
|
3672
|
+
const afterPending = budget.evaluate([
|
|
3673
|
+
...contextMessages,
|
|
3674
|
+
...injectTurnContext([...compactedHistory, ...pendingMessages]),
|
|
3675
|
+
], definitions);
|
|
3676
|
+
budget.assertFits(afterPending);
|
|
3677
|
+
if (options.promptTooLong === true &&
|
|
3678
|
+
!contextRecoveryMadeProgress({
|
|
3679
|
+
beforeOccupancyTokens: predicted.occupancyTokens,
|
|
3680
|
+
afterOccupancyTokens: afterPending.occupancyTokens,
|
|
3681
|
+
})) {
|
|
3682
|
+
throw new Error('Reactive context compaction made no occupancy progress');
|
|
3683
|
+
}
|
|
3684
|
+
const entries = [
|
|
3685
|
+
...compactEntries(afterHistory.estimatedTokens),
|
|
3453
3686
|
...replayEntries,
|
|
3454
|
-
]
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
!contextRecoveryMadeProgress({
|
|
3465
|
-
beforeOccupancyTokens: predicted.occupancyTokens,
|
|
3466
|
-
afterOccupancyTokens: afterPending.occupancyTokens,
|
|
3467
|
-
})) {
|
|
3468
|
-
throw new Error('Reactive context compaction made no occupancy progress');
|
|
3469
|
-
}
|
|
3470
|
-
const entries = [
|
|
3471
|
-
...compactEntries(afterHistory.estimatedTokens),
|
|
3472
|
-
...replayEntries,
|
|
3473
|
-
];
|
|
3474
|
-
if (signal?.aborted)
|
|
3475
|
-
throw new AgentRunCancelledError();
|
|
3476
|
-
// Construct the exact tracker mutations for this single committed
|
|
3477
|
-
// boundary and preflight them against a clone of the live tracker so
|
|
3478
|
-
// invalid input or cumulative overflow rejects before the compact
|
|
3479
|
-
// boundary is appended rather than after a half-commit.
|
|
3480
|
-
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
3481
|
-
if (!tracker) {
|
|
3482
|
-
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
3483
|
-
}
|
|
3484
|
-
const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
|
|
3485
|
-
? compacted.model
|
|
3486
|
-
: provider.model;
|
|
3487
|
-
const compactModelNonBlank = compactModel !== undefined && compactModel.trim() !== '';
|
|
3488
|
-
if (hasNonZeroUsage(compacted.usage) && !compactModelNonBlank) {
|
|
3489
|
-
throw new Error('Auto compact usage requires a nonblank model identity');
|
|
3490
|
-
}
|
|
3491
|
-
let meteringTurnInput;
|
|
3492
|
-
if (compactModelNonBlank && hasNonZeroUsage(compacted.usage)) {
|
|
3493
|
-
const pricing = this.options.pricing?.resolve(compactModel);
|
|
3494
|
-
const costUsd = pricing
|
|
3495
|
-
? usageCostUsd(compacted.usage, pricing)
|
|
3496
|
-
: undefined;
|
|
3497
|
-
meteringTurnInput = {
|
|
3498
|
-
model: compactModel,
|
|
3499
|
-
usage: compacted.usage,
|
|
3500
|
-
...(costUsd === undefined ? {} : { costUsd }),
|
|
3501
|
-
...(compacted.usage.webSearchRequests === undefined
|
|
3502
|
-
? {}
|
|
3503
|
-
: { webSearchRequests: compacted.usage.webSearchRequests }),
|
|
3504
|
-
};
|
|
3505
|
-
}
|
|
3506
|
-
let meteringDurationsInput;
|
|
3507
|
-
if (compactedDurationMs > 0 ||
|
|
3508
|
-
compactedDurationWithoutRetriesMs > 0) {
|
|
3509
|
-
meteringDurationsInput = {
|
|
3510
|
-
...(compactedDurationMs === 0
|
|
3511
|
-
? {}
|
|
3512
|
-
: { apiDurationMs: compactedDurationMs }),
|
|
3513
|
-
apiDurationWithoutRetriesMs: compactedDurationWithoutRetriesMs,
|
|
3514
|
-
};
|
|
3515
|
-
}
|
|
3516
|
-
if (meteringTurnInput !== undefined ||
|
|
3517
|
-
meteringDurationsInput !== undefined) {
|
|
3518
|
-
const preflight = new ClaudeSessionCostTracker({
|
|
3519
|
-
sessionId,
|
|
3520
|
-
restored: tracker.snapshot(),
|
|
3521
|
-
});
|
|
3522
|
-
if (meteringTurnInput !== undefined) {
|
|
3523
|
-
preflight.recordTurn(meteringTurnInput);
|
|
3687
|
+
];
|
|
3688
|
+
if (signal?.aborted)
|
|
3689
|
+
throw new AgentRunCancelledError();
|
|
3690
|
+
// Construct the exact tracker mutations for this single committed
|
|
3691
|
+
// boundary and preflight them against a clone of the live tracker so
|
|
3692
|
+
// invalid input or cumulative overflow rejects before the compact
|
|
3693
|
+
// boundary is appended rather than after a half-commit.
|
|
3694
|
+
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
3695
|
+
if (!tracker) {
|
|
3696
|
+
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
3524
3697
|
}
|
|
3525
|
-
|
|
3526
|
-
|
|
3698
|
+
const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
|
|
3699
|
+
? compacted.model
|
|
3700
|
+
: provider.model;
|
|
3701
|
+
const compactModelNonBlank = compactModel !== undefined && compactModel.trim() !== '';
|
|
3702
|
+
if (hasNonZeroUsage(compacted.usage) && !compactModelNonBlank) {
|
|
3703
|
+
throw new Error('Auto compact usage requires a nonblank model identity');
|
|
3527
3704
|
}
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3705
|
+
let meteringTurnInput;
|
|
3706
|
+
if (compactModelNonBlank && hasNonZeroUsage(compacted.usage)) {
|
|
3707
|
+
const pricing = this.options.pricing?.resolve(compactModel);
|
|
3708
|
+
const costUsd = pricing
|
|
3709
|
+
? usageCostUsd(compacted.usage, pricing)
|
|
3710
|
+
: undefined;
|
|
3711
|
+
meteringTurnInput = {
|
|
3712
|
+
model: compactModel,
|
|
3713
|
+
usage: compacted.usage,
|
|
3714
|
+
...(costUsd === undefined ? {} : { costUsd }),
|
|
3715
|
+
...(compacted.usage.webSearchRequests === undefined
|
|
3716
|
+
? {}
|
|
3717
|
+
: { webSearchRequests: compacted.usage.webSearchRequests }),
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
let meteringDurationsInput;
|
|
3721
|
+
if (compactedDurationMs > 0 ||
|
|
3722
|
+
compactedDurationWithoutRetriesMs > 0) {
|
|
3723
|
+
meteringDurationsInput = {
|
|
3724
|
+
...(compactedDurationMs === 0
|
|
3725
|
+
? {}
|
|
3726
|
+
: { apiDurationMs: compactedDurationMs }),
|
|
3727
|
+
apiDurationWithoutRetriesMs: compactedDurationWithoutRetriesMs,
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
if (meteringTurnInput !== undefined ||
|
|
3731
|
+
meteringDurationsInput !== undefined) {
|
|
3732
|
+
const preflight = new ClaudeSessionCostTracker({
|
|
3733
|
+
sessionId,
|
|
3734
|
+
restored: tracker.snapshot(),
|
|
3735
|
+
});
|
|
3736
|
+
if (meteringTurnInput !== undefined) {
|
|
3737
|
+
preflight.recordTurn(meteringTurnInput);
|
|
3738
|
+
}
|
|
3739
|
+
if (meteringDurationsInput !== undefined) {
|
|
3740
|
+
preflight.recordDurations(meteringDurationsInput);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
3744
|
+
if (appendResult.status === 'conflict') {
|
|
3745
|
+
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
3542
3746
|
}
|
|
3543
3747
|
snapshot = {
|
|
3544
|
-
entries: [...snapshot.entries, ...
|
|
3545
|
-
tail:
|
|
3546
|
-
};
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
await recordHookOutcome(outcome);
|
|
3558
|
-
}
|
|
3559
|
-
this.options.contextAssembler?.invalidate?.({
|
|
3560
|
-
lifecycleId: sessionId,
|
|
3561
|
-
reason: 'compact',
|
|
3562
|
-
});
|
|
3563
|
-
this.options.projectMemoryRecall?.recordCompact(sessionId);
|
|
3564
|
-
await refreshRuntimeContext();
|
|
3565
|
-
this.options.eventSink?.({
|
|
3566
|
-
type: 'compact-boundary',
|
|
3567
|
-
trigger: 'auto',
|
|
3568
|
-
preTokens,
|
|
3569
|
-
uuid: boundaryUuid,
|
|
3570
|
-
});
|
|
3571
|
-
compactionAnchorUuid = compactSummaryUuid;
|
|
3572
|
-
compactionUsage = mergeUsage(compactionUsage, compacted.usage);
|
|
3573
|
-
compactionDurationMs = proposedCompactionDurationMs;
|
|
3574
|
-
compactionDurationWithoutRetriesMs =
|
|
3575
|
-
proposedCompactionDurationWithoutRetriesMs;
|
|
3576
|
-
if (meteringTurnInput !== undefined) {
|
|
3577
|
-
compactionModelUsage = mergeSessionRawModelUsage(compactionModelUsage, { [meteringTurnInput.model]: compacted.usage });
|
|
3578
|
-
}
|
|
3579
|
-
// Commit the preflighted mutations once the boundary is durable so a
|
|
3580
|
-
// later main-provider failure or cancellation cannot lose the
|
|
3581
|
-
// compactor's usage/cost/API durations.
|
|
3582
|
-
if (meteringTurnInput !== undefined) {
|
|
3583
|
-
tracker.recordTurn(meteringTurnInput);
|
|
3584
|
-
}
|
|
3585
|
-
if (meteringDurationsInput !== undefined) {
|
|
3586
|
-
tracker.recordDurations(meteringDurationsInput);
|
|
3587
|
-
}
|
|
3588
|
-
};
|
|
3589
|
-
await compactIfNeeded(pendingUserMessages);
|
|
3590
|
-
for (const [index, message] of expandedMessages.entries()) {
|
|
3591
|
-
if (shellCommand !== undefined)
|
|
3592
|
-
break;
|
|
3593
|
-
const messageImages = [
|
|
3594
|
-
...(index === attachmentIndex ? images : []),
|
|
3595
|
-
...(message.images ?? []),
|
|
3596
|
-
];
|
|
3597
|
-
const persistenceEvent = messageImages.length > 0 ||
|
|
3598
|
-
(index === attachmentIndex && documents.length > 0)
|
|
3599
|
-
? {
|
|
3600
|
-
type: 'user-message',
|
|
3601
|
-
text: message.text,
|
|
3602
|
-
images: messageImages,
|
|
3603
|
-
...(index === attachmentIndex && documents.length > 0
|
|
3604
|
-
? { documents }
|
|
3605
|
-
: {}),
|
|
3748
|
+
entries: [...snapshot.entries, ...entries],
|
|
3749
|
+
tail: appendResult.tail,
|
|
3750
|
+
};
|
|
3751
|
+
const metadataEntries = createClaudeDurableMetadataSnapshot(snapshot.entries, sessionId);
|
|
3752
|
+
if (metadataEntries.length > 0) {
|
|
3753
|
+
const metadataAppend = await lease.appendMany(snapshot.tail, metadataEntries);
|
|
3754
|
+
if (metadataAppend.status === 'conflict') {
|
|
3755
|
+
throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
|
|
3756
|
+
}
|
|
3757
|
+
snapshot = {
|
|
3758
|
+
entries: [...snapshot.entries, ...metadataEntries],
|
|
3759
|
+
tail: metadataAppend.tail,
|
|
3760
|
+
};
|
|
3606
3761
|
}
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
};
|
|
3622
|
-
}
|
|
3623
|
-
if (fileHistory && currentPromptId) {
|
|
3624
|
-
const historySnapshot = await fileHistory.snapshot(snapshot.entries, currentPromptId);
|
|
3625
|
-
const historyTail = await this.append(lease, snapshot.tail, historySnapshot);
|
|
3626
|
-
snapshot = {
|
|
3627
|
-
entries: [...snapshot.entries, historySnapshot],
|
|
3628
|
-
tail: historyTail,
|
|
3629
|
-
};
|
|
3630
|
-
}
|
|
3631
|
-
if (this.options.hooks && !skipUserPrompt) {
|
|
3632
|
-
const outcome = await this.options.hooks.run({
|
|
3633
|
-
...hookSession,
|
|
3634
|
-
hook_event_name: 'UserPromptSubmit',
|
|
3635
|
-
prompt_id: currentPromptId ?? randomUUID(),
|
|
3636
|
-
prompt: effectivePrompt,
|
|
3637
|
-
}, undefined, signal);
|
|
3638
|
-
await recordHookOutcome(outcome);
|
|
3639
|
-
if (outcome.blockedReason) {
|
|
3640
|
-
throw new Error(`UserPromptSubmit hook error: ${outcome.blockedReason}`);
|
|
3641
|
-
}
|
|
3642
|
-
}
|
|
3643
|
-
let shellUsage = { inputTokens: 0, outputTokens: 0 };
|
|
3644
|
-
let shellModelUsage;
|
|
3645
|
-
if (shellCommand !== undefined) {
|
|
3646
|
-
const call = {
|
|
3647
|
-
id: `shell_${randomUUID().replaceAll('-', '')}`,
|
|
3648
|
-
name: 'Bash',
|
|
3649
|
-
input: { command: shellCommand },
|
|
3650
|
-
};
|
|
3651
|
-
this.options.eventSink?.({
|
|
3652
|
-
type: 'shell-command',
|
|
3653
|
-
callId: call.id,
|
|
3654
|
-
command: shellCommand,
|
|
3655
|
-
});
|
|
3656
|
-
let shellResult;
|
|
3657
|
-
try {
|
|
3658
|
-
shellResult = await runtime.executeDirectToolCall(call, {
|
|
3659
|
-
cwd: this.activeCwd(),
|
|
3660
|
-
sessionId,
|
|
3661
|
-
toolResultDirectory,
|
|
3662
|
-
messages: projectClaudeModelMessages(snapshot.entries),
|
|
3663
|
-
observer: {
|
|
3664
|
-
assistantCompleted: async () => undefined,
|
|
3665
|
-
toolCompleted: async () => undefined,
|
|
3666
|
-
followUpUserMessagesCompleted: observer.followUpUserMessagesCompleted,
|
|
3667
|
-
},
|
|
3668
|
-
...(this.options.approveTool
|
|
3669
|
-
? { approveTool: this.options.approveTool }
|
|
3670
|
-
: {}),
|
|
3671
|
-
permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
3672
|
-
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
3673
|
-
...(signal ? { signal } : {}),
|
|
3762
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
3763
|
+
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
|
|
3764
|
+
// The boundary is durable: mirror Claude's full-compact behavior by
|
|
3765
|
+
// rerunning SessionStart with source compact and refreshing the
|
|
3766
|
+
// runtime-only context so the next request retains current
|
|
3767
|
+
// instructions, plan state, session memory, and hook context.
|
|
3768
|
+
if (this.options.hooks) {
|
|
3769
|
+
const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
|
|
3770
|
+
if (outcome)
|
|
3771
|
+
await recordHookOutcome(outcome);
|
|
3772
|
+
}
|
|
3773
|
+
this.options.contextAssembler?.invalidate?.({
|
|
3774
|
+
lifecycleId: sessionId,
|
|
3775
|
+
reason: 'compact',
|
|
3674
3776
|
});
|
|
3675
|
-
|
|
3676
|
-
|
|
3777
|
+
this.options.projectMemoryRecall?.recordCompact(sessionId);
|
|
3778
|
+
await refreshRuntimeContext();
|
|
3677
3779
|
this.options.eventSink?.({
|
|
3678
|
-
type: '
|
|
3679
|
-
|
|
3780
|
+
type: 'compact-boundary',
|
|
3781
|
+
trigger: 'auto',
|
|
3782
|
+
preTokens,
|
|
3783
|
+
uuid: boundaryUuid,
|
|
3680
3784
|
});
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
const stdout = shellResult.processOutput?.stdout ??
|
|
3689
|
-
(shellResult.isError ? '' : shellResult.content);
|
|
3690
|
-
const stderr = shellResult.processOutput?.stderr ??
|
|
3691
|
-
(shellResult.isError ? shellResult.content : '');
|
|
3692
|
-
const shellUuids = [shellInputUuid ?? randomUUID(), randomUUID()];
|
|
3693
|
-
const [inputEntry, outputEntry] = translateProviderEvents([
|
|
3694
|
-
{ type: 'bash-input', command: shellCommand },
|
|
3695
|
-
{ type: 'bash-output', stdout, stderr },
|
|
3696
|
-
], {
|
|
3697
|
-
...this.translationContext(sessionId, snapshot),
|
|
3698
|
-
createUuid: () => shellUuids.shift() ?? randomUUID(),
|
|
3699
|
-
});
|
|
3700
|
-
if (!inputEntry || !outputEntry) {
|
|
3701
|
-
throw new Error('Could not translate shell command result');
|
|
3702
|
-
}
|
|
3703
|
-
const shellAppend = await lease.appendMany(snapshot.tail, [
|
|
3704
|
-
inputEntry,
|
|
3705
|
-
outputEntry,
|
|
3706
|
-
]);
|
|
3707
|
-
if (shellAppend.status === 'conflict') {
|
|
3708
|
-
throw new Error(`Claude transcript append conflict: ${shellAppend.reason}`);
|
|
3709
|
-
}
|
|
3710
|
-
snapshot = {
|
|
3711
|
-
entries: [...snapshot.entries, inputEntry, outputEntry],
|
|
3712
|
-
tail: shellAppend.tail,
|
|
3713
|
-
};
|
|
3714
|
-
currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
|
|
3715
|
-
this.options.eventSink?.({
|
|
3716
|
-
type: 'shell-result',
|
|
3717
|
-
callId: call.id,
|
|
3718
|
-
stdout,
|
|
3719
|
-
stderr,
|
|
3720
|
-
isError: shellResult.isError,
|
|
3721
|
-
});
|
|
3722
|
-
}
|
|
3723
|
-
if (budget) {
|
|
3724
|
-
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
3725
|
-
budget.assertFits(budget.evaluate([
|
|
3726
|
-
...contextMessages,
|
|
3727
|
-
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
3728
|
-
], definitions));
|
|
3729
|
-
}
|
|
3730
|
-
let stopHookActive = false;
|
|
3731
|
-
const runtimeRequest = {
|
|
3732
|
-
sessionId,
|
|
3733
|
-
messages: [
|
|
3734
|
-
...contextMessages,
|
|
3735
|
-
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
3736
|
-
],
|
|
3737
|
-
...(stableSystemMessageCount === undefined
|
|
3738
|
-
? {}
|
|
3739
|
-
: { stableSystemMessageCount }),
|
|
3740
|
-
cwd: this.activeCwd(),
|
|
3741
|
-
toolResultDirectory,
|
|
3742
|
-
observer,
|
|
3743
|
-
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
3744
|
-
...(this.options.maxModelTurns !== undefined
|
|
3745
|
-
? { maxModelTurns: this.options.maxModelTurns }
|
|
3746
|
-
: {}),
|
|
3747
|
-
...(this.options.betas?.length ? { betas: this.options.betas } : {}),
|
|
3748
|
-
...(this.options.collectMetrics || this.options.costStateStore
|
|
3749
|
-
? { collectMetrics: true }
|
|
3750
|
-
: {}),
|
|
3751
|
-
...(budget
|
|
3752
|
-
? { deferFailureKinds: ['prompt_too_long'] }
|
|
3753
|
-
: {}),
|
|
3754
|
-
reloadMessages: async () => {
|
|
3755
|
-
const recalled = projectMemoryRecallTurn?.consumeIfSettled();
|
|
3756
|
-
if (recalled) {
|
|
3757
|
-
projectMemoryRecallMessages = [
|
|
3758
|
-
{ role: 'user', content: recalled.content },
|
|
3759
|
-
];
|
|
3785
|
+
compactionAnchorUuid = compactSummaryUuid;
|
|
3786
|
+
compactionUsage = mergeUsage(compactionUsage, compacted.usage);
|
|
3787
|
+
compactionDurationMs = proposedCompactionDurationMs;
|
|
3788
|
+
compactionDurationWithoutRetriesMs =
|
|
3789
|
+
proposedCompactionDurationWithoutRetriesMs;
|
|
3790
|
+
if (meteringTurnInput !== undefined) {
|
|
3791
|
+
compactionModelUsage = mergeSessionRawModelUsage(compactionModelUsage, { [meteringTurnInput.model]: compacted.usage });
|
|
3760
3792
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3793
|
+
// Commit the preflighted mutations once the boundary is durable so a
|
|
3794
|
+
// later main-provider failure or cancellation cannot lose the
|
|
3795
|
+
// compactor's usage/cost/API durations.
|
|
3796
|
+
if (meteringTurnInput !== undefined) {
|
|
3797
|
+
tracker.recordTurn(meteringTurnInput);
|
|
3764
3798
|
}
|
|
3765
|
-
|
|
3766
|
-
|
|
3799
|
+
if (meteringDurationsInput !== undefined) {
|
|
3800
|
+
tracker.recordDurations(meteringDurationsInput);
|
|
3767
3801
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3802
|
+
};
|
|
3803
|
+
await compactIfNeeded(pendingUserMessages);
|
|
3804
|
+
for (const [index, message] of expandedMessages.entries()) {
|
|
3805
|
+
if (shellCommand !== undefined)
|
|
3806
|
+
break;
|
|
3807
|
+
const messageImages = [
|
|
3808
|
+
...(index === attachmentIndex ? images : []),
|
|
3809
|
+
...(message.images ?? []),
|
|
3774
3810
|
];
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
const background = await subagentExecutor?.notifications(true);
|
|
3798
|
-
if (background)
|
|
3799
|
-
messages.push(...background.messages);
|
|
3800
|
-
const workflow = await this.workflowManager?.notifications(true);
|
|
3801
|
-
if (workflow)
|
|
3802
|
-
messages.push(...workflow.messages);
|
|
3803
|
-
const bashMessages = await taskTools?.notifications(true);
|
|
3804
|
-
if (bashMessages)
|
|
3805
|
-
messages.push(...bashMessages);
|
|
3806
|
-
const scheduled = await this.scheduledPrompts?.drainDue();
|
|
3807
|
-
if (scheduled) {
|
|
3808
|
-
messages.push(...scheduled.map(({ prompt }) => prompt));
|
|
3809
|
-
}
|
|
3810
|
-
const batchUsage = background || workflow
|
|
3811
|
-
? mergeUsage(background?.usage ?? {
|
|
3812
|
-
inputTokens: 0,
|
|
3813
|
-
outputTokens: 0,
|
|
3814
|
-
}, workflow?.usage ?? {
|
|
3815
|
-
inputTokens: 0,
|
|
3816
|
-
outputTokens: 0,
|
|
3817
|
-
})
|
|
3818
|
-
: undefined;
|
|
3819
|
-
const batchModelUsage = mergeSessionRawModelUsage(background?.modelUsage, workflow?.modelUsage);
|
|
3820
|
-
const batchDurationApiMs = addApiDuration(workflow?.durationApiMs, addApiDuration(background?.durationApiMs, 0, 'durationApiMs'), 'durationApiMs');
|
|
3821
|
-
const backgroundDurationWithoutRetries = background?.durationApiWithoutRetriesMs ??
|
|
3822
|
-
background?.durationApiMs;
|
|
3823
|
-
const workflowDurationWithoutRetries = workflow?.durationApiWithoutRetriesMs ??
|
|
3824
|
-
workflow?.durationApiMs;
|
|
3825
|
-
const batchDurationApiWithoutRetriesMs = addApiDuration(workflowDurationWithoutRetries, addApiDuration(backgroundDurationWithoutRetries, 0, 'durationApiWithoutRetriesMs'), 'durationApiWithoutRetriesMs');
|
|
3826
|
-
const hasBatchDuration = background?.durationApiMs !== undefined ||
|
|
3827
|
-
background?.durationApiWithoutRetriesMs !== undefined ||
|
|
3828
|
-
workflow?.durationApiMs !== undefined ||
|
|
3829
|
-
workflow?.durationApiWithoutRetriesMs !== undefined;
|
|
3830
|
-
return {
|
|
3831
|
-
messages,
|
|
3832
|
-
...(batchUsage
|
|
3833
|
-
? {
|
|
3834
|
-
usage: batchUsage,
|
|
3835
|
-
...(batchModelUsage
|
|
3836
|
-
? { modelUsage: batchModelUsage }
|
|
3837
|
-
: {}),
|
|
3838
|
-
}
|
|
3839
|
-
: {}),
|
|
3840
|
-
...(hasBatchDuration
|
|
3841
|
-
? {
|
|
3842
|
-
durationApiMs: batchDurationApiMs,
|
|
3843
|
-
durationApiWithoutRetriesMs: batchDurationApiWithoutRetriesMs,
|
|
3844
|
-
}
|
|
3845
|
-
: {}),
|
|
3846
|
-
};
|
|
3847
|
-
},
|
|
3811
|
+
const persistenceEvent = messageImages.length > 0 ||
|
|
3812
|
+
(index === attachmentIndex && documents.length > 0)
|
|
3813
|
+
? {
|
|
3814
|
+
type: 'user-message',
|
|
3815
|
+
text: message.text,
|
|
3816
|
+
images: messageImages,
|
|
3817
|
+
...(index === attachmentIndex && documents.length > 0
|
|
3818
|
+
? { documents }
|
|
3819
|
+
: {}),
|
|
3820
|
+
}
|
|
3821
|
+
: index === 0
|
|
3822
|
+
? { type: 'user-text', text: message.text }
|
|
3823
|
+
: { type: 'user-text-block', text: message.text };
|
|
3824
|
+
const [userEntry] = translateProviderEvents([persistenceEvent], this.translationContext(sessionId, snapshot));
|
|
3825
|
+
if (!userEntry)
|
|
3826
|
+
throw new Error('Could not translate user prompt');
|
|
3827
|
+
if (currentPromptId === null &&
|
|
3828
|
+
typeof userEntry.uuid === 'string') {
|
|
3829
|
+
currentPromptId = userEntry.uuid;
|
|
3830
|
+
compactionAnchorUuid ??= userEntry.uuid;
|
|
3848
3831
|
}
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
3855
|
-
};
|
|
3856
|
-
const attemptMainTurn = () => signal
|
|
3857
|
-
? runtime.run({ ...runtimeRequest, signal })
|
|
3858
|
-
: runtime.run(runtimeRequest);
|
|
3859
|
-
const surfaceExhaustedRecovery = (error) => {
|
|
3860
|
-
this.options.eventSink?.({
|
|
3861
|
-
type: 'failed',
|
|
3862
|
-
message: error.message,
|
|
3863
|
-
retryable: false,
|
|
3864
|
-
});
|
|
3865
|
-
};
|
|
3866
|
-
let result;
|
|
3867
|
-
try {
|
|
3868
|
-
try {
|
|
3869
|
-
result = await attemptMainTurn();
|
|
3832
|
+
const userTail = await this.append(lease, snapshot.tail, userEntry);
|
|
3833
|
+
snapshot = {
|
|
3834
|
+
entries: [...snapshot.entries, userEntry],
|
|
3835
|
+
tail: userTail,
|
|
3836
|
+
};
|
|
3870
3837
|
}
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3838
|
+
if (fileHistory && currentPromptId) {
|
|
3839
|
+
const historySnapshot = await fileHistory.snapshot(snapshot.entries, currentPromptId);
|
|
3840
|
+
const historyTail = await this.append(lease, snapshot.tail, historySnapshot);
|
|
3841
|
+
snapshot = {
|
|
3842
|
+
entries: [...snapshot.entries, historySnapshot],
|
|
3843
|
+
tail: historyTail,
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
if (this.options.hooks && !skipUserPrompt) {
|
|
3847
|
+
const outcome = await this.options.hooks.run({
|
|
3848
|
+
...hookSession,
|
|
3849
|
+
hook_event_name: 'UserPromptSubmit',
|
|
3850
|
+
prompt_id: currentPromptId ?? randomUUID(),
|
|
3851
|
+
prompt: effectivePrompt,
|
|
3852
|
+
}, undefined, signal);
|
|
3853
|
+
await recordHookOutcome(outcome);
|
|
3854
|
+
if (outcome.blockedReason) {
|
|
3855
|
+
throw new Error(`UserPromptSubmit hook error: ${outcome.blockedReason}`);
|
|
3877
3856
|
}
|
|
3878
|
-
|
|
3857
|
+
}
|
|
3858
|
+
let shellUsage = { inputTokens: 0, outputTokens: 0 };
|
|
3859
|
+
let shellModelUsage;
|
|
3860
|
+
if (shellCommand !== undefined) {
|
|
3861
|
+
const call = {
|
|
3862
|
+
id: `shell_${randomUUID().replaceAll('-', '')}`,
|
|
3863
|
+
name: 'Bash',
|
|
3864
|
+
input: { command: shellCommand },
|
|
3865
|
+
};
|
|
3866
|
+
this.options.eventSink?.({
|
|
3867
|
+
type: 'shell-command',
|
|
3868
|
+
callId: call.id,
|
|
3869
|
+
command: shellCommand,
|
|
3870
|
+
});
|
|
3871
|
+
let shellResult;
|
|
3879
3872
|
try {
|
|
3880
|
-
await
|
|
3881
|
-
|
|
3873
|
+
shellResult = await runtime.executeDirectToolCall(call, {
|
|
3874
|
+
cwd: this.activeCwd(),
|
|
3875
|
+
sessionId,
|
|
3876
|
+
toolResultDirectory,
|
|
3877
|
+
messages: projectClaudeModelMessages(snapshot.entries),
|
|
3878
|
+
observer: {
|
|
3879
|
+
assistantCompleted: async () => undefined,
|
|
3880
|
+
toolCompleted: async () => undefined,
|
|
3881
|
+
followUpUserMessagesCompleted: observer.followUpUserMessagesCompleted,
|
|
3882
|
+
},
|
|
3883
|
+
...(this.options.approveTool
|
|
3884
|
+
? { approveTool: this.options.approveTool }
|
|
3885
|
+
: {}),
|
|
3886
|
+
permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
3887
|
+
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
3888
|
+
...(signal ? { signal } : {}),
|
|
3882
3889
|
});
|
|
3883
3890
|
}
|
|
3884
|
-
catch (
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
}
|
|
3889
|
-
// Compaction could not free the provider-bounded context; surface
|
|
3890
|
-
// the original prompt-too-long error rather than the compaction
|
|
3891
|
-
// failure.
|
|
3892
|
-
surfaceExhaustedRecovery(error);
|
|
3891
|
+
catch (error) {
|
|
3892
|
+
this.options.eventSink?.({
|
|
3893
|
+
type: 'shell-cancelled',
|
|
3894
|
+
callId: call.id,
|
|
3895
|
+
});
|
|
3893
3896
|
throw error;
|
|
3894
3897
|
}
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
...injectTurnContext([
|
|
3900
|
-
...projectClaudeModelMessages(snapshot.entries),
|
|
3901
|
-
...projectMemoryRecallMessages,
|
|
3902
|
-
]),
|
|
3903
|
-
];
|
|
3904
|
-
if (stableSystemMessageCount === undefined) {
|
|
3905
|
-
delete runtimeRequest.stableSystemMessageCount;
|
|
3898
|
+
shellUsage = shellResult.usage ?? shellUsage;
|
|
3899
|
+
if (!shellResult.isError) {
|
|
3900
|
+
shellModelUsage = shellResult.modelUsage;
|
|
3901
|
+
foregroundLineChanges.add(shellResult);
|
|
3906
3902
|
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3903
|
+
const stdout = shellResult.processOutput?.stdout ??
|
|
3904
|
+
(shellResult.isError ? '' : shellResult.content);
|
|
3905
|
+
const stderr = shellResult.processOutput?.stderr ??
|
|
3906
|
+
(shellResult.isError ? shellResult.content : '');
|
|
3907
|
+
const shellUuids = [shellInputUuid ?? randomUUID(), randomUUID()];
|
|
3908
|
+
const [inputEntry, outputEntry] = translateProviderEvents([
|
|
3909
|
+
{ type: 'bash-input', command: shellCommand },
|
|
3910
|
+
{ type: 'bash-output', stdout, stderr },
|
|
3911
|
+
], {
|
|
3912
|
+
...this.translationContext(sessionId, snapshot),
|
|
3913
|
+
createUuid: () => shellUuids.shift() ?? randomUUID(),
|
|
3914
|
+
});
|
|
3915
|
+
if (!inputEntry || !outputEntry) {
|
|
3916
|
+
throw new Error('Could not translate shell command result');
|
|
3909
3917
|
}
|
|
3910
|
-
const
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
throw error;
|
|
3918
|
+
const shellAppend = await lease.appendMany(snapshot.tail, [
|
|
3919
|
+
inputEntry,
|
|
3920
|
+
outputEntry,
|
|
3921
|
+
]);
|
|
3922
|
+
if (shellAppend.status === 'conflict') {
|
|
3923
|
+
throw new Error(`Claude transcript append conflict: ${shellAppend.reason}`);
|
|
3917
3924
|
}
|
|
3918
|
-
|
|
3925
|
+
snapshot = {
|
|
3926
|
+
entries: [...snapshot.entries, inputEntry, outputEntry],
|
|
3927
|
+
tail: shellAppend.tail,
|
|
3928
|
+
};
|
|
3929
|
+
currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
|
|
3930
|
+
this.options.eventSink?.({
|
|
3931
|
+
type: 'shell-result',
|
|
3932
|
+
callId: call.id,
|
|
3933
|
+
stdout,
|
|
3934
|
+
stderr,
|
|
3935
|
+
isError: shellResult.isError,
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
if (budget) {
|
|
3939
|
+
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
3940
|
+
budget.assertFits(budget.evaluate([
|
|
3941
|
+
...contextMessages,
|
|
3942
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
3943
|
+
], definitions));
|
|
3944
|
+
}
|
|
3945
|
+
let stopHookActive = false;
|
|
3946
|
+
const runtimeRequest = {
|
|
3947
|
+
sessionId,
|
|
3948
|
+
messages: [
|
|
3949
|
+
...contextMessages,
|
|
3950
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
3951
|
+
],
|
|
3952
|
+
stableSystemMessageCount,
|
|
3953
|
+
cwd: this.activeCwd(),
|
|
3954
|
+
toolResultDirectory,
|
|
3955
|
+
observer,
|
|
3956
|
+
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
3957
|
+
...(this.options.maxModelTurns !== undefined
|
|
3958
|
+
? { maxModelTurns: this.options.maxModelTurns }
|
|
3959
|
+
: {}),
|
|
3960
|
+
...(this.options.betas?.length
|
|
3961
|
+
? { betas: this.options.betas }
|
|
3962
|
+
: {}),
|
|
3963
|
+
...(this.options.collectMetrics || this.options.costStateStore
|
|
3964
|
+
? { collectMetrics: true }
|
|
3965
|
+
: {}),
|
|
3966
|
+
...(budget
|
|
3967
|
+
? { deferFailureKinds: ['prompt_too_long'] }
|
|
3968
|
+
: {}),
|
|
3969
|
+
reloadMessages: async () => {
|
|
3970
|
+
const recalled = projectMemoryRecallTurn?.consumeIfSettled();
|
|
3971
|
+
if (recalled) {
|
|
3972
|
+
projectMemoryRecallMessages = [
|
|
3973
|
+
{ role: 'user', content: recalled.content },
|
|
3974
|
+
];
|
|
3975
|
+
}
|
|
3976
|
+
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
3977
|
+
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
3978
|
+
return [
|
|
3979
|
+
...contextMessages,
|
|
3980
|
+
...injectTurnContext([
|
|
3981
|
+
...projectClaudeModelMessages(snapshot.entries),
|
|
3982
|
+
...projectMemoryRecallMessages,
|
|
3983
|
+
]),
|
|
3984
|
+
];
|
|
3985
|
+
},
|
|
3986
|
+
...(this.options.hooks ||
|
|
3987
|
+
subagentExecutor ||
|
|
3988
|
+
taskTools ||
|
|
3989
|
+
this.scheduledPrompts ||
|
|
3990
|
+
this.workflowManager
|
|
3991
|
+
? {
|
|
3992
|
+
onStop: async (text) => {
|
|
3993
|
+
const messages = [];
|
|
3994
|
+
const outcome = await this.options.hooks?.run({
|
|
3995
|
+
...hookSession,
|
|
3996
|
+
hook_event_name: 'Stop',
|
|
3997
|
+
stop_hook_active: stopHookActive,
|
|
3998
|
+
last_assistant_message: text,
|
|
3999
|
+
}, undefined, signal);
|
|
4000
|
+
if (outcome) {
|
|
4001
|
+
await recordHookOutcome(outcome);
|
|
4002
|
+
if (outcome.blockedReason) {
|
|
4003
|
+
stopHookActive = true;
|
|
4004
|
+
messages.push(`Stop hook error: ${outcome.blockedReason}`);
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
const claimedAgentIds = new Set(this.sessionSubagentExecutors(sessionId, true).flatMap((executor) => executor.notificationClaimAgentIds()));
|
|
4008
|
+
await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => {
|
|
4009
|
+
const delivered = (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification);
|
|
4010
|
+
return this.hostedSubagents.has(executor)
|
|
4011
|
+
? executor.reconcileDetachedNotifications(delivered)
|
|
4012
|
+
: executor.reconcileDeliveredNotifications(delivered);
|
|
4013
|
+
}));
|
|
4014
|
+
await subagentExecutor?.hydratePersistedTasks(sessionId, this.activeCwd(), claimedAgentIds);
|
|
4015
|
+
const background = await this.collectSubagentNotifications(sessionId, subagentExecutor ?? undefined);
|
|
4016
|
+
if (background)
|
|
4017
|
+
messages.push(...background.messages);
|
|
4018
|
+
const workflow = await this.workflowManager?.notifications(true);
|
|
4019
|
+
if (workflow)
|
|
4020
|
+
messages.push(...workflow.messages);
|
|
4021
|
+
const bashMessages = await taskTools?.notifications(true);
|
|
4022
|
+
if (bashMessages)
|
|
4023
|
+
messages.push(...bashMessages);
|
|
4024
|
+
const scheduled = await this.scheduledPrompts?.drainDue();
|
|
4025
|
+
if (scheduled) {
|
|
4026
|
+
messages.push(...scheduled.map(({ prompt }) => prompt));
|
|
4027
|
+
}
|
|
4028
|
+
const batchUsage = background || workflow
|
|
4029
|
+
? mergeUsage(background?.usage ?? {
|
|
4030
|
+
inputTokens: 0,
|
|
4031
|
+
outputTokens: 0,
|
|
4032
|
+
}, workflow?.usage ?? {
|
|
4033
|
+
inputTokens: 0,
|
|
4034
|
+
outputTokens: 0,
|
|
4035
|
+
})
|
|
4036
|
+
: undefined;
|
|
4037
|
+
const batchModelUsage = mergeSessionRawModelUsage(background?.modelUsage, workflow?.modelUsage);
|
|
4038
|
+
const batchDurationApiMs = addApiDuration(workflow?.durationApiMs, addApiDuration(background?.durationApiMs, 0, 'durationApiMs'), 'durationApiMs');
|
|
4039
|
+
const backgroundDurationWithoutRetries = background?.durationApiWithoutRetriesMs ??
|
|
4040
|
+
background?.durationApiMs;
|
|
4041
|
+
const workflowDurationWithoutRetries = workflow?.durationApiWithoutRetriesMs ??
|
|
4042
|
+
workflow?.durationApiMs;
|
|
4043
|
+
const batchDurationApiWithoutRetriesMs = addApiDuration(workflowDurationWithoutRetries, addApiDuration(backgroundDurationWithoutRetries, 0, 'durationApiWithoutRetriesMs'), 'durationApiWithoutRetriesMs');
|
|
4044
|
+
const hasBatchDuration = background?.durationApiMs !== undefined ||
|
|
4045
|
+
background?.durationApiWithoutRetriesMs !== undefined ||
|
|
4046
|
+
workflow?.durationApiMs !== undefined ||
|
|
4047
|
+
workflow?.durationApiWithoutRetriesMs !== undefined;
|
|
4048
|
+
return {
|
|
4049
|
+
messages,
|
|
4050
|
+
...(batchUsage
|
|
4051
|
+
? {
|
|
4052
|
+
usage: batchUsage,
|
|
4053
|
+
...(batchModelUsage
|
|
4054
|
+
? { modelUsage: batchModelUsage }
|
|
4055
|
+
: {}),
|
|
4056
|
+
}
|
|
4057
|
+
: {}),
|
|
4058
|
+
...(hasBatchDuration
|
|
4059
|
+
? {
|
|
4060
|
+
durationApiMs: batchDurationApiMs,
|
|
4061
|
+
durationApiWithoutRetriesMs: batchDurationApiWithoutRetriesMs,
|
|
4062
|
+
}
|
|
4063
|
+
: {}),
|
|
4064
|
+
};
|
|
4065
|
+
},
|
|
4066
|
+
}
|
|
4067
|
+
: {}),
|
|
4068
|
+
...(this.options.approveTool
|
|
4069
|
+
? { approveTool: this.options.approveTool }
|
|
4070
|
+
: {}),
|
|
4071
|
+
permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
4072
|
+
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
4073
|
+
};
|
|
4074
|
+
const attemptMainTurn = () => signal
|
|
4075
|
+
? runtime.run({ ...runtimeRequest, signal })
|
|
4076
|
+
: runtime.run(runtimeRequest);
|
|
4077
|
+
const surfaceExhaustedRecovery = (error) => {
|
|
4078
|
+
this.options.eventSink?.({
|
|
4079
|
+
type: 'failed',
|
|
4080
|
+
message: error.message,
|
|
4081
|
+
retryable: false,
|
|
4082
|
+
});
|
|
4083
|
+
};
|
|
4084
|
+
let result;
|
|
4085
|
+
try {
|
|
3919
4086
|
try {
|
|
3920
4087
|
result = await attemptMainTurn();
|
|
3921
4088
|
}
|
|
3922
|
-
catch (
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
recoveryPlanner.
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
4089
|
+
catch (error) {
|
|
4090
|
+
if (!budget || !isPromptTooLongError(error))
|
|
4091
|
+
throw error;
|
|
4092
|
+
if (recoveryPlanner.reactiveRetriesRemaining === 0) {
|
|
4093
|
+
surfaceExhaustedRecovery(error);
|
|
4094
|
+
throw error;
|
|
4095
|
+
}
|
|
4096
|
+
const beforeRecovery = budget.evaluate(runtimeRequest.messages, definitions);
|
|
4097
|
+
try {
|
|
4098
|
+
await compactIfNeeded([], currentTurnUserMessages ?? [], {
|
|
4099
|
+
promptTooLong: true,
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
catch (compactionError) {
|
|
4103
|
+
if (signal?.aborted ||
|
|
4104
|
+
compactionError instanceof AgentRunCancelledError) {
|
|
4105
|
+
throw new AgentRunCancelledError();
|
|
4106
|
+
}
|
|
4107
|
+
// Compaction could not free the provider-bounded context; surface
|
|
4108
|
+
// the original prompt-too-long error rather than the compaction
|
|
4109
|
+
// failure.
|
|
4110
|
+
surfaceExhaustedRecovery(error);
|
|
4111
|
+
throw error;
|
|
4112
|
+
}
|
|
4113
|
+
// The single reactive retry must use the compacted transcript, not
|
|
4114
|
+
// the stale request copy captured before the compact boundary.
|
|
4115
|
+
runtimeRequest.messages = [
|
|
4116
|
+
...contextMessages,
|
|
4117
|
+
...injectTurnContext([
|
|
4118
|
+
...projectClaudeModelMessages(snapshot.entries),
|
|
4119
|
+
...projectMemoryRecallMessages,
|
|
4120
|
+
]),
|
|
4121
|
+
];
|
|
4122
|
+
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
4123
|
+
const afterRecovery = budget.evaluate(runtimeRequest.messages, definitions);
|
|
4124
|
+
if (recoveryPlanner.consumeReactiveRetry({
|
|
4125
|
+
beforeOccupancyTokens: beforeRecovery.occupancyTokens,
|
|
4126
|
+
afterOccupancyTokens: afterRecovery.occupancyTokens,
|
|
4127
|
+
}) !== 'reactive-retry') {
|
|
4128
|
+
surfaceExhaustedRecovery(error);
|
|
4129
|
+
throw error;
|
|
4130
|
+
}
|
|
4131
|
+
runtimeRequest.deferFailureKinds = true;
|
|
4132
|
+
try {
|
|
4133
|
+
result = await attemptMainTurn();
|
|
4134
|
+
}
|
|
4135
|
+
catch (retryError) {
|
|
4136
|
+
// Exactly one reactive retry is consumed; fail deterministically
|
|
4137
|
+
// and surface the original prompt-too-long error.
|
|
4138
|
+
recoveryPlanner.recordFailure();
|
|
4139
|
+
if (signal?.aborted ||
|
|
4140
|
+
retryError instanceof AgentRunCancelledError) {
|
|
4141
|
+
throw new AgentRunCancelledError();
|
|
4142
|
+
}
|
|
4143
|
+
surfaceExhaustedRecovery(error);
|
|
4144
|
+
throw error;
|
|
3929
4145
|
}
|
|
3930
|
-
surfaceExhaustedRecovery(error);
|
|
3931
|
-
throw error;
|
|
3932
4146
|
}
|
|
3933
4147
|
}
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
4148
|
+
catch (error) {
|
|
4149
|
+
if (!signal?.aborted &&
|
|
4150
|
+
!(error instanceof AgentRunCancelledError) &&
|
|
4151
|
+
!(error instanceof ModelProviderError &&
|
|
4152
|
+
error.kind === 'cancelled')) {
|
|
4153
|
+
const failureKind = error instanceof ModelProviderError
|
|
4154
|
+
? claudeStopFailureError(error.kind)
|
|
4155
|
+
: 'unknown';
|
|
4156
|
+
await this.runAdvisoryHook(sessionId, 'StopFailure', {
|
|
4157
|
+
error: failureKind,
|
|
4158
|
+
error_details: error instanceof Error ? error.message : String(error),
|
|
4159
|
+
}, failureKind, signal);
|
|
4160
|
+
}
|
|
4161
|
+
throw error;
|
|
3946
4162
|
}
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
if (structuredCapture && structuredCapture.calls !== 1) {
|
|
3960
|
-
throw new Error(`StructuredOutput must be called exactly once (received ${structuredCapture.calls})`);
|
|
3961
|
-
}
|
|
3962
|
-
const finalLeafUuid = lastAssistantUuid;
|
|
3963
|
-
if (!finalLeafUuid) {
|
|
3964
|
-
throw new Error('Could not locate final assistant response');
|
|
3965
|
-
}
|
|
3966
|
-
if (!skipUserPrompt) {
|
|
3967
|
-
const lastPrompt = createClaudeLastPromptEntry({
|
|
3968
|
-
sessionId,
|
|
3969
|
-
lastPrompt: effectivePrompt,
|
|
3970
|
-
leafUuid: finalLeafUuid,
|
|
3971
|
-
});
|
|
3972
|
-
const tail = await this.append(lease, snapshot.tail, lastPrompt);
|
|
3973
|
-
snapshot = {
|
|
3974
|
-
entries: [...snapshot.entries, lastPrompt],
|
|
3975
|
-
tail,
|
|
3976
|
-
};
|
|
3977
|
-
}
|
|
3978
|
-
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
3979
|
-
const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
|
|
3980
|
-
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
3981
|
-
if (!tracker) {
|
|
3982
|
-
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
3983
|
-
}
|
|
3984
|
-
const turnModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, compactionModelUsage, shellModelUsage, result.modelUsage);
|
|
3985
|
-
const combinedDurationMs = compactionDurationMs === undefined &&
|
|
3986
|
-
result.durationApiMs === undefined
|
|
3987
|
-
? undefined
|
|
3988
|
-
: (compactionDurationMs ?? 0) + (result.durationApiMs ?? 0);
|
|
3989
|
-
let rawCostUsd;
|
|
3990
|
-
if (turnModelUsage) {
|
|
3991
|
-
for (const [model, usage] of Object.entries(turnModelUsage)) {
|
|
3992
|
-
const pricing = this.options.pricing?.resolve(model);
|
|
3993
|
-
const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
3994
|
-
if (costUsd !== undefined)
|
|
3995
|
-
rawCostUsd = (rawCostUsd ?? 0) + costUsd;
|
|
4163
|
+
recoveryPlanner.recordSuccess();
|
|
4164
|
+
const mainModel = provider.model !== undefined && provider.model.trim() !== ''
|
|
4165
|
+
? provider.model
|
|
4166
|
+
: undefined;
|
|
4167
|
+
const observedUsage = (mainModel !== undefined
|
|
4168
|
+
? result.modelUsage?.[mainModel]
|
|
4169
|
+
: undefined) ??
|
|
4170
|
+
Object.values(result.modelUsage ?? {})[0] ??
|
|
4171
|
+
result.usage;
|
|
4172
|
+
budget?.observeUsage(observedUsage, runtimeRequest.messages, definitions);
|
|
4173
|
+
if (structuredCapture && structuredCapture.calls !== 1) {
|
|
4174
|
+
throw new Error(`StructuredOutput must be called exactly once (received ${structuredCapture.calls})`);
|
|
3996
4175
|
}
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
const pricing = this.options.pricing?.resolve(model);
|
|
4007
|
-
const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
4008
|
-
tracker.recordTurn({
|
|
4009
|
-
model,
|
|
4010
|
-
usage,
|
|
4011
|
-
...(costUsd === undefined ? {} : { costUsd }),
|
|
4012
|
-
...(usage.webSearchRequests === undefined
|
|
4013
|
-
? {}
|
|
4014
|
-
: { webSearchRequests: usage.webSearchRequests }),
|
|
4176
|
+
const finalLeafUuid = lastAssistantUuid;
|
|
4177
|
+
if (!finalLeafUuid) {
|
|
4178
|
+
throw new Error('Could not locate final assistant response');
|
|
4179
|
+
}
|
|
4180
|
+
if (!skipUserPrompt) {
|
|
4181
|
+
const lastPrompt = createClaudeLastPromptEntry({
|
|
4182
|
+
sessionId,
|
|
4183
|
+
lastPrompt: effectivePrompt,
|
|
4184
|
+
leafUuid: finalLeafUuid,
|
|
4015
4185
|
});
|
|
4186
|
+
const tail = await this.append(lease, snapshot.tail, lastPrompt);
|
|
4187
|
+
snapshot = {
|
|
4188
|
+
entries: [...snapshot.entries, lastPrompt],
|
|
4189
|
+
tail,
|
|
4190
|
+
};
|
|
4016
4191
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4192
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
4193
|
+
const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
|
|
4194
|
+
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
4195
|
+
if (!tracker) {
|
|
4196
|
+
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
4197
|
+
}
|
|
4198
|
+
const turnModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, compactionModelUsage, shellModelUsage, result.modelUsage);
|
|
4199
|
+
const combinedDurationMs = compactionDurationMs === undefined &&
|
|
4200
|
+
result.durationApiMs === undefined
|
|
4201
|
+
? undefined
|
|
4202
|
+
: (compactionDurationMs ?? 0) + (result.durationApiMs ?? 0);
|
|
4203
|
+
let rawCostUsd;
|
|
4204
|
+
if (turnModelUsage) {
|
|
4205
|
+
for (const [model, usage] of Object.entries(turnModelUsage)) {
|
|
4206
|
+
const pricing = this.options.pricing?.resolve(model);
|
|
4207
|
+
const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
4208
|
+
if (costUsd !== undefined)
|
|
4209
|
+
rawCostUsd = (rawCostUsd ?? 0) + costUsd;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
// Auto-compaction metering was already recorded atomically with each
|
|
4213
|
+
// committed boundary, and externally metered tool-summary metrics were
|
|
4214
|
+
// committed through the summary callback, so the live tracker receives
|
|
4215
|
+
// only the unrecorded subset here. The inclusive public aggregates
|
|
4216
|
+
// above still contain every row and duration.
|
|
4217
|
+
const trackedModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, shellModelUsage, result.unrecordedModelUsage ?? result.modelUsage);
|
|
4218
|
+
if (trackedModelUsage) {
|
|
4219
|
+
for (const [model, usage] of Object.entries(trackedModelUsage)) {
|
|
4220
|
+
const pricing = this.options.pricing?.resolve(model);
|
|
4221
|
+
const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
4222
|
+
tracker.recordTurn({
|
|
4223
|
+
model,
|
|
4224
|
+
usage,
|
|
4225
|
+
...(costUsd === undefined ? {} : { costUsd }),
|
|
4226
|
+
...(usage.webSearchRequests === undefined
|
|
4227
|
+
? {}
|
|
4228
|
+
: { webSearchRequests: usage.webSearchRequests }),
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
let combinedToolDurationMs = 0;
|
|
4233
|
+
for (const recoveryResult of recoveryResults) {
|
|
4234
|
+
combinedToolDurationMs = addToolDuration(recoveryResult.durationToolMs, combinedToolDurationMs);
|
|
4235
|
+
}
|
|
4236
|
+
combinedToolDurationMs = addToolDuration(result.durationToolMs, combinedToolDurationMs);
|
|
4237
|
+
const trackedDurationApiMs = result.unrecordedDurationApiMs ?? result.durationApiMs;
|
|
4238
|
+
const trackedDurationApiWithoutRetriesMs = result.unrecordedDurationApiWithoutRetriesMs ??
|
|
4239
|
+
result.durationApiWithoutRetriesMs;
|
|
4240
|
+
tracker.recordDurations({
|
|
4241
|
+
...(trackedDurationApiMs === undefined
|
|
4052
4242
|
? {}
|
|
4053
|
-
: {
|
|
4054
|
-
...(
|
|
4243
|
+
: { apiDurationMs: trackedDurationApiMs }),
|
|
4244
|
+
...(trackedDurationApiWithoutRetriesMs === undefined
|
|
4055
4245
|
? {}
|
|
4056
|
-
: {
|
|
4246
|
+
: {
|
|
4247
|
+
apiDurationWithoutRetriesMs: trackedDurationApiWithoutRetriesMs,
|
|
4248
|
+
}),
|
|
4249
|
+
...(combinedToolDurationMs === 0
|
|
4250
|
+
? {}
|
|
4251
|
+
: { toolDurationMs: combinedToolDurationMs }),
|
|
4057
4252
|
});
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
...
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
type: 'warning',
|
|
4077
|
-
message: `Session memory observation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
4253
|
+
foregroundLineChanges.add({
|
|
4254
|
+
isError: false,
|
|
4255
|
+
...(result.linesAdded === undefined
|
|
4256
|
+
? {}
|
|
4257
|
+
: { linesAdded: result.linesAdded }),
|
|
4258
|
+
...(result.linesRemoved === undefined
|
|
4259
|
+
? {}
|
|
4260
|
+
: { linesRemoved: result.linesRemoved }),
|
|
4261
|
+
});
|
|
4262
|
+
if (foregroundLineChanges.linesAdded !== 0 ||
|
|
4263
|
+
foregroundLineChanges.linesRemoved !== 0) {
|
|
4264
|
+
tracker.recordLineChanges({
|
|
4265
|
+
...(foregroundLineChanges.linesAdded === 0
|
|
4266
|
+
? {}
|
|
4267
|
+
: { linesAdded: foregroundLineChanges.linesAdded }),
|
|
4268
|
+
...(foregroundLineChanges.linesRemoved === 0
|
|
4269
|
+
? {}
|
|
4270
|
+
: { linesRemoved: foregroundLineChanges.linesRemoved }),
|
|
4078
4271
|
});
|
|
4079
4272
|
}
|
|
4273
|
+
if (sessionMemory && finalLeafUuid) {
|
|
4274
|
+
const memorySnapshot = projectClaudeModelMessages(snapshot.entries);
|
|
4275
|
+
const providerVisibleMessages = [
|
|
4276
|
+
...contextMessages,
|
|
4277
|
+
...injectTurnContext(memorySnapshot),
|
|
4278
|
+
];
|
|
4279
|
+
const currentContextTokens = budget
|
|
4280
|
+
? budget.evaluate(providerVisibleMessages, definitions)
|
|
4281
|
+
.occupancyTokens
|
|
4282
|
+
: estimateModelRequestTokens(providerVisibleMessages, definitions);
|
|
4283
|
+
try {
|
|
4284
|
+
await sessionMemory.observeContext(currentContextTokens, currentTurnToolCalls, finalLeafUuid, memorySnapshot);
|
|
4285
|
+
}
|
|
4286
|
+
catch (error) {
|
|
4287
|
+
// A failed observation must not fail the user turn; the sidecar
|
|
4288
|
+
// retains a retryable error for the next observation.
|
|
4289
|
+
this.options.eventSink?.({
|
|
4290
|
+
type: 'warning',
|
|
4291
|
+
message: `Session memory observation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
this.options.projectMemoryExtraction?.observe({
|
|
4296
|
+
sessionId,
|
|
4297
|
+
directMaintenance: projectMemoryMaintained,
|
|
4298
|
+
messages: projectMemoryMessages(snapshot.entries),
|
|
4299
|
+
});
|
|
4300
|
+
turnCompleted = true;
|
|
4301
|
+
return {
|
|
4302
|
+
sessionId,
|
|
4303
|
+
text: structuredCapture && structuredCapture.calls === 1
|
|
4304
|
+
? JSON.stringify(structuredCapture.value)
|
|
4305
|
+
: result.text,
|
|
4306
|
+
usage: totalUsage,
|
|
4307
|
+
...(combinedDurationMs === undefined
|
|
4308
|
+
? {}
|
|
4309
|
+
: { durationApiMs: combinedDurationMs }),
|
|
4310
|
+
...(rawCostUsd === undefined ? {} : { costUsd: rawCostUsd }),
|
|
4311
|
+
...(turnModelUsage ? { modelUsage: { ...turnModelUsage } } : {}),
|
|
4312
|
+
...(structuredCapture && structuredCapture.calls === 1
|
|
4313
|
+
? { structuredOutput: structuredCapture.value }
|
|
4314
|
+
: {}),
|
|
4315
|
+
};
|
|
4080
4316
|
}
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
});
|
|
4086
|
-
turnCompleted = true;
|
|
4087
|
-
return {
|
|
4088
|
-
sessionId,
|
|
4089
|
-
text: structuredCapture && structuredCapture.calls === 1
|
|
4090
|
-
? JSON.stringify(structuredCapture.value)
|
|
4091
|
-
: result.text,
|
|
4092
|
-
usage: totalUsage,
|
|
4093
|
-
...(combinedDurationMs === undefined
|
|
4094
|
-
? {}
|
|
4095
|
-
: { durationApiMs: combinedDurationMs }),
|
|
4096
|
-
...(rawCostUsd === undefined ? {} : { costUsd: rawCostUsd }),
|
|
4097
|
-
...(turnModelUsage ? { modelUsage: { ...turnModelUsage } } : {}),
|
|
4098
|
-
...(structuredCapture && structuredCapture.calls === 1
|
|
4099
|
-
? { structuredOutput: structuredCapture.value }
|
|
4100
|
-
: {}),
|
|
4101
|
-
};
|
|
4102
|
-
}
|
|
4103
|
-
finally {
|
|
4104
|
-
if (!turnCompleted) {
|
|
4105
|
-
await this.hookLifecycle.end(sessionId, 'other');
|
|
4317
|
+
finally {
|
|
4318
|
+
if (!turnCompleted) {
|
|
4319
|
+
await this.hookLifecycle.end(sessionId, 'other');
|
|
4320
|
+
}
|
|
4106
4321
|
}
|
|
4322
|
+
});
|
|
4323
|
+
if (leaseResult.status === 'conflict') {
|
|
4324
|
+
throw new Error(`Claude transcript append conflict: ${leaseResult.reason}`);
|
|
4107
4325
|
}
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4326
|
+
const result = leaseResult.value;
|
|
4327
|
+
controller.complete();
|
|
4328
|
+
return result;
|
|
4329
|
+
}
|
|
4330
|
+
catch (error) {
|
|
4331
|
+
controller.fail(error, signal);
|
|
4332
|
+
throw error;
|
|
4111
4333
|
}
|
|
4112
|
-
return leaseResult.value;
|
|
4113
4334
|
}
|
|
4114
4335
|
async ensureFileResources(sessionId, signal) {
|
|
4115
4336
|
const resources = this.options.fileResources ?? [];
|
|
@@ -4622,7 +4843,7 @@ export class ClaudeSessionService {
|
|
|
4622
4843
|
}
|
|
4623
4844
|
}
|
|
4624
4845
|
async appendBackgroundNotification(sessionId, content) {
|
|
4625
|
-
while (
|
|
4846
|
+
while (!this.closing) {
|
|
4626
4847
|
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
4627
4848
|
const snapshot = await lease.load();
|
|
4628
4849
|
if (snapshot.entries.length === 0) {
|
|
@@ -4668,16 +4889,23 @@ export class ClaudeSessionService {
|
|
|
4668
4889
|
}
|
|
4669
4890
|
});
|
|
4670
4891
|
if (result.status === 'completed')
|
|
4671
|
-
return;
|
|
4892
|
+
return true;
|
|
4672
4893
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
4673
4894
|
}
|
|
4895
|
+
return false;
|
|
4674
4896
|
}
|
|
4675
|
-
enqueueBackgroundNotifications(sessionId, messages) {
|
|
4897
|
+
enqueueBackgroundNotifications(sessionId, messages, onAppended) {
|
|
4676
4898
|
const previous = this.backgroundNotificationWrites.get(sessionId);
|
|
4677
|
-
const queued = (previous ?? Promise.resolve()).then(async () => {
|
|
4899
|
+
const queued = (previous ?? Promise.resolve(true)).then(async (previousCompleted) => {
|
|
4900
|
+
if (!previousCompleted || this.closing)
|
|
4901
|
+
return false;
|
|
4678
4902
|
for (const message of messages) {
|
|
4679
|
-
await this.appendBackgroundNotification(sessionId, message)
|
|
4903
|
+
if (!(await this.appendBackgroundNotification(sessionId, message))) {
|
|
4904
|
+
return false;
|
|
4905
|
+
}
|
|
4906
|
+
await onAppended?.(message);
|
|
4680
4907
|
}
|
|
4908
|
+
return true;
|
|
4681
4909
|
});
|
|
4682
4910
|
this.backgroundNotificationWrites.set(sessionId, queued);
|
|
4683
4911
|
const cleanup = () => {
|
|
@@ -4868,13 +5096,6 @@ export class ClaudeSessionService {
|
|
|
4868
5096
|
const system = memory ? `${agent.body}\n\n${memory}` : agent.body;
|
|
4869
5097
|
return system.trim() ? system : null;
|
|
4870
5098
|
}
|
|
4871
|
-
assembledSystemMessages(agent, messages) {
|
|
4872
|
-
return agent &&
|
|
4873
|
-
this.options.explicitSystemPrompt &&
|
|
4874
|
-
this.options.agentSystemPromptOverridesExplicit
|
|
4875
|
-
? messages.slice(1)
|
|
4876
|
-
: messages;
|
|
4877
|
-
}
|
|
4878
5099
|
contextBudget(provider) {
|
|
4879
5100
|
if (this.options.contextBudget)
|
|
4880
5101
|
return this.options.contextBudget;
|