Create agents in worktrees with auto-archive (#1120)
* Add daemon worktree auto-archive spawning * Refactor create agent lifecycle dispatch * Stabilize Claude autonomous turn test
This commit is contained in:
@@ -27,6 +27,8 @@ Both look the same in storage. This is an accepted limitation — see [Limitatio
|
|||||||
|
|
||||||
Archive is a **soft delete**: the agent record stays on disk with `archivedAt` set, the runtime is closed, and the agent disappears from active lists. Archive is **global** — it lives on the server and propagates to every connected client.
|
Archive is a **soft delete**: the agent record stays on disk with `archivedAt` set, the runtime is closed, and the agent disappears from active lists. Archive is **global** — it lives on the server and propagates to every connected client.
|
||||||
|
|
||||||
|
`create_agent_request` can opt an agent into `autoArchive`. In that mode the daemon archives the agent after the first terminal turn event (`turn_completed`, `turn_failed`, or `turn_canceled`). If the same request created a Paseo worktree through its `worktree` field, auto-archive archives that worktree too, which removes the agent records inside the worktree.
|
||||||
|
|
||||||
Archiving runs through `AgentManager.archiveAgent` (`packages/server/src/server/agent/agent-manager.ts`):
|
Archiving runs through `AgentManager.archiveAgent` (`packages/server/src/server/agent/agent-manager.ts`):
|
||||||
|
|
||||||
1. Snapshot the current session into the registry
|
1. Snapshot the current session into the registry
|
||||||
|
|||||||
@@ -608,6 +608,62 @@ test("sends create_agent_request with string workspace ids", async () => {
|
|||||||
await expect(createPromise).rejects.toThrow("compat test sentinel");
|
await expect(createPromise).rejects.toThrow("compat test sentinel");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("sends worktree target and autoArchive in create_agent_request", 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 createPromise = client.createAgent({
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/tmp/project",
|
||||||
|
worktree: {
|
||||||
|
mode: "branch-off",
|
||||||
|
newBranch: "agent-lifecycle-dispatch",
|
||||||
|
base: "main",
|
||||||
|
},
|
||||||
|
autoArchive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mock.sent).toHaveLength(1);
|
||||||
|
const request = parseSentFrame(mock.sent[0]);
|
||||||
|
expect(request).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "create_agent_request",
|
||||||
|
worktree: {
|
||||||
|
mode: "branch-off",
|
||||||
|
newBranch: "agent-lifecycle-dispatch",
|
||||||
|
base: "main",
|
||||||
|
},
|
||||||
|
autoArchive: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
mock.triggerMessage(
|
||||||
|
wrapSessionMessage({
|
||||||
|
type: "status",
|
||||||
|
payload: {
|
||||||
|
status: "agent_create_failed",
|
||||||
|
requestId: request.requestId,
|
||||||
|
error: "worktree auto archive sentinel",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(createPromise).rejects.toThrow("worktree auto archive sentinel");
|
||||||
|
});
|
||||||
|
|
||||||
test("sends structured attachments with create_agent_request", async () => {
|
test("sends structured attachments with create_agent_request", async () => {
|
||||||
const logger = createMockLogger();
|
const logger = createMockLogger();
|
||||||
const mock = createMockTransport();
|
const mock = createMockTransport();
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
|
|||||||
images?: CreateAgentRequestMessage["images"];
|
images?: CreateAgentRequestMessage["images"];
|
||||||
attachments?: CreateAgentRequestMessage["attachments"];
|
attachments?: CreateAgentRequestMessage["attachments"];
|
||||||
git?: GitSetupOptions;
|
git?: GitSetupOptions;
|
||||||
|
worktree?: CreateAgentRequestMessage["worktree"];
|
||||||
|
autoArchive?: CreateAgentRequestMessage["autoArchive"];
|
||||||
worktreeName?: string;
|
worktreeName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
labels?: Record<string, string>;
|
labels?: Record<string, string>;
|
||||||
@@ -1848,6 +1850,8 @@ export class DaemonClient {
|
|||||||
? { attachments: options.attachments }
|
? { attachments: options.attachments }
|
||||||
: {}),
|
: {}),
|
||||||
...(options.git ? { git: options.git } : {}),
|
...(options.git ? { git: options.git } : {}),
|
||||||
|
...(options.worktree ? { worktree: options.worktree } : {}),
|
||||||
|
...(options.autoArchive !== undefined ? { autoArchive: options.autoArchive } : {}),
|
||||||
...(options.worktreeName ? { worktreeName: options.worktreeName } : {}),
|
...(options.worktreeName ? { worktreeName: options.worktreeName } : {}),
|
||||||
...(options.labels && Object.keys(options.labels).length > 0
|
...(options.labels && Object.keys(options.labels).length > 0
|
||||||
? { labels: options.labels }
|
? { labels: options.labels }
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import type pino from "pino";
|
||||||
|
|
||||||
|
import type { GitHubService } from "../../services/github-service.js";
|
||||||
|
import { isPaseoOwnedWorktreeCwd } from "../../utils/worktree.js";
|
||||||
|
import { archivePaseoWorktree } from "../paseo-worktree-archive-service.js";
|
||||||
|
import type {
|
||||||
|
CreatePaseoWorktreeWorkflowFn,
|
||||||
|
CreatePaseoWorktreeWorkflowResult,
|
||||||
|
} from "../worktree-session.js";
|
||||||
|
import type { WorkspaceGitService } from "../workspace-git-service.js";
|
||||||
|
import type {
|
||||||
|
CreateAgentWorktreeTarget,
|
||||||
|
FirstAgentContext,
|
||||||
|
SessionOutboundMessage,
|
||||||
|
} from "../messages.js";
|
||||||
|
import type { AgentManager } from "./agent-manager.js";
|
||||||
|
import type { AgentStorage } from "./agent-storage.js";
|
||||||
|
|
||||||
|
interface CreateAgentLifecycleDispatchDependencies {
|
||||||
|
paseoHome: string;
|
||||||
|
agentManager: AgentManager;
|
||||||
|
agentStorage: AgentStorage;
|
||||||
|
github: GitHubService;
|
||||||
|
workspaceGitService: WorkspaceGitService;
|
||||||
|
createPaseoWorktreeWorkflow: CreatePaseoWorktreeWorkflowFn;
|
||||||
|
archiveAgentForClose: (agentId: string) => Promise<unknown>;
|
||||||
|
archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
|
||||||
|
emit: (message: SessionOutboundMessage) => void;
|
||||||
|
emitAgentRemove: (agentId: string) => void;
|
||||||
|
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds: Iterable<string>) => Promise<void>;
|
||||||
|
markWorkspaceArchiving: (workspaceIds: Iterable<string>, archivingAt: string) => void;
|
||||||
|
clearWorkspaceArchiving: (workspaceIds: Iterable<string>) => void;
|
||||||
|
isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
|
||||||
|
killTerminalsUnderPath: (rootPath: string) => Promise<void>;
|
||||||
|
logger: pino.Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateAgentLifecycleDispatch {
|
||||||
|
private readonly autoArchiveAgentIds = new Set<string>();
|
||||||
|
|
||||||
|
constructor(private readonly dependencies: CreateAgentLifecycleDispatchDependencies) {}
|
||||||
|
|
||||||
|
async createWorktreeForRequest(input: {
|
||||||
|
cwd: string;
|
||||||
|
target: CreateAgentWorktreeTarget | undefined;
|
||||||
|
firstAgentContext: FirstAgentContext;
|
||||||
|
hasLegacyGitOptions: boolean;
|
||||||
|
}): Promise<CreatePaseoWorktreeWorkflowResult | null> {
|
||||||
|
if (input.target && input.hasLegacyGitOptions) {
|
||||||
|
throw new Error("create_agent_request worktree cannot be combined with git options");
|
||||||
|
}
|
||||||
|
if (!input.target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createWorktreeForTarget(input.cwd, input.target, input.firstAgentContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerAutoArchiveIfRequested(input: {
|
||||||
|
autoArchive: boolean | undefined;
|
||||||
|
agentId: string;
|
||||||
|
createdWorktree: CreatePaseoWorktreeWorkflowResult | null;
|
||||||
|
}): void {
|
||||||
|
if (input.autoArchive !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerAutoArchiveOnTerminalState(input.agentId, {
|
||||||
|
worktreePath: input.createdWorktree?.worktree.worktreePath ?? null,
|
||||||
|
repoRoot: input.createdWorktree?.repoRoot ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupCreatedWorktreeAfterFailedAgentCreate(input: {
|
||||||
|
createdWorktree: CreatePaseoWorktreeWorkflowResult | null;
|
||||||
|
createdAgentId: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { createdWorktree, createdAgentId } = input;
|
||||||
|
if (!createdWorktree || createdAgentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.archiveAutoCreatedWorktree({
|
||||||
|
agentId: null,
|
||||||
|
worktreePath: createdWorktree.worktree.worktreePath,
|
||||||
|
repoRoot: createdWorktree.repoRoot,
|
||||||
|
}).catch((archiveError) => {
|
||||||
|
this.dependencies.logger.warn(
|
||||||
|
{
|
||||||
|
err: archiveError,
|
||||||
|
worktreePath: createdWorktree.worktree.worktreePath,
|
||||||
|
},
|
||||||
|
"Failed to clean up worktree after create_agent_request failed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createWorktreeForTarget(
|
||||||
|
cwd: string,
|
||||||
|
target: CreateAgentWorktreeTarget,
|
||||||
|
firstAgentContext: FirstAgentContext,
|
||||||
|
): Promise<CreatePaseoWorktreeWorkflowResult> {
|
||||||
|
const baseInput = {
|
||||||
|
cwd,
|
||||||
|
firstAgentContext,
|
||||||
|
runSetup: false,
|
||||||
|
paseoHome: this.dependencies.paseoHome,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
switch (target.mode) {
|
||||||
|
case "branch-off":
|
||||||
|
return this.dependencies.createPaseoWorktreeWorkflow(
|
||||||
|
{
|
||||||
|
...baseInput,
|
||||||
|
worktreeSlug: target.newBranch,
|
||||||
|
action: "branch-off",
|
||||||
|
...(target.base ? { refName: target.base } : {}),
|
||||||
|
},
|
||||||
|
target.base ? { resolveDefaultBranch: async () => target.base! } : undefined,
|
||||||
|
);
|
||||||
|
case "checkout-branch":
|
||||||
|
return this.dependencies.createPaseoWorktreeWorkflow({
|
||||||
|
...baseInput,
|
||||||
|
action: "checkout",
|
||||||
|
refName: target.branch,
|
||||||
|
});
|
||||||
|
case "checkout-pr":
|
||||||
|
return this.dependencies.createPaseoWorktreeWorkflow({
|
||||||
|
...baseInput,
|
||||||
|
action: "checkout",
|
||||||
|
githubPrNumber: target.prNumber,
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported create_agent_request worktree target");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerAutoArchiveOnTerminalState(
|
||||||
|
agentId: string,
|
||||||
|
options: { worktreePath: string | null; repoRoot: string | null },
|
||||||
|
): void {
|
||||||
|
const unsubscribe = this.dependencies.agentManager.subscribe(
|
||||||
|
(event) => {
|
||||||
|
if (event.type !== "agent_stream") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.event.type !== "turn_completed" &&
|
||||||
|
event.event.type !== "turn_failed" &&
|
||||||
|
event.event.type !== "turn_canceled"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
void this.autoArchiveAgentOnce(agentId, options);
|
||||||
|
},
|
||||||
|
{ agentId, replayState: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async autoArchiveAgentOnce(
|
||||||
|
agentId: string,
|
||||||
|
options: { worktreePath: string | null; repoRoot: string | null },
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.autoArchiveAgentIds.has(agentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.autoArchiveAgentIds.add(agentId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (options.worktreePath) {
|
||||||
|
await this.archiveAutoCreatedWorktree({
|
||||||
|
agentId,
|
||||||
|
worktreePath: options.worktreePath,
|
||||||
|
repoRoot: options.repoRoot,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.dependencies.archiveAgentForClose(agentId);
|
||||||
|
} catch (error) {
|
||||||
|
this.dependencies.logger.warn({ err: error, agentId }, "Failed to auto-archive agent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async archiveAutoCreatedWorktree(options: {
|
||||||
|
agentId: string | null;
|
||||||
|
worktreePath: string;
|
||||||
|
repoRoot: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
const ownership = await isPaseoOwnedWorktreeCwd(options.worktreePath, {
|
||||||
|
paseoHome: this.dependencies.paseoHome,
|
||||||
|
});
|
||||||
|
if (!ownership.allowed) {
|
||||||
|
throw new Error("Auto-created worktree is not a Paseo-owned worktree");
|
||||||
|
}
|
||||||
|
|
||||||
|
await archivePaseoWorktree(
|
||||||
|
{
|
||||||
|
paseoHome: this.dependencies.paseoHome,
|
||||||
|
github: this.dependencies.github,
|
||||||
|
workspaceGitService: this.dependencies.workspaceGitService,
|
||||||
|
agentManager: this.dependencies.agentManager,
|
||||||
|
agentStorage: this.dependencies.agentStorage,
|
||||||
|
archiveWorkspaceRecord: this.dependencies.archiveWorkspaceRecord,
|
||||||
|
emit: this.dependencies.emit,
|
||||||
|
emitWorkspaceUpdatesForWorkspaceIds: this.dependencies.emitWorkspaceUpdatesForWorkspaceIds,
|
||||||
|
markWorkspaceArchiving: this.dependencies.markWorkspaceArchiving,
|
||||||
|
clearWorkspaceArchiving: this.dependencies.clearWorkspaceArchiving,
|
||||||
|
isPathWithinRoot: this.dependencies.isPathWithinRoot,
|
||||||
|
killTerminalsUnderPath: this.dependencies.killTerminalsUnderPath,
|
||||||
|
sessionLogger: this.dependencies.logger,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetPath: options.worktreePath,
|
||||||
|
repoRoot: options.repoRoot ?? ownership.repoRoot ?? null,
|
||||||
|
worktreesRoot: ownership.worktreeRoot,
|
||||||
|
requestId: randomUUID(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options.agentId) {
|
||||||
|
this.dependencies.emitAgentRemove(options.agentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@ async function collectUntil(
|
|||||||
|
|
||||||
function collectSubscribedUntil(
|
function collectSubscribedUntil(
|
||||||
session: AgentSession,
|
session: AgentSession,
|
||||||
predicate: (event: AgentStreamEvent) => boolean,
|
predicate: (event: AgentStreamEvent, events: AgentStreamEvent[]) => boolean,
|
||||||
timeoutMs = 45_000,
|
timeoutMs = 45_000,
|
||||||
): Promise<AgentStreamEvent[]> {
|
): Promise<AgentStreamEvent[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -103,7 +103,7 @@ function collectSubscribedUntil(
|
|||||||
|
|
||||||
const unsubscribe = session.subscribe((event) => {
|
const unsubscribe = session.subscribe((event) => {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
if (!predicate(event)) {
|
if (!predicate(event, events)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@@ -252,7 +252,7 @@ describe("ClaudeAgentSession integration", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
value: "default",
|
value: "default",
|
||||||
displayName: "Default (recommended)",
|
displayName: "Default (recommended)",
|
||||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
supportedEffortLevels: expect.arrayContaining(["low", "medium", "high", "max"]),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(models).toContainEqual(
|
expect(models).toContainEqual(
|
||||||
@@ -374,6 +374,14 @@ describe("ClaudeAgentSession integration", () => {
|
|||||||
const autonomousWakeToken = `AUTONOMOUS_WAKE_${Date.now().toString(36)}`;
|
const autonomousWakeToken = `AUTONOMOUS_WAKE_${Date.now().toString(36)}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const liveEventsPromise = collectSubscribedUntil(
|
||||||
|
handle.session,
|
||||||
|
(event, events) =>
|
||||||
|
isTerminalEvent(event) &&
|
||||||
|
compactText(getAssistantText(events)).includes(compactText(autonomousWakeToken)),
|
||||||
|
90_000,
|
||||||
|
);
|
||||||
|
|
||||||
const foregroundEvents = await collectUntilTerminal(
|
const foregroundEvents = await collectUntilTerminal(
|
||||||
streamSession(
|
streamSession(
|
||||||
handle.session,
|
handle.session,
|
||||||
@@ -390,15 +398,12 @@ describe("ClaudeAgentSession integration", () => {
|
|||||||
|
|
||||||
expect(compactText(getAssistantText(foregroundEvents))).toContain("spawned");
|
expect(compactText(getAssistantText(foregroundEvents))).toContain("spawned");
|
||||||
|
|
||||||
const liveEvents = await collectSubscribedUntil(
|
const liveEvents = await liveEventsPromise;
|
||||||
handle.session,
|
|
||||||
(event) => isTerminalEvent(event),
|
|
||||||
90_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
liveEvents.some((event) => event.type === "turn_started" && event.provider === "claude"),
|
liveEvents.some((event) => event.type === "turn_started" && event.provider === "claude"),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(compactText(getAssistantText(liveEvents))).toContain(compactText(autonomousWakeToken));
|
||||||
expect(liveEvents.at(-1)).toMatchObject({
|
expect(liveEvents.at(-1)).toMatchObject({
|
||||||
type: "turn_completed",
|
type: "turn_completed",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, beforeEach, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { getFullAccessConfig } from "./daemon-e2e/agent-configs.js";
|
||||||
|
import { createDaemonTestContext, type DaemonTestContext } from "./test-utils/index.js";
|
||||||
|
import type { CreateAgentOptions } from "./test-utils/index.js";
|
||||||
|
import type { CreateAgentWorktreeTarget } from "./messages.js";
|
||||||
|
|
||||||
|
let ctx: DaemonTestContext;
|
||||||
|
const tempRoots: string[] = [];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
ctx = await createDaemonTestContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await ctx.cleanup();
|
||||||
|
for (const tempRoot of tempRoots.splice(0)) {
|
||||||
|
rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function createGitRepo(): string {
|
||||||
|
const tempRoot = mkdtempSync(path.join(tmpdir(), "create-agent-worktree-"));
|
||||||
|
tempRoots.push(tempRoot);
|
||||||
|
const repoDir = path.join(tempRoot, "repo");
|
||||||
|
execFileSync("git", ["init", "-b", "main", repoDir], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["config", "user.email", "test@getpaseo.local"], {
|
||||||
|
cwd: repoDir,
|
||||||
|
stdio: "pipe",
|
||||||
|
});
|
||||||
|
execFileSync("git", ["config", "user.name", "Paseo Test"], { cwd: repoDir, stdio: "pipe" });
|
||||||
|
writeFileSync(path.join(repoDir, "README.md"), "hello\n");
|
||||||
|
execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "initial"], {
|
||||||
|
cwd: repoDir,
|
||||||
|
stdio: "pipe",
|
||||||
|
});
|
||||||
|
return repoDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectAgentAbsentFromActiveList(agentId: string): Promise<void> {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const active = await ctx.client.fetchAgents();
|
||||||
|
return active.entries.map((entry) => entry.agent.id).includes(agentId);
|
||||||
|
},
|
||||||
|
{ timeout: 15000, interval: 100 },
|
||||||
|
)
|
||||||
|
.toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectAgentPresentInActiveList(agentId: string): Promise<void> {
|
||||||
|
const active = await ctx.client.fetchAgents();
|
||||||
|
expect(active.entries.map((entry) => entry.agent.id)).toContain(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectActiveAgentListEmpty(): Promise<void> {
|
||||||
|
const active = await ctx.client.fetchAgents();
|
||||||
|
expect(active.entries).toEqual([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectWorktreeAbsentFromList(repoDir: string, worktreePath: string): Promise<void> {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const listed = await ctx.client.getPaseoWorktreeList({ cwd: repoDir });
|
||||||
|
return listed.worktrees.map((worktree) => worktree.worktreePath).includes(worktreePath);
|
||||||
|
},
|
||||||
|
{ timeout: 15000, interval: 100 },
|
||||||
|
)
|
||||||
|
.toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectWorktreePresentInList(repoDir: string, worktreePath: string): Promise<void> {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const listed = await ctx.client.getPaseoWorktreeList({ cwd: repoDir });
|
||||||
|
return listed.worktrees.map((worktree) => worktree.worktreePath).includes(worktreePath);
|
||||||
|
},
|
||||||
|
{ timeout: 5000, interval: 100 },
|
||||||
|
)
|
||||||
|
.toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectWorktreeListEmpty(repoDir: string): Promise<void> {
|
||||||
|
const listed = await ctx.client.getPaseoWorktreeList({ cwd: repoDir });
|
||||||
|
expect(listed.worktrees).toEqual([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAgentInBranchOffWorktree(options?: {
|
||||||
|
autoArchive?: boolean;
|
||||||
|
branchName?: string;
|
||||||
|
}): Promise<{ repoDir: string; agentId: string; worktreePath: string }> {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
const branchName = options?.branchName ?? `agent-lifecycle-${Date.now()}`;
|
||||||
|
const created = await ctx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd: repoDir,
|
||||||
|
},
|
||||||
|
worktree: {
|
||||||
|
mode: "branch-off",
|
||||||
|
newBranch: branchName,
|
||||||
|
base: "main",
|
||||||
|
},
|
||||||
|
...(options?.autoArchive !== undefined ? { autoArchive: options.autoArchive } : {}),
|
||||||
|
initialPrompt: "Say done.",
|
||||||
|
});
|
||||||
|
return { repoDir, agentId: created.id, worktreePath: created.cwd };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("create_agent_request creates a worktree and auto-archives both after the first turn", async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
const worktree: CreateAgentWorktreeTarget = {
|
||||||
|
mode: "branch-off",
|
||||||
|
newBranch: "agent-lifecycle-dispatch-test",
|
||||||
|
base: "main",
|
||||||
|
};
|
||||||
|
const request: CreateAgentOptions & {
|
||||||
|
worktree: CreateAgentWorktreeTarget;
|
||||||
|
autoArchive: true;
|
||||||
|
} = {
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd: repoDir,
|
||||||
|
},
|
||||||
|
worktree,
|
||||||
|
autoArchive: true,
|
||||||
|
initialPrompt: "Say done.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await ctx.client.createAgent(request);
|
||||||
|
|
||||||
|
expect(created.cwd).not.toBe(repoDir);
|
||||||
|
const listedWithWorktree = await ctx.client.getPaseoWorktreeList({ cwd: repoDir });
|
||||||
|
expect(listedWithWorktree.worktrees).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
worktreePath: created.cwd,
|
||||||
|
branchName: "agent-lifecycle-dispatch-test",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ctx.client.waitForFinish(created.id, 10000);
|
||||||
|
|
||||||
|
await expectAgentAbsentFromActiveList(created.id);
|
||||||
|
await expectWorktreeAbsentFromList(repoDir, created.cwd);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test("create_agent_request with autoArchive archives only the agent when no worktree was created", async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
const created = await ctx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd: repoDir,
|
||||||
|
},
|
||||||
|
autoArchive: true,
|
||||||
|
initialPrompt: "Say done.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.client.waitForFinish(created.id, 10000);
|
||||||
|
|
||||||
|
await expectAgentAbsentFromActiveList(created.id);
|
||||||
|
const archived = await ctx.client.fetchAgents({ filter: { includeArchived: true } });
|
||||||
|
expect(archived.entries.map((entry) => entry.agent.id)).toContain(created.id);
|
||||||
|
const worktrees = await ctx.client.getPaseoWorktreeList({ cwd: repoDir });
|
||||||
|
expect(worktrees.worktrees).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create_agent_request with autoArchive archives an agent whose first turn fails", async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
const created = await ctx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd: repoDir,
|
||||||
|
},
|
||||||
|
autoArchive: true,
|
||||||
|
initialPrompt: "Emit a turn failure.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.client.waitForFinish(created.id, 10000);
|
||||||
|
|
||||||
|
await expectAgentAbsentFromActiveList(created.id);
|
||||||
|
const archived = await ctx.client.fetchAgents({ filter: { includeArchived: true } });
|
||||||
|
expect(archived.entries.map((entry) => entry.agent.id)).toContain(created.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create_agent_request without autoArchive keeps today's active listing behavior", async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
const created = await ctx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd: repoDir,
|
||||||
|
},
|
||||||
|
initialPrompt: "Say done.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.client.waitForFinish(created.id, 10000);
|
||||||
|
|
||||||
|
await expectAgentPresentInActiveList(created.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create_agent_request with worktree but no autoArchive leaves agent and worktree active", async () => {
|
||||||
|
const created = await createAgentInBranchOffWorktree();
|
||||||
|
|
||||||
|
await ctx.client.waitForFinish(created.agentId, 10000);
|
||||||
|
|
||||||
|
await expectAgentPresentInActiveList(created.agentId);
|
||||||
|
await expectWorktreePresentInList(created.repoDir, created.worktreePath);
|
||||||
|
|
||||||
|
await ctx.client.archivePaseoWorktree({ worktreePath: created.worktreePath });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("archiving a created worktree still archives nested agents", async () => {
|
||||||
|
const created = await createAgentInBranchOffWorktree();
|
||||||
|
|
||||||
|
await ctx.client.waitForFinish(created.agentId, 10000);
|
||||||
|
await ctx.client.archivePaseoWorktree({ worktreePath: created.worktreePath });
|
||||||
|
|
||||||
|
await expectAgentAbsentFromActiveList(created.agentId);
|
||||||
|
await expectWorktreeAbsentFromList(created.repoDir, created.worktreePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create_agent_request rejects legacy git options before creating a worktree", async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd: repoDir,
|
||||||
|
},
|
||||||
|
git: {
|
||||||
|
createNewBranch: true,
|
||||||
|
newBranchName: "legacy-agent-branch",
|
||||||
|
},
|
||||||
|
worktree: {
|
||||||
|
mode: "branch-off",
|
||||||
|
newBranch: "agent-lifecycle-dispatch-test",
|
||||||
|
base: "main",
|
||||||
|
},
|
||||||
|
initialPrompt: "Say done.",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("worktree cannot be combined with git options");
|
||||||
|
|
||||||
|
await expectActiveAgentListEmpty();
|
||||||
|
await expectWorktreeListEmpty(repoDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create_agent_request fails cleanly when worktree creation cannot resolve target", async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd: repoDir,
|
||||||
|
},
|
||||||
|
worktree: {
|
||||||
|
mode: "checkout-branch",
|
||||||
|
branch: "does-not-exist",
|
||||||
|
},
|
||||||
|
initialPrompt: "Say done.",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
|
await expectActiveAgentListEmpty();
|
||||||
|
await expectWorktreeListEmpty(repoDir);
|
||||||
|
});
|
||||||
@@ -252,6 +252,7 @@ import {
|
|||||||
handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage,
|
handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage,
|
||||||
} from "./worktree-session.js";
|
} from "./worktree-session.js";
|
||||||
import { toWorktreeWireError } from "./worktree-errors.js";
|
import { toWorktreeWireError } from "./worktree-errors.js";
|
||||||
|
import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
|
||||||
|
|
||||||
const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
|
const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
|
||||||
|
|
||||||
@@ -826,6 +827,7 @@ export class Session {
|
|||||||
private readonly serverId: string | undefined;
|
private readonly serverId: string | undefined;
|
||||||
private readonly daemonVersion: string | undefined;
|
private readonly daemonVersion: string | undefined;
|
||||||
private readonly daemonRuntimeConfig: SessionOptions["daemonRuntimeConfig"];
|
private readonly daemonRuntimeConfig: SessionOptions["daemonRuntimeConfig"];
|
||||||
|
private readonly createAgentLifecycleDispatch: CreateAgentLifecycleDispatch;
|
||||||
private voiceModeAgentId: string | null = null;
|
private voiceModeAgentId: string | null = null;
|
||||||
private voiceModeBaseConfig: VoiceModeBaseConfig | null = null;
|
private voiceModeBaseConfig: VoiceModeBaseConfig | null = null;
|
||||||
|
|
||||||
@@ -911,6 +913,35 @@ export class Session {
|
|||||||
isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
|
isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
|
||||||
sessionLogger: this.sessionLogger,
|
sessionLogger: this.sessionLogger,
|
||||||
});
|
});
|
||||||
|
this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
|
||||||
|
paseoHome: this.paseoHome,
|
||||||
|
agentManager: this.agentManager,
|
||||||
|
agentStorage: this.agentStorage,
|
||||||
|
github: this.github,
|
||||||
|
workspaceGitService: this.workspaceGitService,
|
||||||
|
createPaseoWorktreeWorkflow: (input, workflowOptions) =>
|
||||||
|
this.createPaseoWorktreeWorkflow(input, workflowOptions),
|
||||||
|
archiveAgentForClose: (agentId) => this.archiveAgentForClose(agentId),
|
||||||
|
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
||||||
|
emit: (message) => this.emit(message),
|
||||||
|
emitAgentRemove: (agentId) => {
|
||||||
|
if (this.agentUpdatesSubscription) {
|
||||||
|
this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
|
||||||
|
kind: "remove",
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) =>
|
||||||
|
this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
|
||||||
|
markWorkspaceArchiving: (workspaceIds, archivingAt) =>
|
||||||
|
this.markWorkspaceArchiving(workspaceIds, archivingAt),
|
||||||
|
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
||||||
|
isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
|
||||||
|
killTerminalsUnderPath: (rootPath) =>
|
||||||
|
this.terminalController.killTerminalsUnderPath(rootPath),
|
||||||
|
logger: this.sessionLogger,
|
||||||
|
});
|
||||||
this.providerSnapshotManager = providerSnapshotManager ?? null;
|
this.providerSnapshotManager = providerSnapshotManager ?? null;
|
||||||
this.scriptRouteStore = scriptRouteStore ?? null;
|
this.scriptRouteStore = scriptRouteStore ?? null;
|
||||||
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
|
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
|
||||||
@@ -3081,6 +3112,8 @@ export class Session {
|
|||||||
clientMessageId,
|
clientMessageId,
|
||||||
outputSchema,
|
outputSchema,
|
||||||
git,
|
git,
|
||||||
|
worktree,
|
||||||
|
autoArchive,
|
||||||
images,
|
images,
|
||||||
attachments,
|
attachments,
|
||||||
labels,
|
labels,
|
||||||
@@ -3093,6 +3126,8 @@ export class Session {
|
|||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let createdWorktreeForCleanup: CreatePaseoWorktreeWorkflowResult | null = null;
|
||||||
|
let createdAgentId: string | null = null;
|
||||||
try {
|
try {
|
||||||
const trimmedPrompt = initialPrompt?.trim();
|
const trimmedPrompt = initialPrompt?.trim();
|
||||||
const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({
|
const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({
|
||||||
@@ -3108,8 +3143,18 @@ export class Session {
|
|||||||
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
|
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
|
||||||
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
||||||
};
|
};
|
||||||
|
const createdWorktree = await this.createAgentLifecycleDispatch.createWorktreeForRequest({
|
||||||
|
cwd: config.cwd,
|
||||||
|
target: worktree,
|
||||||
|
firstAgentContext,
|
||||||
|
hasLegacyGitOptions: Boolean(git),
|
||||||
|
});
|
||||||
|
createdWorktreeForCleanup = createdWorktree;
|
||||||
|
const createAgentConfig: AgentSessionConfig = createdWorktree
|
||||||
|
? { ...resolvedConfig, cwd: createdWorktree.worktree.worktreePath }
|
||||||
|
: resolvedConfig;
|
||||||
const { sessionConfig, setupContinuation } = await this.buildAgentSessionConfig(
|
const { sessionConfig, setupContinuation } = await this.buildAgentSessionConfig(
|
||||||
resolvedConfig,
|
createAgentConfig,
|
||||||
git,
|
git,
|
||||||
worktreeName,
|
worktreeName,
|
||||||
firstAgentContext,
|
firstAgentContext,
|
||||||
@@ -3127,7 +3172,13 @@ export class Session {
|
|||||||
initialPrompt: trimmedPrompt,
|
initialPrompt: trimmedPrompt,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
|
createdAgentId = snapshot.id;
|
||||||
await this.forwardAgentUpdate(snapshot);
|
await this.forwardAgentUpdate(snapshot);
|
||||||
|
this.createAgentLifecycleDispatch.registerAutoArchiveIfRequested({
|
||||||
|
autoArchive,
|
||||||
|
agentId: snapshot.id,
|
||||||
|
createdWorktree,
|
||||||
|
});
|
||||||
|
|
||||||
await this.sendInitialCreateAgentPrompt({
|
await this.sendInitialCreateAgentPrompt({
|
||||||
snapshot,
|
snapshot,
|
||||||
@@ -3161,6 +3212,10 @@ export class Session {
|
|||||||
`Created agent ${snapshot.id} (${snapshot.provider})`,
|
`Created agent ${snapshot.id} (${snapshot.provider})`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await this.createAgentLifecycleDispatch.cleanupCreatedWorktreeAfterFailedAgentCreate({
|
||||||
|
createdWorktree: createdWorktreeForCleanup,
|
||||||
|
createdAgentId,
|
||||||
|
});
|
||||||
const wireError = toWorktreeWireError(error);
|
const wireError = toWorktreeWireError(error);
|
||||||
this.sessionLogger.error({ err: error }, "Failed to create agent");
|
this.sessionLogger.error({ err: error }, "Failed to create agent");
|
||||||
if (requestId) {
|
if (requestId) {
|
||||||
|
|||||||
@@ -703,6 +703,17 @@ class FakeAgentSession implements AgentSession {
|
|||||||
await this.appendHistoryEvent(turnStarted);
|
await this.appendHistoryEvent(turnStarted);
|
||||||
this.notifySubscribers(turnStarted);
|
this.notifySubscribers(turnStarted);
|
||||||
|
|
||||||
|
if (textPrompt.toLowerCase().includes("emit a turn failure")) {
|
||||||
|
const failed: AgentStreamEvent = {
|
||||||
|
type: "turn_failed",
|
||||||
|
provider: this.providerName,
|
||||||
|
error: "Requested fake provider failure",
|
||||||
|
};
|
||||||
|
await this.appendHistoryEvent(failed);
|
||||||
|
this.notifySubscribers(failed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const stress = parseAgentStreamStressPrompt(textPrompt);
|
const stress = parseAgentStreamStressPrompt(textPrompt);
|
||||||
if (stress !== null) {
|
if (stress !== null) {
|
||||||
await this.emitStressTurn(stress);
|
await this.emitStressTurn(stress);
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { SessionInboundMessageSchema } from "./messages.js";
|
||||||
|
|
||||||
|
describe("create_agent_request worktree and autoArchive fields", () => {
|
||||||
|
test("accepts optional worktree branch-off target and autoArchive", () => {
|
||||||
|
const parsed = SessionInboundMessageSchema.parse({
|
||||||
|
type: "create_agent_request",
|
||||||
|
requestId: "create-agent-worktree",
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/repo/app",
|
||||||
|
},
|
||||||
|
worktree: {
|
||||||
|
mode: "branch-off",
|
||||||
|
newBranch: "agent-lifecycle-dispatch",
|
||||||
|
base: "main",
|
||||||
|
},
|
||||||
|
autoArchive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsed).toEqual({
|
||||||
|
type: "create_agent_request",
|
||||||
|
requestId: "create-agent-worktree",
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/repo/app",
|
||||||
|
},
|
||||||
|
worktree: {
|
||||||
|
mode: "branch-off",
|
||||||
|
newBranch: "agent-lifecycle-dispatch",
|
||||||
|
base: "main",
|
||||||
|
},
|
||||||
|
autoArchive: true,
|
||||||
|
labels: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps legacy create_agent_request defaults unchanged", () => {
|
||||||
|
const parsed = SessionInboundMessageSchema.parse({
|
||||||
|
type: "create_agent_request",
|
||||||
|
requestId: "legacy-create-agent",
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/repo/app",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsed).toEqual({
|
||||||
|
type: "create_agent_request",
|
||||||
|
requestId: "legacy-create-agent",
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/repo/app",
|
||||||
|
},
|
||||||
|
labels: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1058,6 +1058,24 @@ const GitSetupOptionsSchema = z.object({
|
|||||||
|
|
||||||
export type GitSetupOptions = z.infer<typeof GitSetupOptionsSchema>;
|
export type GitSetupOptions = z.infer<typeof GitSetupOptionsSchema>;
|
||||||
|
|
||||||
|
export const CreateAgentWorktreeTargetSchema = z.discriminatedUnion("mode", [
|
||||||
|
z.object({
|
||||||
|
mode: z.literal("branch-off"),
|
||||||
|
newBranch: z.string().min(1),
|
||||||
|
base: z.string().min(1).optional(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
mode: z.literal("checkout-branch"),
|
||||||
|
branch: z.string().min(1),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
mode: z.literal("checkout-pr"),
|
||||||
|
prNumber: z.number().int().positive(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type CreateAgentWorktreeTarget = z.infer<typeof CreateAgentWorktreeTargetSchema>;
|
||||||
|
|
||||||
export const CreateAgentRequestMessageSchema = z.object({
|
export const CreateAgentRequestMessageSchema = z.object({
|
||||||
type: z.literal("create_agent_request"),
|
type: z.literal("create_agent_request"),
|
||||||
config: AgentSessionConfigSchema,
|
config: AgentSessionConfigSchema,
|
||||||
@@ -1070,6 +1088,8 @@ export const CreateAgentRequestMessageSchema = z.object({
|
|||||||
images: z.array(ImageAttachmentSchema).optional(),
|
images: z.array(ImageAttachmentSchema).optional(),
|
||||||
attachments: AgentAttachmentsSchema,
|
attachments: AgentAttachmentsSchema,
|
||||||
git: GitSetupOptionsSchema.optional(),
|
git: GitSetupOptionsSchema.optional(),
|
||||||
|
worktree: CreateAgentWorktreeTargetSchema.optional(),
|
||||||
|
autoArchive: z.boolean().optional(),
|
||||||
labels: z.record(z.string()).default({}),
|
labels: z.record(z.string()).default({}),
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user