feat: wait returns on permission request and improve CLI logging
This commit is contained in:
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user