mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-15 04:45:11 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7ed5e555 | ||
|
|
a0d8eb66f8 | ||
|
|
84bb8b8cd7 |
@@ -3020,6 +3020,72 @@ test("opening during reload waits for the replacement", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("clearing attention during reload waits for the replacement", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-clear-attention-"));
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
const client = new HeldReloadCloseClient();
|
||||
const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger });
|
||||
try {
|
||||
const created = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
await manager.markAgentUnread(created.id);
|
||||
const reloading = manager.reloadAgentSession(created.id);
|
||||
await client.waitForCloseToStart();
|
||||
let cleared = false;
|
||||
const clearing = manager.clearAgentAttention(created.id).then(() => {
|
||||
cleared = true;
|
||||
return true;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(cleared).toBe(false);
|
||||
client.finishClosing();
|
||||
await Promise.all([reloading, clearing]);
|
||||
expect(manager.getAgent(created.id)?.attention.requiresAttention).toBe(false);
|
||||
expect((await storage.get(created.id))?.requiresAttention).toBe(false);
|
||||
await manager.closeAgent(created.id);
|
||||
} finally {
|
||||
client.finishClosing();
|
||||
await storage.flush();
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resuming after a queued attention clear keeps the agent read", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-resume-clear-attention-"));
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: new TestAgentClient() },
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
try {
|
||||
const created = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
const handle = manager.getAgent(created.id)!.persistence!;
|
||||
await manager.closeAgent(created.id);
|
||||
await manager.markAgentUnread(created.id);
|
||||
// A loader read the record before the clear landed, then queued the resume behind it.
|
||||
const unreadAttention = {
|
||||
requiresAttention: true,
|
||||
attentionReason: "finished",
|
||||
attentionTimestamp: new Date(),
|
||||
} as const;
|
||||
const clearing = manager.clearAgentAttention(created.id);
|
||||
const resumed = await manager.resumeAgentFromPersistence(handle, { cwd: workdir }, created.id, {
|
||||
attention: unreadAttention,
|
||||
});
|
||||
await clearing;
|
||||
expect(resumed.attention.requiresAttention).toBe(false);
|
||||
expect((await storage.get(created.id))?.requiresAttention).toBe(false);
|
||||
await manager.closeAgent(created.id);
|
||||
} finally {
|
||||
await storage.flush();
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("closing during reload leaves the replacement closed", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-close-race-"));
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
|
||||
@@ -1722,9 +1722,9 @@ export class AgentManager {
|
||||
return this.registerSession(session, storedConfig, resolvedAgentId, {
|
||||
launchedOptions,
|
||||
...options,
|
||||
// The caller read its metadata before this turn in the lane; a setMetadata queued ahead of
|
||||
// it has saved a newer record, the one the open hooks saw.
|
||||
...(record ? { metadata: record.metadata } : {}),
|
||||
// The caller read its metadata and attention before this turn in the lane; a setMetadata
|
||||
// or attention clear queued ahead of it has saved a newer record, the one the open hooks saw.
|
||||
...(record ? { metadata: record.metadata, attention: extractAttention(record) } : {}),
|
||||
workspaceId,
|
||||
launchEnv,
|
||||
persistence: handle,
|
||||
@@ -2763,6 +2763,10 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
async clearAgentAttention(agentId: string): Promise<void> {
|
||||
return this.runLifecycleMutation(agentId, () => this.clearAgentAttentionUnlocked(agentId));
|
||||
}
|
||||
|
||||
private async clearAgentAttentionUnlocked(agentId: string): Promise<void> {
|
||||
const agent = this.agents.get(agentId);
|
||||
if (agent) {
|
||||
if (agent.attention.requiresAttention) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@getpaseo/protocol/binary-frames/index";
|
||||
import { DaemonClient } from "./test-utils/daemon-client.js";
|
||||
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
|
||||
import { withTimeout } from "../utils/promise-timeout.js";
|
||||
import {
|
||||
SessionInboundMessageSchema,
|
||||
type SessionInboundMessage,
|
||||
@@ -1645,6 +1646,69 @@ test("request outcomes for import and attention reach only their requesting sock
|
||||
}
|
||||
});
|
||||
|
||||
test("clearing a closed agent's attention does not resume its provider session", async () => {
|
||||
const daemon = await createTestPaseoDaemon({
|
||||
mcpEnabled: false,
|
||||
isDev: true,
|
||||
agentClients: { mock: new MockLoadTestAgentClient() },
|
||||
});
|
||||
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
||||
try {
|
||||
await client.connect();
|
||||
const agent = await client.createAgent({ provider: "mock", cwd: daemon.staticDir });
|
||||
await daemon.daemon.agentManager.closeAgent(agent.id);
|
||||
await daemon.daemon.agentManager.markAgentUnread(agent.id);
|
||||
await client.clearAgentAttention(agent.id);
|
||||
expect(daemon.daemon.agentManager.getAgent(agent.id)).toBeNull();
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
expect(snapshot?.agent.requiresAttention).toBe(false);
|
||||
expect(snapshot?.agent.status).toBe("closed");
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
class SessionGoneAgentClient extends MockLoadTestAgentClient {
|
||||
resumeCalls = 0;
|
||||
|
||||
override async resumeSession(): Promise<never> {
|
||||
this.resumeCalls += 1;
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
}
|
||||
|
||||
test("clearing a closed agent's attention succeeds when its provider session is gone", async () => {
|
||||
const agentClient = new SessionGoneAgentClient();
|
||||
const daemon = await createTestPaseoDaemon({
|
||||
mcpEnabled: false,
|
||||
isDev: true,
|
||||
agentClients: { mock: agentClient },
|
||||
});
|
||||
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
||||
try {
|
||||
await client.connect();
|
||||
const agent = await client.createAgent({ provider: "mock", cwd: daemon.staticDir });
|
||||
await daemon.daemon.agentManager.closeAgent(agent.id);
|
||||
await client.markWorkspaceUnread(agent.workspaceId!);
|
||||
expect((await client.fetchAgent(agent.id))?.agent.requiresAttention).toBe(true);
|
||||
|
||||
await withTimeout(
|
||||
client.clearAgentAttention(agent.id),
|
||||
5_000,
|
||||
"clear_agent_attention got no response",
|
||||
);
|
||||
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
expect(snapshot?.agent.requiresAttention).toBe(false);
|
||||
expect(snapshot?.agent.status).toBe("closed");
|
||||
expect(agentClient.resumeCalls).toBe(0);
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("SDK import failure settles without waiting for the request timeout", async () => {
|
||||
const daemon = await createTestPaseoDaemon({ mcpEnabled: false });
|
||||
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
||||
|
||||
@@ -5394,15 +5394,11 @@ export class Session {
|
||||
const agentIds = Array.isArray(agentId) ? agentId : [agentId];
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
agentIds.map((id) =>
|
||||
ensureAgentLoaded(id, {
|
||||
agentManager: this.agentManager,
|
||||
agentStorage: this.agentStorage,
|
||||
logger: this.sessionLogger,
|
||||
}),
|
||||
),
|
||||
);
|
||||
for (const id of agentIds) {
|
||||
if (!this.agentManager.getAgent(id) && !(await this.agentStorage.get(id))) {
|
||||
throw new Error(`Agent not found: ${id}`);
|
||||
}
|
||||
}
|
||||
await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
|
||||
if (requestId) {
|
||||
const agents = (
|
||||
|
||||
Reference in New Issue
Block a user