fix: interrupt running agents before archive

This commit is contained in:
Mohamed Boudra
2026-02-20 13:34:06 +07:00
parent e22647af62
commit 6dad3212da
4 changed files with 94 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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-"));

View File

@@ -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)