AkurAI Build
Menu

popagent

public

Latest change 99d92c06b60aa592036b546fad6f57fa7b899194 - Stop learning from runs that ran out of steps, and sweep every checkout by AkurAI Build

import { Cron } from "croner";
import { hookAudit } from "./hook-audit";
import { longTermMemory } from "./long-term-memory";
import { appLogger } from "./observability";
import { storage } from "./storage";
import { agentWorkspaces } from "./agent-workspaces";
import { createWorkspaceSync } from "./workspace-sync";

type MaintenanceStores = {
  memory: Pick<typeof longTermMemory, "pruneEpisodes">;
  audit: Pick<typeof hookAudit, "cleanupAll">;
  observability?: { prune(): Promise<unknown> };
  /**
   * Registered checkouts drift out of date on their own. A scheduled run
   * refreshes the one workspace it uses, but a workspace no schedule touches
   * would never be refreshed at all, so maintenance sweeps all of them.
   */
  workspaces?: {
    list(): Promise<Array<{ id: string }>>;
    sync(workspaceId: string): Promise<void>;
  };
};

function retentionDays(name: string, fallback: number): number {
  const value = Number(process.env[name] ?? fallback);
  if (!Number.isInteger(value) || value < 1 || value > 365) throw new Error(`${name} must be an integer from 1 to 365`);
  return value;
}

export const observabilityRetention = {
  async prune() {
    const store = await storage.getStore("observability");
    if (!store) throw new Error("Observability storage is unavailable");
    const signalDays = retentionDays("POPAGENT_OBSERVABILITY_RETENTION_DAYS", 30);
    const feedbackDays = retentionDays("POPAGENT_OBSERVABILITY_FEEDBACK_RETENTION_DAYS", 90);
    return store.prune({
      spans: { maxAge: `${signalDays}d` },
      metrics: { maxAge: `${signalDays}d` },
      logs: { maxAge: `${signalDays}d` },
      scores: { maxAge: `${signalDays}d` },
      feedback: { maxAge: `${feedbackDays}d` },
    }, { maxBatches: 100 });
  },
};

export async function runMaintenanceOnce(stores: MaintenanceStores = {
  memory: longTermMemory,
  audit: hookAudit,
  observability: observabilityRetention,
  workspaces: {
    list: async () => agentWorkspaces.list(),
    sync: createWorkspaceSync(agentWorkspaces),
  },
}): Promise<void> {
  await Promise.all([
    stores.memory.pruneEpisodes(),
    stores.audit.cleanupAll(),
    stores.observability?.prune(),
    syncWorkspaces(stores.workspaces),
  ]);
}

/** Refreshing a checkout is best effort; one bad repository must not stop the sweep. */
async function syncWorkspaces(workspaces: MaintenanceStores["workspaces"]): Promise<void> {
  if (!workspaces) return;
  for (const workspace of await workspaces.list()) {
    await workspaces.sync(workspace.id).catch((error) =>
      appLogger().warn("maintenance.workspace_sync_failed", { workspaceId: workspace.id, error })
    );
  }
}

export function startMaintenance(): Cron {
  const job = new Cron("0 3 * * *", { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }, () => {
    void runMaintenanceOnce().catch((error) => appLogger().error("maintenance.failed", { error }));
  });
  void runMaintenanceOnce().catch((error) => appLogger().error("maintenance.failed", { error }));
  return job;
}