praxis-agent 0.34.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/session-service.d.ts +0 -1
- package/dist/application/session-service.js +1619 -1617
- package/dist/application/subagent-service.js +27 -16
- package/dist/application/turn-lifecycle.d.ts +38 -0
- package/dist/application/turn-lifecycle.js +36 -0
- package/dist/cli/interactive.js +17 -7
- 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/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/shared-resources.d.ts +7 -38
- 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/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/tools/claude-user-message.d.ts +0 -1
- package/dist/tools/claude-user-message.js +0 -1
- package/package.json +6 -2
|
@@ -24,7 +24,8 @@ import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
|
24
24
|
import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
|
|
25
25
|
import { usageCostUsd } from '../core/usage.js';
|
|
26
26
|
import { ContextBudget, ContextRecoveryPlanner, contextRecoveryMadeProgress, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
27
|
-
import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
27
|
+
import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
|
|
28
|
+
import { assembleContextSnapshot } from '../core/prompt-composer.js';
|
|
28
29
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
29
30
|
import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
|
|
30
31
|
import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
|
|
@@ -34,6 +35,7 @@ import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.
|
|
|
34
35
|
import { ModelCompactor } from './model-compactor.js';
|
|
35
36
|
import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
|
|
36
37
|
import { ScheduledPromptManager, } from './scheduled-prompt-manager.js';
|
|
38
|
+
import { TurnTerminalController } from './turn-lifecycle.js';
|
|
37
39
|
import { ClaudeScheduledToolRegistry } from '../tools/claude-scheduled-tools.js';
|
|
38
40
|
import { ClaudeTaskToolRegistry } from '../tools/claude-task-tools.js';
|
|
39
41
|
import { ClaudeWorkflowToolRegistry } from '../tools/claude-workflow-tools.js';
|
|
@@ -46,7 +48,7 @@ import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, }
|
|
|
46
48
|
import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
|
|
47
49
|
import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
|
|
48
50
|
import { generateToolUseSummary } from './tool-use-summary.js';
|
|
49
|
-
import { ClaudeUserMessageToolRegistry,
|
|
51
|
+
import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
|
|
50
52
|
function agentPermissionMode(mode) {
|
|
51
53
|
return mode === undefined || mode === 'manual' ? 'default' : mode;
|
|
52
54
|
}
|
|
@@ -1232,19 +1234,67 @@ export class ClaudeSessionService {
|
|
|
1232
1234
|
}
|
|
1233
1235
|
async run(prompt, signal, sessionId = randomUUID(), name, images, documents) {
|
|
1234
1236
|
this.worktreeManager?.bindSession(sessionId);
|
|
1235
|
-
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
|
+
});
|
|
1236
1251
|
}
|
|
1237
1252
|
async runShell(command, signal, sessionId = randomUUID(), name) {
|
|
1238
1253
|
this.worktreeManager?.bindSession(sessionId);
|
|
1239
|
-
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
|
+
});
|
|
1240
1263
|
}
|
|
1241
1264
|
async resume(sessionId, prompt, signal, name, images, documents, resumeSessionAt) {
|
|
1242
1265
|
this.worktreeManager?.bindSession(sessionId);
|
|
1243
|
-
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
|
+
});
|
|
1244
1283
|
}
|
|
1245
1284
|
async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
|
|
1246
1285
|
this.worktreeManager?.bindSession(sessionId);
|
|
1247
|
-
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
|
+
});
|
|
1248
1298
|
}
|
|
1249
1299
|
async answerSideQuestion(sessionId, question, signal, onDelta, permissionMode = 'default') {
|
|
1250
1300
|
const prompt = question.trim();
|
|
@@ -1270,16 +1320,17 @@ export class ClaudeSessionService {
|
|
|
1270
1320
|
entries = loaded.value.entries;
|
|
1271
1321
|
this.restoreWorktree(entries);
|
|
1272
1322
|
}
|
|
1273
|
-
const assembledContext = await this.options.contextAssembler
|
|
1323
|
+
const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
|
|
1274
1324
|
cwd: this.activeCwd(),
|
|
1275
1325
|
lifecycleId: activeSessionId,
|
|
1276
1326
|
});
|
|
1327
|
+
const contextProjection = projectContextSnapshot(assembledContext);
|
|
1277
1328
|
const messages = [
|
|
1278
|
-
...
|
|
1329
|
+
...contextProjection.systemMessages,
|
|
1279
1330
|
...injectFirstUserMessageContext([
|
|
1280
1331
|
...projectClaudeModelMessages(entries),
|
|
1281
1332
|
{ role: 'user', content: prompt },
|
|
1282
|
-
],
|
|
1333
|
+
], contextProjection.firstUserMessageContext),
|
|
1283
1334
|
];
|
|
1284
1335
|
const budget = this.contextBudget(provider);
|
|
1285
1336
|
if (budget)
|
|
@@ -1290,6 +1341,7 @@ export class ClaudeSessionService {
|
|
|
1290
1341
|
: 'praxis/provider';
|
|
1291
1342
|
const metrics = await completeMeteredModelRequest(provider, {
|
|
1292
1343
|
messages,
|
|
1344
|
+
stableSystemMessageCount: contextProjection.stableSystemSectionCount,
|
|
1293
1345
|
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
1294
1346
|
...(signal ? { signal } : {}),
|
|
1295
1347
|
}, {
|
|
@@ -1430,27 +1482,21 @@ export class ClaudeSessionService {
|
|
|
1430
1482
|
const provider = this.providerForAgent(agent);
|
|
1431
1483
|
this.activeProvider = provider;
|
|
1432
1484
|
const agentSystem = await this.mainAgentSystemPrompt(agent);
|
|
1433
|
-
const assembledContext = await this.options.contextAssembler
|
|
1485
|
+
const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
|
|
1434
1486
|
cwd: this.activeCwd(),
|
|
1435
1487
|
lifecycleId: sessionId,
|
|
1436
|
-
...(agentSystem
|
|
1488
|
+
...(agentSystem
|
|
1489
|
+
? { mode: 'agent', baseSystemPrompt: agentSystem }
|
|
1490
|
+
: {}),
|
|
1437
1491
|
});
|
|
1438
|
-
const
|
|
1439
|
-
const
|
|
1440
|
-
? (assembledContext?.systemMessages ?? [])
|
|
1441
|
-
: this.assembledSystemMessages(agent, assembledContext?.systemMessages ?? []);
|
|
1442
|
-
const contextMessages = [
|
|
1443
|
-
...(!hasPromptManifest && agentSystem
|
|
1444
|
-
? [{ role: 'system', content: agentSystem }]
|
|
1445
|
-
: []),
|
|
1446
|
-
...assembledSystemMessages,
|
|
1447
|
-
];
|
|
1492
|
+
const contextProjection = projectContextSnapshot(assembledContext);
|
|
1493
|
+
const contextMessages = [...contextProjection.systemMessages];
|
|
1448
1494
|
const messages = [
|
|
1449
1495
|
...contextMessages,
|
|
1450
1496
|
...injectFirstUserMessageContext([
|
|
1451
1497
|
...projectClaudeModelMessages(entries.entries),
|
|
1452
1498
|
{ role: 'user', content: PROMPT_SUGGESTION_INSTRUCTION },
|
|
1453
|
-
],
|
|
1499
|
+
], contextProjection.firstUserMessageContext),
|
|
1454
1500
|
];
|
|
1455
1501
|
const suggestionTools = agent && this.options.tools
|
|
1456
1502
|
? new FilteredToolRegistry(this.options.tools, {
|
|
@@ -1460,6 +1506,7 @@ export class ClaudeSessionService {
|
|
|
1460
1506
|
await this.activateSessionCostTracker(sessionId);
|
|
1461
1507
|
const metrics = await completeMeteredModelRequest(provider, {
|
|
1462
1508
|
messages,
|
|
1509
|
+
stableSystemMessageCount: contextProjection.stableSystemSectionCount,
|
|
1463
1510
|
...(provider.capabilities.tools
|
|
1464
1511
|
? { tools: suggestionTools?.definitions() ?? [] }
|
|
1465
1512
|
: {}),
|
|
@@ -1987,7 +2034,11 @@ export class ClaudeSessionService {
|
|
|
1987
2034
|
}
|
|
1988
2035
|
async retryRecentlyDenied(sessionId, display, signal) {
|
|
1989
2036
|
await this.appendPermissionGrant(sessionId, display, true);
|
|
1990
|
-
return this.executeTurn(
|
|
2037
|
+
return this.executeTurn({
|
|
2038
|
+
activation: { kind: 'resume', sessionId },
|
|
2039
|
+
submission: { kind: 'retry', prompt: '/permissions' },
|
|
2040
|
+
...(signal ? { signal } : {}),
|
|
2041
|
+
});
|
|
1991
2042
|
}
|
|
1992
2043
|
async recordBtwUsage(sessionId, permissionMode = 'default') {
|
|
1993
2044
|
const activeSessionId = await this.ensureLocalSession(sessionId, permissionMode);
|
|
@@ -2633,1695 +2684,1653 @@ export class ClaudeSessionService {
|
|
|
2633
2684
|
}
|
|
2634
2685
|
return tracker.snapshot();
|
|
2635
2686
|
}
|
|
2636
|
-
async executeTurn(
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
this.
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
this.options.workspace?.setCwd(pinnedCwd);
|
|
2659
|
-
}
|
|
2660
|
-
this.runtimeCwd = pinnedCwd;
|
|
2661
|
-
if (requireExisting) {
|
|
2662
|
-
await this.discoverProjectRoot(sessionId);
|
|
2663
|
-
}
|
|
2664
|
-
const sessionPaths = this.paths(sessionId);
|
|
2665
|
-
const toolResultDirectory = join(sessionPaths.projectRoot, sessionId, 'tool-results');
|
|
2666
|
-
const store = this.turnStore(sessionId);
|
|
2667
|
-
const leaseResult = await store.withLease(async (lease) => {
|
|
2668
|
-
if (!requireExisting) {
|
|
2669
|
-
const initialization = name === undefined
|
|
2670
|
-
? await store.reserve()
|
|
2671
|
-
: await store.create(this.sessionNameEntries(sessionId, name));
|
|
2672
|
-
if (initialization.status === 'conflict') {
|
|
2673
|
-
throw new Error(`Session ID ${sessionId} is already in use`);
|
|
2674
|
-
}
|
|
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');
|
|
2675
2709
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
if (requireExisting &&
|
|
2679
|
-
this.options.sessionPersistence === false &&
|
|
2680
|
-
snapshot.entries.length === 0) {
|
|
2681
|
-
const persisted = await this.store(sessionId).load();
|
|
2682
|
-
if (persisted.entries.length > 0) {
|
|
2683
|
-
const imported = await store.create(persisted.entries);
|
|
2684
|
-
if (imported.status === 'created')
|
|
2685
|
-
snapshot = await lease.load();
|
|
2686
|
-
}
|
|
2710
|
+
if (shellCommand !== undefined && shellCommand.trim().length === 0) {
|
|
2711
|
+
throw new Error('Shell command must not be empty');
|
|
2687
2712
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2713
|
+
await this.activateSessionCostTracker(sessionId);
|
|
2714
|
+
if (this.options.sessionPersistence !== false) {
|
|
2715
|
+
this.durableMetadataSessions.add(sessionId);
|
|
2690
2716
|
}
|
|
2691
|
-
this.
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
|
|
2696
|
-
};
|
|
2717
|
+
await this.ensureFileResources(sessionId, signal);
|
|
2718
|
+
this.worktreeManager?.bindSession(sessionId);
|
|
2719
|
+
if (this.options.initialWorktree) {
|
|
2720
|
+
await this.worktreeManager?.ensureInitial(this.options.initialWorktreeName);
|
|
2697
2721
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
entries: selected.entries,
|
|
2703
|
-
tail: { ...snapshot.tail, branchParentUuid: selected.leafUuid },
|
|
2704
|
-
};
|
|
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);
|
|
2705
2726
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
this.
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
if (
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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}`);
|
|
2757
|
+
}
|
|
2758
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2759
|
+
if (resumeSessionAt !== undefined) {
|
|
2719
2760
|
snapshot = {
|
|
2720
|
-
entries:
|
|
2721
|
-
tail: {
|
|
2722
|
-
...snapshot.tail,
|
|
2723
|
-
branchParentUuid: interruption.replayParentUuid ?? null,
|
|
2724
|
-
},
|
|
2761
|
+
entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
|
|
2762
|
+
tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
|
|
2725
2763
|
};
|
|
2726
2764
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
};
|
|
2735
|
-
const stateTail = await this.append(lease, snapshot.tail, stateEntry);
|
|
2736
|
-
snapshot = {
|
|
2737
|
-
entries: [...snapshot.entries, stateEntry],
|
|
2738
|
-
tail: stateTail,
|
|
2739
|
-
};
|
|
2740
|
-
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2741
|
-
}
|
|
2742
|
-
this.restoreWorktree(snapshot.entries);
|
|
2743
|
-
this.options.interactiveTools?.restore(sessionId, snapshot.entries);
|
|
2744
|
-
if (requireExisting &&
|
|
2745
|
-
name !== undefined &&
|
|
2746
|
-
!this.hasSessionName(snapshot.entries, name)) {
|
|
2747
|
-
const entries = this.sessionNameEntries(sessionId, name);
|
|
2748
|
-
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
2749
|
-
if (appendResult.status === 'conflict') {
|
|
2750
|
-
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
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
|
+
};
|
|
2751
2772
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
};
|
|
2756
|
-
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2757
|
-
}
|
|
2758
|
-
const agentName = this.options.agent ?? getClaudeAgentSetting(snapshot.entries);
|
|
2759
|
-
const agent = this.resolveAgent(agentName);
|
|
2760
|
-
const provider = this.providerForAgent(agent);
|
|
2761
|
-
this.activeProvider = provider;
|
|
2762
|
-
const effectivePrompt = automaticReplayPrompt ??
|
|
2763
|
-
(!requireExisting &&
|
|
2764
|
-
!skipUserPrompt &&
|
|
2765
|
-
!this.options.agentInitialPromptHandledExternally &&
|
|
2773
|
+
if (requireExisting &&
|
|
2774
|
+
resumeSessionAt === undefined &&
|
|
2775
|
+
this.options.resumeInterruptedTurn === true &&
|
|
2766
2776
|
shellCommand === undefined &&
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
let lastAssistantUuid = null;
|
|
2785
|
-
const fileHistory = this.options.fileCheckpointing &&
|
|
2786
|
-
this.options.sessionPersistence !== false
|
|
2787
|
-
? new ClaudeFileHistory(this.options.configRoot, sessionId, [
|
|
2788
|
-
this.activeCwd(),
|
|
2789
|
-
...(this.options.fileRewindRoots ?? []),
|
|
2790
|
-
])
|
|
2791
|
-
: null;
|
|
2792
|
-
const unresolvedToolCalls = findUnresolvedClaudeToolCalls(snapshot.entries);
|
|
2793
|
-
const pendingRecoveryToolCallIds = new Set(unresolvedToolCalls.map((call) => call.id));
|
|
2794
|
-
const pendingRecoveryHookOutcomes = [];
|
|
2795
|
-
const currentHookCwd = () => this.activeCwd();
|
|
2796
|
-
const hookSession = {
|
|
2797
|
-
session_id: sessionId,
|
|
2798
|
-
transcript_path: sessionPaths.sessionFile,
|
|
2799
|
-
get cwd() {
|
|
2800
|
-
return currentHookCwd();
|
|
2801
|
-
},
|
|
2802
|
-
permission_mode: 'default',
|
|
2803
|
-
};
|
|
2804
|
-
const appendHookOutcome = async (outcome) => {
|
|
2805
|
-
for (const entry of createClaudeHookAttachmentEntries(outcome, this.translationContext(sessionId, snapshot))) {
|
|
2806
|
-
const tail = await this.append(lease, snapshot.tail, entry);
|
|
2807
|
-
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
2808
|
-
}
|
|
2809
|
-
};
|
|
2810
|
-
const recordHookOutcome = async (outcome, deferUntilApproval = false) => {
|
|
2811
|
-
if (deferUntilApproval) {
|
|
2812
|
-
pendingRecoveryHookOutcomes.push(outcome);
|
|
2813
|
-
return;
|
|
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
|
+
}
|
|
2814
2794
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
history = [...history, ...outcomeEntries];
|
|
2829
|
-
const lastEntry = outcomeEntries.at(-1);
|
|
2830
|
-
if (typeof lastEntry?.uuid === 'string')
|
|
2831
|
-
parentUuid = lastEntry.uuid;
|
|
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);
|
|
2832
2808
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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);
|
|
2836
2824
|
}
|
|
2837
|
-
const
|
|
2838
|
-
|
|
2839
|
-
|
|
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'}`);
|
|
2840
2848
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
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
|
+
},
|
|
2891
2959
|
},
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
this.options.
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
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);
|
|
3051
|
+
}
|
|
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
|
|
2928
3183
|
? {
|
|
2929
|
-
|
|
3184
|
+
generateToolUseSummary: ({ tools, lastAssistantText, signal: summarySignal, }) => generateToolUseSummary(provider, tools, summarySignal, lastAssistantText, (metrics) => this.recordAuxiliaryMetrics(sessionId, metrics)),
|
|
2930
3185
|
}
|
|
2931
3186
|
: {}),
|
|
2932
|
-
|
|
2933
|
-
this.options.permissionMode),
|
|
2934
|
-
...(this.options.subagentToolNames
|
|
3187
|
+
...(this.options.pricing
|
|
2935
3188
|
? {
|
|
2936
|
-
|
|
3189
|
+
costUsd: (usage) => {
|
|
3190
|
+
const pricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
|
|
3191
|
+
return pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
3192
|
+
},
|
|
2937
3193
|
}
|
|
2938
3194
|
: {}),
|
|
2939
|
-
...(this.options.
|
|
2940
|
-
? { extensions: this.options.extensions }
|
|
2941
|
-
: {}),
|
|
2942
|
-
...(this.options.mcp ? { mcp: this.options.mcp } : {}),
|
|
2943
|
-
...(this.options.hooks ? { hooks: this.options.hooks } : {}),
|
|
2944
|
-
...(this.options.contextAssembler
|
|
2945
|
-
? { contextAssembler: this.options.contextAssembler }
|
|
2946
|
-
: {}),
|
|
2947
|
-
...((this.options.contextBudget?.reserveTokens ??
|
|
2948
|
-
this.options.contextReserveTokens) === undefined
|
|
3195
|
+
...(this.options.maxBudgetUsd === undefined
|
|
2949
3196
|
? {}
|
|
3197
|
+
: { maxBudgetUsd: this.options.maxBudgetUsd }),
|
|
3198
|
+
...(hookTools
|
|
3199
|
+
? { tools: hookTools, permissions: hookTools }
|
|
2950
3200
|
: {
|
|
2951
|
-
|
|
2952
|
-
|
|
3201
|
+
...(structuredTools ? { tools: structuredTools } : {}),
|
|
3202
|
+
...(turnPermissions ? { permissions: turnPermissions } : {}),
|
|
2953
3203
|
}),
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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;
|
|
2965
3229
|
},
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
}
|
|
2985
|
-
const agentTools = subagentExecutor
|
|
2986
|
-
? subagentExecutor.registry(sessionId, 0, (callId) => currentPromptId ??
|
|
2987
|
-
this.promptIdForToolCall(snapshot.entries, callId))
|
|
2988
|
-
: baseTools;
|
|
2989
|
-
const turnTools = this.workflowManager && subagentExecutor && agentTools
|
|
2990
|
-
? new ClaudeWorkflowToolRegistry({
|
|
2991
|
-
base: agentTools,
|
|
2992
|
-
manager: this.workflowManager,
|
|
2993
|
-
executor: subagentExecutor,
|
|
2994
|
-
cwd: this.activeCwd(),
|
|
2995
|
-
cwdProvider: () => this.activeCwd(),
|
|
2996
|
-
configRoot: this.options.configRoot,
|
|
2997
|
-
sessionId,
|
|
2998
|
-
promptIdForCall: (callId) => currentPromptId ??
|
|
2999
|
-
this.promptIdForToolCall(snapshot.entries, callId),
|
|
3000
|
-
defaultModel: provider.model ?? 'praxis/provider',
|
|
3001
|
-
tokenBudget: workflowTokenTarget(effectivePrompt),
|
|
3002
|
-
enabled: capabilities.has('Workflow'),
|
|
3003
|
-
dataPlane: this.options.dataPlane ?? 'claude',
|
|
3004
|
-
})
|
|
3005
|
-
: agentTools;
|
|
3006
|
-
const workspaceTools = this.worktreeManager &&
|
|
3007
|
-
turnTools &&
|
|
3008
|
-
this.options.workspace &&
|
|
3009
|
-
(this.options.worktreeToolNames?.length ?? 0) > 0
|
|
3010
|
-
? new ClaudeWorktreeToolRegistry({
|
|
3011
|
-
base: turnTools,
|
|
3012
|
-
manager: this.worktreeManager,
|
|
3013
|
-
workspace: this.options.workspace,
|
|
3014
|
-
enabledTools: this.options.worktreeToolNames ?? [],
|
|
3015
|
-
dataPlane: this.options.dataPlane ?? 'claude',
|
|
3016
|
-
})
|
|
3017
|
-
: turnTools;
|
|
3018
|
-
const messageTools = this.options.brief && workspaceTools
|
|
3019
|
-
? new ClaudeUserMessageToolRegistry(workspaceTools, (message) => this.options.eventSink?.({
|
|
3020
|
-
type: 'user-message',
|
|
3021
|
-
message: message.message,
|
|
3022
|
-
status: message.status,
|
|
3023
|
-
...(message.attachments.length
|
|
3024
|
-
? { attachments: message.attachments }
|
|
3025
|
-
: {}),
|
|
3026
|
-
}))
|
|
3027
|
-
: workspaceTools;
|
|
3028
|
-
const interactiveMessageTools = this.options.interactiveTools && messageTools
|
|
3029
|
-
? this.options.interactiveTools.registry(messageTools, sessionId)
|
|
3030
|
-
: messageTools;
|
|
3031
|
-
const fileHistoryTools = fileHistory && interactiveMessageTools
|
|
3032
|
-
? {
|
|
3033
|
-
definitions: () => interactiveMessageTools.definitions(),
|
|
3034
|
-
schedulingPolicy: (call) => ({
|
|
3035
|
-
...resolveToolSchedulingPolicy(interactiveMessageTools, call),
|
|
3036
|
-
startAfterAssistant: true,
|
|
3037
|
-
}),
|
|
3038
|
-
prepare: (call, context) => interactiveMessageTools.prepare(call, context),
|
|
3039
|
-
execute: async (call, context) => {
|
|
3040
|
-
const path = call.name === 'Write' || call.name === 'Edit'
|
|
3041
|
-
? call.input.file_path
|
|
3042
|
-
: call.name === 'NotebookEdit'
|
|
3043
|
-
? call.input.notebook_path
|
|
3044
|
-
: undefined;
|
|
3045
|
-
if (typeof path !== 'string') {
|
|
3046
|
-
return interactiveMessageTools.execute(call, context);
|
|
3047
|
-
}
|
|
3048
|
-
if ((call.name === 'Write' || call.name === 'Edit') &&
|
|
3049
|
-
(await this.options.interactiveTools?.isPlanFile(sessionId, path))) {
|
|
3050
|
-
return interactiveMessageTools.execute(call, context);
|
|
3051
|
-
}
|
|
3052
|
-
const snapshotMessageId = currentPromptId ??
|
|
3053
|
-
this.promptIdForToolCall(snapshot.entries, call.id);
|
|
3054
|
-
const assistantMessageId = lastAssistantUuid ??
|
|
3055
|
-
this.assistantIdForToolCall(snapshot.entries, call.id);
|
|
3056
|
-
if (!snapshotMessageId || !assistantMessageId) {
|
|
3057
|
-
throw new Error('Claude file history could not link tool call');
|
|
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
|
+
};
|
|
3058
3248
|
}
|
|
3059
|
-
const
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
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
|
+
};
|
|
3063
3261
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
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
|
+
}
|
|
3300
|
+
}
|
|
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
|
+
}
|
|
3308
|
+
}
|
|
3067
3309
|
}
|
|
3068
|
-
if (
|
|
3069
|
-
|
|
3070
|
-
|
|
3310
|
+
if (toolResult.isError ||
|
|
3311
|
+
call.name !== 'Read' ||
|
|
3312
|
+
!this.options.conditionalRuleResolver ||
|
|
3313
|
+
!toolResult.accessedPaths) {
|
|
3314
|
+
return;
|
|
3071
3315
|
}
|
|
3072
|
-
const
|
|
3073
|
-
|
|
3074
|
-
const
|
|
3075
|
-
|
|
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');
|
|
3340
|
+
}
|
|
3076
3341
|
}
|
|
3077
|
-
return result;
|
|
3078
3342
|
},
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
const structuredTools = this.options.structuredOutputSchema && structuredCapture
|
|
3093
|
-
? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
|
|
3094
|
-
: agentScopedTools;
|
|
3095
|
-
const hookTools = this.options.hooks && structuredTools && turnPermissions
|
|
3096
|
-
? new ClaudeHookToolCoordinator({
|
|
3097
|
-
tools: structuredTools,
|
|
3098
|
-
permissions: turnPermissions,
|
|
3099
|
-
hooks: this.options.hooks,
|
|
3100
|
-
session: hookSession,
|
|
3101
|
-
recordOutcome: recordHookOutcome,
|
|
3102
|
-
...(this.options.eventSink
|
|
3103
|
-
? {
|
|
3104
|
-
warn: (message) => this.options.eventSink?.({ type: 'warning', message }),
|
|
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);
|
|
3350
|
+
snapshot = {
|
|
3351
|
+
entries: [...snapshot.entries, followUpEntry],
|
|
3352
|
+
tail: followUpTail,
|
|
3353
|
+
};
|
|
3354
|
+
currentTurnUserMessages?.push(content);
|
|
3355
|
+
await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => executor.acknowledgeNotifications([content])));
|
|
3105
3356
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
? {
|
|
3114
|
-
generateToolUseSummary: ({ tools, lastAssistantText, signal }) => generateToolUseSummary(provider, tools, signal, lastAssistantText, (metrics) => this.recordAuxiliaryMetrics(sessionId, metrics)),
|
|
3115
|
-
}
|
|
3116
|
-
: {}),
|
|
3117
|
-
...(this.options.pricing
|
|
3118
|
-
? {
|
|
3119
|
-
costUsd: (usage) => {
|
|
3120
|
-
const pricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
|
|
3121
|
-
return pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
3122
|
-
},
|
|
3123
|
-
}
|
|
3124
|
-
: {}),
|
|
3125
|
-
...(this.options.maxBudgetUsd === undefined
|
|
3126
|
-
? {}
|
|
3127
|
-
: { maxBudgetUsd: this.options.maxBudgetUsd }),
|
|
3128
|
-
...(hookTools
|
|
3129
|
-
? { tools: hookTools, permissions: hookTools }
|
|
3130
|
-
: {
|
|
3131
|
-
...(structuredTools ? { tools: structuredTools } : {}),
|
|
3132
|
-
...(turnPermissions ? { permissions: turnPermissions } : {}),
|
|
3133
|
-
}),
|
|
3134
|
-
});
|
|
3135
|
-
let currentTurnUserMessages = null;
|
|
3136
|
-
let currentTurnToolCalls = 0;
|
|
3137
|
-
let projectMemoryMaintained = false;
|
|
3138
|
-
let projectMemoryRecallMessages = [];
|
|
3139
|
-
const observer = {
|
|
3140
|
-
assistantCompleted: async (message) => {
|
|
3141
|
-
const [entry] = translateProviderEvents([
|
|
3142
|
-
{
|
|
3143
|
-
type: 'assistant-message',
|
|
3144
|
-
text: message.content,
|
|
3145
|
-
...(message.thinkingBlocks
|
|
3146
|
-
? { thinkingBlocks: message.thinkingBlocks }
|
|
3147
|
-
: {}),
|
|
3148
|
-
toolCalls: message.toolCalls ?? [],
|
|
3149
|
-
providerMessageId: `msg_${randomUUID().replaceAll('-', '')}`,
|
|
3150
|
-
model: provider.model ?? 'praxis/provider',
|
|
3151
|
-
},
|
|
3152
|
-
], this.translationContext(sessionId, snapshot));
|
|
3153
|
-
if (!entry)
|
|
3154
|
-
throw new Error('Could not translate assistant response');
|
|
3155
|
-
const tail = await this.append(lease, snapshot.tail, entry);
|
|
3156
|
-
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
3157
|
-
lastAssistantUuid =
|
|
3158
|
-
typeof entry.uuid === 'string' ? entry.uuid : lastAssistantUuid;
|
|
3159
|
-
},
|
|
3160
|
-
toolCompleted: async (call, toolResult) => {
|
|
3161
|
-
currentTurnToolCalls += 1;
|
|
3162
|
-
const transition = this.worktreeManager?.consumeTransition(call.id);
|
|
3163
|
-
if (transition) {
|
|
3164
|
-
this.options.contextAssembler?.invalidate?.({
|
|
3165
|
-
lifecycleId: sessionId,
|
|
3166
|
-
reason: 'worktree',
|
|
3167
|
-
});
|
|
3168
|
-
const stateEntry = {
|
|
3169
|
-
type: 'worktree-state',
|
|
3170
|
-
worktreeSession: transition.state,
|
|
3171
|
-
sessionId,
|
|
3172
|
-
};
|
|
3173
|
-
const stateTail = await this.append(lease, snapshot.tail, stateEntry);
|
|
3174
|
-
snapshot = {
|
|
3175
|
-
entries: [...snapshot.entries, stateEntry],
|
|
3176
|
-
tail: stateTail,
|
|
3177
|
-
};
|
|
3178
|
-
}
|
|
3179
|
-
const permissionMode = this.options.interactiveTools?.consumeTransition(call.id);
|
|
3180
|
-
if (permissionMode) {
|
|
3181
|
-
const modeEntry = {
|
|
3182
|
-
type: 'permission-mode',
|
|
3183
|
-
permissionMode,
|
|
3184
|
-
sessionId,
|
|
3185
|
-
};
|
|
3186
|
-
const modeTail = await this.append(lease, snapshot.tail, modeEntry);
|
|
3187
|
-
snapshot = {
|
|
3188
|
-
entries: [...snapshot.entries, modeEntry],
|
|
3189
|
-
tail: modeTail,
|
|
3190
|
-
};
|
|
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);
|
|
3191
3364
|
}
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
: {
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
throw new Error('Could not translate tool result');
|
|
3215
|
-
const tail = await this.append(lease, snapshot.tail, entry);
|
|
3216
|
-
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
3217
|
-
if (!toolResult.isError && this.options.projectMemoryDirectory) {
|
|
3218
|
-
const pathValue = call.input.file_path;
|
|
3219
|
-
const path = typeof pathValue === 'string'
|
|
3220
|
-
? resolve(this.activeCwd(), pathValue)
|
|
3221
|
-
: null;
|
|
3222
|
-
if (path &&
|
|
3223
|
-
isPathWithin(this.options.projectMemoryDirectory, path)) {
|
|
3224
|
-
if (call.name === 'Read') {
|
|
3225
|
-
this.options.projectMemoryRecall?.recordRead(sessionId, path);
|
|
3226
|
-
}
|
|
3227
|
-
else if (call.name === 'Write' || call.name === 'Edit') {
|
|
3228
|
-
projectMemoryMaintained = true;
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
if (call.name === 'Read') {
|
|
3232
|
-
for (const accessedPath of toolResult.accessedPaths ?? []) {
|
|
3233
|
-
const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
|
|
3234
|
-
if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
|
|
3235
|
-
this.options.projectMemoryRecall?.recordRead(sessionId, resolvedAccessedPath);
|
|
3236
|
-
}
|
|
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,
|
|
3237
3387
|
}
|
|
3238
|
-
|
|
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`);
|
|
3239
3393
|
}
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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);
|
|
3245
3400
|
}
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
const attachmentTail = await this.append(lease, snapshot.tail, attachment);
|
|
3252
|
-
snapshot = {
|
|
3253
|
-
entries: [...snapshot.entries, attachment],
|
|
3254
|
-
tail: attachmentTail,
|
|
3255
|
-
};
|
|
3256
|
-
attachedRulePaths.add(rule.path);
|
|
3257
|
-
await this.instructionLoaded(sessionId, {
|
|
3258
|
-
path: rule.path,
|
|
3259
|
-
memoryType: rule.scope === 'user'
|
|
3260
|
-
? 'User'
|
|
3261
|
-
: rule.scope === 'local'
|
|
3262
|
-
? 'Local'
|
|
3263
|
-
: 'Project',
|
|
3264
|
-
globs: rule.globs,
|
|
3265
|
-
triggerFilePath: filePath,
|
|
3266
|
-
...(rule.importedFrom === undefined
|
|
3267
|
-
? {}
|
|
3268
|
-
: { parentFilePath: rule.importedFrom }),
|
|
3269
|
-
}, 'path_glob_match');
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
},
|
|
3273
|
-
followUpUserMessagesCompleted: async (messages) => {
|
|
3274
|
-
for (const content of messages) {
|
|
3275
|
-
const [followUpEntry] = translateProviderEvents([{ type: 'user-text-block', text: content }], this.translationContext(sessionId, snapshot));
|
|
3276
|
-
if (!followUpEntry) {
|
|
3277
|
-
throw new Error('Could not translate tool follow-up message');
|
|
3278
|
-
}
|
|
3279
|
-
const followUpTail = await this.append(lease, snapshot.tail, followUpEntry);
|
|
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);
|
|
3280
3406
|
snapshot = {
|
|
3281
|
-
entries: [...snapshot.entries,
|
|
3282
|
-
tail:
|
|
3407
|
+
entries: [...snapshot.entries, agentSetting],
|
|
3408
|
+
tail: settingTail,
|
|
3283
3409
|
};
|
|
3284
|
-
currentTurnUserMessages?.push(content);
|
|
3285
|
-
await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => executor.acknowledgeNotifications([content])));
|
|
3286
3410
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
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
|
+
: {}),
|
|
3315
3443
|
},
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
if (unresolvedToolCall && !approveRecovery) {
|
|
3322
|
-
throw new Error(`Claude session tool call ${unresolvedToolCall.id} requires explicit recovery approval`);
|
|
3323
|
-
}
|
|
3324
|
-
const recoveryResults = await runtime.recoverToolCalls(unresolvedToolCalls, recoveryRequest);
|
|
3325
|
-
const recoveryUsage = recoveryResults.reduce((usage, result) => result.usage ? mergeUsage(usage, result.usage) : usage, { inputTokens: 0, outputTokens: 0 });
|
|
3326
|
-
const recoveryModelUsage = mergeSessionRawModelUsage(...recoveryResults.map((result) => result.isError ? undefined : result.modelUsage));
|
|
3327
|
-
const foregroundLineChanges = createLineCountAccumulator();
|
|
3328
|
-
for (const result of recoveryResults) {
|
|
3329
|
-
foregroundLineChanges.add(result);
|
|
3330
|
-
}
|
|
3331
|
-
if (this.options.agent &&
|
|
3332
|
-
agent &&
|
|
3333
|
-
getClaudeAgentSetting(snapshot.entries) !== this.options.agent) {
|
|
3334
|
-
const agentSetting = createClaudeAgentSettingEntry(sessionId, this.options.agent);
|
|
3335
|
-
const settingTail = await this.append(lease, snapshot.tail, agentSetting);
|
|
3336
|
-
snapshot = {
|
|
3337
|
-
entries: [...snapshot.entries, agentSetting],
|
|
3338
|
-
tail: settingTail,
|
|
3444
|
+
});
|
|
3445
|
+
contextProjection = projectContextSnapshot(assembledContext);
|
|
3446
|
+
stableSystemMessageCount =
|
|
3447
|
+
contextProjection.stableSystemSectionCount;
|
|
3448
|
+
contextMessages = [...contextProjection.systemMessages];
|
|
3339
3449
|
};
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
placement: 'system',
|
|
3361
|
-
stability: 'volatile',
|
|
3362
|
-
content: planModeMessage,
|
|
3363
|
-
},
|
|
3364
|
-
]
|
|
3365
|
-
: []),
|
|
3366
|
-
...(sessionMemoryMessage
|
|
3367
|
-
? [
|
|
3368
|
-
{
|
|
3369
|
-
id: 'session-memory',
|
|
3370
|
-
placement: 'system',
|
|
3371
|
-
stability: 'volatile',
|
|
3372
|
-
content: sessionMemoryMessage,
|
|
3373
|
-
},
|
|
3374
|
-
]
|
|
3375
|
-
: []),
|
|
3376
|
-
...(this.options.brief
|
|
3377
|
-
? [
|
|
3378
|
-
{
|
|
3379
|
-
id: 'brief-output',
|
|
3380
|
-
placement: 'system',
|
|
3381
|
-
stability: 'volatile',
|
|
3382
|
-
content: CLAUDE_USER_MESSAGE_PROMPT,
|
|
3383
|
-
},
|
|
3384
|
-
]
|
|
3385
|
-
: []),
|
|
3386
|
-
...(this.options.structuredOutputSchema
|
|
3387
|
-
? [
|
|
3388
|
-
{
|
|
3389
|
-
id: 'structured-output',
|
|
3390
|
-
placement: 'system',
|
|
3391
|
-
stability: 'volatile',
|
|
3392
|
-
content: 'You MUST call StructuredOutput exactly once at the end with a value matching the requested JSON Schema.',
|
|
3393
|
-
},
|
|
3394
|
-
]
|
|
3395
|
-
: []),
|
|
3396
|
-
];
|
|
3397
|
-
assembledContext = await this.options.contextAssembler?.assemble({
|
|
3398
|
-
cwd: this.activeCwd(),
|
|
3399
|
-
lifecycleId: sessionId,
|
|
3400
|
-
...(agentSystem
|
|
3401
|
-
? { mode: 'agent', baseSystemPrompt: agentSystem }
|
|
3402
|
-
: {}),
|
|
3403
|
-
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',
|
|
3404
3470
|
});
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
...(
|
|
3421
|
-
?
|
|
3422
|
-
:
|
|
3423
|
-
...(
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
? expandedMessages.length - 1
|
|
3454
|
-
: 0;
|
|
3455
|
-
currentTurnUserMessages = [...expansion.userMessages];
|
|
3456
|
-
this.options.eventSink?.({
|
|
3457
|
-
type: 'state',
|
|
3458
|
-
state: 'assembling-context',
|
|
3459
|
-
});
|
|
3460
|
-
let compactionUsage = {
|
|
3461
|
-
inputTokens: 0,
|
|
3462
|
-
outputTokens: 0,
|
|
3463
|
-
};
|
|
3464
|
-
let compactionDurationMs;
|
|
3465
|
-
let compactionDurationWithoutRetriesMs;
|
|
3466
|
-
let compactionModelUsage;
|
|
3467
|
-
const definitions = provider.capabilities.tools
|
|
3468
|
-
? (structuredTools?.definitions() ?? [])
|
|
3469
|
-
: [];
|
|
3470
|
-
const budget = this.contextBudget(provider);
|
|
3471
|
-
const recoveryPlanner = new ContextRecoveryPlanner();
|
|
3472
|
-
const pendingUserMessages = expandedMessages.map((message, index) => ({
|
|
3473
|
-
role: 'user',
|
|
3474
|
-
content: message.text,
|
|
3475
|
-
...(message.contentBlocks?.length
|
|
3476
|
-
? { contentBlocks: message.contentBlocks }
|
|
3477
|
-
: {}),
|
|
3478
|
-
...((index === attachmentIndex && images.length > 0) ||
|
|
3479
|
-
message.images?.length
|
|
3480
|
-
? {
|
|
3481
|
-
images: [
|
|
3482
|
-
...(index === attachmentIndex ? images : []),
|
|
3483
|
-
...(message.images ?? []),
|
|
3484
|
-
],
|
|
3485
|
-
}
|
|
3486
|
-
: {}),
|
|
3487
|
-
...(index === attachmentIndex && documents.length > 0
|
|
3488
|
-
? { documents }
|
|
3489
|
-
: {}),
|
|
3490
|
-
}));
|
|
3491
|
-
const agentMentionMessages = shellCommand === undefined && !skipUserPrompt
|
|
3492
|
-
? (this.options.extensions?.agentMentionMessages(effectivePrompt) ??
|
|
3493
|
-
[])
|
|
3494
|
-
: [];
|
|
3495
|
-
const injectAgentMentionContext = (messages) => {
|
|
3496
|
-
if (agentMentionMessages.length === 0)
|
|
3497
|
-
return [...messages];
|
|
3498
|
-
let insertionIndex = messages.length;
|
|
3499
|
-
let foundPrompt = false;
|
|
3500
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
3501
|
-
const message = messages[index];
|
|
3502
|
-
if (message?.role === 'user' &&
|
|
3503
|
-
typeof message.content === 'string' &&
|
|
3504
|
-
message.content.endsWith(effectivePrompt)) {
|
|
3505
|
-
insertionIndex = index;
|
|
3506
|
-
foundPrompt = true;
|
|
3507
|
-
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
|
+
}
|
|
3508
3519
|
}
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
return [
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
...agentMentionMessages.map((content) => ({
|
|
3515
|
-
role: 'user',
|
|
3516
|
-
content,
|
|
3517
|
-
})),
|
|
3518
|
-
...messages.slice(insertionIndex),
|
|
3519
|
-
];
|
|
3520
|
-
};
|
|
3521
|
-
const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, assembledContext?.firstUserMessageContext);
|
|
3522
|
-
const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
|
|
3523
|
-
let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
|
|
3524
|
-
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
|
|
3525
|
-
if (!budget || this.options.autoCompact === false)
|
|
3526
|
-
return;
|
|
3527
|
-
const historyMessages = [
|
|
3528
|
-
...projectClaudeModelMessages(snapshot.entries),
|
|
3529
|
-
...projectMemoryRecallMessages,
|
|
3530
|
-
];
|
|
3531
|
-
const predicted = budget.evaluate([
|
|
3532
|
-
...contextMessages,
|
|
3533
|
-
...injectTurnContext([...historyMessages, ...pendingMessages]),
|
|
3534
|
-
], definitions, options);
|
|
3535
|
-
if (!predicted.shouldCompact)
|
|
3536
|
-
return;
|
|
3537
|
-
const irreducibleMessages = [
|
|
3538
|
-
...contextMessages,
|
|
3539
|
-
...injectTurnContext([
|
|
3540
|
-
...pendingMessages,
|
|
3541
|
-
...preservedUserMessages.map((content) => ({
|
|
3520
|
+
if (!foundPrompt)
|
|
3521
|
+
return [...messages];
|
|
3522
|
+
return [
|
|
3523
|
+
...messages.slice(0, insertionIndex),
|
|
3524
|
+
...agentMentionMessages.map((content) => ({
|
|
3542
3525
|
role: 'user',
|
|
3543
3526
|
content,
|
|
3544
3527
|
})),
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
{
|
|
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) => ({
|
|
3578
3578
|
role: 'user',
|
|
3579
|
-
content:
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
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([
|
|
3585
3586
|
...irreducibleMessages,
|
|
3586
3587
|
{
|
|
3587
3588
|
role: 'user',
|
|
3588
|
-
content: formatClaudeCompactSummary('
|
|
3589
|
+
content: formatClaudeCompactSummary(''),
|
|
3589
3590
|
},
|
|
3590
|
-
], definitions)
|
|
3591
|
-
targetTokens =
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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 }), {
|
|
3616
3654
|
sessionId,
|
|
3617
|
-
|
|
3618
|
-
summary: compacted.summary,
|
|
3619
|
-
preTokens,
|
|
3620
|
-
postTokens,
|
|
3621
|
-
previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
|
|
3622
|
-
durationMs: compactedDurationMs,
|
|
3655
|
+
parentUuid: compactSummaryUuid,
|
|
3623
3656
|
cwd: this.activeCwd(),
|
|
3624
3657
|
claudeVersion: this.options.claudeVersion,
|
|
3625
3658
|
gitBranch: null,
|
|
3626
|
-
...
|
|
3627
|
-
|
|
3628
|
-
preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
|
|
3629
|
-
}
|
|
3630
|
-
: {}),
|
|
3631
|
-
createUuid: () => uuids.shift() ?? randomUUID(),
|
|
3659
|
+
history: [...snapshot.entries, ...provisionalEntries],
|
|
3660
|
+
createUuid: () => replayUuids.shift() ?? randomUUID(),
|
|
3632
3661
|
now: () => timestamp,
|
|
3633
3662
|
});
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
...
|
|
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),
|
|
3657
3686
|
...replayEntries,
|
|
3658
|
-
]
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
meteringTurnInput
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
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}`);
|
|
3697
|
+
}
|
|
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');
|
|
3704
|
+
}
|
|
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}`);
|
|
3746
|
+
}
|
|
3747
|
+
snapshot = {
|
|
3748
|
+
entries: [...snapshot.entries, ...entries],
|
|
3749
|
+
tail: appendResult.tail,
|
|
3718
3750
|
};
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
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
|
+
};
|
|
3761
|
+
}
|
|
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',
|
|
3725
3776
|
});
|
|
3777
|
+
this.options.projectMemoryRecall?.recordCompact(sessionId);
|
|
3778
|
+
await refreshRuntimeContext();
|
|
3779
|
+
this.options.eventSink?.({
|
|
3780
|
+
type: 'compact-boundary',
|
|
3781
|
+
trigger: 'auto',
|
|
3782
|
+
preTokens,
|
|
3783
|
+
uuid: boundaryUuid,
|
|
3784
|
+
});
|
|
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 });
|
|
3792
|
+
}
|
|
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.
|
|
3726
3796
|
if (meteringTurnInput !== undefined) {
|
|
3727
|
-
|
|
3797
|
+
tracker.recordTurn(meteringTurnInput);
|
|
3728
3798
|
}
|
|
3729
3799
|
if (meteringDurationsInput !== undefined) {
|
|
3730
|
-
|
|
3800
|
+
tracker.recordDurations(meteringDurationsInput);
|
|
3731
3801
|
}
|
|
3732
|
-
}
|
|
3733
|
-
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
3734
|
-
if (appendResult.status === 'conflict') {
|
|
3735
|
-
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
3736
|
-
}
|
|
3737
|
-
snapshot = {
|
|
3738
|
-
entries: [...snapshot.entries, ...entries],
|
|
3739
|
-
tail: appendResult.tail,
|
|
3740
3802
|
};
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
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 ?? []),
|
|
3810
|
+
];
|
|
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;
|
|
3746
3831
|
}
|
|
3832
|
+
const userTail = await this.append(lease, snapshot.tail, userEntry);
|
|
3747
3833
|
snapshot = {
|
|
3748
|
-
entries: [...snapshot.entries,
|
|
3749
|
-
tail:
|
|
3834
|
+
entries: [...snapshot.entries, userEntry],
|
|
3835
|
+
tail: userTail,
|
|
3750
3836
|
};
|
|
3751
3837
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
|
|
3760
|
-
if (outcome)
|
|
3761
|
-
await recordHookOutcome(outcome);
|
|
3762
|
-
}
|
|
3763
|
-
this.options.contextAssembler?.invalidate?.({
|
|
3764
|
-
lifecycleId: sessionId,
|
|
3765
|
-
reason: 'compact',
|
|
3766
|
-
});
|
|
3767
|
-
this.options.projectMemoryRecall?.recordCompact(sessionId);
|
|
3768
|
-
await refreshRuntimeContext();
|
|
3769
|
-
this.options.eventSink?.({
|
|
3770
|
-
type: 'compact-boundary',
|
|
3771
|
-
trigger: 'auto',
|
|
3772
|
-
preTokens,
|
|
3773
|
-
uuid: boundaryUuid,
|
|
3774
|
-
});
|
|
3775
|
-
compactionAnchorUuid = compactSummaryUuid;
|
|
3776
|
-
compactionUsage = mergeUsage(compactionUsage, compacted.usage);
|
|
3777
|
-
compactionDurationMs = proposedCompactionDurationMs;
|
|
3778
|
-
compactionDurationWithoutRetriesMs =
|
|
3779
|
-
proposedCompactionDurationWithoutRetriesMs;
|
|
3780
|
-
if (meteringTurnInput !== undefined) {
|
|
3781
|
-
compactionModelUsage = mergeSessionRawModelUsage(compactionModelUsage, { [meteringTurnInput.model]: compacted.usage });
|
|
3782
|
-
}
|
|
3783
|
-
// Commit the preflighted mutations once the boundary is durable so a
|
|
3784
|
-
// later main-provider failure or cancellation cannot lose the
|
|
3785
|
-
// compactor's usage/cost/API durations.
|
|
3786
|
-
if (meteringTurnInput !== undefined) {
|
|
3787
|
-
tracker.recordTurn(meteringTurnInput);
|
|
3788
|
-
}
|
|
3789
|
-
if (meteringDurationsInput !== undefined) {
|
|
3790
|
-
tracker.recordDurations(meteringDurationsInput);
|
|
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
|
+
};
|
|
3791
3845
|
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
(index === attachmentIndex && documents.length > 0)
|
|
3803
|
-
? {
|
|
3804
|
-
type: 'user-message',
|
|
3805
|
-
text: message.text,
|
|
3806
|
-
images: messageImages,
|
|
3807
|
-
...(index === attachmentIndex && documents.length > 0
|
|
3808
|
-
? { documents }
|
|
3809
|
-
: {}),
|
|
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}`);
|
|
3810
3856
|
}
|
|
3811
|
-
: index === 0
|
|
3812
|
-
? { type: 'user-text', text: message.text }
|
|
3813
|
-
: { type: 'user-text-block', text: message.text };
|
|
3814
|
-
const [userEntry] = translateProviderEvents([persistenceEvent], this.translationContext(sessionId, snapshot));
|
|
3815
|
-
if (!userEntry)
|
|
3816
|
-
throw new Error('Could not translate user prompt');
|
|
3817
|
-
if (currentPromptId === null && typeof userEntry.uuid === 'string') {
|
|
3818
|
-
currentPromptId = userEntry.uuid;
|
|
3819
|
-
compactionAnchorUuid ??= userEntry.uuid;
|
|
3820
|
-
}
|
|
3821
|
-
const userTail = await this.append(lease, snapshot.tail, userEntry);
|
|
3822
|
-
snapshot = {
|
|
3823
|
-
entries: [...snapshot.entries, userEntry],
|
|
3824
|
-
tail: userTail,
|
|
3825
|
-
};
|
|
3826
|
-
}
|
|
3827
|
-
if (fileHistory && currentPromptId) {
|
|
3828
|
-
const historySnapshot = await fileHistory.snapshot(snapshot.entries, currentPromptId);
|
|
3829
|
-
const historyTail = await this.append(lease, snapshot.tail, historySnapshot);
|
|
3830
|
-
snapshot = {
|
|
3831
|
-
entries: [...snapshot.entries, historySnapshot],
|
|
3832
|
-
tail: historyTail,
|
|
3833
|
-
};
|
|
3834
|
-
}
|
|
3835
|
-
if (this.options.hooks && !skipUserPrompt) {
|
|
3836
|
-
const outcome = await this.options.hooks.run({
|
|
3837
|
-
...hookSession,
|
|
3838
|
-
hook_event_name: 'UserPromptSubmit',
|
|
3839
|
-
prompt_id: currentPromptId ?? randomUUID(),
|
|
3840
|
-
prompt: effectivePrompt,
|
|
3841
|
-
}, undefined, signal);
|
|
3842
|
-
await recordHookOutcome(outcome);
|
|
3843
|
-
if (outcome.blockedReason) {
|
|
3844
|
-
throw new Error(`UserPromptSubmit hook error: ${outcome.blockedReason}`);
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
let shellUsage = { inputTokens: 0, outputTokens: 0 };
|
|
3848
|
-
let shellModelUsage;
|
|
3849
|
-
if (shellCommand !== undefined) {
|
|
3850
|
-
const call = {
|
|
3851
|
-
id: `shell_${randomUUID().replaceAll('-', '')}`,
|
|
3852
|
-
name: 'Bash',
|
|
3853
|
-
input: { command: shellCommand },
|
|
3854
|
-
};
|
|
3855
|
-
this.options.eventSink?.({
|
|
3856
|
-
type: 'shell-command',
|
|
3857
|
-
callId: call.id,
|
|
3858
|
-
command: shellCommand,
|
|
3859
|
-
});
|
|
3860
|
-
let shellResult;
|
|
3861
|
-
try {
|
|
3862
|
-
shellResult = await runtime.executeDirectToolCall(call, {
|
|
3863
|
-
cwd: this.activeCwd(),
|
|
3864
|
-
sessionId,
|
|
3865
|
-
toolResultDirectory,
|
|
3866
|
-
messages: projectClaudeModelMessages(snapshot.entries),
|
|
3867
|
-
observer: {
|
|
3868
|
-
assistantCompleted: async () => undefined,
|
|
3869
|
-
toolCompleted: async () => undefined,
|
|
3870
|
-
followUpUserMessagesCompleted: observer.followUpUserMessagesCompleted,
|
|
3871
|
-
},
|
|
3872
|
-
...(this.options.approveTool
|
|
3873
|
-
? { approveTool: this.options.approveTool }
|
|
3874
|
-
: {}),
|
|
3875
|
-
permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
3876
|
-
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
3877
|
-
...(signal ? { signal } : {}),
|
|
3878
|
-
});
|
|
3879
3857
|
}
|
|
3880
|
-
|
|
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
|
+
};
|
|
3881
3866
|
this.options.eventSink?.({
|
|
3882
|
-
type: 'shell-
|
|
3867
|
+
type: 'shell-command',
|
|
3883
3868
|
callId: call.id,
|
|
3869
|
+
command: shellCommand,
|
|
3884
3870
|
});
|
|
3885
|
-
|
|
3886
|
-
}
|
|
3887
|
-
shellUsage = shellResult.usage ?? shellUsage;
|
|
3888
|
-
if (!shellResult.isError) {
|
|
3889
|
-
shellModelUsage = shellResult.modelUsage;
|
|
3890
|
-
foregroundLineChanges.add(shellResult);
|
|
3891
|
-
}
|
|
3892
|
-
const stdout = shellResult.processOutput?.stdout ??
|
|
3893
|
-
(shellResult.isError ? '' : shellResult.content);
|
|
3894
|
-
const stderr = shellResult.processOutput?.stderr ??
|
|
3895
|
-
(shellResult.isError ? shellResult.content : '');
|
|
3896
|
-
const shellUuids = [shellInputUuid ?? randomUUID(), randomUUID()];
|
|
3897
|
-
const [inputEntry, outputEntry] = translateProviderEvents([
|
|
3898
|
-
{ type: 'bash-input', command: shellCommand },
|
|
3899
|
-
{ type: 'bash-output', stdout, stderr },
|
|
3900
|
-
], {
|
|
3901
|
-
...this.translationContext(sessionId, snapshot),
|
|
3902
|
-
createUuid: () => shellUuids.shift() ?? randomUUID(),
|
|
3903
|
-
});
|
|
3904
|
-
if (!inputEntry || !outputEntry) {
|
|
3905
|
-
throw new Error('Could not translate shell command result');
|
|
3906
|
-
}
|
|
3907
|
-
const shellAppend = await lease.appendMany(snapshot.tail, [
|
|
3908
|
-
inputEntry,
|
|
3909
|
-
outputEntry,
|
|
3910
|
-
]);
|
|
3911
|
-
if (shellAppend.status === 'conflict') {
|
|
3912
|
-
throw new Error(`Claude transcript append conflict: ${shellAppend.reason}`);
|
|
3913
|
-
}
|
|
3914
|
-
snapshot = {
|
|
3915
|
-
entries: [...snapshot.entries, inputEntry, outputEntry],
|
|
3916
|
-
tail: shellAppend.tail,
|
|
3917
|
-
};
|
|
3918
|
-
currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
|
|
3919
|
-
this.options.eventSink?.({
|
|
3920
|
-
type: 'shell-result',
|
|
3921
|
-
callId: call.id,
|
|
3922
|
-
stdout,
|
|
3923
|
-
stderr,
|
|
3924
|
-
isError: shellResult.isError,
|
|
3925
|
-
});
|
|
3926
|
-
}
|
|
3927
|
-
if (budget) {
|
|
3928
|
-
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
3929
|
-
budget.assertFits(budget.evaluate([
|
|
3930
|
-
...contextMessages,
|
|
3931
|
-
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
3932
|
-
], definitions));
|
|
3933
|
-
}
|
|
3934
|
-
let stopHookActive = false;
|
|
3935
|
-
const runtimeRequest = {
|
|
3936
|
-
sessionId,
|
|
3937
|
-
messages: [
|
|
3938
|
-
...contextMessages,
|
|
3939
|
-
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
3940
|
-
],
|
|
3941
|
-
...(stableSystemMessageCount === undefined
|
|
3942
|
-
? {}
|
|
3943
|
-
: { stableSystemMessageCount }),
|
|
3944
|
-
cwd: this.activeCwd(),
|
|
3945
|
-
toolResultDirectory,
|
|
3946
|
-
observer,
|
|
3947
|
-
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
3948
|
-
...(this.options.maxModelTurns !== undefined
|
|
3949
|
-
? { maxModelTurns: this.options.maxModelTurns }
|
|
3950
|
-
: {}),
|
|
3951
|
-
...(this.options.betas?.length ? { betas: this.options.betas } : {}),
|
|
3952
|
-
...(this.options.collectMetrics || this.options.costStateStore
|
|
3953
|
-
? { collectMetrics: true }
|
|
3954
|
-
: {}),
|
|
3955
|
-
...(budget
|
|
3956
|
-
? { deferFailureKinds: ['prompt_too_long'] }
|
|
3957
|
-
: {}),
|
|
3958
|
-
reloadMessages: async () => {
|
|
3959
|
-
const recalled = projectMemoryRecallTurn?.consumeIfSettled();
|
|
3960
|
-
if (recalled) {
|
|
3961
|
-
projectMemoryRecallMessages = [
|
|
3962
|
-
{ role: 'user', content: recalled.content },
|
|
3963
|
-
];
|
|
3964
|
-
}
|
|
3965
|
-
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
3966
|
-
if (stableSystemMessageCount === undefined) {
|
|
3967
|
-
delete runtimeRequest.stableSystemMessageCount;
|
|
3968
|
-
}
|
|
3969
|
-
else {
|
|
3970
|
-
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
3971
|
-
}
|
|
3972
|
-
return [
|
|
3973
|
-
...contextMessages,
|
|
3974
|
-
...injectTurnContext([
|
|
3975
|
-
...projectClaudeModelMessages(snapshot.entries),
|
|
3976
|
-
...projectMemoryRecallMessages,
|
|
3977
|
-
]),
|
|
3978
|
-
];
|
|
3979
|
-
},
|
|
3980
|
-
...(this.options.hooks ||
|
|
3981
|
-
subagentExecutor ||
|
|
3982
|
-
taskTools ||
|
|
3983
|
-
this.scheduledPrompts ||
|
|
3984
|
-
this.workflowManager
|
|
3985
|
-
? {
|
|
3986
|
-
onStop: async (text) => {
|
|
3987
|
-
const messages = [];
|
|
3988
|
-
const outcome = await this.options.hooks?.run({
|
|
3989
|
-
...hookSession,
|
|
3990
|
-
hook_event_name: 'Stop',
|
|
3991
|
-
stop_hook_active: stopHookActive,
|
|
3992
|
-
last_assistant_message: text,
|
|
3993
|
-
}, undefined, signal);
|
|
3994
|
-
if (outcome) {
|
|
3995
|
-
await recordHookOutcome(outcome);
|
|
3996
|
-
if (outcome.blockedReason) {
|
|
3997
|
-
stopHookActive = true;
|
|
3998
|
-
messages.push(`Stop hook error: ${outcome.blockedReason}`);
|
|
3999
|
-
}
|
|
4000
|
-
}
|
|
4001
|
-
const claimedAgentIds = new Set(this.sessionSubagentExecutors(sessionId, true).flatMap((executor) => executor.notificationClaimAgentIds()));
|
|
4002
|
-
await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => {
|
|
4003
|
-
const delivered = (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification);
|
|
4004
|
-
return this.hostedSubagents.has(executor)
|
|
4005
|
-
? executor.reconcileDetachedNotifications(delivered)
|
|
4006
|
-
: executor.reconcileDeliveredNotifications(delivered);
|
|
4007
|
-
}));
|
|
4008
|
-
await subagentExecutor?.hydratePersistedTasks(sessionId, this.activeCwd(), claimedAgentIds);
|
|
4009
|
-
const background = await this.collectSubagentNotifications(sessionId, subagentExecutor ?? undefined);
|
|
4010
|
-
if (background)
|
|
4011
|
-
messages.push(...background.messages);
|
|
4012
|
-
const workflow = await this.workflowManager?.notifications(true);
|
|
4013
|
-
if (workflow)
|
|
4014
|
-
messages.push(...workflow.messages);
|
|
4015
|
-
const bashMessages = await taskTools?.notifications(true);
|
|
4016
|
-
if (bashMessages)
|
|
4017
|
-
messages.push(...bashMessages);
|
|
4018
|
-
const scheduled = await this.scheduledPrompts?.drainDue();
|
|
4019
|
-
if (scheduled) {
|
|
4020
|
-
messages.push(...scheduled.map(({ prompt }) => prompt));
|
|
4021
|
-
}
|
|
4022
|
-
const batchUsage = background || workflow
|
|
4023
|
-
? mergeUsage(background?.usage ?? {
|
|
4024
|
-
inputTokens: 0,
|
|
4025
|
-
outputTokens: 0,
|
|
4026
|
-
}, workflow?.usage ?? {
|
|
4027
|
-
inputTokens: 0,
|
|
4028
|
-
outputTokens: 0,
|
|
4029
|
-
})
|
|
4030
|
-
: undefined;
|
|
4031
|
-
const batchModelUsage = mergeSessionRawModelUsage(background?.modelUsage, workflow?.modelUsage);
|
|
4032
|
-
const batchDurationApiMs = addApiDuration(workflow?.durationApiMs, addApiDuration(background?.durationApiMs, 0, 'durationApiMs'), 'durationApiMs');
|
|
4033
|
-
const backgroundDurationWithoutRetries = background?.durationApiWithoutRetriesMs ??
|
|
4034
|
-
background?.durationApiMs;
|
|
4035
|
-
const workflowDurationWithoutRetries = workflow?.durationApiWithoutRetriesMs ??
|
|
4036
|
-
workflow?.durationApiMs;
|
|
4037
|
-
const batchDurationApiWithoutRetriesMs = addApiDuration(workflowDurationWithoutRetries, addApiDuration(backgroundDurationWithoutRetries, 0, 'durationApiWithoutRetriesMs'), 'durationApiWithoutRetriesMs');
|
|
4038
|
-
const hasBatchDuration = background?.durationApiMs !== undefined ||
|
|
4039
|
-
background?.durationApiWithoutRetriesMs !== undefined ||
|
|
4040
|
-
workflow?.durationApiMs !== undefined ||
|
|
4041
|
-
workflow?.durationApiWithoutRetriesMs !== undefined;
|
|
4042
|
-
return {
|
|
4043
|
-
messages,
|
|
4044
|
-
...(batchUsage
|
|
4045
|
-
? {
|
|
4046
|
-
usage: batchUsage,
|
|
4047
|
-
...(batchModelUsage
|
|
4048
|
-
? { modelUsage: batchModelUsage }
|
|
4049
|
-
: {}),
|
|
4050
|
-
}
|
|
4051
|
-
: {}),
|
|
4052
|
-
...(hasBatchDuration
|
|
4053
|
-
? {
|
|
4054
|
-
durationApiMs: batchDurationApiMs,
|
|
4055
|
-
durationApiWithoutRetriesMs: batchDurationApiWithoutRetriesMs,
|
|
4056
|
-
}
|
|
4057
|
-
: {}),
|
|
4058
|
-
};
|
|
4059
|
-
},
|
|
4060
|
-
}
|
|
4061
|
-
: {}),
|
|
4062
|
-
...(this.options.approveTool
|
|
4063
|
-
? { approveTool: this.options.approveTool }
|
|
4064
|
-
: {}),
|
|
4065
|
-
permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
4066
|
-
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
4067
|
-
};
|
|
4068
|
-
const attemptMainTurn = () => signal
|
|
4069
|
-
? runtime.run({ ...runtimeRequest, signal })
|
|
4070
|
-
: runtime.run(runtimeRequest);
|
|
4071
|
-
const surfaceExhaustedRecovery = (error) => {
|
|
4072
|
-
this.options.eventSink?.({
|
|
4073
|
-
type: 'failed',
|
|
4074
|
-
message: error.message,
|
|
4075
|
-
retryable: false,
|
|
4076
|
-
});
|
|
4077
|
-
};
|
|
4078
|
-
let result;
|
|
4079
|
-
try {
|
|
4080
|
-
try {
|
|
4081
|
-
result = await attemptMainTurn();
|
|
4082
|
-
}
|
|
4083
|
-
catch (error) {
|
|
4084
|
-
if (!budget || !isPromptTooLongError(error))
|
|
4085
|
-
throw error;
|
|
4086
|
-
if (recoveryPlanner.reactiveRetriesRemaining === 0) {
|
|
4087
|
-
surfaceExhaustedRecovery(error);
|
|
4088
|
-
throw error;
|
|
4089
|
-
}
|
|
4090
|
-
const beforeRecovery = budget.evaluate(runtimeRequest.messages, definitions);
|
|
3871
|
+
let shellResult;
|
|
4091
3872
|
try {
|
|
4092
|
-
await
|
|
4093
|
-
|
|
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 } : {}),
|
|
4094
3889
|
});
|
|
4095
3890
|
}
|
|
4096
|
-
catch (
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
}
|
|
4101
|
-
// Compaction could not free the provider-bounded context; surface
|
|
4102
|
-
// the original prompt-too-long error rather than the compaction
|
|
4103
|
-
// failure.
|
|
4104
|
-
surfaceExhaustedRecovery(error);
|
|
3891
|
+
catch (error) {
|
|
3892
|
+
this.options.eventSink?.({
|
|
3893
|
+
type: 'shell-cancelled',
|
|
3894
|
+
callId: call.id,
|
|
3895
|
+
});
|
|
4105
3896
|
throw error;
|
|
4106
3897
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
...injectTurnContext([
|
|
4112
|
-
...projectClaudeModelMessages(snapshot.entries),
|
|
4113
|
-
...projectMemoryRecallMessages,
|
|
4114
|
-
]),
|
|
4115
|
-
];
|
|
4116
|
-
if (stableSystemMessageCount === undefined) {
|
|
4117
|
-
delete runtimeRequest.stableSystemMessageCount;
|
|
3898
|
+
shellUsage = shellResult.usage ?? shellUsage;
|
|
3899
|
+
if (!shellResult.isError) {
|
|
3900
|
+
shellModelUsage = shellResult.modelUsage;
|
|
3901
|
+
foregroundLineChanges.add(shellResult);
|
|
4118
3902
|
}
|
|
4119
|
-
|
|
4120
|
-
|
|
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');
|
|
4121
3917
|
}
|
|
4122
|
-
const
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
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}`);
|
|
4129
3924
|
}
|
|
4130
|
-
|
|
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 {
|
|
4131
4086
|
try {
|
|
4132
4087
|
result = await attemptMainTurn();
|
|
4133
4088
|
}
|
|
4134
|
-
catch (
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
recoveryPlanner.
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
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;
|
|
4141
4145
|
}
|
|
4142
|
-
surfaceExhaustedRecovery(error);
|
|
4143
|
-
throw error;
|
|
4144
4146
|
}
|
|
4145
4147
|
}
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
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;
|
|
4158
4162
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
if (structuredCapture && structuredCapture.calls !== 1) {
|
|
4172
|
-
throw new Error(`StructuredOutput must be called exactly once (received ${structuredCapture.calls})`);
|
|
4173
|
-
}
|
|
4174
|
-
const finalLeafUuid = lastAssistantUuid;
|
|
4175
|
-
if (!finalLeafUuid) {
|
|
4176
|
-
throw new Error('Could not locate final assistant response');
|
|
4177
|
-
}
|
|
4178
|
-
if (!skipUserPrompt) {
|
|
4179
|
-
const lastPrompt = createClaudeLastPromptEntry({
|
|
4180
|
-
sessionId,
|
|
4181
|
-
lastPrompt: effectivePrompt,
|
|
4182
|
-
leafUuid: finalLeafUuid,
|
|
4183
|
-
});
|
|
4184
|
-
const tail = await this.append(lease, snapshot.tail, lastPrompt);
|
|
4185
|
-
snapshot = {
|
|
4186
|
-
entries: [...snapshot.entries, lastPrompt],
|
|
4187
|
-
tail,
|
|
4188
|
-
};
|
|
4189
|
-
}
|
|
4190
|
-
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
4191
|
-
const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
|
|
4192
|
-
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
4193
|
-
if (!tracker) {
|
|
4194
|
-
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
4195
|
-
}
|
|
4196
|
-
const turnModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, compactionModelUsage, shellModelUsage, result.modelUsage);
|
|
4197
|
-
const combinedDurationMs = compactionDurationMs === undefined &&
|
|
4198
|
-
result.durationApiMs === undefined
|
|
4199
|
-
? undefined
|
|
4200
|
-
: (compactionDurationMs ?? 0) + (result.durationApiMs ?? 0);
|
|
4201
|
-
let rawCostUsd;
|
|
4202
|
-
if (turnModelUsage) {
|
|
4203
|
-
for (const [model, usage] of Object.entries(turnModelUsage)) {
|
|
4204
|
-
const pricing = this.options.pricing?.resolve(model);
|
|
4205
|
-
const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
4206
|
-
if (costUsd !== undefined)
|
|
4207
|
-
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})`);
|
|
4208
4175
|
}
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
const pricing = this.options.pricing?.resolve(model);
|
|
4219
|
-
const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
|
|
4220
|
-
tracker.recordTurn({
|
|
4221
|
-
model,
|
|
4222
|
-
usage,
|
|
4223
|
-
...(costUsd === undefined ? {} : { costUsd }),
|
|
4224
|
-
...(usage.webSearchRequests === undefined
|
|
4225
|
-
? {}
|
|
4226
|
-
: { 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,
|
|
4227
4185
|
});
|
|
4186
|
+
const tail = await this.append(lease, snapshot.tail, lastPrompt);
|
|
4187
|
+
snapshot = {
|
|
4188
|
+
entries: [...snapshot.entries, lastPrompt],
|
|
4189
|
+
tail,
|
|
4190
|
+
};
|
|
4228
4191
|
}
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
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
|
|
4264
4242
|
? {}
|
|
4265
|
-
: {
|
|
4266
|
-
...(
|
|
4243
|
+
: { apiDurationMs: trackedDurationApiMs }),
|
|
4244
|
+
...(trackedDurationApiWithoutRetriesMs === undefined
|
|
4267
4245
|
? {}
|
|
4268
|
-
: {
|
|
4246
|
+
: {
|
|
4247
|
+
apiDurationWithoutRetriesMs: trackedDurationApiWithoutRetriesMs,
|
|
4248
|
+
}),
|
|
4249
|
+
...(combinedToolDurationMs === 0
|
|
4250
|
+
? {}
|
|
4251
|
+
: { toolDurationMs: combinedToolDurationMs }),
|
|
4269
4252
|
});
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
...
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
type: 'warning',
|
|
4289
|
-
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 }),
|
|
4290
4271
|
});
|
|
4291
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
|
+
};
|
|
4292
4316
|
}
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
});
|
|
4298
|
-
turnCompleted = true;
|
|
4299
|
-
return {
|
|
4300
|
-
sessionId,
|
|
4301
|
-
text: structuredCapture && structuredCapture.calls === 1
|
|
4302
|
-
? JSON.stringify(structuredCapture.value)
|
|
4303
|
-
: result.text,
|
|
4304
|
-
usage: totalUsage,
|
|
4305
|
-
...(combinedDurationMs === undefined
|
|
4306
|
-
? {}
|
|
4307
|
-
: { durationApiMs: combinedDurationMs }),
|
|
4308
|
-
...(rawCostUsd === undefined ? {} : { costUsd: rawCostUsd }),
|
|
4309
|
-
...(turnModelUsage ? { modelUsage: { ...turnModelUsage } } : {}),
|
|
4310
|
-
...(structuredCapture && structuredCapture.calls === 1
|
|
4311
|
-
? { structuredOutput: structuredCapture.value }
|
|
4312
|
-
: {}),
|
|
4313
|
-
};
|
|
4314
|
-
}
|
|
4315
|
-
finally {
|
|
4316
|
-
if (!turnCompleted) {
|
|
4317
|
-
await this.hookLifecycle.end(sessionId, 'other');
|
|
4317
|
+
finally {
|
|
4318
|
+
if (!turnCompleted) {
|
|
4319
|
+
await this.hookLifecycle.end(sessionId, 'other');
|
|
4320
|
+
}
|
|
4318
4321
|
}
|
|
4322
|
+
});
|
|
4323
|
+
if (leaseResult.status === 'conflict') {
|
|
4324
|
+
throw new Error(`Claude transcript append conflict: ${leaseResult.reason}`);
|
|
4319
4325
|
}
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4326
|
+
const result = leaseResult.value;
|
|
4327
|
+
controller.complete();
|
|
4328
|
+
return result;
|
|
4329
|
+
}
|
|
4330
|
+
catch (error) {
|
|
4331
|
+
controller.fail(error, signal);
|
|
4332
|
+
throw error;
|
|
4323
4333
|
}
|
|
4324
|
-
return leaseResult.value;
|
|
4325
4334
|
}
|
|
4326
4335
|
async ensureFileResources(sessionId, signal) {
|
|
4327
4336
|
const resources = this.options.fileResources ?? [];
|
|
@@ -5087,13 +5096,6 @@ export class ClaudeSessionService {
|
|
|
5087
5096
|
const system = memory ? `${agent.body}\n\n${memory}` : agent.body;
|
|
5088
5097
|
return system.trim() ? system : null;
|
|
5089
5098
|
}
|
|
5090
|
-
assembledSystemMessages(agent, messages) {
|
|
5091
|
-
return agent &&
|
|
5092
|
-
this.options.explicitSystemPrompt &&
|
|
5093
|
-
this.options.agentSystemPromptOverridesExplicit
|
|
5094
|
-
? messages.slice(1)
|
|
5095
|
-
: messages;
|
|
5096
|
-
}
|
|
5097
5099
|
contextBudget(provider) {
|
|
5098
5100
|
if (this.options.contextBudget)
|
|
5099
5101
|
return this.options.contextBudget;
|