diff --git a/packages/core/src/tool-output.ts b/packages/core/src/tool-output.ts index 3fba417534..976ce1ff5f 100644 --- a/packages/core/src/tool-output.ts +++ b/packages/core/src/tool-output.ts @@ -2,7 +2,7 @@ export * as ToolOutput from "./tool-output.js" import path from "path" import type { Tool } from "@opencode-ai/schema/tool" -import { Context, Duration, Effect, Layer, Schedule } from "effect" +import { Context, Duration, Effect, Layer, Option, Schedule } from "effect" import { makeGlobalNode, makeLocationNode } from "@opencode-ai/util/effect/app-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" @@ -24,15 +24,23 @@ export interface Interface { export class Service extends Context.Service()("@opencode/ToolOutput") {} const cleanup = Effect.fn("ToolOutput.cleanup")(function* (fs: FSUtil.Interface, directory: string) { - const cutoff = Identifier.timestamp(Identifier.create("tool", "ascending", Date.now() - Duration.toMillis(RETENTION))) + const cutoff = Date.now() - Duration.toMillis(RETENTION) const entries = yield* fs.readDirectory(directory).pipe( Effect.map((entries) => entries.filter((entry) => /^tool_[0-9a-f]{12}/.test(entry))), Effect.catch(() => Effect.succeed([])), ) - for (const entry of entries) { - if (Identifier.timestamp(entry) >= cutoff) continue - yield* fs.remove(path.join(directory, entry)).pipe(Effect.catch(() => Effect.void)) - } + yield* Effect.forEach( + entries, + (entry) => + Effect.gen(function* () { + const file = path.join(directory, entry) + const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined))) + const mtime = info && Option.getOrUndefined(info.mtime) + if (!mtime || mtime.getTime() >= cutoff) return + yield* fs.remove(file).pipe(Effect.catch(() => Effect.void)) + }), + { concurrency: 8, discard: true }, + ) }) const layer = Layer.effect( diff --git a/packages/core/test/tool-output.test.ts b/packages/core/test/tool-output.test.ts index 962187af64..94db5b0a5e 100644 --- a/packages/core/test/tool-output.test.ts +++ b/packages/core/test/tool-output.test.ts @@ -143,15 +143,16 @@ describe("ToolOutput", () => { ), ) - it.live("removes expired managed files", () => + it.live("uses file modification time when IDs wrap", () => withStore((output, fs, root) => Effect.gen(function* () { const directory = path.join(root, ToolOutput.DIRECTORY) - const old = path.join(directory, Identifier.create("tool", "ascending", Date.now() - 8 * 24 * 60 * 60 * 1_000)) - const recent = path.join(directory, Identifier.ascending("tool")) + const old = path.join(directory, Identifier.create("tool", "ascending", 2 ** 36 - 1)) + const recent = path.join(directory, Identifier.create("tool", "ascending", 2 ** 36 + 1)) yield* fs.ensureDir(directory) yield* fs.writeFileString(old, "old") yield* fs.writeFileString(recent, "recent") + yield* fs.utimes(old, new Date(), new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000)) yield* output.cleanup() expect(yield* fs.exists(old)).toBe(false) expect(yield* fs.exists(recent)).toBe(true)