From 005ffbffd540c8fa62423214512ffe7efef57949 Mon Sep 17 00:00:00 2001
From: Darrell
Date: Thu, 22 Oct 2026 13:38:44 -0400
Subject: [PATCH] fix(server): give agents their workspace id as
PASEO_WORKSPACE_ID
Agents got PASEO_AGENT_ID and PASEO_AGENT_CWD but not their workspace id, so an
agent couldn't rename or archive its own workspace, or name it to create_agent,
when several workspaces share its cwd. The launch env now sets
PASEO_WORKSPACE_ID whenever the agent has a workspace, including on resume after
a daemon restart, where the id now comes from the stored record. OpenCode treats
it as a per-session key, so it doesn't force a dedicated server.
Refs #5098
Co-Authored-By: Claude Opus 5.5
---
.../src/server/agent/agent-manager.test.ts | 66 +++++++++++++++++++
.../server/src/server/agent/agent-manager.ts | 35 +++++++++-
.../server/agent/providers/opencode-agent.ts | 6 +-
.../opencode/v2/configuration.test.ts | 9 ++-
.../providers/opencode/v2/configuration.ts | 2 +-
public-docs/cli.md | 2 +
public-docs/cli.zh-CN.md | 2 +
public-docs/plugins/hooks.md | 2 +-
public-docs/plugins/hooks.zh-CN.md | 2 +-
9 files changed, 118 insertions(+), 8 deletions(-)
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
```