fix: interrupt running agents before archive
This commit is contained in:
@@ -83,7 +83,8 @@ export async function runArchiveCommand(
|
|||||||
const error: CommandError = {
|
const error: CommandError = {
|
||||||
code: 'AGENT_RUNNING',
|
code: 'AGENT_RUNNING',
|
||||||
message: `Agent ${agentId.slice(0, 7)} is currently 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
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function createAgentCommand(): Command {
|
|||||||
.command('archive')
|
.command('archive')
|
||||||
.description('Archive an agent (soft-delete)')
|
.description('Archive an agent (soft-delete)')
|
||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.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('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runArchiveCommand))
|
.action(withOutput(runArchiveCommand))
|
||||||
|
|||||||
@@ -196,6 +196,64 @@ describe("daemon client E2E", () => {
|
|||||||
await expect(ctx.client.archiveAgent(randomUUID())).rejects.toThrow();
|
await expect(ctx.client.archiveAgent(randomUUID())).rejects.toThrow();
|
||||||
}, 10000);
|
}, 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 () => {
|
test("returns home-scoped directory suggestions", async () => {
|
||||||
const insideHomeDir = mkdtempSync(path.join(homedir(), "paseo-dir-suggestion-"));
|
const insideHomeDir = mkdtempSync(path.join(homedir(), "paseo-dir-suggestion-"));
|
||||||
const outsideHomeDir = mkdtempSync(path.join(tmpdir(), "paseo-dir-suggestion-outside-"));
|
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> {
|
private async handleArchiveAgentRequest(agentId: string, requestId: string): Promise<void> {
|
||||||
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`)
|
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`)
|
||||||
|
|
||||||
|
if (this.agentManager.getAgent(agentId)) {
|
||||||
|
await this.interruptAgentIfRunning(agentId)
|
||||||
|
}
|
||||||
|
|
||||||
const archivedAt = new Date().toISOString()
|
const archivedAt = new Date().toISOString()
|
||||||
|
|
||||||
const existing = await this.agentStorage.get(agentId)
|
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(
|
private async handleUpdateAgentRequest(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
name: string | undefined,
|
name: string | undefined,
|
||||||
@@ -2308,6 +2317,16 @@ export class Session {
|
|||||||
return
|
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 {
|
try {
|
||||||
await this.interruptAgentIfRunning(agentId)
|
await this.interruptAgentIfRunning(agentId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -5250,6 +5269,20 @@ export class Session {
|
|||||||
try {
|
try {
|
||||||
const agentId = resolved.agentId
|
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.ensureAgentLoaded(agentId)
|
||||||
await this.interruptAgentIfRunning(agentId)
|
await this.interruptAgentIfRunning(agentId)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user