mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 11:12:53 +00:00
* fix(cli): exit quietly when stdout's reader is gone When the program that launched the CLI closed its end of stdout, the output write failed with EPIPE and nothing handled the error. Node printed an unhandled 'error' event and exited 1, and the desktop app's CLI passthrough could raise Electron's main-process error dialog. runCli now exits with code 0 on EPIPE from stdout. Other stdout errors still surface as before. Closes #5982 * fix(cli): keep running when stdout's reader is gone Exiting on EPIPE ended a command's own lifecycle: `paseo daemon run` writes its ready line and then supervises the daemon child, so exiting there left the child running without its parent. Drop output nobody reads instead, and let every command finish with its own exit code. * fix(cli): stop following when stdout's reader is gone With EPIPE ignored, `paseo agent logs --follow` and `paseo agent attach` kept running after the program reading their output closed it, until interrupted. Both now wait through one helper that stops on SIGINT, SIGTERM, or EPIPE on stdout, so a closed reader stops them at their next line of output the same way Ctrl+C does. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
172 lines
4.9 KiB
TypeScript
172 lines
4.9 KiB
TypeScript
import type { Command } from "commander";
|
|
|
|
export function addAttachOptions(cmd: Command): Command {
|
|
return cmd
|
|
.description("Attach to a running agent's output stream")
|
|
.argument("<id>", "Agent ID (or prefix)");
|
|
}
|
|
import { connectToDaemon } from "../../utils/client.js";
|
|
import { waitForStop } from "../../utils/wait-for-stop.js";
|
|
import {
|
|
fetchProjectedTimelineItems,
|
|
LIVE_HISTORY_FETCH_TIMEOUT_MS,
|
|
} from "../../utils/timeline.js";
|
|
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
|
import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages";
|
|
|
|
export interface AgentAttachOptions {
|
|
host?: string;
|
|
daemonTarget: import("../../utils/daemon-target.js").DaemonTarget;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* Format and print a timeline item to the terminal
|
|
*/
|
|
function printTimelineItem(item: AgentTimelineItem): void {
|
|
switch (item.type) {
|
|
case "assistant_message":
|
|
// Print assistant text directly
|
|
process.stdout.write(item.text);
|
|
break;
|
|
|
|
case "reasoning":
|
|
// Print reasoning in a muted color if available
|
|
console.log(`\n[Reasoning] ${item.text}`);
|
|
break;
|
|
|
|
case "tool_call": {
|
|
const toolName = item.name;
|
|
const status = item.status ?? "started";
|
|
console.log(`\n[Tool: ${toolName}] ${status}`);
|
|
break;
|
|
}
|
|
|
|
case "todo": {
|
|
const completed = item.items.filter((i) => i.completed).length;
|
|
const total = item.items.length;
|
|
console.log(`\n[Todo] ${completed}/${total} completed`);
|
|
break;
|
|
}
|
|
|
|
case "error":
|
|
console.error(`\n[Error] ${item.message}`);
|
|
break;
|
|
|
|
case "user_message":
|
|
console.log(`\n[User] ${item.text}`);
|
|
break;
|
|
|
|
default:
|
|
// Unknown item type, skip
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format and print a stream event to the terminal
|
|
*/
|
|
function printStreamEvent(event: AgentStreamEventPayload): void {
|
|
switch (event.type) {
|
|
case "timeline":
|
|
// Print the timeline item
|
|
printTimelineItem(event.item);
|
|
break;
|
|
|
|
case "permission_requested":
|
|
console.log(`\n[Permission Required] ${event.request.name}`);
|
|
if (event.request.description) {
|
|
console.log(` ${event.request.description}`);
|
|
}
|
|
break;
|
|
|
|
case "permission_resolved":
|
|
console.log(`\n[Permission ${event.resolution.behavior}]`);
|
|
break;
|
|
|
|
case "turn_failed":
|
|
console.error(`\n[Turn Failed] ${event.error}`);
|
|
break;
|
|
|
|
case "attention_required":
|
|
console.log(`\n[Attention Required: ${event.reason}]`);
|
|
break;
|
|
|
|
default:
|
|
// Other event types are internal
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach to a running agent's output stream
|
|
*/
|
|
export async function runAttachCommand(
|
|
id: string,
|
|
options: AgentAttachOptions,
|
|
_command: Command,
|
|
): Promise<void> {
|
|
if (!id) {
|
|
console.error("Error: Agent ID required");
|
|
console.error("Usage: paseo attach <id>");
|
|
process.exit(1);
|
|
}
|
|
|
|
const client = await connectToDaemon({ target: options.daemonTarget });
|
|
|
|
try {
|
|
const fetchResult = await client.fetchAgent({ agentId: id });
|
|
if (!fetchResult) {
|
|
console.error(`Error: No agent found matching: ${id}`);
|
|
console.error("Use `paseo ls` to list available agents");
|
|
await client.close();
|
|
process.exit(1);
|
|
}
|
|
const resolvedId = fetchResult.agent.id;
|
|
|
|
// Print header
|
|
console.log(`Fetching history for agent ${resolvedId.substring(0, 7)}...`);
|
|
console.log(`(Press Ctrl+C to detach)\n`);
|
|
|
|
// Print existing output from timeline fetch.
|
|
try {
|
|
const timelineItems = await fetchProjectedTimelineItems({
|
|
client,
|
|
agentId: resolvedId,
|
|
timeoutMs: LIVE_HISTORY_FETCH_TIMEOUT_MS,
|
|
});
|
|
for (const item of timelineItems) {
|
|
printTimelineItem(item);
|
|
}
|
|
} catch (error) {
|
|
console.warn("Warning: failed to fetch existing timeline", error);
|
|
}
|
|
|
|
// Subscribe to new events
|
|
const unsubscribe = client.subscribeAgentTimeline(resolvedId, (message) => {
|
|
if (message.type === "agent.timeline.replacement") {
|
|
console.log("\n[Timeline replaced; earlier output is no longer current]");
|
|
} else if (message.type === "agent.timeline.subscription_restored") {
|
|
console.log("\n[Reconnected; live output resumed. Events may have been missed.]");
|
|
} else if (message.type === "agent.timeline.error") {
|
|
console.error(`Timeline observation stopped: ${message.payload.error}`);
|
|
} else {
|
|
printStreamEvent(message.payload.event);
|
|
}
|
|
});
|
|
|
|
await unsubscribe.ready;
|
|
console.log(`Attached to agent ${resolvedId.substring(0, 7)}.`);
|
|
|
|
await waitForStop();
|
|
console.log("\n\nDetaching from agent...");
|
|
unsubscribe();
|
|
await client.close();
|
|
} catch (err) {
|
|
await client.close().catch(() => {});
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
console.error(`Error: Failed to attach to agent: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|