mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix: interrupt running agents before archive
This commit is contained in:
@@ -83,7 +83,8 @@ export async function runArchiveCommand(
|
||||
const error: CommandError = {
|
||||
code: 'AGENT_RUNNING',
|
||||
message: `Agent ${agentId.slice(0, 7)} is currently running`,
|
||||
details: 'Use --force to archive a running agent, or stop it first with: paseo agent stop',
|
||||
details:
|
||||
'Use --force to archive a running agent (it will interrupt the active run), or stop it first with: paseo agent stop',
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export function createAgentCommand(): Command {
|
||||
.command('archive')
|
||||
.description('Archive an agent (soft-delete)')
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--force', 'Force archive running agent')
|
||||
.option('--force', 'Force archive running agent (interrupts active run first)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runArchiveCommand))
|
||||
|
||||
@@ -196,6 +196,64 @@ describe("daemon client E2E", () => {
|
||||
await expect(ctx.client.archiveAgent(randomUUID())).rejects.toThrow();
|
||||
}, 10000);
|
||||
|
||||
test("interrupts a running agent before archiving", async () => {
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const created = await ctx.client.createAgent({
|
||||
config: {
|
||||
...getFullAccessConfig("codex"),
|
||||
cwd,
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.client.sendMessage(
|
||||
created.id,
|
||||
"Use your shell tool to run `sleep 30` and then confirm when done."
|
||||
);
|
||||
await ctx.client.waitForAgentUpsert(
|
||||
created.id,
|
||||
(snapshot) => snapshot.status === "running",
|
||||
15000
|
||||
);
|
||||
|
||||
const result = await ctx.client.archiveAgent(created.id);
|
||||
expect(result.archivedAt).toBeTruthy();
|
||||
|
||||
const archived = await ctx.client.fetchAgent(created.id);
|
||||
expect(archived).not.toBeNull();
|
||||
expect(archived?.archivedAt).toBeTruthy();
|
||||
expect(archived?.status).not.toBe("running");
|
||||
|
||||
const runningAgents = await ctx.client.fetchAgents({
|
||||
filter: { includeArchived: true, statuses: ["running"] },
|
||||
});
|
||||
expect(
|
||||
runningAgents.entries.some((entry) => entry.agent.id === created.id)
|
||||
).toBe(false);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
test("rejects send_agent_message for archived agents", async () => {
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const created = await ctx.client.createAgent({
|
||||
config: {
|
||||
...getFullAccessConfig("codex"),
|
||||
cwd,
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.client.archiveAgent(created.id);
|
||||
await expect(
|
||||
ctx.client.sendMessage(created.id, "Say hello and nothing else")
|
||||
).rejects.toThrow("archived");
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test("returns home-scoped directory suggestions", async () => {
|
||||
const insideHomeDir = mkdtempSync(path.join(homedir(), "paseo-dir-suggestion-"));
|
||||
const outsideHomeDir = mkdtempSync(path.join(tmpdir(), "paseo-dir-suggestion-outside-"));
|
||||
|
||||
@@ -1674,6 +1674,10 @@ export class Session {
|
||||
private async handleArchiveAgentRequest(agentId: string, requestId: string): Promise<void> {
|
||||
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`)
|
||||
|
||||
if (this.agentManager.getAgent(agentId)) {
|
||||
await this.interruptAgentIfRunning(agentId)
|
||||
}
|
||||
|
||||
const archivedAt = new Date().toISOString()
|
||||
|
||||
const existing = await this.agentStorage.get(agentId)
|
||||
@@ -1717,6 +1721,11 @@ export class Session {
|
||||
})
|
||||
}
|
||||
|
||||
private async getArchivedAt(agentId: string): Promise<string | null> {
|
||||
const record = await this.agentStorage.get(agentId)
|
||||
return record?.archivedAt ?? null
|
||||
}
|
||||
|
||||
private async handleUpdateAgentRequest(
|
||||
agentId: string,
|
||||
name: string | undefined,
|
||||
@@ -2308,6 +2317,16 @@ export class Session {
|
||||
return
|
||||
}
|
||||
|
||||
const archivedAt = await this.getArchivedAt(agentId)
|
||||
if (archivedAt) {
|
||||
this.handleAgentRunError(
|
||||
agentId,
|
||||
new Error(`Agent ${agentId} is archived`),
|
||||
'Refusing to send prompt to archived agent'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.interruptAgentIfRunning(agentId)
|
||||
} catch (error) {
|
||||
@@ -5250,6 +5269,20 @@ export class Session {
|
||||
try {
|
||||
const agentId = resolved.agentId
|
||||
|
||||
const archivedAt = await this.getArchivedAt(agentId)
|
||||
if (archivedAt) {
|
||||
this.emit({
|
||||
type: 'send_agent_message_response',
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
agentId,
|
||||
accepted: false,
|
||||
error: `Agent ${agentId} is archived`,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await this.ensureAgentLoaded(agentId)
|
||||
await this.interruptAgentIfRunning(agentId)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user