mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Let Hub finish the executions it starts (#2395)
* feat(server): control Hub execution lifecycle * fix(server): archive only execution-owned worktrees * fix(server): serialize Hub execution control after create * test(server): compare Hub worktree paths canonically * fix(server): make absent Hub controls idempotent A Hub can own an execution before agent creation materializes. Treating an absent daemon-scoped record as already stopped or archived lets finality complete without revealing or affecting another daemon's execution.
This commit is contained in:
@@ -36,7 +36,7 @@ const agent = {
|
||||
labels: {},
|
||||
};
|
||||
|
||||
// Frozen at the Hub create request shape shipped before worktree and autoArchive.
|
||||
// Frozen at the Hub create request shape shipped before worktree.
|
||||
const PreviousHubAgentCreateRequestSchema = z.object({
|
||||
type: z.literal("hub.execution.agent.create.request"),
|
||||
requestId: z.string(),
|
||||
@@ -52,6 +52,19 @@ const PreviousHubAgentCreateRequestSchema = z.object({
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
// Frozen at the Hub create request shape that temporarily carried turn-based auto-archive policy.
|
||||
const PreviousHubAgentCreateWithAutoArchiveRequestSchema =
|
||||
PreviousHubAgentCreateRequestSchema.extend({
|
||||
worktree: z
|
||||
.object({
|
||||
mode: z.literal("branch-off"),
|
||||
newBranch: z.string(),
|
||||
base: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
autoArchive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
describe("Hub session protocol", () => {
|
||||
test("accepts the Hub execution create request", () => {
|
||||
const message = {
|
||||
@@ -80,12 +93,64 @@ describe("Hub session protocol", () => {
|
||||
provider: "codex",
|
||||
cwd: "/repo",
|
||||
prompt: "Work in the requested target",
|
||||
...(worktree ? { worktree, autoArchive: true } : {}),
|
||||
...(worktree ? { worktree } : {}),
|
||||
};
|
||||
|
||||
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
||||
});
|
||||
|
||||
test("accepts old Hub creates while ignoring their removed auto-archive policy", () => {
|
||||
const oldRequest = {
|
||||
type: "hub.execution.agent.create.request" as const,
|
||||
requestId: "hub-old-policy",
|
||||
executionId: "execution-old-policy",
|
||||
provider: "codex",
|
||||
cwd: "/repo",
|
||||
prompt: "Work in the requested target",
|
||||
worktree: { mode: "branch-off" as const, newBranch: "hub-work", base: "main" },
|
||||
autoArchive: true,
|
||||
};
|
||||
|
||||
expect(PreviousHubAgentCreateWithAutoArchiveRequestSchema.parse(oldRequest)).toEqual(
|
||||
oldRequest,
|
||||
);
|
||||
expect(SessionInboundMessageSchema.parse(oldRequest)).toEqual({
|
||||
type: "hub.execution.agent.create.request",
|
||||
requestId: "hub-old-policy",
|
||||
executionId: "execution-old-policy",
|
||||
provider: "codex",
|
||||
cwd: "/repo",
|
||||
prompt: "Work in the requested target",
|
||||
worktree: { mode: "branch-off", newBranch: "hub-work", base: "main" },
|
||||
});
|
||||
});
|
||||
|
||||
test.each(["interrupt", "archive"] as const)(
|
||||
"round-trips the Hub execution %s command",
|
||||
(action) => {
|
||||
const request = {
|
||||
type: "hub.execution.control.request" as const,
|
||||
requestId: `control-${action}`,
|
||||
executionId: "execution-1",
|
||||
action,
|
||||
};
|
||||
const response = {
|
||||
type: "hub.execution.control.response" as const,
|
||||
payload: {
|
||||
requestId: request.requestId,
|
||||
executionId: request.executionId,
|
||||
action,
|
||||
success: true,
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(SessionInboundMessageSchema.parse(request)).toEqual(request);
|
||||
expect(SessionOutboundMessageSchema.parse(response)).toEqual(response);
|
||||
expect(parseHubExecutionOutboundMessage(response)).toEqual(response);
|
||||
},
|
||||
);
|
||||
|
||||
test("the previous Hub create parser ignores additive worktree and auto-archive fields", () => {
|
||||
const newRequest = {
|
||||
type: "hub.execution.agent.create.request" as const,
|
||||
@@ -120,6 +185,16 @@ describe("Hub session protocol", () => {
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "hub.execution.control.response",
|
||||
payload: {
|
||||
requestId: "control-1",
|
||||
executionId: "execution-1",
|
||||
action: "archive",
|
||||
success: false,
|
||||
error: "Execution not found",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "hub.execution.agent.update",
|
||||
payload: { executionId: "execution-1", agentId: "agent-1", agent },
|
||||
|
||||
@@ -2418,13 +2418,25 @@ export const HubExecutionAgentCreateRequestSchema = z.object({
|
||||
featureValues: z.record(z.string(), z.unknown()).optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
worktree: CreateAgentWorktreeTargetSchema.optional(),
|
||||
autoArchive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type HubExecutionAgentCreateRequest = z.infer<typeof HubExecutionAgentCreateRequestSchema>;
|
||||
|
||||
export const HubExecutionControlActionSchema = z.enum(["interrupt", "archive"]);
|
||||
export type HubExecutionControlAction = z.infer<typeof HubExecutionControlActionSchema>;
|
||||
|
||||
export const HubExecutionControlRequestSchema = z.object({
|
||||
type: z.literal("hub.execution.control.request"),
|
||||
requestId: z.string(),
|
||||
executionId: z.string(),
|
||||
action: HubExecutionControlActionSchema,
|
||||
});
|
||||
|
||||
export type HubExecutionControlRequest = z.infer<typeof HubExecutionControlRequestSchema>;
|
||||
|
||||
export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
HubExecutionAgentCreateRequestSchema,
|
||||
HubExecutionControlRequestSchema,
|
||||
BrowserAutomationExecuteResponseSchema,
|
||||
VoiceAudioChunkMessageSchema,
|
||||
AbortRequestMessageSchema,
|
||||
@@ -5079,6 +5091,17 @@ export const HubExecutionAgentCreateResponseSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const HubExecutionControlResponseSchema = z.object({
|
||||
type: z.literal("hub.execution.control.response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
executionId: z.string(),
|
||||
action: HubExecutionControlActionSchema,
|
||||
success: z.boolean(),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const HubExecutionAgentUpdateSchema = z.object({
|
||||
type: z.literal("hub.execution.agent.update"),
|
||||
payload: z.object({
|
||||
@@ -5098,11 +5121,13 @@ export const HubExecutionAgentStreamSchema = z.object({
|
||||
});
|
||||
|
||||
export type HubExecutionAgentCreateResponse = z.infer<typeof HubExecutionAgentCreateResponseSchema>;
|
||||
export type HubExecutionControlResponse = z.infer<typeof HubExecutionControlResponseSchema>;
|
||||
export type HubExecutionAgentUpdate = z.infer<typeof HubExecutionAgentUpdateSchema>;
|
||||
export type HubExecutionAgentStream = z.infer<typeof HubExecutionAgentStreamSchema>;
|
||||
|
||||
export const HubExecutionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
HubExecutionAgentCreateResponseSchema,
|
||||
HubExecutionControlResponseSchema,
|
||||
HubExecutionAgentUpdateSchema,
|
||||
HubExecutionAgentStreamSchema,
|
||||
]);
|
||||
@@ -5135,6 +5160,7 @@ export type DaemonUpdateProgressMessage = z.infer<typeof DaemonUpdateProgressMes
|
||||
|
||||
export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
HubExecutionAgentCreateResponseSchema,
|
||||
HubExecutionControlResponseSchema,
|
||||
HubExecutionAgentUpdateSchema,
|
||||
HubExecutionAgentStreamSchema,
|
||||
BrowserAutomationExecuteRequestSchema,
|
||||
|
||||
Reference in New Issue
Block a user