From bb1d602c2960948fda1c23fc359066609cddd9d3 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 15:16:57 -0600 Subject: [PATCH] core: add schedule cleanup module --- packages/opencode/src/project/bootstrap.ts | 4 ++ packages/opencode/src/scheduler/index.ts | 50 ++++++++++++++++++++++ packages/opencode/src/snapshot/index.ts | 36 ++++++++++++++++ packages/opencode/src/tool/truncation.ts | 14 ++++-- 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/src/scheduler/index.ts diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 56fe4d13e6..efdcaba990 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,6 +11,8 @@ import { Instance } from "./instance" import { Vcs } from "./vcs" import { Log } from "@/util/log" import { ShareNext } from "@/share/share-next" +import { Snapshot } from "../snapshot" +import { Truncate } from "../tool/truncation" export async function InstanceBootstrap() { Log.Default.info("bootstrapping", { directory: Instance.directory }) @@ -22,6 +24,8 @@ export async function InstanceBootstrap() { FileWatcher.init() File.init() Vcs.init() + Snapshot.init() + Truncate.init() Bus.subscribe(Command.Event.Executed, async (payload) => { if (payload.properties.name === Command.Default.INIT) { diff --git a/packages/opencode/src/scheduler/index.ts b/packages/opencode/src/scheduler/index.ts new file mode 100644 index 0000000000..e1fea455da --- /dev/null +++ b/packages/opencode/src/scheduler/index.ts @@ -0,0 +1,50 @@ +import { Instance } from "../project/instance" +import { Log } from "../util/log" + +export namespace Scheduler { + const log = Log.create({ service: "scheduler" }) + + export type Task = { + id: string + interval: number + run: () => Promise + } + + type Timer = ReturnType + + const state = Instance.state( + () => { + const tasks = new Map() + const timers = new Map() + return { tasks, timers } + }, + async (entry) => { + for (const timer of entry.timers.values()) { + clearInterval(timer) + } + entry.tasks.clear() + entry.timers.clear() + }, + ) + + export function register(task: Task) { + const entry = state() + const current = entry.timers.get(task.id) + if (current) clearInterval(current) + + entry.tasks.set(task.id, task) + void run(task) + const timer = setInterval(() => { + void run(task) + }, task.interval) + timer.unref() + entry.timers.set(task.id, timer) + } + + async function run(task: Task) { + log.info("run", { id: task.id }) + await task.run().catch((error) => { + log.error("run failed", { id: task.id, error }) + }) + } +} diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 69f2abc790..bac807d29c 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -6,9 +6,45 @@ import { Global } from "../global" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" +import { Scheduler } from "../scheduler" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) + const hour = 60 * 60 * 1000 + const prune = "7.days" + + export function init() { + Scheduler.register({ + id: "snapshot.cleanup", + interval: hour, + run: cleanup, + }) + } + + export async function cleanup() { + if (Instance.project.vcs !== "git") return + const cfg = await Config.get() + if (cfg.snapshot === false) return + const git = gitdir() + const exists = await fs + .stat(git) + .then(() => true) + .catch(() => false) + if (!exists) return + const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} gc --prune=${prune}` + .quiet() + .cwd(Instance.directory) + .nothrow() + if (result.exitCode !== 0) { + log.warn("cleanup failed", { + exitCode: result.exitCode, + stderr: result.stderr.toString(), + stdout: result.stdout.toString(), + }) + return + } + log.info("cleanup", { prune }) + } export async function track() { if (Instance.project.vcs !== "git") return diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 4172b6447e..16aea14e7c 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -2,9 +2,9 @@ import fs from "fs/promises" import path from "path" import { Global } from "../global" import { Identifier } from "../id/id" -import { lazy } from "../util/lazy" import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" +import { Scheduler } from "../scheduler" export namespace Truncate { export const MAX_LINES = 2000 @@ -12,6 +12,7 @@ export namespace Truncate { export const DIR = path.join(Global.Path.data, "tool-output") export const GLOB = path.join(DIR, "*") const RETENTION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days + const HOUR_MS = 60 * 60 * 1000 export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string } @@ -21,6 +22,14 @@ export namespace Truncate { direction?: "head" | "tail" } + export function init() { + Scheduler.register({ + id: "tool.truncation.cleanup", + interval: HOUR_MS, + run: cleanup, + }) + } + export async function cleanup() { const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS)) const glob = new Bun.Glob("tool_*") @@ -31,8 +40,6 @@ export namespace Truncate { } } - const init = lazy(cleanup) - function hasTaskTool(agent?: Agent.Info): boolean { if (!agent?.permission) return false const rule = PermissionNext.evaluate("task", "*", agent.permission) @@ -81,7 +88,6 @@ export namespace Truncate { const unit = hitBytes ? "bytes" : "lines" const preview = out.join("\n") - await init() const id = Identifier.ascending("tool") const filepath = path.join(DIR, id) await Bun.write(Bun.file(filepath), text)