From 71ce96b9139bbf479ba4eb45c43031b0d4d269f4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 4 Jun 2026 00:16:05 +0800 Subject: [PATCH] Prioritize ready actions and scheduled agent prompts (#1316) * Sort sessions by last activity * Prioritize shipping actions in git controls * Fix scheduled new agent titles --- .../src/server/schedule/service.test.ts | 206 ++++++++++++++++++ .../server/src/server/schedule/service.ts | 16 +- 2 files changed, 218 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/schedule/service.test.ts b/packages/server/src/server/schedule/service.test.ts index 7cacb731e..dc82e27ae 100644 --- a/packages/server/src/server/schedule/service.test.ts +++ b/packages/server/src/server/schedule/service.test.ts @@ -216,6 +216,212 @@ describe("ScheduleService", () => { ); }); + test("titles scheduled new agents from the schedule prompt", async () => { + const manager = new AgentManager({ + logger: createTestLogger(), + clients: createTestAgentClients(), + registry: agentStorage, + }); + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: manager, + agentStorage, + providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, + now: () => now, + }); + + const created = await service.create({ + prompt: "Audit flaky checkout flow\n\nReport only blockers.", + cadence: { type: "every", everyMs: 60_000 }, + target: { + type: "new-agent", + config: { + provider: "claude", + cwd: tempDir, + approvalPolicy: "never", + }, + }, + maxRuns: 1, + }); + + now = new Date("2026-01-01T00:01:00.000Z"); + await service.tick(); + + const inspected = await service.inspect(created.id); + const agentId = inspected.runs[0]?.agentId; + expect(agentId).toMatch(/^[0-9a-f-]{36}$/); + const storedAgent = await agentStorage.get(agentId!); + expect(storedAgent?.title).toBe("Audit flaky checkout flow"); + }); + + test("shows scheduled new-agent prompts as normal user turns", async () => { + class PromptEchoScheduleSession implements AgentSession { + readonly provider = "claude"; + readonly capabilities = SCHEDULE_TEST_CAPABILITIES; + readonly id = "scheduled-prompt-echo-session"; + private turnCount = 0; + private readonly subscribers = new Set<(event: AgentStreamEvent) => void>(); + + async run(_prompt: AgentPromptInput, _options?: AgentRunOptions): Promise { + return { + sessionId: this.id, + finalText: "done", + timeline: [{ type: "assistant_message", text: "done" }], + }; + } + + async startTurn(prompt: AgentPromptInput): Promise<{ turnId: string }> { + const turnId = `turn-${++this.turnCount}`; + const textPrompt = typeof prompt === "string" ? prompt : JSON.stringify(prompt); + setImmediate(() => { + this.emit({ type: "turn_started", provider: this.provider, turnId }); + this.emit({ + type: "timeline", + provider: this.provider, + turnId, + item: { type: "user_message", text: textPrompt }, + }); + this.emit({ + type: "timeline", + provider: this.provider, + turnId, + item: { type: "assistant_message", text: "done" }, + }); + this.emit({ + type: "turn_completed", + provider: this.provider, + turnId, + usage: { inputTokens: 1, outputTokens: 1 }, + }); + }); + return { turnId }; + } + + subscribe(callback: (event: AgentStreamEvent) => void): () => void { + this.subscribers.add(callback); + return () => { + this.subscribers.delete(callback); + }; + } + + async *streamHistory(): AsyncGenerator {} + + async getRuntimeInfo() { + return { + provider: this.provider, + sessionId: this.id, + model: null, + modeId: null, + }; + } + + async getAvailableModes(): Promise { + return []; + } + + async getCurrentMode(): Promise { + return null; + } + + async setMode(_modeId: string): Promise {} + + getPendingPermissions(): AgentPermissionRequest[] { + return []; + } + + async respondToPermission( + _requestId: string, + _response: AgentPermissionResponse, + ): Promise {} + + describePersistence(): AgentPersistenceHandle { + return { + provider: this.provider, + sessionId: this.id, + }; + } + + async interrupt(): Promise {} + + async close(): Promise {} + + private emit(event: AgentStreamEvent): void { + for (const subscriber of this.subscribers) { + subscriber(event); + } + } + } + + class PromptEchoScheduleClient implements AgentClient { + readonly provider = "claude"; + readonly capabilities = SCHEDULE_TEST_CAPABILITIES; + + async createSession(_config: AgentSessionConfig): Promise { + return new PromptEchoScheduleSession(); + } + + async resumeSession(_handle: AgentPersistenceHandle): Promise { + return new PromptEchoScheduleSession(); + } + + async listModels(_options: ListModelsOptions): Promise { + return []; + } + + async isAvailable(): Promise { + return true; + } + } + + const manager = new AgentManager({ + logger: createTestLogger(), + clients: { claude: new PromptEchoScheduleClient() }, + registry: agentStorage, + }); + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: manager, + agentStorage, + providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, + now: () => now, + }); + const observedUserMessages: string[] = []; + const unsubscribe = manager.subscribe((event) => { + if (event.type !== "agent_stream" || event.event.type !== "timeline") { + return; + } + if (event.event.item.type === "user_message") { + observedUserMessages.push(event.event.item.text); + } + }); + + const created = await service.create({ + prompt: "Audit nightly run", + cadence: { type: "every", everyMs: 60_000 }, + target: { + type: "new-agent", + config: { + provider: "claude", + cwd: tempDir, + approvalPolicy: "never", + }, + }, + maxRuns: 1, + }); + + now = new Date("2026-01-01T00:01:00.000Z"); + try { + await service.tick(); + } finally { + unsubscribe(); + } + + expect(observedUserMessages).toEqual(["Audit nightly run"]); + expect((await service.inspect(created.id)).runs[0]?.status).toBe("succeeded"); + }); + test("archives new-agent schedule sessions after the run finishes", async () => { class CountingScheduleSession implements AgentSession { readonly provider = "claude"; diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index c99ca4381..5e89a0d48 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -7,6 +7,7 @@ import type { AgentSessionConfig } from "../agent/agent-sdk-types.js"; import { curateAgentActivity } from "../agent/activity-curator.js"; import { ensureAgentLoaded } from "../agent/agent-loading.js"; import { formatSystemNotificationPrompt } from "../agent/agent-prompt.js"; +import { resolveCreateAgentTitles } from "../agent/create-agent-title.js"; import { ScheduleStore } from "./store.js"; import { computeNextRunAt, validateScheduleCadence } from "./cron.js"; import type { @@ -528,9 +529,8 @@ export class ScheduleService { schedule: StoredSchedule, runId: string, ): Promise { - const wrappedPrompt = formatSystemNotificationPrompt(buildScheduleFireBody(schedule, runId)); - if (schedule.target.type === "agent") { + const wrappedPrompt = formatSystemNotificationPrompt(buildScheduleFireBody(schedule, runId)); const record = await this.agentStorage.get(schedule.target.agentId); if (record?.archivedAt) { throw new Error(`Agent ${schedule.target.agentId} is archived`); @@ -583,14 +583,22 @@ export class ScheduleService { systemPrompt: targetConfig.systemPrompt, mcpServers: targetConfig.mcpServers as AgentSessionConfig["mcpServers"], }; + const { provisionalTitle } = resolveCreateAgentTitles({ + configTitle: config.title, + initialPrompt: schedule.prompt, + }); const labels = { "paseo.schedule-id": schedule.id, "paseo.schedule-run": runId, }; - const agent = await this.agentManager.createAgent(config, undefined, { labels }); + const agent = await this.agentManager.createAgent(config, undefined, { + labels, + initialPrompt: schedule.prompt, + initialTitle: provisionalTitle, + }); let result; try { - result = await this.agentManager.runAgent(agent.id, wrappedPrompt); + result = await this.agentManager.runAgent(agent.id, schedule.prompt); } catch (error) { try { await this.agentManager.archiveAgent(agent.id);