authorgravatar for lucenz@proton.meSimone <lucenz@proton.me> 2026-09-01 00:59:39+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-31 18:59:39-04:00
log6d15c5bbc3f3c331a7466bb94c17413a35f7b659
treeecbd537a5103ee3cc17b4d4b8f3d4bf6c5caf6f0
parentf47e74004af232f0e3df8dc10093601d1c2c3ea3
signature Signed by PGP key B5690EEEBB952194

fix(server): preserve usage cache outside walked roots (#8540)

Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com>

2 files changed, 26 insertions(+), 1 deletions(-)

apps/server/src/usage/usageScanCache.test.ts+14
...@@ -193,6 +193,20 @@ describe("pruneScanCache with an unwalked root", () => {...@@ -193,6 +193,20 @@ describe("pruneScanCache with an unwalked root", () => {
193 expect(removed).toBe(0);193 expect(removed).toBe(0);
194 expect(cache.size).toBe(1);194 expect(cache.size).toBe(1);
195 });195 });
196
197 it("keeps entries under a sibling path that only shares the walked root prefix", () => {
198 const cache = cacheWith([["/claude/projects-copy/a.jsonl", 5000, [record()]]]);
199
200 const removed = pruneScanCache(cache, {
201 livePaths: new Set(),
202 walkedRoots: ["/claude/projects"],
203 windowStartMs: 4000,
204 retentionCutoffMs: 1000,
205 });
206
207 expect(removed).toBe(0);
208 expect(cache.size).toBe(1);
209 });
196});210});
197211
198describe("dedupeWithinFile", () => {212describe("dedupeWithinFile", () => {
apps/server/src/usage/usageScanCache.ts+12-1
...@@ -14,6 +14,9 @@...@@ -14,6 +14,9 @@
14 *14 *
15 * @module usageScanCache15 * @module usageScanCache
16 */16 */
17// @effect-diagnostics nodeBuiltinImport:off
18import * as NodePath from "node:path";
19
17import type { UsageProviderKind } from "@t3tools/contracts";20import type { UsageProviderKind } from "@t3tools/contracts";
1821
19import type { UsageRecord } from "./usageTranscripts.ts";22import type { UsageRecord } from "./usageTranscripts.ts";
...@@ -229,7 +232,15 @@ export function pruneScanCache(cache: ScanCache, options: PruneOptions): number...@@ -229,7 +232,15 @@ export function pruneScanCache(cache: ScanCache, options: PruneOptions): number
229 let removed = 0;232 let removed = 0;
230 for (const [path, entry] of cache) {233 for (const [path, entry] of cache) {
231 const agedOut = entry.mtimeMs < options.retentionCutoffMs;234 const agedOut = entry.mtimeMs < options.retentionCutoffMs;
232 const underWalkedRoot = options.walkedRoots.some((root) => path.startsWith(root));235 const underWalkedRoot = options.walkedRoots.some((root) => {
236 const relative = NodePath.relative(root, path);
237 return (
238 relative === "" ||
239 (relative !== ".." &&
240 !relative.startsWith(`..${NodePath.sep}`) &&
241 !NodePath.isAbsolute(relative))
242 );
243 });
233 const deleted =244 const deleted =
234 underWalkedRoot && entry.mtimeMs >= options.windowStartMs && !options.livePaths.has(path);245 underWalkedRoot && entry.mtimeMs >= options.windowStartMs && !options.livePaths.has(path);
235 if (agedOut || deleted) {246 if (agedOut || deleted) {