diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 5b77522473..2721cd67f9 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -3286,6 +3286,71 @@ test.each([ }, ); +test("createAgent and resume give the agent its workspace id", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-workspace-env-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + + class CaptureClient extends TestAgentClient { + createLaunchContext: AgentLaunchContext | undefined; + resumeLaunchContext: AgentLaunchContext | undefined; + + override async createSession( + config: AgentSessionConfig, + launchContext?: AgentLaunchContext, + ): Promise { + this.createLaunchContext = launchContext; + return new TestAgentSession(config); + } + + override async resumeSession( + handle: AgentPersistenceHandle, + overrides?: Partial, + launchContext?: AgentLaunchContext, + ): Promise { + this.resumeLaunchContext = launchContext; + const metadata = (handle.metadata ?? {}) as Partial; + return new TestAgentSession({ ...metadata, ...overrides, provider: "codex", cwd: workdir }); + } + } + + const client = new CaptureClient(); + const manager = new AgentManager({ + clients: { + codex: client, + }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000140", + }); + + const snapshot = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: "ws-agent-home", + }); + expect(client.createLaunchContext?.env).toEqual({ + PASEO_AGENT_ID: snapshot.id, + PASEO_AGENT_CWD: workdir, + PASEO_WORKSPACE_ID: "ws-agent-home", + }); + + // A daemon restart resumes from the stored record, without a workspace id in the call. + await manager.closeAgent(snapshot.id); + await manager.resumeAgentFromPersistence( + { + provider: "codex", + sessionId: "workspace-env-session", + metadata: { provider: "codex", cwd: workdir }, + }, + { cwd: workdir }, + snapshot.id, + ); + expect(client.resumeLaunchContext?.env).toEqual({ + PASEO_AGENT_ID: snapshot.id, + PASEO_AGENT_CWD: workdir, + PASEO_WORKSPACE_ID: "ws-agent-home", + }); +}); + test("createAgent passes persistSession to provider create options", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const storagePath = join(workdir, "agents"); @@ -4731,6 +4796,7 @@ test("importProviderSession imports the selected session without listing and pub env: { PASEO_AGENT_ID: imported.id, PASEO_AGENT_CWD: workdir, + PASEO_WORKSPACE_ID: "ws-imported", }, }); expect(imported.lifecycle).toBe("idle"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index fdecc4ad1c..c008999944 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -664,6 +664,36 @@ function createAbortError(signal: AbortSignal | undefined, fallbackMessage: stri return Object.assign(new Error(message), { name: "AbortError" }); } +// PASEO_WORKSPACE_ID lets an agent rename or archive its own workspace; several workspaces can +// share one cwd, so the agent can't work it out from PASEO_AGENT_CWD. +function agentIdentityEnv({ + agentId, + cwd, + opening, +}: { + agentId: string; + cwd: string; + opening: { workspaceId?: string | null } | undefined; +}): Record { + const workspaceId = opening?.workspaceId; + return { + PASEO_AGENT_ID: agentId, + PASEO_AGENT_CWD: cwd, + ...(workspaceId ? { PASEO_WORKSPACE_ID: workspaceId } : {}), + }; +} + +// A restart resumes from the stored record, with no workspace id in the call. +function resolveResumeWorkspaceId({ + options, + record, +}: { + options: { workspaceId?: string } | undefined; + record: Pick | null; +}): string | null { + return options?.workspaceId ?? record?.workspaceId ?? null; +} + function validateAgentId(agentId: string, source: string): string { const result = AgentIdSchema.safeParse(agentId); if (!result.success) { @@ -1593,7 +1623,7 @@ export class AgentManager { { reason: "resume", purpose, - workspaceId: options?.workspaceId ?? null, + workspaceId: resolveResumeWorkspaceId({ options, record }), title: record?.title ?? mergedConfig.title ?? null, labels: record?.labels ?? options?.labels ?? {}, }, @@ -5774,8 +5804,7 @@ export class AgentManager { mcpServerOwners, env: { ...env, - PASEO_AGENT_ID: agentId, - PASEO_AGENT_CWD: cwd, + ...agentIdentityEnv({ agentId, cwd, opening }), }, }; const useNativeTools = diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 6fef290d91..4feef0a46b 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -349,7 +349,11 @@ type OpenCodeAgentConfig = Omit & { providerOptions: OpenCodeProviderOptions; }; -const OPENCODE_SESSION_ENV_KEYS = new Set(["PASEO_AGENT_ID", "PASEO_AGENT_CWD"]); +const OPENCODE_SESSION_ENV_KEYS = new Set([ + "PASEO_AGENT_ID", + "PASEO_AGENT_CWD", + "PASEO_WORKSPACE_ID", +]); function requiresDedicatedOpenCodeServer( config: OpenCodeAgentConfig, diff --git a/packages/server/src/server/agent/providers/opencode/v2/configuration.test.ts b/packages/server/src/server/agent/providers/opencode/v2/configuration.test.ts index 6479ca3851..27ddeef4f3 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/configuration.test.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/configuration.test.ts @@ -24,7 +24,14 @@ describe("OpenCode v2 helper isolation", () => { expect( requiresDedicatedV2Server( { provider: "opencode", cwd: "/tmp/project" }, - { agentId: "agent", env: { PASEO_AGENT_ID: "agent", PASEO_AGENT_CWD: "/tmp/project" } }, + { + agentId: "agent", + env: { + PASEO_AGENT_ID: "agent", + PASEO_AGENT_CWD: "/tmp/project", + PASEO_WORKSPACE_ID: "ws-1", + }, + }, ), ).toBe(false); }); diff --git a/packages/server/src/server/agent/providers/opencode/v2/configuration.ts b/packages/server/src/server/agent/providers/opencode/v2/configuration.ts index 9a565e6fca..ab4ae1e2f6 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/configuration.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/configuration.ts @@ -72,7 +72,7 @@ export async function applyResumeOverrides( // it must not force a dedicated server. Custom MCP is location-scoped, and any // other env key belongs to the process, so both need isolation. Mirrors the v1 // adapter's `requiresDedicatedOpenCodeServer`. -const V2_SESSION_ENV_KEYS = new Set(["PASEO_AGENT_ID", "PASEO_AGENT_CWD"]); +const V2_SESSION_ENV_KEYS = new Set(["PASEO_AGENT_ID", "PASEO_AGENT_CWD", "PASEO_WORKSPACE_ID"]); export function requiresDedicatedV2Server( config: AgentSessionConfig, diff --git a/public-docs/cli.md b/public-docs/cli.md index f28911ac5a..700206e5a5 100644 --- a/public-docs/cli.md +++ b/public-docs/cli.md @@ -211,6 +211,8 @@ paseo workspace rename --reset # back to the branch or director paseo workspace archive ``` +Inside an agent session, `PASEO_WORKSPACE_ID` holds the agent's own workspace ID, so an agent can run `paseo workspace rename "$PASEO_WORKSPACE_ID" "Auth rework"` or `paseo workspace archive "$PASEO_WORKSPACE_ID"` even when other workspaces share its directory. + Add `--forge ` to PR checkout when Paseo cannot infer the forge from the source checkout. See [Git worktrees](/docs/worktrees) for setup hooks and services. ## Terminals diff --git a/public-docs/cli.zh-CN.md b/public-docs/cli.zh-CN.md index 6e47391771..29b8aec65f 100644 --- a/public-docs/cli.zh-CN.md +++ b/public-docs/cli.zh-CN.md @@ -202,6 +202,8 @@ paseo workspace rename --reset # back to the branch or director paseo workspace archive ``` +在 Agent 会话中,`PASEO_WORKSPACE_ID` 保存该 Agent 自己的工作区 ID。即使其他工作区共享同一个目录,Agent 也可以运行 `paseo workspace rename "$PASEO_WORKSPACE_ID" "Auth rework"` 或 `paseo workspace archive "$PASEO_WORKSPACE_ID"`。 + 当 Paseo 无法从源检出推断代码托管平台时,为 PR 检出添加 `--forge `。setup 钩子和服务见 [Git worktree](/docs/worktrees)。 ## 终端 {#terminals} diff --git a/public-docs/plugins/hooks.md b/public-docs/plugins/hooks.md index 25c8cf0527..17340a00a9 100644 --- a/public-docs/plugins/hooks.md +++ b/public-docs/plugins/hooks.md @@ -598,7 +598,7 @@ Creation request → resolve defaults and validate provider configuration → derive launch configuration with Paseo runtime tools and daemon prompt → agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers) - → set PASEO_AGENT_ID and PASEO_AGENT_CWD + → set PASEO_AGENT_ID, PASEO_AGENT_CWD, and PASEO_WORKSPACE_ID when the agent has a workspace → open provider session and save agent configuration ``` diff --git a/public-docs/plugins/hooks.zh-CN.md b/public-docs/plugins/hooks.zh-CN.md index 5469582b3f..0c35f814b3 100644 --- a/public-docs/plugins/hooks.zh-CN.md +++ b/public-docs/plugins/hooks.zh-CN.md @@ -581,7 +581,7 @@ Creation request → resolve defaults and validate provider configuration → derive launch configuration with Paseo runtime tools and daemon prompt → agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers) - → set PASEO_AGENT_ID and PASEO_AGENT_CWD + → set PASEO_AGENT_ID, PASEO_AGENT_CWD, and PASEO_WORKSPACE_ID when the agent has a workspace → open provider session and save agent configuration ```