praxis-agent 0.35.0 → 0.37.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 (111) hide show
  1. package/README.md +20 -2
  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/durable-follow-up.d.ts +14 -0
  7. package/dist/application/durable-follow-up.js +52 -0
  8. package/dist/application/native-session-transcript.d.ts +72 -0
  9. package/dist/application/native-session-transcript.js +351 -0
  10. package/dist/application/native-sidechain-transcript.d.ts +34 -0
  11. package/dist/application/native-sidechain-transcript.js +156 -0
  12. package/dist/application/session-service.d.ts +23 -4
  13. package/dist/application/session-service.js +1130 -494
  14. package/dist/application/subagent-service.d.ts +5 -1
  15. package/dist/application/subagent-service.js +749 -320
  16. package/dist/application/team-agent-runtime.d.ts +29 -0
  17. package/dist/application/team-agent-runtime.js +169 -0
  18. package/dist/application/team-capability.d.ts +27 -0
  19. package/dist/application/team-capability.js +41 -0
  20. package/dist/application/team-lead-operations.d.ts +38 -0
  21. package/dist/application/team-lead-operations.js +155 -0
  22. package/dist/application/team-mailbox.d.ts +54 -0
  23. package/dist/application/team-mailbox.js +196 -0
  24. package/dist/application/team-manager.d.ts +113 -0
  25. package/dist/application/team-manager.js +681 -0
  26. package/dist/application/team-workspace.d.ts +29 -0
  27. package/dist/application/team-workspace.js +197 -0
  28. package/dist/application/top-level-agent-manager.d.ts +1 -0
  29. package/dist/application/top-level-agent-manager.js +1025 -403
  30. package/dist/application/transcript-projection.d.ts +35 -0
  31. package/dist/application/transcript-projection.js +139 -0
  32. package/dist/application/turn-lifecycle.d.ts +2 -0
  33. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  34. package/dist/application/turn-memory-coordinator.js +98 -0
  35. package/dist/cli/agents-dashboard.js +10 -5
  36. package/dist/cli/interactive.d.ts +7 -0
  37. package/dist/cli/interactive.js +645 -334
  38. package/dist/cli/tui/claude-style.d.ts +8 -25
  39. package/dist/cli/tui/claude-style.js +299 -277
  40. package/dist/cli/tui/decision-surface-model.d.ts +71 -0
  41. package/dist/cli/tui/decision-surface-model.js +131 -0
  42. package/dist/cli/tui/decision-surface.d.ts +7 -0
  43. package/dist/cli/tui/decision-surface.js +41 -0
  44. package/dist/cli/tui/help-surface-model.d.ts +68 -0
  45. package/dist/cli/tui/help-surface-model.js +131 -0
  46. package/dist/cli/tui/mcp-panel.js +1 -2
  47. package/dist/cli/tui/permission-surface-model.d.ts +195 -0
  48. package/dist/cli/tui/permission-surface-model.js +360 -0
  49. package/dist/cli/tui/permission-surface.d.ts +7 -0
  50. package/dist/cli/tui/permission-surface.js +69 -0
  51. package/dist/cli/tui/presentation-environment.d.ts +32 -0
  52. package/dist/cli/tui/presentation-environment.js +149 -0
  53. package/dist/cli/tui/task-panel.js +10 -1
  54. package/dist/cli/tui/theme.d.ts +46 -3
  55. package/dist/cli/tui/theme.js +414 -56
  56. package/dist/cli/tui/tool-permission.js +16 -8
  57. package/dist/cli/tui/transcript-presentation.d.ts +19 -1
  58. package/dist/cli/tui/transcript-presentation.js +26 -6
  59. package/dist/cli/tui/transcript-viewport.d.ts +18 -0
  60. package/dist/cli/tui/transcript-viewport.js +517 -111
  61. package/dist/cli/tui/transcript-window-model.d.ts +108 -0
  62. package/dist/cli/tui/transcript-window-model.js +772 -0
  63. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  64. package/dist/cli/tui/tui-interaction-router.js +127 -0
  65. package/dist/cli/tui/tui-screen-model.d.ts +114 -0
  66. package/dist/cli/tui/tui-screen-model.js +210 -0
  67. package/dist/cli/tui/tui-view-model.d.ts +2 -49
  68. package/dist/cli/tui/tui-view-model.js +1 -68
  69. package/dist/cli-runtime.d.ts +4 -2
  70. package/dist/cli-runtime.js +397 -32
  71. package/dist/compatibility/claude/transcript-codec.js +6 -0
  72. package/dist/core/agent-orchestration.d.ts +31 -0
  73. package/dist/core/agent-orchestration.js +235 -0
  74. package/dist/core/permission-policy.d.ts +4 -0
  75. package/dist/core/permission-policy.js +21 -0
  76. package/dist/core/runtime.d.ts +6 -0
  77. package/dist/core/runtime.js +20 -2
  78. package/dist/core/team-mailbox.d.ts +54 -0
  79. package/dist/core/team-mailbox.js +212 -0
  80. package/dist/core/team-ownership.d.ts +84 -0
  81. package/dist/core/team-ownership.js +505 -0
  82. package/dist/core/tool-execution-scheduler.js +6 -1
  83. package/dist/core/transcript-event.d.ts +5 -1
  84. package/dist/core/transcript-event.js +12 -0
  85. package/dist/persistence/claude-job-store.d.ts +58 -0
  86. package/dist/persistence/claude-job-store.js +483 -1
  87. package/dist/persistence/claude-transcript-store.d.ts +2 -1
  88. package/dist/persistence/claude-transcript-store.js +2 -10
  89. package/dist/persistence/native-team-mailbox.d.ts +68 -0
  90. package/dist/persistence/native-team-mailbox.js +542 -0
  91. package/dist/persistence/native-team-store.d.ts +34 -0
  92. package/dist/persistence/native-team-store.js +257 -0
  93. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  94. package/dist/persistence/native-transcript-reader.js +283 -0
  95. package/dist/persistence/native-transcript-store.d.ts +69 -0
  96. package/dist/persistence/native-transcript-store.js +321 -0
  97. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  98. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  99. package/dist/persistence/transcript-file-append.d.ts +2 -0
  100. package/dist/persistence/transcript-file-append.js +10 -0
  101. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  102. package/dist/platform/exclusive-file-lease.js +3 -0
  103. package/dist/tools/claude-capabilities.d.ts +4 -0
  104. package/dist/tools/claude-capabilities.js +18 -2
  105. package/dist/tools/team-lead-tools.d.ts +19 -0
  106. package/dist/tools/team-lead-tools.js +479 -0
  107. package/dist/tools/team-member-tools.d.ts +21 -0
  108. package/dist/tools/team-member-tools.js +259 -0
  109. package/package.json +1 -1
  110. package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
  111. package/dist/cli/tui/fullscreen-renderer.js +0 -13
@@ -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,14 +23,21 @@ 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';
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';
27
30
  import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
28
31
  import { assembleContextSnapshot } from '../core/prompt-composer.js';
29
32
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
30
33
  import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
31
34
  import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
32
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, unresolvedActiveToolCallIds, } from './transcript-projection.js';
33
38
  import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
39
+ import { NativeTranscriptStore } from '../persistence/native-transcript-store.js';
40
+ import { NativeSessionTranscript, } from './native-session-transcript.js';
34
41
  import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
35
42
  import { ModelCompactor } from './model-compactor.js';
36
43
  import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
@@ -49,6 +56,7 @@ import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
49
56
  import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
50
57
  import { generateToolUseSummary } from './tool-use-summary.js';
51
58
  import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
59
+ import { DurableFollowUpTracker } from './durable-follow-up.js';
52
60
  function agentPermissionMode(mode) {
53
61
  return mode === undefined || mode === 'manual' ? 'default' : mode;
54
62
  }
@@ -78,6 +86,42 @@ function mainAgentToolNames(tools, agent) {
78
86
  .map(({ name }) => name)
79
87
  .filter((name) => (!requested || requested.has(name)) && !disallowed.has(name));
80
88
  }
89
+ function sessionMetadataFields(metadata, options) {
90
+ const name = metadata.title ??
91
+ (options.agentNameFallback ? metadata.agentName : undefined);
92
+ const prLink = metadata.prLink;
93
+ return {
94
+ ...(name === undefined ? {} : { name }),
95
+ ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
96
+ ...(metadata.agentName === undefined
97
+ ? {}
98
+ : { agentName: metadata.agentName }),
99
+ ...(metadata.agentColor === undefined
100
+ ? {}
101
+ : { agentColor: metadata.agentColor }),
102
+ ...(metadata.agentSetting === undefined
103
+ ? {}
104
+ : { agentSetting: metadata.agentSetting }),
105
+ ...(metadata.permissionMode === undefined
106
+ ? {}
107
+ : { permissionMode: metadata.permissionMode }),
108
+ ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
109
+ ...(prLink === undefined
110
+ ? {}
111
+ : {
112
+ prNumber: prLink.prNumber,
113
+ ...(prLink.prUrl === undefined ? {} : { prUrl: prLink.prUrl }),
114
+ ...(prLink.prRepository === undefined
115
+ ? {}
116
+ : { prRepository: prLink.prRepository }),
117
+ }),
118
+ };
119
+ }
120
+ function isSessionCandidateError(error) {
121
+ return (error instanceof ClaudeSessionIndexCandidateError ||
122
+ error instanceof NativeTranscriptIndexCandidateError ||
123
+ ['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? ''));
124
+ }
81
125
  const emptyToolRegistry = {
82
126
  definitions: () => [],
83
127
  schedulingPolicy: () => ({ concurrency: 'exclusive' }),
@@ -671,10 +715,13 @@ export class ClaudeSessionService {
671
715
  sessionMemoryControllers = new Map();
672
716
  resolvedSessionMemoryProvider;
673
717
  hookLifecycle;
718
+ leadOperations;
674
719
  fileChangeWatcher;
675
720
  runtimeCwd;
676
721
  constructor(options) {
677
722
  this.options = options;
723
+ this.assertNativeTranscriptOptions();
724
+ this.leadOperations = options.teamLeadOperations ?? null;
678
725
  this.hookLifecycle = new HookLifecycle(options.hooks, options.eventSink);
679
726
  this.runtimeCwd = options.workspace?.cwd() ?? options.cwd;
680
727
  this.fileChangeWatcher = options.hooks
@@ -901,6 +948,7 @@ export class ClaudeSessionService {
901
948
  await this.closeCostSavePromise;
902
949
  this.mcpClosePromise ??= this.options.mcp?.close?.() ?? Promise.resolve();
903
950
  await this.mcpClosePromise;
951
+ await this.leadOperations?.close();
904
952
  }
905
953
  async transitionHookSession(sessionId, reason) {
906
954
  await this.hookLifecycle.transition(sessionId, reason);
@@ -1078,6 +1126,9 @@ export class ClaudeSessionService {
1078
1126
  throw new Error('A model provider is required to execute hosted Agent tools');
1079
1127
  })(),
1080
1128
  persistence: this.options.sessionPersistence === false ? 'memory' : 'disk',
1129
+ ...(this.options.experimentalNativeTranscriptWrites === true
1130
+ ? { experimentalNativeTranscriptWrites: true }
1131
+ : {}),
1081
1132
  ...(this.options.providerForModel
1082
1133
  ? { providerForModel: this.options.providerForModel }
1083
1134
  : {}),
@@ -1179,7 +1230,9 @@ export class ClaudeSessionService {
1179
1230
  const interactiveRegistry = this.options.interactiveTools
1180
1231
  ? this.options.interactiveTools.registry(messageRegistry, sessionId)
1181
1232
  : messageRegistry;
1182
- const capabilityRegistry = new ClaudeCapabilityToolRegistry(interactiveRegistry, capabilities);
1233
+ const teamToolNames = this.capabilityToolNames(this.options.teamToolNames, capabilities);
1234
+ const teamRegistry = this.teamRegistry(interactiveRegistry, sessionId, teamToolNames);
1235
+ const capabilityRegistry = new ClaudeCapabilityToolRegistry(teamRegistry, capabilities);
1183
1236
  const preferredOrder = [
1184
1237
  'Agent',
1185
1238
  'AskUserQuestion',
@@ -1212,6 +1265,11 @@ export class ClaudeSessionService {
1212
1265
  'ScheduleWakeup',
1213
1266
  'Monitor',
1214
1267
  'PushNotification',
1268
+ 'TeamCreate',
1269
+ 'TeamResume',
1270
+ 'TeamList',
1271
+ 'TeamAccept',
1272
+ 'TeamStop',
1215
1273
  ];
1216
1274
  const hostedRegistry = {
1217
1275
  definitions: () => {
@@ -1554,19 +1612,62 @@ export class ClaudeSessionService {
1554
1612
  }
1555
1613
  catch (error) {
1556
1614
  if (error.code === 'ENOENT')
1557
- return [];
1558
- throw error;
1615
+ names = [];
1616
+ else
1617
+ throw error;
1559
1618
  }
1560
1619
  const discoveredSessionIds = names
1561
1620
  .filter((name) => extname(name) === '.jsonl')
1562
1621
  .map((name) => basename(name, '.jsonl'))
1563
- .filter((sessionId) => isClaudeSessionId(sessionId));
1622
+ .filter((sessionId) => isSessionId(sessionId));
1564
1623
  const sessionIds = [
1565
1624
  ...new Set([
1566
1625
  ...discoveredSessionIds,
1567
1626
  ...this.explicitSessionFiles.keys(),
1568
1627
  ]),
1569
1628
  ];
1629
+ if (this.options.dataPlane === 'native') {
1630
+ const nativeResults = await readNativeTranscriptIndexes(sessionIds.map((sessionId) => ({
1631
+ sessionId,
1632
+ path: this.explicitSessionFiles.get(sessionId) ??
1633
+ join(projectRoot, `${sessionId}.jsonl`),
1634
+ })));
1635
+ const legacyResults = await readClaudeSessionIndexes(nativeResults.flatMap((result) => 'result' in result && result.result.format === 'legacy'
1636
+ ? [{ sessionId: result.sessionId, path: result.path }]
1637
+ : []), this.schema);
1638
+ const legacyBySessionId = new Map(legacyResults.map((result) => [result.sessionId, result]));
1639
+ const nativeSummaries = nativeResults.map((result) => {
1640
+ try {
1641
+ if ('error' in result)
1642
+ throw result.error;
1643
+ const file = result.result;
1644
+ if (file.format === 'legacy') {
1645
+ const legacy = legacyBySessionId.get(result.sessionId);
1646
+ if (!legacy)
1647
+ throw new Error(`Missing legacy transcript index for ${result.sessionId}`);
1648
+ if ('error' in legacy)
1649
+ throw legacy.error;
1650
+ return this.claudeSessionSummary(result.sessionId, legacy.index);
1651
+ }
1652
+ return {
1653
+ sessionId: result.sessionId,
1654
+ lastPrompt: lastUserPrompt(file.records.map((record) => record.event)),
1655
+ updatedAt: file.updatedAt,
1656
+ status: this.nativeSessionStatus(file.issue, file.records.length),
1657
+ issue: file.issue,
1658
+ };
1659
+ }
1660
+ catch (error) {
1661
+ if (isSessionCandidateError(error))
1662
+ return null;
1663
+ throw error;
1664
+ }
1665
+ });
1666
+ return nativeSummaries
1667
+ .filter((summary) => summary !== null)
1668
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) ||
1669
+ left.sessionId.localeCompare(right.sessionId));
1670
+ }
1570
1671
  if (discoveredRoot !== undefined) {
1571
1672
  for (const sessionId of discoveredSessionIds) {
1572
1673
  this.discoveredProjectRoots.set(sessionId, projectRoot);
@@ -1581,46 +1682,11 @@ export class ClaudeSessionService {
1581
1682
  try {
1582
1683
  if ('error' in result)
1583
1684
  throw result.error;
1584
- const { sessionId, index } = result;
1585
- const metadata = reduceClaudeSessionMetadata(index.entries, sessionId);
1586
- const name = metadata.title ?? metadata.agentName;
1587
- const prLink = metadata.prLink;
1588
- return {
1589
- sessionId,
1590
- ...(name === undefined ? {} : { name }),
1591
- ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
1592
- ...(metadata.agentName === undefined
1593
- ? {}
1594
- : { agentName: metadata.agentName }),
1595
- ...(metadata.agentColor === undefined
1596
- ? {}
1597
- : { agentColor: metadata.agentColor }),
1598
- ...(metadata.agentSetting === undefined
1599
- ? {}
1600
- : { agentSetting: metadata.agentSetting }),
1601
- ...(metadata.permissionMode === undefined
1602
- ? {}
1603
- : { permissionMode: metadata.permissionMode }),
1604
- ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
1605
- lastPrompt: metadata.lastPrompt ?? null,
1606
- updatedAt: index.updatedAt,
1607
- status: this.sessionStatus(index.issue, index.entries.length),
1608
- issue: index.issue,
1609
- ...(prLink
1610
- ? {
1611
- prNumber: prLink.prNumber,
1612
- prUrl: prLink.prUrl,
1613
- prRepository: prLink.prRepository,
1614
- }
1615
- : {}),
1616
- };
1685
+ return this.claudeSessionSummary(result.sessionId, result.index);
1617
1686
  }
1618
1687
  catch (error) {
1619
- if (error instanceof ClaudeSessionIndexCandidateError)
1688
+ if (isSessionCandidateError(error))
1620
1689
  return null;
1621
- if (['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? '')) {
1622
- return null;
1623
- }
1624
1690
  throw error;
1625
1691
  }
1626
1692
  });
@@ -1631,6 +1697,37 @@ export class ClaudeSessionService {
1631
1697
  }
1632
1698
  async inspect(sessionId) {
1633
1699
  this.assertSessionPersistence();
1700
+ if (this.options.dataPlane === 'native') {
1701
+ const paths = this.paths(sessionId);
1702
+ const file = await readNativeTranscript(paths.sessionFile);
1703
+ if (file.format === 'legacy') {
1704
+ const recovery = await this.store(sessionId).loadReadOnly();
1705
+ const metadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
1706
+ return {
1707
+ sessionId,
1708
+ ...sessionMetadataFields(metadata, { agentNameFallback: false }),
1709
+ lastPrompt: metadata.lastPrompt ?? null,
1710
+ updatedAt: file.updatedAt,
1711
+ status: this.sessionStatus(recovery.issue, recovery.entries.length),
1712
+ issue: recovery.issue,
1713
+ writeMode: this.schema.writeMode,
1714
+ entryCount: recovery.entries.length,
1715
+ byteLength: file.byteLength,
1716
+ newlineTerminated: file.newlineTerminated,
1717
+ };
1718
+ }
1719
+ return {
1720
+ sessionId,
1721
+ lastPrompt: lastUserPrompt(file.records.map((record) => record.event)),
1722
+ updatedAt: file.updatedAt,
1723
+ status: this.nativeSessionStatus(file.issue, file.records.length),
1724
+ issue: file.issue,
1725
+ writeMode: file.writeMode,
1726
+ entryCount: file.records.length,
1727
+ byteLength: file.byteLength,
1728
+ newlineTerminated: file.newlineTerminated,
1729
+ };
1730
+ }
1634
1731
  await this.discoverProjectRoot(sessionId);
1635
1732
  const paths = this.paths(sessionId);
1636
1733
  let metadata;
@@ -1645,30 +1742,9 @@ export class ClaudeSessionService {
1645
1742
  }
1646
1743
  const recovery = await this.store(sessionId).loadReadOnly();
1647
1744
  const sessionMetadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
1648
- const prLink = sessionMetadata.prLink;
1649
1745
  return {
1650
1746
  sessionId,
1651
- ...(sessionMetadata.title === undefined
1652
- ? {}
1653
- : { name: sessionMetadata.title }),
1654
- ...(sessionMetadata.tag === undefined
1655
- ? {}
1656
- : { tag: sessionMetadata.tag }),
1657
- ...(sessionMetadata.agentName === undefined
1658
- ? {}
1659
- : { agentName: sessionMetadata.agentName }),
1660
- ...(sessionMetadata.agentColor === undefined
1661
- ? {}
1662
- : { agentColor: sessionMetadata.agentColor }),
1663
- ...(sessionMetadata.agentSetting === undefined
1664
- ? {}
1665
- : { agentSetting: sessionMetadata.agentSetting }),
1666
- ...(sessionMetadata.permissionMode === undefined
1667
- ? {}
1668
- : { permissionMode: sessionMetadata.permissionMode }),
1669
- ...(sessionMetadata.mode === undefined
1670
- ? {}
1671
- : { mode: sessionMetadata.mode }),
1747
+ ...sessionMetadataFields(sessionMetadata, { agentNameFallback: false }),
1672
1748
  lastPrompt: sessionMetadata.lastPrompt ?? null,
1673
1749
  updatedAt: metadata.mtime.toISOString(),
1674
1750
  status: this.sessionStatus(recovery.issue, recovery.entries.length),
@@ -1678,13 +1754,6 @@ export class ClaudeSessionService {
1678
1754
  entryCount: recovery.entries.length,
1679
1755
  byteLength: recovery.tail.byteLength,
1680
1756
  newlineTerminated: recovery.tail.newlineTerminated,
1681
- ...(prLink
1682
- ? {
1683
- prNumber: prLink.prNumber,
1684
- prUrl: prLink.prUrl,
1685
- prRepository: prLink.prRepository,
1686
- }
1687
- : {}),
1688
1757
  };
1689
1758
  }
1690
1759
  async readEffectiveAgentColor(sessionId) {
@@ -1694,6 +1763,8 @@ export class ClaudeSessionService {
1694
1763
  }
1695
1764
  async export(sessionId) {
1696
1765
  this.assertSessionPersistence();
1766
+ if (this.options.dataPlane === 'native')
1767
+ return exportNativeTranscript(this.paths(sessionId).sessionFile);
1697
1768
  await this.discoverProjectRoot(sessionId);
1698
1769
  try {
1699
1770
  return await this.store(sessionId).exportReadOnly();
@@ -1709,17 +1780,40 @@ export class ClaudeSessionService {
1709
1780
  this.assertSessionPersistence();
1710
1781
  const path = resolve(requestedPath);
1711
1782
  const pathMetadata = await lstat(path);
1783
+ const dataPlaneName = this.options.dataPlane === 'native' ? 'Native' : 'Claude';
1712
1784
  if (!pathMetadata.isFile()) {
1713
- throw new Error(`Claude resume path must be a regular JSONL file: ${path}`);
1785
+ throw new Error(`${dataPlaneName} resume path must be a regular JSONL file: ${path}`);
1714
1786
  }
1715
1787
  if (extname(path).toLowerCase() !== '.jsonl') {
1716
- throw new Error(`Claude resume path must end in .jsonl: ${path}`);
1788
+ throw new Error(`${dataPlaneName} resume path must end in .jsonl: ${path}`);
1717
1789
  }
1718
1790
  const sessionId = basename(path, '.jsonl');
1719
- if (!isClaudeSessionId(sessionId)) {
1720
- throw new Error(`Claude resume path filename must be a session UUID: ${path}`);
1791
+ if (!isSessionId(sessionId)) {
1792
+ throw new Error(`${dataPlaneName} resume path filename must be a session UUID: ${path}`);
1721
1793
  }
1722
1794
  const canonicalPath = await realpath(path);
1795
+ if (this.options.dataPlane === 'native') {
1796
+ const detected = await readNativeTranscript(canonicalPath);
1797
+ if (detected.format === 'native') {
1798
+ if (detected.issue?.kind === 'unsupported-version')
1799
+ throw new Error('Native resume transcript version is unsupported');
1800
+ if (detected.issue ||
1801
+ !detected.newlineTerminated ||
1802
+ detected.records.length === 0)
1803
+ throw new Error('Native resume transcript must be a complete non-empty newline-terminated file');
1804
+ if (detected.records.some((record) => record.event.sessionId !== sessionId))
1805
+ throw new Error('Native resume transcript contains a different sessionId');
1806
+ this.explicitSessionFiles.set(sessionId, canonicalPath);
1807
+ this.discoveredProjectRoots.set(sessionId, dirname(canonicalPath));
1808
+ return {
1809
+ sessionId,
1810
+ lastPrompt: lastUserPrompt(detected.records.map((record) => record.event)),
1811
+ updatedAt: detected.updatedAt,
1812
+ status: 'read-only',
1813
+ issue: detected.issue,
1814
+ };
1815
+ }
1816
+ }
1723
1817
  const exact = this.pathsForCwd(sessionId, this.activeCwd());
1724
1818
  const candidate = new ClaudeTranscriptStore({
1725
1819
  sessionFile: canonicalPath,
@@ -1733,6 +1827,10 @@ export class ClaudeSessionService {
1733
1827
  if (snapshot.entries.length === 0) {
1734
1828
  throw new Error('Claude resume transcript must not be empty');
1735
1829
  }
1830
+ if (this.options.dataPlane === 'native' &&
1831
+ this.schema.writeMode !== 'read-write') {
1832
+ throw new Error('Native legacy resume transcript is read-only');
1833
+ }
1736
1834
  let matchingSessionIdentity = false;
1737
1835
  for (const entry of snapshot.entries) {
1738
1836
  if (entry.sessionId === sessionId)
@@ -1750,40 +1848,21 @@ export class ClaudeSessionService {
1750
1848
  this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
1751
1849
  this.discoveredProjectRoots.set(sessionId, dirname(canonicalPath));
1752
1850
  const metadata = reduceClaudeSessionMetadata(snapshot.entries, sessionId);
1753
- const prLink = metadata.prLink;
1754
1851
  return {
1755
1852
  sessionId,
1756
- ...(metadata.title === undefined ? {} : { name: metadata.title }),
1757
- ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
1758
- ...(metadata.agentName === undefined
1759
- ? {}
1760
- : { agentName: metadata.agentName }),
1761
- ...(metadata.agentColor === undefined
1762
- ? {}
1763
- : { agentColor: metadata.agentColor }),
1764
- ...(metadata.agentSetting === undefined
1765
- ? {}
1766
- : { agentSetting: metadata.agentSetting }),
1767
- ...(metadata.permissionMode === undefined
1768
- ? {}
1769
- : { permissionMode: metadata.permissionMode }),
1770
- ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
1853
+ ...sessionMetadataFields(metadata, { agentNameFallback: false }),
1771
1854
  lastPrompt: metadata.lastPrompt ?? null,
1772
1855
  updatedAt: pathMetadata.mtime.toISOString(),
1773
1856
  status: this.sessionStatus(null, snapshot.entries.length),
1774
1857
  issue: null,
1775
- ...(prLink === undefined
1776
- ? {}
1777
- : {
1778
- prNumber: prLink.prNumber,
1779
- ...(prLink.prUrl === undefined ? {} : { prUrl: prLink.prUrl }),
1780
- ...(prLink.prRepository === undefined
1781
- ? {}
1782
- : { prRepository: prLink.prRepository }),
1783
- }),
1784
1858
  };
1785
1859
  }
1786
1860
  async transcript(sessionId, resumeSessionAt) {
1861
+ if (this.options.dataPlane === 'native') {
1862
+ const file = await readNativeTranscript(this.paths(sessionId).sessionFile);
1863
+ if (file.format === 'native')
1864
+ return projectTranscriptDisplay(file.records.map((record) => record.event), resumeSessionAt);
1865
+ }
1787
1866
  await this.discoverProjectRoot(sessionId);
1788
1867
  try {
1789
1868
  const recovery = await this.store(sessionId).loadReadOnly();
@@ -1804,6 +1883,25 @@ export class ClaudeSessionService {
1804
1883
  }
1805
1884
  async interruption(sessionId) {
1806
1885
  this.assertSessionPersistence();
1886
+ if (this.options.dataPlane === 'native') {
1887
+ const sessionPaths = this.paths(sessionId);
1888
+ const nativeTranscript = new NativeSessionTranscript({
1889
+ sessionId,
1890
+ store: new NativeTranscriptStore({
1891
+ transcriptFile: sessionPaths.sessionFile,
1892
+ lockFile: join(sessionPaths.praxisRoot, 'locks', `${sessionId}.lock`),
1893
+ }),
1894
+ });
1895
+ return nativeTranscript.withLease({ kind: 'resume' }, async (lease) => {
1896
+ const interruption = lease.interruption();
1897
+ if (interruption.kind === 'recoverable-tools' ||
1898
+ interruption.kind === 'indeterminate-tools')
1899
+ return { kind: 'interrupted-turn' };
1900
+ if (interruption.kind === 'interrupted-prompt')
1901
+ return { kind: 'interrupted-prompt', prompt: interruption.prompt };
1902
+ return { kind: interruption.kind };
1903
+ });
1904
+ }
1807
1905
  await this.discoverProjectRoot(sessionId);
1808
1906
  const snapshot = await this.store(sessionId).loadReadOnly();
1809
1907
  if (snapshot.entries.length === 0) {
@@ -2163,6 +2261,97 @@ export class ClaudeSessionService {
2163
2261
  return { resumeSessionAt, entryCount: result.value.entries.length };
2164
2262
  }
2165
2263
  async compact(sessionId, signal, selection) {
2264
+ if (this.nativeTranscriptWritesEnabled()) {
2265
+ if (selection)
2266
+ throw new Error('Native compaction supports only the full active branch');
2267
+ this.assertSessionPersistence();
2268
+ const provider = this.provider();
2269
+ await this.activateSessionCostTracker(sessionId);
2270
+ const sessionPaths = this.paths(sessionId);
2271
+ const nativeTranscript = new NativeSessionTranscript({
2272
+ sessionId,
2273
+ store: new NativeTranscriptStore({
2274
+ transcriptFile: sessionPaths.sessionFile,
2275
+ lockFile: join(sessionPaths.praxisRoot, 'locks', `${sessionId}.lock`),
2276
+ }),
2277
+ });
2278
+ return nativeTranscript.withLease({ kind: 'resume' }, async (lease) => {
2279
+ const messages = lease.activeMessages();
2280
+ if (messages.length === 0)
2281
+ throw new Error('Cannot compact an empty native transcript');
2282
+ const preTokens = estimateModelRequestTokens(messages);
2283
+ this.options.eventSink?.({ type: 'state', state: 'compacting' });
2284
+ const contextWindowTokens = this.contextBudget(provider)?.contextWindowTokens ??
2285
+ provider.capabilities.contextWindowTokens ??
2286
+ 200_000;
2287
+ const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
2288
+ messages,
2289
+ targetTokens: Math.min(8192, Math.max(1, Math.floor(contextWindowTokens / 4))),
2290
+ contextWindowTokens,
2291
+ ...(signal ? { signal } : {}),
2292
+ });
2293
+ if (signal?.aborted)
2294
+ throw new AgentRunCancelledError();
2295
+ const { durationMs, durationWithoutRetriesMs } = requireCompactionDurations(compacted);
2296
+ requireManualCompactUsage(compacted.usage);
2297
+ const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
2298
+ ? compacted.model
2299
+ : provider.model;
2300
+ const meaningfulMetering = compacted.summary.trim().length > 0 &&
2301
+ (hasNonZeroUsage(compacted.usage) || durationMs > 0);
2302
+ if (meaningfulMetering &&
2303
+ (compactModel === undefined || compactModel.trim() === ''))
2304
+ throw new Error('Manual compact usage requires a nonblank model identity');
2305
+ const tracker = this.sessionCostTrackers.get(sessionId);
2306
+ if (!tracker)
2307
+ throw new Error(`Session cost tracker is not active for session ${sessionId}`);
2308
+ let meteringTurnInput;
2309
+ if (meaningfulMetering &&
2310
+ compactModel !== undefined &&
2311
+ compactModel.trim() !== '') {
2312
+ const pricing = this.options.pricing?.resolve(compactModel);
2313
+ const costUsd = pricing
2314
+ ? usageCostUsd(compacted.usage, pricing)
2315
+ : undefined;
2316
+ meteringTurnInput = {
2317
+ model: compactModel,
2318
+ usage: compacted.usage,
2319
+ ...(costUsd === undefined ? {} : { costUsd }),
2320
+ ...(compacted.usage.webSearchRequests === undefined
2321
+ ? {}
2322
+ : { webSearchRequests: compacted.usage.webSearchRequests }),
2323
+ apiDurationMs: durationMs,
2324
+ apiDurationWithoutRetriesMs: durationWithoutRetriesMs,
2325
+ };
2326
+ const preflight = new ClaudeSessionCostTracker({
2327
+ sessionId,
2328
+ restored: tracker.snapshot(),
2329
+ });
2330
+ preflight.recordTurn(meteringTurnInput);
2331
+ }
2332
+ const postTokens = estimateModelRequestTokens([
2333
+ { role: 'user', content: compacted.summary },
2334
+ ]);
2335
+ if (signal?.aborted)
2336
+ throw new AgentRunCancelledError();
2337
+ const ids = await lease.appendCompaction({
2338
+ summary: compacted.summary,
2339
+ trigger: 'manual',
2340
+ preTokens,
2341
+ postTokens,
2342
+ durationMs,
2343
+ });
2344
+ if (meteringTurnInput !== undefined)
2345
+ tracker.recordTurn(meteringTurnInput);
2346
+ this.options.eventSink?.({
2347
+ type: 'compact-boundary',
2348
+ trigger: 'manual',
2349
+ preTokens,
2350
+ uuid: ids.boundaryId,
2351
+ });
2352
+ return { summary: compacted.summary, usage: compacted.usage, preTokens };
2353
+ });
2354
+ }
2166
2355
  this.assertWritable();
2167
2356
  const provider = this.provider();
2168
2357
  const result = await this.turnStore(sessionId).withLease(async (lease) => {
@@ -2370,7 +2559,38 @@ export class ClaudeSessionService {
2370
2559
  return result.value;
2371
2560
  }
2372
2561
  async fork(parentSessionId, sessionId = randomUUID(), resumeSessionAt) {
2562
+ if (this.nativeTranscriptWritesEnabled()) {
2563
+ this.assertSessionPersistence();
2564
+ const sourcePaths = this.paths(parentSessionId);
2565
+ const targetPaths = this.paths(sessionId);
2566
+ const source = new NativeSessionTranscript({
2567
+ sessionId: parentSessionId,
2568
+ store: new NativeTranscriptStore({
2569
+ transcriptFile: sourcePaths.sessionFile,
2570
+ lockFile: join(sourcePaths.praxisRoot, 'locks', `${parentSessionId}.lock`),
2571
+ }),
2572
+ });
2573
+ const target = new NativeSessionTranscript({
2574
+ sessionId,
2575
+ store: new NativeTranscriptStore({
2576
+ transcriptFile: targetPaths.sessionFile,
2577
+ lockFile: join(targetPaths.praxisRoot, 'locks', `${sessionId}.lock`),
2578
+ }),
2579
+ });
2580
+ await source.forkTo(target, {
2581
+ ...(resumeSessionAt === undefined
2582
+ ? {}
2583
+ : { atEventId: resumeSessionAt }),
2584
+ ensureExisting: false,
2585
+ });
2586
+ this.options.contextAssembler?.invalidate?.({
2587
+ lifecycleId: sessionId,
2588
+ reason: 'fork',
2589
+ });
2590
+ return { sessionId, parentSessionId };
2591
+ }
2373
2592
  this.assertSessionPersistence();
2593
+ await this.assertNativeWriteTargetIsLegacy(parentSessionId);
2374
2594
  this.assertWritable();
2375
2595
  const entries = await this.nativeForkEntries(parentSessionId, sessionId, resumeSessionAt);
2376
2596
  const result = await this.store(sessionId).create(entries);
@@ -2384,6 +2604,39 @@ export class ClaudeSessionService {
2384
2604
  return { sessionId, parentSessionId };
2385
2605
  }
2386
2606
  async ensureFork(parentSessionId, sessionId, checkpoint) {
2607
+ if (this.nativeTranscriptWritesEnabled()) {
2608
+ this.assertSessionPersistence();
2609
+ const sourcePaths = this.paths(parentSessionId);
2610
+ const targetPaths = this.paths(sessionId);
2611
+ const source = new NativeSessionTranscript({
2612
+ sessionId: parentSessionId,
2613
+ store: new NativeTranscriptStore({
2614
+ transcriptFile: sourcePaths.sessionFile,
2615
+ lockFile: join(sourcePaths.praxisRoot, 'locks', `${parentSessionId}.lock`),
2616
+ }),
2617
+ });
2618
+ const target = new NativeSessionTranscript({
2619
+ sessionId,
2620
+ store: new NativeTranscriptStore({
2621
+ transcriptFile: targetPaths.sessionFile,
2622
+ lockFile: join(targetPaths.praxisRoot, 'locks', `${sessionId}.lock`),
2623
+ }),
2624
+ });
2625
+ await source.forkTo(target, {
2626
+ ...(checkpoint?.resumeSessionAt === undefined
2627
+ ? {}
2628
+ : { atEventId: checkpoint.resumeSessionAt }),
2629
+ ...(checkpoint?.entryCount === undefined
2630
+ ? {}
2631
+ : { recordCount: checkpoint.entryCount }),
2632
+ ensureExisting: true,
2633
+ });
2634
+ this.options.contextAssembler?.invalidate?.({
2635
+ lifecycleId: sessionId,
2636
+ reason: 'fork',
2637
+ });
2638
+ return { sessionId, parentSessionId };
2639
+ }
2387
2640
  this.assertSessionPersistence();
2388
2641
  this.assertWritable();
2389
2642
  const expected = await this.nativeForkEntries(parentSessionId, sessionId, checkpoint?.resumeSessionAt, checkpoint?.entryCount);
@@ -2701,7 +2954,12 @@ export class ClaudeSessionService {
2701
2954
  const skipUserPrompt = submission.kind === 'retry';
2702
2955
  const controller = new TurnTerminalController(this.options.eventSink ?? (() => undefined));
2703
2956
  try {
2704
- this.assertWritable();
2957
+ if (this.nativeTranscriptWritesEnabled() && name !== undefined) {
2958
+ throw new Error('experimental native session names are not enabled');
2959
+ }
2960
+ if (requireExisting)
2961
+ await this.assertNativeWriteTargetIsLegacy(sessionId);
2962
+ this.assertTurnWritable();
2705
2963
  if (prompt.length === 0 && images.length === 0 && documents.length === 0)
2706
2964
  throw new Error('Prompt must not be empty');
2707
2965
  if (name !== undefined && name.length === 0) {
@@ -2711,7 +2969,8 @@ export class ClaudeSessionService {
2711
2969
  throw new Error('Shell command must not be empty');
2712
2970
  }
2713
2971
  await this.activateSessionCostTracker(sessionId);
2714
- if (this.options.sessionPersistence !== false) {
2972
+ if (this.options.sessionPersistence !== false &&
2973
+ !this.nativeTranscriptWritesEnabled()) {
2715
2974
  this.durableMetadataSessions.add(sessionId);
2716
2975
  }
2717
2976
  await this.ensureFileResources(sessionId, signal);
@@ -2730,9 +2989,13 @@ export class ClaudeSessionService {
2730
2989
  }
2731
2990
  const sessionPaths = this.paths(sessionId);
2732
2991
  const toolResultDirectory = join(sessionPaths.projectRoot, sessionId, 'tool-results');
2733
- const store = this.turnStore(sessionId);
2734
- const leaseResult = await store.withLease(async (lease) => {
2735
- if (!requireExisting) {
2992
+ const runUnderLease = async (lease, nativeLease) => {
2993
+ const rememberDurableMetadata = (observed) => {
2994
+ if (!nativeLease)
2995
+ this.rememberDurableMetadata(sessionId, observed);
2996
+ };
2997
+ const store = nativeLease ? undefined : this.turnStore(sessionId);
2998
+ if (store && !requireExisting) {
2736
2999
  const initialization = name === undefined
2737
3000
  ? await store.reserve()
2738
3001
  : await store.create(this.sessionNameEntries(sessionId, name));
@@ -2741,8 +3004,21 @@ export class ClaudeSessionService {
2741
3004
  }
2742
3005
  }
2743
3006
  let snapshot = await lease.load();
3007
+ const activeTurnMessages = () => nativeLease
3008
+ ? nativeLease.activeMessages()
3009
+ : projectClaudeModelMessages(snapshot.entries);
3010
+ const nativeInterruption = nativeLease?.interruption();
3011
+ if (nativeInterruption?.kind === 'indeterminate-tools')
3012
+ throw new Error(`Native tool execution is indeterminate: ${nativeInterruption.callIds.join(', ')}`);
3013
+ const nativeResumeInterrupted = nativeLease !== undefined &&
3014
+ this.options.resumeInterruptedTurn === true &&
3015
+ (nativeInterruption?.kind === 'interrupted-prompt' ||
3016
+ nativeInterruption?.kind === 'interrupted-turn');
3017
+ const shouldSkipUserPrompt = () => skipUserPrompt || nativeResumeInterrupted;
2744
3018
  let automaticReplayPrompt;
2745
- if (requireExisting &&
3019
+ if (store &&
3020
+ !nativeLease &&
3021
+ requireExisting &&
2746
3022
  this.options.sessionPersistence === false &&
2747
3023
  snapshot.entries.length === 0) {
2748
3024
  const persisted = await this.store(sessionId).load();
@@ -2752,17 +3028,18 @@ export class ClaudeSessionService {
2752
3028
  snapshot = await lease.load();
2753
3029
  }
2754
3030
  }
2755
- if (requireExisting && snapshot.entries.length === 0) {
3031
+ if (!nativeLease && requireExisting && snapshot.entries.length === 0) {
2756
3032
  throw new Error(`Claude session not found: ${sessionId}`);
2757
3033
  }
2758
- this.rememberDurableMetadata(sessionId, snapshot.entries);
2759
- if (resumeSessionAt !== undefined) {
3034
+ rememberDurableMetadata(snapshot.entries);
3035
+ if (!nativeLease && resumeSessionAt !== undefined) {
2760
3036
  snapshot = {
2761
3037
  entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
2762
3038
  tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
2763
3039
  };
2764
3040
  }
2765
- else if (this.explicitResumeLeafUuids.has(sessionId)) {
3041
+ else if (!nativeLease &&
3042
+ this.explicitResumeLeafUuids.has(sessionId)) {
2766
3043
  const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
2767
3044
  this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
2768
3045
  snapshot = {
@@ -2774,7 +3051,7 @@ export class ClaudeSessionService {
2774
3051
  resumeSessionAt === undefined &&
2775
3052
  this.options.resumeInterruptedTurn === true &&
2776
3053
  shellCommand === undefined &&
2777
- !skipUserPrompt &&
3054
+ !shouldSkipUserPrompt() &&
2778
3055
  !(this.options.approveRecovery !== undefined &&
2779
3056
  findUnresolvedClaudeToolCalls(snapshot.entries).length > 0)) {
2780
3057
  const interruption = classifyClaudeInterruption(snapshot.entries);
@@ -2804,7 +3081,7 @@ export class ClaudeSessionService {
2804
3081
  entries: [...snapshot.entries, stateEntry],
2805
3082
  tail: stateTail,
2806
3083
  };
2807
- this.rememberDurableMetadata(sessionId, snapshot.entries);
3084
+ rememberDurableMetadata(snapshot.entries);
2808
3085
  }
2809
3086
  this.restoreWorktree(snapshot.entries);
2810
3087
  this.options.interactiveTools?.restore(sessionId, snapshot.entries);
@@ -2820,7 +3097,7 @@ export class ClaudeSessionService {
2820
3097
  entries: [...snapshot.entries, ...entries],
2821
3098
  tail: appendResult.tail,
2822
3099
  };
2823
- this.rememberDurableMetadata(sessionId, snapshot.entries);
3100
+ rememberDurableMetadata(snapshot.entries);
2824
3101
  }
2825
3102
  const agentName = this.options.agent ?? getClaudeAgentSetting(snapshot.entries);
2826
3103
  const agent = this.resolveAgent(agentName);
@@ -2828,20 +3105,31 @@ export class ClaudeSessionService {
2828
3105
  this.activeProvider = provider;
2829
3106
  const effectivePrompt = automaticReplayPrompt ??
2830
3107
  (!requireExisting &&
2831
- !skipUserPrompt &&
3108
+ !shouldSkipUserPrompt() &&
2832
3109
  !this.options.agentInitialPromptHandledExternally &&
2833
3110
  shellCommand === undefined &&
2834
3111
  agent?.initialPrompt
2835
3112
  ? `${agent.initialPrompt}\n\n${prompt}`
2836
3113
  : prompt);
2837
- const projectMemoryRecallTurn = !skipUserPrompt && shellCommand === undefined
2838
- ? this.options.projectMemoryRecall?.prefetch({
2839
- sessionId,
3114
+ const sessionMemory = this.sessionMemoryController(sessionId);
3115
+ const turnMemory = new TurnMemoryCoordinator({
3116
+ sessionId,
3117
+ ...(sessionMemory ? { session: sessionMemory } : {}),
3118
+ ...(this.options.projectMemoryRecall
3119
+ ? { projectRecall: this.options.projectMemoryRecall }
3120
+ : {}),
3121
+ ...(this.options.projectMemoryExtraction
3122
+ ? { projectExtraction: this.options.projectMemoryExtraction }
3123
+ : {}),
3124
+ warn: (message) => this.options.eventSink?.({ type: 'warning', message }),
3125
+ });
3126
+ if (!shouldSkipUserPrompt() && shellCommand === undefined) {
3127
+ turnMemory.prefetch({
2840
3128
  turnId: randomUUID(),
2841
3129
  prompt: effectivePrompt,
2842
3130
  ...(signal ? { signal } : {}),
2843
- })
2844
- : undefined;
3131
+ });
3132
+ }
2845
3133
  const initialPricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
2846
3134
  if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
2847
3135
  throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
@@ -2856,7 +3144,11 @@ export class ClaudeSessionService {
2856
3144
  ...(this.options.fileRewindRoots ?? []),
2857
3145
  ])
2858
3146
  : null;
2859
- const unresolvedToolCalls = findUnresolvedClaudeToolCalls(snapshot.entries);
3147
+ const unresolvedToolCalls = nativeLease
3148
+ ? nativeInterruption?.kind === 'recoverable-tools'
3149
+ ? [...nativeInterruption.calls]
3150
+ : []
3151
+ : findUnresolvedClaudeToolCalls(snapshot.entries);
2860
3152
  const pendingRecoveryToolCallIds = new Set(unresolvedToolCalls.map((call) => call.id));
2861
3153
  const pendingRecoveryHookOutcomes = [];
2862
3154
  const currentHookCwd = () => this.activeCwd();
@@ -2986,6 +3278,9 @@ export class ClaudeSessionService {
2986
3278
  claudeVersion: this.options.claudeVersion,
2987
3279
  provider,
2988
3280
  persistence: this.options.sessionPersistence === false ? 'memory' : 'disk',
3281
+ ...(this.options.experimentalNativeTranscriptWrites === true
3282
+ ? { experimentalNativeTranscriptWrites: true }
3283
+ : {}),
2989
3284
  ...(this.options.providerForModel
2990
3285
  ? { providerForModel: this.options.providerForModel }
2991
3286
  : {}),
@@ -3146,7 +3441,7 @@ export class ClaudeSessionService {
3146
3441
  }
3147
3442
  : interactiveMessageTools;
3148
3443
  const capabilityTools = fileHistoryTools
3149
- ? new ClaudeCapabilityToolRegistry(fileHistoryTools, capabilities)
3444
+ ? new ClaudeCapabilityToolRegistry(this.teamRegistry(fileHistoryTools, sessionId, this.capabilityToolNames(this.options.teamToolNames, capabilities)), capabilities)
3150
3445
  : undefined;
3151
3446
  const structuredCapture = this.options.structuredOutputSchema
3152
3447
  ? { calls: 0, value: undefined }
@@ -3206,7 +3501,19 @@ export class ClaudeSessionService {
3206
3501
  let currentTurnToolCalls = 0;
3207
3502
  let projectMemoryMaintained = false;
3208
3503
  let projectMemoryRecallMessages = [];
3504
+ let observeModelRequestUsage = () => undefined;
3505
+ const durableFollowUps = new DurableFollowUpTracker();
3209
3506
  const observer = {
3507
+ ...(nativeLease
3508
+ ? {
3509
+ toolExecutionStarted: async (call) => {
3510
+ await nativeLease.beginToolExecution(call.id);
3511
+ },
3512
+ }
3513
+ : {}),
3514
+ modelRequestCompleted: async (input) => {
3515
+ observeModelRequestUsage(input);
3516
+ },
3210
3517
  assistantCompleted: async (message) => {
3211
3518
  const [entry] = translateProviderEvents([
3212
3519
  {
@@ -3224,8 +3531,25 @@ export class ClaudeSessionService {
3224
3531
  throw new Error('Could not translate assistant response');
3225
3532
  const tail = await this.append(lease, snapshot.tail, entry);
3226
3533
  snapshot = { entries: [...snapshot.entries, entry], tail };
3227
- lastAssistantUuid =
3228
- typeof entry.uuid === 'string' ? entry.uuid : lastAssistantUuid;
3534
+ lastAssistantUuid = nativeLease
3535
+ ? await nativeLease.appendMessages({
3536
+ messages: [
3537
+ {
3538
+ role: 'assistant',
3539
+ content: message.content,
3540
+ ...(message.thinkingBlocks?.length
3541
+ ? { thinkingBlocks: message.thinkingBlocks }
3542
+ : {}),
3543
+ ...(message.toolCalls?.length
3544
+ ? { toolCalls: message.toolCalls }
3545
+ : {}),
3546
+ },
3547
+ ],
3548
+ model: provider.model ?? 'praxis/provider',
3549
+ })
3550
+ : typeof entry.uuid === 'string'
3551
+ ? entry.uuid
3552
+ : lastAssistantUuid;
3229
3553
  },
3230
3554
  toolCompleted: async (call, toolResult) => {
3231
3555
  currentTurnToolCalls += 1;
@@ -3259,31 +3583,42 @@ export class ClaudeSessionService {
3259
3583
  tail: modeTail,
3260
3584
  };
3261
3585
  }
3262
- const [entry] = translateProviderEvents([
3263
- {
3264
- type: 'tool-result',
3265
- toolCallId: call.id,
3266
- content: toolResult.content,
3267
- ...(toolResult.contentBlocks
3268
- ? { contentBlocks: toolResult.contentBlocks }
3269
- : {}),
3270
- ...(toolResult.images ? { images: toolResult.images } : {}),
3271
- ...(toolResult.documents
3272
- ? { documents: toolResult.documents }
3273
- : {}),
3274
- isError: toolResult.isError,
3275
- ...(toolResult.nativeToolUseResult
3276
- ? { nativeToolUseResult: toolResult.nativeToolUseResult }
3277
- : {}),
3278
- ...(toolResult.nativeMcpMeta
3279
- ? { nativeMcpMeta: toolResult.nativeMcpMeta }
3280
- : {}),
3281
- },
3282
- ], this.translationContext(sessionId, snapshot));
3283
- if (!entry)
3284
- throw new Error('Could not translate tool result');
3285
- const tail = await this.append(lease, snapshot.tail, entry);
3286
- snapshot = { entries: [...snapshot.entries, entry], tail };
3586
+ if (nativeLease) {
3587
+ await nativeLease.appendToolCompletion({
3588
+ callId: call.id,
3589
+ result: toolResult,
3590
+ ...(toolResult.followUpUserMessages === undefined
3591
+ ? {}
3592
+ : { followUpUserMessages: toolResult.followUpUserMessages }),
3593
+ });
3594
+ }
3595
+ else {
3596
+ const [entry] = translateProviderEvents([
3597
+ {
3598
+ type: 'tool-result',
3599
+ toolCallId: call.id,
3600
+ content: toolResult.content,
3601
+ ...(toolResult.contentBlocks
3602
+ ? { contentBlocks: toolResult.contentBlocks }
3603
+ : {}),
3604
+ ...(toolResult.images ? { images: toolResult.images } : {}),
3605
+ ...(toolResult.documents
3606
+ ? { documents: toolResult.documents }
3607
+ : {}),
3608
+ isError: toolResult.isError,
3609
+ ...(toolResult.nativeToolUseResult
3610
+ ? { nativeToolUseResult: toolResult.nativeToolUseResult }
3611
+ : {}),
3612
+ ...(toolResult.nativeMcpMeta
3613
+ ? { nativeMcpMeta: toolResult.nativeMcpMeta }
3614
+ : {}),
3615
+ },
3616
+ ], this.translationContext(sessionId, snapshot));
3617
+ if (!entry)
3618
+ throw new Error('Could not translate tool result');
3619
+ const tail = await this.append(lease, snapshot.tail, entry);
3620
+ snapshot = { entries: [...snapshot.entries, entry], tail };
3621
+ }
3287
3622
  if (!toolResult.isError && this.options.projectMemoryDirectory) {
3288
3623
  const pathValue = call.input.file_path;
3289
3624
  const path = typeof pathValue === 'string'
@@ -3292,7 +3627,7 @@ export class ClaudeSessionService {
3292
3627
  if (path &&
3293
3628
  isPathWithin(this.options.projectMemoryDirectory, path)) {
3294
3629
  if (call.name === 'Read') {
3295
- this.options.projectMemoryRecall?.recordRead(sessionId, path);
3630
+ turnMemory.recordRead(path);
3296
3631
  }
3297
3632
  else if (call.name === 'Write' || call.name === 'Edit') {
3298
3633
  projectMemoryMaintained = true;
@@ -3302,7 +3637,7 @@ export class ClaudeSessionService {
3302
3637
  for (const accessedPath of toolResult.accessedPaths ?? []) {
3303
3638
  const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
3304
3639
  if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
3305
- this.options.projectMemoryRecall?.recordRead(sessionId, resolvedAccessedPath);
3640
+ turnMemory.recordRead(resolvedAccessedPath);
3306
3641
  }
3307
3642
  }
3308
3643
  }
@@ -3354,6 +3689,7 @@ export class ClaudeSessionService {
3354
3689
  currentTurnUserMessages?.push(content);
3355
3690
  await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => executor.acknowledgeNotifications([content])));
3356
3691
  }
3692
+ await durableFollowUps.followUpUserMessagesCompleted(messages);
3357
3693
  },
3358
3694
  };
3359
3695
  let turnCompleted = false;
@@ -3366,7 +3702,7 @@ export class ClaudeSessionService {
3366
3702
  const recoveryRequest = {
3367
3703
  cwd: this.activeCwd(),
3368
3704
  toolResultDirectory,
3369
- messages: projectClaudeModelMessages(snapshot.entries),
3705
+ messages: activeTurnMessages(),
3370
3706
  observer,
3371
3707
  permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
3372
3708
  onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
@@ -3408,7 +3744,6 @@ export class ClaudeSessionService {
3408
3744
  tail: settingTail,
3409
3745
  };
3410
3746
  }
3411
- const sessionMemory = this.sessionMemoryController(sessionId);
3412
3747
  let agentSystem = null;
3413
3748
  let planModeMessage;
3414
3749
  let sessionMemoryMessage = null;
@@ -3422,9 +3757,7 @@ export class ClaudeSessionService {
3422
3757
  agentSystem = await this.mainAgentSystemPrompt(agent);
3423
3758
  planModeMessage =
3424
3759
  this.options.interactiveTools?.contextMessage(sessionId);
3425
- sessionMemoryMessage = sessionMemory
3426
- ? this.sessionMemoryMessage(await sessionMemory.summary())
3427
- : null;
3760
+ sessionMemoryMessage = this.sessionMemoryMessage(await turnMemory.sessionSummary());
3428
3761
  const assembledContext = await assembleContextSnapshot(this.options.contextAssembler, {
3429
3762
  cwd: this.activeCwd(),
3430
3763
  lifecycleId: sessionId,
@@ -3448,7 +3781,7 @@ export class ClaudeSessionService {
3448
3781
  contextMessages = [...contextProjection.systemMessages];
3449
3782
  };
3450
3783
  await refreshRuntimeContext();
3451
- const expansion = skipUserPrompt
3784
+ const expansion = shouldSkipUserPrompt()
3452
3785
  ? { userMessages: [] }
3453
3786
  : shellCommand === undefined
3454
3787
  ? this.options.extensions
@@ -3479,7 +3812,17 @@ export class ClaudeSessionService {
3479
3812
  ? (structuredTools?.definitions() ?? [])
3480
3813
  : [];
3481
3814
  const budget = this.contextBudget(provider);
3482
- const recoveryPlanner = new ContextRecoveryPlanner();
3815
+ const contextEngine = new ContextEngine({
3816
+ ...(budget ? { budget } : {}),
3817
+ autoCompact: this.options.autoCompact !== false,
3818
+ memory: {
3819
+ beforeCompact: () => turnMemory.beforeCompact(),
3820
+ afterCompact: () => turnMemory.afterCompact(),
3821
+ },
3822
+ });
3823
+ observeModelRequestUsage = ({ usage, messages, tools }) => {
3824
+ contextEngine.observeUsage(usage, messages, tools);
3825
+ };
3483
3826
  const pendingUserMessages = expandedMessages.map((message, index) => ({
3484
3827
  role: 'user',
3485
3828
  content: message.text,
@@ -3499,7 +3842,7 @@ export class ClaudeSessionService {
3499
3842
  ? { documents }
3500
3843
  : {}),
3501
3844
  }));
3502
- const agentMentionMessages = shellCommand === undefined && !skipUserPrompt
3845
+ const agentMentionMessages = shellCommand === undefined && !shouldSkipUserPrompt()
3503
3846
  ? (this.options.extensions?.agentMentionMessages(effectivePrompt) ?? [])
3504
3847
  : [];
3505
3848
  const injectAgentMentionContext = (messages) => {
@@ -3531,276 +3874,445 @@ export class ClaudeSessionService {
3531
3874
  const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, contextProjection.firstUserMessageContext);
3532
3875
  const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
3533
3876
  let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
3534
- const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
3535
- if (!budget || this.options.autoCompact === false)
3536
- return;
3537
- const historyMessages = [
3538
- ...projectClaudeModelMessages(snapshot.entries),
3539
- ...projectMemoryRecallMessages,
3540
- ];
3541
- const predicted = budget.evaluate([
3542
- ...contextMessages,
3543
- ...injectTurnContext([...historyMessages, ...pendingMessages]),
3544
- ], definitions, options);
3545
- if (!predicted.shouldCompact)
3546
- return;
3547
- const irreducibleMessages = [
3548
- ...contextMessages,
3549
- ...injectTurnContext([
3550
- ...pendingMessages,
3551
- ...preservedUserMessages.map((content) => ({
3877
+ const contextTransitionPort = (pendingMessages = [], preservedUserMessages = []) => ({
3878
+ current: () => {
3879
+ const historyMessages = [
3880
+ ...activeTurnMessages(),
3881
+ ...projectMemoryRecallMessages,
3882
+ ];
3883
+ return {
3884
+ messages: [
3885
+ ...contextMessages,
3886
+ ...injectTurnContext([
3887
+ ...historyMessages,
3888
+ ...pendingMessages,
3889
+ ]),
3890
+ ],
3891
+ tools: definitions,
3892
+ };
3893
+ },
3894
+ irreducible: () => ({
3895
+ messages: [
3896
+ ...contextMessages,
3897
+ ...injectTurnContext([
3898
+ ...pendingMessages,
3899
+ ...preservedUserMessages.map((content) => ({
3900
+ role: 'user',
3901
+ content,
3902
+ })),
3903
+ ]),
3904
+ ],
3905
+ tools: definitions,
3906
+ }),
3907
+ propose: async () => {
3908
+ if (nativeLease) {
3909
+ if (!budget)
3910
+ throw new Error('Context budget is unavailable');
3911
+ const historyMessages = activeTurnMessages();
3912
+ if (historyMessages.length === 0)
3913
+ throw new Error('Cannot compact an empty native transcript');
3914
+ if (unresolvedActiveToolCallIds(historyMessages).length > 0)
3915
+ throw new Error('Cannot compact a native transcript with unresolved tool calls');
3916
+ let compactableMessages = historyMessages;
3917
+ let preservedMessages = [];
3918
+ if (preservedUserMessages.length > 0) {
3919
+ let searchFrom = historyMessages.length;
3920
+ const matchedIndexes = [];
3921
+ for (let markerIndex = preservedUserMessages.length - 1; markerIndex >= 0; markerIndex -= 1) {
3922
+ const marker = preservedUserMessages[markerIndex];
3923
+ let found = -1;
3924
+ for (let index = searchFrom - 1; index >= 0; index -= 1) {
3925
+ const message = historyMessages[index];
3926
+ if (message?.role === 'user' &&
3927
+ message.content === marker) {
3928
+ found = index;
3929
+ break;
3930
+ }
3931
+ }
3932
+ if (found < 0)
3933
+ throw new Error('Native automatic compaction could not match the current-turn suffix');
3934
+ matchedIndexes.push(found);
3935
+ searchFrom = found;
3936
+ }
3937
+ const earliest = Math.min(...matchedIndexes);
3938
+ compactableMessages = historyMessages.slice(0, earliest);
3939
+ preservedMessages = historyMessages.slice(earliest);
3940
+ }
3941
+ if (compactableMessages.length === 0)
3942
+ throw new Error('Cannot compact native transcript: no compactable prefix');
3943
+ const preTokens = estimateModelRequestTokens(compactableMessages);
3944
+ const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
3945
+ messages: compactableMessages,
3946
+ targetTokens: Math.min(8192, Math.max(1, Math.floor(budget.contextWindowTokens / 4))),
3947
+ contextWindowTokens: budget.contextWindowTokens,
3948
+ ...(signal ? { signal } : {}),
3949
+ });
3950
+ const { durationMs, durationWithoutRetriesMs } = requireCompactionDurations(compacted);
3951
+ if (signal?.aborted)
3952
+ throw new AgentRunCancelledError();
3953
+ const postTokens = estimateModelRequestTokens([
3954
+ { role: 'user', content: compacted.summary },
3955
+ ...preservedMessages,
3956
+ ]);
3957
+ const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
3958
+ ? compacted.model
3959
+ : provider.model;
3960
+ const meaningfulUsage = hasNonZeroUsage(compacted.usage);
3961
+ if (meaningfulUsage &&
3962
+ (compactModel === undefined || compactModel.trim() === ''))
3963
+ throw new Error('Auto compact usage requires a nonblank model identity');
3964
+ const tracker = this.sessionCostTrackers.get(sessionId);
3965
+ if (!tracker)
3966
+ throw new Error(`Session cost tracker is not active for session ${sessionId}`);
3967
+ let meteringTurnInput;
3968
+ if (meaningfulUsage &&
3969
+ compactModel !== undefined &&
3970
+ compactModel.trim() !== '') {
3971
+ const pricing = this.options.pricing?.resolve(compactModel);
3972
+ const costUsd = pricing
3973
+ ? usageCostUsd(compacted.usage, pricing)
3974
+ : undefined;
3975
+ meteringTurnInput = {
3976
+ model: compactModel,
3977
+ usage: compacted.usage,
3978
+ ...(costUsd === undefined ? {} : { costUsd }),
3979
+ ...(compacted.usage.webSearchRequests === undefined
3980
+ ? {}
3981
+ : {
3982
+ webSearchRequests: compacted.usage.webSearchRequests,
3983
+ }),
3984
+ };
3985
+ }
3986
+ let meteringDurationsInput;
3987
+ if (durationMs > 0 || durationWithoutRetriesMs > 0)
3988
+ meteringDurationsInput = {
3989
+ ...(durationMs === 0 ? {} : { apiDurationMs: durationMs }),
3990
+ apiDurationWithoutRetriesMs: durationWithoutRetriesMs,
3991
+ };
3992
+ if (meteringTurnInput !== undefined ||
3993
+ meteringDurationsInput !== undefined) {
3994
+ const preflight = new ClaudeSessionCostTracker({
3995
+ sessionId,
3996
+ restored: tracker.snapshot(),
3997
+ });
3998
+ if (meteringTurnInput !== undefined)
3999
+ preflight.recordTurn(meteringTurnInput);
4000
+ if (meteringDurationsInput !== undefined)
4001
+ preflight.recordDurations(meteringDurationsInput);
4002
+ }
4003
+ const summaryMessage = {
3552
4004
  role: 'user',
3553
- content,
3554
- })),
3555
- ]),
3556
- ];
3557
- const irreducible = budget.evaluate(irreducibleMessages, definitions);
3558
- budget.assertFits(irreducible);
3559
- let logicalParentUuid = compactionAnchorUuid;
3560
- if (!logicalParentUuid || historyMessages.length === 0) {
3561
- budget.assertFits(predicted);
3562
- throw new Error('Cannot compact an empty Claude transcript');
3563
- }
3564
- if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
3565
- throw new Error('Cannot compact a Claude session with unresolved tool calls');
3566
- }
3567
- const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', { trigger: 'auto', custom_instructions: null }, 'auto', signal);
3568
- const memorySelection = sessionMemory
3569
- ? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries))
3570
- : null;
3571
- const compactorMessages = memorySelection
3572
- ? [
3573
- memorySelection.memoryMessage,
3574
- ...projectClaudeModelMessages(memorySelection.compactedEntries),
3575
- ]
3576
- : historyMessages;
3577
- compactorMessages.push(...successfulHookOutput(preCompact).map((content) => ({
3578
- role: 'user',
3579
- content: `Additional summarization context: ${content}`,
3580
- })));
3581
- if (memorySelection) {
3582
- logicalParentUuid = memorySelection.logicalParentUuid;
3583
- }
3584
- this.options.eventSink?.({ type: 'state', state: 'compacting' });
3585
- const compactEnvelope = budget.evaluate([
3586
- ...irreducibleMessages,
3587
- {
4005
+ content: compacted.summary,
4006
+ };
4007
+ const proposedMessages = [
4008
+ ...contextMessages,
4009
+ ...injectTurnContext([
4010
+ summaryMessage,
4011
+ ...preservedMessages,
4012
+ ...pendingMessages,
4013
+ ]),
4014
+ ];
4015
+ return {
4016
+ envelope: { messages: proposedMessages, tools: definitions },
4017
+ commit: async () => {
4018
+ if (signal?.aborted)
4019
+ throw new AgentRunCancelledError();
4020
+ const ids = await nativeLease.appendCompaction({
4021
+ summary: compacted.summary,
4022
+ trigger: 'auto',
4023
+ preTokens,
4024
+ postTokens,
4025
+ durationMs,
4026
+ preservedMessages,
4027
+ });
4028
+ this.options.eventSink?.({
4029
+ type: 'compact-boundary',
4030
+ trigger: 'auto',
4031
+ preTokens,
4032
+ uuid: ids.boundaryId,
4033
+ });
4034
+ compactionUsage = mergeUsage(compactionUsage, compacted.usage);
4035
+ compactionDurationMs =
4036
+ (compactionDurationMs ?? 0) + durationMs;
4037
+ compactionDurationWithoutRetriesMs =
4038
+ (compactionDurationWithoutRetriesMs ?? 0) +
4039
+ durationWithoutRetriesMs;
4040
+ if (meteringTurnInput !== undefined) {
4041
+ tracker.recordTurn(meteringTurnInput);
4042
+ }
4043
+ if (compactModel !== undefined &&
4044
+ compactModel.trim() !== '') {
4045
+ compactionModelUsage = mergeSessionRawModelUsage(compactionModelUsage, { [compactModel]: compacted.usage });
4046
+ }
4047
+ if (meteringDurationsInput !== undefined)
4048
+ tracker.recordDurations(meteringDurationsInput);
4049
+ },
4050
+ };
4051
+ }
4052
+ if (!budget) {
4053
+ throw new Error('Context budget is unavailable');
4054
+ }
4055
+ const historyMessages = [
4056
+ ...activeTurnMessages(),
4057
+ ...projectMemoryRecallMessages,
4058
+ ];
4059
+ const irreducibleMessages = [
4060
+ ...contextMessages,
4061
+ ...injectTurnContext([
4062
+ ...pendingMessages,
4063
+ ...preservedUserMessages.map((content) => ({
4064
+ role: 'user',
4065
+ content,
4066
+ })),
4067
+ ]),
4068
+ ];
4069
+ let logicalParentUuid = compactionAnchorUuid;
4070
+ if (!logicalParentUuid || historyMessages.length === 0) {
4071
+ throw new Error('Cannot compact an empty Claude transcript');
4072
+ }
4073
+ if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
4074
+ throw new Error('Cannot compact a Claude session with unresolved tool calls');
4075
+ }
4076
+ const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', { trigger: 'auto', custom_instructions: null }, 'auto', signal);
4077
+ const memorySelection = sessionMemory
4078
+ ? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries), { sessionMemoryReady: true })
4079
+ : null;
4080
+ const compactorMessages = memorySelection
4081
+ ? [
4082
+ memorySelection.memoryMessage,
4083
+ ...projectClaudeModelMessages(memorySelection.compactedEntries),
4084
+ ]
4085
+ : historyMessages;
4086
+ compactorMessages.push(...successfulHookOutput(preCompact).map((content) => ({
3588
4087
  role: 'user',
3589
- content: formatClaudeCompactSummary(''),
3590
- },
3591
- ], definitions);
3592
- let targetTokens = Math.min(8192, compactEnvelope.availableTokens - compactEnvelope.estimatedTokens);
3593
- if (targetTokens < 1) {
3594
- budget.assertFits(budget.evaluate([
4088
+ content: `Additional summarization context: ${content}`,
4089
+ })));
4090
+ if (memorySelection) {
4091
+ logicalParentUuid = memorySelection.logicalParentUuid;
4092
+ }
4093
+ this.options.eventSink?.({ type: 'state', state: 'compacting' });
4094
+ const compactEnvelope = budget.evaluate([
3595
4095
  ...irreducibleMessages,
3596
4096
  {
3597
4097
  role: 'user',
3598
- content: formatClaudeCompactSummary('a'),
4098
+ content: formatClaudeCompactSummary(''),
3599
4099
  },
3600
- ], definitions));
3601
- targetTokens = 1;
3602
- }
3603
- const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
3604
- messages: compactorMessages,
3605
- targetTokens,
3606
- contextWindowTokens: budget.contextWindowTokens,
3607
- ...(signal ? { signal } : {}),
3608
- });
3609
- const { durationMs: compactedDurationMs, durationWithoutRetriesMs: compactedDurationWithoutRetriesMs, } = requireCompactionDurations(compacted);
3610
- const proposedCompactionDurationMs = (compactionDurationMs ?? 0) + compactedDurationMs;
3611
- if (!Number.isFinite(proposedCompactionDurationMs)) {
3612
- throw new TypeError('compaction durationMs total overflow');
3613
- }
3614
- const proposedCompactionDurationWithoutRetriesMs = (compactionDurationWithoutRetriesMs ?? 0) +
3615
- compactedDurationWithoutRetriesMs;
3616
- if (!Number.isFinite(proposedCompactionDurationWithoutRetriesMs)) {
3617
- throw new TypeError('compaction durationWithoutRetriesMs total overflow');
3618
- }
3619
- const boundaryUuid = randomUUID();
3620
- const summaryUuid = randomUUID();
3621
- const timestamp = new Date().toISOString();
3622
- const preTokens = budget.evaluate([...contextMessages, ...injectTurnContext(historyMessages)], definitions).estimatedTokens;
3623
- const compactEntries = (postTokens) => {
3624
- const uuids = [boundaryUuid, summaryUuid];
3625
- return createClaudeCompactEntries({
4100
+ ], definitions);
4101
+ let targetTokens = Math.min(8192, compactEnvelope.availableTokens -
4102
+ compactEnvelope.estimatedTokens);
4103
+ if (targetTokens < 1) {
4104
+ budget.assertFits(budget.evaluate([
4105
+ ...irreducibleMessages,
4106
+ {
4107
+ role: 'user',
4108
+ content: formatClaudeCompactSummary('a'),
4109
+ },
4110
+ ], definitions));
4111
+ targetTokens = 1;
4112
+ }
4113
+ const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
4114
+ messages: compactorMessages,
4115
+ targetTokens,
4116
+ contextWindowTokens: budget.contextWindowTokens,
4117
+ ...(signal ? { signal } : {}),
4118
+ });
4119
+ const { durationMs: compactedDurationMs, durationWithoutRetriesMs: compactedDurationWithoutRetriesMs, } = requireCompactionDurations(compacted);
4120
+ const proposedCompactionDurationMs = (compactionDurationMs ?? 0) + compactedDurationMs;
4121
+ if (!Number.isFinite(proposedCompactionDurationMs)) {
4122
+ throw new TypeError('compaction durationMs total overflow');
4123
+ }
4124
+ const proposedCompactionDurationWithoutRetriesMs = (compactionDurationWithoutRetriesMs ?? 0) +
4125
+ compactedDurationWithoutRetriesMs;
4126
+ if (!Number.isFinite(proposedCompactionDurationWithoutRetriesMs)) {
4127
+ throw new TypeError('compaction durationWithoutRetriesMs total overflow');
4128
+ }
4129
+ const boundaryUuid = randomUUID();
4130
+ const summaryUuid = randomUUID();
4131
+ const timestamp = new Date().toISOString();
4132
+ const preTokens = budget.evaluate([...contextMessages, ...injectTurnContext(historyMessages)], definitions).estimatedTokens;
4133
+ const compactEntries = (postTokens) => {
4134
+ const uuids = [boundaryUuid, summaryUuid];
4135
+ return createClaudeCompactEntries({
4136
+ sessionId,
4137
+ logicalParentUuid,
4138
+ summary: compacted.summary,
4139
+ preTokens,
4140
+ postTokens,
4141
+ previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
4142
+ durationMs: compactedDurationMs,
4143
+ cwd: this.activeCwd(),
4144
+ claudeVersion: this.options.claudeVersion,
4145
+ gitBranch: null,
4146
+ ...(memorySelection
4147
+ ? {
4148
+ preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
4149
+ }
4150
+ : {}),
4151
+ createUuid: () => uuids.shift() ?? randomUUID(),
4152
+ now: () => timestamp,
4153
+ });
4154
+ };
4155
+ const provisionalEntries = compactEntries(0);
4156
+ const compactSummaryUuid = provisionalEntries.at(-1)?.uuid;
4157
+ if (typeof compactSummaryUuid !== 'string') {
4158
+ throw new Error('Could not create Claude compact summary');
4159
+ }
4160
+ const replayUuids = preservedUserMessages.map(() => randomUUID());
4161
+ const replayEntries = translateProviderEvents(preservedUserMessages.map((text, index) => index === 0
4162
+ ? { type: 'user-text', text }
4163
+ : { type: 'user-text-block', text }), {
3626
4164
  sessionId,
3627
- logicalParentUuid,
3628
- summary: compacted.summary,
3629
- preTokens,
3630
- postTokens,
3631
- previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
3632
- durationMs: compactedDurationMs,
4165
+ parentUuid: compactSummaryUuid,
3633
4166
  cwd: this.activeCwd(),
3634
4167
  claudeVersion: this.options.claudeVersion,
3635
4168
  gitBranch: null,
3636
- ...(memorySelection
3637
- ? {
3638
- preservedUuids: memorySelection.preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
3639
- }
3640
- : {}),
3641
- createUuid: () => uuids.shift() ?? randomUUID(),
4169
+ history: [...snapshot.entries, ...provisionalEntries],
4170
+ createUuid: () => replayUuids.shift() ?? randomUUID(),
3642
4171
  now: () => timestamp,
3643
4172
  });
3644
- };
3645
- const provisionalEntries = compactEntries(0);
3646
- const compactSummaryUuid = provisionalEntries.at(-1)?.uuid;
3647
- if (typeof compactSummaryUuid !== 'string') {
3648
- throw new Error('Could not create Claude compact summary');
3649
- }
3650
- const replayUuids = preservedUserMessages.map(() => randomUUID());
3651
- const replayEntries = translateProviderEvents(preservedUserMessages.map((text, index) => index === 0
3652
- ? { type: 'user-text', text }
3653
- : { type: 'user-text-block', text }), {
3654
- sessionId,
3655
- parentUuid: compactSummaryUuid,
3656
- cwd: this.activeCwd(),
3657
- claudeVersion: this.options.claudeVersion,
3658
- gitBranch: null,
3659
- history: [...snapshot.entries, ...provisionalEntries],
3660
- createUuid: () => replayUuids.shift() ?? randomUUID(),
3661
- now: () => timestamp,
3662
- });
3663
- const compactedHistory = [
3664
- ...projectClaudeModelMessages([
3665
- ...snapshot.entries,
3666
- ...provisionalEntries,
4173
+ const compactedHistory = [
4174
+ ...projectClaudeModelMessages([
4175
+ ...snapshot.entries,
4176
+ ...provisionalEntries,
4177
+ ...replayEntries,
4178
+ ]),
4179
+ ...projectMemoryRecallMessages,
4180
+ ];
4181
+ const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
4182
+ const proposedMessages = [
4183
+ ...contextMessages,
4184
+ ...injectTurnContext([...compactedHistory, ...pendingMessages]),
4185
+ ];
4186
+ const entries = [
4187
+ ...compactEntries(afterHistory.estimatedTokens),
3667
4188
  ...replayEntries,
3668
- ]),
3669
- ...projectMemoryRecallMessages,
3670
- ];
3671
- const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
3672
- const afterPending = budget.evaluate([
3673
- ...contextMessages,
3674
- ...injectTurnContext([...compactedHistory, ...pendingMessages]),
3675
- ], definitions);
3676
- budget.assertFits(afterPending);
3677
- if (options.promptTooLong === true &&
3678
- !contextRecoveryMadeProgress({
3679
- beforeOccupancyTokens: predicted.occupancyTokens,
3680
- afterOccupancyTokens: afterPending.occupancyTokens,
3681
- })) {
3682
- throw new Error('Reactive context compaction made no occupancy progress');
3683
- }
3684
- const entries = [
3685
- ...compactEntries(afterHistory.estimatedTokens),
3686
- ...replayEntries,
3687
- ];
3688
- if (signal?.aborted)
3689
- throw new AgentRunCancelledError();
3690
- // Construct the exact tracker mutations for this single committed
3691
- // boundary and preflight them against a clone of the live tracker so
3692
- // invalid input or cumulative overflow rejects before the compact
3693
- // boundary is appended rather than after a half-commit.
3694
- const tracker = this.sessionCostTrackers.get(sessionId);
3695
- if (!tracker) {
3696
- throw new Error(`Session cost tracker is not active for session ${sessionId}`);
3697
- }
3698
- const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
3699
- ? compacted.model
3700
- : provider.model;
3701
- const compactModelNonBlank = compactModel !== undefined && compactModel.trim() !== '';
3702
- if (hasNonZeroUsage(compacted.usage) && !compactModelNonBlank) {
3703
- throw new Error('Auto compact usage requires a nonblank model identity');
3704
- }
3705
- let meteringTurnInput;
3706
- if (compactModelNonBlank && hasNonZeroUsage(compacted.usage)) {
3707
- const pricing = this.options.pricing?.resolve(compactModel);
3708
- const costUsd = pricing
3709
- ? usageCostUsd(compacted.usage, pricing)
3710
- : undefined;
3711
- meteringTurnInput = {
3712
- model: compactModel,
3713
- usage: compacted.usage,
3714
- ...(costUsd === undefined ? {} : { costUsd }),
3715
- ...(compacted.usage.webSearchRequests === undefined
3716
- ? {}
3717
- : { webSearchRequests: compacted.usage.webSearchRequests }),
3718
- };
3719
- }
3720
- let meteringDurationsInput;
3721
- if (compactedDurationMs > 0 ||
3722
- compactedDurationWithoutRetriesMs > 0) {
3723
- meteringDurationsInput = {
3724
- ...(compactedDurationMs === 0
3725
- ? {}
3726
- : { apiDurationMs: compactedDurationMs }),
3727
- apiDurationWithoutRetriesMs: compactedDurationWithoutRetriesMs,
3728
- };
3729
- }
3730
- if (meteringTurnInput !== undefined ||
3731
- meteringDurationsInput !== undefined) {
3732
- const preflight = new ClaudeSessionCostTracker({
3733
- sessionId,
3734
- restored: tracker.snapshot(),
3735
- });
3736
- if (meteringTurnInput !== undefined) {
3737
- preflight.recordTurn(meteringTurnInput);
4189
+ ];
4190
+ if (signal?.aborted)
4191
+ throw new AgentRunCancelledError();
4192
+ // Construct the exact tracker mutations for this single committed
4193
+ // boundary and preflight them against a clone of the live tracker so
4194
+ // invalid input or cumulative overflow rejects before the compact
4195
+ // boundary is appended rather than after a half-commit.
4196
+ const tracker = this.sessionCostTrackers.get(sessionId);
4197
+ if (!tracker) {
4198
+ throw new Error(`Session cost tracker is not active for session ${sessionId}`);
3738
4199
  }
3739
- if (meteringDurationsInput !== undefined) {
3740
- preflight.recordDurations(meteringDurationsInput);
4200
+ const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
4201
+ ? compacted.model
4202
+ : provider.model;
4203
+ const compactModelNonBlank = compactModel !== undefined && compactModel.trim() !== '';
4204
+ if (hasNonZeroUsage(compacted.usage) && !compactModelNonBlank) {
4205
+ throw new Error('Auto compact usage requires a nonblank model identity');
3741
4206
  }
3742
- }
3743
- const appendResult = await lease.appendMany(snapshot.tail, entries);
3744
- if (appendResult.status === 'conflict') {
3745
- throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
3746
- }
3747
- snapshot = {
3748
- entries: [...snapshot.entries, ...entries],
3749
- tail: appendResult.tail,
3750
- };
3751
- const metadataEntries = createClaudeDurableMetadataSnapshot(snapshot.entries, sessionId);
3752
- if (metadataEntries.length > 0) {
3753
- const metadataAppend = await lease.appendMany(snapshot.tail, metadataEntries);
3754
- if (metadataAppend.status === 'conflict') {
3755
- throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
4207
+ let meteringTurnInput;
4208
+ if (compactModelNonBlank && hasNonZeroUsage(compacted.usage)) {
4209
+ const pricing = this.options.pricing?.resolve(compactModel);
4210
+ const costUsd = pricing
4211
+ ? usageCostUsd(compacted.usage, pricing)
4212
+ : undefined;
4213
+ meteringTurnInput = {
4214
+ model: compactModel,
4215
+ usage: compacted.usage,
4216
+ ...(costUsd === undefined ? {} : { costUsd }),
4217
+ ...(compacted.usage.webSearchRequests === undefined
4218
+ ? {}
4219
+ : { webSearchRequests: compacted.usage.webSearchRequests }),
4220
+ };
3756
4221
  }
3757
- snapshot = {
3758
- entries: [...snapshot.entries, ...metadataEntries],
3759
- tail: metadataAppend.tail,
4222
+ let meteringDurationsInput;
4223
+ if (compactedDurationMs > 0 ||
4224
+ compactedDurationWithoutRetriesMs > 0) {
4225
+ meteringDurationsInput = {
4226
+ ...(compactedDurationMs === 0
4227
+ ? {}
4228
+ : { apiDurationMs: compactedDurationMs }),
4229
+ apiDurationWithoutRetriesMs: compactedDurationWithoutRetriesMs,
4230
+ };
4231
+ }
4232
+ if (meteringTurnInput !== undefined ||
4233
+ meteringDurationsInput !== undefined) {
4234
+ const preflight = new ClaudeSessionCostTracker({
4235
+ sessionId,
4236
+ restored: tracker.snapshot(),
4237
+ });
4238
+ if (meteringTurnInput !== undefined) {
4239
+ preflight.recordTurn(meteringTurnInput);
4240
+ }
4241
+ if (meteringDurationsInput !== undefined) {
4242
+ preflight.recordDurations(meteringDurationsInput);
4243
+ }
4244
+ }
4245
+ return {
4246
+ envelope: {
4247
+ messages: proposedMessages,
4248
+ tools: definitions,
4249
+ },
4250
+ commit: async () => {
4251
+ if (signal?.aborted)
4252
+ throw new AgentRunCancelledError();
4253
+ const appendResult = await lease.appendMany(snapshot.tail, entries);
4254
+ if (appendResult.status === 'conflict') {
4255
+ throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
4256
+ }
4257
+ snapshot = {
4258
+ entries: [...snapshot.entries, ...entries],
4259
+ tail: appendResult.tail,
4260
+ };
4261
+ const metadataEntries = createClaudeDurableMetadataSnapshot(snapshot.entries, sessionId);
4262
+ if (metadataEntries.length > 0) {
4263
+ const metadataAppend = await lease.appendMany(snapshot.tail, metadataEntries);
4264
+ if (metadataAppend.status === 'conflict') {
4265
+ throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
4266
+ }
4267
+ snapshot = {
4268
+ entries: [...snapshot.entries, ...metadataEntries],
4269
+ tail: metadataAppend.tail,
4270
+ };
4271
+ }
4272
+ rememberDurableMetadata(snapshot.entries);
4273
+ await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
4274
+ // The boundary is durable: mirror Claude's full-compact behavior by
4275
+ // rerunning SessionStart with source compact and refreshing the
4276
+ // runtime-only context so the next request retains current
4277
+ // instructions, plan state, session memory, and hook context.
4278
+ if (this.options.hooks) {
4279
+ const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
4280
+ if (outcome)
4281
+ await recordHookOutcome(outcome);
4282
+ }
4283
+ this.options.contextAssembler?.invalidate?.({
4284
+ lifecycleId: sessionId,
4285
+ reason: 'compact',
4286
+ });
4287
+ await refreshRuntimeContext();
4288
+ this.options.eventSink?.({
4289
+ type: 'compact-boundary',
4290
+ trigger: 'auto',
4291
+ preTokens,
4292
+ uuid: boundaryUuid,
4293
+ });
4294
+ compactionAnchorUuid = compactSummaryUuid;
4295
+ compactionUsage = mergeUsage(compactionUsage, compacted.usage);
4296
+ compactionDurationMs = proposedCompactionDurationMs;
4297
+ compactionDurationWithoutRetriesMs =
4298
+ proposedCompactionDurationWithoutRetriesMs;
4299
+ if (meteringTurnInput !== undefined) {
4300
+ compactionModelUsage = mergeSessionRawModelUsage(compactionModelUsage, { [meteringTurnInput.model]: compacted.usage });
4301
+ }
4302
+ // Commit the preflighted mutations once the boundary is durable so a
4303
+ // later main-provider failure or cancellation cannot lose the
4304
+ // compactor's usage/cost/API durations.
4305
+ if (meteringTurnInput !== undefined) {
4306
+ tracker.recordTurn(meteringTurnInput);
4307
+ }
4308
+ if (meteringDurationsInput !== undefined) {
4309
+ tracker.recordDurations(meteringDurationsInput);
4310
+ }
4311
+ },
3760
4312
  };
3761
- }
3762
- this.rememberDurableMetadata(sessionId, snapshot.entries);
3763
- await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
3764
- // The boundary is durable: mirror Claude's full-compact behavior by
3765
- // rerunning SessionStart with source compact and refreshing the
3766
- // runtime-only context so the next request retains current
3767
- // instructions, plan state, session memory, and hook context.
3768
- if (this.options.hooks) {
3769
- const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
3770
- if (outcome)
3771
- await recordHookOutcome(outcome);
3772
- }
3773
- this.options.contextAssembler?.invalidate?.({
3774
- lifecycleId: sessionId,
3775
- reason: 'compact',
3776
- });
3777
- this.options.projectMemoryRecall?.recordCompact(sessionId);
3778
- await refreshRuntimeContext();
3779
- this.options.eventSink?.({
3780
- type: 'compact-boundary',
3781
- trigger: 'auto',
3782
- preTokens,
3783
- uuid: boundaryUuid,
3784
- });
3785
- compactionAnchorUuid = compactSummaryUuid;
3786
- compactionUsage = mergeUsage(compactionUsage, compacted.usage);
3787
- compactionDurationMs = proposedCompactionDurationMs;
3788
- compactionDurationWithoutRetriesMs =
3789
- proposedCompactionDurationWithoutRetriesMs;
3790
- if (meteringTurnInput !== undefined) {
3791
- compactionModelUsage = mergeSessionRawModelUsage(compactionModelUsage, { [meteringTurnInput.model]: compacted.usage });
3792
- }
3793
- // Commit the preflighted mutations once the boundary is durable so a
3794
- // later main-provider failure or cancellation cannot lose the
3795
- // compactor's usage/cost/API durations.
3796
- if (meteringTurnInput !== undefined) {
3797
- tracker.recordTurn(meteringTurnInput);
3798
- }
3799
- if (meteringDurationsInput !== undefined) {
3800
- tracker.recordDurations(meteringDurationsInput);
3801
- }
3802
- };
3803
- await compactIfNeeded(pendingUserMessages);
4313
+ },
4314
+ });
4315
+ await contextEngine.prepare(contextTransitionPort(pendingUserMessages), signal);
3804
4316
  for (const [index, message] of expandedMessages.entries()) {
3805
4317
  if (shellCommand !== undefined)
3806
4318
  break;
@@ -3834,6 +4346,20 @@ export class ClaudeSessionService {
3834
4346
  entries: [...snapshot.entries, userEntry],
3835
4347
  tail: userTail,
3836
4348
  };
4349
+ if (nativeLease) {
4350
+ await nativeLease.appendMessages({
4351
+ messages: [
4352
+ {
4353
+ role: 'user',
4354
+ content: message.text,
4355
+ ...(messageImages.length ? { images: messageImages } : {}),
4356
+ ...(index === attachmentIndex && documents.length > 0
4357
+ ? { documents }
4358
+ : {}),
4359
+ },
4360
+ ],
4361
+ });
4362
+ }
3837
4363
  }
3838
4364
  if (fileHistory && currentPromptId) {
3839
4365
  const historySnapshot = await fileHistory.snapshot(snapshot.entries, currentPromptId);
@@ -3843,7 +4369,7 @@ export class ClaudeSessionService {
3843
4369
  tail: historyTail,
3844
4370
  };
3845
4371
  }
3846
- if (this.options.hooks && !skipUserPrompt) {
4372
+ if (this.options.hooks && !shouldSkipUserPrompt()) {
3847
4373
  const outcome = await this.options.hooks.run({
3848
4374
  ...hookSession,
3849
4375
  hook_event_name: 'UserPromptSubmit',
@@ -3874,7 +4400,7 @@ export class ClaudeSessionService {
3874
4400
  cwd: this.activeCwd(),
3875
4401
  sessionId,
3876
4402
  toolResultDirectory,
3877
- messages: projectClaudeModelMessages(snapshot.entries),
4403
+ messages: activeTurnMessages(),
3878
4404
  observer: {
3879
4405
  assistantCompleted: async () => undefined,
3880
4406
  toolCompleted: async () => undefined,
@@ -3926,6 +4452,20 @@ export class ClaudeSessionService {
3926
4452
  entries: [...snapshot.entries, inputEntry, outputEntry],
3927
4453
  tail: shellAppend.tail,
3928
4454
  };
4455
+ if (nativeLease) {
4456
+ await nativeLease.appendMessages({
4457
+ messages: [
4458
+ {
4459
+ role: 'user',
4460
+ content: `<bash-input>${shellCommand}</bash-input>`,
4461
+ },
4462
+ {
4463
+ role: 'user',
4464
+ content: `<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`,
4465
+ },
4466
+ ],
4467
+ });
4468
+ }
3929
4469
  currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
3930
4470
  this.options.eventSink?.({
3931
4471
  type: 'shell-result',
@@ -3936,10 +4476,10 @@ export class ClaudeSessionService {
3936
4476
  });
3937
4477
  }
3938
4478
  if (budget) {
3939
- await compactIfNeeded([], currentTurnUserMessages ?? []);
4479
+ await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
3940
4480
  budget.assertFits(budget.evaluate([
3941
4481
  ...contextMessages,
3942
- ...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
4482
+ ...injectTurnContext(activeTurnMessages()),
3943
4483
  ], definitions));
3944
4484
  }
3945
4485
  let stopHookActive = false;
@@ -3947,7 +4487,7 @@ export class ClaudeSessionService {
3947
4487
  sessionId,
3948
4488
  messages: [
3949
4489
  ...contextMessages,
3950
- ...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
4490
+ ...injectTurnContext(activeTurnMessages()),
3951
4491
  ],
3952
4492
  stableSystemMessageCount,
3953
4493
  cwd: this.activeCwd(),
@@ -3967,18 +4507,18 @@ export class ClaudeSessionService {
3967
4507
  ? { deferFailureKinds: ['prompt_too_long'] }
3968
4508
  : {}),
3969
4509
  reloadMessages: async () => {
3970
- const recalled = projectMemoryRecallTurn?.consumeIfSettled();
4510
+ const recalled = turnMemory.consumeRecall();
3971
4511
  if (recalled) {
3972
4512
  projectMemoryRecallMessages = [
3973
4513
  { role: 'user', content: recalled.content },
3974
4514
  ];
3975
4515
  }
3976
- await compactIfNeeded([], currentTurnUserMessages ?? []);
4516
+ await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
3977
4517
  runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
3978
4518
  return [
3979
4519
  ...contextMessages,
3980
4520
  ...injectTurnContext([
3981
- ...projectClaudeModelMessages(snapshot.entries),
4521
+ ...activeTurnMessages(),
3982
4522
  ...projectMemoryRecallMessages,
3983
4523
  ]),
3984
4524
  ];
@@ -3987,10 +4527,16 @@ export class ClaudeSessionService {
3987
4527
  subagentExecutor ||
3988
4528
  taskTools ||
3989
4529
  this.scheduledPrompts ||
3990
- this.workflowManager
4530
+ this.workflowManager ||
4531
+ this.leadOperations
3991
4532
  ? {
3992
4533
  onStop: async (text) => {
3993
4534
  const messages = [];
4535
+ const teamInbox = await this.leadOperations?.projectInbox(sessionId);
4536
+ if (teamInbox) {
4537
+ durableFollowUps.register(teamInbox);
4538
+ messages.push(...teamInbox.messages);
4539
+ }
3994
4540
  const outcome = await this.options.hooks?.run({
3995
4541
  ...hookSession,
3996
4542
  hook_event_name: 'Stop',
@@ -4089,24 +4635,8 @@ export class ClaudeSessionService {
4089
4635
  catch (error) {
4090
4636
  if (!budget || !isPromptTooLongError(error))
4091
4637
  throw error;
4092
- if (recoveryPlanner.reactiveRetriesRemaining === 0) {
4093
- surfaceExhaustedRecovery(error);
4094
- throw error;
4095
- }
4096
- const beforeRecovery = budget.evaluate(runtimeRequest.messages, definitions);
4097
- try {
4098
- await compactIfNeeded([], currentTurnUserMessages ?? [], {
4099
- promptTooLong: true,
4100
- });
4101
- }
4102
- catch (compactionError) {
4103
- if (signal?.aborted ||
4104
- compactionError instanceof AgentRunCancelledError) {
4105
- throw new AgentRunCancelledError();
4106
- }
4107
- // Compaction could not free the provider-bounded context; surface
4108
- // the original prompt-too-long error rather than the compaction
4109
- // failure.
4638
+ const recovery = await contextEngine.recover(error, contextTransitionPort([], currentTurnUserMessages ?? []), signal);
4639
+ if (recovery.kind !== 'retry') {
4110
4640
  surfaceExhaustedRecovery(error);
4111
4641
  throw error;
4112
4642
  }
@@ -4115,19 +4645,11 @@ export class ClaudeSessionService {
4115
4645
  runtimeRequest.messages = [
4116
4646
  ...contextMessages,
4117
4647
  ...injectTurnContext([
4118
- ...projectClaudeModelMessages(snapshot.entries),
4648
+ ...activeTurnMessages(),
4119
4649
  ...projectMemoryRecallMessages,
4120
4650
  ]),
4121
4651
  ];
4122
4652
  runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
4123
- const afterRecovery = budget.evaluate(runtimeRequest.messages, definitions);
4124
- if (recoveryPlanner.consumeReactiveRetry({
4125
- beforeOccupancyTokens: beforeRecovery.occupancyTokens,
4126
- afterOccupancyTokens: afterRecovery.occupancyTokens,
4127
- }) !== 'reactive-retry') {
4128
- surfaceExhaustedRecovery(error);
4129
- throw error;
4130
- }
4131
4653
  runtimeRequest.deferFailureKinds = true;
4132
4654
  try {
4133
4655
  result = await attemptMainTurn();
@@ -4135,7 +4657,6 @@ export class ClaudeSessionService {
4135
4657
  catch (retryError) {
4136
4658
  // Exactly one reactive retry is consumed; fail deterministically
4137
4659
  // and surface the original prompt-too-long error.
4138
- recoveryPlanner.recordFailure();
4139
4660
  if (signal?.aborted ||
4140
4661
  retryError instanceof AgentRunCancelledError) {
4141
4662
  throw new AgentRunCancelledError();
@@ -4160,16 +4681,6 @@ export class ClaudeSessionService {
4160
4681
  }
4161
4682
  throw error;
4162
4683
  }
4163
- recoveryPlanner.recordSuccess();
4164
- const mainModel = provider.model !== undefined && provider.model.trim() !== ''
4165
- ? provider.model
4166
- : undefined;
4167
- const observedUsage = (mainModel !== undefined
4168
- ? result.modelUsage?.[mainModel]
4169
- : undefined) ??
4170
- Object.values(result.modelUsage ?? {})[0] ??
4171
- result.usage;
4172
- budget?.observeUsage(observedUsage, runtimeRequest.messages, definitions);
4173
4684
  if (structuredCapture && structuredCapture.calls !== 1) {
4174
4685
  throw new Error(`StructuredOutput must be called exactly once (received ${structuredCapture.calls})`);
4175
4686
  }
@@ -4177,7 +4688,7 @@ export class ClaudeSessionService {
4177
4688
  if (!finalLeafUuid) {
4178
4689
  throw new Error('Could not locate final assistant response');
4179
4690
  }
4180
- if (!skipUserPrompt) {
4691
+ if (!nativeLease && !skipUserPrompt) {
4181
4692
  const lastPrompt = createClaudeLastPromptEntry({
4182
4693
  sessionId,
4183
4694
  lastPrompt: effectivePrompt,
@@ -4189,7 +4700,7 @@ export class ClaudeSessionService {
4189
4700
  tail,
4190
4701
  };
4191
4702
  }
4192
- this.rememberDurableMetadata(sessionId, snapshot.entries);
4703
+ rememberDurableMetadata(snapshot.entries);
4193
4704
  const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
4194
4705
  const tracker = this.sessionCostTrackers.get(sessionId);
4195
4706
  if (!tracker) {
@@ -4270,32 +4781,25 @@ export class ClaudeSessionService {
4270
4781
  : { linesRemoved: foregroundLineChanges.linesRemoved }),
4271
4782
  });
4272
4783
  }
4273
- if (sessionMemory && finalLeafUuid) {
4274
- const memorySnapshot = projectClaudeModelMessages(snapshot.entries);
4275
- const providerVisibleMessages = [
4276
- ...contextMessages,
4277
- ...injectTurnContext(memorySnapshot),
4278
- ];
4279
- const currentContextTokens = budget
4280
- ? budget.evaluate(providerVisibleMessages, definitions)
4281
- .occupancyTokens
4282
- : estimateModelRequestTokens(providerVisibleMessages, definitions);
4283
- try {
4284
- await sessionMemory.observeContext(currentContextTokens, currentTurnToolCalls, finalLeafUuid, memorySnapshot);
4285
- }
4286
- catch (error) {
4287
- // A failed observation must not fail the user turn; the sidecar
4288
- // retains a retryable error for the next observation.
4289
- this.options.eventSink?.({
4290
- type: 'warning',
4291
- message: `Session memory observation failed: ${error instanceof Error ? error.message : String(error)}`,
4292
- });
4293
- }
4294
- }
4295
- this.options.projectMemoryExtraction?.observe({
4296
- sessionId,
4784
+ const memorySnapshot = activeTurnMessages();
4785
+ const providerVisibleMessages = [
4786
+ ...contextMessages,
4787
+ ...injectTurnContext(memorySnapshot),
4788
+ ];
4789
+ const currentContextTokens = contextEngine.report({
4790
+ messages: providerVisibleMessages,
4791
+ tools: definitions,
4792
+ })?.occupancyTokens ??
4793
+ estimateModelRequestTokens(providerVisibleMessages, definitions);
4794
+ await turnMemory.observeSuccess({
4795
+ ...(finalLeafUuid ? { messageId: finalLeafUuid } : {}),
4796
+ occupancyTokens: currentContextTokens,
4797
+ toolCalls: currentTurnToolCalls,
4798
+ messages: memorySnapshot,
4799
+ projectMessages: nativeLease
4800
+ ? []
4801
+ : projectMemoryMessages(snapshot.entries),
4297
4802
  directMaintenance: projectMemoryMaintained,
4298
- messages: projectMemoryMessages(snapshot.entries),
4299
4803
  });
4300
4804
  turnCompleted = true;
4301
4805
  return {
@@ -4319,11 +4823,39 @@ export class ClaudeSessionService {
4319
4823
  await this.hookLifecycle.end(sessionId, 'other');
4320
4824
  }
4321
4825
  }
4322
- });
4323
- if (leaseResult.status === 'conflict') {
4324
- throw new Error(`Claude transcript append conflict: ${leaseResult.reason}`);
4826
+ };
4827
+ let result;
4828
+ if (this.nativeTranscriptWritesEnabled()) {
4829
+ const nativeStore = new NativeTranscriptStore({
4830
+ transcriptFile: sessionPaths.sessionFile,
4831
+ lockFile: join(sessionPaths.praxisRoot, 'locks', `${sessionId}.lock`),
4832
+ });
4833
+ const nativeTranscript = new NativeSessionTranscript({
4834
+ sessionId,
4835
+ store: nativeStore,
4836
+ });
4837
+ const activationKind = requireExisting
4838
+ ? resumeSessionAt === undefined
4839
+ ? { kind: 'resume' }
4840
+ : { kind: 'resume', atEventId: resumeSessionAt }
4841
+ : { kind: 'start' };
4842
+ result = await nativeTranscript.withLease(activationKind, async (nativeLease) => {
4843
+ const scratch = new InMemoryTranscriptStore();
4844
+ const scratchResult = await scratch.withLease((scratchLease) => runUnderLease(scratchLease, nativeLease));
4845
+ if (scratchResult.status === 'conflict') {
4846
+ throw new Error(`native scratch transcript conflict: ${scratchResult.reason}`);
4847
+ }
4848
+ return scratchResult.value;
4849
+ });
4850
+ }
4851
+ else {
4852
+ const store = this.turnStore(sessionId);
4853
+ const leaseResult = await store.withLease((lease) => runUnderLease(lease));
4854
+ if (leaseResult.status === 'conflict') {
4855
+ throw new Error(`Claude transcript append conflict: ${leaseResult.reason}`);
4856
+ }
4857
+ result = leaseResult.value;
4325
4858
  }
4326
- const result = leaseResult.value;
4327
4859
  controller.complete();
4328
4860
  return result;
4329
4861
  }
@@ -4425,11 +4957,12 @@ export class ClaudeSessionService {
4425
4957
  * recent suffix. Returns null to fall back to the existing full-compaction
4426
4958
  * behavior (missing/invalid watermark, empty projection, or oversized
4427
4959
  * projection). */
4428
- async selectMemoryPreservedCompact(sessionId, activeEntries) {
4960
+ async selectMemoryPreservedCompact(sessionId, activeEntries, options = {}) {
4429
4961
  const controller = this.sessionMemoryController(sessionId);
4430
4962
  if (controller === null)
4431
4963
  return null;
4432
- await controller.waitForCompact();
4964
+ if (options.sessionMemoryReady !== true)
4965
+ await controller.waitForCompact();
4433
4966
  let watermark = null;
4434
4967
  let memorySummary = '';
4435
4968
  try {
@@ -5052,21 +5585,110 @@ export class ClaudeSessionService {
5052
5585
  }
5053
5586
  }
5054
5587
  assertWritable() {
5588
+ if (this.nativeTranscriptWritesEnabled()) {
5589
+ throw new Error('experimental native operational writes are not enabled');
5590
+ }
5055
5591
  if (this.schema.writeMode !== 'read-write') {
5056
5592
  throw new Error(`Claude ${this.options.claudeVersion} session is read-only`);
5057
5593
  }
5058
5594
  }
5595
+ assertTurnWritable() {
5596
+ if (this.nativeTranscriptWritesEnabled())
5597
+ return;
5598
+ this.assertWritable();
5599
+ }
5059
5600
  sessionStatus(issue, entryCount) {
5060
5601
  if (issue || entryCount === 0)
5061
5602
  return 'corrupt';
5062
5603
  return this.schema.writeMode === 'read-write' ? 'ready' : 'read-only';
5063
5604
  }
5605
+ nativeSessionStatus(issue, entryCount) {
5606
+ if (issue?.kind === 'unsupported-version')
5607
+ return 'read-only';
5608
+ if (entryCount === 0 || issue)
5609
+ return 'corrupt';
5610
+ return this.nativeTranscriptWritesEnabled() ? 'ready' : 'read-only';
5611
+ }
5612
+ nativeTranscriptWritesEnabled() {
5613
+ return this.options.experimentalNativeTranscriptWrites === true;
5614
+ }
5615
+ assertNativeTranscriptOptions() {
5616
+ if (!this.nativeTranscriptWritesEnabled())
5617
+ return;
5618
+ if (this.options.dataPlane !== 'native')
5619
+ throw new Error('experimental native transcript writes require dataPlane native');
5620
+ if (this.options.sessionPersistence === false)
5621
+ throw new Error('experimental native transcript writes require sessionPersistence');
5622
+ const incompatible = [
5623
+ ['hooks', this.options.hooks !== undefined],
5624
+ [
5625
+ 'conditionalRuleResolver',
5626
+ this.options.conditionalRuleResolver !== undefined,
5627
+ ],
5628
+ ['extensions', this.options.extensions !== undefined],
5629
+ ['agent', this.options.agent !== undefined],
5630
+ ['fileCheckpointing', this.options.fileCheckpointing === true],
5631
+ ['fileRewindRoots', (this.options.fileRewindRoots?.length ?? 0) > 0],
5632
+ ['interactiveTools', this.options.interactiveTools !== undefined],
5633
+ ['mcp', this.options.mcp !== undefined],
5634
+ ['taskToolNames', (this.options.taskToolNames?.length ?? 0) > 0],
5635
+ [
5636
+ 'scheduledToolNames',
5637
+ (this.options.scheduledToolNames?.length ?? 0) > 0,
5638
+ ],
5639
+ ['enableDynamicWakeups', this.options.enableDynamicWakeups === true],
5640
+ ['enableSessionMemory', this.options.enableSessionMemory === true],
5641
+ [
5642
+ 'sessionMemoryProviderFactory',
5643
+ this.options.sessionMemoryProviderFactory !== undefined,
5644
+ ],
5645
+ [
5646
+ 'projectMemoryDirectory',
5647
+ this.options.projectMemoryDirectory !== undefined,
5648
+ ],
5649
+ ['projectMemoryRecall', this.options.projectMemoryRecall !== undefined],
5650
+ [
5651
+ 'projectMemoryExtraction',
5652
+ this.options.projectMemoryExtraction !== undefined,
5653
+ ],
5654
+ ['sessionKind', this.options.sessionKind !== undefined],
5655
+ ['workspace', this.options.workspace !== undefined],
5656
+ ['initialWorktree', this.options.initialWorktree === true],
5657
+ ['initialWorktreeName', this.options.initialWorktreeName !== undefined],
5658
+ ['enableWorktrees', this.options.enableWorktrees === true],
5659
+ ['worktreeToolNames', (this.options.worktreeToolNames?.length ?? 0) > 0],
5660
+ ['worktreeBaseRef', this.options.worktreeBaseRef !== undefined],
5661
+ ];
5662
+ const active = incompatible.find(([, enabled]) => enabled);
5663
+ if (active)
5664
+ throw new Error(`experimental native transcript writes incompatible with option ${active[0]}`);
5665
+ }
5666
+ claudeSessionSummary(sessionId, index) {
5667
+ const metadata = reduceClaudeSessionMetadata(index.entries, sessionId);
5668
+ return {
5669
+ sessionId,
5670
+ ...sessionMetadataFields(metadata, { agentNameFallback: true }),
5671
+ lastPrompt: metadata.lastPrompt ?? null,
5672
+ updatedAt: index.updatedAt,
5673
+ status: this.sessionStatus(index.issue, index.entries.length),
5674
+ issue: index.issue,
5675
+ };
5676
+ }
5064
5677
  provider() {
5065
5678
  if (!this.options.provider) {
5066
5679
  throw new Error('A model provider is required for run and resume');
5067
5680
  }
5068
5681
  return this.options.provider;
5069
5682
  }
5683
+ async assertNativeWriteTargetIsLegacy(sessionId) {
5684
+ if (this.nativeTranscriptWritesEnabled())
5685
+ return;
5686
+ if (this.options.dataPlane !== 'native')
5687
+ return;
5688
+ const existing = await readNativeTranscript(this.paths(sessionId).sessionFile);
5689
+ if (existing.format === 'native')
5690
+ throw new Error('native transcript is read-only until native writes are enabled');
5691
+ }
5070
5692
  model() {
5071
5693
  return this.activeProvider?.model ?? this.options.provider?.model;
5072
5694
  }
@@ -5124,7 +5746,7 @@ export class ClaudeSessionService {
5124
5746
  return this.sessionMemoryProviderFactory() !== null;
5125
5747
  }
5126
5748
  sessionMemoryController(sessionId) {
5127
- if (!this.sessionMemoryEnabled() || !isClaudeSessionId(sessionId)) {
5749
+ if (!this.sessionMemoryEnabled() || !isSessionId(sessionId)) {
5128
5750
  return null;
5129
5751
  }
5130
5752
  let controller = this.sessionMemoryControllers.get(sessionId);
@@ -5267,6 +5889,7 @@ export class ClaudeSessionService {
5267
5889
  ...(this.options.enableSubagents === undefined
5268
5890
  ? {}
5269
5891
  : { subagents: this.options.enableSubagents }),
5892
+ teams: this.leadOperations !== null,
5270
5893
  ...(this.options.toolCapabilityEnvironment
5271
5894
  ? { env: this.options.toolCapabilityEnvironment }
5272
5895
  : {}),
@@ -5283,7 +5906,20 @@ export class ClaudeSessionService {
5283
5906
  'CronDelete',
5284
5907
  'CronList',
5285
5908
  'ScheduleWakeup',
5286
- ].includes(name) || capabilities.has(name));
5909
+ ].includes(name) || capabilities.has(name))
5910
+ .filter((name) => !name.startsWith('Team') || capabilities.has(name));
5911
+ }
5912
+ teamRegistry(base, sessionId, enabledTools) {
5913
+ if (enabledTools.length === 0)
5914
+ return base;
5915
+ const operations = this.leadOperations;
5916
+ if (!operations)
5917
+ return base;
5918
+ const factory = this.options.teamLeadToolRegistryFactory;
5919
+ if (!factory) {
5920
+ throw new Error('Team lead operations require a Team lead tool registry factory');
5921
+ }
5922
+ return factory(base, operations, sessionId, enabledTools);
5287
5923
  }
5288
5924
  async append(lease, tail, entry) {
5289
5925
  const result = await lease.append(tail, entry);