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:
@@ -169,6 +169,8 @@ There is no dedicated welcome message; the server emits a `status` session messa
|
||||
|
||||
Client liveness checks use the top-level JSON `ping`/`pong` envelope, not a session RPC and not RFC6455 protocol ping. The app runs through browser and React Native WebSocket APIs, which do not expose protocol ping, so this envelope is the portable way to test the direct or relay data path. Session RPC timeouts are operation failures and must not be treated as proof that the socket is dead.
|
||||
|
||||
Client session RPC waits default to 60s so slow relay or mobile networks do not turn a live but delayed daemon response into a false operation failure. Keep connect timeouts, app-level grace windows, explicit diagnostic latency probes, liveness ping timers, and genuinely long-running RPCs separate from this default.
|
||||
|
||||
New session RPCs use dotted names with `.request` and `.response` suffixes, such as `checkout.github.set_auto_merge.request` and `checkout.github.set_auto_merge.response`. See [rpc-namespacing.md](rpc-namespacing.md) for the convention and migration rules for older flat RPC names.
|
||||
|
||||
**Notable session message types:**
|
||||
|
||||
@@ -28,6 +28,8 @@ Page limits are projected-item targets. A tool call lifecycle is one projected i
|
||||
|
||||
When the app fetches `direction: "after"` and the daemon responds with `hasNewer: true`, the app must immediately fetch the next page from `endCursor`. The catch-up is complete only when `hasNewer: false`.
|
||||
|
||||
Initialization timeouts guard lack of catch-up progress, not the full multi-page sync. A successful page that queues the next `after` page refreshes the watchdog.
|
||||
|
||||
The first load of an agent without a local cursor is different: it fetches a bounded latest tail page. Older history remains user-driven by scrolling upward.
|
||||
|
||||
## Resume behavior
|
||||
|
||||
@@ -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({
|
||||
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,16 +263,9 @@ async function waitForDaemonReady(args: {
|
||||
onStatus?: (message: string) => void;
|
||||
}): Promise<{ listen: string; host: string | null }> {
|
||||
const deadline = Date.now() + args.timeoutMs;
|
||||
|
||||
async function poll(state: ProgressState): Promise<{ listen: string; host: string | null }> {
|
||||
const probe = await probeDaemonReady(args.home);
|
||||
if (probe.kind === "ready") {
|
||||
return { listen: probe.listen, host: probe.host };
|
||||
}
|
||||
const nextState = announceProgress(args.home, state, args.onStatus);
|
||||
if (Date.now() >= deadline) {
|
||||
const createTimeoutError = () => {
|
||||
const recentLogs = tailDaemonLog(args.home, 60);
|
||||
throw new Error(
|
||||
return new Error(
|
||||
[
|
||||
`Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`,
|
||||
recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
|
||||
@@ -272,6 +273,19 @@ async function waitForDaemonReady(args: {
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
);
|
||||
};
|
||||
|
||||
async function poll(state: ProgressState): Promise<{ listen: string; host: string | null }> {
|
||||
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) {
|
||||
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