refactor: return agent on create and resume instead of waiting

This commit is contained in:
Mohamed Boudra
2026-02-02 12:51:04 +07:00
parent bf9555829b
commit b2f9bb1294
6 changed files with 30 additions and 63 deletions
+1 -3
View File
@@ -121,7 +121,6 @@ Run `npx expo-doctor` to diagnose version mismatches and native module issues.
- **Launch agents in the most permissive mode**: Use full access or bypass permissions mode. - **Launch agents in the most permissive mode**: Use full access or bypass permissions mode.
- **Set cwd to the repository root** - The agent's working directory should usually be the repo root - **Set cwd to the repository root** - The agent's working directory should usually be the repo root
**CRITICAL: ALWAYS RUN TYPECHECK AFTER EVERY CHANGE.** **CRITICAL: ALWAYS RUN TYPECHECK AFTER EVERY CHANGE.**
## Agent Authentication ## Agent Authentication
@@ -132,7 +131,6 @@ All agent providers (Claude, Codex, OpenCode) handle their own authentication ou
## NEVER DO THESE THINGS ## NEVER DO THESE THINGS
- **NEVER restart the main Paseo daemon on port 6767** - This is the production daemon that launches and manages agents. If you are reading this, you are probably running as an agent under it. Restarting it will kill your own process and all other running agents. The daemon is managed by the user in Tmux. - **NEVER restart the main Paseo daemon on port 6767 without permission** - This is the production daemon that launches and manages agents. If you are reading this, you are probably running as an agent under it. Restarting it will kill your own process and all other running agents. The daemon is managed by the user in Tmux.
- **NEVER kill or restart processes in Tmux** without explicit user permission
- **NEVER assume a timeout means the service needs restarting** - Timeouts can be transient network issues, not service failures - **NEVER assume a timeout means the service needs restarting** - Timeouts can be transient network issues, not service failures
- **NEVER add authentication checks to tests** - Agent providers handle their own auth. If tests fail due to auth issues, report it rather than adding conditional skips or env var checks - **NEVER add authentication checks to tests** - Agent providers handle their own auth. If tests fail due to auth issues, report it rather than adding conditional skips or env var checks
+2 -45
View File
@@ -200,9 +200,6 @@ type SubscribeTerminalPayload = SubscribeTerminalResponse["payload"];
type TerminalOutputPayload = TerminalOutput["payload"]; type TerminalOutputPayload = TerminalOutput["payload"];
type KillTerminalPayload = KillTerminalResponse["payload"]; type KillTerminalPayload = KillTerminalResponse["payload"];
type AgentCreateFailedStatusPayload = z.infer<
typeof AgentCreateFailedStatusPayloadSchema
>;
type AgentRefreshedStatusPayload = z.infer< type AgentRefreshedStatusPayload = z.infer<
typeof AgentRefreshedStatusPayloadSchema typeof AgentRefreshedStatusPayloadSchema
>; >;
@@ -889,43 +886,7 @@ export class DaemonClientV2 {
throw new Error(status.error); throw new Error(status.error);
} }
return this.waitForAgentUpsert( return status.agent;
status.agentId,
(snapshot) => snapshot.status === "idle",
60000
);
}
async createAgentExpectFail(
options: CreateAgentRequestOptions
): Promise<AgentCreateFailedStatusPayload> {
const requestId = this.createRequestId(options.requestId);
const config = resolveAgentConfig(options);
const message = SessionInboundMessageSchema.parse({
type: "create_agent_request",
requestId,
config,
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
});
const response = this.waitFor(
(msg) => {
if (msg.type !== "status") {
return null;
}
const failed = AgentCreateFailedStatusPayloadSchema.safeParse(msg.payload);
if (failed.success && failed.data.requestId === requestId) {
return failed.data;
}
return null;
},
10000,
{ skipQueue: true }
);
await this.sendSessionMessageOrThrow(message);
return response;
} }
async deleteAgent(agentId: string): Promise<void> { async deleteAgent(agentId: string): Promise<void> {
@@ -1007,11 +968,7 @@ export class DaemonClientV2 {
await this.sendSessionMessageOrThrow(message); await this.sendSessionMessageOrThrow(message);
const status = await statusPromise; const status = await statusPromise;
return this.waitForAgentUpsert( return status.agent;
status.agentId,
(snapshot) => snapshot.status === "idle",
60000
);
} }
async refreshAgent( async refreshAgent(
@@ -185,13 +185,14 @@ describe("daemon client v2 E2E", () => {
return unsubscribe; return unsubscribe;
}); });
const failResult = await ctx.client.createAgentExpectFail({ await expect(
...getFullAccessConfig("codex"), ctx.client.createAgent({
cwd: "/this/path/does/not/exist/12345", ...getFullAccessConfig("codex"),
title: "Should Fail", cwd: "/this/path/does/not/exist/12345",
requestId: failRequestId, title: "Should Fail",
}); requestId: failRequestId,
expect(failResult.error).toContain("Working directory does not exist"); })
).rejects.toThrow("Working directory does not exist");
await failedStatusPromise; await failedStatusPromise;
let sawRefresh = false; let sawRefresh = false;
@@ -93,14 +93,13 @@ describe("daemon E2E", () => {
test("fails to create agent with non-existent cwd", async () => { test("fails to create agent with non-existent cwd", async () => {
const nonExistentCwd = "/this/path/does/not/exist/12345"; const nonExistentCwd = "/this/path/does/not/exist/12345";
const result = await ctx.client.createAgentExpectFail({ await expect(
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT, ctx.client.createAgent({
cwd: nonExistentCwd, provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
title: "Should Fail Agent", cwd: nonExistentCwd,
}); title: "Should Fail Agent",
})
expect(result.error).toContain("Working directory does not exist"); ).rejects.toThrow(nonExistentCwd);
expect(result.error).toContain(nonExistentCwd);
}); });
+10
View File
@@ -1435,12 +1435,17 @@ export class Session {
} }
if (requestId) { if (requestId) {
const agentPayload = await this.getAgentPayloadById(snapshot.id);
if (!agentPayload) {
throw new Error(`Agent ${snapshot.id} not found after creation`);
}
this.emit({ this.emit({
type: "status", type: "status",
payload: { payload: {
status: "agent_created", status: "agent_created",
agentId: snapshot.id, agentId: snapshot.id,
requestId, requestId,
agent: agentPayload,
}, },
}); });
} }
@@ -1510,6 +1515,10 @@ export class Session {
await this.forwardAgentUpdate(snapshot); await this.forwardAgentUpdate(snapshot);
const timelineSize = this.emitAgentTimelineSnapshot(snapshot); const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
if (requestId) { if (requestId) {
const agentPayload = await this.getAgentPayloadById(snapshot.id);
if (!agentPayload) {
throw new Error(`Agent ${snapshot.id} not found after resume`);
}
this.emit({ this.emit({
type: "status", type: "status",
payload: { payload: {
@@ -1517,6 +1526,7 @@ export class Session {
agentId: snapshot.id, agentId: snapshot.id,
requestId, requestId,
timelineSize, timelineSize,
agent: agentPayload,
}, },
}); });
} }
+2
View File
@@ -979,6 +979,7 @@ const AgentStatusWithTimelineSchema = AgentStatusWithRequestSchema.extend({
export const AgentCreatedStatusPayloadSchema = z export const AgentCreatedStatusPayloadSchema = z
.object({ .object({
status: z.literal("agent_created"), status: z.literal("agent_created"),
agent: AgentSnapshotPayloadSchema,
}) })
.extend(AgentStatusWithRequestSchema.shape); .extend(AgentStatusWithRequestSchema.shape);
@@ -991,6 +992,7 @@ export const AgentCreateFailedStatusPayloadSchema = z.object({
export const AgentResumedStatusPayloadSchema = z export const AgentResumedStatusPayloadSchema = z
.object({ .object({
status: z.literal("agent_resumed"), status: z.literal("agent_resumed"),
agent: AgentSnapshotPayloadSchema,
}) })
.extend(AgentStatusWithTimelineSchema.shape); .extend(AgentStatusWithTimelineSchema.shape);