Menu
popagent
publicLatest change 682410a285bb0a73662cc4650c92c31c918e1cc9 - Add idle autonomous improvement workflow by Ólafur Búi Ólafsson
import { Cron } from "croner";
import type { AutonomySettings, AutonomySettingsInput } from "./api-types";
import { initializeAgentRoleStorage } from "./agent-settings";
import { retryableInit } from "./retryable-init";
import { storage } from "./storage";
const AUTONOMY_SETTINGS_COLUMNS = `
enabled,
reflection_interval_ms AS "reflectionIntervalMs",
batch_size AS "batchSize",
max_attempts AS "maxAttempts",
auto_apply_strategies AS "autoApplyStrategies",
auto_create_skills AS "autoCreateSkills",
self_update_enabled AS "selfUpdateEnabled",
self_update_cron AS "selfUpdateCron",
idle_improvement_enabled AS "idleImprovementEnabled",
idle_deployment_enabled AS "idleDeploymentEnabled",
idle_grace_ms AS "idleGraceMs",
idle_workspace_ids AS "idleWorkspaceIds",
last_user_activity_at::text AS "lastUserActivityAt",
updated_at::text AS "updatedAt"
`;
const initializeEvolutionStorageOnce = retryableInit(async () => {
await initializeAgentRoleStorage();
const schema = await Bun.file(new URL("./evolution.sql", import.meta.url)).text();
await storage.db.none(schema);
});
export function initializeEvolutionStorage(): Promise<void> {
return initializeEvolutionStorageOnce();
}
function validate(input: AutonomySettingsInput): AutonomySettingsInput {
if (!Number.isInteger(input.reflectionIntervalMs)
|| input.reflectionIntervalMs < 60_000
|| input.reflectionIntervalMs > 86_400_000) {
throw new RangeError("Reflection interval must be between 60000 and 86400000 milliseconds");
}
if (!Number.isInteger(input.batchSize) || input.batchSize < 1 || input.batchSize > 100) {
throw new RangeError("Evolution batch size must be between 1 and 100");
}
if (!Number.isInteger(input.maxAttempts) || input.maxAttempts < 1 || input.maxAttempts > 10) {
throw new RangeError("Evolution max attempts must be between 1 and 10");
}
if (!Number.isInteger(input.idleGraceMs) || input.idleGraceMs < 60_000 || input.idleGraceMs > 86_400_000) {
throw new RangeError("Idle grace must be between 60000 and 86400000 milliseconds");
}
if (new Set(input.idleWorkspaceIds).size !== input.idleWorkspaceIds.length
|| input.idleWorkspaceIds.some((id) => !id.trim() || id.length > 256)) {
throw new RangeError("Idle workspace IDs must be unique non-empty values");
}
const selfUpdateCron = input.selfUpdateCron.trim();
if (!selfUpdateCron || selfUpdateCron.length > 100) {
throw new RangeError("Self-update cron must be between 1 and 100 characters");
}
try {
new Cron(selfUpdateCron, { paused: true }).nextRun();
} catch {
throw new RangeError("Self-update cron is invalid");
}
return { ...input, selfUpdateCron };
}
export class AutonomySettingsStore {
readonly storage = storage;
private readonly listeners = new Set<(settings: AutonomySettings) => void>();
init(): Promise<void> {
return initializeEvolutionStorage();
}
async get(): Promise<AutonomySettings> {
await this.init();
return this.storage.db.one<AutonomySettings>(`
SELECT ${AUTONOMY_SETTINGS_COLUMNS}
FROM popagent_autonomy_settings
WHERE singleton = TRUE
`);
}
async update(rawInput: AutonomySettingsInput): Promise<AutonomySettings> {
const input = validate(rawInput);
await this.init();
const settings = await this.storage.db.one<AutonomySettings>(`
UPDATE popagent_autonomy_settings
SET enabled = $1,
reflection_interval_ms = $2,
batch_size = $3,
max_attempts = $4,
auto_apply_strategies = $5,
auto_create_skills = $6,
self_update_enabled = $7,
self_update_cron = $8,
idle_improvement_enabled = $9,
idle_deployment_enabled = $10,
idle_grace_ms = $11,
idle_workspace_ids = $12::jsonb,
updated_at = NOW()
WHERE singleton = TRUE
RETURNING ${AUTONOMY_SETTINGS_COLUMNS}
`, [
input.enabled,
input.reflectionIntervalMs,
input.batchSize,
input.maxAttempts,
input.autoApplyStrategies,
input.autoCreateSkills,
input.selfUpdateEnabled,
input.selfUpdateCron,
input.idleImprovementEnabled,
input.idleDeploymentEnabled,
input.idleGraceMs,
JSON.stringify(input.idleWorkspaceIds),
]);
for (const listener of this.listeners) listener(settings);
return settings;
}
async recordUserActivity(): Promise<string> {
await this.init();
return (await this.storage.db.one<{ lastUserActivityAt: string }>(`
UPDATE popagent_autonomy_settings
SET last_user_activity_at=NOW()
WHERE singleton=TRUE
RETURNING last_user_activity_at::text AS "lastUserActivityAt"
`)).lastUserActivityAt;
}
subscribe(listener: (settings: AutonomySettings) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}
export const autonomySettings = new AutonomySettingsStore();