@adecore/agents 0.41.1 → 0.43.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/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +21 -3
- package/dist/chat/chat-store.d.ts +7 -1
- package/dist/chat/chat-store.js +68 -7
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/payload-store.d.ts +28 -0
- package/dist/chat/payload-store.js +350 -0
- package/dist/chat/projector.js +9 -1
- package/dist/host/handlers.js +1 -0
- package/dist/usage/history-file.d.ts +25 -0
- package/dist/usage/history-file.js +72 -0
- package/dist/usage/index-file.d.ts +1 -1
- package/dist/usage/index-file.js +33 -114
- package/dist/usage/rows.d.ts +23 -0
- package/dist/usage/rows.js +94 -0
- package/dist/usage/scan-state.d.ts +21 -0
- package/dist/usage/scan-state.js +53 -0
- package/dist/usage/scanner.d.ts +1 -4
- package/dist/usage/scanner.js +37 -50
- package/dist/usage/usage-store.d.ts +26 -0
- package/dist/usage/usage-store.js +197 -0
- package/package.json +3 -3
- package/src/chat/chat-core.ts +27 -3
- package/src/chat/chat-store.ts +83 -8
- package/src/chat/errors.ts +2 -0
- package/src/chat/payload-store.ts +459 -0
- package/src/chat/projector.ts +11 -1
- package/src/host/handlers.ts +1 -0
- package/src/host/wiring.ts +1 -1
- package/src/usage/history-file.ts +140 -0
- package/src/usage/index-file.ts +35 -143
- package/src/usage/rows.ts +136 -0
- package/src/usage/scan-state.ts +105 -0
- package/src/usage/scanner.ts +39 -54
- package/src/usage/usage-store.ts +237 -0
package/src/usage/scanner.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { createHash, type Hash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { open, readFile, readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { Readable } from 'node:stream';
|
|
5
5
|
import { createZstdDecompress } from 'node:zlib';
|
|
6
6
|
import type { UsageProvider, UsageRoot } from '@adecore/agent-contracts';
|
|
7
7
|
import { errorText } from '../error-text.ts';
|
|
8
|
-
import { isNotFound
|
|
9
|
-
import { decodeIndex, encodeIndex, type IndexedFile, type UsageIndex } from './index-file.ts';
|
|
8
|
+
import { isNotFound } from '../fs.ts';
|
|
10
9
|
import { foldByKey, type UsageRecord } from './record.ts';
|
|
11
10
|
import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.ts';
|
|
12
11
|
import { GROK_USAGE_FILE, parseGrokUsage } from './readers/grok.ts';
|
|
13
12
|
import { hostLogMightCarryUsage, parseHostUsageLine } from './readers/host-log.ts';
|
|
14
13
|
import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine, type CodexParserState } from './readers/codex.ts';
|
|
15
14
|
import { usageRoots, type UsageRootPath } from './roots.ts';
|
|
15
|
+
import type { ScanState } from './scan-state.ts';
|
|
16
|
+
import { UsageStore } from './usage-store.ts';
|
|
16
17
|
|
|
17
18
|
const HASH_CHUNK_BYTES = 65536;
|
|
18
19
|
// How much of a transcript is read at once. Codex writes sessions of more than a gigabyte, which read whole held several times their size.
|
|
@@ -115,6 +116,11 @@ function isUsageFile(provider: UsageProvider, name: string): boolean {
|
|
|
115
116
|
return readsWhole(provider) ? name === GROK_USAGE_FILE : name.endsWith('.jsonl') || (provider === 'codex' && name.endsWith('.jsonl.zst'));
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
/* A compressed Codex session is the same transcript as the plain one it was, so both count under one path. */
|
|
120
|
+
function keyOf(path: string, provider: UsageProvider): string {
|
|
121
|
+
return provider === 'codex' && path.endsWith('.jsonl.zst') ? path.slice(0, -4) : path;
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
async function listUsageFiles(dir: string, provider: UsageProvider, out: string[] = []): Promise<string[]> {
|
|
119
125
|
let entries;
|
|
120
126
|
|
|
@@ -266,47 +272,37 @@ function withAccount(records: UsageRecord[], accountOf: AccountOf): UsageRecord[
|
|
|
266
272
|
|
|
267
273
|
/*
|
|
268
274
|
* Parses only what a transcript appended since the last scan, and starts a file over once it was
|
|
269
|
-
* rewritten or cut. A deleted transcript keeps its totals, since its usage still happened
|
|
275
|
+
* rewritten or cut. A deleted transcript keeps its totals, since its usage still happened; only
|
|
276
|
+
* where the scan left it is forgotten.
|
|
270
277
|
*/
|
|
271
278
|
export class UsageScanner {
|
|
272
|
-
private readonly home: string;
|
|
273
279
|
private readonly roots: () => UsageRootPath[];
|
|
274
280
|
private readonly sessionAccounts: () => ReadonlyMap<string, string>;
|
|
275
|
-
private
|
|
281
|
+
private readonly store: UsageStore;
|
|
276
282
|
private loaded = false;
|
|
277
|
-
private dirty = false;
|
|
278
283
|
|
|
279
284
|
/* `roots` as a function is asked again on every scan, so an account added since is walked too. */
|
|
280
285
|
constructor(home: string, roots: UsageRootPath[] | (() => UsageRootPath[]) = usageRoots(), options: UsageScannerOptions = {}) {
|
|
281
|
-
this.
|
|
286
|
+
this.store = new UsageStore(join(home, 'usage'));
|
|
282
287
|
this.roots = typeof roots === 'function' ? roots : () => roots;
|
|
283
288
|
this.sessionAccounts = options.sessionAccounts ?? (() => new Map());
|
|
284
289
|
}
|
|
285
290
|
|
|
286
|
-
|
|
287
|
-
return join(this.home, 'usage', 'index.json');
|
|
288
|
-
}
|
|
289
|
-
|
|
291
|
+
/* A history that does not read fails the scan rather than being written over, which would lose what only it holds. */
|
|
290
292
|
async load(): Promise<void> {
|
|
291
293
|
if (this.loaded) {
|
|
292
294
|
return;
|
|
293
295
|
}
|
|
294
296
|
|
|
297
|
+
await this.store.load();
|
|
295
298
|
this.loaded = true;
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
this.index = decodeIndex(await readFile(this.file, 'utf8'));
|
|
299
|
-
} catch (e) {
|
|
300
|
-
if (!isNotFound(e)) {
|
|
301
|
-
console.warn('The usage index would not read; starting from a cold scan:', errorText(e));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
299
|
}
|
|
305
300
|
|
|
306
301
|
async scan(): Promise<ScanReport> {
|
|
307
302
|
const started = Date.now();
|
|
308
303
|
await this.load();
|
|
309
304
|
const roots: UsageRoot[] = [];
|
|
305
|
+
const seen = new Set<string>();
|
|
310
306
|
let files = 0;
|
|
311
307
|
let changedFiles = 0;
|
|
312
308
|
let sessions: ReadonlyMap<string, string> | null = null;
|
|
@@ -350,6 +346,7 @@ export class UsageScanner {
|
|
|
350
346
|
}
|
|
351
347
|
|
|
352
348
|
files += 1;
|
|
349
|
+
seen.add(keyOf(path, root.provider));
|
|
353
350
|
|
|
354
351
|
try {
|
|
355
352
|
if (await this.readFileInto(path, root.provider, accountOf)) {
|
|
@@ -364,31 +361,28 @@ export class UsageScanner {
|
|
|
364
361
|
roots.push({ provider: root.provider, path: root.path, status: failure === null ? 'ok' : 'failed', message: failure });
|
|
365
362
|
}
|
|
366
363
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
364
|
+
// A root that is gone for now costs a read from the start once it is back, which replaces and never adds.
|
|
365
|
+
for (const path of [...this.store.state.keys()]) {
|
|
366
|
+
if (!seen.has(path)) {
|
|
367
|
+
this.store.forget(path);
|
|
368
|
+
}
|
|
371
369
|
}
|
|
372
370
|
|
|
371
|
+
await this.store.commit();
|
|
372
|
+
|
|
373
373
|
return { at: Date.now(), files, changedFiles, durationMs: Date.now() - started, roots };
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
/* Every record the
|
|
376
|
+
/* Every record the history holds, with the copies one message left in several transcripts folded. */
|
|
377
377
|
records(): UsageRecord[] {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
for (const file of this.index.values()) {
|
|
381
|
-
all.push(...file.records, ...file.tail);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return foldByKey(all);
|
|
378
|
+
return this.store.records();
|
|
385
379
|
}
|
|
386
380
|
|
|
387
381
|
private async readFileInto(path: string, provider: UsageProvider, accountOf: AccountOf): Promise<boolean> {
|
|
388
382
|
const info = await stat(path);
|
|
389
383
|
const compressed = provider === 'codex' && path.endsWith('.jsonl.zst');
|
|
390
|
-
const key =
|
|
391
|
-
const known = this.
|
|
384
|
+
const key = keyOf(path, provider);
|
|
385
|
+
const known = this.store.state.get(key);
|
|
392
386
|
const currentParser = provider !== 'codex' || known?.parserVersion === CODEX_PARSER_VERSION;
|
|
393
387
|
|
|
394
388
|
if (
|
|
@@ -421,29 +415,30 @@ export class UsageScanner {
|
|
|
421
415
|
const hash = grown === null || prefix === null ? createHash('sha256') : prefix;
|
|
422
416
|
const from = grown?.offset ?? 0;
|
|
423
417
|
const parsed = await parseLines(readBytes(path, from, compressed), hash, provider, from, grown?.codex ?? null);
|
|
424
|
-
const
|
|
418
|
+
const usage = { provider, records: withAccount(parsed.records, accountOf), tail: withAccount(parsed.tail, accountOf) };
|
|
425
419
|
|
|
426
|
-
const
|
|
420
|
+
const state: ScanState = {
|
|
427
421
|
provider,
|
|
428
422
|
size: info.size,
|
|
429
423
|
mtimeMs: info.mtimeMs,
|
|
430
424
|
offset: parsed.offset,
|
|
431
|
-
// Folding here keeps the streaming copies of one message out of the index entirely.
|
|
432
|
-
records: foldByKey([...before, ...withAccount(parsed.records, accountOf)]),
|
|
433
|
-
tail: withAccount(parsed.tail, accountOf),
|
|
434
425
|
codex: parsed.codex,
|
|
435
426
|
head: headOf(hash),
|
|
436
427
|
...(provider === 'codex' ? { parserVersion: CODEX_PARSER_VERSION, compressed } : {})
|
|
437
428
|
};
|
|
438
429
|
|
|
439
|
-
|
|
440
|
-
|
|
430
|
+
if (grown === null) {
|
|
431
|
+
// Folding here keeps the streaming copies of one message out of the history entirely.
|
|
432
|
+
this.store.replace(key, { ...usage, records: foldByKey(usage.records) }, state);
|
|
433
|
+
} else {
|
|
434
|
+
this.store.append(key, usage, state);
|
|
435
|
+
}
|
|
441
436
|
|
|
442
437
|
return true;
|
|
443
438
|
}
|
|
444
439
|
|
|
445
440
|
/*
|
|
446
|
-
* A file rewritten as a whole replaces what the
|
|
441
|
+
* A file rewritten as a whole replaces what the history held of it, so a turn it still lists is never
|
|
447
442
|
* counted twice. One that does not parse, such as one caught halfway through a write, keeps the
|
|
448
443
|
* records it had and is read again on the next scan.
|
|
449
444
|
*/
|
|
@@ -454,18 +449,8 @@ export class UsageScanner {
|
|
|
454
449
|
return false;
|
|
455
450
|
}
|
|
456
451
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
size: info.size,
|
|
460
|
-
mtimeMs: info.mtimeMs,
|
|
461
|
-
offset: info.size,
|
|
462
|
-
records: foldByKey(withAccount(records, accountOf)),
|
|
463
|
-
tail: [],
|
|
464
|
-
codex: null,
|
|
465
|
-
head: null
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
this.dirty = true;
|
|
452
|
+
const state: ScanState = { provider, size: info.size, mtimeMs: info.mtimeMs, offset: info.size, codex: null, head: null };
|
|
453
|
+
this.store.replace(path, { provider, records: foldByKey(withAccount(records, accountOf)), tail: [] }, state);
|
|
469
454
|
|
|
470
455
|
return true;
|
|
471
456
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { appendFile, mkdir, readFile, rename, truncate } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { errorText } from '../error-text.ts';
|
|
6
|
+
import { isNotFound, removeFile, writeAtomic } from '../fs.ts';
|
|
7
|
+
import { applyEntry, decodeHistory, encodeEntry, encodeHeader, type FileUsage, type HistoryEntry } from './history-file.ts';
|
|
8
|
+
import { decodeIndex } from './index-file.ts';
|
|
9
|
+
import { foldByKey, type UsageRecord } from './record.ts';
|
|
10
|
+
import { decodeState, encodeState, type ScanState } from './scan-state.ts';
|
|
11
|
+
|
|
12
|
+
const HISTORY_FILE = 'history.jsonl';
|
|
13
|
+
const STATE_FILE = 'scan-state.json';
|
|
14
|
+
const LEGACY_INDEX_FILE = 'index.json';
|
|
15
|
+
// Below this, appended lines are not worth a rewrite of the whole history.
|
|
16
|
+
const COMPACT_FLOOR_BYTES = 1 << 20;
|
|
17
|
+
|
|
18
|
+
async function readOptional(path: string): Promise<Buffer | null> {
|
|
19
|
+
try {
|
|
20
|
+
return await readFile(path);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (isNotFound(e)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The usage of every transcript ever read, kept apart from where the scanner left each one. The state
|
|
32
|
+
* is saved after the lines it accounts for, against the history's size, so after a crash in between it
|
|
33
|
+
* does not match: it is dropped and every transcript is read from the start again, which replaces what
|
|
34
|
+
* a path counted instead of adding to it.
|
|
35
|
+
*/
|
|
36
|
+
export class UsageStore {
|
|
37
|
+
readonly usage = new Map<string, FileUsage>();
|
|
38
|
+
readonly state = new Map<string, ScanState>();
|
|
39
|
+
private readonly dir: string;
|
|
40
|
+
private pending: HistoryEntry[] = [];
|
|
41
|
+
private stateDirty = false;
|
|
42
|
+
private gen: string | null = null;
|
|
43
|
+
private base = 0;
|
|
44
|
+
private bytes = 0;
|
|
45
|
+
// Set when the history on disk may not end where this store thinks it does.
|
|
46
|
+
private rewriteNext = false;
|
|
47
|
+
|
|
48
|
+
constructor(dir: string) {
|
|
49
|
+
this.dir = dir;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async load(): Promise<void> {
|
|
53
|
+
this.usage.clear();
|
|
54
|
+
this.state.clear();
|
|
55
|
+
const history = await readOptional(join(this.dir, HISTORY_FILE));
|
|
56
|
+
|
|
57
|
+
if (history === null) {
|
|
58
|
+
await this.migrate();
|
|
59
|
+
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const decoded = decodeHistory(history);
|
|
64
|
+
|
|
65
|
+
for (const [path, usage] of decoded.usage) {
|
|
66
|
+
this.usage.set(path, usage);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (decoded.complete < history.length) {
|
|
70
|
+
await truncate(join(this.dir, HISTORY_FILE), decoded.complete);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.gen = decoded.header?.gen ?? null;
|
|
74
|
+
this.base = decoded.header?.base ?? 0;
|
|
75
|
+
this.bytes = decoded.complete;
|
|
76
|
+
this.rewriteNext = this.gen === null;
|
|
77
|
+
|
|
78
|
+
const saved = await readOptional(join(this.dir, STATE_FILE));
|
|
79
|
+
const state = saved === null ? null : decodeState(saved.toString('utf8'));
|
|
80
|
+
|
|
81
|
+
if (state !== null && state.history.gen === this.gen && state.history.bytes === this.bytes) {
|
|
82
|
+
for (const [path, file] of state.files) {
|
|
83
|
+
this.state.set(path, file);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A migration that stopped after writing the history; the index has nothing the history lacks.
|
|
88
|
+
await removeFile(join(this.dir, LEGACY_INDEX_FILE));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
records(): UsageRecord[] {
|
|
92
|
+
const all: UsageRecord[] = [];
|
|
93
|
+
|
|
94
|
+
for (const file of this.usage.values()) {
|
|
95
|
+
all.push(...file.records, ...file.tail);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return foldByKey(all);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* A transcript read from the start: what it counts now replaces what it counted. */
|
|
102
|
+
replace(path: string, usage: FileUsage, state: ScanState): void {
|
|
103
|
+
this.setState(path, state);
|
|
104
|
+
|
|
105
|
+
if (isDeepStrictEqual(this.usage.get(path), usage)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.usage.set(path, usage);
|
|
110
|
+
this.pending.push({ path, op: 'set', usage });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* A transcript read from where it was left: `added` counts on top of what it had. */
|
|
114
|
+
append(path: string, added: FileUsage, state: ScanState): void {
|
|
115
|
+
this.setState(path, state);
|
|
116
|
+
const known = this.usage.get(path);
|
|
117
|
+
|
|
118
|
+
if (added.records.length === 0 && known?.provider === added.provider && isDeepStrictEqual(known.tail, added.tail)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const entry: HistoryEntry = { path, op: 'add', usage: { ...added, records: foldByKey(added.records) } };
|
|
123
|
+
applyEntry(this.usage, entry);
|
|
124
|
+
this.pending.push(entry);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Where the scanner left a transcript that is gone; what it counted stays. */
|
|
128
|
+
forget(path: string): void {
|
|
129
|
+
if (this.state.delete(path)) {
|
|
130
|
+
this.stateDirty = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async commit(): Promise<void> {
|
|
135
|
+
if (this.pending.length === 0 && !this.stateDirty) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (this.rewriteNext || (this.pending.length > 0 && this.gen === null)) {
|
|
143
|
+
await this.rewrite();
|
|
144
|
+
} else if (this.pending.length > 0) {
|
|
145
|
+
await this.appendPending();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await this.saveState();
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// What failed to land is in memory, and a rewrite puts all of it on disk.
|
|
151
|
+
this.pending = [];
|
|
152
|
+
this.rewriteNext = true;
|
|
153
|
+
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private setState(path: string, state: ScanState): void {
|
|
159
|
+
this.state.set(path, state);
|
|
160
|
+
this.stateDirty = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async appendPending(): Promise<void> {
|
|
164
|
+
const text = this.pending.map(encodeEntry).join('');
|
|
165
|
+
this.pending = [];
|
|
166
|
+
await appendFile(join(this.dir, HISTORY_FILE), text, { mode: 0o600 });
|
|
167
|
+
this.bytes += Buffer.byteLength(text);
|
|
168
|
+
|
|
169
|
+
if (this.bytes > 2 * this.base && this.bytes - this.base > COMPACT_FLOOR_BYTES) {
|
|
170
|
+
await this.rewrite();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* The history as one `set` line per path, which drops every line a later one superseded. */
|
|
175
|
+
private async rewrite(): Promise<void> {
|
|
176
|
+
const lines = [...this.usage].map(([path, usage]) => encodeEntry({ path, op: 'set', usage }));
|
|
177
|
+
const gen = randomUUID();
|
|
178
|
+
const body = lines.join('');
|
|
179
|
+
const base = Buffer.byteLength(body);
|
|
180
|
+
const header = encodeHeader({ gen, base });
|
|
181
|
+
|
|
182
|
+
this.pending = [];
|
|
183
|
+
await writeAtomic(join(this.dir, HISTORY_FILE), header + body);
|
|
184
|
+
this.gen = gen;
|
|
185
|
+
this.base = base;
|
|
186
|
+
this.bytes = Buffer.byteLength(header + body);
|
|
187
|
+
this.rewriteNext = false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async saveState(): Promise<void> {
|
|
191
|
+
if (this.gen === null) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await writeAtomic(join(this.dir, STATE_FILE), encodeState(this.state, { gen: this.gen, bytes: this.bytes }));
|
|
196
|
+
this.stateDirty = false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* Moves an index of the single-file format into history and scan state. The history goes first:
|
|
201
|
+
* once it is there, a load treats the index as moved, so an interrupted migration finishes on the next load.
|
|
202
|
+
*/
|
|
203
|
+
private async migrate(): Promise<void> {
|
|
204
|
+
const legacy = join(this.dir, LEGACY_INDEX_FILE);
|
|
205
|
+
const text = await readOptional(legacy);
|
|
206
|
+
|
|
207
|
+
if (text === null) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const index = decodeIndex(text.toString('utf8'));
|
|
212
|
+
|
|
213
|
+
if (index === null) {
|
|
214
|
+
console.warn('The usage index would not read; it is kept beside a fresh history.');
|
|
215
|
+
await rename(legacy, `${legacy}.unreadable`);
|
|
216
|
+
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const [path, file] of index) {
|
|
221
|
+
const { records, tail, provider, ...state } = file;
|
|
222
|
+
this.usage.set(path, { provider, records, tail });
|
|
223
|
+
this.state.set(path, { provider, ...state });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await this.rewrite();
|
|
228
|
+
await this.saveState();
|
|
229
|
+
await removeFile(legacy);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
// The index stays and is moved again on the next load; this run counts from memory.
|
|
232
|
+
this.rewriteNext = true;
|
|
233
|
+
this.stateDirty = true;
|
|
234
|
+
console.warn('The usage index could not be moved to the history yet:', errorText(e));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|