@getpaseo/server 0.1.96 → 0.1.97-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/server/{utils/executable.d.ts → executable-resolution/executable-resolution.d.ts} +2 -2
  2. package/dist/server/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
  3. package/dist/server/executable-resolution/windows.d.ts +18 -0
  4. package/dist/server/executable-resolution/windows.js +62 -0
  5. package/dist/server/server/agent/agent-loading.js +4 -1
  6. package/dist/server/server/agent/agent-manager.d.ts +10 -2
  7. package/dist/server/server/agent/agent-manager.js +34 -46
  8. package/dist/server/server/agent/agent-projections.js +3 -0
  9. package/dist/server/server/agent/agent-prompt.js +19 -1
  10. package/dist/server/server/agent/agent-response-loop.js +2 -4
  11. package/dist/server/server/agent/agent-storage.d.ts +18 -19
  12. package/dist/server/server/agent/agent-storage.js +6 -23
  13. package/dist/server/server/agent/create-agent/create.d.ts +2 -12
  14. package/dist/server/server/agent/create-agent/create.js +28 -30
  15. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +4 -2
  16. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +31 -22
  17. package/dist/server/server/agent/create-agent-title.d.ts +2 -0
  18. package/dist/server/server/agent/create-agent-title.js +5 -0
  19. package/dist/server/server/agent/import-sessions.d.ts +1 -10
  20. package/dist/server/server/agent/import-sessions.js +1 -53
  21. package/dist/server/server/agent/lifecycle-command.js +5 -4
  22. package/dist/server/server/agent/mcp-server.d.ts +8 -5
  23. package/dist/server/server/agent/mcp-server.js +41 -14
  24. package/dist/server/server/agent/mcp-shared.d.ts +6 -3
  25. package/dist/server/server/agent/mcp-shared.js +3 -0
  26. package/dist/server/server/agent/provider-launch-config.js +1 -1
  27. package/dist/server/server/agent/providers/acp-agent.d.ts +5 -0
  28. package/dist/server/server/agent/providers/acp-agent.js +31 -26
  29. package/dist/server/server/agent/providers/claude/agent.js +45 -6
  30. package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -1
  31. package/dist/server/server/agent/providers/copilot-acp-agent.js +1 -0
  32. package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +0 -7
  33. package/dist/server/server/agent/providers/cursor-acp-agent.js +0 -78
  34. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
  35. package/dist/server/server/agent/providers/mock-load-test-agent.js +73 -1
  36. package/dist/server/server/agent/providers/opencode/server-manager.js +1 -1
  37. package/dist/server/server/agent/structured-generation-providers.js +45 -1
  38. package/dist/server/server/agent-attention-policy.d.ts +12 -3
  39. package/dist/server/server/agent-attention-policy.js +15 -3
  40. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +7 -6
  41. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +21 -16
  42. package/dist/server/server/bootstrap.d.ts +3 -0
  43. package/dist/server/server/bootstrap.js +125 -64
  44. package/dist/server/server/config.js +1 -0
  45. package/dist/server/server/daemon-config-store.js +1 -0
  46. package/dist/server/server/exports.d.ts +1 -1
  47. package/dist/server/server/exports.js +1 -1
  48. package/dist/server/server/loop-service.d.ts +24 -24
  49. package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
  50. package/dist/server/server/migrations/backfill-workspace-id.migration.js +60 -0
  51. package/dist/server/server/paseo-worktree-service.d.ts +9 -0
  52. package/dist/server/server/paseo-worktree-service.js +74 -12
  53. package/dist/server/server/path-utils.d.ts +1 -0
  54. package/dist/server/server/path-utils.js +6 -1
  55. package/dist/server/server/persisted-config.d.ts +7 -0
  56. package/dist/server/server/persisted-config.js +1 -0
  57. package/dist/server/server/persistence-hooks.d.ts +1 -0
  58. package/dist/server/server/persistence-hooks.js +13 -5
  59. package/dist/server/server/resolve-workspace-id-for-path.d.ts +3 -0
  60. package/dist/server/server/resolve-workspace-id-for-path.js +41 -0
  61. package/dist/server/server/script-proxy.d.ts +1 -1
  62. package/dist/server/server/script-proxy.js +1 -1
  63. package/dist/server/server/service-proxy.js +1 -1
  64. package/dist/server/server/session.d.ts +33 -6
  65. package/dist/server/server/session.js +691 -202
  66. package/dist/server/server/websocket-server.d.ts +5 -0
  67. package/dist/server/server/websocket-server.js +137 -3
  68. package/dist/server/server/workspace-archive-service.d.ts +60 -3
  69. package/dist/server/server/workspace-archive-service.js +217 -4
  70. package/dist/server/server/workspace-directory.d.ts +20 -2
  71. package/dist/server/server/workspace-directory.js +148 -70
  72. package/dist/server/server/workspace-git-service.js +21 -21
  73. package/dist/server/server/workspace-reconciliation-service.d.ts +1 -1
  74. package/dist/server/server/workspace-reconciliation-service.js +21 -22
  75. package/dist/server/server/workspace-registry-bootstrap.js +23 -10
  76. package/dist/server/server/workspace-registry-model.d.ts +3 -3
  77. package/dist/server/server/workspace-registry-model.js +9 -10
  78. package/dist/server/server/workspace-registry.d.ts +17 -4
  79. package/dist/server/server/workspace-registry.js +27 -0
  80. package/dist/server/server/worktree/commands.d.ts +7 -5
  81. package/dist/server/server/worktree/commands.js +38 -18
  82. package/dist/server/server/worktree-bootstrap.d.ts +1 -0
  83. package/dist/server/server/worktree-bootstrap.js +4 -1
  84. package/dist/server/server/worktree-branch-name-generator.d.ts +5 -1
  85. package/dist/server/server/worktree-branch-name-generator.js +29 -7
  86. package/dist/server/server/worktree-session.d.ts +4 -5
  87. package/dist/server/server/worktree-session.js +9 -3
  88. package/dist/server/services/github-service.js +1 -1
  89. package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
  90. package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
  91. package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
  92. package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
  93. package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
  94. package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
  95. package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
  96. package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
  97. package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
  98. package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
  99. package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
  100. package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
  101. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
  102. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
  103. package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
  104. package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
  105. package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
  106. package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
  107. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
  108. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
  109. package/dist/server/terminal/terminal-manager-factory.d.ts +4 -1
  110. package/dist/server/terminal/terminal-manager-factory.js +2 -2
  111. package/dist/server/terminal/terminal-manager.d.ts +33 -2
  112. package/dist/server/terminal/terminal-manager.js +144 -18
  113. package/dist/server/terminal/terminal-output-coalescer.d.ts +4 -0
  114. package/dist/server/terminal/terminal-output-coalescer.js +18 -0
  115. package/dist/server/terminal/terminal-restore.d.ts +1 -0
  116. package/dist/server/terminal/terminal-restore.js +6 -0
  117. package/dist/server/terminal/terminal-session-controller.d.ts +4 -2
  118. package/dist/server/terminal/terminal-session-controller.js +65 -24
  119. package/dist/server/terminal/terminal-worker-process.js +146 -63
  120. package/dist/server/terminal/terminal-worker-protocol.d.ts +19 -14
  121. package/dist/server/terminal/terminal.d.ts +42 -0
  122. package/dist/server/terminal/terminal.js +235 -16
  123. package/dist/server/terminal/worker-terminal-manager.d.ts +1 -0
  124. package/dist/server/terminal/worker-terminal-manager.js +220 -36
  125. package/dist/server/utils/build-metadata-prompt.d.ts +8 -3
  126. package/dist/server/utils/build-metadata-prompt.js +10 -9
  127. package/dist/server/utils/github-remote.js +1 -1
  128. package/dist/server/utils/tree-kill.d.ts +2 -2
  129. package/dist/src/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
  130. package/dist/src/executable-resolution/windows.js +62 -0
  131. package/dist/src/server/agent/provider-launch-config.js +1 -1
  132. package/dist/src/server/persisted-config.js +1 -0
  133. package/package.json +10 -5
  134. package/dist/server/server/agent/agent-metadata-generator.d.ts +0 -36
  135. package/dist/server/server/agent/agent-metadata-generator.js +0 -112
  136. package/dist/server/server/paseo-worktree-archive-service.d.ts +0 -41
  137. package/dist/server/server/paseo-worktree-archive-service.js +0 -144
  138. package/dist/server/utils/wrap-user-instructions.d.ts +0 -2
  139. package/dist/server/utils/wrap-user-instructions.js +0 -13
@@ -2,7 +2,7 @@ import equal from "fast-deep-equal";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { stat } from "node:fs/promises";
5
- import { basename, resolve, sep } from "path";
5
+ import { resolve, sep } from "path";
6
6
  import { homedir } from "node:os";
7
7
  import { z } from "zod";
8
8
  import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
@@ -23,7 +23,7 @@ import { createVoiceTurnController, } from "./voice/voice-turn-controller.js";
23
23
  import { buildConfigOverrides, extractTimestamps, isStoredAgentProviderAvailable, toAgentPersistenceHandle, } from "./persistence-hooks.js";
24
24
  import { ensureAgentLoaded } from "./agent/agent-loading.js";
25
25
  import { formatSystemNotificationPrompt, sendPromptToAgent, waitForAgentRunStartWithTimeout, unarchiveAgentState, } from "./agent/agent-prompt.js";
26
- import { resolveCreateAgentTitles } from "./agent/create-agent-title.js";
26
+ import { resolveCreateAgentTitles, resolveFirstAgentPromptTitle, } from "./agent/create-agent-title.js";
27
27
  import { respondToAgentPermission } from "./agent/permission-response.js";
28
28
  import { experimental_createMCPClient } from "ai";
29
29
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -42,8 +42,9 @@ import { StructuredAgentFallbackError, StructuredAgentResponseError, generateStr
42
42
  import { resolveStructuredGenerationProviders, } from "./agent/structured-generation-providers.js";
43
43
  import { getAgentStreamEventTurnId, } from "./agent/agent-sdk-types.js";
44
44
  import { ImportSessionsRequestError, importProviderSession, listImportableProviderSessions, normalizeImportAgentRequest, } from "./agent/import-sessions.js";
45
- import { checkoutLiteFromGitSnapshot, normalizeWorkspaceId as normalizePersistedWorkspaceId, deriveProjectGroupingName, classifyDirectoryForProjectMembership, deriveWorkspaceDisplayName, } from "./workspace-registry-model.js";
46
- import { createPersistedProjectRecord, createPersistedWorkspaceRecord, resolveProjectDisplayName, } from "./workspace-registry.js";
45
+ import { checkoutLiteFromGitSnapshot, classifyDirectoryForProjectMembership, deriveWorkspaceDisplayName, generateWorkspaceId, } from "./workspace-registry-model.js";
46
+ import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
47
+ import { createPersistedProjectRecord, createPersistedWorkspaceRecord, resolveProjectDisplayName, resolveWorkspaceDisplayName, resolveWorkspaceName, } from "./workspace-registry.js";
47
48
  import { buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, wrapSpokenInput, } from "./voice-config.js";
48
49
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
49
50
  import { listDirectoryEntries, readExplorerFile, readExplorerFileBytes, getDownloadableFileInfo, } from "./file-explorer/service.js";
@@ -51,7 +52,7 @@ import { readPaseoConfigForEdit, writePaseoConfigForEdit, } from "../utils/paseo
51
52
  import { buildMetadataPrompt } from "../utils/build-metadata-prompt.js";
52
53
  import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
53
54
  import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
54
- import { checkoutResolvedBranch, commitChanges, mergeToBase, mergeFromBase, pullCurrentBranch, pushCurrentBranch, createPullRequest, renameCurrentBranch, } from "../utils/checkout-git.js";
55
+ import { checkoutResolvedBranch, commitChanges, mergeToBase, mergeFromBase, pullCurrentBranch, pushCurrentBranch, createPullRequest, renameCurrentBranch as renameCurrentBranchDefault, } from "../utils/checkout-git.js";
55
56
  import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
56
57
  import { getProjectIcon } from "../utils/project-icon.js";
57
58
  import { expandTilde } from "../utils/path.js";
@@ -63,11 +64,12 @@ import { ChatServiceError, parseMentionAgentIds, } from "./chat/chat-service.js"
63
64
  import { notifyChatMentions, prepareChatMentionFanout } from "./chat/chat-mentions.js";
64
65
  import { execCommand } from "../utils/spawn.js";
65
66
  import { assertPullRequestAutoMergeDisableReady, assertPullRequestAutoMergeEnableReady, createGitHubService, } from "../services/github-service.js";
66
- import { summarizeFetchWorkspacesEntries, WorkspaceDirectory, } from "./workspace-directory.js";
67
+ import { summarizeFetchWorkspacesEntries, workspaceIdsOnCheckout, WorkspaceDirectory, } from "./workspace-directory.js";
67
68
  import { shouldEmitPendingBootstrapUpdate } from "./workspace-bootstrap-dedupe.js";
68
- import { attemptFirstAgentBranchAutoName, createPaseoWorktree, } from "./paseo-worktree-service.js";
69
- import { generateBranchNameFromFirstAgentContext } from "./worktree-branch-name-generator.js";
69
+ import { attemptFirstAgentBranchAutoName, createLocalCheckoutWorkspace, createPaseoWorktree, } from "./paseo-worktree-service.js";
70
+ import { generateBranchNameFromFirstAgentContext, } from "./worktree-branch-name-generator.js";
70
71
  import { assertSafeGitRef as assertWorktreeSafeGitRef, buildAgentSessionConfig as buildWorktreeAgentSessionConfig, createPaseoWorktreeWorkflow as createWorktreeWorkflow, handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest, handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest, handlePaseoWorktreeListRequest as handleWorktreeListRequest, handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage, } from "./worktree-session.js";
72
+ import { archiveByScope } from "./workspace-archive-service.js";
71
73
  import { toWorktreeWireError } from "./worktree-errors.js";
72
74
  import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
73
75
  const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
@@ -136,7 +138,12 @@ function diffChangeTypeFor(file) {
136
138
  return "D";
137
139
  return "M";
138
140
  }
139
- function buildWorkspaceCheckout(workspace, project) {
141
+ function buildWorkspaceCheckout(workspace, project,
142
+ // The persisted `branch` field is the source of truth, but it is null for
143
+ // records created before branch was lifted to its own field (no migrations,
144
+ // per data-model.md) and for any path that didn't backfill it. Fall back to
145
+ // the live git branch so checkout.currentBranch never regresses to null.
146
+ fallbackBranch) {
140
147
  if (project.kind !== "git") {
141
148
  return {
142
149
  cwd: workspace.cwd,
@@ -148,11 +155,12 @@ function buildWorkspaceCheckout(workspace, project) {
148
155
  mainRepoRoot: null,
149
156
  };
150
157
  }
158
+ const currentBranch = workspace.branch ?? fallbackBranch ?? null;
151
159
  if (workspace.kind === "worktree") {
152
160
  return {
153
161
  cwd: workspace.cwd,
154
162
  isGit: true,
155
- currentBranch: workspace.displayName,
163
+ currentBranch,
156
164
  remoteUrl: null,
157
165
  worktreeRoot: workspace.cwd,
158
166
  isPaseoOwnedWorktree: true,
@@ -162,7 +170,7 @@ function buildWorkspaceCheckout(workspace, project) {
162
170
  return {
163
171
  cwd: workspace.cwd,
164
172
  isGit: true,
165
- currentBranch: workspace.displayName,
173
+ currentBranch,
166
174
  remoteUrl: null,
167
175
  worktreeRoot: workspace.cwd,
168
176
  isPaseoOwnedWorktree: false,
@@ -296,6 +304,7 @@ export class Session {
296
304
  this.agentMcpClient = null;
297
305
  this.agentTools = null;
298
306
  this.unsubscribeAgentEvents = null;
307
+ this.unsubscribeTerminalWorkspaceContributionEvents = null;
299
308
  this.agentUpdatesSubscription = null;
300
309
  this.workspaceUpdatesSubscription = null;
301
310
  this.clientActivity = null;
@@ -331,13 +340,14 @@ export class Session {
331
340
  }
332
341
  },
333
342
  });
334
- const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
343
+ const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, getTransportBufferedAmount, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, renameCurrentBranch, generateWorkspaceName, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
335
344
  this.clientId = clientId;
336
345
  this.appVersion = appVersion ?? null;
337
346
  this.clientCapabilities = parseClientCapabilities(clientCapabilities);
338
347
  this.sessionId = uuidv4();
339
348
  this.onMessage = onMessage;
340
349
  this.onBinaryMessage = onBinaryMessage ?? null;
350
+ this.getTransportBufferedAmount = getTransportBufferedAmount ?? (() => 0);
341
351
  this.onLifecycleIntent = onLifecycleIntent ?? null;
342
352
  this.downloadTokenStore = downloadTokenStore;
343
353
  this.pushTokenStore = pushTokenStore;
@@ -359,6 +369,8 @@ export class Session {
359
369
  this.loopService = loopService;
360
370
  this.checkoutDiffManager = checkoutDiffManager;
361
371
  this.github = github ?? createGitHubService();
372
+ this.renameCurrentBranch = renameCurrentBranch ?? renameCurrentBranchDefault;
373
+ this.generateWorkspaceName = generateWorkspaceName ?? generateBranchNameFromFirstAgentContext;
362
374
  this.workspaceGitService = workspaceGitService;
363
375
  this.daemonConfigStore = daemonConfigStore;
364
376
  this.mcpBaseUrl = mcpBaseUrl ?? null;
@@ -377,6 +389,7 @@ export class Session {
377
389
  .map((workspace) => workspace.cwd);
378
390
  },
379
391
  clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
392
+ getClientBufferedAmount: () => this.getTransportBufferedAmount(),
380
393
  });
381
394
  this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
382
395
  paseoHome: this.paseoHome,
@@ -387,6 +400,8 @@ export class Session {
387
400
  workspaceGitService: this.workspaceGitService,
388
401
  createPaseoWorktreeWorkflow: (input, workflowOptions) => this.createPaseoWorktreeWorkflow(input, workflowOptions),
389
402
  archiveAgentForClose: (agentId) => this.archiveAgentForClose(agentId),
403
+ findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
404
+ listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
390
405
  archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
391
406
  emit: (message) => this.emit(message),
392
407
  emitAgentRemove: (agentId) => {
@@ -400,8 +415,7 @@ export class Session {
400
415
  emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
401
416
  markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
402
417
  clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
403
- isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
404
- killTerminalsUnderPath: (rootPath) => this.terminalController.killTerminalsUnderPath(rootPath),
418
+ killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
405
419
  logger: this.sessionLogger,
406
420
  });
407
421
  this.providerSnapshotManager = providerSnapshotManager;
@@ -425,6 +439,7 @@ export class Session {
425
439
  projectRegistry: this.projectRegistry,
426
440
  workspaceRegistry: this.workspaceRegistry,
427
441
  listAgentPayloads: () => this.listAgentPayloads(),
442
+ listTerminalActivityContributions: () => this.listTerminalActivityContributions(),
428
443
  isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
429
444
  buildWorkspaceDescriptor: (input) => this.buildWorkspaceDescriptor(input),
430
445
  });
@@ -477,9 +492,6 @@ export class Session {
477
492
  async emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIds) {
478
493
  await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds);
479
494
  }
480
- async emitWorkspaceUpdatesForExternalCwds(cwds) {
481
- await Promise.all(Array.from(cwds, (cwd) => this.emitWorkspaceUpdateForCwd(cwd)));
482
- }
483
495
  async warmWorkspaceGitDataForWorkspace(workspace) {
484
496
  await this.syncWorkspaceGitObserverForWorkspace(workspace);
485
497
  await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
@@ -626,6 +638,14 @@ export class Session {
626
638
  */
627
639
  subscribeToOptionalManagers() {
628
640
  this.terminalController.start();
641
+ if (this.terminalManager) {
642
+ this.unsubscribeTerminalWorkspaceContributionEvents =
643
+ this.terminalManager.subscribeTerminalWorkspaceContributionChanged((event) => {
644
+ void this.emitWorkspaceUpdateForTerminalContribution(event).catch((error) => {
645
+ this.sessionLogger.warn({ err: error, terminalId: event.terminalId }, "Failed to emit workspace update after terminal contribution changed");
646
+ });
647
+ });
648
+ }
629
649
  const handleProviderSnapshotChange = (entries, cwd) => {
630
650
  // COMPAT(providersSnapshot): keep provider visibility gating for older clients.
631
651
  const visibleEntries = entries.filter((entry) => this.isProviderVisibleToClient(entry.provider));
@@ -763,7 +783,7 @@ export class Session {
763
783
  payload.archivedAt = storedRecord?.archivedAt ?? null;
764
784
  return payload;
765
785
  }
766
- buildStoredAgentPayload(record, registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds()) {
786
+ buildStoredAgentPayload(record, registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds())) {
767
787
  return buildStoredAgentPayload(record, registeredProviderIds);
768
788
  }
769
789
  isProviderVisibleToClient(provider) {
@@ -867,61 +887,37 @@ export class Session {
867
887
  });
868
888
  }
869
889
  }
870
- async findWorkspaceByDirectory(cwd, options) {
871
- const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
872
- const workspaces = await this.workspaceRegistry.list();
873
- const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(normalizedCwd, workspaces);
874
- return workspaces.find((workspace) => workspace.workspaceId === workspaceId) ?? null;
875
- }
876
890
  async findExactWorkspaceByDirectory(cwd, options) {
877
891
  const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
878
892
  const workspaces = await this.workspaceRegistry.list();
879
893
  return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null;
880
894
  }
881
895
  async resolveWorkspaceDirectory(cwd, options) {
882
- const normalizedCwd = normalizePersistedWorkspaceId(cwd);
896
+ const normalizedCwd = resolve(cwd);
883
897
  if (options?.refreshGit === false) {
884
898
  const snapshot = this.workspaceGitService.peekSnapshot(normalizedCwd);
885
- return normalizePersistedWorkspaceId(snapshot?.git.repoRoot ?? normalizedCwd);
899
+ return resolve(snapshot?.git.repoRoot ?? normalizedCwd);
886
900
  }
887
901
  const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
888
- return normalizePersistedWorkspaceId(checkout.worktreeRoot ?? normalizedCwd);
902
+ return resolve(checkout.worktreeRoot ?? normalizedCwd);
889
903
  }
890
904
  async buildProjectPlacementForWorkspace(workspace, projectRecord) {
891
905
  const project = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
892
906
  if (!project) {
893
907
  throw new Error(`Project not found for workspace ${workspace.workspaceId}`);
894
908
  }
895
- const checkout = buildWorkspaceCheckout(workspace, project);
909
+ const liveBranch = this.workspaceGitService.peekSnapshot(workspace.cwd)?.git.currentBranch ?? null;
910
+ const checkout = buildWorkspaceCheckout(workspace, project, liveBranch);
896
911
  return {
897
912
  projectKey: project.projectId,
898
913
  projectName: resolveProjectDisplayName(project),
899
914
  checkout,
900
915
  };
901
916
  }
902
- async buildProjectPlacementForCwd(cwd, options) {
903
- const workspace = await this.findWorkspaceByDirectory(cwd, {
904
- refreshGit: options?.refreshGit,
905
- });
906
- if (!workspace) {
907
- if (!options?.fallback) {
908
- return null;
909
- }
910
- const normalizedCwd = normalizePersistedWorkspaceId(cwd);
911
- return {
912
- projectKey: normalizedCwd,
913
- projectName: deriveProjectGroupingName(normalizedCwd),
914
- checkout: {
915
- cwd: normalizedCwd,
916
- isGit: false,
917
- currentBranch: null,
918
- remoteUrl: null,
919
- worktreeRoot: null,
920
- isPaseoOwnedWorktree: false,
921
- mainRepoRoot: null,
922
- },
923
- };
924
- }
917
+ async buildProjectPlacementForWorkspaceId(workspaceId) {
918
+ const workspace = await this.workspaceRegistry.get(workspaceId);
919
+ if (!workspace)
920
+ return null;
925
921
  return this.buildProjectPlacementForWorkspace(workspace);
926
922
  }
927
923
  async forwardAgentUpdate(agent) {
@@ -929,33 +925,41 @@ export class Session {
929
925
  const subscription = this.agentUpdatesSubscription;
930
926
  const payload = await this.buildAgentPayload(agent);
931
927
  if (subscription) {
932
- const project = await this.buildProjectPlacementForCwd(payload.cwd, {
933
- refreshGit: false,
934
- fallback: true,
935
- });
928
+ const project = payload.workspaceId
929
+ ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
930
+ : null;
936
931
  if (!project) {
937
- throw new Error(`Workspace not found for agent ${payload.id}`);
938
- }
939
- const matches = this.matchesAgentFilter({
940
- agent: payload,
941
- project,
942
- filter: subscription.filter,
943
- });
944
- if (matches) {
945
932
  this.bufferOrEmitAgentUpdate(subscription, {
946
- kind: "upsert",
947
- agent: payload,
948
- project,
933
+ kind: "remove",
934
+ agentId: payload.id,
949
935
  });
950
936
  }
951
937
  else {
952
- this.bufferOrEmitAgentUpdate(subscription, {
953
- kind: "remove",
954
- agentId: payload.id,
938
+ const matches = this.matchesAgentFilter({
939
+ agent: payload,
940
+ project,
941
+ filter: subscription.filter,
955
942
  });
943
+ if (matches) {
944
+ this.bufferOrEmitAgentUpdate(subscription, {
945
+ kind: "upsert",
946
+ agent: payload,
947
+ project,
948
+ });
949
+ }
950
+ else {
951
+ this.bufferOrEmitAgentUpdate(subscription, {
952
+ kind: "remove",
953
+ agentId: payload.id,
954
+ });
955
+ }
956
956
  }
957
957
  }
958
- await this.emitWorkspaceUpdateForCwd(payload.cwd);
958
+ // A lifecycle change updates exactly the agent's owning workspace, never
959
+ // every workspace sharing its cwd. Ownership is the agent's workspaceId.
960
+ if (payload.workspaceId) {
961
+ await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
962
+ }
959
963
  }
960
964
  catch (error) {
961
965
  this.sessionLogger.error({ err: error }, "Failed to emit agent update");
@@ -1341,8 +1345,12 @@ export class Session {
1341
1345
  return this.handleOpenProjectRequest(msg);
1342
1346
  case "archive_workspace_request":
1343
1347
  return this.handleArchiveWorkspaceRequest(msg);
1348
+ case "workspace.create.request":
1349
+ return this.handleWorkspaceCreateRequest(msg);
1344
1350
  case "workspace.clear_attention.request":
1345
1351
  return this.handleWorkspaceClearAttentionRequest(msg);
1352
+ case "workspace.title.set.request":
1353
+ return this.handleWorkspaceTitleSetRequest(msg.workspaceId, msg.title, msg.requestId);
1346
1354
  case "file_explorer_request":
1347
1355
  return this.handleFileExplorerRequest(msg);
1348
1356
  case "project_icon_request":
@@ -1506,8 +1514,8 @@ export class Session {
1506
1514
  }
1507
1515
  async handleDeleteAgentRequest(agentId, requestId) {
1508
1516
  this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
1509
- const knownCwd = this.agentManager.getAgent(agentId)?.cwd ??
1510
- (await this.agentStorage.get(agentId))?.cwd ??
1517
+ const knownWorkspaceId = this.agentManager.getAgent(agentId)?.workspaceId ??
1518
+ (await this.agentStorage.get(agentId))?.workspaceId ??
1511
1519
  null;
1512
1520
  // File-backed storage still needs an early delete fence before closeAgent().
1513
1521
  beginAgentDeleteIfSupported(this.agentStorage, agentId);
@@ -1540,8 +1548,8 @@ export class Session {
1540
1548
  agentId,
1541
1549
  });
1542
1550
  }
1543
- if (knownCwd) {
1544
- await this.emitWorkspaceUpdateForCwd(knownCwd);
1551
+ if (knownWorkspaceId) {
1552
+ await this.emitWorkspaceUpdateForWorkspaceId(knownWorkspaceId);
1545
1553
  }
1546
1554
  }
1547
1555
  async handleArchiveAgentRequest(agentId, requestId) {
@@ -1564,7 +1572,9 @@ export class Session {
1564
1572
  }, agentId);
1565
1573
  if (this.agentUpdatesSubscription) {
1566
1574
  const payload = this.buildStoredAgentPayload(archivedRecord);
1567
- const project = await this.buildProjectPlacementForCwd(payload.cwd);
1575
+ const project = payload.workspaceId
1576
+ ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
1577
+ : null;
1568
1578
  if (project) {
1569
1579
  const matches = this.matchesAgentFilter({
1570
1580
  agent: payload,
@@ -1588,7 +1598,9 @@ export class Session {
1588
1598
  agentId,
1589
1599
  });
1590
1600
  }
1591
- await this.emitWorkspaceUpdateForCwd(payload.cwd);
1601
+ if (payload.workspaceId) {
1602
+ await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
1603
+ }
1592
1604
  }
1593
1605
  return { agentId, archivedAt };
1594
1606
  }
@@ -1752,6 +1764,67 @@ export class Session {
1752
1764
  });
1753
1765
  }
1754
1766
  }
1767
+ async handleWorkspaceTitleSetRequest(workspaceId, title, requestId) {
1768
+ this.sessionLogger.info({ workspaceId, requestId, hasTitle: typeof title === "string" }, "session: workspace.title.set.request");
1769
+ try {
1770
+ const existing = await this.workspaceRegistry.get(workspaceId);
1771
+ if (!existing) {
1772
+ this.emit({
1773
+ type: "workspace.title.set.response",
1774
+ payload: {
1775
+ requestId,
1776
+ workspaceId,
1777
+ accepted: false,
1778
+ title: null,
1779
+ error: "Workspace not found",
1780
+ },
1781
+ });
1782
+ return;
1783
+ }
1784
+ const trimmed = title?.trim() ?? "";
1785
+ const nextTitle = trimmed.length === 0 ? null : trimmed;
1786
+ await this.workspaceRegistry.upsert({
1787
+ ...existing,
1788
+ title: nextTitle,
1789
+ updatedAt: new Date().toISOString(),
1790
+ });
1791
+ this.emit({
1792
+ type: "workspace.title.set.response",
1793
+ payload: {
1794
+ requestId,
1795
+ workspaceId,
1796
+ accepted: true,
1797
+ title: nextTitle,
1798
+ error: null,
1799
+ },
1800
+ });
1801
+ await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], {
1802
+ skipReconcile: true,
1803
+ });
1804
+ }
1805
+ catch (error) {
1806
+ this.sessionLogger.error({ err: error, workspaceId, requestId }, "session: workspace.title.set.request error");
1807
+ this.emit({
1808
+ type: "activity_log",
1809
+ payload: {
1810
+ id: uuidv4(),
1811
+ timestamp: new Date(),
1812
+ type: "error",
1813
+ content: `Failed to set workspace title: ${getErrorMessage(error)}`,
1814
+ },
1815
+ });
1816
+ this.emit({
1817
+ type: "workspace.title.set.response",
1818
+ payload: {
1819
+ requestId,
1820
+ workspaceId,
1821
+ accepted: false,
1822
+ title: null,
1823
+ error: getErrorMessageOr(error, "Failed to set workspace title"),
1824
+ },
1825
+ });
1826
+ }
1827
+ }
1755
1828
  toVoiceFeatureUnavailableContext(state) {
1756
1829
  return {
1757
1830
  reasonCode: state.reasonCode,
@@ -2088,7 +2161,7 @@ export class Session {
2088
2161
  let createdAgentId = null;
2089
2162
  try {
2090
2163
  const trimmedPrompt = initialPrompt?.trim();
2091
- const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({
2164
+ const { provisionalTitle } = resolveCreateAgentTitles({
2092
2165
  configTitle: config.title,
2093
2166
  initialPrompt: trimmedPrompt,
2094
2167
  });
@@ -2096,6 +2169,7 @@ export class Session {
2096
2169
  ...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
2097
2170
  ...(attachments && attachments.length > 0 ? { attachments } : {}),
2098
2171
  };
2172
+ const workspacePromptTitle = resolveFirstAgentPromptTitle(firstAgentContext);
2099
2173
  const createdWorktree = await this.createAgentLifecycleDispatch.createWorktreeForRequest({
2100
2174
  cwd: config.cwd,
2101
2175
  target: worktree,
@@ -2106,19 +2180,23 @@ export class Session {
2106
2180
  const createAgentConfig = createdWorktree
2107
2181
  ? { ...config, cwd: createdWorktree.worktree.worktreePath }
2108
2182
  : config;
2183
+ const workspaceId = await this.resolveOrCreateWorkspaceIdForCreateAgent({
2184
+ createdWorktree,
2185
+ requestedWorkspaceId: msg.workspaceId,
2186
+ cwd: createAgentConfig.cwd,
2187
+ initialTitle: workspacePromptTitle,
2188
+ });
2109
2189
  const { snapshot, liveSnapshot } = await createAgentCommand({
2110
2190
  agentManager: this.agentManager,
2111
2191
  agentStorage: this.agentStorage,
2112
2192
  logger: this.sessionLogger,
2113
2193
  paseoHome: this.paseoHome,
2114
2194
  worktreesRoot: this.worktreesRoot,
2115
- workspaceGitService: this.workspaceGitService,
2116
2195
  providerSnapshotManager: this.providerSnapshotManager,
2117
- daemonConfig: this.readStructuredGenerationDaemonConfig(),
2118
2196
  }, {
2119
2197
  kind: "session",
2120
2198
  config: createAgentConfig,
2121
- workspaceId: msg.workspaceId,
2199
+ workspaceId,
2122
2200
  worktreeName,
2123
2201
  initialPrompt,
2124
2202
  clientMessageId,
@@ -2129,13 +2207,21 @@ export class Session {
2129
2207
  labels,
2130
2208
  env,
2131
2209
  provisionalTitle,
2132
- explicitTitle,
2133
2210
  firstAgentContext,
2134
2211
  buildSessionConfig: (sessionConfig, gitOptions, legacyWorktreeName, ctx) => this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx),
2135
- resolveWorkspace: ({ cwd, workspaceId }) => this.resolveCreateAgentWorkspace(cwd, workspaceId),
2136
2212
  });
2137
2213
  createdAgentId = snapshot.id;
2214
+ if (!createdWorktree && msg.workspaceId) {
2215
+ await this.writeInitialWorkspaceTitleIfUntitled(workspaceId, workspacePromptTitle);
2216
+ }
2138
2217
  await this.forwardAgentUpdate(snapshot);
2218
+ if (!createdWorktree && trimmedPrompt) {
2219
+ await this.scheduleAutoNameLocalWorkspaceTitleForFirstAgent({
2220
+ workspaceId,
2221
+ cwd: createAgentConfig.cwd,
2222
+ firstAgentContext,
2223
+ });
2224
+ }
2139
2225
  this.createAgentLifecycleDispatch.registerAutoArchiveIfRequested({
2140
2226
  autoArchive,
2141
2227
  agentId: snapshot.id,
@@ -2184,16 +2270,6 @@ export class Session {
2184
2270
  });
2185
2271
  }
2186
2272
  }
2187
- async resolveCreateAgentWorkspace(cwd, workspaceId) {
2188
- const resolvedWorkspace = workspaceId
2189
- ? await this.workspaceRegistry.get(workspaceId)
2190
- : ((await this.findWorkspaceByDirectory(cwd)) ??
2191
- (await this.findOrCreateWorkspaceForDirectory(cwd)));
2192
- if (!resolvedWorkspace) {
2193
- throw new Error(`Workspace not found: ${workspaceId}`);
2194
- }
2195
- return { workspaceId: resolvedWorkspace.workspaceId };
2196
- }
2197
2273
  async handleResumeAgentRequest(msg) {
2198
2274
  const { handle, overrides, requestId } = msg;
2199
2275
  if (!handle) {
@@ -2283,17 +2359,20 @@ export class Session {
2283
2359
  const { provider, providerHandleId, requestId } = normalized;
2284
2360
  this.sessionLogger.info({ providerHandleId, provider }, `Importing agent ${providerHandleId} (${provider})`);
2285
2361
  try {
2362
+ if (!normalized.cwd) {
2363
+ throw new Error("Import requires cwd from the selected provider session");
2364
+ }
2365
+ // An imported agent mints its own workspace; ownership is its workspaceId,
2366
+ // never an existing same-cwd workspace resolved by path.
2367
+ const workspace = await this.createWorkspaceForDirectory(normalized.cwd);
2286
2368
  const { snapshot, timelineSize } = await importProviderSession({
2287
2369
  request: normalized,
2370
+ workspaceId: workspace.workspaceId,
2288
2371
  agentManager: this.agentManager,
2289
2372
  agentStorage: this.agentStorage,
2290
- workspaceGitService: this.workspaceGitService,
2291
- providerSnapshotManager: this.providerSnapshotManager,
2292
- daemonConfig: this.readStructuredGenerationDaemonConfig(),
2293
- paseoHome: this.paseoHome,
2294
2373
  logger: this.sessionLogger,
2295
2374
  });
2296
- await this.registerWorkspaceForImportedAgent(snapshot.cwd);
2375
+ await this.registerWorkspaceForImportedAgent(workspace);
2297
2376
  const agentPayload = await this.buildAgentPayload(snapshot);
2298
2377
  this.emit({
2299
2378
  type: "status",
@@ -2472,18 +2551,20 @@ export class Session {
2472
2551
  }, config, gitOptions, legacyWorktreeName, firstAgentContext);
2473
2552
  }
2474
2553
  scheduleAutoNameWorkspaceBranchForFirstAgent(input) {
2475
- setTimeout(() => {
2476
- void this.maybeAutoNameWorkspaceBranchForFirstAgent(input).catch((error) => {
2477
- this.sessionLogger.warn({ err: error, cwd: input.workspace.cwd }, "Failed to auto-name worktree branch");
2478
- });
2479
- }, 0);
2554
+ this.scheduleWorkspaceNaming(() => this.maybeAutoNameWorkspaceBranchForFirstAgent(input), {
2555
+ cwd: input.workspace.cwd,
2556
+ message: "Failed to auto-name worktree branch",
2557
+ });
2480
2558
  }
2481
2559
  async maybeAutoNameWorkspaceBranchForFirstAgent(input) {
2560
+ // Capture the generated title from the generator callback so we can write
2561
+ // title := generated title after the branch rename completes.
2562
+ let generatedTitle = null;
2482
2563
  const result = await attemptFirstAgentBranchAutoName({
2483
2564
  cwd: input.workspace.cwd,
2484
2565
  firstAgentContext: input.firstAgentContext,
2485
2566
  generateBranchNameFromContext: ({ cwd, firstAgentContext }) => {
2486
- return generateBranchNameFromFirstAgentContext({
2567
+ return this.generateWorkspaceName({
2487
2568
  agentManager: this.agentManager,
2488
2569
  cwd,
2489
2570
  workspaceGitService: this.workspaceGitService,
@@ -2492,21 +2573,99 @@ export class Session {
2492
2573
  currentSelection: this.getFocusedAgentSelectionForCwd(cwd),
2493
2574
  firstAgentContext,
2494
2575
  logger: this.sessionLogger,
2576
+ }).then((r) => {
2577
+ generatedTitle = r?.title ?? null;
2578
+ return r?.branch ?? null;
2495
2579
  });
2496
2580
  },
2497
2581
  });
2498
- if (!result.renamed || !result.branchName) {
2499
- return input.workspace;
2582
+ if (!result.renamed || !generatedTitle) {
2583
+ return;
2500
2584
  }
2501
- const updatedWorkspace = {
2502
- ...input.workspace,
2503
- displayName: result.branchName,
2504
- updatedAt: new Date().toISOString(),
2505
- };
2506
- await this.workspaceRegistry.upsert(updatedWorkspace);
2585
+ // K4: re-read from the registry before writing so any concurrent upsert
2586
+ // that happened between workspace creation and this async path is not clobbered.
2587
+ // The first-agent rename renamed the git branch too, so persist the new branch
2588
+ // alongside the title — both are this path's own fields.
2589
+ await this.applyGeneratedWorkspaceTitle(input.workspace.workspaceId, {
2590
+ title: generatedTitle,
2591
+ branch: result.branchName,
2592
+ promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
2593
+ });
2507
2594
  await this.notifyGitMutation(input.workspace.cwd, "rename-branch");
2508
2595
  await this.emitWorkspaceUpdateForCwd(input.workspace.cwd);
2509
- return updatedWorkspace;
2596
+ }
2597
+ // Generated names may replace the prompt title set at creation, but not a user
2598
+ // rename that landed while the async generator was running.
2599
+ async applyGeneratedWorkspaceTitle(workspaceId, input) {
2600
+ const current = await this.workspaceRegistry.get(workspaceId);
2601
+ if (!current) {
2602
+ return;
2603
+ }
2604
+ let title = current.title;
2605
+ if (!title || (input.promptTitle && title === input.promptTitle)) {
2606
+ title = input.title;
2607
+ }
2608
+ await this.workspaceRegistry.upsert({
2609
+ ...current,
2610
+ title,
2611
+ ...(input.branch ? { branch: input.branch } : {}),
2612
+ updatedAt: new Date().toISOString(),
2613
+ });
2614
+ }
2615
+ async writeInitialWorkspaceTitleIfUntitled(workspaceId, title) {
2616
+ if (!title) {
2617
+ return;
2618
+ }
2619
+ const current = await this.workspaceRegistry.get(workspaceId);
2620
+ if (!current || current.title) {
2621
+ return;
2622
+ }
2623
+ await this.workspaceRegistry.upsert({
2624
+ ...current,
2625
+ title,
2626
+ updatedAt: new Date().toISOString(),
2627
+ });
2628
+ }
2629
+ // Wraps the injected workspace-name generator for a directory workspace.
2630
+ async generateWorkspaceTitleFromContext(input) {
2631
+ return this.generateWorkspaceName({
2632
+ agentManager: this.agentManager,
2633
+ cwd: input.cwd,
2634
+ workspaceGitService: this.workspaceGitService,
2635
+ providerSnapshotManager: this.providerSnapshotManager,
2636
+ daemonConfig: this.readStructuredGenerationDaemonConfig(),
2637
+ currentSelection: this.getFocusedAgentSelectionForCwd(input.cwd),
2638
+ firstAgentContext: input.firstAgentContext,
2639
+ logger: this.sessionLogger,
2640
+ });
2641
+ }
2642
+ // Generates a human title for a directory workspace from the firstAgentContext
2643
+ // prompt. No branch rename — directory workspaces have no worktree git state.
2644
+ // TODO(K7): same-dir directory-workspace display disambiguation not yet implemented.
2645
+ async maybeAutoNameDirectoryWorkspaceTitle(input) {
2646
+ const generated = await this.generateWorkspaceTitleFromContext({
2647
+ cwd: input.cwd,
2648
+ firstAgentContext: input.firstAgentContext,
2649
+ });
2650
+ const title = generated?.title ?? null;
2651
+ if (!title) {
2652
+ return;
2653
+ }
2654
+ // K4: applyGeneratedWorkspaceTitle re-reads from the registry before writing.
2655
+ // Directory workspaces have no branch — write only the title.
2656
+ await this.applyGeneratedWorkspaceTitle(input.workspaceId, {
2657
+ title,
2658
+ promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
2659
+ });
2660
+ await this.emitWorkspaceUpdateForWorkspaceId(input.workspaceId);
2661
+ }
2662
+ async scheduleAutoNameLocalWorkspaceTitleForFirstAgent(input) {
2663
+ const workspaceId = input.workspaceId;
2664
+ this.scheduleWorkspaceNaming(() => this.maybeAutoNameDirectoryWorkspaceTitle({
2665
+ workspaceId,
2666
+ cwd: input.cwd,
2667
+ firstAgentContext: input.firstAgentContext,
2668
+ }), { cwd: input.cwd, message: "Failed to auto-name local workspace title" });
2510
2669
  }
2511
2670
  emitProviderDisabledResponse(kind, provider, requestId, fetchedAt) {
2512
2671
  const payload = {
@@ -2719,6 +2878,7 @@ export class Session {
2719
2878
  relayPublicEndpoint: relay?.publicEndpoint,
2720
2879
  relayUseTls: relay?.useTls,
2721
2880
  relayPublicUseTls: relay?.publicUseTls,
2881
+ appBaseUrl: this.daemonRuntimeConfig?.appBaseUrl,
2722
2882
  includeQr: true,
2723
2883
  logger: this.sessionLogger,
2724
2884
  });
@@ -2874,8 +3034,13 @@ export class Session {
2874
3034
  const prompt = await buildMetadataPrompt({
2875
3035
  cwd,
2876
3036
  workspaceGitService: this.workspaceGitService,
2877
- configKey: "commitMessage",
2878
- before: "Write a concise git commit message for the changes below.",
3037
+ contract: "Write a concise git commit message for the changes below.",
3038
+ styles: [
3039
+ {
3040
+ configKey: "commitMessage",
3041
+ default: "Concise, imperative mood, no trailing period.",
3042
+ },
3043
+ ],
2879
3044
  after: [
2880
3045
  "Return JSON only with a single field 'message'.",
2881
3046
  "",
@@ -2942,8 +3107,13 @@ export class Session {
2942
3107
  const prompt = await buildMetadataPrompt({
2943
3108
  cwd,
2944
3109
  workspaceGitService: this.workspaceGitService,
2945
- configKey: "pullRequest",
2946
- before: "Write a pull request title and body for the changes below.",
3110
+ contract: "Write a pull request title and body for the changes below.",
3111
+ styles: [
3112
+ {
3113
+ configKey: "pullRequest",
3114
+ default: "Clear, descriptive title; body explaining what changed and why.",
3115
+ },
3116
+ ],
2947
3117
  after: [
2948
3118
  "Return JSON only with fields 'title' and 'body'.",
2949
3119
  "",
@@ -3211,16 +3381,33 @@ export class Session {
3211
3381
  * Handle client heartbeat for activity tracking
3212
3382
  */
3213
3383
  handleClientHeartbeat(msg) {
3384
+ const focusedTerminalId = msg.focusedTerminalId?.trim() || null;
3214
3385
  const appVisibilityChangedAt = msg.appVisibilityChangedAt
3215
3386
  ? new Date(msg.appVisibilityChangedAt)
3216
3387
  : new Date(msg.lastActivityAt);
3217
3388
  this.clientActivity = {
3218
3389
  deviceType: msg.deviceType,
3219
3390
  focusedAgentId: msg.focusedAgentId,
3391
+ focusedTerminalId,
3220
3392
  lastActivityAt: new Date(msg.lastActivityAt),
3221
3393
  appVisible: msg.appVisible,
3222
3394
  appVisibilityChangedAt,
3223
3395
  };
3396
+ if (msg.appVisible && focusedTerminalId) {
3397
+ void this.clearFocusedTerminalAttention(focusedTerminalId);
3398
+ }
3399
+ }
3400
+ async clearFocusedTerminalAttention(terminalId) {
3401
+ const terminalManager = this.terminalManager;
3402
+ if (!terminalManager) {
3403
+ return;
3404
+ }
3405
+ try {
3406
+ await terminalManager.clearTerminalAttention(terminalId);
3407
+ }
3408
+ catch (error) {
3409
+ this.sessionLogger.warn({ err: error, terminalId }, "Failed to clear terminal attention");
3410
+ }
3224
3411
  }
3225
3412
  /**
3226
3413
  * Handle push token registration
@@ -3542,7 +3729,7 @@ export class Session {
3542
3729
  target.watchers.length = 0;
3543
3730
  }
3544
3731
  async removeWorkspaceGitWatchTarget(cwd) {
3545
- const normalizedCwd = normalizePersistedWorkspaceId(cwd);
3732
+ const normalizedCwd = resolve(cwd);
3546
3733
  const target = this.workspaceGitWatchTargets.get(normalizedCwd);
3547
3734
  if (target) {
3548
3735
  this.closeWorkspaceGitWatchTarget(target);
@@ -3550,7 +3737,7 @@ export class Session {
3550
3737
  }
3551
3738
  }
3552
3739
  removeWorkspaceGitSubscription(cwd) {
3553
- const normalizedCwd = normalizePersistedWorkspaceId(cwd);
3740
+ const normalizedCwd = resolve(cwd);
3554
3741
  const target = this.workspaceGitWatchTargets.get(normalizedCwd);
3555
3742
  if (target) {
3556
3743
  const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(normalizedCwd);
@@ -3571,8 +3758,16 @@ export class Session {
3571
3758
  workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
3572
3759
  ]);
3573
3760
  }
3761
+ resolveWorkspaceGitWatchTarget(workspaceId) {
3762
+ for (const target of this.workspaceGitWatchTargets.values()) {
3763
+ if (target.workspaceId === workspaceId) {
3764
+ return target;
3765
+ }
3766
+ }
3767
+ return null;
3768
+ }
3574
3769
  shouldSkipWorkspaceGitWatchUpdate(workspaceId, workspace) {
3575
- const target = this.workspaceGitWatchTargets.get(workspaceId);
3770
+ const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
3576
3771
  if (!target) {
3577
3772
  return false;
3578
3773
  }
@@ -3584,7 +3779,7 @@ export class Session {
3584
3779
  return false;
3585
3780
  }
3586
3781
  rememberWorkspaceGitDescriptorState(workspaceId, workspace) {
3587
- const target = this.workspaceGitWatchTargets.get(workspaceId);
3782
+ const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
3588
3783
  if (!target) {
3589
3784
  return;
3590
3785
  }
@@ -3592,7 +3787,7 @@ export class Session {
3592
3787
  target.lastBranchName = workspace?.name ?? null;
3593
3788
  }
3594
3789
  handleWorkspaceGitBranchSnapshot(cwd, branchName) {
3595
- const target = this.workspaceGitWatchTargets.get(normalizePersistedWorkspaceId(cwd));
3790
+ const target = this.workspaceGitWatchTargets.get(resolve(cwd));
3596
3791
  if (!target) {
3597
3792
  return;
3598
3793
  }
@@ -3613,7 +3808,7 @@ export class Session {
3613
3808
  }
3614
3809
  }
3615
3810
  syncWorkspaceGitObserver(cwd, options) {
3616
- const normalizedCwd = normalizePersistedWorkspaceId(cwd);
3811
+ const normalizedCwd = resolve(cwd);
3617
3812
  if (!options.isGit) {
3618
3813
  this.removeWorkspaceGitSubscription(normalizedCwd);
3619
3814
  return;
@@ -3634,7 +3829,9 @@ export class Session {
3634
3829
  this.workspaceGitWatchTargets.set(normalizedCwd, target);
3635
3830
  const subscription = this.workspaceGitService.registerWorkspace({ cwd: normalizedCwd }, (snapshot) => {
3636
3831
  this.handleWorkspaceGitBranchSnapshot(normalizedCwd, snapshot.git.currentBranch ?? null);
3637
- void this.emitWorkspaceUpdateForCwd(normalizedCwd);
3832
+ void this.emitWorkspaceUpdateForCwd(normalizedCwd).catch((error) => {
3833
+ this.sessionLogger.warn({ err: error, cwd: normalizedCwd }, "Failed to emit workspace update after git branch snapshot");
3834
+ });
3638
3835
  this.emitCheckoutStatusUpdate(normalizedCwd, snapshot);
3639
3836
  });
3640
3837
  this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
@@ -3739,10 +3936,14 @@ export class Session {
3739
3936
  return;
3740
3937
  }
3741
3938
  try {
3742
- const result = await renameCurrentBranch(cwd, branch);
3939
+ const result = await this.renameCurrentBranch(cwd, branch);
3743
3940
  await this.notifyGitMutation(cwd, "rename-branch", { invalidateGithub: true });
3744
3941
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3745
3942
  this.handleWorkspaceGitBranchSnapshot(cwd, result.currentBranch);
3943
+ // Branch is a git fact derived per-descriptor from each workspace's own
3944
+ // live git snapshot (id → cwd); the reconciliation pass re-persists the
3945
+ // `branch` field per workspace from its own cwd. No cwd → ids fan-out here.
3946
+ // TODO(K10): PR-binding on branch rename is deferred — see plan K10.
3746
3947
  // Push a workspace_update immediately so the sidebar/header reflect
3747
3948
  // the new branch name without waiting for the background git watcher.
3748
3949
  await this.emitWorkspaceUpdateForCwd(cwd);
@@ -4348,18 +4549,19 @@ export class Session {
4348
4549
  async handlePaseoWorktreeArchiveRequest(msg) {
4349
4550
  return handleWorktreeArchiveRequest({
4350
4551
  paseoHome: this.paseoHome,
4351
- worktreesRoot: this.worktreesRoot,
4552
+ paseoWorktreesBaseRoot: this.worktreesRoot,
4352
4553
  github: this.github,
4353
4554
  workspaceGitService: this.workspaceGitService,
4354
4555
  agentManager: this.agentManager,
4355
4556
  agentStorage: this.agentStorage,
4557
+ findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
4558
+ listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
4356
4559
  archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
4357
4560
  emit: (message) => this.emit(message),
4358
4561
  emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
4359
4562
  markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
4360
4563
  clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
4361
- isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
4362
- killTerminalsUnderPath: (rootPath) => this.terminalController.killTerminalsUnderPath(rootPath),
4564
+ killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
4363
4565
  sessionLogger: this.sessionLogger,
4364
4566
  }, msg);
4365
4567
  }
@@ -4569,10 +4771,30 @@ export class Session {
4569
4771
  });
4570
4772
  }
4571
4773
  }
4774
+ async listTerminalActivityContributions() {
4775
+ const terminalManager = this.terminalManager;
4776
+ if (!terminalManager) {
4777
+ return [];
4778
+ }
4779
+ const directories = terminalManager.listDirectories();
4780
+ const terminalsByDirectory = await Promise.all(directories.map((cwd) => terminalManager.getTerminals(cwd)));
4781
+ return terminalsByDirectory.flat().map((session) => {
4782
+ const contribution = {
4783
+ cwd: session.cwd,
4784
+ activity: session.getActivity(),
4785
+ };
4786
+ if (session.workspaceId) {
4787
+ contribution.workspaceId = session.workspaceId;
4788
+ }
4789
+ return contribution;
4790
+ });
4791
+ }
4572
4792
  /**
4573
4793
  * Build the current agent list payload (live + persisted), optionally filtered by labels.
4574
4794
  */
4575
4795
  async listAgentPayloads(filter) {
4796
+ const includeArchived = filter?.includeArchived === true;
4797
+ const labelEntries = filter?.labels ? Object.entries(filter.labels) : [];
4576
4798
  // Get live agents with session modes
4577
4799
  const agentSnapshots = this.agentManager.listAgents();
4578
4800
  const liveAgents = await Promise.all(agentSnapshots.map((agent) => this.buildAgentPayload(agent)));
@@ -4580,18 +4802,23 @@ export class Session {
4580
4802
  // (excluding internal agents which are for ephemeral system tasks)
4581
4803
  const registryRecords = await this.agentStorage.list();
4582
4804
  const liveIds = new Set(agentSnapshots.map((a) => a.id));
4583
- const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
4805
+ const registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds());
4584
4806
  const persistedAgents = registryRecords
4585
4807
  .filter((record) => !liveIds.has(record.id) && !record.internal)
4808
+ // Keep raw-record filters ahead of projection; seeded homes can carry thousands of archived agents.
4809
+ .filter((record) => includeArchived || !record.archivedAt)
4810
+ .filter((record) => labelEntries.every(([key, value]) => record.labels?.[key] === value))
4586
4811
  .filter((record) => filter?.includeUnavailablePersisted === true ||
4587
4812
  isStoredAgentProviderAvailable(record, registeredProviderIds))
4588
4813
  .map((record) => this.buildStoredAgentPayload(record, registeredProviderIds));
4589
4814
  let agents = [...liveAgents, ...persistedAgents];
4590
4815
  agents = agents.filter((agent) => this.isProviderVisibleToClient(agent.provider));
4816
+ if (!includeArchived) {
4817
+ agents = agents.filter((agent) => !agent.archivedAt);
4818
+ }
4591
4819
  // Filter by labels if filter provided
4592
- if (filter?.labels) {
4593
- const filterLabels = filter.labels;
4594
- agents = agents.filter((agent) => Object.entries(filterLabels).every(([key, _value]) => agent.labels[key] === filterLabels[key]));
4820
+ if (labelEntries.length > 0) {
4821
+ agents = agents.filter((agent) => labelEntries.every(([key, value]) => agent.labels[key] === value));
4595
4822
  }
4596
4823
  return agents;
4597
4824
  }
@@ -4653,7 +4880,7 @@ export class Session {
4653
4880
  const payload = this.buildStoredAgentPayload(record);
4654
4881
  return this.isProviderVisibleToClient(payload.provider) ? payload : null;
4655
4882
  }
4656
- async buildActiveProjectPlacementsByWorkspaceCwd() {
4883
+ async buildActiveProjectPlacementsByWorkspaceId() {
4657
4884
  const [persistedWorkspaces, persistedProjects] = await Promise.all([
4658
4885
  this.workspaceRegistry.list(),
4659
4886
  this.projectRegistry.list(),
@@ -4661,7 +4888,7 @@ export class Session {
4661
4888
  const activeProjects = new Map(persistedProjects
4662
4889
  .filter((project) => !project.archivedAt)
4663
4890
  .map((project) => [project.projectId, project]));
4664
- const placementsByCwd = new Map();
4891
+ const placementsByWorkspaceId = new Map();
4665
4892
  const pairs = persistedWorkspaces.flatMap((workspace) => {
4666
4893
  if (workspace.archivedAt)
4667
4894
  return [];
@@ -4672,9 +4899,9 @@ export class Session {
4672
4899
  });
4673
4900
  const placements = await Promise.all(pairs.map(({ workspace, project }) => this.buildProjectPlacementForWorkspace(workspace, project)));
4674
4901
  for (let i = 0; i < pairs.length; i += 1) {
4675
- placementsByCwd.set(normalizePersistedWorkspaceId(pairs[i].workspace.cwd), placements[i]);
4902
+ placementsByWorkspaceId.set(pairs[i].workspace.workspaceId, placements[i]);
4676
4903
  }
4677
- return placementsByCwd;
4904
+ return placementsByWorkspaceId;
4678
4905
  }
4679
4906
  async collectFetchAgentsEntries(params) {
4680
4907
  const { candidates, limit, getPlacement, filter } = params;
@@ -4683,7 +4910,7 @@ export class Session {
4683
4910
  for (let start = 0; start < candidates.length && matchedEntries.length <= limit; start += batchSize) {
4684
4911
  const batch = candidates.slice(start, start + batchSize);
4685
4912
  const batchEntries = await Promise.all(batch.map(async (agent) => {
4686
- const project = await getPlacement(agent.cwd);
4913
+ const project = await getPlacement(agent.workspaceId);
4687
4914
  return project ? { agent, project } : null;
4688
4915
  }));
4689
4916
  for (const entry of batchEntries) {
@@ -4714,23 +4941,29 @@ export class Session {
4714
4941
  const sort = this.agentsPager.normalizeSort(request.sort);
4715
4942
  let agents = await this.listAgentPayloads({
4716
4943
  labels: filter?.labels,
4944
+ includeArchived: filter?.includeArchived,
4717
4945
  includeUnavailablePersisted: request.type === "fetch_agent_history_request",
4718
4946
  });
4719
- const activePlacementsByCwd = scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceCwd() : null;
4720
- if (activePlacementsByCwd) {
4721
- agents = agents.filter((agent) => !agent.archivedAt && activePlacementsByCwd.has(normalizePersistedWorkspaceId(agent.cwd)));
4722
- }
4723
- const placementByCwd = new Map();
4724
- const getPlacement = (cwd) => {
4725
- if (activePlacementsByCwd) {
4726
- return Promise.resolve(activePlacementsByCwd.get(normalizePersistedWorkspaceId(cwd)) ?? null);
4947
+ const activePlacementsByWorkspaceId = scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceId() : null;
4948
+ if (activePlacementsByWorkspaceId) {
4949
+ agents = agents.filter((agent) => !agent.archivedAt &&
4950
+ agent.workspaceId != null &&
4951
+ activePlacementsByWorkspaceId.has(agent.workspaceId));
4952
+ }
4953
+ const placementByWorkspaceId = new Map();
4954
+ const getPlacement = (workspaceId) => {
4955
+ if (!workspaceId) {
4956
+ return Promise.resolve(null);
4727
4957
  }
4728
- const existing = placementByCwd.get(cwd);
4958
+ if (activePlacementsByWorkspaceId) {
4959
+ return Promise.resolve(activePlacementsByWorkspaceId.get(workspaceId) ?? null);
4960
+ }
4961
+ const existing = placementByWorkspaceId.get(workspaceId);
4729
4962
  if (existing) {
4730
4963
  return existing;
4731
4964
  }
4732
- const placementPromise = this.buildProjectPlacementForCwd(cwd);
4733
- placementByCwd.set(cwd, placementPromise);
4965
+ const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
4966
+ placementByWorkspaceId.set(workspaceId, placementPromise);
4734
4967
  return placementPromise;
4735
4968
  };
4736
4969
  let candidates = [...agents];
@@ -4790,7 +5023,8 @@ export class Session {
4790
5023
  workspaceDirectory: workspace.cwd,
4791
5024
  projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git",
4792
5025
  workspaceKind: workspace.kind,
4793
- name: workspace.displayName,
5026
+ name: resolveWorkspaceDisplayName(workspace),
5027
+ title: workspace.title,
4794
5028
  archivingAt: null,
4795
5029
  status: "done",
4796
5030
  statusEnteredAt: null,
@@ -4847,7 +5081,7 @@ export class Session {
4847
5081
  const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout });
4848
5082
  return {
4849
5083
  ...base,
4850
- name: displayName,
5084
+ name: resolveWorkspaceName({ title: workspace.title, derivedDisplayName: displayName }),
4851
5085
  diffStat: snapshot.git.diffStat ?? null,
4852
5086
  gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot) ?? undefined,
4853
5087
  githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot),
@@ -4866,10 +5100,14 @@ export class Session {
4866
5100
  workspaceDirectory: result.workspace.cwd,
4867
5101
  projectKind: "git",
4868
5102
  workspaceKind: result.workspace.kind,
4869
- name: result.worktree.branchName || result.workspace.displayName,
5103
+ name: resolveWorkspaceName({
5104
+ title: result.workspace.title,
5105
+ derivedDisplayName: result.worktree.branchName || result.workspace.displayName,
5106
+ }),
5107
+ title: result.workspace.title,
4870
5108
  archivingAt: null,
4871
5109
  status: "done",
4872
- statusEnteredAt: null,
5110
+ statusEnteredAt: result.workspace.createdAt,
4873
5111
  activityAt: null,
4874
5112
  diffStat: { additions: 0, deletions: 0 },
4875
5113
  scripts: [],
@@ -4900,8 +5138,13 @@ export class Session {
4900
5138
  async buildWorkspaceDescriptorMap(options) {
4901
5139
  return this.workspaceDirectory.buildDescriptorMap(options);
4902
5140
  }
4903
- resolveRegisteredWorkspaceIdForCwd(cwd, workspaces) {
4904
- return this.workspaceDirectory.resolveRegisteredWorkspaceIdForCwd(cwd, workspaces);
5141
+ // external path→workspace adapter, not ownership. Used by archive-by-path flows
5142
+ // where the request carries a worktree path (unique to one workspace) rather
5143
+ // than a workspaceId. This is a directory lookup for an archive target, not a
5144
+ // status/ownership attribution.
5145
+ async findWorkspaceIdForCwd(cwd) {
5146
+ const workspaces = await this.workspaceRegistry.list();
5147
+ return resolveWorkspaceIdForPath(cwd, workspaces);
4905
5148
  }
4906
5149
  matchesWorkspaceFilter(input) {
4907
5150
  return this.workspaceDirectory.matchesFilter(input);
@@ -4969,7 +5212,7 @@ export class Session {
4969
5212
  }
4970
5213
  }
4971
5214
  async findOrCreateWorkspaceForDirectory(cwd) {
4972
- const inputCwd = normalizePersistedWorkspaceId(cwd);
5215
+ const inputCwd = resolve(cwd);
4973
5216
  const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
4974
5217
  const existingWorkspace = await this.findExactWorkspaceByDirectory(normalizedCwd, {
4975
5218
  refreshGit: false,
@@ -4977,22 +5220,26 @@ export class Session {
4977
5220
  if (existingWorkspace) {
4978
5221
  if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) {
4979
5222
  const timestamp = new Date().toISOString();
4980
- const displayName = basename(inputCwd) || inputCwd;
4981
- const projectRecord = createPersistedProjectRecord({
4982
- projectId: inputCwd,
4983
- rootPath: inputCwd,
4984
- kind: "non_git",
4985
- displayName,
4986
- createdAt: timestamp,
4987
- updatedAt: timestamp,
5223
+ const checkout = checkoutLiteFromGitSnapshot(inputCwd, {
5224
+ isGit: false,
5225
+ currentBranch: null,
5226
+ remoteUrl: null,
5227
+ repoRoot: null,
5228
+ isPaseoOwnedWorktree: false,
5229
+ mainRepoRoot: null,
5230
+ });
5231
+ const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout });
5232
+ const projectRecord = await this.resolveProjectRecordForPlacement({
5233
+ membership,
5234
+ timestamp,
4988
5235
  });
4989
5236
  await this.projectRegistry.upsert(projectRecord);
4990
5237
  const workspaceRecord = createPersistedWorkspaceRecord({
4991
- workspaceId: inputCwd,
5238
+ workspaceId: generateWorkspaceId(),
4992
5239
  projectId: projectRecord.projectId,
4993
5240
  cwd: inputCwd,
4994
- kind: "directory",
4995
- displayName,
5241
+ kind: membership.workspaceKind,
5242
+ displayName: membership.workspaceDisplayName,
4996
5243
  createdAt: timestamp,
4997
5244
  updatedAt: timestamp,
4998
5245
  });
@@ -5007,7 +5254,16 @@ export class Session {
5007
5254
  }
5008
5255
  return this.createWorkspaceForDirectory(normalizedCwd);
5009
5256
  }
5010
- async createWorkspaceForDirectory(cwd) {
5257
+ async resolveOrCreateWorkspaceIdForCreateAgent(input) {
5258
+ if (input.createdWorktree) {
5259
+ return input.createdWorktree.workspace.workspaceId;
5260
+ }
5261
+ if (input.requestedWorkspaceId) {
5262
+ return input.requestedWorkspaceId;
5263
+ }
5264
+ return (await this.createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId;
5265
+ }
5266
+ async createWorkspaceForDirectory(cwd, title) {
5011
5267
  const checkout = await this.workspaceGitService.getCheckout(cwd);
5012
5268
  const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
5013
5269
  const timestamp = new Date().toISOString();
@@ -5017,11 +5273,12 @@ export class Session {
5017
5273
  });
5018
5274
  await this.projectRegistry.upsert(projectRecord);
5019
5275
  const workspaceRecord = createPersistedWorkspaceRecord({
5020
- workspaceId: membership.workspaceId,
5276
+ workspaceId: generateWorkspaceId(),
5021
5277
  projectId: projectRecord.projectId,
5022
5278
  cwd,
5023
5279
  kind: membership.workspaceKind,
5024
5280
  displayName: membership.workspaceDisplayName,
5281
+ title: title ?? null,
5025
5282
  createdAt: timestamp,
5026
5283
  updatedAt: timestamp,
5027
5284
  });
@@ -5039,8 +5296,7 @@ export class Session {
5039
5296
  const projectId = projectRecord.projectId;
5040
5297
  const kind = membership.workspaceKind;
5041
5298
  const displayName = membership.workspaceDisplayName;
5042
- if (input.workspace.workspaceId === membership.workspaceId &&
5043
- input.workspace.projectId === projectId &&
5299
+ if (input.workspace.projectId === projectId &&
5044
5300
  input.workspace.kind === kind &&
5045
5301
  input.workspace.displayName === displayName) {
5046
5302
  return this.ensureWorkspaceRecordUnarchived(input.workspace);
@@ -5048,7 +5304,7 @@ export class Session {
5048
5304
  await this.projectRegistry.upsert(projectRecord);
5049
5305
  const nextWorkspace = {
5050
5306
  ...input.workspace,
5051
- workspaceId: membership.workspaceId,
5307
+ workspaceId: input.workspace.workspaceId,
5052
5308
  projectId,
5053
5309
  cwd: input.cwd,
5054
5310
  kind,
@@ -5122,16 +5378,28 @@ export class Session {
5122
5378
  });
5123
5379
  return result;
5124
5380
  }
5381
+ async listActiveWorkspaceRefs() {
5382
+ const workspaces = await this.workspaceRegistry.list();
5383
+ return workspaces
5384
+ .filter((workspace) => !workspace.archivedAt)
5385
+ .map((workspace) => ({
5386
+ workspaceId: workspace.workspaceId,
5387
+ cwd: workspace.cwd,
5388
+ kind: workspace.kind,
5389
+ }));
5390
+ }
5125
5391
  async archiveWorkspaceRecord(workspaceId, archivedAt) {
5126
5392
  const archiveTimestamp = archivedAt ?? new Date().toISOString();
5127
5393
  const existingWorkspace = await archivePersistedWorkspaceRecord({
5128
5394
  workspaceId,
5129
5395
  archivedAt: archiveTimestamp,
5130
5396
  workspaceRegistry: this.workspaceRegistry,
5131
- projectRegistry: this.projectRegistry,
5132
5397
  });
5133
5398
  if (!existingWorkspace) {
5134
- this.removeWorkspaceGitSubscription(workspaceId);
5399
+ const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
5400
+ if (watchTarget) {
5401
+ this.removeWorkspaceGitSubscription(watchTarget.cwd);
5402
+ }
5135
5403
  return;
5136
5404
  }
5137
5405
  if (!existingWorkspace.archivedAt) {
@@ -5144,9 +5412,18 @@ export class Session {
5144
5412
  archivedAt: archiveTimestamp,
5145
5413
  }, "Workspace archived");
5146
5414
  }
5147
- await this.removeWorkspaceGitWatchTarget(existingWorkspace.cwd);
5148
- this.scriptRuntimeStore?.removeForWorkspace(existingWorkspace.cwd);
5149
- this.removeWorkspaceGitSubscription(workspaceId);
5415
+ await this.teardownArchivedWorkspace({
5416
+ workspaceId: existingWorkspace.workspaceId,
5417
+ cwd: existingWorkspace.cwd,
5418
+ });
5419
+ }
5420
+ // Git watch and subscription state is keyed by directory; the script runtime
5421
+ // store is keyed by the opaque workspace id. Each cleanup uses its own key so an
5422
+ // opaque id is never resolved as a filesystem path.
5423
+ async teardownArchivedWorkspace(input) {
5424
+ await this.removeWorkspaceGitWatchTarget(input.cwd);
5425
+ this.scriptRuntimeStore?.removeForWorkspace(input.workspaceId);
5426
+ this.removeWorkspaceGitSubscription(input.cwd);
5150
5427
  }
5151
5428
  async reconcileAndEmitWorkspaceUpdates() {
5152
5429
  if (!this.workspaceUpdatesSubscription) {
@@ -5178,9 +5455,10 @@ export class Session {
5178
5455
  await Promise.all(result.changesApplied.map(async (change) => {
5179
5456
  switch (change.kind) {
5180
5457
  case "workspace_archived":
5181
- await this.removeWorkspaceGitWatchTarget(change.directory);
5182
- this.scriptRuntimeStore?.removeForWorkspace(change.directory);
5183
- this.removeWorkspaceGitSubscription(change.workspaceId);
5458
+ await this.teardownArchivedWorkspace({
5459
+ workspaceId: change.workspaceId,
5460
+ cwd: change.directory,
5461
+ });
5184
5462
  changedWorkspaceIds.add(change.workspaceId);
5185
5463
  break;
5186
5464
  case "workspace_updated":
@@ -5223,7 +5501,7 @@ export class Session {
5223
5501
  this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)) {
5224
5502
  continue;
5225
5503
  }
5226
- const watchTarget = this.workspaceGitWatchTargets.get(workspaceId);
5504
+ const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
5227
5505
  if (watchTarget && this.onBranchChanged) {
5228
5506
  const newBranchName = nextWorkspace?.name ?? null;
5229
5507
  if (newBranchName !== watchTarget.lastBranchName) {
@@ -5236,6 +5514,7 @@ export class Session {
5236
5514
  this.bufferOrEmitWorkspaceUpdate(subscription, {
5237
5515
  kind: "remove",
5238
5516
  id: workspaceId,
5517
+ ...(await this.resolveEmptyProjectForArchivedWorkspace(workspaceId)),
5239
5518
  });
5240
5519
  continue;
5241
5520
  }
@@ -5255,10 +5534,38 @@ export class Session {
5255
5534
  void this.reconcileAndEmitWorkspaceUpdates();
5256
5535
  }
5257
5536
  }
5537
+ // When a workspace is archived its project may become empty. Resolve the
5538
+ // now-empty project parent so the `remove` update can carry it, keeping the
5539
+ // sidebar's empty project row in sync without a full re-hydration.
5540
+ async resolveEmptyProjectForArchivedWorkspace(workspaceId) {
5541
+ const archivedWorkspace = await this.workspaceRegistry.get(workspaceId);
5542
+ if (!archivedWorkspace) {
5543
+ return null;
5544
+ }
5545
+ const emptyProject = (await this.workspaceDirectory.listEmptyProjects()).find((project) => project.projectId === archivedWorkspace.projectId);
5546
+ return emptyProject ? { emptyProject } : null;
5547
+ }
5548
+ async emitWorkspaceUpdateForTerminalContribution(event) {
5549
+ // A terminal's activity contributes only to the workspace it carries. A
5550
+ // terminal with no workspaceId attributes to nothing — status is per-id.
5551
+ if (!event.workspaceId) {
5552
+ return;
5553
+ }
5554
+ await this.emitWorkspaceUpdatesForWorkspaceIds([event.workspaceId], {
5555
+ skipReconcile: true,
5556
+ });
5557
+ }
5558
+ // A git fact (branch, diff, dirty, PR) changed at `cwd`. Every workspace whose
5559
+ // OWN cwd is this folder re-derives its git facts from that folder (id → cwd)
5560
+ // and emits its own per-id descriptor. This is a deliberate same-folder fan,
5561
+ // not a cwd → id ownership lookup: git never resolves which workspace owns a
5562
+ // path. See `workspaceIdsOnCheckout`.
5258
5563
  async emitWorkspaceUpdateForCwd(cwd, options) {
5259
- const workspaces = await this.workspaceRegistry.list();
5260
- const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(cwd, workspaces);
5261
- await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], options);
5564
+ const workspaceIds = workspaceIdsOnCheckout(await this.workspaceRegistry.list(), cwd);
5565
+ if (workspaceIds.length === 0) {
5566
+ return;
5567
+ }
5568
+ await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options);
5262
5569
  }
5263
5570
  async handleFetchAgents(request) {
5264
5571
  const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
@@ -5450,17 +5757,165 @@ export class Session {
5450
5757
  }
5451
5758
  return { snapshotByWorkspaceId };
5452
5759
  }
5453
- async registerWorkspaceForImportedAgent(cwd) {
5760
+ async registerWorkspaceForImportedAgent(workspace) {
5454
5761
  try {
5455
- const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
5456
5762
  await this.syncWorkspaceGitObserverForWorkspace(workspace);
5457
5763
  await this.describeWorkspaceRecord(workspace);
5458
- await this.emitWorkspaceUpdateForCwd(workspace.cwd);
5764
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
5765
+ }
5766
+ catch (error) {
5767
+ this.sessionLogger.warn({ err: error, workspaceId: workspace.workspaceId, cwd: workspace.cwd }, "Failed to register workspace for imported agent");
5768
+ }
5769
+ }
5770
+ async handleWorkspaceCreateRequest(request) {
5771
+ try {
5772
+ if (request.source.kind === "directory") {
5773
+ await this.handleWorkspaceCreateLocal(request);
5774
+ return;
5775
+ }
5776
+ await this.handleWorkspaceCreateWorktree(request);
5459
5777
  }
5460
5778
  catch (error) {
5461
- this.sessionLogger.warn({ err: error, cwd }, "Failed to register workspace for imported agent");
5779
+ const message = error instanceof Error ? error.message : "Failed to create workspace";
5780
+ this.sessionLogger.error({ err: error, sourceKind: request.source.kind, requestId: request.requestId }, "Failed to create workspace");
5781
+ this.emit({
5782
+ type: "workspace.create.response",
5783
+ payload: {
5784
+ requestId: request.requestId,
5785
+ workspace: null,
5786
+ setupTerminalId: null,
5787
+ error: message,
5788
+ },
5789
+ });
5462
5790
  }
5463
5791
  }
5792
+ async handleWorkspaceCreateLocal(request) {
5793
+ if (request.source.kind !== "directory") {
5794
+ return;
5795
+ }
5796
+ const cwd = expandTilde(request.source.path);
5797
+ const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
5798
+ if (!directoryExists) {
5799
+ this.emit({
5800
+ type: "workspace.create.response",
5801
+ payload: {
5802
+ requestId: request.requestId,
5803
+ workspace: null,
5804
+ setupTerminalId: null,
5805
+ error: `Directory not found: ${cwd}`,
5806
+ errorCode: "directory_not_found",
5807
+ },
5808
+ });
5809
+ return;
5810
+ }
5811
+ const explicitTitle = request.title?.trim() || null;
5812
+ const promptTitle = resolveFirstAgentPromptTitle(request.firstAgentContext);
5813
+ const workspace = await createLocalCheckoutWorkspace({ cwd, title: explicitTitle ?? promptTitle }, {
5814
+ projectRegistry: this.projectRegistry,
5815
+ workspaceRegistry: this.workspaceRegistry,
5816
+ workspaceGitService: this.workspaceGitService,
5817
+ });
5818
+ await this.syncWorkspaceGitObserverForWorkspace(workspace);
5819
+ const descriptor = await this.describeWorkspaceRecord(workspace);
5820
+ this.emit({
5821
+ type: "workspace.create.response",
5822
+ payload: {
5823
+ requestId: request.requestId,
5824
+ workspace: descriptor,
5825
+ setupTerminalId: null,
5826
+ error: null,
5827
+ },
5828
+ });
5829
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
5830
+ void this.workspaceGitService
5831
+ .getSnapshot(workspace.cwd, { force: true, includeGitHub: true, reason: "open_project" })
5832
+ .catch((error) => {
5833
+ this.sessionLogger.warn({ err: error, cwd: workspace.cwd }, "Background snapshot refresh failed after workspace.create");
5834
+ });
5835
+ if (request.firstAgentContext) {
5836
+ const firstAgentContext = request.firstAgentContext;
5837
+ this.scheduleWorkspaceNaming(() => this.maybeAutoNameDirectoryWorkspaceTitle({
5838
+ workspaceId: workspace.workspaceId,
5839
+ cwd: workspace.cwd,
5840
+ firstAgentContext,
5841
+ }), { cwd: workspace.cwd, message: "Failed to auto-name directory workspace title" });
5842
+ }
5843
+ }
5844
+ // Schedules a background workspace-naming write off the request path. The
5845
+ // setTimeout(0) keeps the LLM call off the hot path.
5846
+ scheduleWorkspaceNaming(run, context) {
5847
+ setTimeout(() => {
5848
+ void run().catch((error) => {
5849
+ this.sessionLogger.warn({ err: error, cwd: context.cwd }, context.message);
5850
+ });
5851
+ }, 0);
5852
+ }
5853
+ async handleWorkspaceCreateWorktree(request) {
5854
+ if (request.source.kind !== "worktree") {
5855
+ return;
5856
+ }
5857
+ const source = request.source;
5858
+ if (!source.cwd && !source.projectId) {
5859
+ this.emit({
5860
+ type: "workspace.create.response",
5861
+ payload: {
5862
+ requestId: request.requestId,
5863
+ workspace: null,
5864
+ setupTerminalId: null,
5865
+ error: "cwd or projectId is required for a worktree-backed workspace",
5866
+ errorCode: "source_required",
5867
+ },
5868
+ });
5869
+ return;
5870
+ }
5871
+ const sourceCwd = await this.resolveWorktreeSourceCwd({
5872
+ cwd: source.cwd,
5873
+ projectId: source.projectId,
5874
+ });
5875
+ const result = await this.createPaseoWorktreeWorkflow({
5876
+ cwd: sourceCwd,
5877
+ projectId: source.projectId,
5878
+ worktreeSlug: source.worktreeSlug,
5879
+ action: source.action,
5880
+ refName: source.refName,
5881
+ githubPrNumber: source.githubPrNumber,
5882
+ firstAgentContext: request.firstAgentContext,
5883
+ }, source.baseBranch
5884
+ ? { resolveDefaultBranch: async () => source.baseBranch }
5885
+ : undefined);
5886
+ if (request.title?.trim()) {
5887
+ await this.workspaceRegistry.upsert({
5888
+ ...result.workspace,
5889
+ title: request.title.trim(),
5890
+ updatedAt: new Date().toISOString(),
5891
+ });
5892
+ result.workspace.title = request.title.trim();
5893
+ }
5894
+ const descriptor = await this.describeCreatedWorktreeWorkspace(result);
5895
+ this.emit({
5896
+ type: "workspace.create.response",
5897
+ payload: {
5898
+ requestId: request.requestId,
5899
+ workspace: descriptor,
5900
+ setupTerminalId: null,
5901
+ error: null,
5902
+ },
5903
+ });
5904
+ this.emit({
5905
+ type: "workspace_update",
5906
+ payload: { kind: "upsert", workspace: descriptor },
5907
+ });
5908
+ }
5909
+ async resolveWorktreeSourceCwd(input) {
5910
+ if (input.cwd) {
5911
+ return expandTilde(input.cwd);
5912
+ }
5913
+ const project = await this.projectRegistry.get(input.projectId);
5914
+ if (!project || project.archivedAt) {
5915
+ throw new Error(`Project not found: ${input.projectId}`);
5916
+ }
5917
+ return project.rootPath;
5918
+ }
5464
5919
  async handleOpenProjectRequest(request) {
5465
5920
  const requestedCwd = request.cwd;
5466
5921
  const cwd = expandTilde(requestedCwd);
@@ -5491,7 +5946,7 @@ export class Session {
5491
5946
  const project = await this.projectRegistry.get(workspace.projectId);
5492
5947
  await this.syncWorkspaceGitObserverForWorkspace(workspace);
5493
5948
  const descriptor = await this.describeWorkspaceRecord(workspace);
5494
- await this.emitWorkspaceUpdateForCwd(workspace.cwd);
5949
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
5495
5950
  this.sessionLogger.info({
5496
5951
  requestedCwd,
5497
5952
  resolvedCwd: cwd,
@@ -5664,7 +6119,7 @@ export class Session {
5664
6119
  createPaseoWorktree: (workflowInput, serviceOptions) => this.createPaseoWorktree(workflowInput, serviceOptions),
5665
6120
  warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
5666
6121
  autoNameWorkspaceBranchForFirstAgent: (autoNameInput) => this.scheduleAutoNameWorkspaceBranchForFirstAgent(autoNameInput),
5667
- emitWorkspaceUpdateForCwd: (cwd, emitOptions) => this.emitWorkspaceUpdateForCwd(cwd, emitOptions),
6122
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
5668
6123
  cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
5669
6124
  this.workspaceSetupSnapshots.set(workspaceId, snapshot);
5670
6125
  },
@@ -5694,12 +6149,33 @@ export class Session {
5694
6149
  if (!existing) {
5695
6150
  throw new Error(`Workspace not found: ${request.workspaceId}`);
5696
6151
  }
5697
- if (existing.kind === "worktree") {
5698
- throw new Error("Use worktree archive for Paseo worktrees");
5699
- }
5700
- const archivedAt = new Date().toISOString();
5701
- await this.archiveWorkspaceRecord(existing.workspaceId, archivedAt);
5702
- await this.emitWorkspaceUpdateForCwd(existing.cwd);
6152
+ const gitSnapshot = await this.workspaceGitService
6153
+ .getSnapshot(existing.cwd)
6154
+ .catch(() => null);
6155
+ const repoRoot = gitSnapshot?.git?.repoRoot ?? null;
6156
+ await archiveByScope({
6157
+ paseoHome: this.paseoHome,
6158
+ paseoWorktreesBaseRoot: this.worktreesRoot,
6159
+ github: this.github,
6160
+ workspaceGitService: this.workspaceGitService,
6161
+ agentManager: this.agentManager,
6162
+ agentStorage: this.agentStorage,
6163
+ findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
6164
+ listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
6165
+ archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
6166
+ emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
6167
+ markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
6168
+ clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
6169
+ killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
6170
+ sessionLogger: this.sessionLogger,
6171
+ }, {
6172
+ scope: { kind: "workspace", workspaceId: existing.workspaceId },
6173
+ repoRoot,
6174
+ paseoWorktreesBaseRoot: this.worktreesRoot,
6175
+ requestId: request.requestId,
6176
+ });
6177
+ const archivedWorkspace = await this.workspaceRegistry.get(request.workspaceId);
6178
+ const archivedAt = archivedWorkspace?.archivedAt ?? new Date().toISOString();
5703
6179
  this.emit({
5704
6180
  type: "archive_workspace_response",
5705
6181
  payload: {
@@ -5760,10 +6236,12 @@ export class Session {
5760
6236
  if (!workspace || workspace.archivedAt) {
5761
6237
  throw new Error(`Workspace not found: ${requestedWorkspaceId}`);
5762
6238
  }
5763
- const workspaceCwd = normalizePersistedWorkspaceId(workspace.cwd);
6239
+ // Clearing attention is scoped to the workspace that OWNS the agent, by
6240
+ // workspaceId — never by comparing cwd strings. A sibling workspace
6241
+ // sharing the same directory keeps its own agents' attention.
5764
6242
  const clearableAgentIds = agents
5765
6243
  .filter((agent) => !agent.archivedAt)
5766
- .filter((agent) => normalizePersistedWorkspaceId(agent.cwd) === workspaceCwd)
6244
+ .filter((agent) => agent.workspaceId === workspace.workspaceId)
5767
6245
  .filter((agent) => agent.requiresAttention === true)
5768
6246
  .filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0)
5769
6247
  .filter((agent) => agent.attentionReason !== "permission")
@@ -5791,7 +6269,7 @@ export class Session {
5791
6269
  };
5792
6270
  await this.agentStorage.upsert(nextRecord);
5793
6271
  const agent = this.buildStoredAgentPayload(nextRecord);
5794
- const project = await this.buildProjectPlacementForCwd(agent.cwd);
6272
+ const project = await this.buildProjectPlacementForWorkspace(workspace);
5795
6273
  this.emit({
5796
6274
  type: "agent_update",
5797
6275
  payload: {
@@ -5862,7 +6340,9 @@ export class Session {
5862
6340
  });
5863
6341
  return;
5864
6342
  }
5865
- const project = await this.buildProjectPlacementForCwd(agent.cwd);
6343
+ const project = agent.workspaceId
6344
+ ? await this.buildProjectPlacementForWorkspaceId(agent.workspaceId)
6345
+ : null;
5866
6346
  this.emit({
5867
6347
  type: "fetch_agent_response",
5868
6348
  payload: { requestId, agent, project, error: null },
@@ -6651,10 +7131,15 @@ export class Session {
6651
7131
  * Emit a message to the client
6652
7132
  */
6653
7133
  emit(msg) {
6654
- this.sessionLogger.trace({
6655
- messageType: msg.type,
6656
- payloadBytes: JSON.stringify(msg).length,
6657
- }, "agent.session.outbound");
7134
+ // JSON.stringify(msg) is only computed when trace is enabled — it runs for
7135
+ // every outbound message otherwise, and trace is disabled by default.
7136
+ // Optional-chained because test logger stubs don't implement isLevelEnabled.
7137
+ if (this.sessionLogger.isLevelEnabled?.("trace")) {
7138
+ this.sessionLogger.trace({
7139
+ messageType: msg.type,
7140
+ payloadBytes: JSON.stringify(msg).length,
7141
+ }, "agent.session.outbound");
7142
+ }
6658
7143
  if (msg.type === "audio_output" &&
6659
7144
  (process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
6660
7145
  msg.payload.groupId &&
@@ -6714,6 +7199,10 @@ export class Session {
6714
7199
  this.unsubscribeAgentEvents();
6715
7200
  this.unsubscribeAgentEvents = null;
6716
7201
  }
7202
+ if (this.unsubscribeTerminalWorkspaceContributionEvents) {
7203
+ this.unsubscribeTerminalWorkspaceContributionEvents();
7204
+ this.unsubscribeTerminalWorkspaceContributionEvents = null;
7205
+ }
6717
7206
  if (this.unsubscribeProviderSnapshotEvents) {
6718
7207
  this.unsubscribeProviderSnapshotEvents();
6719
7208
  this.unsubscribeProviderSnapshotEvents = null;