Default client RPC waits to 60 seconds (#1789)
* fix(client): wait longer for session responses * fix(client): default RPC waits to sixty seconds * fix(app): preserve detached stream scroll on delayed history Code drift: longer client RPC waits let delayed timeline responses arrive after a user scroll-away, so web stream anchoring must not reattach on transient scroll-top resets. Restore the 15s connect deadline and leave app initialization slack above the default 60s session RPC wait. * fix(client): keep helper waits within caller deadlines Review fix: the 60s default session RPC wait leaked into wait previews and waitForAgentUpsert helper fetches. Bound those helper RPCs to the caller deadline or a short best-effort preview timeout, and allow small scroll ranges to reattach at bottom. * fix(client): respect caller timeout budgets * fix(cli): keep diagnostic probes responsive * Refactor daemon client request options * Preserve daemon client legacy overloads
This commit is contained in:
@@ -88,11 +88,13 @@ export async function archiveAgentFromDaemon(
|
||||
|
||||
export async function fetchAgentArchivedAt(
|
||||
client: {
|
||||
fetchAgent(agentId: string): Promise<{ agent: { archivedAt?: string | null } } | null>;
|
||||
fetchAgent(options: {
|
||||
agentId: string;
|
||||
}): Promise<{ agent: { archivedAt?: string | null } } | null>;
|
||||
},
|
||||
agentId: string,
|
||||
): Promise<string | null> {
|
||||
const result = await client.fetchAgent(agentId);
|
||||
const result = await client.fetchAgent({ agentId });
|
||||
return result?.agent.archivedAt ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,9 +132,9 @@ export interface SeedDaemonClient {
|
||||
timeout?: number,
|
||||
): Promise<{ status: string; final?: { lastError?: string | null } | null }>;
|
||||
archiveAgent(agentId: string): Promise<{ archivedAt: string }>;
|
||||
fetchAgent(
|
||||
agentId: string,
|
||||
): Promise<{ agent: { id: string; archivedAt?: string | null } } | null>;
|
||||
fetchAgent(options: {
|
||||
agentId: string;
|
||||
}): Promise<{ agent: { id: string; archivedAt?: string | null } } | null>;
|
||||
getLastServerInfoMessage(): {
|
||||
features?: { projectAdd?: boolean; worktreeRestore?: boolean } | null;
|
||||
} | null;
|
||||
|
||||
@@ -404,4 +404,211 @@ describe("createWebStreamStrategy", () => {
|
||||
|
||||
expect(scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not force bottom after upward wheel when cached scroll top is stale", async () => {
|
||||
const scrollTo = vi.fn(function (
|
||||
this: HTMLElement,
|
||||
options?: ScrollToOptions | number,
|
||||
y?: number,
|
||||
) {
|
||||
const top = typeof options === "object" ? (options.top ?? 0) : (y ?? 0);
|
||||
Object.defineProperty(this, "scrollTop", {
|
||||
configurable: true,
|
||||
value: top,
|
||||
});
|
||||
});
|
||||
HTMLElement.prototype.scrollTo = scrollTo;
|
||||
|
||||
const strategy = createWebStreamStrategy({ isMobileBreakpoint: true });
|
||||
const viewportRef = React.createRef<StreamViewportHandle>();
|
||||
const historyMounted = Array.from({ length: 20 }, (_, index) => userMessage(index));
|
||||
const routeBottomAnchorRequest = {
|
||||
agentId: "agent",
|
||||
reason: "initial-entry" as const,
|
||||
requestKey: "server:agent:initial-entry",
|
||||
};
|
||||
const renderInput = {
|
||||
agentId: "agent",
|
||||
segments: {
|
||||
historyVirtualized: [],
|
||||
historyMounted,
|
||||
liveHead: [],
|
||||
},
|
||||
boundary: {
|
||||
hasVirtualizedHistory: false,
|
||||
hasMountedHistory: true,
|
||||
hasLiveHead: false,
|
||||
},
|
||||
renderers: createRenderers(vi.fn()),
|
||||
listEmptyComponent: null,
|
||||
viewportRef,
|
||||
routeBottomAnchorRequest,
|
||||
onNearBottomChange: vi.fn(),
|
||||
onNearHistoryStart: vi.fn(),
|
||||
isLoadingOlderHistory: false,
|
||||
hasOlderHistory: false,
|
||||
scrollEnabled: true,
|
||||
listStyle: null,
|
||||
baseListContentContainerStyle: null,
|
||||
forwardListContentContainerStyle: null,
|
||||
};
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root?.render(
|
||||
strategy.render({
|
||||
...renderInput,
|
||||
isAuthoritativeHistoryReady: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const scrollContainer = container.querySelector('[data-testid="agent-chat-scroll"]');
|
||||
if (!(scrollContainer instanceof HTMLElement)) {
|
||||
throw new Error("Expected agent chat scroll container");
|
||||
}
|
||||
Object.defineProperty(scrollContainer, "clientHeight", { configurable: true, value: 500 });
|
||||
Object.defineProperty(scrollContainer, "scrollHeight", { configurable: true, value: 1491 });
|
||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 0 });
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
});
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
scrollTo.mockClear();
|
||||
|
||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 991 });
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new WheelEvent("wheel", { deltaY: -900 }));
|
||||
});
|
||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 91 });
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
expect(scrollTo).not.toHaveBeenCalled();
|
||||
|
||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 0 });
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
expect(scrollTo).not.toHaveBeenCalled();
|
||||
|
||||
Object.defineProperty(scrollContainer, "scrollHeight", { configurable: true, value: 2531 });
|
||||
act(() => {
|
||||
root?.render(
|
||||
strategy.render({
|
||||
...renderInput,
|
||||
isAuthoritativeHistoryReady: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reattaches follow-output when a small scroll range returns to bottom", async () => {
|
||||
const scrollTo = vi.fn(function (
|
||||
this: HTMLElement,
|
||||
options?: ScrollToOptions | number,
|
||||
y?: number,
|
||||
) {
|
||||
const top = typeof options === "object" ? (options.top ?? 0) : (y ?? 0);
|
||||
Object.defineProperty(this, "scrollTop", {
|
||||
configurable: true,
|
||||
value: top,
|
||||
});
|
||||
});
|
||||
HTMLElement.prototype.scrollTo = scrollTo;
|
||||
|
||||
const strategy = createWebStreamStrategy({ isMobileBreakpoint: true });
|
||||
const viewportRef = React.createRef<StreamViewportHandle>();
|
||||
const renderInput = {
|
||||
agentId: "agent",
|
||||
segments: {
|
||||
historyVirtualized: [],
|
||||
historyMounted: [userMessage(1), userMessage(2)],
|
||||
liveHead: [],
|
||||
},
|
||||
boundary: {
|
||||
hasVirtualizedHistory: false,
|
||||
hasMountedHistory: true,
|
||||
hasLiveHead: false,
|
||||
},
|
||||
renderers: createRenderers(vi.fn()),
|
||||
listEmptyComponent: null,
|
||||
viewportRef,
|
||||
routeBottomAnchorRequest: null,
|
||||
onNearBottomChange: vi.fn(),
|
||||
onNearHistoryStart: vi.fn(),
|
||||
isLoadingOlderHistory: false,
|
||||
hasOlderHistory: false,
|
||||
scrollEnabled: true,
|
||||
listStyle: null,
|
||||
baseListContentContainerStyle: null,
|
||||
forwardListContentContainerStyle: null,
|
||||
};
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root?.render(
|
||||
strategy.render({
|
||||
...renderInput,
|
||||
isAuthoritativeHistoryReady: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const scrollContainer = container.querySelector('[data-testid="agent-chat-scroll"]');
|
||||
if (!(scrollContainer instanceof HTMLElement)) {
|
||||
throw new Error("Expected agent chat scroll container");
|
||||
}
|
||||
Object.defineProperty(scrollContainer, "clientHeight", { configurable: true, value: 500 });
|
||||
Object.defineProperty(scrollContainer, "scrollHeight", { configurable: true, value: 550 });
|
||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 50 });
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
});
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
scrollTo.mockClear();
|
||||
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new WheelEvent("wheel", { deltaY: -30 }));
|
||||
});
|
||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 20 });
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
expect(scrollTo).not.toHaveBeenCalled();
|
||||
|
||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 50 });
|
||||
act(() => {
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
scrollTo.mockClear();
|
||||
|
||||
act(() => {
|
||||
root?.render(
|
||||
strategy.render({
|
||||
...renderInput,
|
||||
segments: {
|
||||
...renderInput.segments,
|
||||
liveHead: [userMessage(3)],
|
||||
},
|
||||
isAuthoritativeHistoryReady: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
});
|
||||
|
||||
expect(scrollTo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,11 +117,12 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
contentRef.current = node;
|
||||
}, []);
|
||||
const [followOutput, setFollowOutputr] = useState(true);
|
||||
const followOutputRef = useRef(followOutput);
|
||||
const setFollowOutput = (value: boolean) => {
|
||||
followOutputRef.current = value;
|
||||
setFollowOutputr(value);
|
||||
return value;
|
||||
};
|
||||
const followOutputRef = useRef(followOutput);
|
||||
const lastKnownScrollTopRef = useRef(0);
|
||||
const pendingUserScrollUpIntentRef = useRef(false);
|
||||
const isPointerScrollActiveRef = useRef(false);
|
||||
@@ -277,12 +278,14 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
const currentScrollTop = scrollContainer.scrollTop;
|
||||
const isAtBottom = isScrollContainerAtBottom(scrollContainer);
|
||||
const scrolledUp = currentScrollTop < lastKnownScrollTopRef.current - USER_SCROLL_DELTA_EPSILON;
|
||||
const scrolledDown =
|
||||
currentScrollTop > lastKnownScrollTopRef.current + USER_SCROLL_DELTA_EPSILON;
|
||||
|
||||
if (!followOutputRef.current && isAtBottom) {
|
||||
if (!followOutputRef.current && isAtBottom && scrolledDown) {
|
||||
setFollowOutput(true);
|
||||
pendingUserScrollUpIntentRef.current = false;
|
||||
} else if (followOutputRef.current && pendingUserScrollUpIntentRef.current) {
|
||||
if (scrolledUp) {
|
||||
if (scrolledUp || !isAtBottom) {
|
||||
cancelPendingStickToBottom();
|
||||
setFollowOutput(false);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ function HostAgentReadyRouteContent() {
|
||||
|
||||
let cancelled = false;
|
||||
void client
|
||||
.fetchAgent(agentId)
|
||||
.fetchAgent({ agentId })
|
||||
.then((result) => {
|
||||
if (cancelled || redirectedRef.current) {
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useClientActivity } from "@/hooks/use-client-activity";
|
||||
import { usePushTokenRegistration } from "@/hooks/use-push-token-registration";
|
||||
import { clearArchiveAgentPending } from "@/hooks/use-archive-agent";
|
||||
import { refreshAgentInitializationTimeout } from "@/hooks/use-agent-initialization";
|
||||
import { prefetchProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||
import { generateMessageId, type StreamItem } from "@/types/stream";
|
||||
import {
|
||||
@@ -1210,6 +1211,16 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
error: payload.error,
|
||||
});
|
||||
if (followUp?.direction === "after") {
|
||||
refreshAgentInitializationTimeout({
|
||||
key: initKey,
|
||||
agentId,
|
||||
setAgentInitializing: (id, initializing) => {
|
||||
if (initializing) {
|
||||
return;
|
||||
}
|
||||
clearAgentInitializingFlag(setInitializingAgents, serverId, id);
|
||||
},
|
||||
});
|
||||
requestCanonicalCatchUp(agentId, {
|
||||
epoch: followUp.cursor.epoch,
|
||||
endSeq: followUp.cursor.seq,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import {
|
||||
type AgentCommandsClient,
|
||||
type DraftCommandConfig,
|
||||
fetchAgentCommands,
|
||||
} from "./use-agent-commands-query";
|
||||
|
||||
type ListCommands = DaemonClient["listCommands"];
|
||||
type ListCommands = AgentCommandsClient["listCommands"];
|
||||
type ListCommandsResult = Awaited<ReturnType<ListCommands>>;
|
||||
|
||||
interface ListCommandsCall {
|
||||
@@ -22,15 +21,8 @@ function createClient(response: ListCommandsResult): FakeAgentCommandsClient {
|
||||
const calls: ListCommandsCall[] = [];
|
||||
return {
|
||||
calls,
|
||||
listCommands: (async (
|
||||
agentId: string,
|
||||
requestIdOrOptions?: string | { draftConfig?: DraftCommandConfig },
|
||||
) => {
|
||||
const options =
|
||||
typeof requestIdOrOptions === "object" && requestIdOrOptions !== null
|
||||
? requestIdOrOptions
|
||||
: undefined;
|
||||
calls.push({ agentId, draftConfig: options?.draftConfig });
|
||||
listCommands: (async (options: Parameters<ListCommands>[0]) => {
|
||||
calls.push({ agentId: options.agentId, draftConfig: options.draftConfig });
|
||||
return response;
|
||||
}) as ListCommands,
|
||||
};
|
||||
|
||||
@@ -16,14 +16,22 @@ export interface AgentSlashCommand {
|
||||
|
||||
export type DraftCommandConfig = AgentCommandsDraftConfig;
|
||||
|
||||
export type AgentCommandsClient = Pick<DaemonClient, "listCommands">;
|
||||
interface ListAgentCommandsOptions {
|
||||
agentId: string;
|
||||
draftConfig?: DraftCommandConfig;
|
||||
}
|
||||
|
||||
export interface AgentCommandsClient {
|
||||
listCommands(options: ListAgentCommandsOptions): ReturnType<DaemonClient["listCommands"]>;
|
||||
}
|
||||
|
||||
export async function fetchAgentCommands(input: {
|
||||
client: AgentCommandsClient;
|
||||
agentId: string;
|
||||
draftConfig?: DraftCommandConfig;
|
||||
}): Promise<AgentSlashCommand[]> {
|
||||
const response = await input.client.listCommands(input.agentId, {
|
||||
const response = await input.client.listCommands({
|
||||
agentId: input.agentId,
|
||||
draftConfig: input.draftConfig,
|
||||
});
|
||||
return response.commands as AgentSlashCommand[];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getInitDeferred, getInitKey, resolveInitDeferred } from "@/utils/agent-
|
||||
import {
|
||||
createSetAgentInitializing,
|
||||
ensureAgentIsInitialized,
|
||||
refreshAgentInitializationTimeout,
|
||||
refreshAgent,
|
||||
} from "./use-agent-initialization";
|
||||
|
||||
@@ -80,7 +81,7 @@ describe("ensureAgentIsInitialized", () => {
|
||||
expect(getInitDeferred(getInitKey(serverId, agentId))?.requestDirection).toBe("tail");
|
||||
});
|
||||
|
||||
it("times out initialization after 30 seconds", async () => {
|
||||
it("times out initialization after 65 seconds", async () => {
|
||||
vi.useFakeTimers();
|
||||
const client = makeClient();
|
||||
useSessionStore.getState().initializeSession(serverId, client as never);
|
||||
@@ -92,18 +93,50 @@ describe("ensureAgentIsInitialized", () => {
|
||||
setAgentInitializing: bindSetAgentInitializing(),
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(29_999);
|
||||
vi.advanceTimersByTime(64_999);
|
||||
expect(getInitDeferred(getInitKey(serverId, agentId))).toBeDefined();
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
await expect(promise).rejects.toThrow("History sync timed out after 30s");
|
||||
await expect(promise).rejects.toThrow("History sync timed out after 65s");
|
||||
expect(getInitDeferred(getInitKey(serverId, agentId))).toBeUndefined();
|
||||
expect(useSessionStore.getState().sessions[serverId]?.initializingAgents.get(agentId)).toBe(
|
||||
false,
|
||||
);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("refreshes the initialization timeout after paged catch-up progress", async () => {
|
||||
vi.useFakeTimers();
|
||||
const client = makeClient();
|
||||
useSessionStore.getState().initializeSession(serverId, client as never);
|
||||
const setAgentInitializing = bindSetAgentInitializing();
|
||||
const key = getInitKey(serverId, agentId);
|
||||
|
||||
const promise = ensureAgentIsInitialized({
|
||||
serverId,
|
||||
agentId,
|
||||
client: client as never,
|
||||
setAgentInitializing,
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(64_999);
|
||||
refreshAgentInitializationTimeout({ key, agentId, setAgentInitializing });
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(getInitDeferred(key)).toBeDefined();
|
||||
|
||||
const rejection = expect(promise).rejects.toThrow("History sync timed out after 65s");
|
||||
|
||||
vi.advanceTimersByTime(64_998);
|
||||
expect(getInitDeferred(key)).toBeDefined();
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
await rejection;
|
||||
expect(getInitDeferred(key)).toBeUndefined();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshAgent", () => {
|
||||
|
||||
@@ -3,19 +3,36 @@ import { useTranslation } from "react-i18next";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
attachInitTimeout,
|
||||
createInitDeferred,
|
||||
getInitDeferred,
|
||||
getInitKey,
|
||||
INIT_TIMEOUT_MS,
|
||||
rejectInitDeferred,
|
||||
refreshInitTimeout,
|
||||
} from "@/utils/agent-initialization";
|
||||
import { planInitialAgentTimelineSync, planTimelineTailFetch } from "@/timeline/timeline-sync-plan";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
|
||||
export const INIT_TIMEOUT_MS = 30_000;
|
||||
|
||||
export type SetAgentInitializing = (agentId: string, initializing: boolean) => void;
|
||||
|
||||
export function createHistorySyncTimeoutError(): Error {
|
||||
return new Error(`History sync timed out after ${Math.round(INIT_TIMEOUT_MS / 1000)}s`);
|
||||
}
|
||||
|
||||
export function refreshAgentInitializationTimeout(input: {
|
||||
key: string;
|
||||
agentId: string;
|
||||
setAgentInitializing: SetAgentInitializing;
|
||||
}): void {
|
||||
refreshInitTimeout({
|
||||
key: input.key,
|
||||
onTimeout: () => {
|
||||
input.setAgentInitializing(input.agentId, false);
|
||||
rejectInitDeferred(input.key, createHistorySyncTimeoutError());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface EnsureAgentIsInitializedInput {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
@@ -38,14 +55,7 @@ export function ensureAgentIsInitialized(input: EnsureAgentIsInitializedInput):
|
||||
const timelineRequest = planInitialAgentTimelineSync({ cursor, hasAuthoritativeHistory });
|
||||
|
||||
const deferred = createInitDeferred(key, timelineRequest.direction);
|
||||
const timeoutId = setTimeout(() => {
|
||||
setAgentInitializing(agentId, false);
|
||||
rejectInitDeferred(
|
||||
key,
|
||||
new Error(`History sync timed out after ${Math.round(INIT_TIMEOUT_MS / 1000)}s`),
|
||||
);
|
||||
}, INIT_TIMEOUT_MS);
|
||||
attachInitTimeout(key, timeoutId);
|
||||
refreshAgentInitializationTimeout({ key, agentId, setAgentInitializing });
|
||||
|
||||
setAgentInitializing(agentId, true);
|
||||
|
||||
|
||||
@@ -588,7 +588,7 @@ function AgentPanelBody({
|
||||
const attemptToken = ++lookupAttemptTokenRef.current;
|
||||
|
||||
client
|
||||
.fetchAgent(agentId)
|
||||
.fetchAgent({ agentId })
|
||||
.then((result) => {
|
||||
if (attemptToken !== lookupAttemptTokenRef.current) {
|
||||
return;
|
||||
@@ -985,7 +985,7 @@ function ChatAgentContent({
|
||||
const currentAgent =
|
||||
currentSession?.agents.get(agentId) ?? currentSession?.agentDetails.get(agentId);
|
||||
if (!currentAgent) {
|
||||
const result = await client.fetchAgent(agentId);
|
||||
const result = await client.fetchAgent({ agentId });
|
||||
if (attemptToken !== initAttemptTokenRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface DeferredInit {
|
||||
}
|
||||
|
||||
const initPromises = new Map<string, DeferredInit>();
|
||||
export const INIT_TIMEOUT_MS = 65_000;
|
||||
|
||||
export function getInitKey(serverId: string, agentId: string): string {
|
||||
return `${serverId}:${agentId}`;
|
||||
@@ -36,12 +37,20 @@ export function createInitDeferred(key: string, requestDirection: "tail" | "afte
|
||||
return deferred;
|
||||
}
|
||||
|
||||
export function attachInitTimeout(key: string, timeoutId: ReturnType<typeof setTimeout>): void {
|
||||
const deferred = initPromises.get(key);
|
||||
export function refreshInitTimeout(input: {
|
||||
key: string;
|
||||
onTimeout: () => void;
|
||||
timeoutMs?: number;
|
||||
}): void {
|
||||
const timeoutId = setTimeout(input.onTimeout, input.timeoutMs ?? INIT_TIMEOUT_MS);
|
||||
const deferred = initPromises.get(input.key);
|
||||
if (!deferred) {
|
||||
clearTimeout(timeoutId);
|
||||
return;
|
||||
}
|
||||
if (deferred.timeoutId) {
|
||||
clearTimeout(deferred.timeoutId);
|
||||
}
|
||||
deferred.timeoutId = timeoutId;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ export function addAttachOptions(cmd: Command): Command {
|
||||
.argument("<id>", "Agent ID (or prefix)");
|
||||
}
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import { fetchProjectedTimelineItems } from "../../utils/timeline.js";
|
||||
import {
|
||||
fetchProjectedTimelineItems,
|
||||
LIVE_HISTORY_FETCH_TIMEOUT_MS,
|
||||
} from "../../utils/timeline.js";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
import type { AgentStreamEventPayload, AgentStreamMessage } from "@getpaseo/protocol/messages";
|
||||
@@ -121,7 +124,7 @@ export async function runAttachCommand(
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchResult = await client.fetchAgent(id);
|
||||
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");
|
||||
@@ -139,6 +142,7 @@ export async function runAttachCommand(
|
||||
const timelineItems = await fetchProjectedTimelineItems({
|
||||
client,
|
||||
agentId: resolvedId,
|
||||
timeoutMs: LIVE_HISTORY_FETCH_TIMEOUT_MS,
|
||||
});
|
||||
for (const item of timelineItems) {
|
||||
printTimelineItem(item);
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function runDeleteCommand(
|
||||
return isSameOrDescendantPath(options.cwd!, a.cwd);
|
||||
});
|
||||
} else if (id) {
|
||||
const fetchResult = await client.fetchAgent(id);
|
||||
const fetchResult = await client.fetchAgent({ agentId: id });
|
||||
if (!fetchResult) {
|
||||
const error: CommandError = {
|
||||
code: "AGENT_NOT_FOUND",
|
||||
|
||||
@@ -243,7 +243,7 @@ export async function runInspectCommand(
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchResult = await client.fetchAgent(agentIdArg);
|
||||
const fetchResult = await client.fetchAgent({ agentId: agentIdArg });
|
||||
if (!fetchResult) {
|
||||
const error: CommandError = {
|
||||
code: "AGENT_NOT_FOUND",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Command } from "commander";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandOptions } from "../../output/index.js";
|
||||
import { fetchProjectedTimelineItems } from "../../utils/timeline.js";
|
||||
import {
|
||||
fetchProjectedTimelineItems,
|
||||
LIVE_HISTORY_FETCH_TIMEOUT_MS,
|
||||
} from "../../utils/timeline.js";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
import type { AgentStreamMessage } from "@getpaseo/protocol/messages";
|
||||
@@ -32,8 +35,9 @@ export const NO_ACTIVITY_MESSAGE = "No activity to display.";
|
||||
export async function fetchAgentTimelineItems(
|
||||
client: DaemonClient,
|
||||
agentId: string,
|
||||
options?: { timeoutMs?: number },
|
||||
): Promise<AgentTimelineItem[]> {
|
||||
return fetchProjectedTimelineItems({ client, agentId });
|
||||
return fetchProjectedTimelineItems({ client, agentId, timeoutMs: options?.timeoutMs });
|
||||
}
|
||||
|
||||
export function formatAgentActivityTranscript(
|
||||
@@ -107,7 +111,7 @@ export async function runLogsCommand(
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchResult = await client.fetchAgent(id);
|
||||
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");
|
||||
@@ -173,7 +177,14 @@ async function runFollowMode(
|
||||
const tailCount = parseTailCount(options.tail) ?? DEFAULT_FOLLOW_TAIL;
|
||||
|
||||
// First, get existing timeline.
|
||||
let existingItems = await fetchAgentTimelineItems(client, agentId);
|
||||
let existingItems: AgentTimelineItem[] = [];
|
||||
try {
|
||||
existingItems = await fetchAgentTimelineItems(client, agentId, {
|
||||
timeoutMs: LIVE_HISTORY_FETCH_TIMEOUT_MS,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Warning: failed to fetch existing timeline", error);
|
||||
}
|
||||
|
||||
// Apply filter to existing items
|
||||
if (options.filter) {
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function runModeCommand(
|
||||
let client: Awaited<ReturnType<typeof connectToDaemon>> | undefined;
|
||||
try {
|
||||
client = await connectToDaemon({ host: options.host });
|
||||
const fetchResult = await client.fetchAgent(id);
|
||||
const fetchResult = await client.fetchAgent({ agentId: id });
|
||||
if (!fetchResult) {
|
||||
const error: CommandError = {
|
||||
code: "AGENT_NOT_FOUND",
|
||||
|
||||
@@ -83,7 +83,7 @@ export async function runStopCommand(
|
||||
});
|
||||
} else if (id) {
|
||||
// Stop specific agent
|
||||
const fetchResult = await client.fetchAgent(id);
|
||||
const fetchResult = await client.fetchAgent({ agentId: id });
|
||||
if (!fetchResult) {
|
||||
const error: CommandError = {
|
||||
code: "AGENT_NOT_FOUND",
|
||||
|
||||
@@ -132,7 +132,7 @@ export async function runUpdateCommand(
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchResult = await client.fetchAgent(agentIdArg);
|
||||
const fetchResult = await client.fetchAgent({ agentId: agentIdArg });
|
||||
if (!fetchResult) {
|
||||
const error: CommandError = {
|
||||
code: "AGENT_NOT_FOUND",
|
||||
@@ -148,7 +148,7 @@ export async function runUpdateCommand(
|
||||
...(Object.keys(labels).length > 0 ? { labels } : {}),
|
||||
});
|
||||
|
||||
const updatedResult = await client.fetchAgent(agentId);
|
||||
const updatedResult = await client.fetchAgent({ agentId });
|
||||
if (!updatedResult) {
|
||||
throw new Error(`Agent not found after update: ${agentId}`);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface AgentWaitOptions extends CommandOptions {
|
||||
}
|
||||
|
||||
const WAIT_ACTIVITY_PREVIEW_COUNT = 5;
|
||||
const WAIT_ACTIVITY_PREVIEW_TIMEOUT_MS = 2_000;
|
||||
|
||||
function appendRecentActivity(message: string, transcript: string | null): string {
|
||||
if (!transcript || transcript.trim().length === 0) {
|
||||
@@ -46,7 +47,9 @@ async function getRecentActivityTranscript(
|
||||
agentId: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const timelineItems = await fetchAgentTimelineItems(client, agentId);
|
||||
const timelineItems = await fetchAgentTimelineItems(client, agentId, {
|
||||
timeoutMs: WAIT_ACTIVITY_PREVIEW_TIMEOUT_MS,
|
||||
});
|
||||
return formatAgentActivityTranscript(timelineItems, WAIT_ACTIVITY_PREVIEW_COUNT);
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -26,6 +26,7 @@ export async function connectChatClient(host?: string) {
|
||||
export async function attachAgentNamesToMessages(
|
||||
client: Awaited<ReturnType<typeof connectToDaemon>>,
|
||||
messages: ChatMessageRow[],
|
||||
options: { timeout?: number; bestEffort?: boolean } = {},
|
||||
): Promise<ChatMessageRow[]> {
|
||||
const agentIds = new Set<string>();
|
||||
for (const message of messages) {
|
||||
@@ -39,9 +40,18 @@ export async function attachAgentNamesToMessages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
const payload = await client.fetchAgents({
|
||||
filter: { includeArchived: true },
|
||||
});
|
||||
let payload: Awaited<ReturnType<typeof client.fetchAgents>>;
|
||||
try {
|
||||
payload = await client.fetchAgents({
|
||||
filter: { includeArchived: true },
|
||||
...(typeof options.timeout === "number" ? { timeout: options.timeout } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (options.bestEffort) {
|
||||
return messages;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const agentNames = new Map<string, string>();
|
||||
for (const entry of payload.entries) {
|
||||
const title = entry.agent.title?.trim();
|
||||
|
||||
@@ -13,26 +13,44 @@ export interface ChatWaitOptions extends ChatCommandOptions {
|
||||
timeout?: string;
|
||||
}
|
||||
|
||||
const CHAT_WAIT_PREFLIGHT_TIMEOUT_MS = 2000;
|
||||
|
||||
export async function runWaitCommand(
|
||||
room: string,
|
||||
options: ChatWaitOptions,
|
||||
_command: Command,
|
||||
): Promise<ListResult<ChatMessageRow>> {
|
||||
const timeoutMs = parseTimeoutMs(options.timeout);
|
||||
const { client } = await connectChatClient(options.host);
|
||||
const deadline = typeof timeoutMs === "number" ? Date.now() + timeoutMs : null;
|
||||
const hasExplicitTimeout = deadline !== null;
|
||||
const remainingTimeoutMs = () =>
|
||||
deadline === null ? undefined : Math.max(1, deadline - Date.now());
|
||||
try {
|
||||
const latest = await client.readChatMessages({
|
||||
room,
|
||||
limit: 1,
|
||||
...(hasExplicitTimeout
|
||||
? {
|
||||
timeout: Math.min(remainingTimeoutMs() ?? 1, CHAT_WAIT_PREFLIGHT_TIMEOUT_MS),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const afterMessageId = latest.messages[0]?.id;
|
||||
const payload = await client.waitForChatMessages({
|
||||
room,
|
||||
afterMessageId,
|
||||
timeoutMs: parseTimeoutMs(options.timeout),
|
||||
timeoutMs: remainingTimeoutMs() ?? timeoutMs,
|
||||
});
|
||||
const messages = await attachAgentNamesToMessages(
|
||||
client,
|
||||
payload.messages.map(toChatMessageRow),
|
||||
hasExplicitTimeout
|
||||
? {
|
||||
timeout: remainingTimeoutMs(),
|
||||
bestEffort: true,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
return {
|
||||
type: "list",
|
||||
|
||||
@@ -667,7 +667,9 @@ async function requestLifecycleShutdown(
|
||||
};
|
||||
}
|
||||
|
||||
const client = await tryConnectToDaemon({ host, timeout: Math.min(timeoutMs, 5000) });
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const remainingTimeoutMs = () => Math.max(1, deadline - Date.now());
|
||||
const client = await tryConnectToDaemon({ host, timeout: Math.min(remainingTimeoutMs(), 5000) });
|
||||
if (!client) {
|
||||
return {
|
||||
requested: false,
|
||||
@@ -676,7 +678,7 @@ async function requestLifecycleShutdown(
|
||||
}
|
||||
|
||||
try {
|
||||
await client.shutdownServer();
|
||||
await client.shutdownServer({ timeout: Math.min(remainingTimeoutMs(), 5000) });
|
||||
return { requested: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -696,8 +698,10 @@ export async function stopLocalDaemon(
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
|
||||
const killTimeoutMs = options.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
|
||||
const state = resolveLocalDaemonState({ home: options.home });
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const remainingTimeoutMs = () => Math.max(1, deadline - Date.now());
|
||||
|
||||
const shutdownAttempt = await requestLifecycleShutdown(state, timeoutMs);
|
||||
const shutdownAttempt = await requestLifecycleShutdown(state, remainingTimeoutMs());
|
||||
const lifecycleRequested = shutdownAttempt.requested;
|
||||
|
||||
if (!state.pidInfo || (!state.running && !lifecycleRequested)) {
|
||||
@@ -720,7 +724,7 @@ export async function stopLocalDaemon(
|
||||
const { stopped, forced } = await waitForStopAfterRequest({
|
||||
state,
|
||||
pid,
|
||||
timeoutMs,
|
||||
timeoutMs: remainingTimeoutMs(),
|
||||
killTimeoutMs,
|
||||
force: options.force,
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ interface PairOptions {
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
const PAIRING_DAEMON_RPC_TIMEOUT_MS = 1500;
|
||||
|
||||
export function pairCommand(): Command {
|
||||
return addJsonOption(new Command("pair").description("Print the daemon pairing QR code and link"))
|
||||
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
|
||||
@@ -35,7 +37,9 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
|
||||
client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
|
||||
if (supportsDaemonStatusRpc) {
|
||||
try {
|
||||
const offer = await client.getDaemonPairingOffer();
|
||||
const offer = await client.getDaemonPairingOffer({
|
||||
timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
|
||||
});
|
||||
await client.close().catch(() => {});
|
||||
outputPairingResult(
|
||||
{ relayEnabled: offer.relayEnabled, url: offer.url, qr: offer.qr ?? null },
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { CommandOptions, ListResult, OutputSchema } from "../../output/inde
|
||||
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
||||
import { resolveNodePathFromPid } from "./runtime-toolchain.js";
|
||||
|
||||
const DAEMON_STATUS_PROBE_TIMEOUT_MS = 1500;
|
||||
|
||||
interface ProviderBinaryStatus {
|
||||
label: string;
|
||||
path: string | null;
|
||||
@@ -276,7 +278,10 @@ async function probeDaemonOverWebsocket(args: {
|
||||
const supportsDaemonStatusRpc =
|
||||
client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
|
||||
try {
|
||||
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
|
||||
const agentsPayload = await client.fetchAgents({
|
||||
filter: { includeArchived: true },
|
||||
timeout: DAEMON_STATUS_PROBE_TIMEOUT_MS,
|
||||
});
|
||||
const agents = agentsPayload.entries.map((entry) => entry.agent);
|
||||
const runningAgents = agents.filter((a) => a.status === "running").length;
|
||||
const idleAgents = agents.filter((a) => a.status === "idle").length;
|
||||
@@ -284,7 +289,9 @@ async function probeDaemonOverWebsocket(args: {
|
||||
let daemonProviders: ProviderBinaryStatus[] | undefined;
|
||||
if (supportsDaemonStatusRpc) {
|
||||
try {
|
||||
const statusPayload = await client.getDaemonStatus();
|
||||
const statusPayload = await client.getDaemonStatus({
|
||||
timeout: DAEMON_STATUS_PROBE_TIMEOUT_MS,
|
||||
});
|
||||
const labelMap = new Map(PROVIDER_BINARIES.map((p) => [p.binary, p.label]));
|
||||
daemonProviders = statusPayload.providers.map((p) => ({
|
||||
label: labelMap.get(p.provider) ?? p.provider,
|
||||
|
||||
@@ -40,6 +40,7 @@ type OnboardPersistedConfig = PersistedConfig & {
|
||||
};
|
||||
|
||||
const DEFAULT_READY_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const READY_PROBE_TIMEOUT_MS = 1200;
|
||||
|
||||
class OnboardCancelledError extends Error {}
|
||||
|
||||
@@ -201,15 +202,22 @@ function renderProgressLine(progress: DownloadProgress): string {
|
||||
|
||||
type ProbeResult = { kind: "ready"; listen: string; host: string | null } | { kind: "pending" };
|
||||
|
||||
async function probeDaemonReady(home: string): Promise<ProbeResult> {
|
||||
async function probeDaemonReady(home: string, timeoutMs: number): Promise<ProbeResult> {
|
||||
const state = resolveLocalDaemonState({ home });
|
||||
const host = resolveTcpHostFromListen(state.listen);
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const remainingTimeoutMs = () => Math.max(1, deadline - Date.now());
|
||||
|
||||
if (state.running && host) {
|
||||
const client = await tryConnectToDaemon({ host, timeout: 1200 });
|
||||
const client = await tryConnectToDaemon({
|
||||
host,
|
||||
timeout: Math.min(remainingTimeoutMs(), READY_PROBE_TIMEOUT_MS),
|
||||
});
|
||||
if (client) {
|
||||
try {
|
||||
await client.fetchAgents();
|
||||
await client.fetchAgents({
|
||||
timeout: Math.min(remainingTimeoutMs(), READY_PROBE_TIMEOUT_MS),
|
||||
});
|
||||
return { kind: "ready", listen: state.listen, host };
|
||||
} catch {
|
||||
// Daemon process is alive but not API-ready yet.
|
||||
@@ -255,23 +263,29 @@ async function waitForDaemonReady(args: {
|
||||
onStatus?: (message: string) => void;
|
||||
}): Promise<{ listen: string; host: string | null }> {
|
||||
const deadline = Date.now() + args.timeoutMs;
|
||||
const createTimeoutError = () => {
|
||||
const recentLogs = tailDaemonLog(args.home, 60);
|
||||
return new Error(
|
||||
[
|
||||
`Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`,
|
||||
recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
);
|
||||
};
|
||||
|
||||
async function poll(state: ProgressState): Promise<{ listen: string; host: string | null }> {
|
||||
const probe = await probeDaemonReady(args.home);
|
||||
if (Date.now() >= deadline) {
|
||||
throw createTimeoutError();
|
||||
}
|
||||
const probe = await probeDaemonReady(args.home, Math.max(1, deadline - Date.now()));
|
||||
if (probe.kind === "ready") {
|
||||
return { listen: probe.listen, host: probe.host };
|
||||
}
|
||||
const nextState = announceProgress(args.home, state, args.onStatus);
|
||||
if (Date.now() >= deadline) {
|
||||
const recentLogs = tailDaemonLog(args.home, 60);
|
||||
throw new Error(
|
||||
[
|
||||
`Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`,
|
||||
recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
);
|
||||
throw createTimeoutError();
|
||||
}
|
||||
await sleep(200);
|
||||
return poll(nextState);
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function runAllowCommand(
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchResult = await client.fetchAgent(agentIdOrPrefix);
|
||||
const fetchResult = await client.fetchAgent({ agentId: agentIdOrPrefix });
|
||||
if (!fetchResult) {
|
||||
await client.close();
|
||||
const error: CommandError = {
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function runDenyCommand(
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchResult = await client.fetchAgent(agentIdOrPrefix);
|
||||
const fetchResult = await client.fetchAgent({ agentId: agentIdOrPrefix });
|
||||
if (!fetchResult) {
|
||||
await client.close();
|
||||
const error: CommandError = {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
|
||||
export const LIVE_HISTORY_FETCH_TIMEOUT_MS = 2_000;
|
||||
|
||||
interface FetchProjectedTimelineItemsInput {
|
||||
client: DaemonClient;
|
||||
agentId: string;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export async function fetchProjectedTimelineItems(
|
||||
@@ -13,6 +16,7 @@ export async function fetchProjectedTimelineItems(
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
projection: "projected",
|
||||
timeout: input.timeoutMs,
|
||||
});
|
||||
return timeline.entries.map((entry) => entry.item);
|
||||
}
|
||||
|
||||
@@ -522,6 +522,520 @@ test("keeps the transport connected when a session RPC ping times out", async ()
|
||||
expect(client.getConnectionState().status).toBe("connected");
|
||||
});
|
||||
|
||||
test("defaults session RPC waiters to sixty seconds", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.fetchAgent({
|
||||
agentId: "agent-1",
|
||||
requestId: "req-agent-1",
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "fetch_agent_request",
|
||||
requestId: "req-agent-1",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(59_999);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (60000ms)");
|
||||
});
|
||||
|
||||
test("honors explicit fetchAgent timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.fetchAgent({
|
||||
agentId: "agent-1",
|
||||
requestId: "req-agent-1",
|
||||
timeout: 5_000,
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "fetch_agent_request",
|
||||
requestId: "req-agent-1",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(4_999);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (5000ms)");
|
||||
});
|
||||
|
||||
test("preserves legacy fetchAgent id overload", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.fetchAgent("agent-1", "req-agent-legacy");
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "fetch_agent_request",
|
||||
requestId: "req-agent-legacy",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: "fetch_agent_response",
|
||||
payload: {
|
||||
requestId: "req-agent-legacy",
|
||||
agent: null,
|
||||
project: null,
|
||||
error: "legacy fetch sentinel",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(responsePromise).rejects.toThrow("legacy fetch sentinel");
|
||||
});
|
||||
|
||||
test("honors explicit fetchAgentTimeline timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.fetchAgentTimeline("agent-1", {
|
||||
requestId: "req-timeline-1",
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
projection: "projected",
|
||||
timeout: 2_000,
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "fetch_agent_timeline_request",
|
||||
requestId: "req-timeline-1",
|
||||
agentId: "agent-1",
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
projection: "projected",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_999);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (2000ms)");
|
||||
});
|
||||
|
||||
test("honors explicit fetchAgents timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.fetchAgents({
|
||||
requestId: "req-agents-1",
|
||||
scope: "active",
|
||||
timeout: 1_200,
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "fetch_agents_request",
|
||||
requestId: "req-agents-1",
|
||||
scope: "active",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_199);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1200ms)");
|
||||
});
|
||||
|
||||
test("honors explicit shutdownServer timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.shutdownServer({
|
||||
requestId: "req-shutdown-1",
|
||||
timeout: 1_500,
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "shutdown_server_request",
|
||||
requestId: "req-shutdown-1",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_499);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1500ms)");
|
||||
});
|
||||
|
||||
test("honors explicit readChatMessages timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.readChatMessages({
|
||||
requestId: "req-chat-read-1",
|
||||
room: "room-1",
|
||||
limit: 1,
|
||||
timeout: 2_500,
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "chat/read",
|
||||
requestId: "req-chat-read-1",
|
||||
room: "room-1",
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2_499);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (2500ms)");
|
||||
});
|
||||
|
||||
test("honors explicit getDaemonStatus timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.getDaemonStatus({
|
||||
requestId: "req-daemon-status-1",
|
||||
timeout: 1_500,
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "daemon.get_status.request",
|
||||
requestId: "req-daemon-status-1",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_499);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1500ms)");
|
||||
});
|
||||
|
||||
test("honors explicit getDaemonPairingOffer timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.getDaemonPairingOffer({
|
||||
requestId: "req-pairing-offer-1",
|
||||
timeout: 1_500,
|
||||
});
|
||||
let settled = false;
|
||||
void responsePromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "daemon.get_pairing_offer.request",
|
||||
requestId: "req-pairing-offer-1",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_499);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1500ms)");
|
||||
});
|
||||
|
||||
test("keeps waitForAgentUpsert initial fetch inside the requested deadline", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const waitPromise = client.waitForAgentUpsert("agent-1", () => false, 5_000);
|
||||
let settled = false;
|
||||
void waitPromise.then(
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "fetch_agent_request",
|
||||
requestId: expect.any(String),
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(4_999);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await expect(waitPromise).rejects.toThrow("Timed out waiting for agent agent-1");
|
||||
});
|
||||
|
||||
test("keeps default connect timeout shorter than session RPC waiters", async () => {
|
||||
useHeartbeatClock();
|
||||
try {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
let settled = false;
|
||||
const pendingConnect = client.connect().then(
|
||||
() => {
|
||||
settled = true;
|
||||
return { ok: true as const };
|
||||
},
|
||||
(error) => {
|
||||
settled = true;
|
||||
return { ok: false as const, error };
|
||||
},
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(14_999);
|
||||
expect(settled).toBe(false);
|
||||
expect(client.getConnectionState().status).toBe("connecting");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
const result = await pendingConnect;
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
if (result.error instanceof Error) {
|
||||
expect(result.error.message).toContain("Connection timed out");
|
||||
}
|
||||
}
|
||||
expect(client.getConnectionState().status).toBe("disconnected");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
test("stays online through ten minutes of pongs that arrive five seconds late", async () => {
|
||||
useHeartbeatClock();
|
||||
const session = new DaemonClientSession();
|
||||
@@ -1754,16 +2268,8 @@ test("sends explicit shutdown_server_request via shutdownServer", async () => {
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const lifecycleClient = client as unknown as {
|
||||
shutdownServer: (requestId?: string) => Promise<{
|
||||
status: "shutdown_requested";
|
||||
clientId: string;
|
||||
requestId: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
expect(typeof lifecycleClient.shutdownServer).toBe("function");
|
||||
const promise = lifecycleClient.shutdownServer("req-shutdown-1");
|
||||
expect(typeof client.shutdownServer).toBe("function");
|
||||
const promise = client.shutdownServer({ requestId: "req-shutdown-1" });
|
||||
|
||||
expect(mock.sent).toHaveLength(1);
|
||||
const request = parseSentFrame(mock.sent[0]);
|
||||
@@ -3347,7 +3853,8 @@ test("lists commands with draft config via RPC", async () => {
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const promise = client.listCommands("__new_agent__", {
|
||||
const promise = client.listCommands({
|
||||
agentId: "__new_agent__",
|
||||
draftConfig: {
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
@@ -3392,7 +3899,7 @@ test("lists commands with draft config via RPC", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("lists commands with legacy requestId signature via RPC", async () => {
|
||||
test("lists commands with explicit requestId via RPC", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
@@ -3409,13 +3916,16 @@ test("lists commands with legacy requestId signature via RPC", async () => {
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const promise = client.listCommands("agent-1", "req-legacy");
|
||||
const promise = client.listCommands({
|
||||
agentId: "agent-1",
|
||||
requestId: "req-commands",
|
||||
});
|
||||
expect(mock.sent).toHaveLength(1);
|
||||
|
||||
const request = parseSentFrame(mock.sent[0]);
|
||||
expect(request.type).toBe("list_commands_request");
|
||||
expect(request.agentId).toBe("agent-1");
|
||||
expect(request.requestId).toBe("req-legacy");
|
||||
expect(request.requestId).toBe("req-commands");
|
||||
expect(request.draftConfig).toBeUndefined();
|
||||
|
||||
mock.triggerMessage(
|
||||
@@ -3427,7 +3937,7 @@ test("lists commands with legacy requestId signature via RPC", async () => {
|
||||
agentId: "agent-1",
|
||||
commands: [],
|
||||
error: null,
|
||||
requestId: "req-legacy",
|
||||
requestId: "req-commands",
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -3437,7 +3947,113 @@ test("lists commands with legacy requestId signature via RPC", async () => {
|
||||
agentId: "agent-1",
|
||||
commands: [],
|
||||
error: null,
|
||||
requestId: "req-legacy",
|
||||
requestId: "req-commands",
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves legacy listCommands id overload", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const promise = client.listCommands("agent-1", "req-commands-legacy");
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "list_commands_request",
|
||||
requestId: "req-commands-legacy",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId: "agent-1",
|
||||
commands: [],
|
||||
error: null,
|
||||
requestId: "req-commands-legacy",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(promise).resolves.toEqual({
|
||||
agentId: "agent-1",
|
||||
commands: [],
|
||||
error: null,
|
||||
requestId: "req-commands-legacy",
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves legacy listCommands options overload", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const promise = client.listCommands("__new_agent__", {
|
||||
requestId: "req-commands-draft-legacy",
|
||||
draftConfig: {
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
},
|
||||
});
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "list_commands_request",
|
||||
requestId: "req-commands-draft-legacy",
|
||||
agentId: "__new_agent__",
|
||||
draftConfig: {
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
},
|
||||
});
|
||||
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId: "__new_agent__",
|
||||
commands: [{ name: "help", description: "Show help", argumentHint: "" }],
|
||||
error: null,
|
||||
requestId: "req-commands-draft-legacy",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(promise).resolves.toEqual({
|
||||
agentId: "__new_agent__",
|
||||
commands: [{ name: "help", description: "Show help", argumentHint: "" }],
|
||||
error: null,
|
||||
requestId: "req-commands-draft-legacy",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -374,9 +374,11 @@ export interface WriteProjectConfigInput {
|
||||
requestId?: string;
|
||||
}
|
||||
interface ListCommandsOptions {
|
||||
agentId: string;
|
||||
requestId?: string;
|
||||
draftConfig?: ListCommandsDraftConfig;
|
||||
}
|
||||
type LegacyListCommandsOptions = Omit<ListCommandsOptions, "agentId">;
|
||||
type SetVoiceModePayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: "set_voice_mode_response" }
|
||||
@@ -458,17 +460,64 @@ export type FetchAgentTimelinePayload = FetchAgentTimelineResponseMessage["paylo
|
||||
export type FetchAgentTimelineDirection = FetchAgentTimelinePayload["direction"];
|
||||
export type FetchAgentTimelineProjection = FetchAgentTimelinePayload["projection"];
|
||||
export type FetchAgentTimelineCursor = NonNullable<FetchAgentTimelinePayload["startCursor"]>;
|
||||
export interface FetchAgentOptions {
|
||||
agentId: string;
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
type LegacyFetchAgentOptions = Omit<FetchAgentOptions, "agentId">;
|
||||
export interface FetchAgentTimelineOptions {
|
||||
direction?: FetchAgentTimelineDirection;
|
||||
cursor?: FetchAgentTimelineCursor;
|
||||
limit?: number;
|
||||
projection?: FetchAgentTimelineProjection;
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// COMPAT(daemon-client-object-options): added in v0.1.102; remove after
|
||||
// 2026-12-29 once SDK callers have migrated to object parameters.
|
||||
function normalizeFetchAgentOptions(
|
||||
input: FetchAgentOptions | string,
|
||||
legacyOptions?: LegacyFetchAgentOptions | string,
|
||||
): FetchAgentOptions {
|
||||
if (typeof input !== "string") {
|
||||
return input;
|
||||
}
|
||||
if (typeof legacyOptions === "string") {
|
||||
return { agentId: input, requestId: legacyOptions };
|
||||
}
|
||||
return { agentId: input, ...legacyOptions };
|
||||
}
|
||||
|
||||
function normalizeListCommandsOptions(
|
||||
input: ListCommandsOptions | string,
|
||||
legacyOptions?: LegacyListCommandsOptions | string,
|
||||
): ListCommandsOptions {
|
||||
if (typeof input !== "string") {
|
||||
return input;
|
||||
}
|
||||
if (typeof legacyOptions === "string") {
|
||||
return { agentId: input, requestId: legacyOptions };
|
||||
}
|
||||
return { agentId: input, ...legacyOptions };
|
||||
}
|
||||
|
||||
type AgentRefreshedStatusPayload = z.infer<typeof AgentRefreshedStatusPayloadSchema>;
|
||||
type RestartRequestedStatusPayload = z.infer<typeof RestartRequestedStatusPayloadSchema>;
|
||||
type ShutdownRequestedStatusPayload = z.infer<typeof ShutdownRequestedStatusPayloadSchema>;
|
||||
export interface ShutdownServerOptions {
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
export interface DaemonStatusOptions {
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
export interface DaemonPairingOfferOptions {
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
type DaemonUpdateResponse = z.infer<typeof DaemonUpdateResponseSchema>;
|
||||
type FetchAgentsPayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
@@ -477,6 +526,7 @@ type FetchAgentsPayload = Extract<
|
||||
type FetchAgentsRequest = Extract<SessionInboundMessage, { type: "fetch_agents_request" }>;
|
||||
export type FetchAgentsOptions = Omit<FetchAgentsRequest, "type" | "requestId"> & {
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
};
|
||||
export type FetchAgentsEntry = FetchAgentsPayload["entries"][number];
|
||||
export type FetchAgentsPageInfo = FetchAgentsPayload["pageInfo"];
|
||||
@@ -544,6 +594,7 @@ export interface ReadChatMessagesOptions {
|
||||
since?: string;
|
||||
authorAgentId?: string;
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
export interface WaitForChatMessagesOptions {
|
||||
room: string;
|
||||
@@ -762,15 +813,16 @@ function toTimeoutError(error: unknown, label: string, timeoutMs: number): Error
|
||||
|
||||
const DEFAULT_RECONNECT_BASE_DELAY_MS = 1500;
|
||||
const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000;
|
||||
const DEFAULT_CONNECT_TIMEOUT_MS = 15000;
|
||||
const DEFAULT_SESSION_RPC_TIMEOUT_MS = 60_000;
|
||||
const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
|
||||
const DEFAULT_LIVENESS_TIMEOUT_MS = 5000;
|
||||
const LIVENESS_HEARTBEAT_INTERVAL_MS = 10_000;
|
||||
const LIVENESS_HEARTBEAT_TIMEOUT_MS = 15_000;
|
||||
const LIVENESS_FAILURE_RECONNECT_THRESHOLD = 2;
|
||||
|
||||
/** Default timeout for waiting for connection before sending queued messages */
|
||||
const DEFAULT_SEND_QUEUE_TIMEOUT_MS = 10000;
|
||||
const DEFAULT_DICTATION_FINISH_ACCEPT_TIMEOUT_MS = 15000;
|
||||
const DEFAULT_SEND_QUEUE_TIMEOUT_MS = DEFAULT_SESSION_RPC_TIMEOUT_MS;
|
||||
const DEFAULT_DICTATION_FINISH_ACCEPT_TIMEOUT_MS = DEFAULT_SESSION_RPC_TIMEOUT_MS;
|
||||
const DEFAULT_DICTATION_FINISH_FALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_DICTATION_FINISH_TIMEOUT_GRACE_MS = 5000;
|
||||
|
||||
@@ -1420,10 +1472,11 @@ export class DaemonClient {
|
||||
private async sendRequest<T>(params: {
|
||||
requestId: string;
|
||||
message: SessionInboundMessage;
|
||||
timeout: number;
|
||||
timeout?: number;
|
||||
select: (msg: SessionOutboundMessage) => T | null;
|
||||
options?: { skipQueue?: boolean };
|
||||
}): Promise<T> {
|
||||
const timeout = params.timeout ?? DEFAULT_SESSION_RPC_TIMEOUT_MS;
|
||||
const { promise, cancel } = this.waitForWithCancel<RpcWaitResult<T>>(
|
||||
(msg) => {
|
||||
if (msg.type === "rpc_error" && msg.payload.requestId === params.requestId) {
|
||||
@@ -1443,7 +1496,7 @@ export class DaemonClient {
|
||||
}
|
||||
return { kind: "ok", value };
|
||||
},
|
||||
params.timeout,
|
||||
timeout,
|
||||
params.options,
|
||||
);
|
||||
|
||||
@@ -1469,7 +1522,7 @@ export class DaemonClient {
|
||||
>(params: {
|
||||
requestId: string;
|
||||
message: SessionInboundMessage;
|
||||
timeout: number;
|
||||
timeout?: number;
|
||||
responseType: TResponseType;
|
||||
options?: { skipQueue?: boolean };
|
||||
selectPayload?: (payload: CorrelatedResponsePayload<TResponseType>) => TResult | null;
|
||||
@@ -1503,7 +1556,7 @@ export class DaemonClient {
|
||||
requestId?: string;
|
||||
message: { type: SessionInboundMessage["type"] } & Record<string, unknown>;
|
||||
responseType: TResponseType;
|
||||
timeout: number;
|
||||
timeout?: number;
|
||||
selectPayload?: (payload: CorrelatedResponsePayload<TResponseType>) => TResult | null;
|
||||
}): Promise<TResult> {
|
||||
const resolvedRequestId = this.createRequestId(params.requestId);
|
||||
@@ -1530,7 +1583,7 @@ export class DaemonClient {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
timeout: number;
|
||||
timeout?: number;
|
||||
selectPayload?: (payload: CorrelatedResponsePayload<TResponseType>) => TResult | null;
|
||||
}): Promise<TResult> {
|
||||
const responseType = params.message.type.replace(/\.request$/, ".response") as TResponseType;
|
||||
@@ -1562,7 +1615,6 @@ export class DaemonClient {
|
||||
await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "clear_agent_attention_response") {
|
||||
@@ -1586,7 +1638,6 @@ export class DaemonClient {
|
||||
const response = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "workspace.clear_attention.response") {
|
||||
@@ -1782,7 +1833,7 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
timeout: options?.timeout,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "fetch_agents_response") {
|
||||
@@ -1808,7 +1859,6 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "fetch_agent_history_response") {
|
||||
@@ -1837,7 +1887,6 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "fetch_recent_provider_sessions_response") {
|
||||
@@ -1864,7 +1913,6 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "fetch_workspaces_response") {
|
||||
@@ -1886,9 +1934,6 @@ export class DaemonClient {
|
||||
cwd,
|
||||
},
|
||||
responseType: "open_project_response",
|
||||
// Large local repos (e.g. a big monorepo/brain checkout) need >10s for the
|
||||
// daemon to resolve the path, detect git, and materialize the workspace.
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1900,7 +1945,6 @@ export class DaemonClient {
|
||||
cwd,
|
||||
},
|
||||
responseType: "project.add.response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1919,7 +1963,6 @@ export class DaemonClient {
|
||||
scriptName,
|
||||
},
|
||||
responseType: "start_workspace_script_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1934,7 +1977,6 @@ export class DaemonClient {
|
||||
workspaceId,
|
||||
},
|
||||
responseType: "archive_workspace_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1949,21 +1991,30 @@ export class DaemonClient {
|
||||
workspaceId,
|
||||
},
|
||||
responseType: "workspace_setup_status_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
async fetchAgent(agentId: string, requestId?: string): Promise<FetchAgentResult | null> {
|
||||
const resolvedRequestId = this.createRequestId(requestId);
|
||||
async fetchAgent(options: FetchAgentOptions): Promise<FetchAgentResult | null>;
|
||||
async fetchAgent(agentId: string, requestId?: string): Promise<FetchAgentResult | null>;
|
||||
async fetchAgent(
|
||||
agentId: string,
|
||||
options?: LegacyFetchAgentOptions,
|
||||
): Promise<FetchAgentResult | null>;
|
||||
async fetchAgent(
|
||||
input: FetchAgentOptions | string,
|
||||
legacyOptions?: LegacyFetchAgentOptions | string,
|
||||
): Promise<FetchAgentResult | null> {
|
||||
const options = normalizeFetchAgentOptions(input, legacyOptions);
|
||||
const resolvedRequestId = this.createRequestId(options.requestId);
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: "fetch_agent_request",
|
||||
requestId: resolvedRequestId,
|
||||
agentId,
|
||||
agentId: options.agentId,
|
||||
});
|
||||
const payload = await this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
timeout: options.timeout,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "fetch_agent_response") {
|
||||
@@ -2048,7 +2099,6 @@ export class DaemonClient {
|
||||
const status = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 60000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "status") {
|
||||
@@ -2082,7 +2132,6 @@ export class DaemonClient {
|
||||
await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "agent_deleted") {
|
||||
@@ -2106,7 +2155,6 @@ export class DaemonClient {
|
||||
const result = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "agent_archived") {
|
||||
@@ -2127,7 +2175,6 @@ export class DaemonClient {
|
||||
type: "agent.detach.request",
|
||||
agentId,
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
if (!payload.accepted) {
|
||||
throw new Error(payload.error ?? "detachAgent rejected");
|
||||
@@ -2151,7 +2198,6 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "update_agent_response") {
|
||||
@@ -2181,7 +2227,6 @@ export class DaemonClient {
|
||||
customName,
|
||||
},
|
||||
responseType: "project.rename.response",
|
||||
timeout: 10000,
|
||||
});
|
||||
if (!payload.accepted) {
|
||||
throw new Error(payload.error ?? "renameProject rejected");
|
||||
@@ -2199,7 +2244,6 @@ export class DaemonClient {
|
||||
type: "project.remove.request",
|
||||
projectId,
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
if (!payload.accepted) {
|
||||
throw new Error(payload.error ?? "removeProject rejected");
|
||||
@@ -2220,7 +2264,6 @@ export class DaemonClient {
|
||||
title,
|
||||
},
|
||||
responseType: "workspace.title.set.response",
|
||||
timeout: 10000,
|
||||
});
|
||||
if (!payload.accepted) {
|
||||
throw new Error(payload.error ?? "setWorkspaceTitle rejected");
|
||||
@@ -2243,7 +2286,6 @@ export class DaemonClient {
|
||||
const status = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "status") {
|
||||
@@ -2275,7 +2317,6 @@ export class DaemonClient {
|
||||
const status = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "status") {
|
||||
@@ -2312,7 +2353,6 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "status") {
|
||||
@@ -2345,7 +2385,7 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
timeout: options.timeout,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "fetch_agent_timeline_response") {
|
||||
@@ -2388,7 +2428,6 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "send_agent_message_response") {
|
||||
@@ -2425,7 +2464,6 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "agent.rewind.response") {
|
||||
@@ -2453,7 +2491,6 @@ export class DaemonClient {
|
||||
await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "cancel_agent_response") {
|
||||
@@ -2478,7 +2515,6 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "set_agent_mode_response") {
|
||||
@@ -2507,7 +2543,6 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "set_agent_model_response") {
|
||||
@@ -2536,7 +2571,6 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "set_agent_feature_response") {
|
||||
@@ -2567,7 +2601,6 @@ export class DaemonClient {
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 15000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "set_agent_thinking_response") {
|
||||
@@ -2595,7 +2628,6 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "status") {
|
||||
@@ -2613,8 +2645,8 @@ export class DaemonClient {
|
||||
});
|
||||
}
|
||||
|
||||
async shutdownServer(requestId?: string): Promise<ShutdownRequestedStatusPayload> {
|
||||
const resolvedRequestId = this.createRequestId(requestId);
|
||||
async shutdownServer(options?: ShutdownServerOptions): Promise<ShutdownRequestedStatusPayload> {
|
||||
const resolvedRequestId = this.createRequestId(options?.requestId);
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: "shutdown_server_request",
|
||||
requestId: resolvedRequestId,
|
||||
@@ -2622,7 +2654,7 @@ export class DaemonClient {
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
timeout: options?.timeout,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "status") {
|
||||
@@ -2679,7 +2711,6 @@ export class DaemonClient {
|
||||
const response = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
select: (msg) => {
|
||||
if (msg.type !== "set_voice_mode_response") {
|
||||
return null;
|
||||
@@ -2948,7 +2979,6 @@ export class DaemonClient {
|
||||
const responsePromise = this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 60000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== "checkout_status_response") {
|
||||
@@ -3049,7 +3079,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
responseType: "subscribe_checkout_diff_response",
|
||||
timeout: 60000,
|
||||
options: { skipQueue: true },
|
||||
selectPayload: (payload) => {
|
||||
if (payload.subscriptionId !== subscriptionId) {
|
||||
@@ -3090,7 +3119,6 @@ export class DaemonClient {
|
||||
addAll: input.addAll,
|
||||
},
|
||||
responseType: "checkout_commit_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3109,7 +3137,6 @@ export class DaemonClient {
|
||||
requireCleanTarget: input.requireCleanTarget,
|
||||
},
|
||||
responseType: "checkout_merge_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3127,7 +3154,6 @@ export class DaemonClient {
|
||||
requireCleanTarget: input.requireCleanTarget,
|
||||
},
|
||||
responseType: "checkout_merge_from_base_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3139,7 +3165,6 @@ export class DaemonClient {
|
||||
cwd,
|
||||
},
|
||||
responseType: "checkout_pull_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3151,7 +3176,6 @@ export class DaemonClient {
|
||||
cwd,
|
||||
},
|
||||
responseType: "checkout_push_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3163,7 +3187,6 @@ export class DaemonClient {
|
||||
cwd,
|
||||
},
|
||||
responseType: "checkout.refresh.response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3182,7 +3205,6 @@ export class DaemonClient {
|
||||
baseRef: input.baseRef,
|
||||
},
|
||||
responseType: "checkout_pr_create_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3199,7 +3221,6 @@ export class DaemonClient {
|
||||
mergeMethod: input.method,
|
||||
},
|
||||
responseType: "checkout_pr_merge_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3216,7 +3237,6 @@ export class DaemonClient {
|
||||
enabled: input.enabled,
|
||||
...(input.enabled ? { mergeMethod: input.method } : {}),
|
||||
},
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3241,7 +3261,6 @@ export class DaemonClient {
|
||||
checkRunId: input.checkRunId,
|
||||
workflowRunId: input.workflowRunId,
|
||||
},
|
||||
timeout: 60000,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -3254,7 +3273,6 @@ export class DaemonClient {
|
||||
cwd,
|
||||
},
|
||||
responseType: "checkout_pr_status_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3272,7 +3290,6 @@ export class DaemonClient {
|
||||
repoName: input.repoName,
|
||||
},
|
||||
responseType: "pull_request_timeline_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3289,7 +3306,6 @@ export class DaemonClient {
|
||||
branch,
|
||||
},
|
||||
responseType: "checkout_switch_branch_response",
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3302,7 +3318,6 @@ export class DaemonClient {
|
||||
branch: input.branch,
|
||||
},
|
||||
responseType: "checkout.rename_branch.response",
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3319,7 +3334,6 @@ export class DaemonClient {
|
||||
branch: options?.branch,
|
||||
},
|
||||
responseType: "stash_save_response",
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3332,7 +3346,6 @@ export class DaemonClient {
|
||||
stashIndex,
|
||||
},
|
||||
responseType: "stash_pop_response",
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3349,7 +3362,6 @@ export class DaemonClient {
|
||||
paseoOnly: options?.paseoOnly,
|
||||
},
|
||||
responseType: "stash_list_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3365,7 +3377,6 @@ export class DaemonClient {
|
||||
repoRoot: input.repoRoot,
|
||||
},
|
||||
responseType: "paseo_worktree_list_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3390,7 +3401,6 @@ export class DaemonClient {
|
||||
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
||||
},
|
||||
responseType: "paseo_worktree_archive_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3413,7 +3423,6 @@ export class DaemonClient {
|
||||
...(input.githubPrNumber !== undefined ? { githubPrNumber: input.githubPrNumber } : {}),
|
||||
},
|
||||
responseType: "create_paseo_worktree_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3436,7 +3445,6 @@ export class DaemonClient {
|
||||
: {}),
|
||||
},
|
||||
responseType: "workspace.create.response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3452,7 +3460,6 @@ export class DaemonClient {
|
||||
branchName: options.branchName,
|
||||
},
|
||||
responseType: "validate_branch_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3469,7 +3476,6 @@ export class DaemonClient {
|
||||
limit: options.limit,
|
||||
},
|
||||
responseType: "branch_suggestions_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3487,7 +3493,6 @@ export class DaemonClient {
|
||||
kinds: options.kinds,
|
||||
},
|
||||
responseType: "github_search_response",
|
||||
timeout: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3516,7 +3521,6 @@ export class DaemonClient {
|
||||
responseType: "directory_suggestions_response",
|
||||
// Home-tree scans on large home dirs can take several seconds; don't cut
|
||||
// the suggestion request off early (it would surface as an empty list).
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3541,7 +3545,6 @@ export class DaemonClient {
|
||||
...(acceptBinary ? { acceptBinary: true } : {}),
|
||||
},
|
||||
responseType: "file_explorer_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3601,7 +3604,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
},
|
||||
responseType: "file.upload.response",
|
||||
timeout: 60000,
|
||||
options: { skipQueue: true },
|
||||
});
|
||||
|
||||
@@ -3653,7 +3655,6 @@ export class DaemonClient {
|
||||
path,
|
||||
},
|
||||
responseType: "file_download_token_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3668,7 +3669,6 @@ export class DaemonClient {
|
||||
cwd,
|
||||
},
|
||||
responseType: "project_icon_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3733,7 +3733,6 @@ export class DaemonClient {
|
||||
type: "list_available_providers_request",
|
||||
},
|
||||
responseType: "list_available_providers_response",
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3748,7 +3747,6 @@ export class DaemonClient {
|
||||
cwd: options?.cwd,
|
||||
},
|
||||
responseType: "get_providers_snapshot_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3761,29 +3759,30 @@ export class DaemonClient {
|
||||
type: "get_daemon_config_request",
|
||||
},
|
||||
responseType: "get_daemon_config_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
async getDaemonStatus(requestId?: string): Promise<DaemonStatusPayload> {
|
||||
async getDaemonStatus(options?: DaemonStatusOptions): Promise<DaemonStatusPayload> {
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId,
|
||||
requestId: options?.requestId,
|
||||
message: {
|
||||
type: "daemon.get_status.request",
|
||||
},
|
||||
responseType: "daemon.get_status.response",
|
||||
timeout: 10000,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async getDaemonPairingOffer(requestId?: string): Promise<DaemonPairingOfferPayload> {
|
||||
async getDaemonPairingOffer(
|
||||
options?: DaemonPairingOfferOptions,
|
||||
): Promise<DaemonPairingOfferPayload> {
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId,
|
||||
requestId: options?.requestId,
|
||||
message: {
|
||||
type: "daemon.get_pairing_offer.request",
|
||||
},
|
||||
responseType: "daemon.get_pairing_offer.response",
|
||||
timeout: 10000,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3793,7 +3792,6 @@ export class DaemonClient {
|
||||
message: {
|
||||
type: "diagnostics.request",
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3808,7 +3806,6 @@ export class DaemonClient {
|
||||
config,
|
||||
},
|
||||
responseType: "set_daemon_config_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3820,7 +3817,6 @@ export class DaemonClient {
|
||||
repoRoot,
|
||||
},
|
||||
responseType: "read_project_config_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3834,7 +3830,6 @@ export class DaemonClient {
|
||||
expectedRevision: input.expectedRevision,
|
||||
},
|
||||
responseType: "write_project_config_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3876,30 +3871,28 @@ export class DaemonClient {
|
||||
message: {
|
||||
type: "provider.usage.list.request",
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
async listCommands(options: ListCommandsOptions): Promise<ListCommandsPayload>;
|
||||
async listCommands(agentId: string, requestId?: string): Promise<ListCommandsPayload>;
|
||||
async listCommands(agentId: string, options?: ListCommandsOptions): Promise<ListCommandsPayload>;
|
||||
async listCommands(
|
||||
agentId: string,
|
||||
requestIdOrOptions?: string | ListCommandsOptions,
|
||||
options?: LegacyListCommandsOptions,
|
||||
): Promise<ListCommandsPayload>;
|
||||
async listCommands(
|
||||
input: ListCommandsOptions | string,
|
||||
legacyOptions?: LegacyListCommandsOptions | string,
|
||||
): Promise<ListCommandsPayload> {
|
||||
const requestId =
|
||||
typeof requestIdOrOptions === "string" ? requestIdOrOptions : requestIdOrOptions?.requestId;
|
||||
const draftConfig =
|
||||
typeof requestIdOrOptions === "string" ? undefined : requestIdOrOptions?.draftConfig;
|
||||
|
||||
const options = normalizeListCommandsOptions(input, legacyOptions);
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId,
|
||||
requestId: options.requestId,
|
||||
message: {
|
||||
type: "list_commands_request",
|
||||
agentId,
|
||||
...(draftConfig ? { draftConfig } : {}),
|
||||
agentId: options.agentId,
|
||||
...(options.draftConfig ? { draftConfig: options.draftConfig } : {}),
|
||||
},
|
||||
responseType: "list_commands_response",
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3961,12 +3954,20 @@ export class DaemonClient {
|
||||
predicate: (snapshot: AgentSnapshotPayload) => boolean,
|
||||
timeout = 60000,
|
||||
): Promise<AgentSnapshotPayload> {
|
||||
const initialResult = await this.fetchAgent(agentId).catch(() => null);
|
||||
const deadline = Date.now() + timeout;
|
||||
const remainingTimeoutMs = () => Math.max(1, deadline - Date.now());
|
||||
const timeoutError = () => new Error(`Timed out waiting for agent ${agentId}`);
|
||||
const fetchAgentWithinDeadline = () =>
|
||||
this.fetchAgent({ agentId, timeout: remainingTimeoutMs() }).catch(() => null);
|
||||
|
||||
const initialResult = await fetchAgentWithinDeadline();
|
||||
if (initialResult && predicate(initialResult.agent)) {
|
||||
return initialResult.agent;
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
throw timeoutError();
|
||||
}
|
||||
|
||||
const deadline = Date.now() + timeout;
|
||||
return await new Promise<AgentSnapshotPayload>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let pollInFlight = false;
|
||||
@@ -4017,7 +4018,7 @@ export class DaemonClient {
|
||||
}
|
||||
pollInFlight = true;
|
||||
try {
|
||||
const result = await this.fetchAgent(agentId).catch(() => null);
|
||||
const result = await fetchAgentWithinDeadline();
|
||||
maybeResolve(result?.agent ?? null);
|
||||
} finally {
|
||||
pollInFlight = false;
|
||||
@@ -4042,7 +4043,7 @@ export class DaemonClient {
|
||||
timeoutTimer = setTimeout(() => {
|
||||
finish({
|
||||
kind: "error",
|
||||
error: new Error(`Timed out waiting for agent ${agentId}`),
|
||||
error: timeoutError(),
|
||||
});
|
||||
}, remaining);
|
||||
|
||||
@@ -4126,7 +4127,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
responseType: "list_terminals_response",
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
});
|
||||
}
|
||||
@@ -4152,7 +4152,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
responseType: "create_terminal_response",
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
});
|
||||
}
|
||||
@@ -4166,7 +4165,6 @@ export class DaemonClient {
|
||||
title: input.title,
|
||||
},
|
||||
responseType: "terminal.rename.response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4190,7 +4188,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
responseType: "subscribe_terminal_response",
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
});
|
||||
if (payload.error === null) {
|
||||
@@ -4231,7 +4228,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
responseType: "kill_terminal_response",
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
});
|
||||
}
|
||||
@@ -4251,7 +4247,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
responseType: "close_items_response",
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
});
|
||||
}
|
||||
@@ -4274,7 +4269,6 @@ export class DaemonClient {
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
responseType: "capture_terminal_response",
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
});
|
||||
}
|
||||
@@ -4288,7 +4282,6 @@ export class DaemonClient {
|
||||
...(options.purpose ? { purpose: options.purpose } : {}),
|
||||
},
|
||||
responseType: "chat/create/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4299,7 +4292,6 @@ export class DaemonClient {
|
||||
type: "chat/list",
|
||||
},
|
||||
responseType: "chat/list/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4311,7 +4303,6 @@ export class DaemonClient {
|
||||
room: options.room,
|
||||
},
|
||||
responseType: "chat/inspect/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4323,7 +4314,6 @@ export class DaemonClient {
|
||||
room: options.room,
|
||||
},
|
||||
responseType: "chat/delete/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4338,7 +4328,6 @@ export class DaemonClient {
|
||||
...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}),
|
||||
},
|
||||
responseType: "chat/post/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4353,7 +4342,7 @@ export class DaemonClient {
|
||||
...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}),
|
||||
},
|
||||
responseType: "chat/read/response",
|
||||
timeout: 10000,
|
||||
timeout: options.timeout,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4385,7 +4374,6 @@ export class DaemonClient {
|
||||
...(typeof options.runOnCreate === "boolean" ? { runOnCreate: options.runOnCreate } : {}),
|
||||
},
|
||||
responseType: "schedule/create/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4396,7 +4384,6 @@ export class DaemonClient {
|
||||
type: "schedule/list",
|
||||
},
|
||||
responseType: "schedule/list/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4408,7 +4395,6 @@ export class DaemonClient {
|
||||
scheduleId: options.id,
|
||||
},
|
||||
responseType: "schedule/inspect/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4420,7 +4406,6 @@ export class DaemonClient {
|
||||
scheduleId: options.id,
|
||||
},
|
||||
responseType: "schedule/logs/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4432,7 +4417,6 @@ export class DaemonClient {
|
||||
scheduleId: options.id,
|
||||
},
|
||||
responseType: "schedule/pause/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4444,7 +4428,6 @@ export class DaemonClient {
|
||||
scheduleId: options.id,
|
||||
},
|
||||
responseType: "schedule/resume/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4456,7 +4439,6 @@ export class DaemonClient {
|
||||
scheduleId: options.id,
|
||||
},
|
||||
responseType: "schedule/delete/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4468,7 +4450,6 @@ export class DaemonClient {
|
||||
scheduleId: options.id,
|
||||
},
|
||||
responseType: "schedule/run-once/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4486,7 +4467,6 @@ export class DaemonClient {
|
||||
...(options.expiresAt !== undefined ? { expiresAt: options.expiresAt } : {}),
|
||||
},
|
||||
responseType: "schedule/update/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4515,7 +4495,6 @@ export class DaemonClient {
|
||||
...(typeof options.maxTimeMs === "number" ? { maxTimeMs: options.maxTimeMs } : {}),
|
||||
},
|
||||
responseType: "loop/run/response",
|
||||
timeout: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4526,7 +4505,6 @@ export class DaemonClient {
|
||||
type: "loop/list",
|
||||
},
|
||||
responseType: "loop/list/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4539,7 +4517,6 @@ export class DaemonClient {
|
||||
id: normalized.id,
|
||||
},
|
||||
responseType: "loop/inspect/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4553,7 +4530,6 @@ export class DaemonClient {
|
||||
...(typeof normalized.afterSeq === "number" ? { afterSeq: normalized.afterSeq } : {}),
|
||||
},
|
||||
responseType: "loop/logs/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4566,7 +4542,6 @@ export class DaemonClient {
|
||||
id: normalized.id,
|
||||
},
|
||||
responseType: "loop/stop/response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ function createAgentHandleFactory(daemonClient: DaemonClient): AgentHandleFactor
|
||||
},
|
||||
latest: () => latest,
|
||||
refetch: async (requestId) => {
|
||||
const result = await daemonClient.fetchAgent(id, requestId);
|
||||
const result = await daemonClient.fetchAgent({ agentId: id, requestId });
|
||||
latest = result?.agent ?? null;
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("claude agent commands E2E", () => {
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
// List commands
|
||||
const result = await ctx.client.listCommands(agent.id);
|
||||
const result = await ctx.client.listCommands({ agentId: agent.id });
|
||||
|
||||
// Should have no error
|
||||
expect(result.error).toBeNull();
|
||||
@@ -52,7 +52,7 @@ describe("claude agent commands E2E", () => {
|
||||
}, 60000);
|
||||
|
||||
test("returns error for non-existent agent", async () => {
|
||||
const result = await ctx.client.listCommands("non-existent-agent-id");
|
||||
const result = await ctx.client.listCommands({ agentId: "non-existent-agent-id" });
|
||||
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(result.error).toContain("Agent not found");
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("codex agent commands E2E", () => {
|
||||
expect(agent.provider).toBe("codex");
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
const result = await ctx.client.listCommands(agent.id);
|
||||
const result = await ctx.client.listCommands({ agentId: agent.id });
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.commands.length).toBeGreaterThan(0);
|
||||
@@ -177,7 +177,7 @@ describe("codex agent commands E2E", () => {
|
||||
}, 30_000);
|
||||
|
||||
test("returns error for non-existent agent", async () => {
|
||||
const result = await ctx.client.listCommands("non-existent-agent-id");
|
||||
const result = await ctx.client.listCommands({ agentId: "non-existent-agent-id" });
|
||||
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(result.error).toContain("Agent not found");
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("opencode agent commands E2E", () => {
|
||||
expect(agent.provider).toBe("opencode");
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
const result = await ctx.client.listCommands(agent.id);
|
||||
const result = await ctx.client.listCommands({ agentId: agent.id });
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.commands.length).toBeGreaterThan(0);
|
||||
@@ -87,7 +87,7 @@ describe("opencode agent commands E2E", () => {
|
||||
}, 30_000);
|
||||
|
||||
test("returns error for non-existent agent", async () => {
|
||||
const result = await ctx.client.listCommands("non-existent-agent-id");
|
||||
const result = await ctx.client.listCommands({ agentId: "non-existent-agent-id" });
|
||||
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(result.error).toContain("Agent not found");
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ describe("opencode custom command Big Pickle E2E (real)", () => {
|
||||
expect(agent.model).toBe(OPENCODE_REAL_TEST_MODEL);
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
const commands = await ctx.client.listCommands(agent.id);
|
||||
const commands = await ctx.client.listCommands({ agentId: agent.id });
|
||||
expect(commands.error).toBeNull();
|
||||
expect(commands.commands).toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
@@ -53,7 +53,7 @@ test("daemon mints a distinct local workspace per run and stamps agents by id",
|
||||
expect(firstAgent.workspaceId).toBe(firstWorkspaceId);
|
||||
expect(await workspaceIds(client)).toContain(firstWorkspaceId);
|
||||
|
||||
const fetchedFirst = await client.fetchAgent(firstAgent.id);
|
||||
const fetchedFirst = await client.fetchAgent({ agentId: firstAgent.id });
|
||||
expect(fetchedFirst?.agent.workspaceId).toBe(firstWorkspaceId);
|
||||
|
||||
// A second bare run in the SAME cwd mints a DISTINCT workspace; each run
|
||||
|
||||
@@ -197,12 +197,12 @@ test("createAgent with background initialPrompt returns a running snapshot befor
|
||||
|
||||
expect(agent.status).toBe("running");
|
||||
|
||||
const fetchedWhileRunning = await client.fetchAgent(agent.id);
|
||||
const fetchedWhileRunning = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(fetchedWhileRunning?.agent.status).toBe("running");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
|
||||
const fetchedAfterCompletion = await client.fetchAgent(agent.id);
|
||||
const fetchedAfterCompletion = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(fetchedAfterCompletion?.agent.status).toBe("idle");
|
||||
} finally {
|
||||
await client.close();
|
||||
@@ -638,7 +638,7 @@ test("interrupts a running agent before archiving", async () => {
|
||||
const result = await ctx.client.archiveAgent(created.id);
|
||||
expect(result.archivedAt).toBeTruthy();
|
||||
|
||||
const archivedResult = await ctx.client.fetchAgent(created.id);
|
||||
const archivedResult = await ctx.client.fetchAgent({ agentId: created.id });
|
||||
expect(archivedResult).not.toBeNull();
|
||||
expect(archivedResult?.agent.archivedAt).toBeTruthy();
|
||||
expect(archivedResult?.agent.status).not.toBe("running");
|
||||
@@ -671,7 +671,7 @@ test("send_agent_message auto-unarchives archived agents", async () => {
|
||||
const finalState = await ctx.client.waitForFinish(created.id, 120000);
|
||||
expect(finalState.status).toBe("idle");
|
||||
|
||||
const refreshed = await ctx.client.fetchAgent(created.id);
|
||||
const refreshed = await ctx.client.fetchAgent({ agentId: created.id });
|
||||
expect(refreshed).not.toBeNull();
|
||||
expect(refreshed?.agent.archivedAt).toBeNull();
|
||||
} finally {
|
||||
@@ -691,7 +691,7 @@ test("refresh_agent auto-unarchives archived agents", async () => {
|
||||
await ctx.client.archiveAgent(created.id);
|
||||
await ctx.client.refreshAgent(created.id);
|
||||
|
||||
const refreshed = await ctx.client.fetchAgent(created.id);
|
||||
const refreshed = await ctx.client.fetchAgent({ agentId: created.id });
|
||||
expect(refreshed).not.toBeNull();
|
||||
expect(refreshed?.agent.archivedAt).toBeNull();
|
||||
} finally {
|
||||
@@ -770,7 +770,7 @@ test("resume_agent auto-unarchives archived agents", async () => {
|
||||
cwd,
|
||||
},
|
||||
});
|
||||
const agentBeforeArchive = await ctx.client.fetchAgent(created.id);
|
||||
const agentBeforeArchive = await ctx.client.fetchAgent({ agentId: created.id });
|
||||
expect(agentBeforeArchive?.agent.persistence).toBeTruthy();
|
||||
await ctx.client.archiveAgent(created.id);
|
||||
|
||||
@@ -779,7 +779,7 @@ test("resume_agent auto-unarchives archived agents", async () => {
|
||||
throw new Error("Expected persistence handle for resume test");
|
||||
}
|
||||
const resumed = await ctx.client.resumeAgent(handle);
|
||||
const resumedDetails = await ctx.client.fetchAgent(resumed.id);
|
||||
const resumedDetails = await ctx.client.fetchAgent({ agentId: resumed.id });
|
||||
expect(resumedDetails).not.toBeNull();
|
||||
expect(resumedDetails?.agent.archivedAt).toBeNull();
|
||||
|
||||
@@ -807,7 +807,7 @@ test("update_agent persists unloaded title and labels across auto-unarchive", as
|
||||
labels: { lane: "phase-1a" },
|
||||
});
|
||||
|
||||
const archived = await ctx.client.fetchAgent(created.id);
|
||||
const archived = await ctx.client.fetchAgent({ agentId: created.id });
|
||||
expect(archived).not.toBeNull();
|
||||
expect(archived?.agent.archivedAt).toBeTruthy();
|
||||
expect(archived?.agent.title).toBe("Pinned Title");
|
||||
@@ -817,7 +817,7 @@ test("update_agent persists unloaded title and labels across auto-unarchive", as
|
||||
const finalState = await ctx.client.waitForFinish(created.id, 120000);
|
||||
expect(finalState.status).toBe("idle");
|
||||
|
||||
const unarchived = await ctx.client.fetchAgent(created.id);
|
||||
const unarchived = await ctx.client.fetchAgent({ agentId: created.id });
|
||||
expect(unarchived).not.toBeNull();
|
||||
expect(unarchived?.agent.archivedAt).toBeNull();
|
||||
expect(unarchived?.agent.title).toBe("Pinned Title");
|
||||
@@ -974,7 +974,7 @@ test("creates agent and exercises lifecycle", async () => {
|
||||
|
||||
expect(agent.id).toBeTruthy();
|
||||
expect(agent.status).toBe("idle");
|
||||
const fetchedResult = await ctx.client.fetchAgent(agent.id);
|
||||
const fetchedResult = await ctx.client.fetchAgent({ agentId: agent.id });
|
||||
expect(fetchedResult?.agent.id).toBe(agent.id);
|
||||
|
||||
const agentUpdate = await agentUpdatePromise;
|
||||
@@ -1149,7 +1149,10 @@ test("creates agent and exercises lifecycle", async () => {
|
||||
return unsubscribeCommands;
|
||||
});
|
||||
|
||||
const commands = await ctx.client.listCommands(agent.id, commandsRequestId);
|
||||
const commands = await ctx.client.listCommands({
|
||||
agentId: agent.id,
|
||||
requestId: commandsRequestId,
|
||||
});
|
||||
const commandsMessage = await commandsResponsePromise;
|
||||
expect(commands.agentId).toBe(agent.id);
|
||||
expect(Array.isArray(commands.commands)).toBe(true);
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("timestamp behavior", () => {
|
||||
direction: "tail",
|
||||
limit: 200,
|
||||
});
|
||||
const refreshedResult = await ctx.client.fetchAgent(agent.id);
|
||||
const refreshedResult = await ctx.client.fetchAgent({ agentId: agent.id });
|
||||
|
||||
// Verify agent is still idle
|
||||
expect(refreshedResult?.agent.status).toBe("idle");
|
||||
@@ -79,7 +79,7 @@ describe("timestamp behavior", () => {
|
||||
direction: "tail",
|
||||
limit: 200,
|
||||
});
|
||||
const clearResult = await ctx.client.fetchAgent(agent.id);
|
||||
const clearResult = await ctx.client.fetchAgent({ agentId: agent.id });
|
||||
|
||||
// Timestamp should STILL not have changed
|
||||
expect(clearResult?.agent.updatedAt).toBe(initialUpdatedAt);
|
||||
|
||||
@@ -575,7 +575,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
|
||||
}
|
||||
expect(sawTimelineGrowth).toBe(true);
|
||||
} else {
|
||||
const current = await client.fetchAgent(agent.id);
|
||||
const current = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(current.agent.status).toBe("idle");
|
||||
}
|
||||
} finally {
|
||||
@@ -642,7 +642,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
|
||||
expect(settled.status).toBe("idle");
|
||||
}
|
||||
|
||||
const finalResult = await client.fetchAgent(agent.id);
|
||||
const finalResult = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(finalResult?.agent.status).toBe("idle");
|
||||
} finally {
|
||||
await client.close();
|
||||
@@ -835,7 +835,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
|
||||
try {
|
||||
secondCompletion = await client.waitForFinish(agent.id, 20_000);
|
||||
} catch {
|
||||
const atTimeoutResult = await client.fetchAgent(agent.id);
|
||||
const atTimeoutResult = await client.fetchAgent({ agentId: agent.id });
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
@@ -911,7 +911,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
|
||||
try {
|
||||
secondCompletion = await client.waitForFinish(agent.id, 20_000);
|
||||
} catch {
|
||||
const atTimeoutResult = await client.fetchAgent(agent.id);
|
||||
const atTimeoutResult = await client.fetchAgent({ agentId: agent.id });
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
@@ -1068,7 +1068,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
|
||||
error instanceof Error ? error : new Error(String(error ?? "wait_for_finish failed"));
|
||||
}
|
||||
|
||||
let afterWaitResult = await client.fetchAgent(agent.id);
|
||||
let afterWaitResult = await client.fetchAgent({ agentId: agent.id });
|
||||
let cancelRecovered = true;
|
||||
if (afterWaitResult?.agent.status === "running") {
|
||||
await client.cancelAgent(agent.id);
|
||||
@@ -1078,7 +1078,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () =
|
||||
} catch {
|
||||
cancelRecovered = false;
|
||||
}
|
||||
afterWaitResult = await client.fetchAgent(agent.id);
|
||||
afterWaitResult = await client.fetchAgent({ agentId: agent.id });
|
||||
}
|
||||
|
||||
expect(waitError).toBeNull();
|
||||
|
||||
@@ -179,7 +179,7 @@ async function runtimeSessionId(
|
||||
harness: ClaudeRewindHarness,
|
||||
session: ClaudeRewindSession,
|
||||
): Promise<string | null> {
|
||||
const snapshot = await harness.client.fetchAgent(session.agentId);
|
||||
const snapshot = await harness.client.fetchAgent({ agentId: session.agentId });
|
||||
return snapshot?.agent.runtimeInfo?.sessionId ?? snapshot?.agent.persistence?.sessionId ?? null;
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ describe("daemon E2E (real claude) - rewind", () => {
|
||||
expect(sessionIdAfter).not.toBe(sessionIdBefore);
|
||||
}
|
||||
|
||||
const snapshot = await harness.client.fetchAgent(session.agentId);
|
||||
const snapshot = await harness.client.fetchAgent({ agentId: session.agentId });
|
||||
expect(snapshot?.agent.status).toBe("idle");
|
||||
expect(snapshot?.agent.pendingPermissions).toEqual([]);
|
||||
} finally {
|
||||
|
||||
@@ -34,7 +34,7 @@ interface CodexRewindSession {
|
||||
const TURN_TIMEOUT_MS = 180_000;
|
||||
|
||||
async function fetchThreadId(client: DaemonClient, agentId: string): Promise<string> {
|
||||
const result = await client.fetchAgent(agentId);
|
||||
const result = await client.fetchAgent({ agentId });
|
||||
const threadId = result?.agent.persistence?.sessionId ?? result?.agent.runtimeInfo?.sessionId;
|
||||
if (!threadId) {
|
||||
throw new Error(`Agent ${agentId} does not have a visible Codex thread id`);
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ describe("daemon E2E (real claude) - runtime model reconciliation", () => {
|
||||
const finish = await client.waitForFinish(agent.id, 180_000);
|
||||
expect(finish.status).toBe("idle");
|
||||
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
const snapshot = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(snapshot).not.toBeNull();
|
||||
const runtimeModelId = snapshot?.agent.runtimeInfo?.model ?? null;
|
||||
expect(typeof runtimeModelId).toBe("string");
|
||||
|
||||
@@ -109,7 +109,8 @@ describe("daemon E2E (real opencode) - draft feature discovery", () => {
|
||||
expect(await provider.listImportableSessions?.({ cwd })).toEqual([]);
|
||||
|
||||
await withConnectedOpenCodeDaemon(provider, async ({ client }) => {
|
||||
const response = await client.listCommands("draft-opencode-agent", {
|
||||
const response = await client.listCommands({
|
||||
agentId: "draft-opencode-agent",
|
||||
draftConfig: {
|
||||
provider: "opencode",
|
||||
cwd,
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("daemon E2E (real opencode) - initial prompt wait", () => {
|
||||
expect(finish.status).toBe("idle");
|
||||
expect(finish.lastMessage).toContain("BIG_PICKLE_OK");
|
||||
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
const snapshot = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(snapshot.agent?.status).toBe("idle");
|
||||
|
||||
const timeline = await client.fetchAgentTimeline(agent.id, {
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("daemon E2E (real opencode) - invalid mode handling", () => {
|
||||
expect(finish.status).toBe("error");
|
||||
expect(finish.error).toBeTruthy();
|
||||
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
const snapshot = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(snapshot.agent?.status).toBe("error");
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("daemon E2E (real opencode) - invalid model handling", () => {
|
||||
expect(finish.status).toBe("error");
|
||||
expect(finish.error).toBeTruthy();
|
||||
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
const snapshot = await client.fetchAgent({ agentId: agent.id });
|
||||
expect(snapshot.agent?.status).toBe("error");
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
|
||||
@@ -69,7 +69,7 @@ async function runtimeSessionId(
|
||||
harness: OpenCodeRewindHarness,
|
||||
session: OpenCodeRewindSession,
|
||||
): Promise<string> {
|
||||
const snapshot = await harness.client.fetchAgent(session.agentId);
|
||||
const snapshot = await harness.client.fetchAgent({ agentId: session.agentId });
|
||||
const sessionId =
|
||||
snapshot?.agent.runtimeInfo?.sessionId ?? snapshot?.agent.persistence?.sessionId;
|
||||
if (!sessionId) {
|
||||
@@ -107,7 +107,7 @@ async function recordAgentStatusTransitions(
|
||||
session: OpenCodeRewindSession,
|
||||
): Promise<{ records: AgentStatusRecord[]; stop: () => void }> {
|
||||
const records: AgentStatusRecord[] = [];
|
||||
const initial = await harness.client.fetchAgent(session.agentId);
|
||||
const initial = await harness.client.fetchAgent({ agentId: session.agentId });
|
||||
if (initial?.agent.status) {
|
||||
records.push({ at: Date.now(), status: initial.agent.status });
|
||||
}
|
||||
@@ -143,7 +143,7 @@ async function expectAgentIdle(
|
||||
harness: OpenCodeRewindHarness,
|
||||
session: OpenCodeRewindSession,
|
||||
): Promise<void> {
|
||||
const snapshot = await harness.client.fetchAgent(session.agentId);
|
||||
const snapshot = await harness.client.fetchAgent({ agentId: session.agentId });
|
||||
expect(snapshot?.agent.status).toBe("idle");
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ async function approvePendingPermissions(
|
||||
agentId: string,
|
||||
handledPermissionIds: Set<string>,
|
||||
): Promise<void> {
|
||||
const snapshot = await client.fetchAgent(agentId).catch(() => null);
|
||||
const snapshot = await client.fetchAgent({ agentId }).catch(() => null);
|
||||
const pending = snapshot?.agent.pendingPermissions ?? [];
|
||||
const toApprove = pending.filter((permission) => !handledPermissionIds.has(permission.id));
|
||||
for (const permission of toApprove) {
|
||||
|
||||
@@ -50,7 +50,7 @@ async function runtimeSessionId(
|
||||
harness: PiRewindHarness,
|
||||
session: PiRewindSession,
|
||||
): Promise<string> {
|
||||
const snapshot = await harness.client.fetchAgent(session.agentId);
|
||||
const snapshot = await harness.client.fetchAgent({ agentId: session.agentId });
|
||||
const sessionId =
|
||||
snapshot?.agent.runtimeInfo?.sessionId ?? snapshot?.agent.persistence?.sessionId;
|
||||
if (!sessionId) {
|
||||
|
||||
@@ -149,7 +149,7 @@ test(
|
||||
model: PI_FREE_COMPACTION_TEST_MODEL,
|
||||
});
|
||||
|
||||
const result = await client.listCommands(agent.id);
|
||||
const result = await client.listCommands({ agentId: agent.id });
|
||||
expect(result.commands).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -244,7 +244,7 @@ describe("daemon E2E (real claude) - send message during tool call", () => {
|
||||
const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 });
|
||||
|
||||
if (finish.status !== "idle") {
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
const snapshot = await client.fetchAgent({ agentId: agent.id });
|
||||
throw new Error(
|
||||
`Expected idle after replacement, got ${finish.status}. postSendStatuses=${JSON.stringify(postSendStatuses)} statusesBeforeFirstAssistant=${JSON.stringify(statusesBeforeFirstAssistant)} postSendAssistantTexts=${JSON.stringify(postSendAssistantTexts)} turnStarted=${countTurnStarted(postSendMessages, agent.id)} agentStatus=${snapshot?.agent.status ?? null} recentTimeline=${JSON.stringify(summarizeTimelineItems(timeline))}`,
|
||||
);
|
||||
|
||||
@@ -204,7 +204,7 @@ describe("daemon E2E (real codex) - send message during tool call", () => {
|
||||
const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 });
|
||||
|
||||
if (finish.status !== "idle") {
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
const snapshot = await client.fetchAgent({ agentId: agent.id });
|
||||
throw new Error(
|
||||
`Expected idle after replacement, got ${finish.status}. postSendStatuses=${JSON.stringify(postSendStatuses)} statusesBeforeFirstAssistant=${JSON.stringify(statusesBeforeFirstAssistant)} postSendAssistantTexts=${JSON.stringify(getAssistantTexts(postSendMessages, agent.id))} agentStatus=${snapshot?.agent.status ?? null} recentTimeline=${JSON.stringify(summarizeTimelineItems(timeline))}`,
|
||||
);
|
||||
@@ -278,7 +278,7 @@ describe("daemon E2E (real codex) - send message during tool call", () => {
|
||||
const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 });
|
||||
|
||||
if (finish.status !== "idle") {
|
||||
const snapshot = await client.fetchAgent(agent.id);
|
||||
const snapshot = await client.fetchAgent({ agentId: agent.id });
|
||||
throw new Error(
|
||||
`Expected idle after quick follow-up, got ${finish.status}. postSendStatuses=${JSON.stringify(postSendStatuses)} statusesBeforeFirstAssistant=${JSON.stringify(statusesBeforeFirstAssistant)} postSendAssistantTexts=${JSON.stringify(getAssistantTexts(postSendMessages, agent.id))} agentStatus=${snapshot?.agent.status ?? null} recentTimeline=${JSON.stringify(summarizeTimelineItems(timeline))}`,
|
||||
);
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ describe("daemon E2E (real claude) - send while running recovery", () => {
|
||||
}
|
||||
|
||||
await secondary.waitForFinish(agent.id, 180_000);
|
||||
const finalResult = await secondary.fetchAgent(agent.id);
|
||||
const finalResult = await secondary.fetchAgent({ agentId: agent.id });
|
||||
if (finalResult) {
|
||||
applySnapshot(finalResult.agent);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("daemon E2E (real codex) - send while running recovery", () => {
|
||||
}
|
||||
|
||||
await secondary.waitForFinish(agent.id, 120_000);
|
||||
const finalResult = await secondary.fetchAgent(agent.id);
|
||||
const finalResult = await secondary.fetchAgent({ agentId: agent.id });
|
||||
if (finalResult) {
|
||||
applySnapshot(finalResult.agent);
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ test("creating another same-cwd local workspace keeps running status on the owni
|
||||
throw new Error(third.error ?? "Expected third workspace to be created");
|
||||
}
|
||||
|
||||
expect((await client.fetchAgent(agent.id))?.agent.status).toBe("running");
|
||||
expect((await client.fetchAgent({ agentId: agent.id }))?.agent.status).toBe("running");
|
||||
expect(await statusByWorkspaceId(client)).toEqual(
|
||||
new Map([
|
||||
[firstWorkspaceId, "running"],
|
||||
@@ -565,7 +565,7 @@ test("two workspaces sharing one cwd compute agent status per workspaceId", asyn
|
||||
);
|
||||
await waitForPermission(client, agentA.id);
|
||||
|
||||
const fetchedA = await client.fetchAgent(agentA.id);
|
||||
const fetchedA = await client.fetchAgent({ agentId: agentA.id });
|
||||
expect(fetchedA?.agent.workspaceId).toBe(WORKSPACE_A);
|
||||
|
||||
expect(await statusByWorkspaceId(client)).toEqual(
|
||||
@@ -609,7 +609,7 @@ test("two workspaces sharing one cwd compute agent status per workspaceId", asyn
|
||||
);
|
||||
await waitForPermission(client, agentB.id);
|
||||
|
||||
const fetchedB = await client.fetchAgent(agentB.id);
|
||||
const fetchedB = await client.fetchAgent({ agentId: agentB.id });
|
||||
expect(fetchedB?.agent.workspaceId).toBe(WORKSPACE_B);
|
||||
|
||||
expect(await statusByWorkspaceId(client)).toEqual(
|
||||
|
||||
Reference in New Issue
Block a user