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.
Files changed (49) hide show
  1. package/README.md +4 -0
  2. package/dist/application/session-service.d.ts +0 -1
  3. package/dist/application/session-service.js +1619 -1617
  4. package/dist/application/subagent-service.js +27 -16
  5. package/dist/application/turn-lifecycle.d.ts +38 -0
  6. package/dist/application/turn-lifecycle.js +36 -0
  7. package/dist/cli/interactive.js +17 -7
  8. package/dist/cli/protocol.js +5 -2
  9. package/dist/cli/tui/claude-style.d.ts +5 -48
  10. package/dist/cli/tui/claude-style.js +19 -73
  11. package/dist/cli/tui/transcript-presentation.d.ts +89 -0
  12. package/dist/cli/tui/transcript-presentation.js +169 -0
  13. package/dist/cli/tui/transcript-viewport.d.ts +5 -34
  14. package/dist/cli/tui/transcript-viewport.js +639 -158
  15. package/dist/cli/tui/tui-view-model.d.ts +6 -2
  16. package/dist/cli/tui/tui-view-model.js +23 -6
  17. package/dist/compatibility/claude/context.d.ts +2 -2
  18. package/dist/compatibility/claude/context.js +2 -14
  19. package/dist/compatibility/claude/data-plane-adapter.d.ts +9 -0
  20. package/dist/compatibility/claude/data-plane-adapter.js +44 -0
  21. package/dist/compatibility/claude/paths.js +2 -2
  22. package/dist/compatibility/claude/shared-resources.d.ts +7 -38
  23. package/dist/compatibility/claude/transcript-codec.d.ts +21 -0
  24. package/dist/compatibility/claude/transcript-codec.js +512 -0
  25. package/dist/core/context.d.ts +25 -7
  26. package/dist/core/context.js +46 -0
  27. package/dist/core/prompt-composer.d.ts +8 -16
  28. package/dist/core/prompt-composer.js +65 -25
  29. package/dist/core/resources.d.ts +39 -0
  30. package/dist/core/resources.js +2 -0
  31. package/dist/core/runtime.d.ts +1 -1
  32. package/dist/core/session.d.ts +2 -0
  33. package/dist/core/session.js +5 -0
  34. package/dist/core/transcript-codec.d.ts +70 -0
  35. package/dist/core/transcript-codec.js +60 -0
  36. package/dist/core/transcript-event.d.ts +33 -0
  37. package/dist/core/transcript-event.js +198 -0
  38. package/dist/persistence/data-plane-adapter.d.ts +32 -0
  39. package/dist/persistence/data-plane-adapter.js +2 -0
  40. package/dist/persistence/data-plane.d.ts +4 -20
  41. package/dist/persistence/data-plane.js +10 -47
  42. package/dist/persistence/native-data-plane-adapter.d.ts +9 -0
  43. package/dist/persistence/native-data-plane-adapter.js +39 -0
  44. package/dist/persistence/native-resources.d.ts +4 -4
  45. package/dist/persistence/native-transcript-codec.d.ts +13 -0
  46. package/dist/persistence/native-transcript-codec.js +102 -0
  47. package/dist/tools/claude-user-message.d.ts +0 -1
  48. package/dist/tools/claude-user-message.js +0 -1
  49. 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, CLAUDE_USER_MESSAGE_PROMPT, } from '../tools/claude-user-message.js';
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(sessionId, prompt, false, signal, name, images, documents);
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(sessionId, `! ${command}`, false, signal, name, [], [], undefined, command);
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(sessionId, prompt, true, signal, name, images, documents, resumeSessionAt);
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(sessionId, `! ${command}`, true, signal, name, [], [], resumeSessionAt, command);
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?.assemble({
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
- ...(assembledContext?.systemMessages ?? []),
1329
+ ...contextProjection.systemMessages,
1279
1330
  ...injectFirstUserMessageContext([
1280
1331
  ...projectClaudeModelMessages(entries),
1281
1332
  { role: 'user', content: prompt },
1282
- ], assembledContext?.firstUserMessageContext),
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?.assemble({
1485
+ const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
1434
1486
  cwd: this.activeCwd(),
1435
1487
  lifecycleId: sessionId,
1436
- ...(agentSystem ? { mode: 'agent', baseSystemPrompt: agentSystem } : {}),
1488
+ ...(agentSystem
1489
+ ? { mode: 'agent', baseSystemPrompt: agentSystem }
1490
+ : {}),
1437
1491
  });
1438
- const hasPromptManifest = assembledContext?.promptSections !== undefined;
1439
- const assembledSystemMessages = hasPromptManifest
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
- ], assembledContext?.firstUserMessageContext),
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(sessionId, '/permissions', true, signal, undefined, [], [], undefined, undefined, true);
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(sessionId, prompt, requireExisting, signal, name, images = [], documents = [], resumeSessionAt, shellCommand, skipUserPrompt = false) {
2637
- this.assertWritable();
2638
- if (prompt.length === 0 && images.length === 0 && documents.length === 0)
2639
- throw new Error('Prompt must not be empty');
2640
- if (name !== undefined && name.length === 0) {
2641
- throw new Error('Session name must not be empty');
2642
- }
2643
- if (shellCommand !== undefined && shellCommand.trim().length === 0) {
2644
- throw new Error('Shell command must not be empty');
2645
- }
2646
- await this.activateSessionCostTracker(sessionId);
2647
- if (this.options.sessionPersistence !== false) {
2648
- this.durableMetadataSessions.add(sessionId);
2649
- }
2650
- await this.ensureFileResources(sessionId, signal);
2651
- this.worktreeManager?.bindSession(sessionId);
2652
- if (this.options.initialWorktree) {
2653
- await this.worktreeManager?.ensureInitial(this.options.initialWorktreeName);
2654
- }
2655
- const pinnedCwd = this.sessionCwds.get(sessionId) ?? this.activeCwd();
2656
- this.sessionCwds.set(sessionId, pinnedCwd);
2657
- if (this.options.workspace?.cwd() !== pinnedCwd) {
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
- let snapshot = await lease.load();
2677
- let automaticReplayPrompt;
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
- if (requireExisting && snapshot.entries.length === 0) {
2689
- throw new Error(`Claude session not found: ${sessionId}`);
2713
+ await this.activateSessionCostTracker(sessionId);
2714
+ if (this.options.sessionPersistence !== false) {
2715
+ this.durableMetadataSessions.add(sessionId);
2690
2716
  }
2691
- this.rememberDurableMetadata(sessionId, snapshot.entries);
2692
- if (resumeSessionAt !== undefined) {
2693
- snapshot = {
2694
- entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
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
- else if (this.explicitResumeLeafUuids.has(sessionId)) {
2699
- const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
2700
- this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
2701
- snapshot = {
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
- if (requireExisting &&
2707
- resumeSessionAt === undefined &&
2708
- this.options.resumeInterruptedTurn === true &&
2709
- shellCommand === undefined &&
2710
- !skipUserPrompt &&
2711
- !(this.options.approveRecovery !== undefined &&
2712
- findUnresolvedClaudeToolCalls(snapshot.entries).length > 0)) {
2713
- const interruption = classifyClaudeInterruption(snapshot.entries);
2714
- if ((interruption.kind === 'interrupted-prompt' ||
2715
- interruption.kind === 'interrupted-turn') &&
2716
- interruption.prompt !== undefined &&
2717
- interruption.replayEntries !== undefined) {
2718
- automaticReplayPrompt = interruption.prompt;
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: interruption.replayEntries,
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
- const initialTransition = this.worktreeManager?.consumeTransition('__initial__');
2729
- if (initialTransition && snapshot.entries.length === 0) {
2730
- const stateEntry = {
2731
- type: 'worktree-state',
2732
- worktreeSession: initialTransition.state,
2733
- sessionId,
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
- snapshot = {
2753
- entries: [...snapshot.entries, ...entries],
2754
- tail: appendResult.tail,
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
- agent?.initialPrompt
2768
- ? `${agent.initialPrompt}\n\n${prompt}`
2769
- : prompt);
2770
- const projectMemoryRecallTurn = !skipUserPrompt && shellCommand === undefined
2771
- ? this.options.projectMemoryRecall?.prefetch({
2772
- sessionId,
2773
- turnId: randomUUID(),
2774
- prompt: effectivePrompt,
2775
- ...(signal ? { signal } : {}),
2776
- })
2777
- : undefined;
2778
- const initialPricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
2779
- if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
2780
- throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
2781
- }
2782
- const shellInputUuid = shellCommand === undefined ? null : randomUUID();
2783
- let currentPromptId = shellInputUuid;
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
- await appendHookOutcome(outcome);
2816
- };
2817
- const flushRecoveryHookOutcomes = async () => {
2818
- const entries = [];
2819
- let history = snapshot.entries;
2820
- let parentUuid = this.logicalTailUuid(snapshot.tail);
2821
- for (const outcome of pendingRecoveryHookOutcomes) {
2822
- const outcomeEntries = createClaudeHookAttachmentEntries(outcome, {
2823
- ...this.translationContext(sessionId, snapshot),
2824
- parentUuid,
2825
- history,
2826
- });
2827
- entries.push(...outcomeEntries);
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
- if (entries.length === 0) {
2834
- pendingRecoveryHookOutcomes.length = 0;
2835
- return;
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 appendResult = await lease.appendMany(snapshot.tail, entries);
2838
- if (appendResult.status === 'conflict') {
2839
- throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
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
- snapshot = { entries: history, tail: appendResult.tail };
2842
- pendingRecoveryHookOutcomes.length = 0;
2843
- };
2844
- const capabilities = this.toolCapabilities();
2845
- const taskToolNames = this.capabilityToolNames(this.options.taskToolNames, capabilities);
2846
- const scheduledToolNames = this.capabilityToolNames(this.options.scheduledToolNames, capabilities);
2847
- const taskTools = this.options.tools && taskToolNames.length > 0
2848
- ? new ClaudeTaskToolRegistry({
2849
- base: this.options.tools,
2850
- cwd: this.activeCwd(),
2851
- cwdProvider: () => this.activeCwd(),
2852
- praxisRoot: sessionPaths.praxisRoot,
2853
- sessionId,
2854
- taskRoot: sessionPaths.taskRoot,
2855
- ...(this.options.eventSink
2856
- ? { eventSink: this.options.eventSink }
2857
- : {}),
2858
- enabledTools: taskToolNames,
2859
- ...(this.options.hooks
2860
- ? {
2861
- taskHooks: {
2862
- created: async (task, taskSignal) => {
2863
- const outcome = await this.options.hooks?.run({
2864
- ...hookSession,
2865
- hook_event_name: 'TaskCreated',
2866
- task_id: task.id,
2867
- task_subject: task.subject,
2868
- task_description: task.description,
2869
- }, undefined, taskSignal);
2870
- if (!outcome)
2871
- return;
2872
- await recordHookOutcome(outcome);
2873
- if (outcome.blockedReason) {
2874
- throw new Error(`TaskCreated hook error: ${outcome.blockedReason}`);
2875
- }
2876
- },
2877
- completed: async (task, taskSignal) => {
2878
- const outcome = await this.options.hooks?.run({
2879
- ...hookSession,
2880
- hook_event_name: 'TaskCompleted',
2881
- task_id: task.id,
2882
- task_subject: task.subject,
2883
- task_description: task.description,
2884
- }, undefined, taskSignal);
2885
- if (!outcome)
2886
- return;
2887
- await recordHookOutcome(outcome);
2888
- if (outcome.blockedReason) {
2889
- throw new Error(`TaskCompleted hook error: ${outcome.blockedReason}`);
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
- : null;
2897
- const scheduledTools = this.scheduledPrompts &&
2898
- this.options.tools &&
2899
- scheduledToolNames.length > 0
2900
- ? new ClaudeScheduledToolRegistry({
2901
- base: taskTools ?? this.options.tools,
2902
- manager: this.scheduledPrompts,
2903
- sessionId,
2904
- enabledTools: scheduledToolNames,
2905
- dataPlane: this.options.dataPlane ?? 'claude',
2906
- })
2907
- : null;
2908
- const baseTools = scheduledTools ?? taskTools ?? this.options.tools;
2909
- const turnPermissions = this.options.interactiveTools?.permissions(sessionId) ??
2910
- this.options.permissions;
2911
- const subagentExecutor = (this.options.enableSubagents || this.options.enableWorkflows) &&
2912
- baseTools &&
2913
- turnPermissions
2914
- ? new ClaudeSubagentExecutor({
2915
- configRoot: this.options.configRoot,
2916
- dataPlane: this.options.dataPlane ?? 'claude',
2917
- cwd: this.activeCwd(),
2918
- cwdProvider: () => this.activeCwd(),
2919
- claudeVersion: this.options.claudeVersion,
2920
- provider,
2921
- persistence: this.options.sessionPersistence === false ? 'memory' : 'disk',
2922
- ...(this.options.providerForModel
2923
- ? { providerForModel: this.options.providerForModel }
2924
- : {}),
2925
- baseTools,
2926
- permissions: turnPermissions,
2927
- ...(this.options.permissionResolverForMode
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
- permissionResolverForMode: this.options.permissionResolverForMode,
3184
+ generateToolUseSummary: ({ tools, lastAssistantText, signal: summarySignal, }) => generateToolUseSummary(provider, tools, summarySignal, lastAssistantText, (metrics) => this.recordAuxiliaryMetrics(sessionId, metrics)),
2930
3185
  }
2931
3186
  : {}),
2932
- parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
2933
- this.options.permissionMode),
2934
- ...(this.options.subagentToolNames
3187
+ ...(this.options.pricing
2935
3188
  ? {
2936
- toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
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.extensions
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
- contextReserveTokens: this.options.contextBudget?.reserveTokens ??
2952
- this.options.contextReserveTokens,
3201
+ ...(structuredTools ? { tools: structuredTools } : {}),
3202
+ ...(turnPermissions ? { permissions: turnPermissions } : {}),
2953
3203
  }),
2954
- ...(this.options.approveTool
2955
- ? { approveTool: this.options.approveTool }
2956
- : {}),
2957
- permissionUpdates: () => this.sessionPermissionUpdates.get(sessionId) ?? [],
2958
- onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
2959
- onLineChanges: (changes) => {
2960
- const tracker = this.sessionCostTrackers.get(sessionId);
2961
- if (!tracker) {
2962
- throw new Error(`Session cost tracker is not active for session ${sessionId}`);
2963
- }
2964
- tracker.recordLineChanges(changes);
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
- ...(this.options.eventSink
2967
- ? { eventSink: this.options.eventSink }
2968
- : {}),
2969
- ...(taskTools
2970
- ? {
2971
- backgroundTaskNotifications: (waitForRunning) => taskTools.notifications(waitForRunning),
2972
- }
2973
- : {}),
2974
- notificationDelivered: (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification),
2975
- stopOwnedBackgroundAgent: (ownerSessionId, agentId) => this.backgroundTasks.stopAgent(ownerSessionId, agentId),
2976
- outputOwnedBackgroundAgent: (ownerSessionId, agentId, options) => this.backgroundTasks.outputAgent(ownerSessionId, agentId, options),
2977
- sendOwnedBackgroundAgent: (ownerSessionId, agentId, message, summary, toolUseId) => this.sendOwnedBackgroundAgent(ownerSessionId, agentId, message, summary, toolUseId),
2978
- })
2979
- : null;
2980
- if (subagentExecutor) {
2981
- this.subagentExecutors.add(subagentExecutor);
2982
- this.subagentExecutorSessions.set(subagentExecutor, sessionId);
2983
- this.backgroundTasks.registerAgents(sessionId, subagentExecutor);
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 prepared = await fileHistory.prepareMutation(snapshot.entries, snapshotMessageId, path);
3060
- let result;
3061
- try {
3062
- result = await interactiveMessageTools.execute(call, context);
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
- catch (error) {
3065
- await prepared.rollback();
3066
- throw error;
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 (result.isError) {
3069
- await prepared.rollback();
3070
- return result;
3310
+ if (toolResult.isError ||
3311
+ call.name !== 'Read' ||
3312
+ !this.options.conditionalRuleResolver ||
3313
+ !toolResult.accessedPaths) {
3314
+ return;
3071
3315
  }
3072
- const entry = prepared.commit(assistantMessageId);
3073
- if (entry) {
3074
- const tail = await this.append(lease, snapshot.tail, entry);
3075
- snapshot = { entries: [...snapshot.entries, entry], tail };
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
- : interactiveMessageTools;
3081
- const capabilityTools = fileHistoryTools
3082
- ? new ClaudeCapabilityToolRegistry(fileHistoryTools, capabilities)
3083
- : undefined;
3084
- const structuredCapture = this.options.structuredOutputSchema
3085
- ? { calls: 0, value: undefined }
3086
- : undefined;
3087
- const agentScopedTools = agent && capabilityTools
3088
- ? new FilteredToolRegistry(capabilityTools, {
3089
- tools: mainAgentToolNames(capabilityTools, agent),
3090
- })
3091
- : capabilityTools;
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
- deferPreToolUseOutcome: (call) => pendingRecoveryToolCallIds.has(call.id),
3108
- })
3109
- : null;
3110
- const runtime = new AgentRuntime(provider, this.options.eventSink, {
3111
- emitInitialContextState: false,
3112
- ...(this.options.emitToolUseSummaries
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 [entry] = translateProviderEvents([
3193
- {
3194
- type: 'tool-result',
3195
- toolCallId: call.id,
3196
- content: toolResult.content,
3197
- ...(toolResult.contentBlocks
3198
- ? { contentBlocks: toolResult.contentBlocks }
3199
- : {}),
3200
- ...(toolResult.images ? { images: toolResult.images } : {}),
3201
- ...(toolResult.documents
3202
- ? { documents: toolResult.documents }
3203
- : {}),
3204
- isError: toolResult.isError,
3205
- ...(toolResult.nativeToolUseResult
3206
- ? { nativeToolUseResult: toolResult.nativeToolUseResult }
3207
- : {}),
3208
- ...(toolResult.nativeMcpMeta
3209
- ? { nativeMcpMeta: toolResult.nativeMcpMeta }
3210
- : {}),
3211
- },
3212
- ], this.translationContext(sessionId, snapshot));
3213
- if (!entry)
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
- if (toolResult.isError ||
3241
- call.name !== 'Read' ||
3242
- !this.options.conditionalRuleResolver ||
3243
- !toolResult.accessedPaths) {
3244
- return;
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
- const attachedRulePaths = this.attachedRulePaths(snapshot.entries);
3247
- for (const filePath of toolResult.accessedPaths) {
3248
- const rules = await this.options.conditionalRuleResolver.resolve(filePath, [...attachedRulePaths]);
3249
- for (const rule of rules) {
3250
- const attachment = createClaudeRuleAttachmentEntry(rule, this.displayRulePath(rule.path), this.translationContext(sessionId, snapshot));
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, followUpEntry],
3282
- tail: followUpTail,
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
- let turnCompleted = false;
3290
- try {
3291
- const outcome = await this.hookLifecycle.start(sessionId, hookSession, requireExisting ? 'resume' : 'startup', signal);
3292
- if (outcome) {
3293
- await recordHookOutcome(outcome, pendingRecoveryToolCallIds.size > 0);
3294
- }
3295
- const approveRecovery = this.options.approveRecovery;
3296
- const recoveryRequest = {
3297
- cwd: this.activeCwd(),
3298
- toolResultDirectory,
3299
- messages: projectClaudeModelMessages(snapshot.entries),
3300
- observer,
3301
- permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
3302
- onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
3303
- ...(signal ? { signal } : {}),
3304
- ...(approveRecovery
3305
- ? {
3306
- approveRecovery: async (call) => {
3307
- if (!(await approveRecovery(call))) {
3308
- throw new Error(`Claude session tool call ${call.id} recovery was declined`);
3309
- }
3310
- if (signal?.aborted)
3311
- throw new AgentRunCancelledError();
3312
- await flushRecoveryHookOutcomes();
3313
- pendingRecoveryToolCallIds.delete(call.id);
3314
- return true;
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
- approveTool: () => true,
3317
- }
3318
- : {}),
3319
- };
3320
- const unresolvedToolCall = unresolvedToolCalls[0];
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
- const sessionMemory = this.sessionMemoryController(sessionId);
3342
- let assembledContext;
3343
- let agentSystem = null;
3344
- let planModeMessage;
3345
- let sessionMemoryMessage = null;
3346
- let contextMessages = [];
3347
- let stableSystemMessageCount;
3348
- const refreshRuntimeContext = async () => {
3349
- agentSystem = await this.mainAgentSystemPrompt(agent);
3350
- planModeMessage =
3351
- this.options.interactiveTools?.contextMessage(sessionId);
3352
- sessionMemoryMessage = sessionMemory
3353
- ? this.sessionMemoryMessage(await sessionMemory.summary())
3354
- : null;
3355
- const additionalSections = [
3356
- ...(planModeMessage
3357
- ? [
3358
- {
3359
- id: 'plan-mode',
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
- const hasPromptManifest = assembledContext?.promptSections !== undefined;
3406
- stableSystemMessageCount = hasPromptManifest
3407
- ? assembledContext?.stableSystemSectionCount
3408
- : undefined;
3409
- const assembledSystemMessages = hasPromptManifest
3410
- ? (assembledContext?.systemMessages ?? [])
3411
- : this.assembledSystemMessages(agent, assembledContext?.systemMessages ?? []);
3412
- contextMessages = [
3413
- ...(!hasPromptManifest && agentSystem
3414
- ? [{ role: 'system', content: agentSystem }]
3415
- : []),
3416
- ...assembledSystemMessages,
3417
- ...(!hasPromptManifest && planModeMessage
3418
- ? [{ role: 'system', content: planModeMessage }]
3419
- : []),
3420
- ...(!hasPromptManifest && sessionMemoryMessage
3421
- ? [{ role: 'system', content: sessionMemoryMessage }]
3422
- : []),
3423
- ...(!hasPromptManifest && this.options.brief
3424
- ? [
3425
- {
3426
- role: 'system',
3427
- content: CLAUDE_USER_MESSAGE_PROMPT,
3428
- },
3429
- ]
3430
- : []),
3431
- ...(!hasPromptManifest && this.options.structuredOutputSchema
3432
- ? [
3433
- {
3434
- role: 'system',
3435
- content: 'You MUST call StructuredOutput exactly once at the end with a value matching the requested JSON Schema.',
3436
- },
3437
- ]
3438
- : []),
3439
- ];
3440
- };
3441
- await refreshRuntimeContext();
3442
- const expansion = skipUserPrompt
3443
- ? { userMessages: [] }
3444
- : shellCommand === undefined
3445
- ? this.options.extensions
3446
- ? await this.options.extensions.expandPromptAsync(effectivePrompt, signal, toolResultDirectory)
3447
- : { userMessages: [effectivePrompt] }
3448
- : {
3449
- userMessages: [`<bash-input>${shellCommand}</bash-input>`],
3450
- };
3451
- const expandedMessages = expansion.messages ?? expansion.userMessages.map((text) => ({ text }));
3452
- const attachmentIndex = expansion.messages
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
- if (!foundPrompt)
3511
- return [...messages];
3512
- return [
3513
- ...messages.slice(0, insertionIndex),
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
- const irreducible = budget.evaluate(irreducibleMessages, definitions);
3548
- budget.assertFits(irreducible);
3549
- let logicalParentUuid = compactionAnchorUuid;
3550
- if (!logicalParentUuid || historyMessages.length === 0) {
3551
- budget.assertFits(predicted);
3552
- throw new Error('Cannot compact an empty Claude transcript');
3553
- }
3554
- if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
3555
- throw new Error('Cannot compact a Claude session with unresolved tool calls');
3556
- }
3557
- const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', { trigger: 'auto', custom_instructions: null }, 'auto', signal);
3558
- const memorySelection = sessionMemory
3559
- ? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries))
3560
- : null;
3561
- const compactorMessages = memorySelection
3562
- ? [
3563
- memorySelection.memoryMessage,
3564
- ...projectClaudeModelMessages(memorySelection.compactedEntries),
3565
- ]
3566
- : historyMessages;
3567
- compactorMessages.push(...successfulHookOutput(preCompact).map((content) => ({
3568
- role: 'user',
3569
- content: `Additional summarization context: ${content}`,
3570
- })));
3571
- if (memorySelection) {
3572
- logicalParentUuid = memorySelection.logicalParentUuid;
3573
- }
3574
- this.options.eventSink?.({ type: 'state', state: 'compacting' });
3575
- const compactEnvelope = budget.evaluate([
3576
- ...irreducibleMessages,
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: formatClaudeCompactSummary(''),
3580
- },
3581
- ], definitions);
3582
- let targetTokens = Math.min(8192, compactEnvelope.availableTokens - compactEnvelope.estimatedTokens);
3583
- if (targetTokens < 1) {
3584
- budget.assertFits(budget.evaluate([
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('a'),
3589
+ content: formatClaudeCompactSummary(''),
3589
3590
  },
3590
- ], definitions));
3591
- targetTokens = 1;
3592
- }
3593
- const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
3594
- messages: compactorMessages,
3595
- targetTokens,
3596
- contextWindowTokens: budget.contextWindowTokens,
3597
- ...(signal ? { signal } : {}),
3598
- });
3599
- const { durationMs: compactedDurationMs, durationWithoutRetriesMs: compactedDurationWithoutRetriesMs, } = requireCompactionDurations(compacted);
3600
- const proposedCompactionDurationMs = (compactionDurationMs ?? 0) + compactedDurationMs;
3601
- if (!Number.isFinite(proposedCompactionDurationMs)) {
3602
- throw new TypeError('compaction durationMs total overflow');
3603
- }
3604
- const proposedCompactionDurationWithoutRetriesMs = (compactionDurationWithoutRetriesMs ?? 0) +
3605
- compactedDurationWithoutRetriesMs;
3606
- if (!Number.isFinite(proposedCompactionDurationWithoutRetriesMs)) {
3607
- throw new TypeError('compaction durationWithoutRetriesMs total overflow');
3608
- }
3609
- const boundaryUuid = randomUUID();
3610
- const summaryUuid = randomUUID();
3611
- const timestamp = new Date().toISOString();
3612
- const preTokens = budget.evaluate([...contextMessages, ...injectTurnContext(historyMessages)], definitions).estimatedTokens;
3613
- const compactEntries = (postTokens) => {
3614
- const uuids = [boundaryUuid, summaryUuid];
3615
- return createClaudeCompactEntries({
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
- logicalParentUuid,
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
- ...(memorySelection
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
- const provisionalEntries = compactEntries(0);
3636
- const compactSummaryUuid = provisionalEntries.at(-1)?.uuid;
3637
- if (typeof compactSummaryUuid !== 'string') {
3638
- throw new Error('Could not create Claude compact summary');
3639
- }
3640
- const replayUuids = preservedUserMessages.map(() => randomUUID());
3641
- const replayEntries = translateProviderEvents(preservedUserMessages.map((text, index) => index === 0
3642
- ? { type: 'user-text', text }
3643
- : { type: 'user-text-block', text }), {
3644
- sessionId,
3645
- parentUuid: compactSummaryUuid,
3646
- cwd: this.activeCwd(),
3647
- claudeVersion: this.options.claudeVersion,
3648
- gitBranch: null,
3649
- history: [...snapshot.entries, ...provisionalEntries],
3650
- createUuid: () => replayUuids.shift() ?? randomUUID(),
3651
- now: () => timestamp,
3652
- });
3653
- const compactedHistory = [
3654
- ...projectClaudeModelMessages([
3655
- ...snapshot.entries,
3656
- ...provisionalEntries,
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
- ...projectMemoryRecallMessages,
3660
- ];
3661
- const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
3662
- const afterPending = budget.evaluate([
3663
- ...contextMessages,
3664
- ...injectTurnContext([...compactedHistory, ...pendingMessages]),
3665
- ], definitions);
3666
- budget.assertFits(afterPending);
3667
- if (options.promptTooLong === true &&
3668
- !contextRecoveryMadeProgress({
3669
- beforeOccupancyTokens: predicted.occupancyTokens,
3670
- afterOccupancyTokens: afterPending.occupancyTokens,
3671
- })) {
3672
- throw new Error('Reactive context compaction made no occupancy progress');
3673
- }
3674
- const entries = [
3675
- ...compactEntries(afterHistory.estimatedTokens),
3676
- ...replayEntries,
3677
- ];
3678
- if (signal?.aborted)
3679
- throw new AgentRunCancelledError();
3680
- // Construct the exact tracker mutations for this single committed
3681
- // boundary and preflight them against a clone of the live tracker so
3682
- // invalid input or cumulative overflow rejects before the compact
3683
- // boundary is appended rather than after a half-commit.
3684
- const tracker = this.sessionCostTrackers.get(sessionId);
3685
- if (!tracker) {
3686
- throw new Error(`Session cost tracker is not active for session ${sessionId}`);
3687
- }
3688
- const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
3689
- ? compacted.model
3690
- : provider.model;
3691
- const compactModelNonBlank = compactModel !== undefined && compactModel.trim() !== '';
3692
- if (hasNonZeroUsage(compacted.usage) && !compactModelNonBlank) {
3693
- throw new Error('Auto compact usage requires a nonblank model identity');
3694
- }
3695
- let meteringTurnInput;
3696
- if (compactModelNonBlank && hasNonZeroUsage(compacted.usage)) {
3697
- const pricing = this.options.pricing?.resolve(compactModel);
3698
- const costUsd = pricing
3699
- ? usageCostUsd(compacted.usage, pricing)
3700
- : undefined;
3701
- meteringTurnInput = {
3702
- model: compactModel,
3703
- usage: compacted.usage,
3704
- ...(costUsd === undefined ? {} : { costUsd }),
3705
- ...(compacted.usage.webSearchRequests === undefined
3706
- ? {}
3707
- : { webSearchRequests: compacted.usage.webSearchRequests }),
3708
- };
3709
- }
3710
- let meteringDurationsInput;
3711
- if (compactedDurationMs > 0 ||
3712
- compactedDurationWithoutRetriesMs > 0) {
3713
- meteringDurationsInput = {
3714
- ...(compactedDurationMs === 0
3715
- ? {}
3716
- : { apiDurationMs: compactedDurationMs }),
3717
- apiDurationWithoutRetriesMs: compactedDurationWithoutRetriesMs,
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
- if (meteringTurnInput !== undefined ||
3721
- meteringDurationsInput !== undefined) {
3722
- const preflight = new ClaudeSessionCostTracker({
3723
- sessionId,
3724
- restored: tracker.snapshot(),
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
- preflight.recordTurn(meteringTurnInput);
3797
+ tracker.recordTurn(meteringTurnInput);
3728
3798
  }
3729
3799
  if (meteringDurationsInput !== undefined) {
3730
- preflight.recordDurations(meteringDurationsInput);
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
- const metadataEntries = createClaudeDurableMetadataSnapshot(snapshot.entries, sessionId);
3742
- if (metadataEntries.length > 0) {
3743
- const metadataAppend = await lease.appendMany(snapshot.tail, metadataEntries);
3744
- if (metadataAppend.status === 'conflict') {
3745
- throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
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, ...metadataEntries],
3749
- tail: metadataAppend.tail,
3834
+ entries: [...snapshot.entries, userEntry],
3835
+ tail: userTail,
3750
3836
  };
3751
3837
  }
3752
- this.rememberDurableMetadata(sessionId, snapshot.entries);
3753
- await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
3754
- // The boundary is durable: mirror Claude's full-compact behavior by
3755
- // rerunning SessionStart with source compact and refreshing the
3756
- // runtime-only context so the next request retains current
3757
- // instructions, plan state, session memory, and hook context.
3758
- if (this.options.hooks) {
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
- await compactIfNeeded(pendingUserMessages);
3794
- for (const [index, message] of expandedMessages.entries()) {
3795
- if (shellCommand !== undefined)
3796
- break;
3797
- const messageImages = [
3798
- ...(index === attachmentIndex ? images : []),
3799
- ...(message.images ?? []),
3800
- ];
3801
- const persistenceEvent = messageImages.length > 0 ||
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
- catch (error) {
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-cancelled',
3867
+ type: 'shell-command',
3883
3868
  callId: call.id,
3869
+ command: shellCommand,
3884
3870
  });
3885
- throw error;
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 compactIfNeeded([], currentTurnUserMessages ?? [], {
4093
- promptTooLong: true,
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 (compactionError) {
4097
- if (signal?.aborted ||
4098
- compactionError instanceof AgentRunCancelledError) {
4099
- throw new AgentRunCancelledError();
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
- // The single reactive retry must use the compacted transcript, not
4108
- // the stale request copy captured before the compact boundary.
4109
- runtimeRequest.messages = [
4110
- ...contextMessages,
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
- else {
4120
- runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
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 afterRecovery = budget.evaluate(runtimeRequest.messages, definitions);
4123
- if (recoveryPlanner.consumeReactiveRetry({
4124
- beforeOccupancyTokens: beforeRecovery.occupancyTokens,
4125
- afterOccupancyTokens: afterRecovery.occupancyTokens,
4126
- }) !== 'reactive-retry') {
4127
- surfaceExhaustedRecovery(error);
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
- runtimeRequest.deferFailureKinds = true;
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 (retryError) {
4135
- // Exactly one reactive retry is consumed; fail deterministically
4136
- // and surface the original prompt-too-long error.
4137
- recoveryPlanner.recordFailure();
4138
- if (signal?.aborted ||
4139
- retryError instanceof AgentRunCancelledError) {
4140
- throw new AgentRunCancelledError();
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
- catch (error) {
4148
- if (!signal?.aborted &&
4149
- !(error instanceof AgentRunCancelledError) &&
4150
- !(error instanceof ModelProviderError && error.kind === 'cancelled')) {
4151
- const failureKind = error instanceof ModelProviderError
4152
- ? claudeStopFailureError(error.kind)
4153
- : 'unknown';
4154
- await this.runAdvisoryHook(sessionId, 'StopFailure', {
4155
- error: failureKind,
4156
- error_details: error instanceof Error ? error.message : String(error),
4157
- }, failureKind, signal);
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
- throw error;
4160
- }
4161
- recoveryPlanner.recordSuccess();
4162
- const mainModel = provider.model !== undefined && provider.model.trim() !== ''
4163
- ? provider.model
4164
- : undefined;
4165
- const observedUsage = (mainModel !== undefined
4166
- ? result.modelUsage?.[mainModel]
4167
- : undefined) ??
4168
- Object.values(result.modelUsage ?? {})[0] ??
4169
- result.usage;
4170
- budget?.observeUsage(observedUsage, runtimeRequest.messages, definitions);
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
- // Auto-compaction metering was already recorded atomically with each
4211
- // committed boundary, and externally metered tool-summary metrics were
4212
- // committed through the summary callback, so the live tracker receives
4213
- // only the unrecorded subset here. The inclusive public aggregates
4214
- // above still contain every row and duration.
4215
- const trackedModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, shellModelUsage, result.unrecordedModelUsage ?? result.modelUsage);
4216
- if (trackedModelUsage) {
4217
- for (const [model, usage] of Object.entries(trackedModelUsage)) {
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
- let combinedToolDurationMs = 0;
4231
- for (const recoveryResult of recoveryResults) {
4232
- combinedToolDurationMs = addToolDuration(recoveryResult.durationToolMs, combinedToolDurationMs);
4233
- }
4234
- combinedToolDurationMs = addToolDuration(result.durationToolMs, combinedToolDurationMs);
4235
- const trackedDurationApiMs = result.unrecordedDurationApiMs ?? result.durationApiMs;
4236
- const trackedDurationApiWithoutRetriesMs = result.unrecordedDurationApiWithoutRetriesMs ??
4237
- result.durationApiWithoutRetriesMs;
4238
- tracker.recordDurations({
4239
- ...(trackedDurationApiMs === undefined
4240
- ? {}
4241
- : { apiDurationMs: trackedDurationApiMs }),
4242
- ...(trackedDurationApiWithoutRetriesMs === undefined
4243
- ? {}
4244
- : {
4245
- apiDurationWithoutRetriesMs: trackedDurationApiWithoutRetriesMs,
4246
- }),
4247
- ...(combinedToolDurationMs === 0
4248
- ? {}
4249
- : { toolDurationMs: combinedToolDurationMs }),
4250
- });
4251
- foregroundLineChanges.add({
4252
- isError: false,
4253
- ...(result.linesAdded === undefined
4254
- ? {}
4255
- : { linesAdded: result.linesAdded }),
4256
- ...(result.linesRemoved === undefined
4257
- ? {}
4258
- : { linesRemoved: result.linesRemoved }),
4259
- });
4260
- if (foregroundLineChanges.linesAdded !== 0 ||
4261
- foregroundLineChanges.linesRemoved !== 0) {
4262
- tracker.recordLineChanges({
4263
- ...(foregroundLineChanges.linesAdded === 0
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
- : { linesAdded: foregroundLineChanges.linesAdded }),
4266
- ...(foregroundLineChanges.linesRemoved === 0
4243
+ : { apiDurationMs: trackedDurationApiMs }),
4244
+ ...(trackedDurationApiWithoutRetriesMs === undefined
4267
4245
  ? {}
4268
- : { linesRemoved: foregroundLineChanges.linesRemoved }),
4246
+ : {
4247
+ apiDurationWithoutRetriesMs: trackedDurationApiWithoutRetriesMs,
4248
+ }),
4249
+ ...(combinedToolDurationMs === 0
4250
+ ? {}
4251
+ : { toolDurationMs: combinedToolDurationMs }),
4269
4252
  });
4270
- }
4271
- if (sessionMemory && finalLeafUuid) {
4272
- const memorySnapshot = projectClaudeModelMessages(snapshot.entries);
4273
- const providerVisibleMessages = [
4274
- ...contextMessages,
4275
- ...injectTurnContext(memorySnapshot),
4276
- ];
4277
- const currentContextTokens = budget
4278
- ? budget.evaluate(providerVisibleMessages, definitions)
4279
- .occupancyTokens
4280
- : estimateModelRequestTokens(providerVisibleMessages, definitions);
4281
- try {
4282
- await sessionMemory.observeContext(currentContextTokens, currentTurnToolCalls, finalLeafUuid, memorySnapshot);
4283
- }
4284
- catch (error) {
4285
- // A failed observation must not fail the user turn; the sidecar
4286
- // retains a retryable error for the next observation.
4287
- this.options.eventSink?.({
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
- this.options.projectMemoryExtraction?.observe({
4294
- sessionId,
4295
- directMaintenance: projectMemoryMaintained,
4296
- messages: projectMemoryMessages(snapshot.entries),
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
- if (leaseResult.status === 'conflict') {
4322
- throw new Error(`Claude transcript append conflict: ${leaseResult.reason}`);
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;