mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-29 04:32:57 +00:00
Let Hub continue agents through the standard daemon API (#4354)
* feat(hub): continue agents through standard daemon operations Authorize ordinary agent and workspace recovery operations with hub.execute. Keep conversation routing in Hub and persist generic creation and message receipts so retries cannot silently duplicate work. * fix(hub): expose custom providers to continuing clients Allow Hub to advertise provider support independently of the app version. Document the companion continuation policy and migration. Preserve newly observed files in the native watcher inventory so coalesced deletions remain visible to reconciliation, addressing the Windows CI failure. * fix(hub): allow retries after confirmed local failures Prepare message loading before recording dispatch intent. Remove failed creation receipts only when creation cleanup completed and no live or stored agent exists. Keep ambiguous provider deliveries fail-closed. Exercise native file observation with real filesystem dependencies.
This commit is contained in:
@@ -294,6 +294,24 @@ test("advertises consumer-provided browser automation capabilities", async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test("retry-safe creation rejects older hosts before sending any request", async () => {
|
||||
const transport = createMockTransport();
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "receipt-gate",
|
||||
transportFactory: () => transport.transport,
|
||||
reconnect: { enabled: false },
|
||||
});
|
||||
clients.push(client);
|
||||
const connecting = client.connect();
|
||||
transport.triggerOpen();
|
||||
await connecting;
|
||||
await expect(
|
||||
client.createAgent({ provider: "codex", cwd: "/project", idempotencyKey: "creation" }),
|
||||
).rejects.toThrow("Update the host to use retry-safe agent creation.");
|
||||
expect(transport.sent).toEqual([]);
|
||||
});
|
||||
|
||||
test("Hub management requires daemon support before dispatching requests", async () => {
|
||||
const mock = createMockTransport();
|
||||
const client = new DaemonClient({
|
||||
@@ -2586,10 +2604,11 @@ test("sends create_agent_request with workspace and caller identity", async () =
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
mock.triggerOpen({ features: { agentRequestReceipts: true } });
|
||||
await connectPromise;
|
||||
|
||||
const createPromise = client.createAgent({
|
||||
idempotencyKey: "one-creation",
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project/.paseo/worktrees/feature-a",
|
||||
workspaceId: "ws-feature-a",
|
||||
@@ -2603,6 +2622,7 @@ test("sends create_agent_request with workspace and caller identity", async () =
|
||||
expect(request).toEqual(
|
||||
expect.objectContaining({
|
||||
type: "create_agent_request",
|
||||
idempotencyKey: "one-creation",
|
||||
workspaceId: "ws-feature-a",
|
||||
callerAgentId: "parent-agent",
|
||||
}),
|
||||
|
||||
@@ -366,6 +366,7 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
|
||||
workspaceId?: string;
|
||||
callerAgentId?: string;
|
||||
initialPrompt?: string;
|
||||
idempotencyKey?: string;
|
||||
clientMessageId?: string;
|
||||
outputSchema?: Record<string, unknown>;
|
||||
images?: CreateAgentRequestMessage["images"];
|
||||
@@ -2503,6 +2504,7 @@ export class DaemonClient {
|
||||
// ============================================================================
|
||||
|
||||
async createAgent(options: CreateAgentRequestOptions): Promise<AgentSnapshotPayload> {
|
||||
if (options.idempotencyKey !== undefined) this.requireAgentRequestReceipts();
|
||||
const requestId = this.createRequestId(options.requestId);
|
||||
const config = resolveAgentConfig(options);
|
||||
|
||||
@@ -2514,6 +2516,7 @@ export class DaemonClient {
|
||||
...(options.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}),
|
||||
...(options.callerAgentId !== undefined ? { callerAgentId: options.callerAgentId } : {}),
|
||||
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
|
||||
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
||||
...(options.images && options.images.length > 0 ? { images: options.images } : {}),
|
||||
@@ -2555,6 +2558,13 @@ export class DaemonClient {
|
||||
return status.agent;
|
||||
}
|
||||
|
||||
private requireAgentRequestReceipts(): void {
|
||||
// COMPAT(agentRequestReceipts): added in v0.7.3; remove gate after 2027-03-05.
|
||||
if (this.lastServerInfoMessage?.features?.agentRequestReceipts !== true) {
|
||||
throw new Error("Update the host to use retry-safe agent creation.");
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAgent(agentId: string): Promise<void> {
|
||||
const requestId = this.createRequestId();
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
|
||||
Reference in New Issue
Block a user