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 },
|
||||
|
||||
Reference in New Issue
Block a user