praxis-agent 0.66.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/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 -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/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TranscriptEvent } from '../core/transcript-event.js';
|
|
2
|
+
import type { ModelPricingRegistry } from '../core/usage.js';
|
|
3
|
+
import type { ClaudeCostStateStore } from '../persistence/claude-cost-state-store.js';
|
|
4
|
+
import type { CompactionReceiptStore } from '../persistence/native-compaction-receipt-store.js';
|
|
5
|
+
import { ClaudeSessionCostTracker } from './session-cost-tracker.js';
|
|
6
|
+
import type { TurnCompactionMetric } from './turn-accounting.js';
|
|
7
|
+
import { type CompactionTrigger } from './compaction-errors.js';
|
|
8
|
+
export interface CompactionAccountingOptions {
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly tracker: ClaudeSessionCostTracker;
|
|
11
|
+
readonly pricing?: ModelPricingRegistry;
|
|
12
|
+
readonly receiptStore?: CompactionReceiptStore;
|
|
13
|
+
readonly costStateStore?: Pick<ClaudeCostStateStore, 'save'>;
|
|
14
|
+
readonly readTranscript?: () => Promise<readonly TranscriptEvent[]>;
|
|
15
|
+
readonly createId?: () => string;
|
|
16
|
+
}
|
|
17
|
+
export interface PreparedCompactionTransaction {
|
|
18
|
+
readonly receiptId: string;
|
|
19
|
+
readonly boundaryId: string;
|
|
20
|
+
readonly summaryId: string;
|
|
21
|
+
readonly commit: (receipt: {
|
|
22
|
+
readonly kind: 'compaction';
|
|
23
|
+
readonly boundaryId: string;
|
|
24
|
+
readonly summaryId: string;
|
|
25
|
+
}) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare class CompactionAccounting {
|
|
28
|
+
private readonly sessionId;
|
|
29
|
+
private readonly tracker;
|
|
30
|
+
private readonly pricing;
|
|
31
|
+
private readonly receiptStore;
|
|
32
|
+
private readonly costStateStore;
|
|
33
|
+
private readonly readTranscript;
|
|
34
|
+
private readonly createId;
|
|
35
|
+
private recovered;
|
|
36
|
+
private recoveryPromise;
|
|
37
|
+
private recoveredReceipts;
|
|
38
|
+
private appliedReceipts;
|
|
39
|
+
constructor(options: CompactionAccountingOptions);
|
|
40
|
+
recover(): Promise<void>;
|
|
41
|
+
private recoverOnce;
|
|
42
|
+
prepare(input: {
|
|
43
|
+
readonly trigger: CompactionTrigger;
|
|
44
|
+
readonly metric: TurnCompactionMetric;
|
|
45
|
+
}): Promise<PreparedCompactionTransaction>;
|
|
46
|
+
private recoveryError;
|
|
47
|
+
private error;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=compaction-accounting.d.ts.map
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { usageCostUsd } from '../core/usage.js';
|
|
3
|
+
import { ClaudeSessionCostTracker } from './session-cost-tracker.js';
|
|
4
|
+
import { CompactionTransactionError, } from './compaction-errors.js';
|
|
5
|
+
function clone(value) {
|
|
6
|
+
return structuredClone(value);
|
|
7
|
+
}
|
|
8
|
+
function validTransactionId(value) {
|
|
9
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/u.test(value);
|
|
10
|
+
}
|
|
11
|
+
function fingerprint(snapshot) {
|
|
12
|
+
const modelUsage = Object.fromEntries(Object.entries(snapshot.modelUsage).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
13
|
+
const stable = {
|
|
14
|
+
sessionId: snapshot.sessionId,
|
|
15
|
+
totalCostUsd: snapshot.totalCostUsd,
|
|
16
|
+
apiDurationMs: snapshot.apiDurationMs,
|
|
17
|
+
apiDurationWithoutRetriesMs: snapshot.apiDurationWithoutRetriesMs,
|
|
18
|
+
toolDurationMs: snapshot.toolDurationMs,
|
|
19
|
+
linesAdded: snapshot.linesAdded,
|
|
20
|
+
linesRemoved: snapshot.linesRemoved,
|
|
21
|
+
modelUsage,
|
|
22
|
+
hasUnknownModelCost: snapshot.hasUnknownModelCost,
|
|
23
|
+
};
|
|
24
|
+
return createHash('sha256').update(JSON.stringify(stable)).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
function meaningful(usage) {
|
|
27
|
+
return [
|
|
28
|
+
usage.inputTokens,
|
|
29
|
+
usage.outputTokens,
|
|
30
|
+
usage.cacheReadInputTokens,
|
|
31
|
+
usage.cacheCreationInputTokens,
|
|
32
|
+
usage.cacheCreationInputTokens1h,
|
|
33
|
+
usage.webSearchRequests,
|
|
34
|
+
].some((value) => typeof value === 'number' && value > 0);
|
|
35
|
+
}
|
|
36
|
+
function applyMetric(tracker, metric, costUsd) {
|
|
37
|
+
if (meaningful(metric.usage)) {
|
|
38
|
+
if (!metric.model?.trim())
|
|
39
|
+
throw new TypeError('Auto compact usage requires a nonblank model identity');
|
|
40
|
+
tracker.recordTurn({
|
|
41
|
+
model: metric.model,
|
|
42
|
+
usage: metric.usage,
|
|
43
|
+
...(costUsd === null ? {} : { costUsd }),
|
|
44
|
+
...(metric.usage.webSearchRequests === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { webSearchRequests: metric.usage.webSearchRequests }),
|
|
47
|
+
apiDurationMs: metric.durationApiMs,
|
|
48
|
+
apiDurationWithoutRetriesMs: metric.durationApiWithoutRetriesMs,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
tracker.recordDurations({
|
|
53
|
+
apiDurationMs: metric.durationApiMs,
|
|
54
|
+
apiDurationWithoutRetriesMs: metric.durationApiWithoutRetriesMs,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function compactPair(events, receipt) {
|
|
58
|
+
const boundaries = events.filter((event) => event.id === receipt.boundaryId);
|
|
59
|
+
const summaries = events.filter((event) => event.id === receipt.summaryId);
|
|
60
|
+
if (boundaries.length === 0 && summaries.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
if (boundaries.length !== 1 || summaries.length !== 1)
|
|
63
|
+
throw new Error('Compaction receipt has partial or duplicate Transcript evidence');
|
|
64
|
+
const matched = events.some((event, index) => {
|
|
65
|
+
const summary = events[index + 1];
|
|
66
|
+
return (event.kind === 'context-boundary' &&
|
|
67
|
+
event.id === receipt.boundaryId &&
|
|
68
|
+
event.parentId === null &&
|
|
69
|
+
event.sessionId === receipt.sessionId &&
|
|
70
|
+
event.trigger === receipt.trigger &&
|
|
71
|
+
event.durationMs === receipt.metric.durationApiMs &&
|
|
72
|
+
summary?.kind === 'context-summary' &&
|
|
73
|
+
summary.id === receipt.summaryId &&
|
|
74
|
+
summary.parentId === receipt.boundaryId &&
|
|
75
|
+
summary.sessionId === receipt.sessionId);
|
|
76
|
+
});
|
|
77
|
+
if (!matched)
|
|
78
|
+
throw new Error('Compaction receipt Transcript evidence does not match boundary and summary');
|
|
79
|
+
return events.findIndex((event, index) => {
|
|
80
|
+
const summary = events[index + 1];
|
|
81
|
+
return event.id === receipt.boundaryId && summary?.id === receipt.summaryId;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export class CompactionAccounting {
|
|
85
|
+
sessionId;
|
|
86
|
+
tracker;
|
|
87
|
+
pricing;
|
|
88
|
+
receiptStore;
|
|
89
|
+
costStateStore;
|
|
90
|
+
readTranscript;
|
|
91
|
+
createId;
|
|
92
|
+
recovered = false;
|
|
93
|
+
recoveryPromise;
|
|
94
|
+
recoveredReceipts = new Set();
|
|
95
|
+
appliedReceipts = new Set();
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.sessionId = options.sessionId;
|
|
98
|
+
this.tracker = options.tracker;
|
|
99
|
+
this.pricing = options.pricing;
|
|
100
|
+
this.receiptStore = options.receiptStore;
|
|
101
|
+
this.costStateStore = options.costStateStore;
|
|
102
|
+
this.readTranscript = options.readTranscript;
|
|
103
|
+
this.createId = options.createId ?? randomUUID;
|
|
104
|
+
}
|
|
105
|
+
async recover() {
|
|
106
|
+
if (this.recovered)
|
|
107
|
+
return;
|
|
108
|
+
if (this.recoveryPromise)
|
|
109
|
+
return this.recoveryPromise;
|
|
110
|
+
this.recoveryPromise = this.recoverOnce().catch((cause) => {
|
|
111
|
+
if (cause instanceof CompactionTransactionError)
|
|
112
|
+
throw cause;
|
|
113
|
+
throw this.error('auto', 'recovery', 'indeterminate', 'blocked', cause);
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
await this.recoveryPromise;
|
|
117
|
+
this.recovered = true;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
this.recoveryPromise = undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async recoverOnce() {
|
|
124
|
+
if (!this.receiptStore)
|
|
125
|
+
return;
|
|
126
|
+
if (!this.readTranscript)
|
|
127
|
+
throw this.error('auto', 'recovery', 'indeterminate', 'blocked', new Error('Compaction recovery requires the complete native Transcript'));
|
|
128
|
+
const events = await this.readTranscript();
|
|
129
|
+
const rows = await this.receiptStore.list(this.sessionId);
|
|
130
|
+
const committed = [];
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
const receipt = row.receipt;
|
|
133
|
+
if (receipt.sessionId !== this.sessionId)
|
|
134
|
+
throw this.recoveryError(receipt, new Error('Compaction receipt session does not match recovery'));
|
|
135
|
+
let index;
|
|
136
|
+
try {
|
|
137
|
+
index = compactPair(events, receipt);
|
|
138
|
+
}
|
|
139
|
+
catch (cause) {
|
|
140
|
+
throw this.recoveryError(receipt, cause);
|
|
141
|
+
}
|
|
142
|
+
if (index === null)
|
|
143
|
+
continue;
|
|
144
|
+
committed.push({ row, index });
|
|
145
|
+
}
|
|
146
|
+
committed.sort((a, b) => a.index - b.index);
|
|
147
|
+
if (!this.costStateStore) {
|
|
148
|
+
const preflight = new ClaudeSessionCostTracker({
|
|
149
|
+
sessionId: this.sessionId,
|
|
150
|
+
restored: this.tracker.snapshot(),
|
|
151
|
+
});
|
|
152
|
+
for (const { row } of committed) {
|
|
153
|
+
if (this.appliedReceipts.has(row.receipt.receiptId))
|
|
154
|
+
continue;
|
|
155
|
+
try {
|
|
156
|
+
applyMetric(preflight, row.receipt.metric, row.receipt.costUsd);
|
|
157
|
+
}
|
|
158
|
+
catch (cause) {
|
|
159
|
+
throw this.recoveryError(row.receipt, cause);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const { row } of committed) {
|
|
163
|
+
const receipt = row.receipt;
|
|
164
|
+
if (this.recoveredReceipts.has(receipt.receiptId))
|
|
165
|
+
continue;
|
|
166
|
+
if (!this.appliedReceipts.has(receipt.receiptId)) {
|
|
167
|
+
const check = new ClaudeSessionCostTracker({
|
|
168
|
+
sessionId: this.sessionId,
|
|
169
|
+
restored: this.tracker.snapshot(),
|
|
170
|
+
});
|
|
171
|
+
const before = fingerprint(this.tracker.snapshot());
|
|
172
|
+
try {
|
|
173
|
+
applyMetric(check, receipt.metric, receipt.costUsd);
|
|
174
|
+
}
|
|
175
|
+
catch (cause) {
|
|
176
|
+
throw this.recoveryError(receipt, cause);
|
|
177
|
+
}
|
|
178
|
+
if (before === receipt.before &&
|
|
179
|
+
fingerprint(check.snapshot()) !== receipt.after)
|
|
180
|
+
throw this.recoveryError(receipt, new Error('Compaction receipt after fingerprint mismatch'));
|
|
181
|
+
try {
|
|
182
|
+
applyMetric(this.tracker, receipt.metric, receipt.costUsd);
|
|
183
|
+
}
|
|
184
|
+
catch (cause) {
|
|
185
|
+
throw this.recoveryError(receipt, cause);
|
|
186
|
+
}
|
|
187
|
+
this.appliedReceipts.add(receipt.receiptId);
|
|
188
|
+
}
|
|
189
|
+
if (!row.acknowledged) {
|
|
190
|
+
try {
|
|
191
|
+
await this.receiptStore.acknowledge(this.sessionId, receipt.receiptId);
|
|
192
|
+
}
|
|
193
|
+
catch (cause) {
|
|
194
|
+
throw this.recoveryError(receipt, cause, 'reconcile');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.recoveredReceipts.add(receipt.receiptId);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const pending = new Map(committed
|
|
202
|
+
.filter(({ row }) => !row.acknowledged)
|
|
203
|
+
.map(({ row }) => [row.receipt.receiptId, row]));
|
|
204
|
+
while (pending.size > 0) {
|
|
205
|
+
const candidates = [...pending.values()].filter((row) => {
|
|
206
|
+
const current = fingerprint(this.tracker.snapshot());
|
|
207
|
+
return current === row.receipt.before || current === row.receipt.after;
|
|
208
|
+
});
|
|
209
|
+
const candidate = candidates[0];
|
|
210
|
+
const fallback = pending.values().next().value;
|
|
211
|
+
if (candidates.length !== 1 || !candidate) {
|
|
212
|
+
if (!fallback)
|
|
213
|
+
throw this.error('auto', 'recovery', 'indeterminate', 'blocked', new Error('Compaction receipt chain is empty'));
|
|
214
|
+
throw this.recoveryError(fallback.receipt, new Error(candidates.length > 1
|
|
215
|
+
? 'Ambiguous compaction receipt chain'
|
|
216
|
+
: 'No compaction receipt chain candidate'));
|
|
217
|
+
}
|
|
218
|
+
const receipt = candidate.receipt;
|
|
219
|
+
const now = fingerprint(this.tracker.snapshot());
|
|
220
|
+
if (now === receipt.before) {
|
|
221
|
+
const check = new ClaudeSessionCostTracker({
|
|
222
|
+
sessionId: this.sessionId,
|
|
223
|
+
restored: this.tracker.snapshot(),
|
|
224
|
+
});
|
|
225
|
+
try {
|
|
226
|
+
applyMetric(check, receipt.metric, receipt.costUsd);
|
|
227
|
+
}
|
|
228
|
+
catch (cause) {
|
|
229
|
+
throw this.recoveryError(receipt, cause);
|
|
230
|
+
}
|
|
231
|
+
if (fingerprint(check.snapshot()) !== receipt.after)
|
|
232
|
+
throw this.recoveryError(receipt, new Error('Compaction receipt after fingerprint mismatch'));
|
|
233
|
+
try {
|
|
234
|
+
applyMetric(this.tracker, receipt.metric, receipt.costUsd);
|
|
235
|
+
}
|
|
236
|
+
catch (cause) {
|
|
237
|
+
throw this.recoveryError(receipt, cause);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
await this.costStateStore.save(this.tracker.snapshot());
|
|
242
|
+
await this.receiptStore.acknowledge(this.sessionId, receipt.receiptId);
|
|
243
|
+
}
|
|
244
|
+
catch (cause) {
|
|
245
|
+
throw this.recoveryError(receipt, cause, 'reconcile');
|
|
246
|
+
}
|
|
247
|
+
this.recoveredReceipts.add(receipt.receiptId);
|
|
248
|
+
pending.delete(receipt.receiptId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async prepare(input) {
|
|
252
|
+
const metric = clone(input.metric);
|
|
253
|
+
const preflight = new ClaudeSessionCostTracker({
|
|
254
|
+
sessionId: this.sessionId,
|
|
255
|
+
restored: this.tracker.snapshot(),
|
|
256
|
+
});
|
|
257
|
+
let fixedCost;
|
|
258
|
+
try {
|
|
259
|
+
const resolved = metric.model
|
|
260
|
+
? this.pricing?.resolve(metric.model)
|
|
261
|
+
: undefined;
|
|
262
|
+
fixedCost = resolved ? usageCostUsd(metric.usage, resolved) : null;
|
|
263
|
+
applyMetric(preflight, metric, fixedCost);
|
|
264
|
+
}
|
|
265
|
+
catch (cause) {
|
|
266
|
+
throw this.error(input.trigger, 'validation', 'not_committed', 'none', cause);
|
|
267
|
+
}
|
|
268
|
+
const receiptId = this.createId();
|
|
269
|
+
const boundaryId = this.createId();
|
|
270
|
+
const summaryId = this.createId();
|
|
271
|
+
if (!validTransactionId(receiptId) ||
|
|
272
|
+
!validTransactionId(boundaryId) ||
|
|
273
|
+
!validTransactionId(summaryId) ||
|
|
274
|
+
new Set([receiptId, boundaryId, summaryId]).size !== 3)
|
|
275
|
+
throw this.error(input.trigger, 'validation', 'not_committed', 'blocked', new Error('Compaction transaction IDs must be safe, nonblank, and distinct'));
|
|
276
|
+
const before = fingerprint(this.tracker.snapshot());
|
|
277
|
+
const after = fingerprint(preflight.snapshot());
|
|
278
|
+
const receipt = {
|
|
279
|
+
version: 1,
|
|
280
|
+
receiptId,
|
|
281
|
+
sessionId: this.sessionId,
|
|
282
|
+
boundaryId,
|
|
283
|
+
summaryId,
|
|
284
|
+
trigger: input.trigger,
|
|
285
|
+
metric,
|
|
286
|
+
costUsd: fixedCost,
|
|
287
|
+
before,
|
|
288
|
+
after,
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
await this.receiptStore?.prepare(receipt);
|
|
292
|
+
}
|
|
293
|
+
catch (cause) {
|
|
294
|
+
throw this.error(input.trigger, 'receipt_prepare', 'not_committed', 'retry', cause);
|
|
295
|
+
}
|
|
296
|
+
if (this.receiptStore)
|
|
297
|
+
this.recovered = false;
|
|
298
|
+
let used = false;
|
|
299
|
+
return {
|
|
300
|
+
receiptId,
|
|
301
|
+
boundaryId,
|
|
302
|
+
summaryId,
|
|
303
|
+
commit: async (appendReceipt) => {
|
|
304
|
+
if (used)
|
|
305
|
+
throw this.error(input.trigger, 'accounting_commit', 'committed', 'reconcile', new Error('Prepared compaction transaction was already committed'));
|
|
306
|
+
if (typeof appendReceipt !== 'object' ||
|
|
307
|
+
appendReceipt === null ||
|
|
308
|
+
appendReceipt.kind !== 'compaction' ||
|
|
309
|
+
appendReceipt.boundaryId !== boundaryId ||
|
|
310
|
+
appendReceipt.summaryId !== summaryId)
|
|
311
|
+
throw this.error(input.trigger, 'transcript_commit', 'indeterminate', 'blocked', new Error('Transcript compaction receipt does not match prepared IDs'));
|
|
312
|
+
used = true;
|
|
313
|
+
try {
|
|
314
|
+
const check = new ClaudeSessionCostTracker({
|
|
315
|
+
sessionId: this.sessionId,
|
|
316
|
+
restored: this.tracker.snapshot(),
|
|
317
|
+
});
|
|
318
|
+
applyMetric(check, metric, fixedCost);
|
|
319
|
+
if (fingerprint(check.snapshot()) !== after)
|
|
320
|
+
throw this.error(input.trigger, 'accounting_commit', 'indeterminate', 'blocked', new Error('Compaction receipt after fingerprint mismatch'));
|
|
321
|
+
applyMetric(this.tracker, metric, fixedCost);
|
|
322
|
+
this.appliedReceipts.add(receiptId);
|
|
323
|
+
}
|
|
324
|
+
catch (cause) {
|
|
325
|
+
this.recovered = false;
|
|
326
|
+
throw this.error(input.trigger, 'accounting_commit', 'indeterminate', 'blocked', cause);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
if (this.costStateStore)
|
|
330
|
+
await this.costStateStore.save(this.tracker.snapshot());
|
|
331
|
+
if (this.receiptStore)
|
|
332
|
+
await this.receiptStore.acknowledge(this.sessionId, receiptId);
|
|
333
|
+
}
|
|
334
|
+
catch (cause) {
|
|
335
|
+
this.recovered = false;
|
|
336
|
+
throw this.error(input.trigger, 'accounting_commit', 'committed', 'reconcile', cause);
|
|
337
|
+
}
|
|
338
|
+
this.recoveredReceipts.add(receiptId);
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
recoveryError(receipt, cause, disposition = 'blocked') {
|
|
343
|
+
return this.error(receipt.trigger, 'recovery', 'indeterminate', disposition, cause);
|
|
344
|
+
}
|
|
345
|
+
error(trigger, phase, durableState, recoveryDisposition, cause) {
|
|
346
|
+
return new CompactionTransactionError(`Compaction ${phase} failed: ${cause instanceof Error ? cause.message : String(cause)}`, { trigger, phase, durableState, recoveryDisposition }, cause);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
//# sourceMappingURL=compaction-accounting.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CompactionTrigger = 'auto' | 'manual';
|
|
2
|
+
export type CompactionPhase = 'validation' | 'generation' | 'receipt_prepare' | 'transcript_commit' | 'accounting_commit' | 'post_commit' | 'recovery';
|
|
3
|
+
export type CompactionDurableState = 'not_committed' | 'committed' | 'indeterminate';
|
|
4
|
+
export type CompactionRecoveryDisposition = 'none' | 'retry' | 'reconcile' | 'blocked';
|
|
5
|
+
export interface CompactionTransactionErrorMetadata {
|
|
6
|
+
readonly trigger: CompactionTrigger;
|
|
7
|
+
readonly phase: CompactionPhase;
|
|
8
|
+
readonly durableState: CompactionDurableState;
|
|
9
|
+
readonly recoveryDisposition: CompactionRecoveryDisposition;
|
|
10
|
+
}
|
|
11
|
+
/** A classified compaction failure. The original failure remains available as
|
|
12
|
+
* `cause`, preserving provider error identity and retry/status metadata. */
|
|
13
|
+
export declare class CompactionTransactionError extends Error {
|
|
14
|
+
readonly name = "CompactionTransactionError";
|
|
15
|
+
readonly metadata: Readonly<CompactionTransactionErrorMetadata>;
|
|
16
|
+
readonly cause: unknown;
|
|
17
|
+
constructor(message: string, metadata: CompactionTransactionErrorMetadata, cause?: unknown);
|
|
18
|
+
}
|
|
19
|
+
export declare function isCompactionTransactionError(error: unknown): error is CompactionTransactionError;
|
|
20
|
+
export declare function classifyCompactionError(error: unknown, metadata: CompactionTransactionErrorMetadata): CompactionTransactionError;
|
|
21
|
+
//# sourceMappingURL=compaction-errors.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AgentRunCancelledError } from '../core/runtime.js';
|
|
2
|
+
/** A classified compaction failure. The original failure remains available as
|
|
3
|
+
* `cause`, preserving provider error identity and retry/status metadata. */
|
|
4
|
+
export class CompactionTransactionError extends Error {
|
|
5
|
+
name = 'CompactionTransactionError';
|
|
6
|
+
metadata;
|
|
7
|
+
cause;
|
|
8
|
+
constructor(message, metadata, cause) {
|
|
9
|
+
super(message, { cause });
|
|
10
|
+
this.metadata = Object.freeze({ ...metadata });
|
|
11
|
+
this.cause = cause;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isCompactionTransactionError(error) {
|
|
15
|
+
return error instanceof CompactionTransactionError;
|
|
16
|
+
}
|
|
17
|
+
export function classifyCompactionError(error, metadata) {
|
|
18
|
+
if (error instanceof AgentRunCancelledError)
|
|
19
|
+
throw error;
|
|
20
|
+
if (error instanceof CompactionTransactionError)
|
|
21
|
+
return error;
|
|
22
|
+
return new CompactionTransactionError(`Compaction ${metadata.phase} failed: ${error instanceof Error ? error.message : String(error)}`, metadata, error);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=compaction-errors.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentRunCancelledError, } from '../core/runtime.js';
|
|
2
2
|
import { contextRecoveryMadeProgress, isPromptTooLongError, } from '../core/context-budget.js';
|
|
3
3
|
import { StaleContextGenerationError } from './context-preparation.js';
|
|
4
|
+
import { CompactionTransactionError } from './compaction-errors.js';
|
|
4
5
|
function assertSignal(signal) {
|
|
5
6
|
if (signal?.aborted)
|
|
6
7
|
throw new AgentRunCancelledError();
|
|
@@ -100,7 +101,15 @@ export class ContextEngine {
|
|
|
100
101
|
cause instanceof AgentRunCancelledError ||
|
|
101
102
|
cause instanceof StaleContextGenerationError)
|
|
102
103
|
throw cause;
|
|
103
|
-
|
|
104
|
+
if (cause instanceof CompactionTransactionError) {
|
|
105
|
+
const metadata = cause.metadata;
|
|
106
|
+
if (metadata.trigger !== 'auto' ||
|
|
107
|
+
metadata.phase !== 'generation' ||
|
|
108
|
+
metadata.durableState !== 'not_committed')
|
|
109
|
+
throw cause;
|
|
110
|
+
return { kind: 'exhausted', error };
|
|
111
|
+
}
|
|
112
|
+
throw cause;
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
observeUsage(usage, messages, tools) {
|
|
@@ -28,6 +28,9 @@ export interface NativeCompactionAppend {
|
|
|
28
28
|
readonly direction?: 'from' | 'up_to';
|
|
29
29
|
readonly messagesSummarized?: number;
|
|
30
30
|
readonly preservePrefix?: boolean;
|
|
31
|
+
/** IDs reserved by the compaction accounting transaction. */
|
|
32
|
+
readonly boundaryId?: string;
|
|
33
|
+
readonly summaryId?: string;
|
|
31
34
|
}
|
|
32
35
|
export type NativeInterruption = {
|
|
33
36
|
readonly kind: 'complete' | 'none';
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { activeEvents, projectActiveMessages, unresolvedActiveToolCallIds, } from './transcript-projection.js';
|
|
3
|
+
function isSafeNativeId(value) {
|
|
4
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/u.test(value);
|
|
5
|
+
}
|
|
3
6
|
export class NativeSessionTranscript {
|
|
4
7
|
sessionId;
|
|
5
8
|
store;
|
|
@@ -275,8 +278,22 @@ export class NativeSessionTranscript {
|
|
|
275
278
|
const events = records.map((record) => record.event);
|
|
276
279
|
if (unresolvedActiveToolCallIds(projectActiveMessages(events, activeId)).length > 0)
|
|
277
280
|
throw new Error('Cannot compact a native transcript with unresolved tool calls');
|
|
278
|
-
const boundaryId = createUniqueId();
|
|
279
|
-
const summaryId = createUniqueId();
|
|
281
|
+
const boundaryId = input.boundaryId ?? createUniqueId();
|
|
282
|
+
const summaryId = input.summaryId ?? createUniqueId();
|
|
283
|
+
if (typeof boundaryId !== 'string' ||
|
|
284
|
+
boundaryId.trim() === '' ||
|
|
285
|
+
typeof summaryId !== 'string' ||
|
|
286
|
+
summaryId.trim() === '' ||
|
|
287
|
+
!isSafeNativeId(boundaryId) ||
|
|
288
|
+
!isSafeNativeId(summaryId) ||
|
|
289
|
+
boundaryId === summaryId ||
|
|
290
|
+
(input.boundaryId !== undefined && usedIds.has(boundaryId)) ||
|
|
291
|
+
(input.summaryId !== undefined && usedIds.has(summaryId)))
|
|
292
|
+
throw new Error('Native compaction boundary and summary IDs must be nonblank, distinct, and unused');
|
|
293
|
+
if (input.boundaryId !== undefined)
|
|
294
|
+
usedIds.add(boundaryId);
|
|
295
|
+
if (input.summaryId !== undefined)
|
|
296
|
+
usedIds.add(summaryId);
|
|
280
297
|
const boundary = {
|
|
281
298
|
kind: 'context-boundary',
|
|
282
299
|
id: boundaryId,
|
|
@@ -353,9 +370,24 @@ export class NativeSessionTranscript {
|
|
|
353
370
|
summary,
|
|
354
371
|
...(suffixEvent === undefined ? [] : [suffixEvent]),
|
|
355
372
|
];
|
|
356
|
-
|
|
357
|
-
|
|
373
|
+
let appended;
|
|
374
|
+
try {
|
|
375
|
+
appended = await nativeLease.appendMany(tail, compactedEvents);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (input.boundaryId !== undefined)
|
|
379
|
+
usedIds.delete(boundaryId);
|
|
380
|
+
if (input.summaryId !== undefined)
|
|
381
|
+
usedIds.delete(summaryId);
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
if (appended.status === 'conflict') {
|
|
385
|
+
if (input.boundaryId !== undefined)
|
|
386
|
+
usedIds.delete(boundaryId);
|
|
387
|
+
if (input.summaryId !== undefined)
|
|
388
|
+
usedIds.delete(summaryId);
|
|
358
389
|
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
390
|
+
}
|
|
359
391
|
records.push({ event: boundary }, { event: summary }, ...(suffixEvent === undefined ? [] : [{ event: suffixEvent }]));
|
|
360
392
|
tail = appended.tail;
|
|
361
393
|
activeId = suffixEvent?.id ?? summaryId;
|
|
@@ -230,6 +230,7 @@ export declare class ClaudeSessionService {
|
|
|
230
230
|
private activeProvider;
|
|
231
231
|
private mcpClosePromise;
|
|
232
232
|
private readonly sessionCostTrackers;
|
|
233
|
+
private readonly compactionAccountings;
|
|
233
234
|
private activeCostSessionId;
|
|
234
235
|
private closeCostSavePromise;
|
|
235
236
|
private closeMetadataSavePromise;
|
|
@@ -354,6 +355,7 @@ export declare class ClaudeSessionService {
|
|
|
354
355
|
private enqueueBackgroundNotifications;
|
|
355
356
|
private store;
|
|
356
357
|
private nativeStore;
|
|
358
|
+
private compactionAccounting;
|
|
357
359
|
private assertSessionPersistence;
|
|
358
360
|
private assertWritable;
|
|
359
361
|
private assertTurnWritable;
|