pi-cache-optimizer 2.8.25 → 2.8.27
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 +6 -3
- package/README.zh-CN.md +6 -3
- package/index.ts +103 -14
- package/package.json +2 -2
- package/src/cache-warming.ts +44 -3
- package/src/command-completion.ts +1 -1
- package/src/command.ts +127 -2
- package/src/compat-advice.ts +2 -2
- package/src/diagnostics.ts +14 -5
- package/src/fix-types.ts +9 -3
- package/src/kimi-base-url-fix.ts +249 -0
- package/src/kimi-code.ts +53 -0
- package/src/model-identity.ts +16 -0
- package/src/models-json-fix.ts +133 -4
- package/src/stats-store.ts +110 -50
package/src/stats-store.ts
CHANGED
|
@@ -345,9 +345,75 @@ type CachedShardRead = {
|
|
|
345
345
|
ino: number | bigint;
|
|
346
346
|
size: number;
|
|
347
347
|
mtimeMs: number;
|
|
348
|
+
/** Valid shard whose embedded instanceId matches its filename. */
|
|
348
349
|
shard: PersistedStatsShardV7 | undefined;
|
|
350
|
+
/** Schema-valid parse regardless of filename match (used by retention). */
|
|
351
|
+
parsed: PersistedStatsShardV7 | undefined;
|
|
349
352
|
};
|
|
350
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Upper bound on concurrent filesystem operations during shard scans. Startup
|
|
356
|
+
* on Windows (antivirus-scanned AppData) was dominated by thousands of
|
|
357
|
+
* serialized lstat/readFile awaits; a small pool hides per-call latency
|
|
358
|
+
* without opening an unbounded number of descriptors.
|
|
359
|
+
*/
|
|
360
|
+
export const SHARD_IO_CONCURRENCY = 16;
|
|
361
|
+
|
|
362
|
+
/** Run `worker` over `items` with at most `limit` in flight; results keep input order. */
|
|
363
|
+
export async function mapWithConcurrency<T, R>(
|
|
364
|
+
items: readonly T[],
|
|
365
|
+
limit: number,
|
|
366
|
+
worker: (item: T, index: number) => Promise<R>,
|
|
367
|
+
): Promise<R[]> {
|
|
368
|
+
const results = new Array<R>(items.length);
|
|
369
|
+
let next = 0;
|
|
370
|
+
const run = async (): Promise<void> => {
|
|
371
|
+
while (next < items.length) {
|
|
372
|
+
const index = next++;
|
|
373
|
+
results[index] = await worker(items[index] as T, index);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
const pool = Math.max(1, Math.min(limit, items.length));
|
|
377
|
+
await Promise.all(Array.from({ length: pool }, run));
|
|
378
|
+
return results;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
type ShardFileRead = {
|
|
382
|
+
info: import("node:fs").Stats;
|
|
383
|
+
/** Valid shard with matching filename instanceId. */
|
|
384
|
+
shard: PersistedStatsShardV7 | undefined;
|
|
385
|
+
/** Schema-valid parse regardless of filename match. */
|
|
386
|
+
parsed: PersistedStatsShardV7 | undefined;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Stat a shard file and return its parsed contents, re-reading only when the
|
|
391
|
+
* inode/size/mtime changed. Returns undefined for non-regular files. Throws on
|
|
392
|
+
* read/parse failure (after dropping any cache entry) so callers can apply
|
|
393
|
+
* their own policy.
|
|
394
|
+
*/
|
|
395
|
+
async function readShardFileCached(path: string, name: string): Promise<ShardFileRead | undefined> {
|
|
396
|
+
try {
|
|
397
|
+
const info = await lstat(path);
|
|
398
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
399
|
+
shardReadCache.delete(path);
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
const cached = shardReadCache.get(path);
|
|
403
|
+
if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
404
|
+
return { info, shard: cached.shard, parsed: cached.parsed };
|
|
405
|
+
}
|
|
406
|
+
const filenameInstanceId = name.slice(0, -".json".length);
|
|
407
|
+
const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
408
|
+
const shard = parsed && parsed.instanceId === filenameInstanceId ? parsed : undefined;
|
|
409
|
+
shardReadCache.set(path, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, shard, parsed });
|
|
410
|
+
return { info, shard, parsed };
|
|
411
|
+
} catch (error) {
|
|
412
|
+
shardReadCache.delete(path);
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
351
417
|
/**
|
|
352
418
|
* Parsed-shard cache keyed by path and validated by inode, size, and mtime.
|
|
353
419
|
* Writers always publish through temp-file + rename, so an updated shard gets
|
|
@@ -371,37 +437,22 @@ export async function readValidStatsShardsV7(directory: string = SHARD_FILES_DIR
|
|
|
371
437
|
if (getErrorCode(error) === "ENOENT") return [];
|
|
372
438
|
throw error;
|
|
373
439
|
}
|
|
374
|
-
const shards: PersistedStatsShardV7[] = [];
|
|
375
440
|
const seenPaths = new Set<string>();
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
seenPaths.add(path);
|
|
441
|
+
const candidates = names.filter((name) => /^[0-9a-f-]+\.json$/i.test(name));
|
|
442
|
+
for (const name of candidates) seenPaths.add(join(directory, name));
|
|
443
|
+
const reads = await mapWithConcurrency(candidates, SHARD_IO_CONCURRENCY, async (name) => {
|
|
380
444
|
try {
|
|
381
|
-
|
|
382
|
-
if (!info.isFile() || info.isSymbolicLink()) {
|
|
383
|
-
shardReadCache.delete(path);
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
const cached = shardReadCache.get(path);
|
|
387
|
-
let parsed: PersistedStatsShardV7 | undefined;
|
|
388
|
-
if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
389
|
-
parsed = cached.shard;
|
|
390
|
-
} else {
|
|
391
|
-
const filenameInstanceId = name.slice(0, -".json".length);
|
|
392
|
-
const candidate = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
393
|
-
parsed = candidate && candidate.instanceId === filenameInstanceId ? candidate : undefined;
|
|
394
|
-
shardReadCache.set(path, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, shard: parsed });
|
|
395
|
-
}
|
|
396
|
-
// Callers may mutate the aggregate they build; hand out copies so the
|
|
397
|
-
// cache stays authoritative.
|
|
398
|
-
if (parsed) shards.push(cloneShard(parsed));
|
|
445
|
+
return (await readShardFileCached(join(directory, name), name))?.shard;
|
|
399
446
|
} catch {
|
|
400
447
|
// Ignore malformed or transiently unavailable shards. Atomic writers will
|
|
401
448
|
// publish a complete replacement on the next successful update.
|
|
402
|
-
|
|
449
|
+
return undefined;
|
|
403
450
|
}
|
|
404
|
-
}
|
|
451
|
+
});
|
|
452
|
+
// Callers may mutate the aggregate they build; hand out copies so the cache
|
|
453
|
+
// stays authoritative. Preserve directory order for deterministic ties.
|
|
454
|
+
const shards: PersistedStatsShardV7[] = [];
|
|
455
|
+
for (const shard of reads) if (shard) shards.push(cloneShard(shard));
|
|
405
456
|
for (const path of Array.from(shardReadCache.keys())) {
|
|
406
457
|
if (!seenPaths.has(path) && path.startsWith(directory)) shardReadCache.delete(path);
|
|
407
458
|
}
|
|
@@ -491,67 +542,76 @@ export async function cleanupStatsShardsV7(now = Date.now(), directory: string =
|
|
|
491
542
|
return getErrorCode(error) === "ENOENT" ? 0 : Promise.reject(error);
|
|
492
543
|
}
|
|
493
544
|
const today = currentLocalDay();
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
const isTemp = name.endsWith(".tmp");
|
|
498
|
-
if (!isShard && !isTemp) continue;
|
|
545
|
+
const candidates = names.filter((name) => /^[0-9a-f-]+\.json$/i.test(name) || name.endsWith(".tmp"));
|
|
546
|
+
const outcomes = await mapWithConcurrency(candidates, SHARD_IO_CONCURRENCY, async (name): Promise<number> => {
|
|
547
|
+
const isTemp = !/^[0-9a-f-]+\.json$/i.test(name);
|
|
499
548
|
const path = join(directory, name);
|
|
500
549
|
try {
|
|
501
|
-
const info = await lstat(path);
|
|
502
|
-
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
503
550
|
if (isTemp) {
|
|
504
|
-
|
|
551
|
+
const info = await lstat(path);
|
|
552
|
+
if (!info.isFile() || info.isSymbolicLink()) return 0;
|
|
553
|
+
if (now - info.mtimeMs < SHARD_TEMP_RETENTION_MS) return 0;
|
|
505
554
|
} else {
|
|
506
|
-
|
|
507
|
-
let
|
|
555
|
+
let read: ShardFileRead | undefined;
|
|
556
|
+
let mtimeMs: number;
|
|
508
557
|
try {
|
|
509
|
-
|
|
510
|
-
|
|
558
|
+
read = await readShardFileCached(path, name);
|
|
559
|
+
if (!read) return 0;
|
|
560
|
+
mtimeMs = read.info.mtimeMs;
|
|
561
|
+
} catch (error) {
|
|
511
562
|
// A corrupt JSON shard has no usable lifecycle metadata. Apply the
|
|
512
563
|
// same mtime retention as schema-invalid shards instead of keeping
|
|
513
564
|
// it forever; read failures still leave the file untouched.
|
|
565
|
+
if (!(error instanceof SyntaxError)) return 0;
|
|
566
|
+
const info = await lstat(path);
|
|
567
|
+
if (!info.isFile() || info.isSymbolicLink()) return 0;
|
|
568
|
+
mtimeMs = info.mtimeMs;
|
|
514
569
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (
|
|
570
|
+
const parsed = read?.parsed;
|
|
571
|
+
if (parsed?.day === today) return 0;
|
|
572
|
+
const updatedAt = parsed?.lifecycle.updatedAt ?? mtimeMs;
|
|
573
|
+
if (now - updatedAt < SHARD_RETENTION_MS) return 0;
|
|
574
|
+
if (parsed?.lifecycle.state === "active" && isProcessAlive(parsed.process.pid)) return 0;
|
|
519
575
|
}
|
|
520
576
|
await unlink(path);
|
|
521
|
-
|
|
577
|
+
shardReadCache.delete(path);
|
|
578
|
+
return 1;
|
|
522
579
|
} catch {
|
|
523
580
|
// Best-effort maintenance must never block the extension.
|
|
581
|
+
return 0;
|
|
524
582
|
}
|
|
525
|
-
}
|
|
526
|
-
return
|
|
583
|
+
});
|
|
584
|
+
return outcomes.reduce((sum, value) => sum + value, 0);
|
|
527
585
|
}
|
|
528
586
|
|
|
529
|
-
|
|
587
|
+
/** Run due retention maintenance; resolves to the number of files removed (0 when skipped). */
|
|
588
|
+
export async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<number> {
|
|
530
589
|
await mkdir(SHARD_MAINTENANCE_DIR, { recursive: true });
|
|
531
590
|
try {
|
|
532
591
|
const marker = await stat(SHARD_CLEANUP_MARKER_PATH);
|
|
533
|
-
if (now - marker.mtimeMs < SHARD_CLEANUP_INTERVAL_MS) return;
|
|
592
|
+
if (now - marker.mtimeMs < SHARD_CLEANUP_INTERVAL_MS) return 0;
|
|
534
593
|
} catch {}
|
|
535
594
|
|
|
536
595
|
try {
|
|
537
596
|
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
538
597
|
} catch (error) {
|
|
539
|
-
if (getErrorCode(error) !== "EEXIST") return;
|
|
598
|
+
if (getErrorCode(error) !== "EEXIST") return 0;
|
|
540
599
|
try {
|
|
541
600
|
const lock = await stat(SHARD_CLEANUP_LOCK_PATH);
|
|
542
|
-
if (now - lock.mtimeMs <= SHARD_CLEANUP_LOCK_STALE_MS) return;
|
|
601
|
+
if (now - lock.mtimeMs <= SHARD_CLEANUP_LOCK_STALE_MS) return 0;
|
|
543
602
|
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
544
603
|
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
545
604
|
} catch {
|
|
546
|
-
return;
|
|
605
|
+
return 0;
|
|
547
606
|
}
|
|
548
607
|
}
|
|
549
608
|
|
|
550
609
|
try {
|
|
551
|
-
await cleanupStatsShardsV7(now);
|
|
610
|
+
const removed = await cleanupStatsShardsV7(now);
|
|
552
611
|
const tempPath = `${SHARD_CLEANUP_MARKER_PATH}.${process.pid}.${randomUUID()}.tmp`;
|
|
553
612
|
await writeFile(tempPath, `${now}\n`, "utf8");
|
|
554
613
|
await rename(tempPath, SHARD_CLEANUP_MARKER_PATH);
|
|
614
|
+
return removed;
|
|
555
615
|
} finally {
|
|
556
616
|
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
557
617
|
}
|