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
|
@@ -255,10 +255,6 @@ export class TurnAccounting {
|
|
|
255
255
|
consumed = true;
|
|
256
256
|
try {
|
|
257
257
|
const latestState = this.compactionStateAfter(snapshot);
|
|
258
|
-
const latestPlan = this.compactionPlan(snapshot);
|
|
259
|
-
const latestPreflight = cloneTracker(this.tracker);
|
|
260
|
-
applyPlan(latestPreflight, latestPlan);
|
|
261
|
-
applyPlan(this.tracker, latestPlan);
|
|
262
258
|
this.compactionUsage = clone(latestState.usage);
|
|
263
259
|
this.compactionModelUsage = clone(latestState.modelUsage);
|
|
264
260
|
this.compactionDurationMs = latestState.durationApiMs;
|
package/dist/build-identity.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schema_version":"1.0","source_revision":"git:
|
|
1
|
+
{"schema_version":"1.0","source_revision":"git:044ea290901a55dd5e343e78bdcd6a63d0455a00","source_dirty":false,"artifact_sha256":"sha256:272099586a6fe68b899e7ec80e182b404afe590341a24cb90196eebe90d3ce29"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type NativeDataResource = 'agents' | 'auto-memory' | 'commands' | 'durable-task-graph' | 'file-history' | 'hooks' | 'instructions' | 'mcp' | 'provider-payload' | 'search-index' | 'scheduled-prompts' | 'session-lock' | 'settings' | 'skills' | 'transcript';
|
|
1
|
+
export type NativeDataResource = 'agents' | 'auto-memory' | 'commands' | 'compaction-accounting' | 'durable-task-graph' | 'file-history' | 'hooks' | 'instructions' | 'mcp' | 'provider-payload' | 'search-index' | 'scheduled-prompts' | 'session-lock' | 'settings' | 'skills' | 'transcript';
|
|
2
2
|
export type DataPlane = 'praxis-sidecar' | 'shared';
|
|
3
3
|
export type PraxisAccess = 'append-only' | 'read-only' | 'read-write';
|
|
4
4
|
export interface DataOwnershipPolicy {
|
|
@@ -12,6 +12,11 @@ export declare const NATIVE_DATA_OWNERSHIP: readonly [{
|
|
|
12
12
|
readonly plane: "shared";
|
|
13
13
|
readonly praxisAccess: "append-only";
|
|
14
14
|
readonly location: "sessions/<project-key>/<session-id>.jsonl";
|
|
15
|
+
}, {
|
|
16
|
+
readonly resource: "compaction-accounting";
|
|
17
|
+
readonly plane: "praxis-sidecar";
|
|
18
|
+
readonly praxisAccess: "read-write";
|
|
19
|
+
readonly location: "compaction-receipts/<session-id>/";
|
|
15
20
|
}, {
|
|
16
21
|
readonly resource: "auto-memory";
|
|
17
22
|
readonly plane: "shared";
|
package/dist/native/ownership.js
CHANGED
|
@@ -5,6 +5,12 @@ export const NATIVE_DATA_OWNERSHIP = [
|
|
|
5
5
|
praxisAccess: 'append-only',
|
|
6
6
|
location: 'sessions/<project-key>/<session-id>.jsonl',
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
resource: 'compaction-accounting',
|
|
10
|
+
plane: 'praxis-sidecar',
|
|
11
|
+
praxisAccess: 'read-write',
|
|
12
|
+
location: 'compaction-receipts/<session-id>/',
|
|
13
|
+
},
|
|
8
14
|
{
|
|
9
15
|
resource: 'auto-memory',
|
|
10
16
|
plane: 'shared',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TurnCompactionMetric } from '../application/turn-accounting.js';
|
|
2
|
+
export declare const NATIVE_COMPACTION_RECEIPT_VERSION: 1;
|
|
3
|
+
export interface NativeCompactionReceipt {
|
|
4
|
+
readonly version: 1;
|
|
5
|
+
readonly receiptId: string;
|
|
6
|
+
readonly sessionId: string;
|
|
7
|
+
readonly boundaryId: string;
|
|
8
|
+
readonly summaryId: string;
|
|
9
|
+
readonly trigger: 'auto' | 'manual';
|
|
10
|
+
readonly metric: TurnCompactionMetric;
|
|
11
|
+
readonly costUsd: number | null;
|
|
12
|
+
readonly before: string;
|
|
13
|
+
readonly after: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CompactionReceiptStore {
|
|
16
|
+
prepare(receipt: NativeCompactionReceipt): Promise<void>;
|
|
17
|
+
acknowledge(sessionId: string, receiptId: string): Promise<void>;
|
|
18
|
+
list(sessionId: string): Promise<readonly {
|
|
19
|
+
receipt: NativeCompactionReceipt;
|
|
20
|
+
acknowledged: boolean;
|
|
21
|
+
}[]>;
|
|
22
|
+
}
|
|
23
|
+
export declare class NativeCompactionReceiptStore implements CompactionReceiptStore {
|
|
24
|
+
/** Compatibility alias for callers that name the file-backed adapter. */
|
|
25
|
+
readonly directory: string;
|
|
26
|
+
private readonly lease;
|
|
27
|
+
constructor(options: {
|
|
28
|
+
readonly sidecarRoot: string;
|
|
29
|
+
readonly lockFile?: string;
|
|
30
|
+
});
|
|
31
|
+
prepare(receipt: NativeCompactionReceipt): Promise<void>;
|
|
32
|
+
acknowledge(sessionId: string, receiptId: string): Promise<void>;
|
|
33
|
+
list(sessionId: string): Promise<readonly {
|
|
34
|
+
receipt: NativeCompactionReceipt;
|
|
35
|
+
acknowledged: boolean;
|
|
36
|
+
}[]>;
|
|
37
|
+
private acquire;
|
|
38
|
+
}
|
|
39
|
+
export { NativeCompactionReceiptStore as NativeCompactionReceiptFileStore };
|
|
40
|
+
//# sourceMappingURL=native-compaction-receipt-store.d.ts.map
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, open, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
5
|
+
import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
|
|
6
|
+
export const NATIVE_COMPACTION_RECEIPT_VERSION = 1;
|
|
7
|
+
const MAX_RECEIPTS = 4096;
|
|
8
|
+
const MAX_BYTES = 512 * 1024;
|
|
9
|
+
function recordKeys(value, keys) {
|
|
10
|
+
return Object.keys(value).every((key) => keys.includes(key));
|
|
11
|
+
}
|
|
12
|
+
function exactKeys(value, keys) {
|
|
13
|
+
return Object.keys(value).length === keys.length && recordKeys(value, keys);
|
|
14
|
+
}
|
|
15
|
+
function nonblank(value, field) {
|
|
16
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
17
|
+
throw new TypeError(`${field} must not be blank`);
|
|
18
|
+
}
|
|
19
|
+
function usage(value) {
|
|
20
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const row = value;
|
|
23
|
+
if (!recordKeys(row, [
|
|
24
|
+
'inputTokens',
|
|
25
|
+
'outputTokens',
|
|
26
|
+
'cacheReadInputTokens',
|
|
27
|
+
'cacheCreationInputTokens',
|
|
28
|
+
'cacheCreationInputTokens1h',
|
|
29
|
+
'webSearchRequests',
|
|
30
|
+
'contextWindow',
|
|
31
|
+
'maxOutputTokens',
|
|
32
|
+
]))
|
|
33
|
+
return false;
|
|
34
|
+
for (const field of [
|
|
35
|
+
'inputTokens',
|
|
36
|
+
'outputTokens',
|
|
37
|
+
'cacheReadInputTokens',
|
|
38
|
+
'cacheCreationInputTokens',
|
|
39
|
+
'cacheCreationInputTokens1h',
|
|
40
|
+
'webSearchRequests',
|
|
41
|
+
]) {
|
|
42
|
+
const n = row[field];
|
|
43
|
+
if (n !== undefined && (!Number.isSafeInteger(n) || Number(n) < 0))
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
for (const field of ['contextWindow', 'maxOutputTokens']) {
|
|
47
|
+
const n = row[field];
|
|
48
|
+
if (n !== undefined && (!Number.isSafeInteger(n) || Number(n) < 0))
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (Number(row.cacheCreationInputTokens1h ?? 0) >
|
|
52
|
+
Number(row.cacheCreationInputTokens ?? 0))
|
|
53
|
+
return false;
|
|
54
|
+
return (Number.isSafeInteger(row.inputTokens) &&
|
|
55
|
+
Number(row.inputTokens) >= 0 &&
|
|
56
|
+
Number.isSafeInteger(row.outputTokens) &&
|
|
57
|
+
Number(row.outputTokens) >= 0);
|
|
58
|
+
}
|
|
59
|
+
function metric(value) {
|
|
60
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
61
|
+
return false;
|
|
62
|
+
const row = value;
|
|
63
|
+
if (!recordKeys(row, [
|
|
64
|
+
'usage',
|
|
65
|
+
'model',
|
|
66
|
+
'durationApiMs',
|
|
67
|
+
'durationApiWithoutRetriesMs',
|
|
68
|
+
]) ||
|
|
69
|
+
!usage(row.usage))
|
|
70
|
+
return false;
|
|
71
|
+
if (row.model !== undefined &&
|
|
72
|
+
(typeof row.model !== 'string' || row.model.trim() === ''))
|
|
73
|
+
return false;
|
|
74
|
+
if ([
|
|
75
|
+
row.usage.inputTokens,
|
|
76
|
+
row.usage.outputTokens,
|
|
77
|
+
row.usage.cacheReadInputTokens,
|
|
78
|
+
row.usage.cacheCreationInputTokens,
|
|
79
|
+
row.usage.cacheCreationInputTokens1h,
|
|
80
|
+
row.usage.webSearchRequests,
|
|
81
|
+
].some((value) => typeof value === 'number' && value > 0) &&
|
|
82
|
+
row.model === undefined)
|
|
83
|
+
return false;
|
|
84
|
+
return [row.durationApiMs, row.durationApiWithoutRetriesMs].every((n) => typeof n === 'number' && Number.isFinite(n) && Number(n) >= 0);
|
|
85
|
+
}
|
|
86
|
+
function parseReceipt(value) {
|
|
87
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
88
|
+
throw new TypeError('Invalid compaction receipt');
|
|
89
|
+
const row = value;
|
|
90
|
+
const keys = [
|
|
91
|
+
'version',
|
|
92
|
+
'receiptId',
|
|
93
|
+
'sessionId',
|
|
94
|
+
'boundaryId',
|
|
95
|
+
'summaryId',
|
|
96
|
+
'trigger',
|
|
97
|
+
'metric',
|
|
98
|
+
'costUsd',
|
|
99
|
+
'before',
|
|
100
|
+
'after',
|
|
101
|
+
];
|
|
102
|
+
if (!exactKeys(row, keys) || row.version !== 1 || !metric(row.metric))
|
|
103
|
+
throw new TypeError('Invalid compaction receipt');
|
|
104
|
+
for (const key of [
|
|
105
|
+
'receiptId',
|
|
106
|
+
'sessionId',
|
|
107
|
+
'boundaryId',
|
|
108
|
+
'summaryId',
|
|
109
|
+
'before',
|
|
110
|
+
'after',
|
|
111
|
+
])
|
|
112
|
+
nonblank(row[key], key);
|
|
113
|
+
safeId(row.receiptId);
|
|
114
|
+
safeId(row.sessionId);
|
|
115
|
+
safeId(row.boundaryId);
|
|
116
|
+
safeId(row.summaryId);
|
|
117
|
+
if (!/^[a-f0-9]{64}$/u.test(row.before) ||
|
|
118
|
+
!/^[a-f0-9]{64}$/u.test(row.after))
|
|
119
|
+
throw new TypeError('Invalid compaction accounting fingerprint');
|
|
120
|
+
if (row.receiptId === row.boundaryId ||
|
|
121
|
+
row.receiptId === row.summaryId ||
|
|
122
|
+
row.boundaryId === row.summaryId)
|
|
123
|
+
throw new TypeError('Compaction receipt IDs must be distinct');
|
|
124
|
+
if (row.trigger !== 'auto' && row.trigger !== 'manual')
|
|
125
|
+
throw new TypeError('Invalid compaction receipt trigger');
|
|
126
|
+
if (row.costUsd !== null &&
|
|
127
|
+
(typeof row.costUsd !== 'number' ||
|
|
128
|
+
!Number.isFinite(row.costUsd) ||
|
|
129
|
+
row.costUsd < 0))
|
|
130
|
+
throw new TypeError('Invalid compaction receipt cost');
|
|
131
|
+
const parsedMetric = row.metric;
|
|
132
|
+
if (parsedMetric.durationApiWithoutRetriesMs > parsedMetric.durationApiMs ||
|
|
133
|
+
(![
|
|
134
|
+
parsedMetric.usage.inputTokens,
|
|
135
|
+
parsedMetric.usage.outputTokens,
|
|
136
|
+
parsedMetric.usage.cacheReadInputTokens,
|
|
137
|
+
parsedMetric.usage.cacheCreationInputTokens,
|
|
138
|
+
parsedMetric.usage.cacheCreationInputTokens1h,
|
|
139
|
+
parsedMetric.usage.webSearchRequests,
|
|
140
|
+
].some((entry) => typeof entry === 'number' && entry > 0) &&
|
|
141
|
+
row.costUsd !== null))
|
|
142
|
+
throw new TypeError('Invalid compaction receipt accounting outcome');
|
|
143
|
+
return structuredClone(row);
|
|
144
|
+
}
|
|
145
|
+
function safeId(value) {
|
|
146
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/u.test(value))
|
|
147
|
+
throw new TypeError('Invalid compaction receipt ID');
|
|
148
|
+
}
|
|
149
|
+
export class NativeCompactionReceiptStore {
|
|
150
|
+
/** Compatibility alias for callers that name the file-backed adapter. */
|
|
151
|
+
directory;
|
|
152
|
+
lease;
|
|
153
|
+
constructor(options) {
|
|
154
|
+
nonblank(options.sidecarRoot, 'sidecarRoot');
|
|
155
|
+
this.directory = join(options.sidecarRoot, 'compaction-receipts');
|
|
156
|
+
this.lease = new ExclusiveFileLease(options.lockFile ??
|
|
157
|
+
join(options.sidecarRoot, 'locks', 'compaction-receipts.lock'));
|
|
158
|
+
}
|
|
159
|
+
async prepare(receipt) {
|
|
160
|
+
const checked = parseReceipt(receipt);
|
|
161
|
+
safeId(checked.receiptId);
|
|
162
|
+
const serialized = `${JSON.stringify(checked)}\n`;
|
|
163
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_BYTES)
|
|
164
|
+
throw new Error('Invalid compaction receipt file');
|
|
165
|
+
const handle = await this.acquire();
|
|
166
|
+
try {
|
|
167
|
+
const directory = join(this.directory, checked.sessionId);
|
|
168
|
+
try {
|
|
169
|
+
const stat = await lstat(directory);
|
|
170
|
+
if (!stat.isDirectory())
|
|
171
|
+
throw new Error('Compaction receipt session path is not a directory');
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error.code !== 'ENOENT')
|
|
175
|
+
throw error;
|
|
176
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
177
|
+
}
|
|
178
|
+
const requestedIds = new Set([
|
|
179
|
+
checked.receiptId,
|
|
180
|
+
checked.boundaryId,
|
|
181
|
+
checked.summaryId,
|
|
182
|
+
]);
|
|
183
|
+
for (const { receipt: existingReceipt } of await this.list(checked.sessionId)) {
|
|
184
|
+
if (existingReceipt.receiptId === checked.receiptId)
|
|
185
|
+
continue;
|
|
186
|
+
if ([
|
|
187
|
+
existingReceipt.receiptId,
|
|
188
|
+
existingReceipt.boundaryId,
|
|
189
|
+
existingReceipt.summaryId,
|
|
190
|
+
].some((id) => requestedIds.has(id)))
|
|
191
|
+
throw new Error('Duplicate compaction transaction identity');
|
|
192
|
+
}
|
|
193
|
+
const path = join(directory, `${checked.receiptId}.json`);
|
|
194
|
+
const ackPath = join(directory, `${checked.receiptId}.ack`);
|
|
195
|
+
try {
|
|
196
|
+
await lstat(ackPath);
|
|
197
|
+
throw new Error('Compaction receipt acknowledgement already exists for this receipt');
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error.code !== 'ENOENT')
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
let existing = null;
|
|
204
|
+
try {
|
|
205
|
+
const reader = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
206
|
+
try {
|
|
207
|
+
const stat = await reader.stat();
|
|
208
|
+
if (!stat.isFile() ||
|
|
209
|
+
(stat.mode & 0o077) !== 0 ||
|
|
210
|
+
stat.size > MAX_BYTES)
|
|
211
|
+
throw new Error('Invalid compaction receipt file');
|
|
212
|
+
existing = await reader.readFile('utf8');
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
await reader.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error.code !== 'ENOENT')
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
if (existing !== null) {
|
|
223
|
+
const prior = parseReceipt(JSON.parse(existing));
|
|
224
|
+
if (JSON.stringify(prior) !== JSON.stringify(checked))
|
|
225
|
+
throw new Error('Compaction receipt ID already contains a different receipt');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await writeFileAtomically(path, serialized, { mode: 0o600 });
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await handle.release();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async acknowledge(sessionId, receiptId) {
|
|
235
|
+
safeId(sessionId);
|
|
236
|
+
safeId(receiptId);
|
|
237
|
+
const handle = await this.acquire();
|
|
238
|
+
try {
|
|
239
|
+
const directory = join(this.directory, sessionId);
|
|
240
|
+
const directoryStat = await lstat(directory);
|
|
241
|
+
if (!directoryStat.isDirectory())
|
|
242
|
+
throw new Error('Compaction receipt session path is not a directory');
|
|
243
|
+
const source = join(directory, `${receiptId}.json`);
|
|
244
|
+
const sourceHandle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
245
|
+
try {
|
|
246
|
+
const stat = await sourceHandle.stat();
|
|
247
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0)
|
|
248
|
+
throw new Error('Compaction receipt is not a regular file');
|
|
249
|
+
if (stat.size > MAX_BYTES)
|
|
250
|
+
throw new Error('Invalid compaction receipt file');
|
|
251
|
+
const parsed = parseReceipt(JSON.parse(await sourceHandle.readFile('utf8')));
|
|
252
|
+
if (parsed.sessionId !== sessionId || parsed.receiptId !== receiptId)
|
|
253
|
+
throw new Error('Compaction receipt path mismatch');
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
await sourceHandle.close();
|
|
257
|
+
}
|
|
258
|
+
const ack = join(directory, `${receiptId}.ack`);
|
|
259
|
+
try {
|
|
260
|
+
const marker = await open(ack, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
261
|
+
try {
|
|
262
|
+
const stat = await marker.stat();
|
|
263
|
+
if (!stat.isFile() ||
|
|
264
|
+
(stat.mode & 0o077) !== 0 ||
|
|
265
|
+
stat.size > 256 ||
|
|
266
|
+
(await marker.readFile('utf8')) !== `${receiptId}\n`)
|
|
267
|
+
throw new Error('Invalid compaction receipt acknowledgement');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
await marker.close();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error.code !== 'ENOENT')
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
await writeFileAtomically(ack, `${receiptId}\n`, { mode: 0o600 });
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await handle.release();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async list(sessionId) {
|
|
285
|
+
safeId(sessionId);
|
|
286
|
+
const sessionDirectory = join(this.directory, sessionId);
|
|
287
|
+
let files;
|
|
288
|
+
try {
|
|
289
|
+
const directoryStat = await lstat(sessionDirectory);
|
|
290
|
+
if (!directoryStat.isDirectory())
|
|
291
|
+
throw new Error('Compaction receipt session path is not a directory');
|
|
292
|
+
files = await readdir(sessionDirectory);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (error.code === 'ENOENT')
|
|
296
|
+
return [];
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
if (files.length > MAX_RECEIPTS)
|
|
300
|
+
throw new Error('Too many compaction receipt artifacts');
|
|
301
|
+
const result = [];
|
|
302
|
+
const receiptIds = new Set();
|
|
303
|
+
const transactionIds = new Set();
|
|
304
|
+
const ackIds = new Set();
|
|
305
|
+
for (const name of files.sort()) {
|
|
306
|
+
if (!name.endsWith('.json') && !name.endsWith('.ack'))
|
|
307
|
+
throw new Error('Unknown compaction receipt artifact');
|
|
308
|
+
if (name.endsWith('.ack')) {
|
|
309
|
+
const ackId = name.slice(0, -4);
|
|
310
|
+
safeId(ackId);
|
|
311
|
+
ackIds.add(ackId);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const receiptId = name.slice(0, -5);
|
|
315
|
+
safeId(receiptId);
|
|
316
|
+
const handle = await open(join(sessionDirectory, name), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
317
|
+
try {
|
|
318
|
+
const stat = await handle.stat();
|
|
319
|
+
if (!stat.isFile() ||
|
|
320
|
+
(stat.mode & 0o077) !== 0 ||
|
|
321
|
+
stat.size > MAX_BYTES)
|
|
322
|
+
throw new Error('Invalid compaction receipt file');
|
|
323
|
+
const parsed = parseReceipt(JSON.parse(await handle.readFile('utf8')));
|
|
324
|
+
if (parsed.sessionId !== sessionId || parsed.receiptId !== receiptId)
|
|
325
|
+
throw new Error('Compaction receipt path mismatch');
|
|
326
|
+
if (receiptIds.has(receiptId))
|
|
327
|
+
throw new Error('Duplicate compaction receipt ID');
|
|
328
|
+
receiptIds.add(receiptId);
|
|
329
|
+
for (const id of [receiptId, parsed.boundaryId, parsed.summaryId]) {
|
|
330
|
+
if (transactionIds.has(id))
|
|
331
|
+
throw new Error('Duplicate compaction transaction identity');
|
|
332
|
+
transactionIds.add(id);
|
|
333
|
+
}
|
|
334
|
+
let acknowledged = false;
|
|
335
|
+
try {
|
|
336
|
+
const marker = await open(join(sessionDirectory, `${receiptId}.ack`), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
337
|
+
try {
|
|
338
|
+
const markerStat = await marker.stat();
|
|
339
|
+
if (!markerStat.isFile() ||
|
|
340
|
+
(markerStat.mode & 0o077) !== 0 ||
|
|
341
|
+
markerStat.size > 256 ||
|
|
342
|
+
(await marker.readFile('utf8')) !== `${receiptId}\n`)
|
|
343
|
+
throw new Error('Invalid compaction receipt acknowledgement');
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
await marker.close();
|
|
347
|
+
}
|
|
348
|
+
acknowledged = true;
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (error.code !== 'ENOENT')
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
result.push({ receipt: parsed, acknowledged });
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
await handle.close();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const ackId of ackIds)
|
|
361
|
+
if (!receiptIds.has(ackId))
|
|
362
|
+
throw new Error('Acknowledgement has no receipt');
|
|
363
|
+
return result.sort((a, b) => a.receipt.receiptId < b.receipt.receiptId
|
|
364
|
+
? -1
|
|
365
|
+
: a.receipt.receiptId > b.receipt.receiptId
|
|
366
|
+
? 1
|
|
367
|
+
: 0);
|
|
368
|
+
}
|
|
369
|
+
async acquire() {
|
|
370
|
+
for (let i = 0; i < 400; i += 1) {
|
|
371
|
+
const handle = await this.lease.tryAcquire();
|
|
372
|
+
if (handle)
|
|
373
|
+
return handle;
|
|
374
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
375
|
+
}
|
|
376
|
+
throw new Error('Timed out acquiring compaction receipt lock');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
export { NativeCompactionReceiptStore as NativeCompactionReceiptFileStore };
|
|
380
|
+
//# sourceMappingURL=native-compaction-receipt-store.js.map
|