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.
- package/README.md +20 -2
- package/dist/application/background-agent-manager.d.ts +16 -6
- package/dist/application/background-agent-manager.js +139 -97
- package/dist/application/context-engine.d.ts +56 -0
- package/dist/application/context-engine.js +129 -0
- package/dist/application/durable-follow-up.d.ts +14 -0
- package/dist/application/durable-follow-up.js +52 -0
- package/dist/application/native-session-transcript.d.ts +72 -0
- package/dist/application/native-session-transcript.js +351 -0
- package/dist/application/native-sidechain-transcript.d.ts +34 -0
- package/dist/application/native-sidechain-transcript.js +156 -0
- package/dist/application/session-service.d.ts +23 -4
- package/dist/application/session-service.js +1130 -494
- package/dist/application/subagent-service.d.ts +5 -1
- package/dist/application/subagent-service.js +749 -320
- package/dist/application/team-agent-runtime.d.ts +29 -0
- package/dist/application/team-agent-runtime.js +169 -0
- package/dist/application/team-capability.d.ts +27 -0
- package/dist/application/team-capability.js +41 -0
- package/dist/application/team-lead-operations.d.ts +38 -0
- package/dist/application/team-lead-operations.js +155 -0
- package/dist/application/team-mailbox.d.ts +54 -0
- package/dist/application/team-mailbox.js +196 -0
- package/dist/application/team-manager.d.ts +113 -0
- package/dist/application/team-manager.js +681 -0
- package/dist/application/team-workspace.d.ts +29 -0
- package/dist/application/team-workspace.js +197 -0
- package/dist/application/top-level-agent-manager.d.ts +1 -0
- package/dist/application/top-level-agent-manager.js +1025 -403
- package/dist/application/transcript-projection.d.ts +35 -0
- package/dist/application/transcript-projection.js +139 -0
- package/dist/application/turn-lifecycle.d.ts +2 -0
- package/dist/application/turn-memory-coordinator.d.ts +43 -0
- package/dist/application/turn-memory-coordinator.js +98 -0
- package/dist/cli/agents-dashboard.js +10 -5
- package/dist/cli/interactive.d.ts +7 -0
- package/dist/cli/interactive.js +645 -334
- package/dist/cli/tui/claude-style.d.ts +8 -25
- package/dist/cli/tui/claude-style.js +299 -277
- package/dist/cli/tui/decision-surface-model.d.ts +71 -0
- package/dist/cli/tui/decision-surface-model.js +131 -0
- package/dist/cli/tui/decision-surface.d.ts +7 -0
- package/dist/cli/tui/decision-surface.js +41 -0
- package/dist/cli/tui/help-surface-model.d.ts +68 -0
- package/dist/cli/tui/help-surface-model.js +131 -0
- package/dist/cli/tui/mcp-panel.js +1 -2
- package/dist/cli/tui/permission-surface-model.d.ts +195 -0
- package/dist/cli/tui/permission-surface-model.js +360 -0
- package/dist/cli/tui/permission-surface.d.ts +7 -0
- package/dist/cli/tui/permission-surface.js +69 -0
- package/dist/cli/tui/presentation-environment.d.ts +32 -0
- package/dist/cli/tui/presentation-environment.js +149 -0
- package/dist/cli/tui/task-panel.js +10 -1
- package/dist/cli/tui/theme.d.ts +46 -3
- package/dist/cli/tui/theme.js +414 -56
- package/dist/cli/tui/tool-permission.js +16 -8
- package/dist/cli/tui/transcript-presentation.d.ts +19 -1
- package/dist/cli/tui/transcript-presentation.js +26 -6
- package/dist/cli/tui/transcript-viewport.d.ts +18 -0
- package/dist/cli/tui/transcript-viewport.js +517 -111
- package/dist/cli/tui/transcript-window-model.d.ts +108 -0
- package/dist/cli/tui/transcript-window-model.js +772 -0
- package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
- package/dist/cli/tui/tui-interaction-router.js +127 -0
- package/dist/cli/tui/tui-screen-model.d.ts +114 -0
- package/dist/cli/tui/tui-screen-model.js +210 -0
- package/dist/cli/tui/tui-view-model.d.ts +2 -49
- package/dist/cli/tui/tui-view-model.js +1 -68
- package/dist/cli-runtime.d.ts +4 -2
- package/dist/cli-runtime.js +397 -32
- package/dist/compatibility/claude/transcript-codec.js +6 -0
- package/dist/core/agent-orchestration.d.ts +31 -0
- package/dist/core/agent-orchestration.js +235 -0
- package/dist/core/permission-policy.d.ts +4 -0
- package/dist/core/permission-policy.js +21 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +20 -2
- package/dist/core/team-mailbox.d.ts +54 -0
- package/dist/core/team-mailbox.js +212 -0
- package/dist/core/team-ownership.d.ts +84 -0
- package/dist/core/team-ownership.js +505 -0
- package/dist/core/tool-execution-scheduler.js +6 -1
- package/dist/core/transcript-event.d.ts +5 -1
- package/dist/core/transcript-event.js +12 -0
- package/dist/persistence/claude-job-store.d.ts +58 -0
- package/dist/persistence/claude-job-store.js +483 -1
- package/dist/persistence/claude-transcript-store.d.ts +2 -1
- package/dist/persistence/claude-transcript-store.js +2 -10
- package/dist/persistence/native-team-mailbox.d.ts +68 -0
- package/dist/persistence/native-team-mailbox.js +542 -0
- package/dist/persistence/native-team-store.d.ts +34 -0
- package/dist/persistence/native-team-store.js +257 -0
- package/dist/persistence/native-transcript-reader.d.ts +53 -0
- package/dist/persistence/native-transcript-reader.js +283 -0
- package/dist/persistence/native-transcript-store.d.ts +69 -0
- package/dist/persistence/native-transcript-store.js +321 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
- package/dist/persistence/subagent-lifecycle-store.js +373 -146
- package/dist/persistence/transcript-file-append.d.ts +2 -0
- package/dist/persistence/transcript-file-append.js +10 -0
- package/dist/platform/exclusive-file-lease.d.ts +3 -0
- package/dist/platform/exclusive-file-lease.js +3 -0
- package/dist/tools/claude-capabilities.d.ts +4 -0
- package/dist/tools/claude-capabilities.js +18 -2
- package/dist/tools/team-lead-tools.d.ts +19 -0
- package/dist/tools/team-lead-tools.js +479 -0
- package/dist/tools/team-member-tools.d.ts +21 -0
- package/dist/tools/team-member-tools.js +259 -0
- package/package.json +1 -1
- package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
- 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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
1558
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
1788
|
+
throw new Error(`${dataPlaneName} resume path must end in .jsonl: ${path}`);
|
|
1717
1789
|
}
|
|
1718
1790
|
const sessionId = basename(path, '.jsonl');
|
|
1719
|
-
if (!
|
|
1720
|
-
throw new Error(
|
|
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
|
|
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.
|
|
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
|
|
2734
|
-
|
|
2735
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
!
|
|
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
|
|
2838
|
-
|
|
2839
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3263
|
-
{
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
: {}),
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
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 && !
|
|
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
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
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
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
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:
|
|
3590
|
-
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
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('
|
|
4098
|
+
content: formatClaudeCompactSummary(''),
|
|
3599
4099
|
},
|
|
3600
|
-
], definitions)
|
|
3601
|
-
targetTokens =
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
const
|
|
3625
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
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
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
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
|
-
|
|
3740
|
-
|
|
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
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
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
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
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
|
-
|
|
3763
|
-
|
|
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 && !
|
|
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:
|
|
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
|
|
4479
|
+
await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
|
|
3940
4480
|
budget.assertFits(budget.evaluate([
|
|
3941
4481
|
...contextMessages,
|
|
3942
|
-
...injectTurnContext(
|
|
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(
|
|
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 =
|
|
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
|
|
4516
|
+
await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
|
|
3977
4517
|
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
3978
4518
|
return [
|
|
3979
4519
|
...contextMessages,
|
|
3980
4520
|
...injectTurnContext([
|
|
3981
|
-
...
|
|
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
|
-
|
|
4093
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
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
|
-
|
|
4324
|
-
|
|
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
|
-
|
|
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() || !
|
|
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);
|