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.
Files changed (30) hide show
  1. package/README.md +12 -7
  2. package/dist/application/compaction-accounting.d.ts +49 -0
  3. package/dist/application/compaction-accounting.js +349 -0
  4. package/dist/application/compaction-errors.d.ts +21 -0
  5. package/dist/application/compaction-errors.js +24 -0
  6. package/dist/application/context-engine.js +10 -1
  7. package/dist/application/native-session-transcript.d.ts +3 -0
  8. package/dist/application/native-session-transcript.js +36 -4
  9. package/dist/application/session-service.d.ts +2 -0
  10. package/dist/application/session-service.js +526 -197
  11. package/dist/application/turn-accounting.js +0 -4
  12. package/dist/build-identity.json +1 -0
  13. package/dist/cli-runtime.js +2 -0
  14. package/dist/evals/project-eval-comparison.d.ts +4 -1
  15. package/dist/evals/project-eval-comparison.js +234 -78
  16. package/dist/evals/project-eval-identity.d.ts +4 -1
  17. package/dist/evals/project-eval-identity.js +9 -2
  18. package/dist/evals/project-eval-runner.d.ts +24 -2
  19. package/dist/evals/project-eval-runner.js +118 -59
  20. package/dist/evals/project-eval-schema.d.ts +8 -2
  21. package/dist/evals/project-eval-schema.js +27 -11
  22. package/dist/evals/project-eval.d.ts +22 -2
  23. package/dist/evals/project-eval.js +34 -1
  24. package/dist/native/ownership.d.ts +6 -1
  25. package/dist/native/ownership.js +6 -0
  26. package/dist/persistence/native-compaction-receipt-store.d.ts +40 -0
  27. package/dist/persistence/native-compaction-receipt-store.js +380 -0
  28. package/dist/platform/praxis-build-identity.d.ts +14 -0
  29. package/dist/platform/praxis-build-identity.js +204 -0
  30. 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; usage and cost
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-environment identities match. Unknown token/cost evidence
141
- produces null deltas, while the gate requires no pass-rate or safety-rate
142
- regression and rejects incomplete safety evidence.
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 71-behavior native contract; 63 behaviors
407
- are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
408
- performs the structural check and is part of `npm run check`.
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
- return { kind: 'exhausted', error };
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
- const appended = await nativeLease.appendMany(tail, compactedEvents);
357
- if (appended.status === 'conflict')
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;