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
package/README.md
CHANGED
|
@@ -134,12 +134,16 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
134
134
|
|
|
135
135
|
- **Outcome-driven evaluation** — `praxis eval <target>` runs contained cases
|
|
136
136
|
in isolated workspaces, requires explicit verifier authorization, and writes
|
|
137
|
-
versioned artifacts with deterministic run identities locally
|
|
137
|
+
versioned artifacts with deterministic run identities locally, including the
|
|
138
|
+
Praxis source revision, dirty state, and emitted-runtime digest; usage and cost
|
|
138
139
|
remain explicitly available or unknown. Separate runs can be compared with
|
|
139
140
|
`praxis eval compare` only when provider, model, configuration, tools, prompt,
|
|
140
|
-
corpus, and runtime
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
corpus, and host-runtime identities match; Praxis version and build provenance
|
|
142
|
+
remain attribution fields so candidate builds may differ. Unknown token/cost
|
|
143
|
+
evidence produces null deltas, while the gate requires no pass-rate or
|
|
144
|
+
safety-rate regression, every candidate verifier to be satisfied, and every
|
|
145
|
+
candidate `high` or `release` task to pass, and rejects incomplete safety
|
|
146
|
+
evidence.
|
|
143
147
|
- **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
|
|
144
148
|
`❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
|
|
145
149
|
composer grammar, compact stable tool rows, responsive density,
|
|
@@ -403,9 +407,10 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
|
|
|
403
407
|
`npm run test:coverage` measures all production code under `src/**` with V8 and
|
|
404
408
|
enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
|
|
405
409
|
and rejects any production runtime module with zero covered statements (while allowing
|
|
406
|
-
type-only modules). `npm run test:fixtures` executes the
|
|
407
|
-
are qualified and 8 are explicitly excluded.
|
|
408
|
-
performs the structural check and is part
|
|
410
|
+
type-only modules). `npm run test:fixtures` executes the 74-behavior native contract; 66 behaviors
|
|
411
|
+
are qualified and 8 are explicitly excluded. Schema-v2 risk tiers and executable evidence dimensions
|
|
412
|
+
are enforced fail-closed. `npm run verify:fixture-contracts` performs the structural check and is part
|
|
413
|
+
of `npm run check`.
|
|
409
414
|
`npm run test:core-completion` is retained as a compatibility alias for
|
|
410
415
|
`npm run test:fixtures`.
|
|
411
416
|
|
|
@@ -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;
|