Menu
popagent
publicLatest change bea7c647f451d774dcf4753d7ca5ca5e01d0cf54 - Rename supervisor agent ID to orchistrator by Ólafur Búi Ólafsson
import { constants } from "node:fs";
import { access } from "node:fs/promises";
import { join } from "node:path";
import { agentRuntimeSettings } from "./agent-runtime-settings";
import { agentSettings } from "./agent-settings";
import { agentWorkspaces, type AgentWorkspaceStore } from "./agent-workspaces";
import type { AgentRuntimeSettings, AgentSettings } from "./api-types";
import { autonomySettings, type AutonomySettingsStore } from "./autonomy-settings";
import { appLogger } from "./observability";
import type { AgentTaskStore } from "./tasks";
export const SELF_UPDATE_SOURCE = "self-update";
export const SELF_UPDATE_SCHEDULE_NAME = "[system:self-update] Autonomous source maintenance";
export const SELF_UPDATE_PROMPT = `Perform contained autonomous source maintenance for the registered workspace attached to this task.
1. Inspect the repository rules and available evidence. Choose at most one small, high-confidence improvement justified by that evidence. If no change is justified, finish cleanly without editing files.
2. Delegate implementation to the implementer and then delegate an independent review to the reviewer. Keep the change focused and preserve all existing containment and security boundaries.
3. Run the repository's authoritative verification for the changed behavior and address failures.
4. Commit only the resulting workspace change on the prepared local branch under autonomous/self-update/, using both --no-verify and --no-gpg-sign.
Never push, publish, deploy, restart services, acquire or use credentials, access files outside the contained self-update checkout, change Git remotes or hooks, change authentication/execution bounds/tool or capability membership/workspace or browser access/secrets/user-managed instructions, or bypass the repository's maintained APIs and release path. Fixed server policy enforces these limits independently of this prompt.`;
const RECONCILE_INTERVAL_MS = 60_000;
type TaskScheduleRepository = Pick<
AgentTaskStore,
"upsertSelfUpdateSchedule" | "disableSelfUpdateSchedules"
>;
type AutonomySettingsRepository = Pick<AutonomySettingsStore, "get" | "subscribe">;
type WorkspaceRepository = Pick<AgentWorkspaceStore, "list" | "resolveRepository">;
type AgentAccessRepository = {
get(id: string): Promise<
Pick<AgentSettings, "workspaceAccess" | "delegationEnabled"> | undefined
>;
};
type RuntimeSettingsRepository = {
get(): Promise<Pick<AgentRuntimeSettings, "defaultModel">>;
};
type SelfUpdateSchedulerDependencies = {
tasks: TaskScheduleRepository;
settings?: AutonomySettingsRepository;
workspaces?: WorkspaceRepository;
agents?: AgentAccessRepository;
runtimeSettings?: RuntimeSettingsRepository;
reconcileIntervalMs?: number;
isWritableRepository?: (path: string) => Promise<boolean>;
};
async function isWritableGitRepository(path: string): Promise<boolean> {
try {
await access(path, constants.W_OK);
await access(join(path, ".git"), constants.R_OK | constants.W_OK);
return true;
} catch {
return false;
}
}
export class SelfUpdateScheduler {
private readonly tasks: TaskScheduleRepository;
private readonly settings: AutonomySettingsRepository;
private readonly workspaces: WorkspaceRepository;
private readonly agents: AgentAccessRepository;
private readonly runtimeSettings: RuntimeSettingsRepository;
private readonly reconcileIntervalMs: number;
private readonly isWritableRepository: (path: string) => Promise<boolean>;
private timer?: Timer;
private unsubscribe?: () => void;
private reconciling?: Promise<void>;
private reconcileRequested = false;
private lifecycle = Promise.resolve();
private starting?: Promise<void>;
private stopping?: Promise<void>;
private desiredRunning = false;
constructor(dependencies: SelfUpdateSchedulerDependencies) {
this.tasks = dependencies.tasks;
this.settings = dependencies.settings ?? autonomySettings;
this.workspaces = dependencies.workspaces ?? agentWorkspaces;
this.agents = dependencies.agents ?? agentSettings;
this.runtimeSettings = dependencies.runtimeSettings ?? agentRuntimeSettings;
this.reconcileIntervalMs = dependencies.reconcileIntervalMs ?? RECONCILE_INTERVAL_MS;
this.isWritableRepository = dependencies.isWritableRepository ?? isWritableGitRepository;
}
start(): Promise<void> {
this.desiredRunning = true;
if (this.starting) return this.starting;
const operation = this.serializeLifecycle(async () => {
if (!this.desiredRunning || this.timer) return;
const unsubscribe = this.settings.subscribe(() => this.reconcileSafely());
this.unsubscribe = unsubscribe;
try {
await this.reconcile();
} catch (error) {
unsubscribe();
if (this.unsubscribe === unsubscribe) this.unsubscribe = undefined;
throw error;
}
if (!this.desiredRunning) {
unsubscribe();
if (this.unsubscribe === unsubscribe) this.unsubscribe = undefined;
return;
}
this.timer = setInterval(() => this.reconcileSafely(), this.reconcileIntervalMs);
});
const pending = operation.finally(() => {
if (this.starting === pending) this.starting = undefined;
});
this.starting = pending;
return pending;
}
stop(): Promise<void> {
this.desiredRunning = false;
if (this.stopping) return this.stopping;
const operation = this.serializeLifecycle(async () => {
if (this.desiredRunning) return;
clearInterval(this.timer);
this.timer = undefined;
this.unsubscribe?.();
this.unsubscribe = undefined;
await this.reconciling;
});
const pending = operation.finally(() => {
if (this.stopping === pending) this.stopping = undefined;
});
this.stopping = pending;
return pending;
}
private serializeLifecycle(operation: () => Promise<void>): Promise<void> {
const result = this.lifecycle.then(operation, operation);
this.lifecycle = result.catch(() => undefined);
return result;
}
reconcile(): Promise<void> {
this.reconcileRequested = true;
if (this.reconciling) return this.reconciling;
this.reconciling = (async () => {
while (this.reconcileRequested) {
this.reconcileRequested = false;
await this.reconcileOnce();
}
})().finally(() => {
this.reconciling = undefined;
});
return this.reconciling;
}
private reconcileSafely(): void {
if (!this.desiredRunning) return;
void this.reconcile().catch((error) => {
appLogger().error("self_update.schedule_failed", { error });
});
}
private async reconcileOnce(): Promise<void> {
const settings = await this.settings.get();
if (!settings.enabled || !settings.selfUpdateEnabled) {
await this.tasks.disableSelfUpdateSchedules();
return;
}
const [supervisor, implementer, reviewer, runtimeSettings, workspaces] = await Promise.all([
this.agents.get("orchistrator"),
this.agents.get("implementer"),
this.agents.get("reviewer"),
this.runtimeSettings.get(),
this.workspaces.list(),
]);
const rolesEligible = supervisor?.workspaceAccess === "read-write"
&& implementer?.workspaceAccess === "read-write"
&& implementer.delegationEnabled
&& reviewer?.workspaceAccess !== "none"
&& reviewer?.delegationEnabled;
if (!rolesEligible) {
await this.tasks.disableSelfUpdateSchedules();
return;
}
const writableWorkspaceIds: string[] = [];
for (const workspace of workspaces) {
let repositoryPath: string;
try {
repositoryPath = (await this.workspaces.resolveRepository(workspace.id)).path;
} catch {
continue;
}
if (!(await this.isWritableRepository(repositoryPath))) continue;
writableWorkspaceIds.push(workspace.id);
await this.tasks.upsertSelfUpdateSchedule({
workspaceId: workspace.id,
name: SELF_UPDATE_SCHEDULE_NAME,
prompt: SELF_UPDATE_PROMPT,
model: runtimeSettings.defaultModel,
maxAttempts: settings.maxAttempts,
cron: settings.selfUpdateCron,
enabled: true,
});
}
await this.tasks.disableSelfUpdateSchedules(writableWorkspaceIds);
}
}