pi-cache-optimizer 2.9.0 → 2.9.2
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 +5 -1
- package/README.zh-CN.md +5 -1
- package/index.ts +107 -14
- package/package.json +1 -1
- package/src/stats-archive.ts +1350 -178
- package/src/stats-store.ts +263 -72
package/README.md
CHANGED
|
@@ -137,7 +137,11 @@ The fixture verifier reports numeric tool-order changes and confirms cache-marke
|
|
|
137
137
|
|
|
138
138
|
Current versions store stats under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory. Each loaded extension instance owns one active UUID-named daily shard and writes it through temp-file + atomic rename; at local midnight it closes the old shard and starts a new UUID without erasing session history. This prevents parent/child/parallel Pi processes from overwriting one another. Upgrading from the old v6 single-file format deletes the old local stats files and starts footer counters from zero; upstream provider caches are not affected.
|
|
139
139
|
|
|
140
|
-
Since 2.9.0, maintenance (at most every six hours, under the shared maintenance lock, in the background) folds closed past-day shards, and active ones whose process has been gone for over 24 hours, into
|
|
140
|
+
Since 2.9.0, maintenance (at most every six hours, under the shared maintenance lock, in the background) folds closed past-day shards, and active ones whose process has been gone for over 24 hours, into one archive snapshot under `pi-cache-optimizer-stats.d/archive/`, then deletes the originals. A day is archived only once it is over in every time zone (36 hours after its UTC midnight), so a Pi process running in another time zone never sees its current-day counts change; shards with usage recorded after midnight but before the day rollover stay raw. The shard directory therefore only holds roughly the last two days' shards, so startup and refresh cost no longer grows with the 60-day retention window. Archived counters keep their reset epochs, so `reset` hides them immediately, and they expire on the same 60-day window. The archive records which shards it absorbed, so a crash mid-compaction never double counts. Older versions ignore the archive: if you downgrade or run an older version alongside, its `session` footer shows only history that has not been archived yet; current-day `total` and `process` counters are unaffected.
|
|
141
|
+
|
|
142
|
+
After 2.9.0, each snapshot is an immutable, compact generation file `archive/archive.<generation>.json` (about a fifth of the size of 2.9.0's single `archive.json`). A compaction publishes generation N+1 only if nobody else did since it read generation N (an exclusive hard link, atomic on Windows and POSIX), and deletes raw shards only when its generation is the newest; the maintenance lock is also refreshed while a pass runs. A process that stalls past the stale-lock window (for example a sleeping laptop) therefore can never overwrite a newer archive and lose counters. Readers use the newest generation, and the previous one is kept until the next publish. A 2.9.0 `archive.json` is still read, and the next compaction migrates it and replaces it with a directory of the same name. A 2.9.0 process that is still running then treats the archive as unreadable: it no longer compacts or deletes anything, and its `session` footer shows only unarchived history, as above. If a 2.9.0 process ever writes `archive.json` again after the migration (only possible if that directory is removed), the file is never counted, to rule out double counting; it is set aside as `archive.legacy-<time>.json`, and the shards that process archived into it are missing from the footer. After 2.9.1, the migration first moves `archive.json` aside (to `archive.migrating-<id>.json`), puts the directory in its place, and checks that the moved file is the one it read before linking the first generation, so a 2.9.0 process that republished the file in the meantime is migrated by the next pass instead of being set aside; a migration interrupted at that point is finished from the placeholder.
|
|
143
|
+
|
|
144
|
+
After 2.9.1, an archive that cannot be read heals itself instead of disabling compaction: readers skip an unreadable (damaged or truncated) generation for the previous one, which never double counts because every generation covers exactly the raw shards it lists as absorbed; only what the unreadable generation had already archived and deleted is missing. The next compaction publishes above it and keeps it as `archive.<generation>.corrupt-<time>.json`, with a warning. An unreadable 2.9.0 `archive.json` is set aside as `archive.legacy-<time>.json` and archiving starts over from the raw shards. An archive written in a newer format is never skipped or overwritten. On a filesystem without hard links (FAT/exFAT, some network drives), a generation is created exclusively in place and flushed, and it only counts once it is complete; a compaction that fails with an error is retried after about ten minutes instead of six hours. Transient read errors (for example a Windows scanner holding a file) are retried briefly, and if they persist the footer keeps its previous consistent snapshot for that refresh.
|
|
141
145
|
|
|
142
146
|
Response stats stay with the dispatched catalog/request-local model even if you switch to another model on the same provider while waiting. Legacy response aliases use the request snapshot, not the new selection; the footer continues showing the selected direct model. Old corrupt JSON/schema shards expire by file modification time after two months; young corrupt files, valid current-day shards, live old instances, and symlink targets remain protected.
|
|
143
147
|
|
package/README.zh-CN.md
CHANGED
|
@@ -137,7 +137,11 @@ bun .trellis/tasks/09-03-context-epoch-tool-ordering/verify.ts
|
|
|
137
137
|
|
|
138
138
|
当前版本把统计保存在 Pi agent 目录的 `pi-cache-optimizer-stats.d/shards/` 下。每个已加载的 extension instance 独占一个当天的活跃 UUID shard,并通过临时文件 + 原子 rename 写入;本地日期切换时关闭旧 shard、创建新的 UUID,保留 session 历史,因此父/子/并行 Pi 进程不会互相覆盖。旧 v6 单文件统计在升级时直接删除,本地 footer 计数从零开始;不会影响上游 provider 的实际缓存。
|
|
139
139
|
|
|
140
|
-
从 2.9.0 起,维护任务会把已关闭的往日 shard,以及进程已退出超过 24 小时的活跃 shard,合并进 `pi-cache-optimizer-stats.d/archive
|
|
140
|
+
从 2.9.0 起,维护任务会把已关闭的往日 shard,以及进程已退出超过 24 小时的活跃 shard,合并进 `pi-cache-optimizer-stats.d/archive/` 下的一份归档快照,再删除原文件。维护最多每 6 小时一次,在共享维护锁下后台运行。某一天只有在所有时区都已结束后(该日 UTC 零点之后 36 小时)才会被归档,所以运行在其他时区的 Pi 进程看到的当天计数不会因此变化;午夜之后、日切换之前记下的用量所在的 shard 保持原样不归档。这样 shard 目录里基本只剩最近两天的 shard,启动和刷新的开销不再随 60 天保留期增长。归档计数保留 reset epoch,`reset` 立即生效,并按同样的 60 天窗口过期。归档会记录已吸收的 shard,所以合并中途崩溃也不会重复计数。旧版本不会读取归档:如果降级或与旧版本同时运行,旧版的 `session` footer 只显示尚未归档的历史;当天的 `total` 和 `process` 计数不受影响。
|
|
141
|
+
|
|
142
|
+
2.9.0 之后的版本把每份快照保存为不可变的紧凑代号文件 `archive/archive.<generation>.json`,大小约为 2.9.0 单个 `archive.json` 的五分之一。压缩读到第 N 代后,只有在期间没有别人发布过的情况下才能发布第 N+1 代(用独占的硬链接实现,在 Windows 和 POSIX 上都是原子的),而且只有自己发布的代号仍是最新时才删除原始 shard;压缩运行期间还会刷新维护锁的时间戳。因此,一个停顿超过锁过期时间的进程(比如笔记本休眠)醒来后,再也不会覆盖更新的归档、造成计数丢失。读取端使用最新的代号,上一代保留到下一次发布。2.9.0 的 `archive.json` 仍然会被读取,下一次压缩会迁移它,并用一个同名目录取而代之。仍在运行的 2.9.0 进程随后会把归档视为不可读:它不再压缩或删除任何文件,它的 `session` footer 只显示尚未归档的历史,与上面所说一致。如果迁移之后 2.9.0 又写出了 `archive.json`(只有在这个目录被删除时才可能),为了杜绝重复计数,这个文件永远不会被计入,而是改名为 `archive.legacy-<time>.json` 留存,那个进程归档进去的 shard 将不会出现在 footer 里。2.9.1 之后的版本中,迁移会先把 `archive.json` 移到一旁(`archive.migrating-<id>.json`),用目录占住原位置,并在链接第一代之前核对移走的文件就是自己读到的那一份。因此,如果期间有 2.9.0 进程重新写了这个文件,下一次压缩会迁移它,而不是把它隔离;如果迁移在这一步被中断,之后会从这个占位文件继续完成。
|
|
143
|
+
|
|
144
|
+
2.9.1 之后的版本中,归档读不出来时会自行恢复,而不是永久停止压缩:读取端会跳过损坏或被截断的代号,改用上一代。每一代只覆盖它记录为已吸收的原始 shard,所以回退不会重复计数,缺少的只是那一代已经归档并删除的部分。下一次压缩会在它之上发布新的一代,并把它保留为 `archive.<generation>.corrupt-<time>.json`,同时给出警告。读不出的 2.9.0 `archive.json` 会被改名为 `archive.legacy-<time>.json`,归档从原始 shard 重新开始。更新版本格式写出的归档永远不会被跳过或覆盖。在不支持硬链接的文件系统(FAT/exFAT、部分网络盘)上,新的一代以独占方式直接创建并落盘,写完整之后才会被计入。压缩出错时,约 10 分钟后重试,而不是等 6 小时。瞬时读错误(比如 Windows 上杀毒软件占用文件)会短暂重试;如果一直失败,这次刷新沿用上一次一致的快照。
|
|
141
145
|
|
|
142
146
|
即使等待响应时切换到同一 provider 的另一个模型,响应统计仍归属于实际请求的 catalog/request-local 模型。旧 transport 的响应别名只根据请求快照处理,不使用新选择的模型;footer 继续显示当前选择的 direct 模型。损坏 JSON/schema 的旧 shard 按文件修改时间在两个月后清理;较新的损坏文件、有效的当天 shard、仍存活的旧 instance 和 symlink 目标均受保护。
|
|
143
147
|
|
package/index.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { atomicCreateTextFileNoReplace, atomicReplaceTextFilePreservingMode, ato
|
|
|
13
13
|
import { getAssistantMessageModelTokenValues, getModelIdNameTokenValues, isAdaptiveGenerationModel, isKimiCodingAdaptiveModel } from "./src/model-detect.ts";
|
|
14
14
|
import { findLastExactModelDefinition, mergeCacheCompat, resolveEffectiveCompatFromConfig } from "./src/compat-config.ts";
|
|
15
15
|
import { analyzeModelsJsonForMissingEntry, applyModelsJsonFixTransaction, chooseFixPlacement, composeFixInsertion, composeMissingEntryInsertion, composeModelOverrideInsertion, composeModelsJsonReceiptRollback, composeProviderAffinityInsertion, createModelsJsonFixReceipt, createRollbackBackupPath, decideFixPlacement, formatCompatKeysForInsertion, hasExplicitLongRetentionOptInFromConfig, hasReceiptReasoningProtocolChange, isActionableModelsJsonFixReceipt, locateModelInJsonc, markModelsJsonFixReceiptRolledBack, parseModelsJsonFixReceipt, prepareModelsJsonRollback, readModelsJsonFixReceipt, readModelsJsonFixReceiptSnapshot, receiptBackupPath, resolveExplicitCompatValue, selfCheckFix, selfCheckMissingEntryInsertion, validateModelsJsonRollback, writeModelsJsonFixReceipt } from "./src/models-json-fix.ts";
|
|
16
|
-
import { SHARD_ARCHIVE_PATH, compactStatsShardsV7, maybeRunStatsMaintenance, readStatsArchive } from "./src/stats-archive.ts";
|
|
17
|
-
import { type CacheProviderId, type CacheStats, LEGACY_STATE_FILE_PATH, acquireStatsMaintenanceLock, holdsStatsMaintenanceLock, releaseStatsMaintenanceLock, type PersistedRoutedModelRef, type PersistedStatsShardV7, SHARD_FILES_DIR, SHARD_GLOBAL_EPOCH_PATH, SHARD_RETENTION_MS, SHARD_STATE_DIR, STATE_FILE_PATH, type ShardAggregate, type UsageSnapshot, addUsageToCacheStats, advanceGlobalStatsEpoch, advanceModelStatsEpoch, aggregateStatsShardsV7, cleanupStatsShardsV7, cloneCacheStats, mapWithConcurrency, currentLocalDay, emptyAllCacheStats, emptyCacheStats, initialEpoch, loadStatsShardAggregateV7, modelEpochPath, parseCacheStats, parsePersistedRoutedModelRef, parsePersistedStatsShardV7, readGlobalStatsEpoch, readModelStatsEpoch, readValidStatsShardsV7, removeLegacyStatsFiles, writeStatsShardV7 } from "./src/stats-store.ts";
|
|
16
|
+
import { ARCHIVE_ABANDONED_ACTIVE_MS, SHARD_ARCHIVE_PATH, isArchivableShard, type StatsSnapshot, aggregateStatsWithArchiveV7, compactStatsShardsV7, encodeStatsArchiveV2, listStatsArchiveGenerations, maybeRunStatsMaintenance, parseStatsArchiveV2, readStatsArchive, readStatsArchiveDetailed, readStatsArchiveView, readStatsSnapshot, setStatsArchiveTestHooks, StatsArchiveView, statsArchiveGenerationPath } from "./src/stats-archive.ts";
|
|
17
|
+
import { type CacheProviderId, type CacheStats, LEGACY_STATE_FILE_PATH, acquireStatsMaintenanceLock, isMaintenanceMarkerDue, renameWithRetry, setStatsFsTestHooks, holdsStatsMaintenanceLock, releaseStatsMaintenanceLock, touchStatsMaintenanceLock, type PersistedRoutedModelRef, type PersistedStatsShardV7, SHARD_FILES_DIR, SHARD_GLOBAL_EPOCH_PATH, SHARD_RETENTION_MS, SHARD_STATE_DIR, STATE_FILE_PATH, type ShardAggregate, type UsageSnapshot, addUsageToCacheStats, advanceGlobalStatsEpoch, advanceModelStatsEpoch, aggregateStatsShardsV7, cleanupStatsShardsV7, cloneCacheStats, mapWithConcurrency, currentLocalDay, emptyAllCacheStats, emptyCacheStats, initialEpoch, loadStatsShardAggregateV7, modelEpochPath, parseCacheStats, parsePersistedRoutedModelRef, parsePersistedStatsShardV7, readGlobalStatsEpoch, readModelStatsEpoch, readValidStatsShardsV7, removeLegacyStatsFiles, writeStatsShardV7 } from "./src/stats-store.ts";
|
|
18
18
|
import { PI_VIRTUAL_MODEL_API, getCompat, isAnthropicMessagesApi, isKimiCodingEmptySignatureModel, isKnownThirdPartyOpenAIEndpoint, isMistralConversationsApi, isNativeVirtualModel, isOfficialOpenAIBaseUrl, isOpenAICompatibleApi, isOpenAICompatibleProxyApi, isOpenAIFamilyAssistantMessage, isOpenAIFamilyModel, isOpenAIFamilyToken, isPiBuiltInLlamaCppModel, isResponsesPromptRewriteBypassApi, isRoutedFallbackModel, isValidModelsConfigForEffectiveCompat, modelKey, readEffectiveCompatConfig } from "./src/model-identity.ts";
|
|
19
19
|
import { CONFIG_FILE_PATH, VIRTUAL_REWRITE_ENV, FOOTER_MODE_ENV, TOOL_ORDER_ENV, featureEnabled, footerStatsMode, isEnabledEnv, isToolOrderEnabled, parseFooterStatsMode, parsePersistedCacheOptimizerConfig, readPersistedCacheOptimizerConfig, resolveFooterStatsMode, runtimeOptimizerEnabled, setPersistedCacheOptimizerConfig, shouldInjectOpenAIPromptCacheKey, writePersistedCacheOptimizerConfig, writePersistedFeature, writePersistedFooterMode } from "./src/config.ts";
|
|
20
20
|
import { decideZeroPriceWarming, readCacheWarmingMode } from "./src/cache-warming.ts";
|
|
@@ -327,6 +327,21 @@ export const __internals_for_tests = {
|
|
|
327
327
|
compactStatsShardsV7,
|
|
328
328
|
maybeRunStatsMaintenance,
|
|
329
329
|
readStatsArchive,
|
|
330
|
+
readStatsArchiveView,
|
|
331
|
+
readStatsArchiveDetailed,
|
|
332
|
+
readStatsSnapshot,
|
|
333
|
+
setStatsArchiveTestHooks,
|
|
334
|
+
setStatsFsTestHooks,
|
|
335
|
+
renameWithRetry,
|
|
336
|
+
isArchivableShard,
|
|
337
|
+
isMaintenanceMarkerDue,
|
|
338
|
+
aggregateStatsWithArchiveV7,
|
|
339
|
+
encodeStatsArchiveV2,
|
|
340
|
+
parseStatsArchiveV2,
|
|
341
|
+
listStatsArchiveGenerations,
|
|
342
|
+
statsArchiveGenerationPath,
|
|
343
|
+
StatsArchiveView,
|
|
344
|
+
touchStatsMaintenanceLock,
|
|
330
345
|
acquireStatsMaintenanceLock,
|
|
331
346
|
holdsStatsMaintenanceLock,
|
|
332
347
|
releaseStatsMaintenanceLock,
|
|
@@ -607,14 +622,82 @@ export default function (pi: ExtensionAPI) {
|
|
|
607
622
|
recentSamplesByModelKey.clear();
|
|
608
623
|
}
|
|
609
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Shards this instance persisted under its current id that compaction
|
|
627
|
+
* could have absorbed: the latest one, plus each one that was followed by
|
|
628
|
+
* a gap of at least ARCHIVE_ABANDONED_ACTIVE_MS (compaction only takes an
|
|
629
|
+
* active shard nobody wrote for that long). Used when this live process's
|
|
630
|
+
* own shard was absorbed anyway (see adoptAbsorbedOwnShard).
|
|
631
|
+
*/
|
|
632
|
+
let persistedOwnShards: PersistedStatsShardV7[] = [];
|
|
633
|
+
function recordPersistedOwnShard(shard: PersistedStatsShardV7): void {
|
|
634
|
+
if (shard.instanceId !== instanceId) return;
|
|
635
|
+
const last = persistedOwnShards.at(-1);
|
|
636
|
+
if (last && shard.lifecycle.updatedAt - last.lifecycle.updatedAt < ARCHIVE_ABANDONED_ACTIVE_MS) persistedOwnShards.pop();
|
|
637
|
+
persistedOwnShards.push(shard);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Compaction absorbed this live instance's active shard: its process looked
|
|
642
|
+
* dead (a PID namespace the compactor cannot see: containers, WSL, a shared
|
|
643
|
+
* NFS agent dir) and it had been idle for a day. The archive now counts the
|
|
644
|
+
* version it read, which this instance must neither count again (as its
|
|
645
|
+
* in-memory shard) nor keep writing to (a write to an absorbed id is
|
|
646
|
+
* ignored and then deleted, losing the usage since). Continue under a new
|
|
647
|
+
* id holding only what the archive does not have: the in-memory counters
|
|
648
|
+
* minus the absorbed version, which is the newest persisted one dated at
|
|
649
|
+
* least ARCHIVE_ABANDONED_ACTIVE_MS before the absorption.
|
|
650
|
+
*/
|
|
651
|
+
function adoptAbsorbedOwnShard(absorbedAt: number): void {
|
|
652
|
+
const candidates = persistedOwnShards.filter((shard) => shard.lifecycle.updatedAt <= absorbedAt - ARCHIVE_ABANDONED_ACTIVE_MS);
|
|
653
|
+
// Not identifiable (clock skew between the processes): assume the
|
|
654
|
+
// archive holds everything, so nothing is ever counted twice.
|
|
655
|
+
const absorbed = candidates.at(-1) ?? buildCurrentStatsShard();
|
|
656
|
+
const remaining: Record<string, CacheStats> = {};
|
|
657
|
+
for (const [key, stats] of Object.entries(cacheStatsProcessByModel)) {
|
|
658
|
+
const entry = absorbed.globalEpoch === currentGlobalEpoch ? absorbed.models[key] : undefined;
|
|
659
|
+
const sameEpoch = entry && entry.modelEpoch === (modelEpochByKey.get(key) ?? initialEpoch(`model:${key}`));
|
|
660
|
+
const delta = cloneCacheStats(stats);
|
|
661
|
+
if (sameEpoch) {
|
|
662
|
+
delta.totalRequests = Math.max(0, stats.totalRequests - entry.stats.totalRequests);
|
|
663
|
+
delta.hitRequests = Math.max(0, stats.hitRequests - entry.stats.hitRequests);
|
|
664
|
+
delta.cachedInputTokens = Math.max(0, stats.cachedInputTokens - entry.stats.cachedInputTokens);
|
|
665
|
+
delta.cacheWriteInputTokens = Math.max(0, stats.cacheWriteInputTokens - entry.stats.cacheWriteInputTokens);
|
|
666
|
+
delta.totalInputTokens = Math.max(0, stats.totalInputTokens - entry.stats.totalInputTokens);
|
|
667
|
+
delta.hitRequests = Math.min(delta.hitRequests, delta.totalRequests);
|
|
668
|
+
}
|
|
669
|
+
if (delta.totalRequests > 0 || delta.totalInputTokens > 0 || delta.cachedInputTokens > 0 || delta.cacheWriteInputTokens > 0) {
|
|
670
|
+
remaining[key] = delta;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
cacheStatsProcessByModel = remaining;
|
|
674
|
+
instanceId = randomUUID();
|
|
675
|
+
instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
|
|
676
|
+
shardCreatedAt = Date.now();
|
|
677
|
+
persistedOwnShards = [];
|
|
678
|
+
lastStatusText = undefined;
|
|
679
|
+
// The new id keeps the old shard's day; the next status update rolls it
|
|
680
|
+
// over as usual. Persist in the background (the aggregate below already
|
|
681
|
+
// counts the in-memory shard).
|
|
682
|
+
void persistCacheStats();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let lastStatsSnapshot: StatsSnapshot | undefined;
|
|
686
|
+
|
|
610
687
|
async function refreshShardAggregate(): Promise<ShardAggregate> {
|
|
611
|
-
// Order matters: compaction publishes the archive
|
|
612
|
-
// shards it absorbed, so reading the directory first
|
|
613
|
-
// second always sees each absorbed shard in at least one
|
|
614
|
-
// them concurrently could pair an old archive with a
|
|
615
|
-
// and drop those counters (and the last routed model)
|
|
616
|
-
|
|
617
|
-
|
|
688
|
+
// Order matters (readStatsSnapshot): compaction publishes the archive
|
|
689
|
+
// before deleting the shards it absorbed, so reading the directory first
|
|
690
|
+
// and the archive second always sees each absorbed shard in at least one
|
|
691
|
+
// of them. Reading them concurrently could pair an old archive with a
|
|
692
|
+
// post-delete listing and drop those counters (and the last routed model)
|
|
693
|
+
// for this refresh. A persistent transient archive read error reuses the
|
|
694
|
+
// previous consistent snapshot instead of pairing an old archive with a
|
|
695
|
+
// new listing.
|
|
696
|
+
const snapshot = await readStatsSnapshot(lastStatsSnapshot);
|
|
697
|
+
lastStatsSnapshot = snapshot;
|
|
698
|
+
const { rawShards, archive } = snapshot;
|
|
699
|
+
const ownAbsorbedAt = archive.archive.absorbed[instanceId];
|
|
700
|
+
if (ownAbsorbedAt !== undefined) adoptAbsorbedOwnShard(ownAbsorbedAt);
|
|
618
701
|
// Shards folded into the archive are counted there. Ignore their raw file
|
|
619
702
|
// (a crash may leave it behind) and this process's in-memory retired copy.
|
|
620
703
|
for (const id of archive.absorbedIds) retiredShards.delete(id);
|
|
@@ -622,12 +705,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
622
705
|
for (const shard of persistedShards) retiredShards.delete(shard.instanceId);
|
|
623
706
|
const shards = persistedShards.filter((shard) => shard.instanceId !== instanceId);
|
|
624
707
|
shards.push(...retiredShards.values());
|
|
625
|
-
|
|
626
|
-
if (currentSessionHashSet) shards.push(buildCurrentStatsShard());
|
|
708
|
+
const trailing = currentSessionHashSet ? [buildCurrentStatsShard()] : [];
|
|
627
709
|
// Keep lifetime session footer data separate from daily totals/commands.
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
|
|
710
|
+
// Both equal aggregating [...shards, ...archive.shards, ...trailing]
|
|
711
|
+
// (issue #28 review H): the current day only sees archive entries of that
|
|
712
|
+
// day (normally none: the archive holds past days), and the all-days view
|
|
713
|
+
// merges the archive's pre-reduced per-(session, model) entries instead
|
|
714
|
+
// of re-aggregating every archived bucket. The all-days view is only read
|
|
715
|
+
// for the current session (cacheStatsByModel lookups and the last routed
|
|
716
|
+
// model), so it is built for that session alone; rebuilding every
|
|
717
|
+
// archived session's entries cost more than the rest of the refresh.
|
|
718
|
+
const aggregate = await aggregateStatsWithArchiveV7(shards, archive, trailing, currentLocalDay());
|
|
719
|
+
const sessionAggregate = await aggregateStatsWithArchiveV7(
|
|
720
|
+
shards, archive, trailing, null, new Set(currentSessionHashSet ? [currentSessionHash] : []),
|
|
721
|
+
);
|
|
631
722
|
|
|
632
723
|
// A reset from another process can advance epochs while this instance is
|
|
633
724
|
// idle. Adopt those epochs during every aggregate refresh, not only when a
|
|
@@ -796,6 +887,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
796
887
|
return enqueuePersist(async () => {
|
|
797
888
|
try {
|
|
798
889
|
await writeStatsShardV7(path, shard);
|
|
890
|
+
if (path === instanceShardPath) recordPersistedOwnShard(shard);
|
|
799
891
|
} catch (error) {
|
|
800
892
|
console.warn(`${LOG_PREFIX}: failed to persist cache stats shard`, error);
|
|
801
893
|
if (!persistenceWarningShown) {
|
|
@@ -853,6 +945,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
853
945
|
retiredShards.set(instanceId, buildCurrentStatsShard("closed"));
|
|
854
946
|
instanceId = randomUUID();
|
|
855
947
|
instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
|
|
948
|
+
persistedOwnShards = [];
|
|
856
949
|
shardDay = day;
|
|
857
950
|
shardCreatedAt = Date.now();
|
|
858
951
|
cacheStatsProcessByModel = {};
|
package/package.json
CHANGED