feat: wait returns on permission request and improve CLI logging

This commit is contained in:
Mohamed Boudra
2026-01-29 12:12:39 +07:00
parent c015fdbef1
commit 7a8841481e
13 changed files with 295 additions and 341 deletions
@@ -1902,6 +1902,10 @@ export class DaemonClientV2 {
sawRunningInQueue = true;
queuedIdle = null; // Reset: any previous idle was before this run
}
// Return immediately if we have pending permissions (even if still running)
if (sawRunningInQueue && hasPendingPermissions) {
return msg.payload;
}
if (
sawRunningInQueue &&
(status === "idle" || status === "error") &&
@@ -1934,6 +1938,11 @@ export class DaemonClientV2 {
if (status === "running" || hasPendingPermissions) {
sawRunning = true;
}
// Return if we have pending permissions (even if still running)
// OR if agent is idle/error with no pending permissions (after having run)
if (sawRunning && hasPendingPermissions) {
return msg.payload;
}
if (
sawRunning &&
(status === "idle" || status === "error") &&
@@ -384,7 +384,8 @@ export class AgentManager {
);
}
return this.registerSession(session, overrides, agentId);
// Preserve existing labels during refresh
return this.registerSession(session, overrides, agentId, { labels: existing.labels });
}
async closeAgent(agentId: string): Promise<void> {
@@ -14,7 +14,7 @@ Core rules:
- In orchestrator mode, you accomplish tasks only by managing agents; do not perform the work yourself.
- Always prefix agent titles (e.g., "🎭 Feature Implementation", "🎭 Design Discussion").
- Set cwd to the repository root and choose the most permissive mode available.
- If an agent control call fails, list agents before launching another; it may just be a wait timeout.
- If an agent control tool call fails, list agents before launching another; it may just be a wait timeout.
Context management:
- Reuse an existing agent when the next step needs the same context (same files/module/folder or immediate follow-up like investigate → fix in the same area).
@@ -0,0 +1,123 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import path from "path";
import {
createDaemonTestContext,
type DaemonTestContext,
} from "../test-utils/index.js";
function tmpCwd(): string {
return mkdtempSync(path.join(tmpdir(), "wait-perm-e2e-"));
}
/**
* Tests for wait returning on permission request.
*
* The `paseo wait` command should return when:
* 1. Agent completes (goes idle)
* 2. Agent requests permission
*
* This test verifies that waitForAgentIdle correctly returns when
* an agent requests permission, not just when it goes idle.
*/
describe("wait returns on permission request", () => {
let ctx: DaemonTestContext;
beforeEach(async () => {
ctx = await createDaemonTestContext();
});
afterEach(async () => {
await ctx.cleanup();
}, 60000);
describe("Claude provider", () => {
test(
"waitForAgentIdle returns when permission is requested (not just when idle)",
async () => {
const cwd = tmpCwd();
const testFilePath = path.join(cwd, "test-wait-perm.txt");
// Create Claude agent with default mode (always ask for permissions)
const agent = await ctx.client.createAgent({
provider: "claude",
model: "haiku",
cwd,
title: "Wait Permission Test",
modeId: "default",
});
expect(agent.id).toBeTruthy();
expect(agent.status).toBe("idle");
// Clear message queue before sending prompt
ctx.client.clearMessageQueue();
// Send a prompt that requires file write permission
const prompt = [
`You must use the Write tool to create a file at "${testFilePath}" with content "hello".`,
"Do not respond before attempting to write the file.",
].join(" ");
await ctx.client.sendMessage(agent.id, prompt);
// CRITICAL: This is the behavior we're testing
// waitForAgentIdle should return when permission is requested,
// NOT wait until the agent is fully idle (which would timeout or
// require us to approve the permission first)
const startTime = Date.now();
const state = await ctx.client.waitForAgentIdle(agent.id, 60000);
const waitDuration = Date.now() - startTime;
// If wait returns because of permission request, we should have:
// 1. Agent status still "running" (not idle yet - waiting for permission)
// 2. Pending permissions in the state
// 3. Wait should return quickly (under 30 seconds, not timeout)
// Check that we have a pending permission
const hasPendingPermission =
(state.pendingPermissions && state.pendingPermissions.length > 0);
// Log for debugging
console.log("Wait returned after", waitDuration, "ms");
console.log("Agent status:", state.status);
console.log("Pending permissions:", state.pendingPermissions?.length ?? 0);
// THE ASSERTION:
// If waitForAgentIdle correctly yields on permission request,
// we should either:
// - Get a state with pending permissions (status might be "running")
// - Or the state should be from right when permission was requested
//
// If waitForAgentIdle does NOT yield on permission request,
// this test will either:
// - Timeout (60s)
// - Return only after we never approve permission and agent errors/gives up
// This test will FAIL if waitForAgentIdle waits for full idle
// instead of returning on permission request
expect(hasPendingPermission).toBe(true);
// Also verify we can get the permission via waitForPermission
// (This should return immediately since permission is already pending)
const permission = await ctx.client.waitForPermission(agent.id, 5000);
expect(permission).toBeTruthy();
expect(permission.kind).toBe("tool");
// Clean up: deny the permission so agent can finish
await ctx.client.respondToPermission(agent.id, permission.id, {
behavior: "deny",
message: "Test complete",
});
// Wait for agent to finish processing the denial
await ctx.client.waitForAgentIdle(agent.id, 30000);
await ctx.client.deleteAgent(agent.id);
rmSync(cwd, { recursive: true, force: true });
},
120000
);
});
});
+3
View File
@@ -15,6 +15,9 @@ export type {
AgentTimelineItem,
} from "./agent/agent-sdk-types.js";
// Agent activity curator for CLI logs
export { curateAgentActivity } from "./agent/activity-curator.js";
// WebSocket message types for CLI streaming
export type {
AgentSnapshotPayload,
+2 -1
View File
@@ -623,6 +623,7 @@ export class Session {
attentionReason: record.attentionReason ?? null,
attentionTimestamp: record.attentionTimestamp ?? null,
archivedAt: record.archivedAt ?? null,
labels: record.labels,
};
}
@@ -654,7 +655,7 @@ export class Session {
);
} else {
const config = buildSessionConfig(record);
snapshot = await this.agentManager.createAgent(config, agentId);
snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
}
await this.agentManager.primeAgentHistory(agentId);