praxis-agent 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +9 -1
  2. package/dist/application/background-agent-manager.d.ts +16 -6
  3. package/dist/application/background-agent-manager.js +139 -97
  4. package/dist/application/context-engine.d.ts +56 -0
  5. package/dist/application/context-engine.js +129 -0
  6. package/dist/application/session-service.d.ts +8 -5
  7. package/dist/application/session-service.js +1824 -1728
  8. package/dist/application/subagent-service.js +385 -193
  9. package/dist/application/top-level-agent-manager.d.ts +1 -0
  10. package/dist/application/top-level-agent-manager.js +1025 -403
  11. package/dist/application/transcript-projection.d.ts +33 -0
  12. package/dist/application/transcript-projection.js +110 -0
  13. package/dist/application/turn-lifecycle.d.ts +40 -0
  14. package/dist/application/turn-lifecycle.js +36 -0
  15. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  16. package/dist/application/turn-memory-coordinator.js +98 -0
  17. package/dist/cli/agents-dashboard.js +10 -5
  18. package/dist/cli/interactive.js +279 -209
  19. package/dist/cli/protocol.js +5 -2
  20. package/dist/cli/tui/claude-style.d.ts +5 -48
  21. package/dist/cli/tui/claude-style.js +167 -197
  22. package/dist/cli/tui/task-panel.js +10 -1
  23. package/dist/cli/tui/theme.d.ts +46 -3
  24. package/dist/cli/tui/theme.js +414 -56
  25. package/dist/cli/tui/tool-permission.js +9 -5
  26. package/dist/cli/tui/transcript-presentation.d.ts +89 -0
  27. package/dist/cli/tui/transcript-presentation.js +169 -0
  28. package/dist/cli/tui/transcript-viewport.d.ts +5 -34
  29. package/dist/cli/tui/transcript-viewport.js +639 -158
  30. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  31. package/dist/cli/tui/tui-interaction-router.js +127 -0
  32. package/dist/cli/tui/tui-view-model.d.ts +6 -2
  33. package/dist/cli/tui/tui-view-model.js +23 -6
  34. package/dist/cli-runtime.d.ts +2 -2
  35. package/dist/compatibility/claude/context.d.ts +2 -2
  36. package/dist/compatibility/claude/context.js +2 -14
  37. package/dist/compatibility/claude/data-plane-adapter.d.ts +9 -0
  38. package/dist/compatibility/claude/data-plane-adapter.js +44 -0
  39. package/dist/compatibility/claude/paths.js +2 -2
  40. package/dist/compatibility/claude/shared-resources.d.ts +7 -38
  41. package/dist/compatibility/claude/transcript-codec.d.ts +21 -0
  42. package/dist/compatibility/claude/transcript-codec.js +512 -0
  43. package/dist/core/agent-orchestration.d.ts +31 -0
  44. package/dist/core/agent-orchestration.js +235 -0
  45. package/dist/core/context.d.ts +25 -7
  46. package/dist/core/context.js +46 -0
  47. package/dist/core/prompt-composer.d.ts +8 -16
  48. package/dist/core/prompt-composer.js +65 -25
  49. package/dist/core/resources.d.ts +39 -0
  50. package/dist/core/resources.js +2 -0
  51. package/dist/core/runtime.d.ts +6 -1
  52. package/dist/core/runtime.js +15 -1
  53. package/dist/core/session.d.ts +2 -0
  54. package/dist/core/session.js +5 -0
  55. package/dist/core/transcript-codec.d.ts +70 -0
  56. package/dist/core/transcript-codec.js +60 -0
  57. package/dist/core/transcript-event.d.ts +33 -0
  58. package/dist/core/transcript-event.js +198 -0
  59. package/dist/persistence/claude-job-store.d.ts +58 -0
  60. package/dist/persistence/claude-job-store.js +483 -1
  61. package/dist/persistence/data-plane-adapter.d.ts +32 -0
  62. package/dist/persistence/data-plane-adapter.js +2 -0
  63. package/dist/persistence/data-plane.d.ts +4 -20
  64. package/dist/persistence/data-plane.js +10 -47
  65. package/dist/persistence/native-data-plane-adapter.d.ts +9 -0
  66. package/dist/persistence/native-data-plane-adapter.js +39 -0
  67. package/dist/persistence/native-resources.d.ts +4 -4
  68. package/dist/persistence/native-transcript-codec.d.ts +13 -0
  69. package/dist/persistence/native-transcript-codec.js +102 -0
  70. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  71. package/dist/persistence/native-transcript-reader.js +283 -0
  72. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  73. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  74. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  75. package/dist/platform/exclusive-file-lease.js +3 -0
  76. package/dist/tools/claude-user-message.d.ts +0 -1
  77. package/dist/tools/claude-user-message.js +0 -1
  78. package/package.json +6 -2
@@ -5,7 +5,7 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, } from
5
5
  import { isPathWithin } from '../platform/path-containment.js';
6
6
  import { AGENT_COLOR_DEFAULT, agentColorMessage, getClaudeEffectiveAgentColor, } from '../compatibility/claude/agent-color.js';
7
7
  import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
8
- import { discoverClaudeProjectRoot, isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
8
+ import { discoverClaudeProjectRoot, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
9
9
  import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
10
10
  import { resolveDataPlanePaths, resolveScheduledTaskFile, } from '../persistence/data-plane.js';
11
11
  import { downloadClaudeFileResources, } from '../compatibility/claude/file-resources.js';
@@ -23,17 +23,24 @@ import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
23
23
  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
- import { ContextBudget, ContextRecoveryPlanner, contextRecoveryMadeProgress, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
27
- import { injectFirstUserMessageContext, } from '../core/context.js';
26
+ import { isSessionId } from '../core/session.js';
27
+ import { ContextBudget, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
28
+ import { ContextEngine } from './context-engine.js';
29
+ import { TurnMemoryCoordinator } from './turn-memory-coordinator.js';
30
+ import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
31
+ import { assembleContextSnapshot } from '../core/prompt-composer.js';
28
32
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
29
33
  import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
30
34
  import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
31
35
  import { ClaudeSessionIndexCandidateError, readClaudeSessionIndexes, } from '../persistence/claude-session-index.js';
36
+ import { readNativeTranscript, readNativeTranscriptIndexes, exportNativeTranscript, NativeTranscriptIndexCandidateError, } from '../persistence/native-transcript-reader.js';
37
+ import { lastUserPrompt, projectTranscriptDisplay, } from './transcript-projection.js';
32
38
  import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
33
39
  import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
34
40
  import { ModelCompactor } from './model-compactor.js';
35
41
  import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
36
42
  import { ScheduledPromptManager, } from './scheduled-prompt-manager.js';
43
+ import { TurnTerminalController } from './turn-lifecycle.js';
37
44
  import { ClaudeScheduledToolRegistry } from '../tools/claude-scheduled-tools.js';
38
45
  import { ClaudeTaskToolRegistry } from '../tools/claude-task-tools.js';
39
46
  import { ClaudeWorkflowToolRegistry } from '../tools/claude-workflow-tools.js';
@@ -46,7 +53,7 @@ import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, }
46
53
  import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
47
54
  import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
48
55
  import { generateToolUseSummary } from './tool-use-summary.js';
49
- import { ClaudeUserMessageToolRegistry, CLAUDE_USER_MESSAGE_PROMPT, } from '../tools/claude-user-message.js';
56
+ import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
50
57
  function agentPermissionMode(mode) {
51
58
  return mode === undefined || mode === 'manual' ? 'default' : mode;
52
59
  }
@@ -76,6 +83,42 @@ function mainAgentToolNames(tools, agent) {
76
83
  .map(({ name }) => name)
77
84
  .filter((name) => (!requested || requested.has(name)) && !disallowed.has(name));
78
85
  }
86
+ function sessionMetadataFields(metadata, options) {
87
+ const name = metadata.title ??
88
+ (options.agentNameFallback ? metadata.agentName : undefined);
89
+ const prLink = metadata.prLink;
90
+ return {
91
+ ...(name === undefined ? {} : { name }),
92
+ ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
93
+ ...(metadata.agentName === undefined
94
+ ? {}
95
+ : { agentName: metadata.agentName }),
96
+ ...(metadata.agentColor === undefined
97
+ ? {}
98
+ : { agentColor: metadata.agentColor }),
99
+ ...(metadata.agentSetting === undefined
100
+ ? {}
101
+ : { agentSetting: metadata.agentSetting }),
102
+ ...(metadata.permissionMode === undefined
103
+ ? {}
104
+ : { permissionMode: metadata.permissionMode }),
105
+ ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
106
+ ...(prLink === undefined
107
+ ? {}
108
+ : {
109
+ prNumber: prLink.prNumber,
110
+ ...(prLink.prUrl === undefined ? {} : { prUrl: prLink.prUrl }),
111
+ ...(prLink.prRepository === undefined
112
+ ? {}
113
+ : { prRepository: prLink.prRepository }),
114
+ }),
115
+ };
116
+ }
117
+ function isSessionCandidateError(error) {
118
+ return (error instanceof ClaudeSessionIndexCandidateError ||
119
+ error instanceof NativeTranscriptIndexCandidateError ||
120
+ ['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? ''));
121
+ }
79
122
  const emptyToolRegistry = {
80
123
  definitions: () => [],
81
124
  schedulingPolicy: () => ({ concurrency: 'exclusive' }),
@@ -1232,19 +1275,67 @@ export class ClaudeSessionService {
1232
1275
  }
1233
1276
  async run(prompt, signal, sessionId = randomUUID(), name, images, documents) {
1234
1277
  this.worktreeManager?.bindSession(sessionId);
1235
- return this.executeTurn(sessionId, prompt, false, signal, name, images, documents);
1278
+ return this.executeTurn({
1279
+ activation: {
1280
+ kind: 'start',
1281
+ sessionId,
1282
+ ...(name !== undefined ? { name } : {}),
1283
+ },
1284
+ submission: {
1285
+ kind: 'prompt',
1286
+ text: prompt,
1287
+ ...(images ? { images } : {}),
1288
+ ...(documents ? { documents } : {}),
1289
+ },
1290
+ ...(signal ? { signal } : {}),
1291
+ });
1236
1292
  }
1237
1293
  async runShell(command, signal, sessionId = randomUUID(), name) {
1238
1294
  this.worktreeManager?.bindSession(sessionId);
1239
- return this.executeTurn(sessionId, `! ${command}`, false, signal, name, [], [], undefined, command);
1295
+ return this.executeTurn({
1296
+ activation: {
1297
+ kind: 'start',
1298
+ sessionId,
1299
+ ...(name !== undefined ? { name } : {}),
1300
+ },
1301
+ submission: { kind: 'shell', command },
1302
+ ...(signal ? { signal } : {}),
1303
+ });
1240
1304
  }
1241
1305
  async resume(sessionId, prompt, signal, name, images, documents, resumeSessionAt) {
1242
1306
  this.worktreeManager?.bindSession(sessionId);
1243
- return this.executeTurn(sessionId, prompt, true, signal, name, images, documents, resumeSessionAt);
1307
+ return this.executeTurn({
1308
+ activation: {
1309
+ kind: 'resume',
1310
+ sessionId,
1311
+ ...(name !== undefined ? { name } : {}),
1312
+ ...(resumeSessionAt !== undefined
1313
+ ? { atMessageId: resumeSessionAt }
1314
+ : {}),
1315
+ },
1316
+ submission: {
1317
+ kind: 'prompt',
1318
+ text: prompt,
1319
+ ...(images ? { images } : {}),
1320
+ ...(documents ? { documents } : {}),
1321
+ },
1322
+ ...(signal ? { signal } : {}),
1323
+ });
1244
1324
  }
1245
1325
  async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
1246
1326
  this.worktreeManager?.bindSession(sessionId);
1247
- return this.executeTurn(sessionId, `! ${command}`, true, signal, name, [], [], resumeSessionAt, command);
1327
+ return this.executeTurn({
1328
+ activation: {
1329
+ kind: 'resume',
1330
+ sessionId,
1331
+ ...(name !== undefined ? { name } : {}),
1332
+ ...(resumeSessionAt !== undefined
1333
+ ? { atMessageId: resumeSessionAt }
1334
+ : {}),
1335
+ },
1336
+ submission: { kind: 'shell', command },
1337
+ ...(signal ? { signal } : {}),
1338
+ });
1248
1339
  }
1249
1340
  async answerSideQuestion(sessionId, question, signal, onDelta, permissionMode = 'default') {
1250
1341
  const prompt = question.trim();
@@ -1270,16 +1361,17 @@ export class ClaudeSessionService {
1270
1361
  entries = loaded.value.entries;
1271
1362
  this.restoreWorktree(entries);
1272
1363
  }
1273
- const assembledContext = await this.options.contextAssembler?.assemble({
1364
+ const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
1274
1365
  cwd: this.activeCwd(),
1275
1366
  lifecycleId: activeSessionId,
1276
1367
  });
1368
+ const contextProjection = projectContextSnapshot(assembledContext);
1277
1369
  const messages = [
1278
- ...(assembledContext?.systemMessages ?? []),
1370
+ ...contextProjection.systemMessages,
1279
1371
  ...injectFirstUserMessageContext([
1280
1372
  ...projectClaudeModelMessages(entries),
1281
1373
  { role: 'user', content: prompt },
1282
- ], assembledContext?.firstUserMessageContext),
1374
+ ], contextProjection.firstUserMessageContext),
1283
1375
  ];
1284
1376
  const budget = this.contextBudget(provider);
1285
1377
  if (budget)
@@ -1290,6 +1382,7 @@ export class ClaudeSessionService {
1290
1382
  : 'praxis/provider';
1291
1383
  const metrics = await completeMeteredModelRequest(provider, {
1292
1384
  messages,
1385
+ stableSystemMessageCount: contextProjection.stableSystemSectionCount,
1293
1386
  ...(this.options.effort ? { effort: this.options.effort } : {}),
1294
1387
  ...(signal ? { signal } : {}),
1295
1388
  }, {
@@ -1430,27 +1523,21 @@ export class ClaudeSessionService {
1430
1523
  const provider = this.providerForAgent(agent);
1431
1524
  this.activeProvider = provider;
1432
1525
  const agentSystem = await this.mainAgentSystemPrompt(agent);
1433
- const assembledContext = await this.options.contextAssembler?.assemble({
1526
+ const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
1434
1527
  cwd: this.activeCwd(),
1435
1528
  lifecycleId: sessionId,
1436
- ...(agentSystem ? { mode: 'agent', baseSystemPrompt: agentSystem } : {}),
1529
+ ...(agentSystem
1530
+ ? { mode: 'agent', baseSystemPrompt: agentSystem }
1531
+ : {}),
1437
1532
  });
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
- ];
1533
+ const contextProjection = projectContextSnapshot(assembledContext);
1534
+ const contextMessages = [...contextProjection.systemMessages];
1448
1535
  const messages = [
1449
1536
  ...contextMessages,
1450
1537
  ...injectFirstUserMessageContext([
1451
1538
  ...projectClaudeModelMessages(entries.entries),
1452
1539
  { role: 'user', content: PROMPT_SUGGESTION_INSTRUCTION },
1453
- ], assembledContext?.firstUserMessageContext),
1540
+ ], contextProjection.firstUserMessageContext),
1454
1541
  ];
1455
1542
  const suggestionTools = agent && this.options.tools
1456
1543
  ? new FilteredToolRegistry(this.options.tools, {
@@ -1460,6 +1547,7 @@ export class ClaudeSessionService {
1460
1547
  await this.activateSessionCostTracker(sessionId);
1461
1548
  const metrics = await completeMeteredModelRequest(provider, {
1462
1549
  messages,
1550
+ stableSystemMessageCount: contextProjection.stableSystemSectionCount,
1463
1551
  ...(provider.capabilities.tools
1464
1552
  ? { tools: suggestionTools?.definitions() ?? [] }
1465
1553
  : {}),
@@ -1507,19 +1595,62 @@ export class ClaudeSessionService {
1507
1595
  }
1508
1596
  catch (error) {
1509
1597
  if (error.code === 'ENOENT')
1510
- return [];
1511
- throw error;
1598
+ names = [];
1599
+ else
1600
+ throw error;
1512
1601
  }
1513
1602
  const discoveredSessionIds = names
1514
1603
  .filter((name) => extname(name) === '.jsonl')
1515
1604
  .map((name) => basename(name, '.jsonl'))
1516
- .filter((sessionId) => isClaudeSessionId(sessionId));
1605
+ .filter((sessionId) => isSessionId(sessionId));
1517
1606
  const sessionIds = [
1518
1607
  ...new Set([
1519
1608
  ...discoveredSessionIds,
1520
1609
  ...this.explicitSessionFiles.keys(),
1521
1610
  ]),
1522
1611
  ];
1612
+ if (this.options.dataPlane === 'native') {
1613
+ const nativeResults = await readNativeTranscriptIndexes(sessionIds.map((sessionId) => ({
1614
+ sessionId,
1615
+ path: this.explicitSessionFiles.get(sessionId) ??
1616
+ join(projectRoot, `${sessionId}.jsonl`),
1617
+ })));
1618
+ const legacyResults = await readClaudeSessionIndexes(nativeResults.flatMap((result) => 'result' in result && result.result.format === 'legacy'
1619
+ ? [{ sessionId: result.sessionId, path: result.path }]
1620
+ : []), this.schema);
1621
+ const legacyBySessionId = new Map(legacyResults.map((result) => [result.sessionId, result]));
1622
+ const nativeSummaries = nativeResults.map((result) => {
1623
+ try {
1624
+ if ('error' in result)
1625
+ throw result.error;
1626
+ const file = result.result;
1627
+ if (file.format === 'legacy') {
1628
+ const legacy = legacyBySessionId.get(result.sessionId);
1629
+ if (!legacy)
1630
+ throw new Error(`Missing legacy transcript index for ${result.sessionId}`);
1631
+ if ('error' in legacy)
1632
+ throw legacy.error;
1633
+ return this.claudeSessionSummary(result.sessionId, legacy.index);
1634
+ }
1635
+ return {
1636
+ sessionId: result.sessionId,
1637
+ lastPrompt: lastUserPrompt(file.records.map((record) => record.event)),
1638
+ updatedAt: file.updatedAt,
1639
+ status: this.nativeSessionStatus(file.issue, file.records.length),
1640
+ issue: file.issue,
1641
+ };
1642
+ }
1643
+ catch (error) {
1644
+ if (isSessionCandidateError(error))
1645
+ return null;
1646
+ throw error;
1647
+ }
1648
+ });
1649
+ return nativeSummaries
1650
+ .filter((summary) => summary !== null)
1651
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) ||
1652
+ left.sessionId.localeCompare(right.sessionId));
1653
+ }
1523
1654
  if (discoveredRoot !== undefined) {
1524
1655
  for (const sessionId of discoveredSessionIds) {
1525
1656
  this.discoveredProjectRoots.set(sessionId, projectRoot);
@@ -1534,46 +1665,11 @@ export class ClaudeSessionService {
1534
1665
  try {
1535
1666
  if ('error' in result)
1536
1667
  throw result.error;
1537
- const { sessionId, index } = result;
1538
- const metadata = reduceClaudeSessionMetadata(index.entries, sessionId);
1539
- const name = metadata.title ?? metadata.agentName;
1540
- const prLink = metadata.prLink;
1541
- return {
1542
- sessionId,
1543
- ...(name === undefined ? {} : { name }),
1544
- ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
1545
- ...(metadata.agentName === undefined
1546
- ? {}
1547
- : { agentName: metadata.agentName }),
1548
- ...(metadata.agentColor === undefined
1549
- ? {}
1550
- : { agentColor: metadata.agentColor }),
1551
- ...(metadata.agentSetting === undefined
1552
- ? {}
1553
- : { agentSetting: metadata.agentSetting }),
1554
- ...(metadata.permissionMode === undefined
1555
- ? {}
1556
- : { permissionMode: metadata.permissionMode }),
1557
- ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
1558
- lastPrompt: metadata.lastPrompt ?? null,
1559
- updatedAt: index.updatedAt,
1560
- status: this.sessionStatus(index.issue, index.entries.length),
1561
- issue: index.issue,
1562
- ...(prLink
1563
- ? {
1564
- prNumber: prLink.prNumber,
1565
- prUrl: prLink.prUrl,
1566
- prRepository: prLink.prRepository,
1567
- }
1568
- : {}),
1569
- };
1668
+ return this.claudeSessionSummary(result.sessionId, result.index);
1570
1669
  }
1571
1670
  catch (error) {
1572
- if (error instanceof ClaudeSessionIndexCandidateError)
1671
+ if (isSessionCandidateError(error))
1573
1672
  return null;
1574
- if (['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? '')) {
1575
- return null;
1576
- }
1577
1673
  throw error;
1578
1674
  }
1579
1675
  });
@@ -1584,6 +1680,37 @@ export class ClaudeSessionService {
1584
1680
  }
1585
1681
  async inspect(sessionId) {
1586
1682
  this.assertSessionPersistence();
1683
+ if (this.options.dataPlane === 'native') {
1684
+ const paths = this.paths(sessionId);
1685
+ const file = await readNativeTranscript(paths.sessionFile);
1686
+ if (file.format === 'legacy') {
1687
+ const recovery = await this.store(sessionId).loadReadOnly();
1688
+ const metadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
1689
+ return {
1690
+ sessionId,
1691
+ ...sessionMetadataFields(metadata, { agentNameFallback: false }),
1692
+ lastPrompt: metadata.lastPrompt ?? null,
1693
+ updatedAt: file.updatedAt,
1694
+ status: this.sessionStatus(recovery.issue, recovery.entries.length),
1695
+ issue: recovery.issue,
1696
+ writeMode: this.schema.writeMode,
1697
+ entryCount: recovery.entries.length,
1698
+ byteLength: file.byteLength,
1699
+ newlineTerminated: file.newlineTerminated,
1700
+ };
1701
+ }
1702
+ return {
1703
+ sessionId,
1704
+ lastPrompt: lastUserPrompt(file.records.map((record) => record.event)),
1705
+ updatedAt: file.updatedAt,
1706
+ status: this.nativeSessionStatus(file.issue, file.records.length),
1707
+ issue: file.issue,
1708
+ writeMode: file.writeMode,
1709
+ entryCount: file.records.length,
1710
+ byteLength: file.byteLength,
1711
+ newlineTerminated: file.newlineTerminated,
1712
+ };
1713
+ }
1587
1714
  await this.discoverProjectRoot(sessionId);
1588
1715
  const paths = this.paths(sessionId);
1589
1716
  let metadata;
@@ -1598,30 +1725,9 @@ export class ClaudeSessionService {
1598
1725
  }
1599
1726
  const recovery = await this.store(sessionId).loadReadOnly();
1600
1727
  const sessionMetadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
1601
- const prLink = sessionMetadata.prLink;
1602
1728
  return {
1603
1729
  sessionId,
1604
- ...(sessionMetadata.title === undefined
1605
- ? {}
1606
- : { name: sessionMetadata.title }),
1607
- ...(sessionMetadata.tag === undefined
1608
- ? {}
1609
- : { tag: sessionMetadata.tag }),
1610
- ...(sessionMetadata.agentName === undefined
1611
- ? {}
1612
- : { agentName: sessionMetadata.agentName }),
1613
- ...(sessionMetadata.agentColor === undefined
1614
- ? {}
1615
- : { agentColor: sessionMetadata.agentColor }),
1616
- ...(sessionMetadata.agentSetting === undefined
1617
- ? {}
1618
- : { agentSetting: sessionMetadata.agentSetting }),
1619
- ...(sessionMetadata.permissionMode === undefined
1620
- ? {}
1621
- : { permissionMode: sessionMetadata.permissionMode }),
1622
- ...(sessionMetadata.mode === undefined
1623
- ? {}
1624
- : { mode: sessionMetadata.mode }),
1730
+ ...sessionMetadataFields(sessionMetadata, { agentNameFallback: false }),
1625
1731
  lastPrompt: sessionMetadata.lastPrompt ?? null,
1626
1732
  updatedAt: metadata.mtime.toISOString(),
1627
1733
  status: this.sessionStatus(recovery.issue, recovery.entries.length),
@@ -1631,13 +1737,6 @@ export class ClaudeSessionService {
1631
1737
  entryCount: recovery.entries.length,
1632
1738
  byteLength: recovery.tail.byteLength,
1633
1739
  newlineTerminated: recovery.tail.newlineTerminated,
1634
- ...(prLink
1635
- ? {
1636
- prNumber: prLink.prNumber,
1637
- prUrl: prLink.prUrl,
1638
- prRepository: prLink.prRepository,
1639
- }
1640
- : {}),
1641
1740
  };
1642
1741
  }
1643
1742
  async readEffectiveAgentColor(sessionId) {
@@ -1647,6 +1746,8 @@ export class ClaudeSessionService {
1647
1746
  }
1648
1747
  async export(sessionId) {
1649
1748
  this.assertSessionPersistence();
1749
+ if (this.options.dataPlane === 'native')
1750
+ return exportNativeTranscript(this.paths(sessionId).sessionFile);
1650
1751
  await this.discoverProjectRoot(sessionId);
1651
1752
  try {
1652
1753
  return await this.store(sessionId).exportReadOnly();
@@ -1662,17 +1763,40 @@ export class ClaudeSessionService {
1662
1763
  this.assertSessionPersistence();
1663
1764
  const path = resolve(requestedPath);
1664
1765
  const pathMetadata = await lstat(path);
1766
+ const dataPlaneName = this.options.dataPlane === 'native' ? 'Native' : 'Claude';
1665
1767
  if (!pathMetadata.isFile()) {
1666
- throw new Error(`Claude resume path must be a regular JSONL file: ${path}`);
1768
+ throw new Error(`${dataPlaneName} resume path must be a regular JSONL file: ${path}`);
1667
1769
  }
1668
1770
  if (extname(path).toLowerCase() !== '.jsonl') {
1669
- throw new Error(`Claude resume path must end in .jsonl: ${path}`);
1771
+ throw new Error(`${dataPlaneName} resume path must end in .jsonl: ${path}`);
1670
1772
  }
1671
1773
  const sessionId = basename(path, '.jsonl');
1672
- if (!isClaudeSessionId(sessionId)) {
1673
- throw new Error(`Claude resume path filename must be a session UUID: ${path}`);
1774
+ if (!isSessionId(sessionId)) {
1775
+ throw new Error(`${dataPlaneName} resume path filename must be a session UUID: ${path}`);
1674
1776
  }
1675
1777
  const canonicalPath = await realpath(path);
1778
+ if (this.options.dataPlane === 'native') {
1779
+ const detected = await readNativeTranscript(canonicalPath);
1780
+ if (detected.format === 'native') {
1781
+ if (detected.issue?.kind === 'unsupported-version')
1782
+ throw new Error('Native resume transcript version is unsupported');
1783
+ if (detected.issue ||
1784
+ !detected.newlineTerminated ||
1785
+ detected.records.length === 0)
1786
+ throw new Error('Native resume transcript must be a complete non-empty newline-terminated file');
1787
+ if (detected.records.some((record) => record.event.sessionId !== sessionId))
1788
+ throw new Error('Native resume transcript contains a different sessionId');
1789
+ this.explicitSessionFiles.set(sessionId, canonicalPath);
1790
+ this.discoveredProjectRoots.set(sessionId, dirname(canonicalPath));
1791
+ return {
1792
+ sessionId,
1793
+ lastPrompt: lastUserPrompt(detected.records.map((record) => record.event)),
1794
+ updatedAt: detected.updatedAt,
1795
+ status: 'read-only',
1796
+ issue: detected.issue,
1797
+ };
1798
+ }
1799
+ }
1676
1800
  const exact = this.pathsForCwd(sessionId, this.activeCwd());
1677
1801
  const candidate = new ClaudeTranscriptStore({
1678
1802
  sessionFile: canonicalPath,
@@ -1686,6 +1810,10 @@ export class ClaudeSessionService {
1686
1810
  if (snapshot.entries.length === 0) {
1687
1811
  throw new Error('Claude resume transcript must not be empty');
1688
1812
  }
1813
+ if (this.options.dataPlane === 'native' &&
1814
+ this.schema.writeMode !== 'read-write') {
1815
+ throw new Error('Native legacy resume transcript is read-only');
1816
+ }
1689
1817
  let matchingSessionIdentity = false;
1690
1818
  for (const entry of snapshot.entries) {
1691
1819
  if (entry.sessionId === sessionId)
@@ -1703,40 +1831,21 @@ export class ClaudeSessionService {
1703
1831
  this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
1704
1832
  this.discoveredProjectRoots.set(sessionId, dirname(canonicalPath));
1705
1833
  const metadata = reduceClaudeSessionMetadata(snapshot.entries, sessionId);
1706
- const prLink = metadata.prLink;
1707
1834
  return {
1708
1835
  sessionId,
1709
- ...(metadata.title === undefined ? {} : { name: metadata.title }),
1710
- ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
1711
- ...(metadata.agentName === undefined
1712
- ? {}
1713
- : { agentName: metadata.agentName }),
1714
- ...(metadata.agentColor === undefined
1715
- ? {}
1716
- : { agentColor: metadata.agentColor }),
1717
- ...(metadata.agentSetting === undefined
1718
- ? {}
1719
- : { agentSetting: metadata.agentSetting }),
1720
- ...(metadata.permissionMode === undefined
1721
- ? {}
1722
- : { permissionMode: metadata.permissionMode }),
1723
- ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
1836
+ ...sessionMetadataFields(metadata, { agentNameFallback: false }),
1724
1837
  lastPrompt: metadata.lastPrompt ?? null,
1725
1838
  updatedAt: pathMetadata.mtime.toISOString(),
1726
1839
  status: this.sessionStatus(null, snapshot.entries.length),
1727
1840
  issue: null,
1728
- ...(prLink === undefined
1729
- ? {}
1730
- : {
1731
- prNumber: prLink.prNumber,
1732
- ...(prLink.prUrl === undefined ? {} : { prUrl: prLink.prUrl }),
1733
- ...(prLink.prRepository === undefined
1734
- ? {}
1735
- : { prRepository: prLink.prRepository }),
1736
- }),
1737
1841
  };
1738
1842
  }
1739
1843
  async transcript(sessionId, resumeSessionAt) {
1844
+ if (this.options.dataPlane === 'native') {
1845
+ const file = await readNativeTranscript(this.paths(sessionId).sessionFile);
1846
+ if (file.format === 'native')
1847
+ return projectTranscriptDisplay(file.records.map((record) => record.event), resumeSessionAt);
1848
+ }
1740
1849
  await this.discoverProjectRoot(sessionId);
1741
1850
  try {
1742
1851
  const recovery = await this.store(sessionId).loadReadOnly();
@@ -1987,7 +2096,11 @@ export class ClaudeSessionService {
1987
2096
  }
1988
2097
  async retryRecentlyDenied(sessionId, display, signal) {
1989
2098
  await this.appendPermissionGrant(sessionId, display, true);
1990
- return this.executeTurn(sessionId, '/permissions', true, signal, undefined, [], [], undefined, undefined, true);
2099
+ return this.executeTurn({
2100
+ activation: { kind: 'resume', sessionId },
2101
+ submission: { kind: 'retry', prompt: '/permissions' },
2102
+ ...(signal ? { signal } : {}),
2103
+ });
1991
2104
  }
1992
2105
  async recordBtwUsage(sessionId, permissionMode = 'default') {
1993
2106
  const activeSessionId = await this.ensureLocalSession(sessionId, permissionMode);
@@ -2320,6 +2433,7 @@ export class ClaudeSessionService {
2320
2433
  }
2321
2434
  async fork(parentSessionId, sessionId = randomUUID(), resumeSessionAt) {
2322
2435
  this.assertSessionPersistence();
2436
+ await this.assertNativeWriteTargetIsLegacy(parentSessionId);
2323
2437
  this.assertWritable();
2324
2438
  const entries = await this.nativeForkEntries(parentSessionId, sessionId, resumeSessionAt);
2325
2439
  const result = await this.store(sessionId).create(entries);
@@ -2633,1695 +2747,1658 @@ export class ClaudeSessionService {
2633
2747
  }
2634
2748
  return tracker.snapshot();
2635
2749
  }
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
- }
2675
- }
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
- }
2750
+ async executeTurn(request) {
2751
+ const { activation, submission, signal } = request;
2752
+ const sessionId = activation.sessionId;
2753
+ const requireExisting = activation.kind === 'resume';
2754
+ const name = activation.name;
2755
+ const resumeSessionAt = activation.kind === 'resume' ? activation.atMessageId : undefined;
2756
+ const prompt = submission.kind === 'shell'
2757
+ ? `! ${submission.command}`
2758
+ : submission.kind === 'retry'
2759
+ ? submission.prompt
2760
+ : submission.text;
2761
+ const images = submission.kind === 'prompt' ? (submission.images ?? []) : [];
2762
+ const documents = submission.kind === 'prompt' ? (submission.documents ?? []) : [];
2763
+ const shellCommand = submission.kind === 'shell' ? submission.command : undefined;
2764
+ const skipUserPrompt = submission.kind === 'retry';
2765
+ const controller = new TurnTerminalController(this.options.eventSink ?? (() => undefined));
2766
+ try {
2767
+ if (requireExisting)
2768
+ await this.assertNativeWriteTargetIsLegacy(sessionId);
2769
+ this.assertWritable();
2770
+ if (prompt.length === 0 && images.length === 0 && documents.length === 0)
2771
+ throw new Error('Prompt must not be empty');
2772
+ if (name !== undefined && name.length === 0) {
2773
+ throw new Error('Session name must not be empty');
2687
2774
  }
2688
- if (requireExisting && snapshot.entries.length === 0) {
2689
- throw new Error(`Claude session not found: ${sessionId}`);
2775
+ if (shellCommand !== undefined && shellCommand.trim().length === 0) {
2776
+ throw new Error('Shell command must not be empty');
2690
2777
  }
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
- };
2778
+ await this.activateSessionCostTracker(sessionId);
2779
+ if (this.options.sessionPersistence !== false) {
2780
+ this.durableMetadataSessions.add(sessionId);
2697
2781
  }
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
- };
2782
+ await this.ensureFileResources(sessionId, signal);
2783
+ this.worktreeManager?.bindSession(sessionId);
2784
+ if (this.options.initialWorktree) {
2785
+ await this.worktreeManager?.ensureInitial(this.options.initialWorktreeName);
2705
2786
  }
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;
2719
- snapshot = {
2720
- entries: interruption.replayEntries,
2721
- tail: {
2722
- ...snapshot.tail,
2723
- branchParentUuid: interruption.replayParentUuid ?? null,
2724
- },
2725
- };
2726
- }
2787
+ const pinnedCwd = this.sessionCwds.get(sessionId) ?? this.activeCwd();
2788
+ this.sessionCwds.set(sessionId, pinnedCwd);
2789
+ if (this.options.workspace?.cwd() !== pinnedCwd) {
2790
+ this.options.workspace?.setCwd(pinnedCwd);
2727
2791
  }
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);
2792
+ this.runtimeCwd = pinnedCwd;
2793
+ if (requireExisting) {
2794
+ await this.discoverProjectRoot(sessionId);
2741
2795
  }
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}`);
2796
+ const sessionPaths = this.paths(sessionId);
2797
+ const toolResultDirectory = join(sessionPaths.projectRoot, sessionId, 'tool-results');
2798
+ const store = this.turnStore(sessionId);
2799
+ const leaseResult = await store.withLease(async (lease) => {
2800
+ if (!requireExisting) {
2801
+ const initialization = name === undefined
2802
+ ? await store.reserve()
2803
+ : await store.create(this.sessionNameEntries(sessionId, name));
2804
+ if (initialization.status === 'conflict') {
2805
+ throw new Error(`Session ID ${sessionId} is already in use`);
2806
+ }
2807
+ }
2808
+ let snapshot = await lease.load();
2809
+ let automaticReplayPrompt;
2810
+ if (requireExisting &&
2811
+ this.options.sessionPersistence === false &&
2812
+ snapshot.entries.length === 0) {
2813
+ const persisted = await this.store(sessionId).load();
2814
+ if (persisted.entries.length > 0) {
2815
+ const imported = await store.create(persisted.entries);
2816
+ if (imported.status === 'created')
2817
+ snapshot = await lease.load();
2818
+ }
2819
+ }
2820
+ if (requireExisting && snapshot.entries.length === 0) {
2821
+ throw new Error(`Claude session not found: ${sessionId}`);
2751
2822
  }
2752
- snapshot = {
2753
- entries: [...snapshot.entries, ...entries],
2754
- tail: appendResult.tail,
2755
- };
2756
2823
  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 &&
2766
- 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 };
2824
+ if (resumeSessionAt !== undefined) {
2825
+ snapshot = {
2826
+ entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
2827
+ tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
2828
+ };
2808
2829
  }
2809
- };
2810
- const recordHookOutcome = async (outcome, deferUntilApproval = false) => {
2811
- if (deferUntilApproval) {
2812
- pendingRecoveryHookOutcomes.push(outcome);
2813
- return;
2830
+ else if (this.explicitResumeLeafUuids.has(sessionId)) {
2831
+ const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
2832
+ this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
2833
+ snapshot = {
2834
+ entries: selected.entries,
2835
+ tail: { ...snapshot.tail, branchParentUuid: selected.leafUuid },
2836
+ };
2814
2837
  }
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;
2838
+ if (requireExisting &&
2839
+ resumeSessionAt === undefined &&
2840
+ this.options.resumeInterruptedTurn === true &&
2841
+ shellCommand === undefined &&
2842
+ !skipUserPrompt &&
2843
+ !(this.options.approveRecovery !== undefined &&
2844
+ findUnresolvedClaudeToolCalls(snapshot.entries).length > 0)) {
2845
+ const interruption = classifyClaudeInterruption(snapshot.entries);
2846
+ if ((interruption.kind === 'interrupted-prompt' ||
2847
+ interruption.kind === 'interrupted-turn') &&
2848
+ interruption.prompt !== undefined &&
2849
+ interruption.replayEntries !== undefined) {
2850
+ automaticReplayPrompt = interruption.prompt;
2851
+ snapshot = {
2852
+ entries: interruption.replayEntries,
2853
+ tail: {
2854
+ ...snapshot.tail,
2855
+ branchParentUuid: interruption.replayParentUuid ?? null,
2856
+ },
2857
+ };
2858
+ }
2832
2859
  }
2833
- if (entries.length === 0) {
2834
- pendingRecoveryHookOutcomes.length = 0;
2835
- return;
2860
+ const initialTransition = this.worktreeManager?.consumeTransition('__initial__');
2861
+ if (initialTransition && snapshot.entries.length === 0) {
2862
+ const stateEntry = {
2863
+ type: 'worktree-state',
2864
+ worktreeSession: initialTransition.state,
2865
+ sessionId,
2866
+ };
2867
+ const stateTail = await this.append(lease, snapshot.tail, stateEntry);
2868
+ snapshot = {
2869
+ entries: [...snapshot.entries, stateEntry],
2870
+ tail: stateTail,
2871
+ };
2872
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
2836
2873
  }
2837
- const appendResult = await lease.appendMany(snapshot.tail, entries);
2838
- if (appendResult.status === 'conflict') {
2839
- throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
2874
+ this.restoreWorktree(snapshot.entries);
2875
+ this.options.interactiveTools?.restore(sessionId, snapshot.entries);
2876
+ if (requireExisting &&
2877
+ name !== undefined &&
2878
+ !this.hasSessionName(snapshot.entries, name)) {
2879
+ const entries = this.sessionNameEntries(sessionId, name);
2880
+ const appendResult = await lease.appendMany(snapshot.tail, entries);
2881
+ if (appendResult.status === 'conflict') {
2882
+ throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
2883
+ }
2884
+ snapshot = {
2885
+ entries: [...snapshot.entries, ...entries],
2886
+ tail: appendResult.tail,
2887
+ };
2888
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
2840
2889
  }
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
- }
2891
- },
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,
2890
+ const agentName = this.options.agent ?? getClaudeAgentSetting(snapshot.entries);
2891
+ const agent = this.resolveAgent(agentName);
2892
+ const provider = this.providerForAgent(agent);
2893
+ this.activeProvider = provider;
2894
+ const effectivePrompt = automaticReplayPrompt ??
2895
+ (!requireExisting &&
2896
+ !skipUserPrompt &&
2897
+ !this.options.agentInitialPromptHandledExternally &&
2898
+ shellCommand === undefined &&
2899
+ agent?.initialPrompt
2900
+ ? `${agent.initialPrompt}\n\n${prompt}`
2901
+ : prompt);
2902
+ const sessionMemory = this.sessionMemoryController(sessionId);
2903
+ const turnMemory = new TurnMemoryCoordinator({
2903
2904
  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
2928
- ? {
2929
- permissionResolverForMode: this.options.permissionResolverForMode,
2930
- }
2931
- : {}),
2932
- parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
2933
- this.options.permissionMode),
2934
- ...(this.options.subagentToolNames
2935
- ? {
2936
- toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
2937
- }
2938
- : {}),
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
2949
- ? {}
2950
- : {
2951
- contextReserveTokens: this.options.contextBudget?.reserveTokens ??
2952
- this.options.contextReserveTokens,
2953
- }),
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);
2965
- },
2966
- ...(this.options.eventSink
2967
- ? { eventSink: this.options.eventSink }
2968
- : {}),
2969
- ...(taskTools
2970
- ? {
2971
- backgroundTaskNotifications: (waitForRunning) => taskTools.notifications(waitForRunning),
2972
- }
2905
+ ...(sessionMemory ? { session: sessionMemory } : {}),
2906
+ ...(this.options.projectMemoryRecall
2907
+ ? { projectRecall: this.options.projectMemoryRecall }
2973
2908
  : {}),
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 }
2909
+ ...(this.options.projectMemoryExtraction
2910
+ ? { projectExtraction: this.options.projectMemoryExtraction }
3025
2911
  : {}),
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');
3058
- }
3059
- const prepared = await fileHistory.prepareMutation(snapshot.entries, snapshotMessageId, path);
3060
- let result;
3061
- try {
3062
- result = await interactiveMessageTools.execute(call, context);
3063
- }
3064
- catch (error) {
3065
- await prepared.rollback();
3066
- throw error;
3067
- }
3068
- if (result.isError) {
3069
- await prepared.rollback();
3070
- return result;
3071
- }
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 };
3076
- }
3077
- return result;
3078
- },
2912
+ warn: (message) => this.options.eventSink?.({ type: 'warning', message }),
2913
+ });
2914
+ if (!skipUserPrompt && shellCommand === undefined) {
2915
+ turnMemory.prefetch({
2916
+ turnId: randomUUID(),
2917
+ prompt: effectivePrompt,
2918
+ ...(signal ? { signal } : {}),
2919
+ });
3079
2920
  }
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 }),
3105
- }
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)),
2921
+ const initialPricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
2922
+ if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
2923
+ throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
2924
+ }
2925
+ const shellInputUuid = shellCommand === undefined ? null : randomUUID();
2926
+ let currentPromptId = shellInputUuid;
2927
+ let lastAssistantUuid = null;
2928
+ const fileHistory = this.options.fileCheckpointing &&
2929
+ this.options.sessionPersistence !== false
2930
+ ? new ClaudeFileHistory(this.options.configRoot, sessionId, [
2931
+ this.activeCwd(),
2932
+ ...(this.options.fileRewindRoots ?? []),
2933
+ ])
2934
+ : null;
2935
+ const unresolvedToolCalls = findUnresolvedClaudeToolCalls(snapshot.entries);
2936
+ const pendingRecoveryToolCallIds = new Set(unresolvedToolCalls.map((call) => call.id));
2937
+ const pendingRecoveryHookOutcomes = [];
2938
+ const currentHookCwd = () => this.activeCwd();
2939
+ const hookSession = {
2940
+ session_id: sessionId,
2941
+ transcript_path: sessionPaths.sessionFile,
2942
+ get cwd() {
2943
+ return currentHookCwd();
2944
+ },
2945
+ permission_mode: 'default',
2946
+ };
2947
+ const appendHookOutcome = async (outcome) => {
2948
+ for (const entry of createClaudeHookAttachmentEntries(outcome, this.translationContext(sessionId, snapshot))) {
2949
+ const tail = await this.append(lease, snapshot.tail, entry);
2950
+ snapshot = { entries: [...snapshot.entries, entry], tail };
3115
2951
  }
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
- },
2952
+ };
2953
+ const recordHookOutcome = async (outcome, deferUntilApproval = false) => {
2954
+ if (deferUntilApproval) {
2955
+ pendingRecoveryHookOutcomes.push(outcome);
2956
+ return;
3123
2957
  }
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',
2958
+ await appendHookOutcome(outcome);
2959
+ };
2960
+ const flushRecoveryHookOutcomes = async () => {
2961
+ const entries = [];
2962
+ let history = snapshot.entries;
2963
+ let parentUuid = this.logicalTailUuid(snapshot.tail);
2964
+ for (const outcome of pendingRecoveryHookOutcomes) {
2965
+ const outcomeEntries = createClaudeHookAttachmentEntries(outcome, {
2966
+ ...this.translationContext(sessionId, snapshot),
2967
+ parentUuid,
2968
+ history,
3167
2969
  });
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
- };
2970
+ entries.push(...outcomeEntries);
2971
+ history = [...history, ...outcomeEntries];
2972
+ const lastEntry = outcomeEntries.at(-1);
2973
+ if (typeof lastEntry?.uuid === 'string')
2974
+ parentUuid = lastEntry.uuid;
3178
2975
  }
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
- };
2976
+ if (entries.length === 0) {
2977
+ pendingRecoveryHookOutcomes.length = 0;
2978
+ return;
3191
2979
  }
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
- : {}),
2980
+ const appendResult = await lease.appendMany(snapshot.tail, entries);
2981
+ if (appendResult.status === 'conflict') {
2982
+ throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
2983
+ }
2984
+ snapshot = { entries: history, tail: appendResult.tail };
2985
+ pendingRecoveryHookOutcomes.length = 0;
2986
+ };
2987
+ const capabilities = this.toolCapabilities();
2988
+ const taskToolNames = this.capabilityToolNames(this.options.taskToolNames, capabilities);
2989
+ const scheduledToolNames = this.capabilityToolNames(this.options.scheduledToolNames, capabilities);
2990
+ const taskTools = this.options.tools && taskToolNames.length > 0
2991
+ ? new ClaudeTaskToolRegistry({
2992
+ base: this.options.tools,
2993
+ cwd: this.activeCwd(),
2994
+ cwdProvider: () => this.activeCwd(),
2995
+ praxisRoot: sessionPaths.praxisRoot,
2996
+ sessionId,
2997
+ taskRoot: sessionPaths.taskRoot,
2998
+ ...(this.options.eventSink
2999
+ ? { eventSink: this.options.eventSink }
3000
+ : {}),
3001
+ enabledTools: taskToolNames,
3002
+ ...(this.options.hooks
3003
+ ? {
3004
+ taskHooks: {
3005
+ created: async (task, taskSignal) => {
3006
+ const outcome = await this.options.hooks?.run({
3007
+ ...hookSession,
3008
+ hook_event_name: 'TaskCreated',
3009
+ task_id: task.id,
3010
+ task_subject: task.subject,
3011
+ task_description: task.description,
3012
+ }, undefined, taskSignal);
3013
+ if (!outcome)
3014
+ return;
3015
+ await recordHookOutcome(outcome);
3016
+ if (outcome.blockedReason) {
3017
+ throw new Error(`TaskCreated hook error: ${outcome.blockedReason}`);
3018
+ }
3019
+ },
3020
+ completed: async (task, taskSignal) => {
3021
+ const outcome = await this.options.hooks?.run({
3022
+ ...hookSession,
3023
+ hook_event_name: 'TaskCompleted',
3024
+ task_id: task.id,
3025
+ task_subject: task.subject,
3026
+ task_description: task.description,
3027
+ }, undefined, taskSignal);
3028
+ if (!outcome)
3029
+ return;
3030
+ await recordHookOutcome(outcome);
3031
+ if (outcome.blockedReason) {
3032
+ throw new Error(`TaskCompleted hook error: ${outcome.blockedReason}`);
3033
+ }
3034
+ },
3035
+ },
3036
+ }
3037
+ : {}),
3038
+ })
3039
+ : null;
3040
+ const scheduledTools = this.scheduledPrompts &&
3041
+ this.options.tools &&
3042
+ scheduledToolNames.length > 0
3043
+ ? new ClaudeScheduledToolRegistry({
3044
+ base: taskTools ?? this.options.tools,
3045
+ manager: this.scheduledPrompts,
3046
+ sessionId,
3047
+ enabledTools: scheduledToolNames,
3048
+ dataPlane: this.options.dataPlane ?? 'claude',
3049
+ })
3050
+ : null;
3051
+ const baseTools = scheduledTools ?? taskTools ?? this.options.tools;
3052
+ const turnPermissions = this.options.interactiveTools?.permissions(sessionId) ??
3053
+ this.options.permissions;
3054
+ const subagentExecutor = (this.options.enableSubagents || this.options.enableWorkflows) &&
3055
+ baseTools &&
3056
+ turnPermissions
3057
+ ? new ClaudeSubagentExecutor({
3058
+ configRoot: this.options.configRoot,
3059
+ dataPlane: this.options.dataPlane ?? 'claude',
3060
+ cwd: this.activeCwd(),
3061
+ cwdProvider: () => this.activeCwd(),
3062
+ claudeVersion: this.options.claudeVersion,
3063
+ provider,
3064
+ persistence: this.options.sessionPersistence === false ? 'memory' : 'disk',
3065
+ ...(this.options.providerForModel
3066
+ ? { providerForModel: this.options.providerForModel }
3067
+ : {}),
3068
+ baseTools,
3069
+ permissions: turnPermissions,
3070
+ ...(this.options.permissionResolverForMode
3071
+ ? {
3072
+ permissionResolverForMode: this.options.permissionResolverForMode,
3073
+ }
3074
+ : {}),
3075
+ parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
3076
+ this.options.permissionMode),
3077
+ ...(this.options.subagentToolNames
3078
+ ? {
3079
+ toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
3080
+ }
3081
+ : {}),
3082
+ ...(this.options.extensions
3083
+ ? { extensions: this.options.extensions }
3084
+ : {}),
3085
+ ...(this.options.mcp ? { mcp: this.options.mcp } : {}),
3086
+ ...(this.options.hooks ? { hooks: this.options.hooks } : {}),
3087
+ ...(this.options.contextAssembler
3088
+ ? { contextAssembler: this.options.contextAssembler }
3089
+ : {}),
3090
+ ...((this.options.contextBudget?.reserveTokens ??
3091
+ this.options.contextReserveTokens) === undefined
3092
+ ? {}
3093
+ : {
3094
+ contextReserveTokens: this.options.contextBudget?.reserveTokens ??
3095
+ this.options.contextReserveTokens,
3096
+ }),
3097
+ ...(this.options.approveTool
3098
+ ? { approveTool: this.options.approveTool }
3099
+ : {}),
3100
+ permissionUpdates: () => this.sessionPermissionUpdates.get(sessionId) ?? [],
3101
+ onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
3102
+ onLineChanges: (changes) => {
3103
+ const tracker = this.sessionCostTrackers.get(sessionId);
3104
+ if (!tracker) {
3105
+ throw new Error(`Session cost tracker is not active for session ${sessionId}`);
3106
+ }
3107
+ tracker.recordLineChanges(changes);
3211
3108
  },
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);
3109
+ ...(this.options.eventSink
3110
+ ? { eventSink: this.options.eventSink }
3111
+ : {}),
3112
+ ...(taskTools
3113
+ ? {
3114
+ backgroundTaskNotifications: (waitForRunning) => taskTools.notifications(waitForRunning),
3226
3115
  }
3227
- else if (call.name === 'Write' || call.name === 'Edit') {
3228
- projectMemoryMaintained = true;
3116
+ : {}),
3117
+ notificationDelivered: (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification),
3118
+ stopOwnedBackgroundAgent: (ownerSessionId, agentId) => this.backgroundTasks.stopAgent(ownerSessionId, agentId),
3119
+ outputOwnedBackgroundAgent: (ownerSessionId, agentId, options) => this.backgroundTasks.outputAgent(ownerSessionId, agentId, options),
3120
+ sendOwnedBackgroundAgent: (ownerSessionId, agentId, message, summary, toolUseId) => this.sendOwnedBackgroundAgent(ownerSessionId, agentId, message, summary, toolUseId),
3121
+ })
3122
+ : null;
3123
+ if (subagentExecutor) {
3124
+ this.subagentExecutors.add(subagentExecutor);
3125
+ this.subagentExecutorSessions.set(subagentExecutor, sessionId);
3126
+ this.backgroundTasks.registerAgents(sessionId, subagentExecutor);
3127
+ }
3128
+ const agentTools = subagentExecutor
3129
+ ? subagentExecutor.registry(sessionId, 0, (callId) => currentPromptId ??
3130
+ this.promptIdForToolCall(snapshot.entries, callId))
3131
+ : baseTools;
3132
+ const turnTools = this.workflowManager && subagentExecutor && agentTools
3133
+ ? new ClaudeWorkflowToolRegistry({
3134
+ base: agentTools,
3135
+ manager: this.workflowManager,
3136
+ executor: subagentExecutor,
3137
+ cwd: this.activeCwd(),
3138
+ cwdProvider: () => this.activeCwd(),
3139
+ configRoot: this.options.configRoot,
3140
+ sessionId,
3141
+ promptIdForCall: (callId) => currentPromptId ??
3142
+ this.promptIdForToolCall(snapshot.entries, callId),
3143
+ defaultModel: provider.model ?? 'praxis/provider',
3144
+ tokenBudget: workflowTokenTarget(effectivePrompt),
3145
+ enabled: capabilities.has('Workflow'),
3146
+ dataPlane: this.options.dataPlane ?? 'claude',
3147
+ })
3148
+ : agentTools;
3149
+ const workspaceTools = this.worktreeManager &&
3150
+ turnTools &&
3151
+ this.options.workspace &&
3152
+ (this.options.worktreeToolNames?.length ?? 0) > 0
3153
+ ? new ClaudeWorktreeToolRegistry({
3154
+ base: turnTools,
3155
+ manager: this.worktreeManager,
3156
+ workspace: this.options.workspace,
3157
+ enabledTools: this.options.worktreeToolNames ?? [],
3158
+ dataPlane: this.options.dataPlane ?? 'claude',
3159
+ })
3160
+ : turnTools;
3161
+ const messageTools = this.options.brief && workspaceTools
3162
+ ? new ClaudeUserMessageToolRegistry(workspaceTools, (message) => this.options.eventSink?.({
3163
+ type: 'user-message',
3164
+ message: message.message,
3165
+ status: message.status,
3166
+ ...(message.attachments.length
3167
+ ? { attachments: message.attachments }
3168
+ : {}),
3169
+ }))
3170
+ : workspaceTools;
3171
+ const interactiveMessageTools = this.options.interactiveTools && messageTools
3172
+ ? this.options.interactiveTools.registry(messageTools, sessionId)
3173
+ : messageTools;
3174
+ const fileHistoryTools = fileHistory && interactiveMessageTools
3175
+ ? {
3176
+ definitions: () => interactiveMessageTools.definitions(),
3177
+ schedulingPolicy: (call) => ({
3178
+ ...resolveToolSchedulingPolicy(interactiveMessageTools, call),
3179
+ startAfterAssistant: true,
3180
+ }),
3181
+ prepare: (call, context) => interactiveMessageTools.prepare(call, context),
3182
+ execute: async (call, context) => {
3183
+ const path = call.name === 'Write' || call.name === 'Edit'
3184
+ ? call.input.file_path
3185
+ : call.name === 'NotebookEdit'
3186
+ ? call.input.notebook_path
3187
+ : undefined;
3188
+ if (typeof path !== 'string') {
3189
+ return interactiveMessageTools.execute(call, context);
3229
3190
  }
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
- }
3191
+ if ((call.name === 'Write' || call.name === 'Edit') &&
3192
+ (await this.options.interactiveTools?.isPlanFile(sessionId, path))) {
3193
+ return interactiveMessageTools.execute(call, context);
3237
3194
  }
3238
- }
3239
- }
3240
- if (toolResult.isError ||
3241
- call.name !== 'Read' ||
3242
- !this.options.conditionalRuleResolver ||
3243
- !toolResult.accessedPaths) {
3244
- return;
3245
- }
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
- }
3195
+ const snapshotMessageId = currentPromptId ??
3196
+ this.promptIdForToolCall(snapshot.entries, call.id);
3197
+ const assistantMessageId = lastAssistantUuid ??
3198
+ this.assistantIdForToolCall(snapshot.entries, call.id);
3199
+ if (!snapshotMessageId || !assistantMessageId) {
3200
+ throw new Error('Claude file history could not link tool call');
3201
+ }
3202
+ const prepared = await fileHistory.prepareMutation(snapshot.entries, snapshotMessageId, path);
3203
+ let result;
3204
+ try {
3205
+ result = await interactiveMessageTools.execute(call, context);
3206
+ }
3207
+ catch (error) {
3208
+ await prepared.rollback();
3209
+ throw error;
3210
+ }
3211
+ if (result.isError) {
3212
+ await prepared.rollback();
3213
+ return result;
3214
+ }
3215
+ const entry = prepared.commit(assistantMessageId);
3216
+ if (entry) {
3217
+ const tail = await this.append(lease, snapshot.tail, entry);
3218
+ snapshot = { entries: [...snapshot.entries, entry], tail };
3219
+ }
3220
+ return result;
3221
+ },
3271
3222
  }
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');
3223
+ : interactiveMessageTools;
3224
+ const capabilityTools = fileHistoryTools
3225
+ ? new ClaudeCapabilityToolRegistry(fileHistoryTools, capabilities)
3226
+ : undefined;
3227
+ const structuredCapture = this.options.structuredOutputSchema
3228
+ ? { calls: 0, value: undefined }
3229
+ : undefined;
3230
+ const agentScopedTools = agent && capabilityTools
3231
+ ? new FilteredToolRegistry(capabilityTools, {
3232
+ tools: mainAgentToolNames(capabilityTools, agent),
3233
+ })
3234
+ : capabilityTools;
3235
+ const structuredTools = this.options.structuredOutputSchema && structuredCapture
3236
+ ? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
3237
+ : agentScopedTools;
3238
+ const hookTools = this.options.hooks && structuredTools && turnPermissions
3239
+ ? new ClaudeHookToolCoordinator({
3240
+ tools: structuredTools,
3241
+ permissions: turnPermissions,
3242
+ hooks: this.options.hooks,
3243
+ session: hookSession,
3244
+ recordOutcome: recordHookOutcome,
3245
+ ...(this.options.eventSink
3246
+ ? {
3247
+ warn: (message) => this.options.eventSink?.({
3248
+ type: 'warning',
3249
+ message,
3250
+ }),
3251
+ }
3252
+ : {}),
3253
+ deferPreToolUseOutcome: (call) => pendingRecoveryToolCallIds.has(call.id),
3254
+ })
3255
+ : null;
3256
+ const runtime = new AgentRuntime(provider, controller.emit, {
3257
+ emitInitialContextState: false,
3258
+ ...(this.options.emitToolUseSummaries
3259
+ ? {
3260
+ generateToolUseSummary: ({ tools, lastAssistantText, signal: summarySignal, }) => generateToolUseSummary(provider, tools, summarySignal, lastAssistantText, (metrics) => this.recordAuxiliaryMetrics(sessionId, metrics)),
3278
3261
  }
3279
- const followUpTail = await this.append(lease, snapshot.tail, followUpEntry);
3280
- snapshot = {
3281
- entries: [...snapshot.entries, followUpEntry],
3282
- tail: followUpTail,
3283
- };
3284
- currentTurnUserMessages?.push(content);
3285
- await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => executor.acknowledgeNotifications([content])));
3286
- }
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
3262
+ : {}),
3263
+ ...(this.options.pricing
3305
3264
  ? {
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;
3265
+ costUsd: (usage) => {
3266
+ const pricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
3267
+ return pricing ? usageCostUsd(usage, pricing) : undefined;
3315
3268
  },
3316
- approveTool: () => true,
3317
3269
  }
3318
3270
  : {}),
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,
3339
- };
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,
3404
- });
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] }
3271
+ ...(this.options.maxBudgetUsd === undefined
3272
+ ? {}
3273
+ : { maxBudgetUsd: this.options.maxBudgetUsd }),
3274
+ ...(hookTools
3275
+ ? { tools: hookTools, permissions: hookTools }
3448
3276
  : {
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',
3277
+ ...(structuredTools ? { tools: structuredTools } : {}),
3278
+ ...(turnPermissions ? { permissions: turnPermissions } : {}),
3279
+ }),
3459
3280
  });
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
- ],
3281
+ let currentTurnUserMessages = null;
3282
+ let currentTurnToolCalls = 0;
3283
+ let projectMemoryMaintained = false;
3284
+ let projectMemoryRecallMessages = [];
3285
+ let observeModelRequestUsage = () => undefined;
3286
+ const observer = {
3287
+ modelRequestCompleted: async (input) => {
3288
+ observeModelRequestUsage(input);
3289
+ },
3290
+ assistantCompleted: async (message) => {
3291
+ const [entry] = translateProviderEvents([
3292
+ {
3293
+ type: 'assistant-message',
3294
+ text: message.content,
3295
+ ...(message.thinkingBlocks
3296
+ ? { thinkingBlocks: message.thinkingBlocks }
3297
+ : {}),
3298
+ toolCalls: message.toolCalls ?? [],
3299
+ providerMessageId: `msg_${randomUUID().replaceAll('-', '')}`,
3300
+ model: provider.model ?? 'praxis/provider',
3301
+ },
3302
+ ], this.translationContext(sessionId, snapshot));
3303
+ if (!entry)
3304
+ throw new Error('Could not translate assistant response');
3305
+ const tail = await this.append(lease, snapshot.tail, entry);
3306
+ snapshot = { entries: [...snapshot.entries, entry], tail };
3307
+ lastAssistantUuid =
3308
+ typeof entry.uuid === 'string' ? entry.uuid : lastAssistantUuid;
3309
+ },
3310
+ toolCompleted: async (call, toolResult) => {
3311
+ currentTurnToolCalls += 1;
3312
+ const transition = this.worktreeManager?.consumeTransition(call.id);
3313
+ if (transition) {
3314
+ this.options.contextAssembler?.invalidate?.({
3315
+ lifecycleId: sessionId,
3316
+ reason: 'worktree',
3317
+ });
3318
+ const stateEntry = {
3319
+ type: 'worktree-state',
3320
+ worktreeSession: transition.state,
3321
+ sessionId,
3322
+ };
3323
+ const stateTail = await this.append(lease, snapshot.tail, stateEntry);
3324
+ snapshot = {
3325
+ entries: [...snapshot.entries, stateEntry],
3326
+ tail: stateTail,
3327
+ };
3485
3328
  }
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;
3329
+ const permissionMode = this.options.interactiveTools?.consumeTransition(call.id);
3330
+ if (permissionMode) {
3331
+ const modeEntry = {
3332
+ type: 'permission-mode',
3333
+ permissionMode,
3334
+ sessionId,
3335
+ };
3336
+ const modeTail = await this.append(lease, snapshot.tail, modeEntry);
3337
+ snapshot = {
3338
+ entries: [...snapshot.entries, modeEntry],
3339
+ tail: modeTail,
3340
+ };
3508
3341
  }
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) => ({
3542
- role: 'user',
3543
- content,
3544
- })),
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
- {
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([
3585
- ...irreducibleMessages,
3342
+ const [entry] = translateProviderEvents([
3586
3343
  {
3587
- role: 'user',
3588
- content: formatClaudeCompactSummary('a'),
3344
+ type: 'tool-result',
3345
+ toolCallId: call.id,
3346
+ content: toolResult.content,
3347
+ ...(toolResult.contentBlocks
3348
+ ? { contentBlocks: toolResult.contentBlocks }
3349
+ : {}),
3350
+ ...(toolResult.images ? { images: toolResult.images } : {}),
3351
+ ...(toolResult.documents
3352
+ ? { documents: toolResult.documents }
3353
+ : {}),
3354
+ isError: toolResult.isError,
3355
+ ...(toolResult.nativeToolUseResult
3356
+ ? { nativeToolUseResult: toolResult.nativeToolUseResult }
3357
+ : {}),
3358
+ ...(toolResult.nativeMcpMeta
3359
+ ? { nativeMcpMeta: toolResult.nativeMcpMeta }
3360
+ : {}),
3589
3361
  },
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({
3616
- sessionId,
3617
- logicalParentUuid,
3618
- summary: compacted.summary,
3619
- preTokens,
3620
- postTokens,
3621
- previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
3622
- durationMs: compactedDurationMs,
3623
- cwd: this.activeCwd(),
3624
- claudeVersion: this.options.claudeVersion,
3625
- gitBranch: null,
3626
- ...(memorySelection
3627
- ? {
3628
- preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
3362
+ ], this.translationContext(sessionId, snapshot));
3363
+ if (!entry)
3364
+ throw new Error('Could not translate tool result');
3365
+ const tail = await this.append(lease, snapshot.tail, entry);
3366
+ snapshot = { entries: [...snapshot.entries, entry], tail };
3367
+ if (!toolResult.isError && this.options.projectMemoryDirectory) {
3368
+ const pathValue = call.input.file_path;
3369
+ const path = typeof pathValue === 'string'
3370
+ ? resolve(this.activeCwd(), pathValue)
3371
+ : null;
3372
+ if (path &&
3373
+ isPathWithin(this.options.projectMemoryDirectory, path)) {
3374
+ if (call.name === 'Read') {
3375
+ turnMemory.recordRead(path);
3629
3376
  }
3630
- : {}),
3631
- createUuid: () => uuids.shift() ?? randomUUID(),
3632
- now: () => timestamp,
3633
- });
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,
3657
- ...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,
3718
- };
3719
- }
3720
- if (meteringTurnInput !== undefined ||
3721
- meteringDurationsInput !== undefined) {
3722
- const preflight = new ClaudeSessionCostTracker({
3723
- sessionId,
3724
- restored: tracker.snapshot(),
3725
- });
3726
- if (meteringTurnInput !== undefined) {
3727
- preflight.recordTurn(meteringTurnInput);
3377
+ else if (call.name === 'Write' || call.name === 'Edit') {
3378
+ projectMemoryMaintained = true;
3379
+ }
3380
+ }
3381
+ if (call.name === 'Read') {
3382
+ for (const accessedPath of toolResult.accessedPaths ?? []) {
3383
+ const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
3384
+ if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
3385
+ turnMemory.recordRead(resolvedAccessedPath);
3386
+ }
3387
+ }
3388
+ }
3728
3389
  }
3729
- if (meteringDurationsInput !== undefined) {
3730
- preflight.recordDurations(meteringDurationsInput);
3390
+ if (toolResult.isError ||
3391
+ call.name !== 'Read' ||
3392
+ !this.options.conditionalRuleResolver ||
3393
+ !toolResult.accessedPaths) {
3394
+ return;
3731
3395
  }
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
- };
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}`);
3396
+ const attachedRulePaths = this.attachedRulePaths(snapshot.entries);
3397
+ for (const filePath of toolResult.accessedPaths) {
3398
+ const rules = await this.options.conditionalRuleResolver.resolve(filePath, [...attachedRulePaths]);
3399
+ for (const rule of rules) {
3400
+ const attachment = createClaudeRuleAttachmentEntry(rule, this.displayRulePath(rule.path), this.translationContext(sessionId, snapshot));
3401
+ const attachmentTail = await this.append(lease, snapshot.tail, attachment);
3402
+ snapshot = {
3403
+ entries: [...snapshot.entries, attachment],
3404
+ tail: attachmentTail,
3405
+ };
3406
+ attachedRulePaths.add(rule.path);
3407
+ await this.instructionLoaded(sessionId, {
3408
+ path: rule.path,
3409
+ memoryType: rule.scope === 'user'
3410
+ ? 'User'
3411
+ : rule.scope === 'local'
3412
+ ? 'Local'
3413
+ : 'Project',
3414
+ globs: rule.globs,
3415
+ triggerFilePath: filePath,
3416
+ ...(rule.importedFrom === undefined
3417
+ ? {}
3418
+ : { parentFilePath: rule.importedFrom }),
3419
+ }, 'path_glob_match');
3420
+ }
3746
3421
  }
3747
- snapshot = {
3748
- entries: [...snapshot.entries, ...metadataEntries],
3749
- tail: metadataAppend.tail,
3750
- };
3751
- }
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);
3791
- }
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
- : {}),
3422
+ },
3423
+ followUpUserMessagesCompleted: async (messages) => {
3424
+ for (const content of messages) {
3425
+ const [followUpEntry] = translateProviderEvents([{ type: 'user-text-block', text: content }], this.translationContext(sessionId, snapshot));
3426
+ if (!followUpEntry) {
3427
+ throw new Error('Could not translate tool follow-up message');
3428
+ }
3429
+ const followUpTail = await this.append(lease, snapshot.tail, followUpEntry);
3430
+ snapshot = {
3431
+ entries: [...snapshot.entries, followUpEntry],
3432
+ tail: followUpTail,
3433
+ };
3434
+ currentTurnUserMessages?.push(content);
3435
+ await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => executor.acknowledgeNotifications([content])));
3810
3436
  }
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;
3437
+ },
3438
+ };
3439
+ let turnCompleted = false;
3440
+ try {
3441
+ const outcome = await this.hookLifecycle.start(sessionId, hookSession, requireExisting ? 'resume' : 'startup', signal);
3442
+ if (outcome) {
3443
+ await recordHookOutcome(outcome, pendingRecoveryToolCallIds.size > 0);
3820
3444
  }
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,
3445
+ const approveRecovery = this.options.approveRecovery;
3446
+ const recoveryRequest = {
3447
+ cwd: this.activeCwd(),
3448
+ toolResultDirectory,
3449
+ messages: projectClaudeModelMessages(snapshot.entries),
3450
+ observer,
3451
+ permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
3452
+ onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
3453
+ ...(signal ? { signal } : {}),
3454
+ ...(approveRecovery
3455
+ ? {
3456
+ approveRecovery: async (call) => {
3457
+ if (!(await approveRecovery(call))) {
3458
+ throw new Error(`Claude session tool call ${call.id} recovery was declined`);
3459
+ }
3460
+ if (signal?.aborted)
3461
+ throw new AgentRunCancelledError();
3462
+ await flushRecoveryHookOutcomes();
3463
+ pendingRecoveryToolCallIds.delete(call.id);
3464
+ return true;
3465
+ },
3466
+ approveTool: () => true,
3467
+ }
3468
+ : {}),
3833
3469
  };
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}`);
3470
+ const unresolvedToolCall = unresolvedToolCalls[0];
3471
+ if (unresolvedToolCall && !approveRecovery) {
3472
+ throw new Error(`Claude session tool call ${unresolvedToolCall.id} requires explicit recovery approval`);
3845
3473
  }
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 },
3474
+ const recoveryResults = await runtime.recoverToolCalls(unresolvedToolCalls, recoveryRequest);
3475
+ const recoveryUsage = recoveryResults.reduce((usage, result) => result.usage ? mergeUsage(usage, result.usage) : usage, { inputTokens: 0, outputTokens: 0 });
3476
+ const recoveryModelUsage = mergeSessionRawModelUsage(...recoveryResults.map((result) => result.isError ? undefined : result.modelUsage));
3477
+ const foregroundLineChanges = createLineCountAccumulator();
3478
+ for (const result of recoveryResults) {
3479
+ foregroundLineChanges.add(result);
3480
+ }
3481
+ if (this.options.agent &&
3482
+ agent &&
3483
+ getClaudeAgentSetting(snapshot.entries) !== this.options.agent) {
3484
+ const agentSetting = createClaudeAgentSettingEntry(sessionId, this.options.agent);
3485
+ const settingTail = await this.append(lease, snapshot.tail, agentSetting);
3486
+ snapshot = {
3487
+ entries: [...snapshot.entries, agentSetting],
3488
+ tail: settingTail,
3489
+ };
3490
+ }
3491
+ let agentSystem = null;
3492
+ let planModeMessage;
3493
+ let sessionMemoryMessage = null;
3494
+ let contextMessages = [];
3495
+ let contextProjection = {
3496
+ systemMessages: [],
3497
+ stableSystemSectionCount: 0,
3854
3498
  };
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, {
3499
+ let stableSystemMessageCount = 0;
3500
+ const refreshRuntimeContext = async () => {
3501
+ agentSystem = await this.mainAgentSystemPrompt(agent);
3502
+ planModeMessage =
3503
+ this.options.interactiveTools?.contextMessage(sessionId);
3504
+ sessionMemoryMessage = this.sessionMemoryMessage(await turnMemory.sessionSummary());
3505
+ const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
3863
3506
  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 }
3507
+ lifecycleId: sessionId,
3508
+ ...(agentSystem
3509
+ ? { mode: 'agent', baseSystemPrompt: agentSystem }
3874
3510
  : {}),
3875
- permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
3876
- onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
3877
- ...(signal ? { signal } : {}),
3878
- });
3879
- }
3880
- catch (error) {
3881
- this.options.eventSink?.({
3882
- type: 'shell-cancelled',
3883
- callId: call.id,
3511
+ turn: {
3512
+ ...(planModeMessage ? { planMode: planModeMessage } : {}),
3513
+ ...(sessionMemoryMessage
3514
+ ? { sessionMemory: sessionMemoryMessage }
3515
+ : {}),
3516
+ ...(this.options.brief ? { briefOutput: true } : {}),
3517
+ ...(this.options.structuredOutputSchema
3518
+ ? { structuredOutput: true }
3519
+ : {}),
3520
+ },
3884
3521
  });
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,
3522
+ contextProjection = projectContextSnapshot(assembledContext);
3523
+ stableSystemMessageCount =
3524
+ contextProjection.stableSystemSectionCount;
3525
+ contextMessages = [...contextProjection.systemMessages];
3917
3526
  };
3918
- currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
3527
+ await refreshRuntimeContext();
3528
+ const expansion = skipUserPrompt
3529
+ ? { userMessages: [] }
3530
+ : shellCommand === undefined
3531
+ ? this.options.extensions
3532
+ ? await this.options.extensions.expandPromptAsync(effectivePrompt, signal, toolResultDirectory)
3533
+ : { userMessages: [effectivePrompt] }
3534
+ : {
3535
+ userMessages: [`<bash-input>${shellCommand}</bash-input>`],
3536
+ };
3537
+ const expansionMessages = 'messages' in expansion ? expansion.messages : undefined;
3538
+ const userMessages = 'userMessages' in expansion ? expansion.userMessages : [];
3539
+ const expandedMessages = expansionMessages ?? userMessages.map((text) => ({ text }));
3540
+ const attachmentIndex = expansionMessages
3541
+ ? expandedMessages.length - 1
3542
+ : 0;
3543
+ currentTurnUserMessages = [...expansion.userMessages];
3919
3544
  this.options.eventSink?.({
3920
- type: 'shell-result',
3921
- callId: call.id,
3922
- stdout,
3923
- stderr,
3924
- isError: shellResult.isError,
3545
+ type: 'state',
3546
+ state: 'assembling-context',
3925
3547
  });
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;
3548
+ let compactionUsage = {
3549
+ inputTokens: 0,
3550
+ outputTokens: 0,
3551
+ };
3552
+ let compactionDurationMs;
3553
+ let compactionDurationWithoutRetriesMs;
3554
+ let compactionModelUsage;
3555
+ const definitions = provider.capabilities.tools
3556
+ ? (structuredTools?.definitions() ?? [])
3557
+ : [];
3558
+ const budget = this.contextBudget(provider);
3559
+ const contextEngine = new ContextEngine({
3560
+ ...(budget ? { budget } : {}),
3561
+ autoCompact: this.options.autoCompact !== false,
3562
+ memory: {
3563
+ beforeCompact: () => turnMemory.beforeCompact(),
3564
+ afterCompact: () => turnMemory.afterCompact(),
3565
+ },
3566
+ });
3567
+ observeModelRequestUsage = ({ usage, messages, tools }) => {
3568
+ contextEngine.observeUsage(usage, messages, tools);
3569
+ };
3570
+ const pendingUserMessages = expandedMessages.map((message, index) => ({
3571
+ role: 'user',
3572
+ content: message.text,
3573
+ ...(message.contentBlocks?.length
3574
+ ? { contentBlocks: message.contentBlocks }
3575
+ : {}),
3576
+ ...((index === attachmentIndex && images.length > 0) ||
3577
+ message.images?.length
3578
+ ? {
3579
+ images: [
3580
+ ...(index === attachmentIndex ? images : []),
3581
+ ...(message.images ?? []),
3582
+ ],
3583
+ }
3584
+ : {}),
3585
+ ...(index === attachmentIndex && documents.length > 0
3586
+ ? { documents }
3587
+ : {}),
3588
+ }));
3589
+ const agentMentionMessages = shellCommand === undefined && !skipUserPrompt
3590
+ ? (this.options.extensions?.agentMentionMessages(effectivePrompt) ?? [])
3591
+ : [];
3592
+ const injectAgentMentionContext = (messages) => {
3593
+ if (agentMentionMessages.length === 0)
3594
+ return [...messages];
3595
+ let insertionIndex = messages.length;
3596
+ let foundPrompt = false;
3597
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3598
+ const message = messages[index];
3599
+ if (message?.role === 'user' &&
3600
+ typeof message.content === 'string' &&
3601
+ message.content.endsWith(effectivePrompt)) {
3602
+ insertionIndex = index;
3603
+ foundPrompt = true;
3604
+ break;
3605
+ }
3971
3606
  }
3607
+ if (!foundPrompt)
3608
+ return [...messages];
3972
3609
  return [
3973
- ...contextMessages,
3974
- ...injectTurnContext([
3610
+ ...messages.slice(0, insertionIndex),
3611
+ ...agentMentionMessages.map((content) => ({
3612
+ role: 'user',
3613
+ content,
3614
+ })),
3615
+ ...messages.slice(insertionIndex),
3616
+ ];
3617
+ };
3618
+ const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, contextProjection.firstUserMessageContext);
3619
+ const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
3620
+ let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
3621
+ const contextTransitionPort = (pendingMessages = [], preservedUserMessages = []) => ({
3622
+ current: () => {
3623
+ const historyMessages = [
3975
3624
  ...projectClaudeModelMessages(snapshot.entries),
3976
3625
  ...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
3626
+ ];
3627
+ return {
3628
+ messages: [
3629
+ ...contextMessages,
3630
+ ...injectTurnContext([
3631
+ ...historyMessages,
3632
+ ...pendingMessages,
3633
+ ]),
3634
+ ],
3635
+ tools: definitions,
3636
+ };
3637
+ },
3638
+ irreducible: () => ({
3639
+ messages: [
3640
+ ...contextMessages,
3641
+ ...injectTurnContext([
3642
+ ...pendingMessages,
3643
+ ...preservedUserMessages.map((content) => ({
3644
+ role: 'user',
3645
+ content,
3646
+ })),
3647
+ ]),
3648
+ ],
3649
+ tools: definitions,
3650
+ }),
3651
+ propose: async () => {
3652
+ if (!budget) {
3653
+ throw new Error('Context budget is unavailable');
3654
+ }
3655
+ const historyMessages = [
3656
+ ...projectClaudeModelMessages(snapshot.entries),
3657
+ ...projectMemoryRecallMessages,
3658
+ ];
3659
+ const irreducibleMessages = [
3660
+ ...contextMessages,
3661
+ ...injectTurnContext([
3662
+ ...pendingMessages,
3663
+ ...preservedUserMessages.map((content) => ({
3664
+ role: 'user',
3665
+ content,
3666
+ })),
3667
+ ]),
3668
+ ];
3669
+ let logicalParentUuid = compactionAnchorUuid;
3670
+ if (!logicalParentUuid || historyMessages.length === 0) {
3671
+ throw new Error('Cannot compact an empty Claude transcript');
3672
+ }
3673
+ if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
3674
+ throw new Error('Cannot compact a Claude session with unresolved tool calls');
3675
+ }
3676
+ const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', { trigger: 'auto', custom_instructions: null }, 'auto', signal);
3677
+ const memorySelection = sessionMemory
3678
+ ? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries), { sessionMemoryReady: true })
3679
+ : null;
3680
+ const compactorMessages = memorySelection
3681
+ ? [
3682
+ memorySelection.memoryMessage,
3683
+ ...projectClaudeModelMessages(memorySelection.compactedEntries),
3684
+ ]
3685
+ : historyMessages;
3686
+ compactorMessages.push(...successfulHookOutput(preCompact).map((content) => ({
3687
+ role: 'user',
3688
+ content: `Additional summarization context: ${content}`,
3689
+ })));
3690
+ if (memorySelection) {
3691
+ logicalParentUuid = memorySelection.logicalParentUuid;
3692
+ }
3693
+ this.options.eventSink?.({ type: 'state', state: 'compacting' });
3694
+ const compactEnvelope = budget.evaluate([
3695
+ ...irreducibleMessages,
3696
+ {
3697
+ role: 'user',
3698
+ content: formatClaudeCompactSummary(''),
3699
+ },
3700
+ ], definitions);
3701
+ let targetTokens = Math.min(8192, compactEnvelope.availableTokens -
3702
+ compactEnvelope.estimatedTokens);
3703
+ if (targetTokens < 1) {
3704
+ budget.assertFits(budget.evaluate([
3705
+ ...irreducibleMessages,
3706
+ {
3707
+ role: 'user',
3708
+ content: formatClaudeCompactSummary('a'),
3709
+ },
3710
+ ], definitions));
3711
+ targetTokens = 1;
3712
+ }
3713
+ const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
3714
+ messages: compactorMessages,
3715
+ targetTokens,
3716
+ contextWindowTokens: budget.contextWindowTokens,
3717
+ ...(signal ? { signal } : {}),
3718
+ });
3719
+ const { durationMs: compactedDurationMs, durationWithoutRetriesMs: compactedDurationWithoutRetriesMs, } = requireCompactionDurations(compacted);
3720
+ const proposedCompactionDurationMs = (compactionDurationMs ?? 0) + compactedDurationMs;
3721
+ if (!Number.isFinite(proposedCompactionDurationMs)) {
3722
+ throw new TypeError('compaction durationMs total overflow');
3723
+ }
3724
+ const proposedCompactionDurationWithoutRetriesMs = (compactionDurationWithoutRetriesMs ?? 0) +
3725
+ compactedDurationWithoutRetriesMs;
3726
+ if (!Number.isFinite(proposedCompactionDurationWithoutRetriesMs)) {
3727
+ throw new TypeError('compaction durationWithoutRetriesMs total overflow');
3728
+ }
3729
+ const boundaryUuid = randomUUID();
3730
+ const summaryUuid = randomUUID();
3731
+ const timestamp = new Date().toISOString();
3732
+ const preTokens = budget.evaluate([...contextMessages, ...injectTurnContext(historyMessages)], definitions).estimatedTokens;
3733
+ const compactEntries = (postTokens) => {
3734
+ const uuids = [boundaryUuid, summaryUuid];
3735
+ return createClaudeCompactEntries({
3736
+ sessionId,
3737
+ logicalParentUuid,
3738
+ summary: compacted.summary,
3739
+ preTokens,
3740
+ postTokens,
3741
+ previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
3742
+ durationMs: compactedDurationMs,
3743
+ cwd: this.activeCwd(),
3744
+ claudeVersion: this.options.claudeVersion,
3745
+ gitBranch: null,
3746
+ ...(memorySelection
4053
3747
  ? {
4054
- durationApiMs: batchDurationApiMs,
4055
- durationApiWithoutRetriesMs: batchDurationApiWithoutRetriesMs,
3748
+ preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
4056
3749
  }
4057
3750
  : {}),
3751
+ createUuid: () => uuids.shift() ?? randomUUID(),
3752
+ now: () => timestamp,
3753
+ });
3754
+ };
3755
+ const provisionalEntries = compactEntries(0);
3756
+ const compactSummaryUuid = provisionalEntries.at(-1)?.uuid;
3757
+ if (typeof compactSummaryUuid !== 'string') {
3758
+ throw new Error('Could not create Claude compact summary');
3759
+ }
3760
+ const replayUuids = preservedUserMessages.map(() => randomUUID());
3761
+ const replayEntries = translateProviderEvents(preservedUserMessages.map((text, index) => index === 0
3762
+ ? { type: 'user-text', text }
3763
+ : { type: 'user-text-block', text }), {
3764
+ sessionId,
3765
+ parentUuid: compactSummaryUuid,
3766
+ cwd: this.activeCwd(),
3767
+ claudeVersion: this.options.claudeVersion,
3768
+ gitBranch: null,
3769
+ history: [...snapshot.entries, ...provisionalEntries],
3770
+ createUuid: () => replayUuids.shift() ?? randomUUID(),
3771
+ now: () => timestamp,
3772
+ });
3773
+ const compactedHistory = [
3774
+ ...projectClaudeModelMessages([
3775
+ ...snapshot.entries,
3776
+ ...provisionalEntries,
3777
+ ...replayEntries,
3778
+ ]),
3779
+ ...projectMemoryRecallMessages,
3780
+ ];
3781
+ const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
3782
+ const proposedMessages = [
3783
+ ...contextMessages,
3784
+ ...injectTurnContext([...compactedHistory, ...pendingMessages]),
3785
+ ];
3786
+ const entries = [
3787
+ ...compactEntries(afterHistory.estimatedTokens),
3788
+ ...replayEntries,
3789
+ ];
3790
+ if (signal?.aborted)
3791
+ throw new AgentRunCancelledError();
3792
+ // Construct the exact tracker mutations for this single committed
3793
+ // boundary and preflight them against a clone of the live tracker so
3794
+ // invalid input or cumulative overflow rejects before the compact
3795
+ // boundary is appended rather than after a half-commit.
3796
+ const tracker = this.sessionCostTrackers.get(sessionId);
3797
+ if (!tracker) {
3798
+ throw new Error(`Session cost tracker is not active for session ${sessionId}`);
3799
+ }
3800
+ const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
3801
+ ? compacted.model
3802
+ : provider.model;
3803
+ const compactModelNonBlank = compactModel !== undefined && compactModel.trim() !== '';
3804
+ if (hasNonZeroUsage(compacted.usage) && !compactModelNonBlank) {
3805
+ throw new Error('Auto compact usage requires a nonblank model identity');
3806
+ }
3807
+ let meteringTurnInput;
3808
+ if (compactModelNonBlank && hasNonZeroUsage(compacted.usage)) {
3809
+ const pricing = this.options.pricing?.resolve(compactModel);
3810
+ const costUsd = pricing
3811
+ ? usageCostUsd(compacted.usage, pricing)
3812
+ : undefined;
3813
+ meteringTurnInput = {
3814
+ model: compactModel,
3815
+ usage: compacted.usage,
3816
+ ...(costUsd === undefined ? {} : { costUsd }),
3817
+ ...(compacted.usage.webSearchRequests === undefined
3818
+ ? {}
3819
+ : { webSearchRequests: compacted.usage.webSearchRequests }),
4058
3820
  };
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,
3821
+ }
3822
+ let meteringDurationsInput;
3823
+ if (compactedDurationMs > 0 ||
3824
+ compactedDurationWithoutRetriesMs > 0) {
3825
+ meteringDurationsInput = {
3826
+ ...(compactedDurationMs === 0
3827
+ ? {}
3828
+ : { apiDurationMs: compactedDurationMs }),
3829
+ apiDurationWithoutRetriesMs: compactedDurationWithoutRetriesMs,
3830
+ };
3831
+ }
3832
+ if (meteringTurnInput !== undefined ||
3833
+ meteringDurationsInput !== undefined) {
3834
+ const preflight = new ClaudeSessionCostTracker({
3835
+ sessionId,
3836
+ restored: tracker.snapshot(),
3837
+ });
3838
+ if (meteringTurnInput !== undefined) {
3839
+ preflight.recordTurn(meteringTurnInput);
3840
+ }
3841
+ if (meteringDurationsInput !== undefined) {
3842
+ preflight.recordDurations(meteringDurationsInput);
3843
+ }
3844
+ }
3845
+ return {
3846
+ envelope: {
3847
+ messages: proposedMessages,
3848
+ tools: definitions,
3849
+ },
3850
+ commit: async () => {
3851
+ if (signal?.aborted)
3852
+ throw new AgentRunCancelledError();
3853
+ const appendResult = await lease.appendMany(snapshot.tail, entries);
3854
+ if (appendResult.status === 'conflict') {
3855
+ throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
3856
+ }
3857
+ snapshot = {
3858
+ entries: [...snapshot.entries, ...entries],
3859
+ tail: appendResult.tail,
3860
+ };
3861
+ const metadataEntries = createClaudeDurableMetadataSnapshot(snapshot.entries, sessionId);
3862
+ if (metadataEntries.length > 0) {
3863
+ const metadataAppend = await lease.appendMany(snapshot.tail, metadataEntries);
3864
+ if (metadataAppend.status === 'conflict') {
3865
+ throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
3866
+ }
3867
+ snapshot = {
3868
+ entries: [...snapshot.entries, ...metadataEntries],
3869
+ tail: metadataAppend.tail,
3870
+ };
3871
+ }
3872
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
3873
+ await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
3874
+ // The boundary is durable: mirror Claude's full-compact behavior by
3875
+ // rerunning SessionStart with source compact and refreshing the
3876
+ // runtime-only context so the next request retains current
3877
+ // instructions, plan state, session memory, and hook context.
3878
+ if (this.options.hooks) {
3879
+ const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
3880
+ if (outcome)
3881
+ await recordHookOutcome(outcome);
3882
+ }
3883
+ this.options.contextAssembler?.invalidate?.({
3884
+ lifecycleId: sessionId,
3885
+ reason: 'compact',
3886
+ });
3887
+ await refreshRuntimeContext();
3888
+ this.options.eventSink?.({
3889
+ type: 'compact-boundary',
3890
+ trigger: 'auto',
3891
+ preTokens,
3892
+ uuid: boundaryUuid,
3893
+ });
3894
+ compactionAnchorUuid = compactSummaryUuid;
3895
+ compactionUsage = mergeUsage(compactionUsage, compacted.usage);
3896
+ compactionDurationMs = proposedCompactionDurationMs;
3897
+ compactionDurationWithoutRetriesMs =
3898
+ proposedCompactionDurationWithoutRetriesMs;
3899
+ if (meteringTurnInput !== undefined) {
3900
+ compactionModelUsage = mergeSessionRawModelUsage(compactionModelUsage, { [meteringTurnInput.model]: compacted.usage });
3901
+ }
3902
+ // Commit the preflighted mutations once the boundary is durable so a
3903
+ // later main-provider failure or cancellation cannot lose the
3904
+ // compactor's usage/cost/API durations.
3905
+ if (meteringTurnInput !== undefined) {
3906
+ tracker.recordTurn(meteringTurnInput);
3907
+ }
3908
+ if (meteringDurationsInput !== undefined) {
3909
+ tracker.recordDurations(meteringDurationsInput);
3910
+ }
3911
+ },
3912
+ };
3913
+ },
4076
3914
  });
4077
- };
4078
- let result;
4079
- try {
4080
- try {
4081
- result = await attemptMainTurn();
3915
+ await contextEngine.prepare(contextTransitionPort(pendingUserMessages), signal);
3916
+ for (const [index, message] of expandedMessages.entries()) {
3917
+ if (shellCommand !== undefined)
3918
+ break;
3919
+ const messageImages = [
3920
+ ...(index === attachmentIndex ? images : []),
3921
+ ...(message.images ?? []),
3922
+ ];
3923
+ const persistenceEvent = messageImages.length > 0 ||
3924
+ (index === attachmentIndex && documents.length > 0)
3925
+ ? {
3926
+ type: 'user-message',
3927
+ text: message.text,
3928
+ images: messageImages,
3929
+ ...(index === attachmentIndex && documents.length > 0
3930
+ ? { documents }
3931
+ : {}),
3932
+ }
3933
+ : index === 0
3934
+ ? { type: 'user-text', text: message.text }
3935
+ : { type: 'user-text-block', text: message.text };
3936
+ const [userEntry] = translateProviderEvents([persistenceEvent], this.translationContext(sessionId, snapshot));
3937
+ if (!userEntry)
3938
+ throw new Error('Could not translate user prompt');
3939
+ if (currentPromptId === null &&
3940
+ typeof userEntry.uuid === 'string') {
3941
+ currentPromptId = userEntry.uuid;
3942
+ compactionAnchorUuid ??= userEntry.uuid;
3943
+ }
3944
+ const userTail = await this.append(lease, snapshot.tail, userEntry);
3945
+ snapshot = {
3946
+ entries: [...snapshot.entries, userEntry],
3947
+ tail: userTail,
3948
+ };
4082
3949
  }
4083
- catch (error) {
4084
- if (!budget || !isPromptTooLongError(error))
4085
- throw error;
4086
- if (recoveryPlanner.reactiveRetriesRemaining === 0) {
4087
- surfaceExhaustedRecovery(error);
4088
- throw error;
3950
+ if (fileHistory && currentPromptId) {
3951
+ const historySnapshot = await fileHistory.snapshot(snapshot.entries, currentPromptId);
3952
+ const historyTail = await this.append(lease, snapshot.tail, historySnapshot);
3953
+ snapshot = {
3954
+ entries: [...snapshot.entries, historySnapshot],
3955
+ tail: historyTail,
3956
+ };
3957
+ }
3958
+ if (this.options.hooks && !skipUserPrompt) {
3959
+ const outcome = await this.options.hooks.run({
3960
+ ...hookSession,
3961
+ hook_event_name: 'UserPromptSubmit',
3962
+ prompt_id: currentPromptId ?? randomUUID(),
3963
+ prompt: effectivePrompt,
3964
+ }, undefined, signal);
3965
+ await recordHookOutcome(outcome);
3966
+ if (outcome.blockedReason) {
3967
+ throw new Error(`UserPromptSubmit hook error: ${outcome.blockedReason}`);
4089
3968
  }
4090
- const beforeRecovery = budget.evaluate(runtimeRequest.messages, definitions);
3969
+ }
3970
+ let shellUsage = { inputTokens: 0, outputTokens: 0 };
3971
+ let shellModelUsage;
3972
+ if (shellCommand !== undefined) {
3973
+ const call = {
3974
+ id: `shell_${randomUUID().replaceAll('-', '')}`,
3975
+ name: 'Bash',
3976
+ input: { command: shellCommand },
3977
+ };
3978
+ this.options.eventSink?.({
3979
+ type: 'shell-command',
3980
+ callId: call.id,
3981
+ command: shellCommand,
3982
+ });
3983
+ let shellResult;
4091
3984
  try {
4092
- await compactIfNeeded([], currentTurnUserMessages ?? [], {
4093
- promptTooLong: true,
3985
+ shellResult = await runtime.executeDirectToolCall(call, {
3986
+ cwd: this.activeCwd(),
3987
+ sessionId,
3988
+ toolResultDirectory,
3989
+ messages: projectClaudeModelMessages(snapshot.entries),
3990
+ observer: {
3991
+ assistantCompleted: async () => undefined,
3992
+ toolCompleted: async () => undefined,
3993
+ followUpUserMessagesCompleted: observer.followUpUserMessagesCompleted,
3994
+ },
3995
+ ...(this.options.approveTool
3996
+ ? { approveTool: this.options.approveTool }
3997
+ : {}),
3998
+ permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
3999
+ onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
4000
+ ...(signal ? { signal } : {}),
4094
4001
  });
4095
4002
  }
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);
4003
+ catch (error) {
4004
+ this.options.eventSink?.({
4005
+ type: 'shell-cancelled',
4006
+ callId: call.id,
4007
+ });
4105
4008
  throw error;
4106
4009
  }
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;
4010
+ shellUsage = shellResult.usage ?? shellUsage;
4011
+ if (!shellResult.isError) {
4012
+ shellModelUsage = shellResult.modelUsage;
4013
+ foregroundLineChanges.add(shellResult);
4118
4014
  }
4119
- else {
4120
- runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
4015
+ const stdout = shellResult.processOutput?.stdout ??
4016
+ (shellResult.isError ? '' : shellResult.content);
4017
+ const stderr = shellResult.processOutput?.stderr ??
4018
+ (shellResult.isError ? shellResult.content : '');
4019
+ const shellUuids = [shellInputUuid ?? randomUUID(), randomUUID()];
4020
+ const [inputEntry, outputEntry] = translateProviderEvents([
4021
+ { type: 'bash-input', command: shellCommand },
4022
+ { type: 'bash-output', stdout, stderr },
4023
+ ], {
4024
+ ...this.translationContext(sessionId, snapshot),
4025
+ createUuid: () => shellUuids.shift() ?? randomUUID(),
4026
+ });
4027
+ if (!inputEntry || !outputEntry) {
4028
+ throw new Error('Could not translate shell command result');
4121
4029
  }
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;
4030
+ const shellAppend = await lease.appendMany(snapshot.tail, [
4031
+ inputEntry,
4032
+ outputEntry,
4033
+ ]);
4034
+ if (shellAppend.status === 'conflict') {
4035
+ throw new Error(`Claude transcript append conflict: ${shellAppend.reason}`);
4129
4036
  }
4130
- runtimeRequest.deferFailureKinds = true;
4037
+ snapshot = {
4038
+ entries: [...snapshot.entries, inputEntry, outputEntry],
4039
+ tail: shellAppend.tail,
4040
+ };
4041
+ currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
4042
+ this.options.eventSink?.({
4043
+ type: 'shell-result',
4044
+ callId: call.id,
4045
+ stdout,
4046
+ stderr,
4047
+ isError: shellResult.isError,
4048
+ });
4049
+ }
4050
+ if (budget) {
4051
+ await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
4052
+ budget.assertFits(budget.evaluate([
4053
+ ...contextMessages,
4054
+ ...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
4055
+ ], definitions));
4056
+ }
4057
+ let stopHookActive = false;
4058
+ const runtimeRequest = {
4059
+ sessionId,
4060
+ messages: [
4061
+ ...contextMessages,
4062
+ ...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
4063
+ ],
4064
+ stableSystemMessageCount,
4065
+ cwd: this.activeCwd(),
4066
+ toolResultDirectory,
4067
+ observer,
4068
+ ...(this.options.effort ? { effort: this.options.effort } : {}),
4069
+ ...(this.options.maxModelTurns !== undefined
4070
+ ? { maxModelTurns: this.options.maxModelTurns }
4071
+ : {}),
4072
+ ...(this.options.betas?.length
4073
+ ? { betas: this.options.betas }
4074
+ : {}),
4075
+ ...(this.options.collectMetrics || this.options.costStateStore
4076
+ ? { collectMetrics: true }
4077
+ : {}),
4078
+ ...(budget
4079
+ ? { deferFailureKinds: ['prompt_too_long'] }
4080
+ : {}),
4081
+ reloadMessages: async () => {
4082
+ const recalled = turnMemory.consumeRecall();
4083
+ if (recalled) {
4084
+ projectMemoryRecallMessages = [
4085
+ { role: 'user', content: recalled.content },
4086
+ ];
4087
+ }
4088
+ await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
4089
+ runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
4090
+ return [
4091
+ ...contextMessages,
4092
+ ...injectTurnContext([
4093
+ ...projectClaudeModelMessages(snapshot.entries),
4094
+ ...projectMemoryRecallMessages,
4095
+ ]),
4096
+ ];
4097
+ },
4098
+ ...(this.options.hooks ||
4099
+ subagentExecutor ||
4100
+ taskTools ||
4101
+ this.scheduledPrompts ||
4102
+ this.workflowManager
4103
+ ? {
4104
+ onStop: async (text) => {
4105
+ const messages = [];
4106
+ const outcome = await this.options.hooks?.run({
4107
+ ...hookSession,
4108
+ hook_event_name: 'Stop',
4109
+ stop_hook_active: stopHookActive,
4110
+ last_assistant_message: text,
4111
+ }, undefined, signal);
4112
+ if (outcome) {
4113
+ await recordHookOutcome(outcome);
4114
+ if (outcome.blockedReason) {
4115
+ stopHookActive = true;
4116
+ messages.push(`Stop hook error: ${outcome.blockedReason}`);
4117
+ }
4118
+ }
4119
+ const claimedAgentIds = new Set(this.sessionSubagentExecutors(sessionId, true).flatMap((executor) => executor.notificationClaimAgentIds()));
4120
+ await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => {
4121
+ const delivered = (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification);
4122
+ return this.hostedSubagents.has(executor)
4123
+ ? executor.reconcileDetachedNotifications(delivered)
4124
+ : executor.reconcileDeliveredNotifications(delivered);
4125
+ }));
4126
+ await subagentExecutor?.hydratePersistedTasks(sessionId, this.activeCwd(), claimedAgentIds);
4127
+ const background = await this.collectSubagentNotifications(sessionId, subagentExecutor ?? undefined);
4128
+ if (background)
4129
+ messages.push(...background.messages);
4130
+ const workflow = await this.workflowManager?.notifications(true);
4131
+ if (workflow)
4132
+ messages.push(...workflow.messages);
4133
+ const bashMessages = await taskTools?.notifications(true);
4134
+ if (bashMessages)
4135
+ messages.push(...bashMessages);
4136
+ const scheduled = await this.scheduledPrompts?.drainDue();
4137
+ if (scheduled) {
4138
+ messages.push(...scheduled.map(({ prompt }) => prompt));
4139
+ }
4140
+ const batchUsage = background || workflow
4141
+ ? mergeUsage(background?.usage ?? {
4142
+ inputTokens: 0,
4143
+ outputTokens: 0,
4144
+ }, workflow?.usage ?? {
4145
+ inputTokens: 0,
4146
+ outputTokens: 0,
4147
+ })
4148
+ : undefined;
4149
+ const batchModelUsage = mergeSessionRawModelUsage(background?.modelUsage, workflow?.modelUsage);
4150
+ const batchDurationApiMs = addApiDuration(workflow?.durationApiMs, addApiDuration(background?.durationApiMs, 0, 'durationApiMs'), 'durationApiMs');
4151
+ const backgroundDurationWithoutRetries = background?.durationApiWithoutRetriesMs ??
4152
+ background?.durationApiMs;
4153
+ const workflowDurationWithoutRetries = workflow?.durationApiWithoutRetriesMs ??
4154
+ workflow?.durationApiMs;
4155
+ const batchDurationApiWithoutRetriesMs = addApiDuration(workflowDurationWithoutRetries, addApiDuration(backgroundDurationWithoutRetries, 0, 'durationApiWithoutRetriesMs'), 'durationApiWithoutRetriesMs');
4156
+ const hasBatchDuration = background?.durationApiMs !== undefined ||
4157
+ background?.durationApiWithoutRetriesMs !== undefined ||
4158
+ workflow?.durationApiMs !== undefined ||
4159
+ workflow?.durationApiWithoutRetriesMs !== undefined;
4160
+ return {
4161
+ messages,
4162
+ ...(batchUsage
4163
+ ? {
4164
+ usage: batchUsage,
4165
+ ...(batchModelUsage
4166
+ ? { modelUsage: batchModelUsage }
4167
+ : {}),
4168
+ }
4169
+ : {}),
4170
+ ...(hasBatchDuration
4171
+ ? {
4172
+ durationApiMs: batchDurationApiMs,
4173
+ durationApiWithoutRetriesMs: batchDurationApiWithoutRetriesMs,
4174
+ }
4175
+ : {}),
4176
+ };
4177
+ },
4178
+ }
4179
+ : {}),
4180
+ ...(this.options.approveTool
4181
+ ? { approveTool: this.options.approveTool }
4182
+ : {}),
4183
+ permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
4184
+ onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
4185
+ };
4186
+ const attemptMainTurn = () => signal
4187
+ ? runtime.run({ ...runtimeRequest, signal })
4188
+ : runtime.run(runtimeRequest);
4189
+ const surfaceExhaustedRecovery = (error) => {
4190
+ this.options.eventSink?.({
4191
+ type: 'failed',
4192
+ message: error.message,
4193
+ retryable: false,
4194
+ });
4195
+ };
4196
+ let result;
4197
+ try {
4131
4198
  try {
4132
4199
  result = await attemptMainTurn();
4133
4200
  }
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();
4201
+ catch (error) {
4202
+ if (!budget || !isPromptTooLongError(error))
4203
+ throw error;
4204
+ const recovery = await contextEngine.recover(error, contextTransitionPort([], currentTurnUserMessages ?? []), signal);
4205
+ if (recovery.kind !== 'retry') {
4206
+ surfaceExhaustedRecovery(error);
4207
+ throw error;
4208
+ }
4209
+ // The single reactive retry must use the compacted transcript, not
4210
+ // the stale request copy captured before the compact boundary.
4211
+ runtimeRequest.messages = [
4212
+ ...contextMessages,
4213
+ ...injectTurnContext([
4214
+ ...projectClaudeModelMessages(snapshot.entries),
4215
+ ...projectMemoryRecallMessages,
4216
+ ]),
4217
+ ];
4218
+ runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
4219
+ runtimeRequest.deferFailureKinds = true;
4220
+ try {
4221
+ result = await attemptMainTurn();
4222
+ }
4223
+ catch (retryError) {
4224
+ // Exactly one reactive retry is consumed; fail deterministically
4225
+ // and surface the original prompt-too-long error.
4226
+ if (signal?.aborted ||
4227
+ retryError instanceof AgentRunCancelledError) {
4228
+ throw new AgentRunCancelledError();
4229
+ }
4230
+ surfaceExhaustedRecovery(error);
4231
+ throw error;
4141
4232
  }
4142
- surfaceExhaustedRecovery(error);
4143
- throw error;
4144
4233
  }
4145
4234
  }
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);
4235
+ catch (error) {
4236
+ if (!signal?.aborted &&
4237
+ !(error instanceof AgentRunCancelledError) &&
4238
+ !(error instanceof ModelProviderError &&
4239
+ error.kind === 'cancelled')) {
4240
+ const failureKind = error instanceof ModelProviderError
4241
+ ? claudeStopFailureError(error.kind)
4242
+ : 'unknown';
4243
+ await this.runAdvisoryHook(sessionId, 'StopFailure', {
4244
+ error: failureKind,
4245
+ error_details: error instanceof Error ? error.message : String(error),
4246
+ }, failureKind, signal);
4247
+ }
4248
+ throw error;
4158
4249
  }
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;
4250
+ if (structuredCapture && structuredCapture.calls !== 1) {
4251
+ throw new Error(`StructuredOutput must be called exactly once (received ${structuredCapture.calls})`);
4208
4252
  }
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 }),
4253
+ const finalLeafUuid = lastAssistantUuid;
4254
+ if (!finalLeafUuid) {
4255
+ throw new Error('Could not locate final assistant response');
4256
+ }
4257
+ if (!skipUserPrompt) {
4258
+ const lastPrompt = createClaudeLastPromptEntry({
4259
+ sessionId,
4260
+ lastPrompt: effectivePrompt,
4261
+ leafUuid: finalLeafUuid,
4227
4262
  });
4263
+ const tail = await this.append(lease, snapshot.tail, lastPrompt);
4264
+ snapshot = {
4265
+ entries: [...snapshot.entries, lastPrompt],
4266
+ tail,
4267
+ };
4228
4268
  }
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
4269
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
4270
+ const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
4271
+ const tracker = this.sessionCostTrackers.get(sessionId);
4272
+ if (!tracker) {
4273
+ throw new Error(`Session cost tracker is not active for session ${sessionId}`);
4274
+ }
4275
+ const turnModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, compactionModelUsage, shellModelUsage, result.modelUsage);
4276
+ const combinedDurationMs = compactionDurationMs === undefined &&
4277
+ result.durationApiMs === undefined
4278
+ ? undefined
4279
+ : (compactionDurationMs ?? 0) + (result.durationApiMs ?? 0);
4280
+ let rawCostUsd;
4281
+ if (turnModelUsage) {
4282
+ for (const [model, usage] of Object.entries(turnModelUsage)) {
4283
+ const pricing = this.options.pricing?.resolve(model);
4284
+ const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
4285
+ if (costUsd !== undefined)
4286
+ rawCostUsd = (rawCostUsd ?? 0) + costUsd;
4287
+ }
4288
+ }
4289
+ // Auto-compaction metering was already recorded atomically with each
4290
+ // committed boundary, and externally metered tool-summary metrics were
4291
+ // committed through the summary callback, so the live tracker receives
4292
+ // only the unrecorded subset here. The inclusive public aggregates
4293
+ // above still contain every row and duration.
4294
+ const trackedModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, shellModelUsage, result.unrecordedModelUsage ?? result.modelUsage);
4295
+ if (trackedModelUsage) {
4296
+ for (const [model, usage] of Object.entries(trackedModelUsage)) {
4297
+ const pricing = this.options.pricing?.resolve(model);
4298
+ const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
4299
+ tracker.recordTurn({
4300
+ model,
4301
+ usage,
4302
+ ...(costUsd === undefined ? {} : { costUsd }),
4303
+ ...(usage.webSearchRequests === undefined
4304
+ ? {}
4305
+ : { webSearchRequests: usage.webSearchRequests }),
4306
+ });
4307
+ }
4308
+ }
4309
+ let combinedToolDurationMs = 0;
4310
+ for (const recoveryResult of recoveryResults) {
4311
+ combinedToolDurationMs = addToolDuration(recoveryResult.durationToolMs, combinedToolDurationMs);
4312
+ }
4313
+ combinedToolDurationMs = addToolDuration(result.durationToolMs, combinedToolDurationMs);
4314
+ const trackedDurationApiMs = result.unrecordedDurationApiMs ?? result.durationApiMs;
4315
+ const trackedDurationApiWithoutRetriesMs = result.unrecordedDurationApiWithoutRetriesMs ??
4316
+ result.durationApiWithoutRetriesMs;
4317
+ tracker.recordDurations({
4318
+ ...(trackedDurationApiMs === undefined
4264
4319
  ? {}
4265
- : { linesAdded: foregroundLineChanges.linesAdded }),
4266
- ...(foregroundLineChanges.linesRemoved === 0
4320
+ : { apiDurationMs: trackedDurationApiMs }),
4321
+ ...(trackedDurationApiWithoutRetriesMs === undefined
4267
4322
  ? {}
4268
- : { linesRemoved: foregroundLineChanges.linesRemoved }),
4323
+ : {
4324
+ apiDurationWithoutRetriesMs: trackedDurationApiWithoutRetriesMs,
4325
+ }),
4326
+ ...(combinedToolDurationMs === 0
4327
+ ? {}
4328
+ : { toolDurationMs: combinedToolDurationMs }),
4269
4329
  });
4270
- }
4271
- if (sessionMemory && finalLeafUuid) {
4330
+ foregroundLineChanges.add({
4331
+ isError: false,
4332
+ ...(result.linesAdded === undefined
4333
+ ? {}
4334
+ : { linesAdded: result.linesAdded }),
4335
+ ...(result.linesRemoved === undefined
4336
+ ? {}
4337
+ : { linesRemoved: result.linesRemoved }),
4338
+ });
4339
+ if (foregroundLineChanges.linesAdded !== 0 ||
4340
+ foregroundLineChanges.linesRemoved !== 0) {
4341
+ tracker.recordLineChanges({
4342
+ ...(foregroundLineChanges.linesAdded === 0
4343
+ ? {}
4344
+ : { linesAdded: foregroundLineChanges.linesAdded }),
4345
+ ...(foregroundLineChanges.linesRemoved === 0
4346
+ ? {}
4347
+ : { linesRemoved: foregroundLineChanges.linesRemoved }),
4348
+ });
4349
+ }
4272
4350
  const memorySnapshot = projectClaudeModelMessages(snapshot.entries);
4273
4351
  const providerVisibleMessages = [
4274
4352
  ...contextMessages,
4275
4353
  ...injectTurnContext(memorySnapshot),
4276
4354
  ];
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)}`,
4290
- });
4291
- }
4355
+ const currentContextTokens = contextEngine.report({
4356
+ messages: providerVisibleMessages,
4357
+ tools: definitions,
4358
+ })?.occupancyTokens ??
4359
+ estimateModelRequestTokens(providerVisibleMessages, definitions);
4360
+ await turnMemory.observeSuccess({
4361
+ ...(finalLeafUuid ? { messageId: finalLeafUuid } : {}),
4362
+ occupancyTokens: currentContextTokens,
4363
+ toolCalls: currentTurnToolCalls,
4364
+ messages: memorySnapshot,
4365
+ projectMessages: projectMemoryMessages(snapshot.entries),
4366
+ directMaintenance: projectMemoryMaintained,
4367
+ });
4368
+ turnCompleted = true;
4369
+ return {
4370
+ sessionId,
4371
+ text: structuredCapture && structuredCapture.calls === 1
4372
+ ? JSON.stringify(structuredCapture.value)
4373
+ : result.text,
4374
+ usage: totalUsage,
4375
+ ...(combinedDurationMs === undefined
4376
+ ? {}
4377
+ : { durationApiMs: combinedDurationMs }),
4378
+ ...(rawCostUsd === undefined ? {} : { costUsd: rawCostUsd }),
4379
+ ...(turnModelUsage ? { modelUsage: { ...turnModelUsage } } : {}),
4380
+ ...(structuredCapture && structuredCapture.calls === 1
4381
+ ? { structuredOutput: structuredCapture.value }
4382
+ : {}),
4383
+ };
4292
4384
  }
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');
4385
+ finally {
4386
+ if (!turnCompleted) {
4387
+ await this.hookLifecycle.end(sessionId, 'other');
4388
+ }
4318
4389
  }
4390
+ });
4391
+ if (leaseResult.status === 'conflict') {
4392
+ throw new Error(`Claude transcript append conflict: ${leaseResult.reason}`);
4319
4393
  }
4320
- });
4321
- if (leaseResult.status === 'conflict') {
4322
- throw new Error(`Claude transcript append conflict: ${leaseResult.reason}`);
4394
+ const result = leaseResult.value;
4395
+ controller.complete();
4396
+ return result;
4397
+ }
4398
+ catch (error) {
4399
+ controller.fail(error, signal);
4400
+ throw error;
4323
4401
  }
4324
- return leaseResult.value;
4325
4402
  }
4326
4403
  async ensureFileResources(sessionId, signal) {
4327
4404
  const resources = this.options.fileResources ?? [];
@@ -4416,11 +4493,12 @@ export class ClaudeSessionService {
4416
4493
  * recent suffix. Returns null to fall back to the existing full-compaction
4417
4494
  * behavior (missing/invalid watermark, empty projection, or oversized
4418
4495
  * projection). */
4419
- async selectMemoryPreservedCompact(sessionId, activeEntries) {
4496
+ async selectMemoryPreservedCompact(sessionId, activeEntries, options = {}) {
4420
4497
  const controller = this.sessionMemoryController(sessionId);
4421
4498
  if (controller === null)
4422
4499
  return null;
4423
- await controller.waitForCompact();
4500
+ if (options.sessionMemoryReady !== true)
4501
+ await controller.waitForCompact();
4424
4502
  let watermark = null;
4425
4503
  let memorySummary = '';
4426
4504
  try {
@@ -5052,12 +5130,37 @@ export class ClaudeSessionService {
5052
5130
  return 'corrupt';
5053
5131
  return this.schema.writeMode === 'read-write' ? 'ready' : 'read-only';
5054
5132
  }
5133
+ nativeSessionStatus(issue, entryCount) {
5134
+ if (issue?.kind === 'unsupported-version')
5135
+ return 'read-only';
5136
+ if (entryCount === 0 || issue)
5137
+ return 'corrupt';
5138
+ return 'read-only';
5139
+ }
5140
+ claudeSessionSummary(sessionId, index) {
5141
+ const metadata = reduceClaudeSessionMetadata(index.entries, sessionId);
5142
+ return {
5143
+ sessionId,
5144
+ ...sessionMetadataFields(metadata, { agentNameFallback: true }),
5145
+ lastPrompt: metadata.lastPrompt ?? null,
5146
+ updatedAt: index.updatedAt,
5147
+ status: this.sessionStatus(index.issue, index.entries.length),
5148
+ issue: index.issue,
5149
+ };
5150
+ }
5055
5151
  provider() {
5056
5152
  if (!this.options.provider) {
5057
5153
  throw new Error('A model provider is required for run and resume');
5058
5154
  }
5059
5155
  return this.options.provider;
5060
5156
  }
5157
+ async assertNativeWriteTargetIsLegacy(sessionId) {
5158
+ if (this.options.dataPlane !== 'native')
5159
+ return;
5160
+ const existing = await readNativeTranscript(this.paths(sessionId).sessionFile);
5161
+ if (existing.format === 'native')
5162
+ throw new Error('native transcript is read-only until native writes are enabled');
5163
+ }
5061
5164
  model() {
5062
5165
  return this.activeProvider?.model ?? this.options.provider?.model;
5063
5166
  }
@@ -5087,13 +5190,6 @@ export class ClaudeSessionService {
5087
5190
  const system = memory ? `${agent.body}\n\n${memory}` : agent.body;
5088
5191
  return system.trim() ? system : null;
5089
5192
  }
5090
- assembledSystemMessages(agent, messages) {
5091
- return agent &&
5092
- this.options.explicitSystemPrompt &&
5093
- this.options.agentSystemPromptOverridesExplicit
5094
- ? messages.slice(1)
5095
- : messages;
5096
- }
5097
5193
  contextBudget(provider) {
5098
5194
  if (this.options.contextBudget)
5099
5195
  return this.options.contextBudget;
@@ -5122,7 +5218,7 @@ export class ClaudeSessionService {
5122
5218
  return this.sessionMemoryProviderFactory() !== null;
5123
5219
  }
5124
5220
  sessionMemoryController(sessionId) {
5125
- if (!this.sessionMemoryEnabled() || !isClaudeSessionId(sessionId)) {
5221
+ if (!this.sessionMemoryEnabled() || !isSessionId(sessionId)) {
5126
5222
  return null;
5127
5223
  }
5128
5224
  let controller = this.sessionMemoryControllers.get(sessionId);