praxis-agent 0.65.0 → 0.67.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 +12 -7
- package/dist/application/compaction-accounting.d.ts +49 -0
- package/dist/application/compaction-accounting.js +349 -0
- package/dist/application/compaction-errors.d.ts +21 -0
- package/dist/application/compaction-errors.js +24 -0
- package/dist/application/context-engine.js +10 -1
- package/dist/application/native-session-transcript.d.ts +3 -0
- package/dist/application/native-session-transcript.js +36 -4
- package/dist/application/session-service.d.ts +2 -0
- package/dist/application/session-service.js +526 -197
- package/dist/application/turn-accounting.js +0 -4
- package/dist/build-identity.json +1 -0
- package/dist/cli-runtime.js +2 -0
- package/dist/evals/project-eval-comparison.d.ts +4 -1
- package/dist/evals/project-eval-comparison.js +234 -78
- package/dist/evals/project-eval-identity.d.ts +4 -1
- package/dist/evals/project-eval-identity.js +9 -2
- package/dist/evals/project-eval-runner.d.ts +24 -2
- package/dist/evals/project-eval-runner.js +118 -59
- package/dist/evals/project-eval-schema.d.ts +8 -2
- package/dist/evals/project-eval-schema.js +27 -11
- package/dist/evals/project-eval.d.ts +22 -2
- package/dist/evals/project-eval.js +34 -1
- package/dist/native/ownership.d.ts +6 -1
- package/dist/native/ownership.js +6 -0
- package/dist/persistence/native-compaction-receipt-store.d.ts +40 -0
- package/dist/persistence/native-compaction-receipt-store.js +380 -0
- package/dist/platform/praxis-build-identity.d.ts +14 -0
- package/dist/platform/praxis-build-identity.js +204 -0
- package/package.json +2 -2
|
@@ -22,7 +22,7 @@ import { usageCostUsd } from '../core/usage.js';
|
|
|
22
22
|
import { isSessionId } from '../core/session.js';
|
|
23
23
|
import { ContextBudget, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
24
24
|
import { ContextEngine } from './context-engine.js';
|
|
25
|
-
import { ContextPreparation } from './context-preparation.js';
|
|
25
|
+
import { ContextPreparation, StaleContextGenerationError, } from './context-preparation.js';
|
|
26
26
|
import { TurnMemoryCoordinator } from './turn-memory-coordinator.js';
|
|
27
27
|
import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
|
|
28
28
|
import { assembleContextSnapshot } from '../core/prompt-composer.js';
|
|
@@ -35,6 +35,9 @@ import { NativeTranscriptStore, } from '../persistence/native-transcript-store.j
|
|
|
35
35
|
import { NativeSessionTranscript, } from './native-session-transcript.js';
|
|
36
36
|
import { TurnPersistence } from './turn-persistence.js';
|
|
37
37
|
import { TurnAccounting } from './turn-accounting.js';
|
|
38
|
+
import { CompactionAccounting } from './compaction-accounting.js';
|
|
39
|
+
import { classifyCompactionError } from './compaction-errors.js';
|
|
40
|
+
import { NativeCompactionReceiptFileStore } from '../persistence/native-compaction-receipt-store.js';
|
|
38
41
|
import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
|
|
39
42
|
import { ModelCompactor } from './model-compactor.js';
|
|
40
43
|
import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
|
|
@@ -670,6 +673,7 @@ export class ClaudeSessionService {
|
|
|
670
673
|
activeProvider;
|
|
671
674
|
mcpClosePromise;
|
|
672
675
|
sessionCostTrackers = new Map();
|
|
676
|
+
compactionAccountings = new Map();
|
|
673
677
|
activeCostSessionId;
|
|
674
678
|
closeCostSavePromise;
|
|
675
679
|
closeMetadataSavePromise;
|
|
@@ -918,6 +922,7 @@ export class ClaudeSessionService {
|
|
|
918
922
|
await this.closeMetadataSavePromise;
|
|
919
923
|
this.closeCostSavePromise ??= this.persistActiveSessionCost();
|
|
920
924
|
await this.closeCostSavePromise;
|
|
925
|
+
this.compactionAccountings.clear();
|
|
921
926
|
this.mcpClosePromise ??= this.options.mcp?.close?.() ?? Promise.resolve();
|
|
922
927
|
await this.mcpClosePromise;
|
|
923
928
|
await this.leadOperations?.close();
|
|
@@ -1996,12 +2001,21 @@ export class ClaudeSessionService {
|
|
|
1996
2001
|
this.assertSessionPersistence();
|
|
1997
2002
|
const provider = this.provider();
|
|
1998
2003
|
const sessionPaths = this.paths(sessionId);
|
|
2004
|
+
const nativeStore = new NativeTranscriptStore({
|
|
2005
|
+
transcriptFile: sessionPaths.sessionFile,
|
|
2006
|
+
lockFile: join(sessionPaths.praxisRoot, 'locks', `${sessionId}.lock`),
|
|
2007
|
+
});
|
|
2008
|
+
const existingTranscript = await nativeStore.load();
|
|
2009
|
+
if (existingTranscript.records.length === 0)
|
|
2010
|
+
throw new Error('native transcript session is missing or empty');
|
|
2011
|
+
if (existingTranscript.records.some((record) => record.event.sessionId !== sessionId))
|
|
2012
|
+
throw new Error('native transcript sessionId does not match');
|
|
2013
|
+
await this.activateSessionCostTracker(sessionId);
|
|
2014
|
+
const compactionAccounting = this.compactionAccounting(sessionId, nativeStore);
|
|
2015
|
+
await compactionAccounting.recover();
|
|
1999
2016
|
const nativeTranscript = new NativeSessionTranscript({
|
|
2000
2017
|
sessionId,
|
|
2001
|
-
store:
|
|
2002
|
-
transcriptFile: sessionPaths.sessionFile,
|
|
2003
|
-
lockFile: join(sessionPaths.praxisRoot, 'locks', `${sessionId}.lock`),
|
|
2004
|
-
}),
|
|
2018
|
+
store: nativeStore,
|
|
2005
2019
|
});
|
|
2006
2020
|
return nativeTranscript.withLease({
|
|
2007
2021
|
kind: 'resume',
|
|
@@ -2014,7 +2028,12 @@ export class ClaudeSessionService {
|
|
|
2014
2028
|
if (selection) {
|
|
2015
2029
|
const targetIndex = branchEvents.findIndex((event) => event.id === selection.messageId);
|
|
2016
2030
|
if (targetIndex < 0)
|
|
2017
|
-
throw new Error(`No native rewind point found with event.id: ${selection.messageId}`)
|
|
2031
|
+
throw classifyCompactionError(new Error(`No native rewind point found with event.id: ${selection.messageId}`), {
|
|
2032
|
+
trigger: 'manual',
|
|
2033
|
+
phase: 'validation',
|
|
2034
|
+
durableState: 'not_committed',
|
|
2035
|
+
recoveryDisposition: 'none',
|
|
2036
|
+
});
|
|
2018
2037
|
const selectedEvents = selection.direction === 'from'
|
|
2019
2038
|
? branchEvents.slice(targetIndex)
|
|
2020
2039
|
: branchEvents.slice(0, targetIndex);
|
|
@@ -2029,7 +2048,17 @@ export class ClaudeSessionService {
|
|
|
2029
2048
|
: branchEvents[targetIndex - 1])?.id;
|
|
2030
2049
|
}
|
|
2031
2050
|
else {
|
|
2032
|
-
|
|
2051
|
+
try {
|
|
2052
|
+
memorySelection = await this.selectMemoryPreservedCompact(sessionId, projectNativeSessionEntries(branchEvents));
|
|
2053
|
+
}
|
|
2054
|
+
catch (error) {
|
|
2055
|
+
throw classifyCompactionError(error, {
|
|
2056
|
+
trigger: 'manual',
|
|
2057
|
+
phase: 'validation',
|
|
2058
|
+
durableState: 'not_committed',
|
|
2059
|
+
recoveryDisposition: 'none',
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2033
2062
|
if (memorySelection) {
|
|
2034
2063
|
messages = [
|
|
2035
2064
|
memorySelection.memoryMessage,
|
|
@@ -2040,118 +2069,188 @@ export class ClaudeSessionService {
|
|
|
2040
2069
|
}
|
|
2041
2070
|
}
|
|
2042
2071
|
if (messages.length === 0)
|
|
2043
|
-
throw new Error('Cannot compact an empty native transcript')
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2072
|
+
throw classifyCompactionError(new Error('Cannot compact an empty native transcript'), {
|
|
2073
|
+
trigger: 'manual',
|
|
2074
|
+
phase: 'validation',
|
|
2075
|
+
durableState: 'not_committed',
|
|
2076
|
+
recoveryDisposition: 'none',
|
|
2077
|
+
});
|
|
2047
2078
|
const preTokens = estimateModelRequestTokens(messages);
|
|
2048
2079
|
this.options.eventSink?.({ type: 'state', state: 'compacting' });
|
|
2049
2080
|
const contextWindowTokens = this.contextBudget(provider)?.contextWindowTokens ??
|
|
2050
2081
|
provider.capabilities.contextWindowTokens ??
|
|
2051
2082
|
200_000;
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2083
|
+
let preCompact;
|
|
2084
|
+
try {
|
|
2085
|
+
preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', {
|
|
2086
|
+
trigger: 'manual',
|
|
2087
|
+
custom_instructions: selection?.context ?? null,
|
|
2088
|
+
}, 'manual', signal);
|
|
2089
|
+
}
|
|
2090
|
+
catch (error) {
|
|
2091
|
+
throw classifyCompactionError(error, {
|
|
2092
|
+
trigger: 'manual',
|
|
2093
|
+
phase: 'generation',
|
|
2094
|
+
durableState: 'not_committed',
|
|
2095
|
+
recoveryDisposition: 'none',
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
let compacted;
|
|
2099
|
+
try {
|
|
2100
|
+
compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
|
|
2101
|
+
messages: [
|
|
2102
|
+
...messages,
|
|
2103
|
+
...(selection?.context
|
|
2104
|
+
? [
|
|
2105
|
+
{
|
|
2106
|
+
role: 'user',
|
|
2107
|
+
content: `Additional summarization context: ${selection.context}`,
|
|
2108
|
+
},
|
|
2109
|
+
]
|
|
2110
|
+
: []),
|
|
2111
|
+
...successfulHookOutput(preCompact).map((content) => ({
|
|
2112
|
+
role: 'user',
|
|
2113
|
+
content: `Additional summarization context: ${content}`,
|
|
2114
|
+
})),
|
|
2115
|
+
],
|
|
2116
|
+
targetTokens: Math.min(8192, Math.max(1, Math.floor(contextWindowTokens / 4))),
|
|
2117
|
+
contextWindowTokens,
|
|
2118
|
+
...(signal ? { signal } : {}),
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
catch (error) {
|
|
2122
|
+
throw classifyCompactionError(error, {
|
|
2123
|
+
trigger: 'manual',
|
|
2124
|
+
phase: 'generation',
|
|
2125
|
+
durableState: 'not_committed',
|
|
2126
|
+
recoveryDisposition: 'none',
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2076
2129
|
if (signal?.aborted)
|
|
2077
2130
|
throw new AgentRunCancelledError();
|
|
2078
|
-
|
|
2079
|
-
|
|
2131
|
+
let durationMs;
|
|
2132
|
+
let durationWithoutRetriesMs;
|
|
2133
|
+
try {
|
|
2134
|
+
const durations = requireCompactionDurations(compacted);
|
|
2135
|
+
durationMs = durations.durationMs;
|
|
2136
|
+
durationWithoutRetriesMs = durations.durationWithoutRetriesMs;
|
|
2137
|
+
requireManualCompactUsage(compacted.usage);
|
|
2138
|
+
}
|
|
2139
|
+
catch (error) {
|
|
2140
|
+
throw classifyCompactionError(error, {
|
|
2141
|
+
trigger: 'manual',
|
|
2142
|
+
phase: 'validation',
|
|
2143
|
+
durableState: 'not_committed',
|
|
2144
|
+
recoveryDisposition: 'none',
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2080
2147
|
const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
|
|
2081
2148
|
? compacted.model
|
|
2082
2149
|
: provider.model;
|
|
2083
2150
|
const meaningfulMetering = compacted.summary.trim().length > 0 &&
|
|
2084
|
-
|
|
2151
|
+
hasNonZeroUsage(compacted.usage);
|
|
2152
|
+
if (compacted.summary.trim().length === 0)
|
|
2153
|
+
throw classifyCompactionError(new Error('Native compact summary must not be blank'), {
|
|
2154
|
+
trigger: 'manual',
|
|
2155
|
+
phase: 'validation',
|
|
2156
|
+
durableState: 'not_committed',
|
|
2157
|
+
recoveryDisposition: 'none',
|
|
2158
|
+
});
|
|
2085
2159
|
if (meaningfulMetering &&
|
|
2086
2160
|
(compactModel === undefined || compactModel.trim() === ''))
|
|
2087
|
-
throw new Error('Manual compact usage requires a nonblank model identity')
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
if (meaningfulMetering &&
|
|
2093
|
-
compactModel !== undefined &&
|
|
2094
|
-
compactModel.trim() !== '') {
|
|
2095
|
-
const pricing = this.options.pricing?.resolve(compactModel);
|
|
2096
|
-
const costUsd = pricing
|
|
2097
|
-
? usageCostUsd(compacted.usage, pricing)
|
|
2098
|
-
: undefined;
|
|
2099
|
-
meteringTurnInput = {
|
|
2100
|
-
model: compactModel,
|
|
2101
|
-
usage: compacted.usage,
|
|
2102
|
-
...(costUsd === undefined ? {} : { costUsd }),
|
|
2103
|
-
...(compacted.usage.webSearchRequests === undefined
|
|
2104
|
-
? {}
|
|
2105
|
-
: { webSearchRequests: compacted.usage.webSearchRequests }),
|
|
2106
|
-
apiDurationMs: durationMs,
|
|
2107
|
-
apiDurationWithoutRetriesMs: durationWithoutRetriesMs,
|
|
2108
|
-
};
|
|
2109
|
-
const preflight = new ClaudeSessionCostTracker({
|
|
2110
|
-
sessionId,
|
|
2111
|
-
restored: tracker.snapshot(),
|
|
2161
|
+
throw classifyCompactionError(new Error('Manual compact usage requires a nonblank model identity'), {
|
|
2162
|
+
trigger: 'manual',
|
|
2163
|
+
phase: 'validation',
|
|
2164
|
+
durableState: 'not_committed',
|
|
2165
|
+
recoveryDisposition: 'none',
|
|
2112
2166
|
});
|
|
2113
|
-
|
|
2114
|
-
|
|
2167
|
+
const preparedTransaction = await compactionAccounting.prepare({
|
|
2168
|
+
trigger: 'manual',
|
|
2169
|
+
metric: {
|
|
2170
|
+
usage: compacted.usage,
|
|
2171
|
+
...(hasNonZeroUsage(compacted.usage) && compactModel !== undefined
|
|
2172
|
+
? { model: compactModel }
|
|
2173
|
+
: {}),
|
|
2174
|
+
durationApiMs: durationMs,
|
|
2175
|
+
durationApiWithoutRetriesMs: durationWithoutRetriesMs,
|
|
2176
|
+
},
|
|
2177
|
+
});
|
|
2115
2178
|
const postTokens = estimateModelRequestTokens([
|
|
2116
2179
|
{ role: 'user', content: compacted.summary },
|
|
2117
2180
|
]);
|
|
2118
2181
|
if (signal?.aborted)
|
|
2119
2182
|
throw new AgentRunCancelledError();
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2183
|
+
let ids;
|
|
2184
|
+
try {
|
|
2185
|
+
ids = await lease.appendCompaction({
|
|
2186
|
+
summary: compacted.summary,
|
|
2187
|
+
trigger: 'manual',
|
|
2188
|
+
preTokens,
|
|
2189
|
+
postTokens,
|
|
2190
|
+
durationMs,
|
|
2191
|
+
boundaryId: preparedTransaction.boundaryId,
|
|
2192
|
+
summaryId: preparedTransaction.summaryId,
|
|
2193
|
+
...(preservedMessages.length > 0 ? { preservedMessages } : {}),
|
|
2194
|
+
...(logicalParentId === undefined ? {} : { logicalParentId }),
|
|
2195
|
+
...(selection
|
|
2196
|
+
? {
|
|
2197
|
+
direction: selection.direction === 'to'
|
|
2198
|
+
? 'up_to'
|
|
2199
|
+
: 'from',
|
|
2200
|
+
messagesSummarized: messages.length,
|
|
2201
|
+
preservePrefix: selection.direction === 'from' &&
|
|
2202
|
+
branchEvents.findIndex((event) => event.id === selection.messageId) > 0,
|
|
2203
|
+
}
|
|
2204
|
+
: {}),
|
|
2205
|
+
...(memorySelection
|
|
2206
|
+
? {
|
|
2207
|
+
direction: 'from',
|
|
2208
|
+
messagesSummarized: messages.length,
|
|
2209
|
+
preservePrefix: false,
|
|
2210
|
+
}
|
|
2211
|
+
: {}),
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
catch (error) {
|
|
2215
|
+
throw classifyCompactionError(error, {
|
|
2216
|
+
trigger: 'manual',
|
|
2217
|
+
phase: 'transcript_commit',
|
|
2218
|
+
durableState: 'indeterminate',
|
|
2219
|
+
recoveryDisposition: 'reconcile',
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
await preparedTransaction.commit({
|
|
2223
|
+
kind: 'compaction',
|
|
2224
|
+
boundaryId: ids.boundaryId,
|
|
2225
|
+
summaryId: ids.summaryId,
|
|
2154
2226
|
});
|
|
2227
|
+
try {
|
|
2228
|
+
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'manual', compact_summary: compacted.summary }, 'manual', signal);
|
|
2229
|
+
}
|
|
2230
|
+
catch (error) {
|
|
2231
|
+
throw classifyCompactionError(error, {
|
|
2232
|
+
trigger: 'manual',
|
|
2233
|
+
phase: 'post_commit',
|
|
2234
|
+
durableState: 'committed',
|
|
2235
|
+
recoveryDisposition: 'reconcile',
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
try {
|
|
2239
|
+
this.options.eventSink?.({
|
|
2240
|
+
type: 'compact-boundary',
|
|
2241
|
+
trigger: 'manual',
|
|
2242
|
+
preTokens,
|
|
2243
|
+
uuid: ids.boundaryId,
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
catch (error) {
|
|
2247
|
+
throw classifyCompactionError(error, {
|
|
2248
|
+
trigger: 'manual',
|
|
2249
|
+
phase: 'post_commit',
|
|
2250
|
+
durableState: 'committed',
|
|
2251
|
+
recoveryDisposition: 'reconcile',
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2155
2254
|
return {
|
|
2156
2255
|
summary: compacted.summary,
|
|
2157
2256
|
usage: compacted.usage,
|
|
@@ -2340,7 +2439,7 @@ export class ClaudeSessionService {
|
|
|
2340
2439
|
await this.appendNativeCommand(sessionId, `/permission-mode ${permissionMode}`);
|
|
2341
2440
|
await this.options.interactiveTools?.setMode(sessionId, permissionMode);
|
|
2342
2441
|
}
|
|
2343
|
-
async activateSessionCostTracker(sessionId) {
|
|
2442
|
+
async activateSessionCostTracker(sessionId, persistPrevious = true) {
|
|
2344
2443
|
if (this.activeCostSessionId === sessionId)
|
|
2345
2444
|
return;
|
|
2346
2445
|
const store = this.options.costStateStore;
|
|
@@ -2349,7 +2448,7 @@ export class ClaudeSessionService {
|
|
|
2349
2448
|
// Native state slot is not overwritten before it is read.
|
|
2350
2449
|
const loaded = await store.load(sessionId);
|
|
2351
2450
|
const currentId = this.activeCostSessionId;
|
|
2352
|
-
if (currentId !== undefined) {
|
|
2451
|
+
if (persistPrevious && currentId !== undefined) {
|
|
2353
2452
|
const current = this.sessionCostTrackers.get(currentId);
|
|
2354
2453
|
if (current)
|
|
2355
2454
|
await store.save(current.snapshot());
|
|
@@ -2405,17 +2504,17 @@ export class ClaudeSessionService {
|
|
|
2405
2504
|
});
|
|
2406
2505
|
}
|
|
2407
2506
|
async costSnapshot(sessionId) {
|
|
2408
|
-
const
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
const tracker =
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2507
|
+
const priorActiveCostSessionId = this.activeCostSessionId;
|
|
2508
|
+
await this.activateSessionCostTracker(sessionId, false);
|
|
2509
|
+
// Snapshots are observational and must not change which session close()
|
|
2510
|
+
// persists. A subsequent turn/compact activation will perform the normal
|
|
2511
|
+
// target-load-before-current-save transition.
|
|
2512
|
+
this.activeCostSessionId = priorActiveCostSessionId;
|
|
2513
|
+
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
2514
|
+
if (!tracker)
|
|
2515
|
+
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
2516
|
+
const accounting = this.compactionAccounting(sessionId, this.nativeStore(sessionId));
|
|
2517
|
+
await accounting.recover();
|
|
2419
2518
|
return this.withDetachedSubagentUsage(sessionId, tracker.snapshot());
|
|
2420
2519
|
}
|
|
2421
2520
|
async withDetachedSubagentUsage(sessionId, base) {
|
|
@@ -2524,7 +2623,7 @@ export class ClaudeSessionService {
|
|
|
2524
2623
|
}
|
|
2525
2624
|
const sessionPaths = this.paths(sessionId);
|
|
2526
2625
|
const toolResultDirectory = join(sessionPaths.projectRoot, sessionId, 'tool-results');
|
|
2527
|
-
const runUnderLease = async (persistence) => {
|
|
2626
|
+
const runUnderLease = async (persistence, compactionAccounting) => {
|
|
2528
2627
|
const activeTracker = this.sessionCostTrackers.get(sessionId);
|
|
2529
2628
|
if (!activeTracker) {
|
|
2530
2629
|
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
@@ -3380,8 +3479,32 @@ export class ClaudeSessionService {
|
|
|
3380
3479
|
...(budget ? { budget } : {}),
|
|
3381
3480
|
autoCompact: this.options.autoCompact !== false,
|
|
3382
3481
|
memory: {
|
|
3383
|
-
beforeCompact: () =>
|
|
3384
|
-
|
|
3482
|
+
beforeCompact: async () => {
|
|
3483
|
+
try {
|
|
3484
|
+
await turnMemory.beforeCompact();
|
|
3485
|
+
}
|
|
3486
|
+
catch (error) {
|
|
3487
|
+
throw classifyCompactionError(error, {
|
|
3488
|
+
trigger: 'auto',
|
|
3489
|
+
phase: 'validation',
|
|
3490
|
+
durableState: 'not_committed',
|
|
3491
|
+
recoveryDisposition: 'none',
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
},
|
|
3495
|
+
afterCompact: async () => {
|
|
3496
|
+
try {
|
|
3497
|
+
await turnMemory.afterCompact();
|
|
3498
|
+
}
|
|
3499
|
+
catch (error) {
|
|
3500
|
+
throw classifyCompactionError(error, {
|
|
3501
|
+
trigger: 'auto',
|
|
3502
|
+
phase: 'post_commit',
|
|
3503
|
+
durableState: 'committed',
|
|
3504
|
+
recoveryDisposition: 'reconcile',
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
},
|
|
3385
3508
|
},
|
|
3386
3509
|
});
|
|
3387
3510
|
observeModelRequestUsage = ({ usage, messages, tools }) => {
|
|
@@ -3431,14 +3554,29 @@ export class ClaudeSessionService {
|
|
|
3431
3554
|
}).envelope,
|
|
3432
3555
|
propose: async () => {
|
|
3433
3556
|
if (!budget)
|
|
3434
|
-
throw new Error('Context budget is unavailable')
|
|
3435
|
-
|
|
3436
|
-
|
|
3557
|
+
throw classifyCompactionError(new Error('Context budget is unavailable'), {
|
|
3558
|
+
trigger: 'auto',
|
|
3559
|
+
phase: 'validation',
|
|
3560
|
+
durableState: 'not_committed',
|
|
3561
|
+
recoveryDisposition: 'none',
|
|
3562
|
+
});
|
|
3563
|
+
const definitions = await autoValidation(() => contextPreparation.project().envelope.tools);
|
|
3564
|
+
const historyMessages = await autoValidation(activeTurnMessages);
|
|
3437
3565
|
if (historyMessages.length === 0)
|
|
3438
|
-
throw new Error('Cannot compact an empty native transcript')
|
|
3566
|
+
throw classifyCompactionError(new Error('Cannot compact an empty native transcript'), {
|
|
3567
|
+
trigger: 'auto',
|
|
3568
|
+
phase: 'validation',
|
|
3569
|
+
durableState: 'not_committed',
|
|
3570
|
+
recoveryDisposition: 'none',
|
|
3571
|
+
});
|
|
3439
3572
|
if (unresolvedActiveToolCallIds(historyMessages).length > 0)
|
|
3440
|
-
throw new Error('Cannot compact a native transcript with unresolved tool calls')
|
|
3441
|
-
|
|
3573
|
+
throw classifyCompactionError(new Error('Cannot compact a native transcript with unresolved tool calls'), {
|
|
3574
|
+
trigger: 'auto',
|
|
3575
|
+
phase: 'validation',
|
|
3576
|
+
durableState: 'not_committed',
|
|
3577
|
+
recoveryDisposition: 'none',
|
|
3578
|
+
});
|
|
3579
|
+
const irreducibleMessages = await autoValidation(() => contextPreparation.project({
|
|
3442
3580
|
includeHistory: false,
|
|
3443
3581
|
includeMemory: false,
|
|
3444
3582
|
pendingMessages: [
|
|
@@ -3448,7 +3586,7 @@ export class ClaudeSessionService {
|
|
|
3448
3586
|
content,
|
|
3449
3587
|
})),
|
|
3450
3588
|
],
|
|
3451
|
-
}).envelope.messages;
|
|
3589
|
+
}).envelope.messages);
|
|
3452
3590
|
let compactableMessages = historyMessages;
|
|
3453
3591
|
let preservedMessages = [];
|
|
3454
3592
|
let compactionLogicalParentId;
|
|
@@ -3467,7 +3605,12 @@ export class ClaudeSessionService {
|
|
|
3467
3605
|
}
|
|
3468
3606
|
}
|
|
3469
3607
|
if (found < 0)
|
|
3470
|
-
throw new Error('Native automatic compaction could not match the current-turn suffix')
|
|
3608
|
+
throw classifyCompactionError(new Error('Native automatic compaction could not match the current-turn suffix'), {
|
|
3609
|
+
trigger: 'auto',
|
|
3610
|
+
phase: 'validation',
|
|
3611
|
+
durableState: 'not_committed',
|
|
3612
|
+
recoveryDisposition: 'none',
|
|
3613
|
+
});
|
|
3471
3614
|
matchedIndexes.push(found);
|
|
3472
3615
|
searchFrom = found;
|
|
3473
3616
|
}
|
|
@@ -3493,7 +3636,7 @@ export class ClaudeSessionService {
|
|
|
3493
3636
|
}
|
|
3494
3637
|
}
|
|
3495
3638
|
const memorySelection = sessionMemory
|
|
3496
|
-
? await this.selectMemoryPreservedCompact(sessionId, persistence.view().projectionEntries)
|
|
3639
|
+
? await autoValidation(() => this.selectMemoryPreservedCompact(sessionId, persistence.view().projectionEntries))
|
|
3497
3640
|
: null;
|
|
3498
3641
|
if (memorySelection) {
|
|
3499
3642
|
compactableMessages = [
|
|
@@ -3521,47 +3664,102 @@ export class ClaudeSessionService {
|
|
|
3521
3664
|
message.content === anchor.content))?.id;
|
|
3522
3665
|
}
|
|
3523
3666
|
}
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
messages: [
|
|
3536
|
-
...compactableMessages,
|
|
3537
|
-
...successfulHookOutput(await this.runAdvisoryHook(sessionId, 'PreCompact', {
|
|
3538
|
-
trigger: 'auto',
|
|
3539
|
-
custom_instructions: null,
|
|
3540
|
-
}, 'auto', signal)).map((content) => ({
|
|
3667
|
+
await autoValidation(() => {
|
|
3668
|
+
this.options.eventSink?.({
|
|
3669
|
+
type: 'state',
|
|
3670
|
+
state: 'compacting',
|
|
3671
|
+
});
|
|
3672
|
+
});
|
|
3673
|
+
const preTokens = await autoValidation(() => estimateModelRequestTokens(compactableMessages));
|
|
3674
|
+
const compactEnvelope = await autoValidation(() => {
|
|
3675
|
+
const envelope = budget.evaluate([
|
|
3676
|
+
...irreducibleMessages,
|
|
3677
|
+
{
|
|
3541
3678
|
role: 'user',
|
|
3542
|
-
content:
|
|
3543
|
-
}
|
|
3544
|
-
],
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
contextWindowTokens: budget.contextWindowTokens,
|
|
3548
|
-
...(signal ? { signal } : {}),
|
|
3679
|
+
content: formatClaudeCompactSummary(''),
|
|
3680
|
+
},
|
|
3681
|
+
], definitions);
|
|
3682
|
+
budget.assertFits(envelope);
|
|
3683
|
+
return envelope;
|
|
3549
3684
|
});
|
|
3550
|
-
|
|
3685
|
+
let compacted;
|
|
3686
|
+
try {
|
|
3687
|
+
compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
|
|
3688
|
+
messages: [
|
|
3689
|
+
...compactableMessages,
|
|
3690
|
+
...successfulHookOutput(await this.runAdvisoryHook(sessionId, 'PreCompact', {
|
|
3691
|
+
trigger: 'auto',
|
|
3692
|
+
custom_instructions: null,
|
|
3693
|
+
}, 'auto', signal)).map((content) => ({
|
|
3694
|
+
role: 'user',
|
|
3695
|
+
content: `Additional summarization context: ${content}`,
|
|
3696
|
+
})),
|
|
3697
|
+
],
|
|
3698
|
+
targetTokens: Math.min(8192, Math.max(1, compactEnvelope.availableTokens -
|
|
3699
|
+
compactEnvelope.estimatedTokens)),
|
|
3700
|
+
contextWindowTokens: budget.contextWindowTokens,
|
|
3701
|
+
...(signal ? { signal } : {}),
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
catch (error) {
|
|
3705
|
+
throw classifyCompactionError(error, {
|
|
3706
|
+
trigger: 'auto',
|
|
3707
|
+
phase: 'generation',
|
|
3708
|
+
durableState: 'not_committed',
|
|
3709
|
+
recoveryDisposition: 'none',
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
let durationMs;
|
|
3713
|
+
let durationWithoutRetriesMs;
|
|
3714
|
+
try {
|
|
3715
|
+
const durations = requireCompactionDurations(compacted);
|
|
3716
|
+
durationMs = durations.durationMs;
|
|
3717
|
+
durationWithoutRetriesMs = durations.durationWithoutRetriesMs;
|
|
3718
|
+
}
|
|
3719
|
+
catch (error) {
|
|
3720
|
+
throw classifyCompactionError(error, {
|
|
3721
|
+
trigger: 'auto',
|
|
3722
|
+
phase: 'validation',
|
|
3723
|
+
durableState: 'not_committed',
|
|
3724
|
+
recoveryDisposition: 'none',
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3551
3727
|
if (signal?.aborted)
|
|
3552
3728
|
throw new AgentRunCancelledError();
|
|
3553
|
-
const postTokens = estimateModelRequestTokens([
|
|
3729
|
+
const postTokens = await autoValidation(() => estimateModelRequestTokens([
|
|
3554
3730
|
{ role: 'user', content: compacted.summary },
|
|
3555
3731
|
...preservedMessages,
|
|
3556
|
-
]);
|
|
3732
|
+
]));
|
|
3557
3733
|
const compactModel = compacted.model !== undefined && compacted.model.trim() !== ''
|
|
3558
3734
|
? compacted.model
|
|
3559
3735
|
: provider.model;
|
|
3560
|
-
|
|
3736
|
+
if (compacted.summary.trim().length === 0)
|
|
3737
|
+
throw classifyCompactionError(new Error('Native compact summary must not be blank'), {
|
|
3738
|
+
trigger: 'auto',
|
|
3739
|
+
phase: 'validation',
|
|
3740
|
+
durableState: 'not_committed',
|
|
3741
|
+
recoveryDisposition: 'none',
|
|
3742
|
+
});
|
|
3743
|
+
const preparedCompaction = await autoValidation(() => turnAccounting.prepareCompaction({
|
|
3561
3744
|
usage: compacted.usage,
|
|
3562
|
-
...(
|
|
3745
|
+
...(!hasNonZeroUsage(compacted.usage) ||
|
|
3746
|
+
compactModel === undefined
|
|
3747
|
+
? {}
|
|
3748
|
+
: { model: compactModel }),
|
|
3563
3749
|
durationApiMs: durationMs,
|
|
3564
3750
|
durationApiWithoutRetriesMs: durationWithoutRetriesMs,
|
|
3751
|
+
}));
|
|
3752
|
+
const preparedTransaction = await compactionAccounting.prepare({
|
|
3753
|
+
trigger: 'auto',
|
|
3754
|
+
metric: {
|
|
3755
|
+
usage: compacted.usage,
|
|
3756
|
+
...(!hasNonZeroUsage(compacted.usage) ||
|
|
3757
|
+
compactModel === undefined
|
|
3758
|
+
? {}
|
|
3759
|
+
: { model: compactModel }),
|
|
3760
|
+
durationApiMs: durationMs,
|
|
3761
|
+
durationApiWithoutRetriesMs: durationWithoutRetriesMs,
|
|
3762
|
+
},
|
|
3565
3763
|
});
|
|
3566
3764
|
const summaryMessage = {
|
|
3567
3765
|
role: 'user',
|
|
@@ -3597,54 +3795,138 @@ export class ClaudeSessionService {
|
|
|
3597
3795
|
commit: async () => {
|
|
3598
3796
|
if (signal?.aborted)
|
|
3599
3797
|
throw new AgentRunCancelledError();
|
|
3600
|
-
const committed = await replacement.commit(() =>
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3798
|
+
const committed = await replacement.commit(async () => {
|
|
3799
|
+
try {
|
|
3800
|
+
return await persistence.commit({
|
|
3801
|
+
kind: 'compaction',
|
|
3802
|
+
input: {
|
|
3803
|
+
summary: compacted.summary,
|
|
3804
|
+
trigger: 'auto',
|
|
3805
|
+
preTokens,
|
|
3806
|
+
postTokens,
|
|
3807
|
+
durationMs,
|
|
3808
|
+
boundaryId: preparedTransaction.boundaryId,
|
|
3809
|
+
summaryId: preparedTransaction.summaryId,
|
|
3810
|
+
preservedMessages: replayMessages,
|
|
3811
|
+
...(compactionLogicalParentId === undefined
|
|
3812
|
+
? {}
|
|
3813
|
+
: { logicalParentId: compactionLogicalParentId }),
|
|
3814
|
+
...(memorySelection
|
|
3815
|
+
? {
|
|
3816
|
+
direction: 'from',
|
|
3817
|
+
messagesSummarized: compactableMessages.length,
|
|
3818
|
+
preservePrefix: false,
|
|
3819
|
+
}
|
|
3820
|
+
: {}),
|
|
3821
|
+
},
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3824
|
+
catch (error) {
|
|
3825
|
+
throw classifyCompactionError(error, {
|
|
3826
|
+
trigger: 'auto',
|
|
3827
|
+
phase: 'transcript_commit',
|
|
3828
|
+
durableState: 'indeterminate',
|
|
3829
|
+
recoveryDisposition: 'reconcile',
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3621
3833
|
const ids = committed.value;
|
|
3622
3834
|
if (ids.kind !== 'compaction')
|
|
3623
|
-
throw new Error('Turn persistence returned an invalid compaction receipt')
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3835
|
+
throw classifyCompactionError(new Error('Turn persistence returned an invalid compaction receipt'), {
|
|
3836
|
+
trigger: 'auto',
|
|
3837
|
+
phase: 'transcript_commit',
|
|
3838
|
+
durableState: 'indeterminate',
|
|
3839
|
+
recoveryDisposition: 'reconcile',
|
|
3840
|
+
});
|
|
3841
|
+
try {
|
|
3842
|
+
preparedCompaction.commit();
|
|
3843
|
+
await preparedTransaction.commit({
|
|
3844
|
+
kind: 'compaction',
|
|
3845
|
+
boundaryId: ids.boundaryId,
|
|
3846
|
+
summaryId: ids.summaryId,
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
catch (error) {
|
|
3850
|
+
throw classifyCompactionError(error, {
|
|
3851
|
+
trigger: 'auto',
|
|
3852
|
+
phase: 'accounting_commit',
|
|
3853
|
+
durableState: 'committed',
|
|
3854
|
+
recoveryDisposition: 'reconcile',
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
try {
|
|
3858
|
+
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
|
|
3859
|
+
}
|
|
3860
|
+
catch (error) {
|
|
3861
|
+
throw classifyCompactionError(error, {
|
|
3862
|
+
trigger: 'auto',
|
|
3863
|
+
phase: 'post_commit',
|
|
3864
|
+
durableState: 'committed',
|
|
3865
|
+
recoveryDisposition: 'reconcile',
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
try {
|
|
3869
|
+
if (this.options.hooks) {
|
|
3870
|
+
const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
|
|
3871
|
+
if (outcome)
|
|
3872
|
+
await recordHookOutcome(outcome);
|
|
3873
|
+
}
|
|
3874
|
+
this.options.contextAssembler?.invalidate?.({
|
|
3875
|
+
lifecycleId: sessionId,
|
|
3876
|
+
reason: 'compact',
|
|
3877
|
+
});
|
|
3878
|
+
await refreshRuntimeContext?.();
|
|
3879
|
+
this.options.eventSink?.({
|
|
3880
|
+
type: 'compact-boundary',
|
|
3881
|
+
trigger: 'auto',
|
|
3882
|
+
preTokens,
|
|
3883
|
+
uuid: ids.boundaryId,
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3886
|
+
catch (error) {
|
|
3887
|
+
throw classifyCompactionError(error, {
|
|
3888
|
+
trigger: 'auto',
|
|
3889
|
+
phase: 'post_commit',
|
|
3890
|
+
durableState: 'committed',
|
|
3891
|
+
recoveryDisposition: 'reconcile',
|
|
3892
|
+
});
|
|
3629
3893
|
}
|
|
3630
|
-
this.options.contextAssembler?.invalidate?.({
|
|
3631
|
-
lifecycleId: sessionId,
|
|
3632
|
-
reason: 'compact',
|
|
3633
|
-
});
|
|
3634
|
-
await refreshRuntimeContext?.();
|
|
3635
|
-
this.options.eventSink?.({
|
|
3636
|
-
type: 'compact-boundary',
|
|
3637
|
-
trigger: 'auto',
|
|
3638
|
-
preTokens,
|
|
3639
|
-
uuid: ids.boundaryId,
|
|
3640
|
-
});
|
|
3641
|
-
preparedCompaction.commit();
|
|
3642
3894
|
},
|
|
3643
3895
|
};
|
|
3644
3896
|
},
|
|
3645
3897
|
});
|
|
3898
|
+
const autoValidation = async (operation) => {
|
|
3899
|
+
try {
|
|
3900
|
+
return await operation();
|
|
3901
|
+
}
|
|
3902
|
+
catch (error) {
|
|
3903
|
+
if (error instanceof StaleContextGenerationError)
|
|
3904
|
+
throw error;
|
|
3905
|
+
throw classifyCompactionError(error, {
|
|
3906
|
+
trigger: 'auto',
|
|
3907
|
+
phase: 'validation',
|
|
3908
|
+
durableState: 'not_committed',
|
|
3909
|
+
recoveryDisposition: 'none',
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
};
|
|
3913
|
+
const prepareContext = async (port) => {
|
|
3914
|
+
try {
|
|
3915
|
+
await contextEngine.prepare(port, signal);
|
|
3916
|
+
}
|
|
3917
|
+
catch (error) {
|
|
3918
|
+
if (error instanceof StaleContextGenerationError)
|
|
3919
|
+
throw error;
|
|
3920
|
+
throw classifyCompactionError(error, {
|
|
3921
|
+
trigger: 'auto',
|
|
3922
|
+
phase: 'validation',
|
|
3923
|
+
durableState: 'not_committed',
|
|
3924
|
+
recoveryDisposition: 'none',
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
};
|
|
3646
3928
|
if (shellCommand === undefined) {
|
|
3647
|
-
await
|
|
3929
|
+
await prepareContext(contextTransitionPort(pendingUserMessages));
|
|
3648
3930
|
}
|
|
3649
3931
|
for (const [index, message] of expandedMessages.entries()) {
|
|
3650
3932
|
if (shellCommand !== undefined)
|
|
@@ -3826,7 +4108,7 @@ export class ClaudeSessionService {
|
|
|
3826
4108
|
};
|
|
3827
4109
|
}
|
|
3828
4110
|
if (shellCommand === undefined && budget) {
|
|
3829
|
-
await
|
|
4111
|
+
await prepareContext(contextTransitionPort([], currentTurnUserMessages ?? []));
|
|
3830
4112
|
const projection = contextPreparation.project({
|
|
3831
4113
|
includeMemory: false,
|
|
3832
4114
|
});
|
|
@@ -3866,7 +4148,7 @@ export class ClaudeSessionService {
|
|
|
3866
4148
|
}
|
|
3867
4149
|
await refreshRuntimeContext?.();
|
|
3868
4150
|
if (shellCommand === undefined) {
|
|
3869
|
-
await
|
|
4151
|
+
await prepareContext(contextTransitionPort([], currentTurnUserMessages ?? []));
|
|
3870
4152
|
}
|
|
3871
4153
|
const projection = contextPreparation.project();
|
|
3872
4154
|
runtimeRequest.stableSystemMessageCount =
|
|
@@ -4002,7 +4284,20 @@ export class ClaudeSessionService {
|
|
|
4002
4284
|
catch (error) {
|
|
4003
4285
|
if (!budget || !isPromptTooLongError(error))
|
|
4004
4286
|
throw error;
|
|
4005
|
-
|
|
4287
|
+
let recovery;
|
|
4288
|
+
try {
|
|
4289
|
+
recovery = await contextEngine.recover(error, contextTransitionPort([], currentTurnUserMessages ?? []), signal);
|
|
4290
|
+
}
|
|
4291
|
+
catch (recoveryError) {
|
|
4292
|
+
if (recoveryError instanceof StaleContextGenerationError)
|
|
4293
|
+
throw recoveryError;
|
|
4294
|
+
throw classifyCompactionError(recoveryError, {
|
|
4295
|
+
trigger: 'auto',
|
|
4296
|
+
phase: 'validation',
|
|
4297
|
+
durableState: 'not_committed',
|
|
4298
|
+
recoveryDisposition: 'none',
|
|
4299
|
+
});
|
|
4300
|
+
}
|
|
4006
4301
|
if (recovery.kind !== 'retry') {
|
|
4007
4302
|
surfaceExhaustedRecovery(error);
|
|
4008
4303
|
throw error;
|
|
@@ -4141,6 +4436,8 @@ export class ClaudeSessionService {
|
|
|
4141
4436
|
sessionId,
|
|
4142
4437
|
store: nativeStore,
|
|
4143
4438
|
});
|
|
4439
|
+
const compactionAccounting = this.compactionAccounting(sessionId, nativeStore);
|
|
4440
|
+
await compactionAccounting.recover();
|
|
4144
4441
|
const activationKind = requireExisting
|
|
4145
4442
|
? resumeSessionAt === undefined
|
|
4146
4443
|
? { kind: 'resume' }
|
|
@@ -4148,7 +4445,7 @@ export class ClaudeSessionService {
|
|
|
4148
4445
|
: { kind: 'start' };
|
|
4149
4446
|
result = await nativeTranscript.withLease(activationKind, async (nativeLease) => {
|
|
4150
4447
|
const persistence = new TurnPersistence({ native: nativeLease });
|
|
4151
|
-
return runUnderLease(persistence);
|
|
4448
|
+
return runUnderLease(persistence, compactionAccounting);
|
|
4152
4449
|
});
|
|
4153
4450
|
}
|
|
4154
4451
|
return result;
|
|
@@ -4612,6 +4909,38 @@ export class ClaudeSessionService {
|
|
|
4612
4909
|
}
|
|
4613
4910
|
return this.store(sessionId);
|
|
4614
4911
|
}
|
|
4912
|
+
compactionAccounting(sessionId, nativeStore) {
|
|
4913
|
+
const existing = this.compactionAccountings.get(sessionId);
|
|
4914
|
+
if (existing)
|
|
4915
|
+
return existing;
|
|
4916
|
+
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
4917
|
+
if (!tracker)
|
|
4918
|
+
throw new Error(`Session cost tracker is not active for session ${sessionId}`);
|
|
4919
|
+
const sessionPaths = this.paths(sessionId);
|
|
4920
|
+
const accounting = new CompactionAccounting({
|
|
4921
|
+
sessionId,
|
|
4922
|
+
tracker,
|
|
4923
|
+
...(this.options.pricing ? { pricing: this.options.pricing } : {}),
|
|
4924
|
+
...(this.options.sessionPersistence === false
|
|
4925
|
+
? {}
|
|
4926
|
+
: {
|
|
4927
|
+
receiptStore: new NativeCompactionReceiptFileStore({
|
|
4928
|
+
sidecarRoot: sessionPaths.praxisRoot,
|
|
4929
|
+
}),
|
|
4930
|
+
}),
|
|
4931
|
+
...(this.options.costStateStore
|
|
4932
|
+
? { costStateStore: this.options.costStateStore }
|
|
4933
|
+
: {}),
|
|
4934
|
+
readTranscript: async () => {
|
|
4935
|
+
const loaded = await nativeStore.withLease((lease) => lease.load());
|
|
4936
|
+
if (loaded.status === 'conflict')
|
|
4937
|
+
throw new Error(`native transcript lease conflict: ${loaded.reason}`);
|
|
4938
|
+
return loaded.value.records.map((record) => record.event);
|
|
4939
|
+
},
|
|
4940
|
+
});
|
|
4941
|
+
this.compactionAccountings.set(sessionId, accounting);
|
|
4942
|
+
return accounting;
|
|
4943
|
+
}
|
|
4615
4944
|
assertSessionPersistence() {
|
|
4616
4945
|
if (this.options.sessionPersistence === false) {
|
|
4617
4946
|
throw new Error('Session persistence is disabled');
|