@getpaseo/server 0.1.99 → 0.1.101

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 (83) hide show
  1. package/dist/server/executable-resolution/windows.js +3 -0
  2. package/dist/server/server/agent/agent-manager.d.ts +10 -0
  3. package/dist/server/server/agent/agent-manager.js +65 -27
  4. package/dist/server/server/agent/agent-sdk-types.d.ts +8 -0
  5. package/dist/server/server/agent/mcp-server.d.ts +2 -45
  6. package/dist/server/server/agent/mcp-server.js +45 -1985
  7. package/dist/server/server/agent/prompt-attachments.js +6 -2
  8. package/dist/server/server/agent/provider-registry.js +1 -0
  9. package/dist/server/server/agent/provider-snapshot-manager.d.ts +4 -0
  10. package/dist/server/server/agent/provider-snapshot-manager.js +58 -13
  11. package/dist/server/server/agent/providers/acp-agent.d.ts +39 -2
  12. package/dist/server/server/agent/providers/acp-agent.js +281 -20
  13. package/dist/server/server/agent/providers/claude/agent.js +96 -62
  14. package/dist/server/server/agent/providers/codex-app-server-agent.js +6 -57
  15. package/dist/server/server/agent/providers/copilot-acp-agent.d.ts +2 -1
  16. package/dist/server/server/agent/providers/copilot-acp-agent.js +10 -0
  17. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +1 -0
  18. package/dist/server/server/agent/providers/diagnostic-utils.js +1 -1
  19. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +3 -0
  20. package/dist/server/server/agent/providers/generic-acp-agent.js +41 -23
  21. package/dist/server/server/agent/providers/mock-load-test-agent.js +4 -2
  22. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +14 -11
  23. package/dist/server/server/agent/providers/opencode/server-manager.js +149 -91
  24. package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +6 -5
  25. package/dist/server/server/agent/providers/opencode/test-server-manager.js +13 -3
  26. package/dist/server/server/agent/providers/opencode/test-utils/{test-opencode-runtime.d.ts → test-opencode-harness.d.ts} +11 -11
  27. package/dist/server/server/agent/providers/opencode/test-utils/{test-opencode-runtime.js → test-opencode-harness.js} +23 -10
  28. package/dist/server/server/agent/providers/opencode-agent.d.ts +9 -3
  29. package/dist/server/server/agent/providers/opencode-agent.js +26 -38
  30. package/dist/server/server/agent/providers/pi/agent.d.ts +4 -2
  31. package/dist/server/server/agent/providers/pi/agent.js +8 -3
  32. package/dist/server/server/agent/providers/pi/cli-runtime.d.ts +3 -0
  33. package/dist/server/server/agent/providers/pi/cli-runtime.js +6 -3
  34. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +2 -1
  35. package/dist/server/server/agent/providers/provider-image-output.d.ts +5 -0
  36. package/dist/server/server/agent/providers/provider-image-output.js +55 -0
  37. package/dist/server/server/agent/tools/paseo-tools.d.ts +48 -0
  38. package/dist/server/server/agent/tools/paseo-tools.js +2121 -0
  39. package/dist/server/server/agent/tools/types.d.ts +36 -0
  40. package/dist/server/server/agent/tools/types.js +2 -0
  41. package/dist/server/server/bootstrap.js +71 -62
  42. package/dist/server/server/persisted-config.d.ts +5 -0
  43. package/dist/server/server/persisted-config.js +10 -2
  44. package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +59 -0
  45. package/dist/server/server/session/agent-updates/agent-updates-service.js +220 -0
  46. package/dist/server/server/session/checkout/checkout-session.d.ts +13 -15
  47. package/dist/server/server/session/checkout/checkout-session.js +18 -16
  48. package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
  49. package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
  50. package/dist/server/server/session/daemon/daemon-session.d.ts +14 -0
  51. package/dist/server/server/session/daemon/daemon-session.js +38 -0
  52. package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
  53. package/dist/server/server/session/daemon/diagnostics.js +421 -0
  54. package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
  55. package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
  56. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
  57. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
  58. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +34 -0
  59. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +190 -0
  60. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
  61. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
  62. package/dist/server/server/session.d.ts +7 -51
  63. package/dist/server/server/session.js +113 -938
  64. package/dist/server/server/speech/providers/openai/config.d.ts +1 -2
  65. package/dist/server/server/speech/providers/openai/config.js +13 -9
  66. package/dist/server/server/speech/providers/openai/runtime.js +2 -16
  67. package/dist/server/server/speech/providers/openai/stt.d.ts +1 -0
  68. package/dist/server/server/speech/providers/openai/stt.js +4 -2
  69. package/dist/server/server/speech/providers/openai/tts.d.ts +1 -0
  70. package/dist/server/server/speech/providers/openai/tts.js +1 -0
  71. package/dist/server/server/websocket/runtime-metrics.d.ts +20 -0
  72. package/dist/server/server/websocket-server.d.ts +1 -2
  73. package/dist/server/server/websocket-server.js +26 -21
  74. package/dist/server/server/worktree-bootstrap.d.ts +1 -1
  75. package/dist/server/server/worktree-branch-name-generator.js +3 -1
  76. package/dist/server/utils/checkout-git.js +51 -26
  77. package/dist/src/executable-resolution/windows.js +3 -0
  78. package/dist/src/server/persisted-config.js +10 -2
  79. package/package.json +5 -5
  80. package/dist/server/server/agent/providers/opencode/runtime.d.ts +0 -28
  81. package/dist/server/server/agent/providers/opencode/runtime.js +0 -5
  82. package/dist/server/server/speech/providers/openai/realtime-transcription-session.d.ts +0 -42
  83. package/dist/server/server/speech/providers/openai/realtime-transcription-session.js +0 -168
@@ -3,7 +3,6 @@ import { v4 as uuidv4 } from "uuid";
3
3
  import { stat } from "node:fs/promises";
4
4
  import { basename, normalize, resolve, sep } from "path";
5
5
  import { homedir } from "node:os";
6
- import { z } from "zod";
7
6
  import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
8
7
  import { serializeAgentStreamEvent, } from "./messages.js";
9
8
  import { TerminalSessionController } from "../terminal/terminal-session-controller.js";
@@ -14,55 +13,52 @@ import { ensureAgentLoaded } from "./agent/agent-loading.js";
14
13
  import { formatSystemNotificationPrompt, sendPromptToAgent, waitForAgentRunStartWithTimeout, unarchiveAgentState, } from "./agent/agent-prompt.js";
15
14
  import { resolveCreateAgentTitles, resolveFirstAgentPromptTitle, } from "./agent/create-agent-title.js";
16
15
  import { respondToAgentPermission } from "./agent/permission-response.js";
17
- import { experimental_createMCPClient } from "ai";
18
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
19
- import { buildWorkspaceScriptPayloads, readPaseoConfigForProjection, } from "./script-status-projection.js";
20
- import { deriveProjectSlug } from "./workspace-git-metadata.js";
21
16
  import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
17
+ import { createWorkspaceScriptsService, } from "./session/workspace-scripts/workspace-scripts-service.js";
22
18
  import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
23
19
  import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
24
20
  import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels";
25
21
  import { createAgentCommand } from "./agent/create-agent/create.js";
26
22
  import { archiveAgentCommand, cancelAgentRunCommand, closeAgentCommand, detachAgentCommand, setAgentModeCommand, updateAgentCommand, } from "./agent/lifecycle-command.js";
27
- import { buildStoredAgentPayload, resolveEffectiveThinkingOptionId, resolveStoredAgentPayloadUpdatedAt, toAgentPayload, } from "./agent/agent-projections.js";
23
+ import { buildStoredAgentPayload, resolveStoredAgentPayloadUpdatedAt, toAgentPayload, } from "./agent/agent-projections.js";
28
24
  import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
29
25
  import { projectTimelineRows, selectProjectedTimelinePage, } from "./agent/timeline-projection.js";
30
- import { StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from "./agent/agent-response-loop.js";
31
- import { resolveStructuredGenerationProviders, } from "./agent/structured-generation-providers.js";
32
26
  import { getAgentStreamEventTurnId, } from "./agent/agent-sdk-types.js";
33
27
  import { ImportSessionsRequestError, importProviderSession, listImportableProviderSessions, normalizeImportAgentRequest, } from "./agent/import-sessions.js";
34
- import { checkoutLiteFromGitSnapshot, classifyDirectoryForProjectMembership, deriveWorkspaceDisplayName, generateWorkspaceId, } from "./workspace-registry-model.js";
28
+ import { checkoutLiteFromGitSnapshot, deriveWorkspaceDisplayName, } from "./workspace-registry-model.js";
35
29
  import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
36
- import { createPersistedProjectRecord, createPersistedWorkspaceRecord, resolveProjectDisplayName, resolveWorkspaceDisplayName, resolveWorkspaceName, } from "./workspace-registry.js";
30
+ import { resolveProjectDisplayName, resolveWorkspaceDisplayName, resolveWorkspaceName, } from "./workspace-registry.js";
37
31
  import { wrapSpokenInput } from "./voice-config.js";
38
32
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
39
33
  import { VoiceSession } from "./session/voice/voice-session.js";
40
34
  import { CheckoutSession } from "./session/checkout/checkout-session.js";
35
+ import { createWorkspaceGitObserverService, } from "./session/workspace-git-observer/workspace-git-observer-service.js";
36
+ import { createAgentStructuredTextGeneration, createGitMetadataGenerator, } from "./session/checkout/git-metadata-generator.js";
41
37
  import { ChatScheduleLoopSession } from "./session/chat/chat-schedule-loop-session.js";
42
38
  import { ProviderCatalogSession } from "./session/provider/provider-catalog-session.js";
43
39
  import { WorkspaceFilesSession } from "./session/files/workspace-files-session.js";
44
40
  import { AgentConfigSession } from "./session/agent-config/agent-config-session.js";
45
41
  import { ProjectConfigSession } from "./session/project-config/project-config-session.js";
46
42
  import { DaemonSession } from "./session/daemon/daemon-session.js";
47
- import { buildMetadataPrompt } from "../utils/build-metadata-prompt.js";
48
43
  import { archivePersistedWorkspaceRecord, archiveWorkspaceContents, } from "./workspace-archive-service.js";
49
44
  import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
50
- import { checkoutResolvedBranch, renameCurrentBranch as renameCurrentBranchDefault, } from "../utils/checkout-git.js";
45
+ import { renameCurrentBranch as renameCurrentBranchDefault } from "../utils/checkout-git.js";
46
+ import { createGitMutationService, } from "./session/git-mutation/git-mutation-service.js";
47
+ import { createWorkspaceProvisioningService, } from "./session/workspace-provisioning/workspace-provisioning-service.js";
48
+ import { createAgentUpdatesService, matchesAgentUpdatesFilter, } from "./session/agent-updates/agent-updates-service.js";
51
49
  import { expandTilde } from "../utils/path.js";
52
50
  import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
53
- import { execCommand } from "../utils/spawn.js";
54
51
  import { createGitHubService } from "../services/github-service.js";
55
52
  import { summarizeFetchWorkspacesEntries, workspaceIdsOnCheckout, WorkspaceDirectory, } from "./workspace-directory.js";
56
53
  import { shouldEmitPendingBootstrapUpdate } from "./workspace-bootstrap-dedupe.js";
57
54
  import { attemptFirstAgentBranchAutoName, createLocalCheckoutWorkspace, createPaseoWorktree, } from "./paseo-worktree-service.js";
58
55
  import { generateBranchNameFromFirstAgentContext, } from "./worktree-branch-name-generator.js";
59
- 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";
56
+ import { buildAgentSessionConfig as buildWorktreeAgentSessionConfig, createPaseoWorktreeWorkflow as createWorktreeWorkflow, handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest, handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest, handlePaseoWorktreeListRequest as handleWorktreeListRequest, handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage, } from "./worktree-session.js";
60
57
  import { archiveByScope } from "./workspace-archive-service.js";
61
58
  import { WorktreeRequestError, toWorktreeRequestError, toWorktreeWireError, } from "./worktree-errors.js";
62
59
  import { createWorktree } from "../utils/worktree.js";
63
60
  import { runGitCommand } from "../utils/run-git-command.js";
64
61
  import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
65
- const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
66
62
  // TODO: Remove once all app store clients are on >=0.1.45 and understand arbitrary provider strings.
67
63
  // Clients before 0.1.45 validate providers with z.enum(["claude", "codex", "opencode"]) and reject
68
64
  // the entire session message if they encounter an unknown provider.
@@ -83,13 +79,6 @@ function resolveSubscriptionId(subscribe, requestedSubscriptionId) {
83
79
  }
84
80
  return uuidv4();
85
81
  }
86
- function diffChangeTypeFor(file) {
87
- if (file.isNew)
88
- return "A";
89
- if (file.isDeleted)
90
- return "D";
91
- return "M";
92
- }
93
82
  function buildWorkspaceCheckout(workspace, project,
94
83
  // The persisted `branch` field is the source of truth, but it is null for
95
84
  // records created before branch was lifted to its own field (no migrations,
@@ -201,19 +190,12 @@ function describeRegistryTransition(record) {
201
190
  */
202
191
  export class Session {
203
192
  constructor(options) {
204
- // Per-session MCP client and tools
205
- this.agentMcpClient = null;
206
- this.agentTools = null;
207
193
  this.unsubscribeAgentEvents = null;
208
194
  this.unsubscribeTerminalWorkspaceContributionEvents = null;
209
- this.agentUpdatesSubscription = null;
210
195
  this.workspaceUpdatesSubscription = null;
211
196
  this.clientActivity = null;
212
197
  this.inflightRequests = 0;
213
198
  this.peakInflightRequests = 0;
214
- this.workspaceGitWatchTargets = new Map();
215
- this.workspaceGitFetchSubscriptions = new Map();
216
- this.workspaceGitSubscriptions = new Map();
217
199
  this.agentsPager = new SortablePager({
218
200
  validKeys: FETCH_AGENTS_SORT_KEYS,
219
201
  defaultSort: [{ key: "updated_at", direction: "desc" }],
@@ -237,7 +219,7 @@ export class Session {
237
219
  }
238
220
  },
239
221
  });
240
- 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, providerUsageService, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
222
+ 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, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, providerUsageService, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, getWebSocketRuntimeMetrics, } = options;
241
223
  this.clientId = clientId;
242
224
  this.appVersion = appVersion ?? null;
243
225
  this.clientCapabilities = parseClientCapabilities(clientCapabilities);
@@ -273,24 +255,49 @@ export class Session {
273
255
  this.renameCurrentBranch = renameCurrentBranch ?? renameCurrentBranchDefault;
274
256
  this.generateWorkspaceName = generateWorkspaceName ?? generateBranchNameFromFirstAgentContext;
275
257
  this.workspaceGitService = workspaceGitService;
258
+ this.gitMutation = createGitMutationService({
259
+ workspaceGitService: this.workspaceGitService,
260
+ github: this.github,
261
+ logger: this.sessionLogger,
262
+ });
263
+ this.workspaceProvisioning = createWorkspaceProvisioningService({
264
+ workspaceRegistry: this.workspaceRegistry,
265
+ projectRegistry: this.projectRegistry,
266
+ workspaceGitService: this.workspaceGitService,
267
+ });
276
268
  this.checkoutSession = new CheckoutSession({
277
269
  host: {
278
270
  emit: (msg) => this.emit(msg),
279
- notifyGitMutation: (cwd, reason, mutationOptions) => this.notifyGitMutation(cwd, reason, mutationOptions),
280
271
  emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
281
- handleWorkspaceGitBranchSnapshot: (cwd, branchName) => this.handleWorkspaceGitBranchSnapshot(cwd, branchName),
272
+ handleWorkspaceGitBranchSnapshot: (cwd, branchName) => this.workspaceGitObserver.handleBranchSnapshot(cwd, branchName),
282
273
  renameCurrentBranch: (cwd, branch) => this.renameCurrentBranch(cwd, branch),
283
- checkoutExistingBranch: (cwd, branch) => this.checkoutExistingBranch(cwd, branch),
284
- generateCommitMessage: (cwd) => this.generateCommitMessage(cwd),
285
- generatePullRequestText: (cwd, baseRef) => this.generatePullRequestText(cwd, baseRef),
286
274
  },
275
+ gitMutation: this.gitMutation,
287
276
  workspaceGitService: this.workspaceGitService,
288
277
  github: this.github,
289
278
  checkoutDiffManager,
279
+ gitMetadataGenerator: createGitMetadataGenerator({
280
+ workspaceGitService: this.workspaceGitService,
281
+ generation: createAgentStructuredTextGeneration({
282
+ agentManager: this.agentManager,
283
+ providerSnapshotManager,
284
+ readDaemonConfig: () => this.readStructuredGenerationDaemonConfig(),
285
+ getFocusedSelection: (cwd) => this.getFocusedAgentSelectionForCwd(cwd),
286
+ }),
287
+ }),
290
288
  paseoHome: this.paseoHome,
291
289
  worktreesRoot: this.worktreesRoot,
292
290
  logger: this.sessionLogger,
293
291
  });
292
+ this.workspaceGitObserver = createWorkspaceGitObserverService({
293
+ workspaceGitService: this.workspaceGitService,
294
+ describeWorkspaceRecordWithGitData: (workspace) => this.describeWorkspaceRecordWithGitData(workspace),
295
+ emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
296
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
297
+ emitStatusUpdate: (cwd, snapshot) => this.checkoutSession.emitStatusUpdate(cwd, snapshot),
298
+ onBranchChanged,
299
+ logger: this.sessionLogger,
300
+ });
294
301
  this.chatScheduleLoopSession = new ChatScheduleLoopSession({
295
302
  host: {
296
303
  emit: (msg) => this.emit(msg),
@@ -353,11 +360,14 @@ export class Session {
353
360
  serverId,
354
361
  daemonVersion,
355
362
  daemonRuntimeConfig,
363
+ getWebSocketRuntimeMetrics,
356
364
  listProviderAvailability: () => this.agentManager.listProviderAvailability(),
365
+ listAgents: () => this.agentManager.listAgents(),
366
+ listProjects: () => this.projectRegistry.list(),
367
+ listWorkspaces: () => this.workspaceRegistry.list(),
357
368
  logger: this.sessionLogger,
358
369
  });
359
370
  this.daemonConfigStore = daemonConfigStore;
360
- this.mcpBaseUrl = mcpBaseUrl ?? null;
361
371
  this.terminalManager = terminalManager;
362
372
  this.terminalController = new TerminalSessionController({
363
373
  terminalManager,
@@ -370,6 +380,15 @@ export class Session {
370
380
  clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
371
381
  getClientBufferedAmount: () => this.getTransportBufferedAmount(),
372
382
  });
383
+ this.agentUpdates = createAgentUpdatesService({
384
+ emit: (message) => this.emit(message),
385
+ buildAgentPayload: (agent) => this.buildAgentPayload(agent),
386
+ buildStoredAgentPayload: (record) => this.buildStoredAgentPayload(record),
387
+ isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
388
+ buildProjectPlacementForWorkspaceId: (workspaceId) => this.buildProjectPlacementForWorkspaceId(workspaceId),
389
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
390
+ logger: this.sessionLogger,
391
+ });
373
392
  this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
374
393
  paseoHome: this.paseoHome,
375
394
  worktreesRoot: this.worktreesRoot,
@@ -383,14 +402,7 @@ export class Session {
383
402
  listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
384
403
  archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
385
404
  emit: (message) => this.emit(message),
386
- emitAgentRemove: (agentId) => {
387
- if (this.agentUpdatesSubscription) {
388
- this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
389
- kind: "remove",
390
- agentId,
391
- });
392
- }
393
- },
405
+ emitAgentRemove: (agentId) => this.agentUpdates.removeAgent(agentId),
394
406
  emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
395
407
  markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
396
408
  clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
@@ -401,11 +413,24 @@ export class Session {
401
413
  this.serviceProxy = serviceProxy ?? null;
402
414
  this.scriptRuntimeStore = scriptRuntimeStore ?? null;
403
415
  this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
404
- this.onBranchChanged = onBranchChanged;
405
416
  this.getDaemonTcpPort = getDaemonTcpPort ?? null;
406
417
  this.getDaemonTcpHost = getDaemonTcpHost ?? null;
407
418
  this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null;
408
419
  this.resolveScriptHealth = resolveScriptHealth ?? null;
420
+ this.workspaceScripts = createWorkspaceScriptsService({
421
+ serviceProxy: this.serviceProxy,
422
+ scriptRuntimeStore: this.scriptRuntimeStore,
423
+ terminalManager: this.terminalManager,
424
+ workspaceRegistry: this.workspaceRegistry,
425
+ workspaceGitService: this.workspaceGitService,
426
+ getDaemonTcpPort: this.getDaemonTcpPort,
427
+ getDaemonTcpHost: this.getDaemonTcpHost,
428
+ serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
429
+ resolveScriptHealth: this.resolveScriptHealth,
430
+ logger: this.sessionLogger,
431
+ emit: (message) => this.emit(message),
432
+ spawnWorkspaceScript,
433
+ });
409
434
  this.subscribeToOptionalManagers();
410
435
  this.workspaceDirectory = new WorkspaceDirectory({
411
436
  logger: this.sessionLogger,
@@ -440,8 +465,6 @@ export class Session {
440
465
  voiceBridge,
441
466
  dictation,
442
467
  });
443
- // Initialize agent MCP client asynchronously
444
- void this.initializeAgentMcp();
445
468
  this.subscribeToAgentEvents();
446
469
  this.sessionLogger.trace({}, "agent.session.lifecycle.created");
447
470
  }
@@ -457,8 +480,7 @@ export class Session {
457
480
  return this.clientCapabilities.has(capability);
458
481
  }
459
482
  async syncWorkspaceGitObserverForWorkspace(workspace) {
460
- const descriptor = await this.describeWorkspaceRecordWithGitData(workspace);
461
- this.syncWorkspaceGitObservers([descriptor]);
483
+ await this.workspaceGitObserver.syncObserverForWorkspace(workspace);
462
484
  }
463
485
  async emitWorkspaceUpdateForWorkspaceId(workspaceId) {
464
486
  await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], { skipReconcile: true });
@@ -476,8 +498,7 @@ export class Session {
476
498
  await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds);
477
499
  }
478
500
  async warmWorkspaceGitDataForWorkspace(workspace) {
479
- await this.syncWorkspaceGitObserverForWorkspace(workspace);
480
- await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
501
+ await this.workspaceGitObserver.warmGitData(workspace);
481
502
  }
482
503
  /**
483
504
  * Get the client's current activity state
@@ -592,30 +613,6 @@ export class Session {
592
613
  },
593
614
  });
594
615
  }
595
- /**
596
- * Initialize Agent MCP client for this session using the daemon's HTTP MCP endpoint.
597
- */
598
- async initializeAgentMcp() {
599
- try {
600
- if (!this.mcpBaseUrl) {
601
- this.sessionLogger.info("Skipping Agent MCP initialization because no MCP base URL is configured");
602
- return;
603
- }
604
- const authToken = this.agentManager.getMcpAuthToken();
605
- const transport = new StreamableHTTPClientTransport(new URL(this.mcpBaseUrl), authToken
606
- ? { requestInit: { headers: { Authorization: `Bearer ${authToken}` } } }
607
- : undefined);
608
- this.agentMcpClient = await experimental_createMCPClient({
609
- transport,
610
- });
611
- this.agentTools = (await this.agentMcpClient.tools());
612
- const agentToolCount = Object.keys(this.agentTools ?? {}).length;
613
- this.sessionLogger.trace({ agentToolCount }, "agent.session.mcp_init");
614
- }
615
- catch (error) {
616
- this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
617
- }
618
- }
619
616
  /**
620
617
  * Subscribe to AgentManager events and forward them to the client
621
618
  */
@@ -644,7 +641,7 @@ export class Session {
644
641
  turnId: event.agent.activeForegroundTurnId ?? undefined,
645
642
  lifecycle: event.agent.lifecycle,
646
643
  }, "agent.session.forward_update");
647
- void this.forwardAgentUpdate(event.agent);
644
+ void this.agentUpdates.forwardLiveAgent(event.agent);
648
645
  return;
649
646
  }
650
647
  if (this.voiceSession.isActiveForAgent(event.agentId) &&
@@ -734,148 +731,6 @@ export class Session {
734
731
  }
735
732
  return LEGACY_PROVIDER_IDS.has(provider);
736
733
  }
737
- agentThinkingOptionMatchesFilter(agent, filter) {
738
- if (filter.thinkingOptionId === undefined) {
739
- return true;
740
- }
741
- const expectedThinkingOptionId = resolveEffectiveThinkingOptionId({
742
- configuredThinkingOptionId: filter.thinkingOptionId ?? null,
743
- });
744
- const resolvedThinkingOptionId = agent.effectiveThinkingOptionId ??
745
- resolveEffectiveThinkingOptionId({
746
- runtimeInfo: agent.runtimeInfo,
747
- configuredThinkingOptionId: agent.thinkingOptionId ?? null,
748
- });
749
- return resolvedThinkingOptionId === expectedThinkingOptionId;
750
- }
751
- matchesAgentStructuralFilter(agent, project, filter) {
752
- if (filter.statuses && filter.statuses.length > 0) {
753
- const statuses = new Set(filter.statuses);
754
- if (!statuses.has(agent.status)) {
755
- return false;
756
- }
757
- }
758
- if (typeof filter.requiresAttention === "boolean") {
759
- const requiresAttention = agent.requiresAttention ?? false;
760
- if (requiresAttention !== filter.requiresAttention) {
761
- return false;
762
- }
763
- }
764
- if (filter.projectKeys && filter.projectKeys.length > 0) {
765
- const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
766
- if (projectKeys.size > 0 && !projectKeys.has(project.projectKey)) {
767
- return false;
768
- }
769
- }
770
- return true;
771
- }
772
- matchesAgentFilter(options) {
773
- const { agent, project, filter } = options;
774
- if (filter?.labels) {
775
- const matchesLabels = Object.entries(filter.labels).every(([key, value]) => agent.labels[key] === value);
776
- if (!matchesLabels) {
777
- return false;
778
- }
779
- }
780
- const includeArchived = filter?.includeArchived ?? false;
781
- if (!includeArchived && agent.archivedAt) {
782
- return false;
783
- }
784
- if (filter && !this.agentThinkingOptionMatchesFilter(agent, filter)) {
785
- return false;
786
- }
787
- if (filter && !this.matchesAgentStructuralFilter(agent, project, filter)) {
788
- return false;
789
- }
790
- return true;
791
- }
792
- getAgentUpdateTargetId(update) {
793
- return update.kind === "remove" ? update.agentId : update.agent.id;
794
- }
795
- bufferOrEmitAgentUpdate(subscription, payload) {
796
- if (payload.kind === "upsert" && !this.isProviderVisibleToClient(payload.agent.provider)) {
797
- return;
798
- }
799
- if (subscription.isBootstrapping) {
800
- subscription.pendingUpdatesByAgentId.set(this.getAgentUpdateTargetId(payload), payload);
801
- return;
802
- }
803
- this.emit({
804
- type: "agent_update",
805
- payload,
806
- });
807
- }
808
- async emitStoredAgentUpdate(record) {
809
- const payload = this.buildStoredAgentPayload(record);
810
- const subscription = this.agentUpdatesSubscription;
811
- if (!subscription) {
812
- return payload;
813
- }
814
- const project = payload.workspaceId
815
- ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
816
- : null;
817
- if (!project) {
818
- this.bufferOrEmitAgentUpdate(subscription, {
819
- kind: "remove",
820
- agentId: payload.id,
821
- });
822
- return payload;
823
- }
824
- const matches = this.matchesAgentFilter({
825
- agent: payload,
826
- project,
827
- filter: subscription.filter,
828
- });
829
- this.bufferOrEmitAgentUpdate(subscription, matches
830
- ? {
831
- kind: "upsert",
832
- agent: payload,
833
- project,
834
- }
835
- : {
836
- kind: "remove",
837
- agentId: payload.id,
838
- });
839
- return payload;
840
- }
841
- flushBootstrappedAgentUpdates(options) {
842
- const subscription = this.agentUpdatesSubscription;
843
- if (!subscription || !subscription.isBootstrapping) {
844
- return;
845
- }
846
- subscription.isBootstrapping = false;
847
- const pending = Array.from(subscription.pendingUpdatesByAgentId.values());
848
- subscription.pendingUpdatesByAgentId.clear();
849
- for (const payload of pending) {
850
- if (payload.kind === "upsert") {
851
- const snapshotUpdatedAt = options?.snapshotUpdatedAtByAgentId?.get(payload.agent.id);
852
- if (typeof snapshotUpdatedAt === "number") {
853
- const updateUpdatedAt = Date.parse(payload.agent.updatedAt);
854
- if (!Number.isNaN(updateUpdatedAt) && updateUpdatedAt <= snapshotUpdatedAt) {
855
- continue;
856
- }
857
- }
858
- }
859
- this.emit({
860
- type: "agent_update",
861
- payload,
862
- });
863
- }
864
- }
865
- async findExactWorkspaceByDirectory(cwd, options) {
866
- const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
867
- const workspaces = await this.workspaceRegistry.list();
868
- return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null;
869
- }
870
- async resolveWorkspaceDirectory(cwd, options) {
871
- const normalizedCwd = resolve(cwd);
872
- if (options?.refreshGit === false) {
873
- const snapshot = this.workspaceGitService.peekSnapshot(normalizedCwd);
874
- return resolve(snapshot?.git.repoRoot ?? normalizedCwd);
875
- }
876
- const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
877
- return resolve(checkout.worktreeRoot ?? normalizedCwd);
878
- }
879
734
  async buildProjectPlacementForWorkspace(workspace, projectRecord) {
880
735
  const project = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
881
736
  if (!project) {
@@ -905,51 +760,6 @@ export class Session {
905
760
  return null;
906
761
  return this.buildProjectPlacementForWorkspace(workspace, project);
907
762
  }
908
- async forwardAgentUpdate(agent) {
909
- try {
910
- const subscription = this.agentUpdatesSubscription;
911
- const payload = await this.buildAgentPayload(agent);
912
- if (subscription) {
913
- const project = payload.workspaceId
914
- ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
915
- : null;
916
- if (!project) {
917
- this.bufferOrEmitAgentUpdate(subscription, {
918
- kind: "remove",
919
- agentId: payload.id,
920
- });
921
- }
922
- else {
923
- const matches = this.matchesAgentFilter({
924
- agent: payload,
925
- project,
926
- filter: subscription.filter,
927
- });
928
- if (matches) {
929
- this.bufferOrEmitAgentUpdate(subscription, {
930
- kind: "upsert",
931
- agent: payload,
932
- project,
933
- });
934
- }
935
- else {
936
- this.bufferOrEmitAgentUpdate(subscription, {
937
- kind: "remove",
938
- agentId: payload.id,
939
- });
940
- }
941
- }
942
- }
943
- // A lifecycle change updates exactly the agent's owning workspace, never
944
- // every workspace sharing its cwd. Ownership is the agent's workspaceId.
945
- if (payload.workspaceId) {
946
- await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
947
- }
948
- }
949
- catch (error) {
950
- this.sessionLogger.error({ err: error }, "Failed to emit agent update");
951
- }
952
- }
953
763
  /**
954
764
  * Main entry point for processing session messages
955
765
  */
@@ -1145,6 +955,8 @@ export class Session {
1145
955
  return this.daemonSession.handleGetStatusRequest(msg);
1146
956
  case "daemon.get_pairing_offer.request":
1147
957
  return this.daemonSession.handleGetPairingOfferRequest(msg);
958
+ case "diagnostics.request":
959
+ return this.daemonSession.handleDiagnosticsRequest(msg);
1148
960
  case "set_daemon_config_request":
1149
961
  this.emit({
1150
962
  type: "set_daemon_config_response",
@@ -1437,12 +1249,7 @@ export class Session {
1437
1249
  requestId,
1438
1250
  },
1439
1251
  });
1440
- if (this.agentUpdatesSubscription) {
1441
- this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
1442
- kind: "remove",
1443
- agentId,
1444
- });
1445
- }
1252
+ this.agentUpdates.removeAgent(agentId);
1446
1253
  if (knownWorkspaceId) {
1447
1254
  await this.emitWorkspaceUpdateForWorkspaceId(knownWorkspaceId);
1448
1255
  }
@@ -1465,8 +1272,8 @@ export class Session {
1465
1272
  agentStorage: this.agentStorage,
1466
1273
  logger: this.sessionLogger,
1467
1274
  }, agentId);
1468
- if (this.agentUpdatesSubscription) {
1469
- const payload = await this.emitStoredAgentUpdate(archivedRecord);
1275
+ if (this.agentUpdates.hasSubscription()) {
1276
+ const payload = await this.agentUpdates.emitStoredRecord(archivedRecord);
1470
1277
  if (payload.workspaceId) {
1471
1278
  await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
1472
1279
  }
@@ -1479,7 +1286,7 @@ export class Session {
1479
1286
  const result = await detachAgentCommand({ agentManager: this.agentManager }, agentId);
1480
1287
  const affectedWorkspaceIds = new Set();
1481
1288
  if (!result.live) {
1482
- const payload = await this.emitStoredAgentUpdate(result.record);
1289
+ const payload = await this.agentUpdates.emitStoredRecord(result.record);
1483
1290
  if (payload.workspaceId) {
1484
1291
  affectedWorkspaceIds.add(payload.workspaceId);
1485
1292
  }
@@ -1878,7 +1685,7 @@ export class Session {
1878
1685
  const createAgentConfig = createdWorktree
1879
1686
  ? { ...config, cwd: createdWorktree.worktree.worktreePath }
1880
1687
  : config;
1881
- const workspaceId = await this.resolveOrCreateWorkspaceIdForCreateAgent({
1688
+ const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent({
1882
1689
  createdWorktree,
1883
1690
  requestedWorkspaceId: msg.workspaceId,
1884
1691
  cwd: createAgentConfig.cwd,
@@ -1912,7 +1719,7 @@ export class Session {
1912
1719
  if (!createdWorktree && msg.workspaceId) {
1913
1720
  await this.writeInitialWorkspaceTitleIfUntitled(workspaceId, workspacePromptTitle);
1914
1721
  }
1915
- await this.forwardAgentUpdate(snapshot);
1722
+ await this.agentUpdates.forwardLiveAgent(snapshot);
1916
1723
  if (!createdWorktree && trimmedPrompt) {
1917
1724
  await this.scheduleAutoNameLocalWorkspaceTitleForFirstAgent({
1918
1725
  workspaceId,
@@ -2000,7 +1807,7 @@ export class Session {
2000
1807
  const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
2001
1808
  await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
2002
1809
  await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
2003
- await this.forwardAgentUpdate(snapshot);
1810
+ await this.agentUpdates.forwardLiveAgent(snapshot);
2004
1811
  const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
2005
1812
  if (requestId) {
2006
1813
  const agentPayload = await this.buildAgentPayload(snapshot);
@@ -2062,7 +1869,7 @@ export class Session {
2062
1869
  }
2063
1870
  // An imported agent mints its own workspace; ownership is its workspaceId,
2064
1871
  // never an existing same-cwd workspace resolved by path.
2065
- const workspace = await this.createWorkspaceForDirectory(normalized.cwd);
1872
+ const workspace = await this.workspaceProvisioning.createWorkspaceForDirectory(normalized.cwd);
2066
1873
  const { snapshot, timelineSize } = await importProviderSession({
2067
1874
  request: normalized,
2068
1875
  workspaceId: workspace.workspaceId,
@@ -2135,7 +1942,7 @@ export class Session {
2135
1942
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
2136
1943
  }
2137
1944
  await this.agentManager.hydrateTimelineFromProvider(agentId);
2138
- await this.forwardAgentUpdate(snapshot);
1945
+ await this.agentUpdates.forwardLiveAgent(snapshot);
2139
1946
  const timelineSize = this.agentManager.getTimeline(agentId).length;
2140
1947
  if (requestId) {
2141
1948
  this.emit({
@@ -2244,8 +2051,8 @@ export class Session {
2244
2051
  logger: this.sessionLogger,
2245
2052
  },
2246
2053
  }),
2247
- checkoutExistingBranch: (cwd, branch) => this.checkoutExistingBranch(cwd, branch),
2248
- createBranchFromBase: (params) => this.createBranchFromBase(params),
2054
+ checkoutExistingBranch: (cwd, branch) => this.gitMutation.checkoutExistingBranch(cwd, branch),
2055
+ createBranchFromBase: (params) => this.gitMutation.createBranchFromBase(params),
2249
2056
  github: this.github,
2250
2057
  }, config, gitOptions, legacyWorktreeName, firstAgentContext);
2251
2058
  }
@@ -2290,7 +2097,7 @@ export class Session {
2290
2097
  branch: result.branchName,
2291
2098
  promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
2292
2099
  });
2293
- await this.notifyGitMutation(input.workspace.cwd, "rename-branch");
2100
+ await this.gitMutation.notifyGitMutation(input.workspace.cwd, "rename-branch");
2294
2101
  await this.emitWorkspaceUpdateForCwd(input.workspace.cwd);
2295
2102
  }
2296
2103
  // Generated names may replace the prompt title set at creation, but not a user
@@ -2366,12 +2173,6 @@ export class Session {
2366
2173
  firstAgentContext: input.firstAgentContext,
2367
2174
  }), { cwd: input.cwd, message: "Failed to auto-name local workspace title" });
2368
2175
  }
2369
- assertSafeGitRef(ref, label) {
2370
- if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
2371
- throw new Error(`Invalid ${label}: ${ref}`);
2372
- }
2373
- assertWorktreeSafeGitRef(ref, label);
2374
- }
2375
2176
  isPathWithinRoot(rootPath, candidatePath) {
2376
2177
  const resolvedRoot = resolve(rootPath);
2377
2178
  const resolvedCandidate = resolve(candidatePath);
@@ -2380,220 +2181,6 @@ export class Session {
2380
2181
  }
2381
2182
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2382
2183
  }
2383
- async generateCommitMessage(cwd) {
2384
- const diff = await this.workspaceGitService.getCheckoutDiff(cwd, {
2385
- mode: "uncommitted",
2386
- includeStructured: true,
2387
- });
2388
- const schema = z.object({
2389
- message: z
2390
- .string()
2391
- .min(1)
2392
- .max(72)
2393
- .describe("Concise git commit message, imperative mood, no trailing period."),
2394
- });
2395
- const fileList = diff.structured && diff.structured.length > 0
2396
- ? [
2397
- "Files changed:",
2398
- ...diff.structured.map((file) => {
2399
- const changeType = diffChangeTypeFor(file);
2400
- const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2401
- return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2402
- }),
2403
- ].join("\n")
2404
- : "Files changed: (unknown)";
2405
- const maxPatchChars = 120000;
2406
- const patch = diff.diff.length > maxPatchChars
2407
- ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2408
- : diff.diff;
2409
- const prompt = await buildMetadataPrompt({
2410
- cwd,
2411
- workspaceGitService: this.workspaceGitService,
2412
- contract: "Write a concise git commit message for the changes below.",
2413
- styles: [
2414
- {
2415
- configKey: "commitMessage",
2416
- default: "Concise, imperative mood, no trailing period.",
2417
- },
2418
- ],
2419
- after: [
2420
- "Return JSON only with a single field 'message'.",
2421
- "",
2422
- fileList,
2423
- "",
2424
- patch.length > 0 ? patch : "(No diff available)",
2425
- ].join("\n"),
2426
- });
2427
- const providers = await resolveStructuredGenerationProviders({
2428
- cwd,
2429
- providerSnapshotManager: this.providerSnapshotManager,
2430
- daemonConfig: this.readStructuredGenerationDaemonConfig(),
2431
- currentSelection: this.getFocusedAgentSelectionForCwd(cwd),
2432
- });
2433
- try {
2434
- const result = await generateStructuredAgentResponseWithFallback({
2435
- manager: this.agentManager,
2436
- cwd,
2437
- prompt,
2438
- schema,
2439
- schemaName: "CommitMessage",
2440
- maxRetries: 2,
2441
- providers,
2442
- persistSession: false,
2443
- agentConfigOverrides: {
2444
- title: "Commit generator",
2445
- internal: true,
2446
- },
2447
- });
2448
- return result.message;
2449
- }
2450
- catch (error) {
2451
- if (error instanceof StructuredAgentResponseError ||
2452
- error instanceof StructuredAgentFallbackError) {
2453
- return "Update files";
2454
- }
2455
- throw error;
2456
- }
2457
- }
2458
- async generatePullRequestText(cwd, baseRef) {
2459
- const diff = await this.workspaceGitService.getCheckoutDiff(cwd, {
2460
- mode: "base",
2461
- baseRef,
2462
- includeStructured: true,
2463
- });
2464
- const schema = z.object({
2465
- title: z.string().min(1).max(72),
2466
- body: z.string().min(1),
2467
- });
2468
- const fileList = diff.structured && diff.structured.length > 0
2469
- ? [
2470
- "Files changed:",
2471
- ...diff.structured.map((file) => {
2472
- const changeType = diffChangeTypeFor(file);
2473
- const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2474
- return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2475
- }),
2476
- ].join("\n")
2477
- : "Files changed: (unknown)";
2478
- const maxPatchChars = 200000;
2479
- const patch = diff.diff.length > maxPatchChars
2480
- ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2481
- : diff.diff;
2482
- const prompt = await buildMetadataPrompt({
2483
- cwd,
2484
- workspaceGitService: this.workspaceGitService,
2485
- contract: "Write a pull request title and body for the changes below.",
2486
- styles: [
2487
- {
2488
- configKey: "pullRequest",
2489
- default: "Clear, descriptive title; body explaining what changed and why.",
2490
- },
2491
- ],
2492
- after: [
2493
- "Return JSON only with fields 'title' and 'body'.",
2494
- "",
2495
- fileList,
2496
- "",
2497
- patch.length > 0 ? patch : "(No diff available)",
2498
- ].join("\n"),
2499
- });
2500
- const providers = await resolveStructuredGenerationProviders({
2501
- cwd,
2502
- providerSnapshotManager: this.providerSnapshotManager,
2503
- daemonConfig: this.readStructuredGenerationDaemonConfig(),
2504
- currentSelection: this.getFocusedAgentSelectionForCwd(cwd),
2505
- });
2506
- try {
2507
- return await generateStructuredAgentResponseWithFallback({
2508
- manager: this.agentManager,
2509
- cwd,
2510
- prompt,
2511
- schema,
2512
- schemaName: "PullRequest",
2513
- maxRetries: 2,
2514
- providers,
2515
- persistSession: false,
2516
- agentConfigOverrides: {
2517
- title: "PR generator",
2518
- internal: true,
2519
- },
2520
- });
2521
- }
2522
- catch (error) {
2523
- if (error instanceof StructuredAgentResponseError ||
2524
- error instanceof StructuredAgentFallbackError) {
2525
- return {
2526
- title: "Update changes",
2527
- body: "Automated PR generated by Paseo.",
2528
- };
2529
- }
2530
- throw error;
2531
- }
2532
- }
2533
- async ensureCleanWorkingTree(cwd) {
2534
- const dirty = await this.isWorkingTreeDirty(cwd);
2535
- if (dirty) {
2536
- throw new Error("Working directory has uncommitted changes. Commit or stash before switching branches.");
2537
- }
2538
- }
2539
- async isWorkingTreeDirty(cwd) {
2540
- try {
2541
- const snapshot = await this.workspaceGitService.getSnapshot(cwd);
2542
- return snapshot.git.isDirty === true;
2543
- }
2544
- catch (error) {
2545
- throw new Error(`Unable to inspect git status for ${cwd}: ${getErrorMessage(error)}`, {
2546
- cause: error,
2547
- });
2548
- }
2549
- }
2550
- async checkoutExistingBranch(cwd, branch) {
2551
- this.assertSafeGitRef(branch, "branch");
2552
- const resolution = await this.workspaceGitService.validateBranchRef(cwd, branch);
2553
- if (resolution.kind === "not-found") {
2554
- throw new Error(`Branch not found: ${branch}`);
2555
- }
2556
- await this.ensureCleanWorkingTree(cwd);
2557
- const result = await checkoutResolvedBranch({
2558
- cwd,
2559
- resolution,
2560
- });
2561
- await this.notifyGitMutation(cwd, "switch-branch", { invalidateGithub: true });
2562
- return result;
2563
- }
2564
- async createBranchFromBase(params) {
2565
- const { cwd, baseBranch, newBranchName } = params;
2566
- this.assertSafeGitRef(baseBranch, "base branch");
2567
- this.assertSafeGitRef(newBranchName, "new branch");
2568
- const baseResolution = await this.workspaceGitService.validateBranchRef(cwd, baseBranch);
2569
- if (baseResolution.kind === "not-found") {
2570
- throw new Error(`Base branch not found: ${baseBranch}`);
2571
- }
2572
- const exists = await this.doesLocalBranchExist(cwd, newBranchName);
2573
- if (exists) {
2574
- throw new Error(`Branch already exists: ${newBranchName}`);
2575
- }
2576
- await this.ensureCleanWorkingTree(cwd);
2577
- await execCommand("git", ["checkout", "-b", newBranchName, baseBranch], {
2578
- cwd,
2579
- });
2580
- await this.notifyGitMutation(cwd, "create-branch");
2581
- }
2582
- async doesLocalBranchExist(cwd, branch) {
2583
- this.assertSafeGitRef(branch, "branch");
2584
- return this.workspaceGitService.hasLocalBranch(cwd, branch);
2585
- }
2586
- async notifyGitMutation(cwd, reason, options) {
2587
- if (options?.invalidateGithub) {
2588
- this.github.invalidate({ cwd });
2589
- }
2590
- try {
2591
- await this.workspaceGitService.getSnapshot(cwd, { force: true, reason });
2592
- }
2593
- catch (error) {
2594
- this.sessionLogger.warn({ err: error, cwd, reason }, "Failed to force-refresh workspace git snapshot after mutation");
2595
- }
2596
- }
2597
2184
  /**
2598
2185
  * Handle clearing agent attention flag
2599
2186
  */
@@ -2797,129 +2384,6 @@ export class Session {
2797
2384
  });
2798
2385
  }
2799
2386
  }
2800
- closeWorkspaceGitWatchTarget(target) {
2801
- if (target.debounceTimer) {
2802
- clearTimeout(target.debounceTimer);
2803
- target.debounceTimer = null;
2804
- }
2805
- for (const watcher of target.watchers) {
2806
- try {
2807
- watcher.close();
2808
- }
2809
- catch {
2810
- // Ignore watcher close errors
2811
- }
2812
- }
2813
- target.watchers.length = 0;
2814
- }
2815
- async removeWorkspaceGitWatchTarget(cwd) {
2816
- const normalizedCwd = resolve(cwd);
2817
- const target = this.workspaceGitWatchTargets.get(normalizedCwd);
2818
- if (target) {
2819
- this.closeWorkspaceGitWatchTarget(target);
2820
- this.workspaceGitWatchTargets.delete(normalizedCwd);
2821
- }
2822
- }
2823
- removeWorkspaceGitSubscription(cwd) {
2824
- const normalizedCwd = resolve(cwd);
2825
- const target = this.workspaceGitWatchTargets.get(normalizedCwd);
2826
- if (target) {
2827
- const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(normalizedCwd);
2828
- unsubscribeFetch?.();
2829
- this.workspaceGitFetchSubscriptions.delete(normalizedCwd);
2830
- this.closeWorkspaceGitWatchTarget(target);
2831
- this.workspaceGitWatchTargets.delete(normalizedCwd);
2832
- }
2833
- this.workspaceGitSubscriptions.get(normalizedCwd)?.();
2834
- this.workspaceGitSubscriptions.delete(normalizedCwd);
2835
- }
2836
- workspaceGitDescriptorStateKey(workspace) {
2837
- if (!workspace) {
2838
- return WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY;
2839
- }
2840
- return JSON.stringify([
2841
- workspace.name,
2842
- workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
2843
- ]);
2844
- }
2845
- resolveWorkspaceGitWatchTarget(workspaceId) {
2846
- for (const target of this.workspaceGitWatchTargets.values()) {
2847
- if (target.workspaceId === workspaceId) {
2848
- return target;
2849
- }
2850
- }
2851
- return null;
2852
- }
2853
- shouldSkipWorkspaceGitWatchUpdate(workspaceId, workspace) {
2854
- const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
2855
- if (!target) {
2856
- return false;
2857
- }
2858
- const nextStateKey = this.workspaceGitDescriptorStateKey(workspace);
2859
- if (target.latestDescriptorStateKey === nextStateKey) {
2860
- return true;
2861
- }
2862
- target.latestDescriptorStateKey = nextStateKey;
2863
- return false;
2864
- }
2865
- rememberWorkspaceGitDescriptorState(workspaceId, workspace) {
2866
- const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
2867
- if (!target) {
2868
- return;
2869
- }
2870
- target.latestDescriptorStateKey = this.workspaceGitDescriptorStateKey(workspace);
2871
- target.lastBranchName = workspace?.name ?? null;
2872
- }
2873
- handleWorkspaceGitBranchSnapshot(cwd, branchName) {
2874
- const target = this.workspaceGitWatchTargets.get(resolve(cwd));
2875
- if (!target) {
2876
- return;
2877
- }
2878
- const previousBranchName = target.lastBranchName;
2879
- if (branchName === previousBranchName) {
2880
- return;
2881
- }
2882
- target.lastBranchName = branchName;
2883
- this.onBranchChanged?.(target.workspaceId, previousBranchName, branchName);
2884
- }
2885
- syncWorkspaceGitObservers(workspaces) {
2886
- for (const workspace of workspaces) {
2887
- this.syncWorkspaceGitObserver(workspace.workspaceDirectory, {
2888
- isGit: workspace.projectKind === "git",
2889
- workspaceId: workspace.id,
2890
- });
2891
- this.rememberWorkspaceGitDescriptorState(workspace.workspaceDirectory, workspace);
2892
- }
2893
- }
2894
- syncWorkspaceGitObserver(cwd, options) {
2895
- const normalizedCwd = resolve(cwd);
2896
- if (!options.isGit) {
2897
- this.removeWorkspaceGitSubscription(normalizedCwd);
2898
- return;
2899
- }
2900
- if (this.workspaceGitSubscriptions.has(normalizedCwd)) {
2901
- return;
2902
- }
2903
- const target = {
2904
- cwd: normalizedCwd,
2905
- workspaceId: options.workspaceId,
2906
- watchers: [],
2907
- debounceTimer: null,
2908
- refreshPromise: null,
2909
- refreshQueued: false,
2910
- latestDescriptorStateKey: null,
2911
- lastBranchName: null,
2912
- };
2913
- this.workspaceGitWatchTargets.set(normalizedCwd, target);
2914
- const subscription = this.workspaceGitService.registerWorkspace({ cwd: normalizedCwd }, (snapshot) => {
2915
- this.handleWorkspaceGitBranchSnapshot(normalizedCwd, snapshot.git.currentBranch ?? null);
2916
- void this.emitWorkspaceUpdateForCwd(normalizedCwd).catch((error) => {
2917
- this.sessionLogger.warn({ err: error, cwd: normalizedCwd }, "Failed to emit workspace update after git branch snapshot");
2918
- });
2919
- this.checkoutSession.emitStatusUpdate(normalizedCwd, snapshot);
2920
- });
2921
- this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
2922
- }
2923
2387
  async handlePaseoWorktreeListRequest(msg) {
2924
2388
  return handleWorktreeListRequest({
2925
2389
  emit: (message) => this.emit(message),
@@ -3115,7 +2579,7 @@ export class Session {
3115
2579
  if (!entry) {
3116
2580
  continue;
3117
2581
  }
3118
- if (!this.matchesAgentFilter({
2582
+ if (!matchesAgentUpdatesFilter({
3119
2583
  agent: entry.agent,
3120
2584
  project: entry.project,
3121
2585
  filter,
@@ -3230,19 +2694,7 @@ export class Session {
3230
2694
  statusEnteredAt: null,
3231
2695
  activityAt: null,
3232
2696
  diffStat,
3233
- scripts: this.serviceProxy && this.scriptRuntimeStore
3234
- ? buildWorkspaceScriptPayloads({
3235
- workspaceId: workspace.workspaceId,
3236
- workspaceDirectory: workspace.cwd,
3237
- paseoConfig: readPaseoConfigForProjection(workspace.cwd, this.sessionLogger),
3238
- serviceProxy: this.serviceProxy,
3239
- runtimeStore: this.scriptRuntimeStore,
3240
- daemonPort: this.getDaemonTcpPort?.() ?? null,
3241
- serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
3242
- gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspace.cwd),
3243
- resolveHealth: this.resolveScriptHealth ?? undefined,
3244
- })
3245
- : [],
2697
+ scripts: this.buildWorkspaceScriptPayloadSnapshot(workspace.workspaceId, workspace.cwd),
3246
2698
  ...(resolvedProjectRecord
3247
2699
  ? {
3248
2700
  project: await this.buildProjectPlacementForWorkspace(workspace, resolvedProjectRecord),
@@ -3411,91 +2863,6 @@ export class Session {
3411
2863
  });
3412
2864
  }
3413
2865
  }
3414
- async findOrCreateWorkspaceForDirectory(cwd) {
3415
- const inputCwd = resolve(cwd);
3416
- const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
3417
- const existingWorkspace = await this.findExactWorkspaceByDirectory(normalizedCwd, {
3418
- refreshGit: false,
3419
- });
3420
- if (existingWorkspace) {
3421
- if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) {
3422
- const timestamp = new Date().toISOString();
3423
- const checkout = checkoutLiteFromGitSnapshot(inputCwd, {
3424
- isGit: false,
3425
- currentBranch: null,
3426
- remoteUrl: null,
3427
- repoRoot: null,
3428
- isPaseoOwnedWorktree: false,
3429
- mainRepoRoot: null,
3430
- });
3431
- const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout });
3432
- const projectRecord = await this.resolveProjectRecordForPlacement({
3433
- membership,
3434
- timestamp,
3435
- });
3436
- await this.projectRegistry.upsert(projectRecord);
3437
- const workspaceRecord = createPersistedWorkspaceRecord({
3438
- workspaceId: generateWorkspaceId(),
3439
- projectId: projectRecord.projectId,
3440
- cwd: inputCwd,
3441
- kind: membership.workspaceKind,
3442
- displayName: membership.workspaceDisplayName,
3443
- createdAt: timestamp,
3444
- updatedAt: timestamp,
3445
- });
3446
- await this.workspaceRegistry.upsert(workspaceRecord);
3447
- return workspaceRecord;
3448
- }
3449
- return this.reclassifyOrUnarchiveWorkspaceForDirectory({
3450
- workspace: existingWorkspace,
3451
- project: await this.projectRegistry.get(existingWorkspace.projectId),
3452
- cwd: normalizedCwd,
3453
- });
3454
- }
3455
- return this.createWorkspaceForDirectory(normalizedCwd);
3456
- }
3457
- async resolveOrCreateWorkspaceIdForCreateAgent(input) {
3458
- if (input.createdWorktree) {
3459
- return input.createdWorktree.workspace.workspaceId;
3460
- }
3461
- if (input.requestedWorkspaceId) {
3462
- return input.requestedWorkspaceId;
3463
- }
3464
- return (await this.createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId;
3465
- }
3466
- async createWorkspaceForDirectory(cwd, title) {
3467
- const checkout = await this.workspaceGitService.getCheckout(cwd);
3468
- const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
3469
- const timestamp = new Date().toISOString();
3470
- const projectRecord = await this.resolveProjectRecordForPlacement({
3471
- membership,
3472
- timestamp,
3473
- });
3474
- await this.projectRegistry.upsert(projectRecord);
3475
- const workspaceRecord = createPersistedWorkspaceRecord({
3476
- workspaceId: generateWorkspaceId(),
3477
- projectId: projectRecord.projectId,
3478
- cwd,
3479
- kind: membership.workspaceKind,
3480
- displayName: membership.workspaceDisplayName,
3481
- title: title ?? null,
3482
- createdAt: timestamp,
3483
- updatedAt: timestamp,
3484
- });
3485
- await this.workspaceRegistry.upsert(workspaceRecord);
3486
- return workspaceRecord;
3487
- }
3488
- async findOrCreateProjectForDirectory(cwd) {
3489
- const normalizedCwd = resolve(cwd);
3490
- const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
3491
- const membership = classifyDirectoryForProjectMembership({ cwd: normalizedCwd, checkout });
3492
- const projectRecord = await this.resolveProjectRecordForPlacement({
3493
- membership,
3494
- timestamp: new Date().toISOString(),
3495
- });
3496
- await this.projectRegistry.upsert(projectRecord);
3497
- return projectRecord;
3498
- }
3499
2866
  buildProjectDescriptor(project) {
3500
2867
  return {
3501
2868
  projectId: project.projectId,
@@ -3505,64 +2872,6 @@ export class Session {
3505
2872
  projectKind: project.kind,
3506
2873
  };
3507
2874
  }
3508
- async reclassifyOrUnarchiveWorkspaceForDirectory(input) {
3509
- const checkout = await this.workspaceGitService.getCheckout(input.cwd);
3510
- const membership = classifyDirectoryForProjectMembership({ cwd: input.cwd, checkout });
3511
- const timestamp = new Date().toISOString();
3512
- const projectRecord = await this.resolveProjectRecordForPlacement({
3513
- membership,
3514
- timestamp,
3515
- });
3516
- const projectId = projectRecord.projectId;
3517
- const kind = membership.workspaceKind;
3518
- const displayName = membership.workspaceDisplayName;
3519
- if (input.workspace.projectId === projectId &&
3520
- input.workspace.kind === kind &&
3521
- input.workspace.displayName === displayName) {
3522
- if (!input.project) {
3523
- await this.projectRegistry.upsert(projectRecord);
3524
- }
3525
- return this.ensureWorkspaceRecordUnarchived(input.workspace);
3526
- }
3527
- await this.projectRegistry.upsert(projectRecord);
3528
- const nextWorkspace = {
3529
- ...input.workspace,
3530
- workspaceId: input.workspace.workspaceId,
3531
- projectId,
3532
- cwd: input.cwd,
3533
- kind,
3534
- displayName,
3535
- archivedAt: null,
3536
- updatedAt: timestamp,
3537
- };
3538
- await this.workspaceRegistry.upsert(nextWorkspace);
3539
- return nextWorkspace;
3540
- }
3541
- async resolveProjectRecordForPlacement(input) {
3542
- const rootPath = input.membership.projectRootPath;
3543
- const kind = input.membership.projectKind;
3544
- const projects = await this.projectRegistry.list();
3545
- const existingProject = projects.find((project) => !project.archivedAt && project.rootPath === rootPath) ??
3546
- projects.find((project) => project.rootPath === rootPath) ??
3547
- null;
3548
- if (!existingProject) {
3549
- return createPersistedProjectRecord({
3550
- projectId: input.membership.projectKey,
3551
- rootPath,
3552
- kind,
3553
- displayName: input.membership.projectName,
3554
- createdAt: input.timestamp,
3555
- updatedAt: input.timestamp,
3556
- });
3557
- }
3558
- return {
3559
- ...existingProject,
3560
- rootPath,
3561
- kind,
3562
- archivedAt: null,
3563
- updatedAt: input.timestamp,
3564
- };
3565
- }
3566
2875
  async unarchiveOwningWorkspaceForAgent(agentId) {
3567
2876
  const record = await this.agentStorage.get(agentId);
3568
2877
  if (!record?.workspaceId) {
@@ -3582,7 +2891,7 @@ export class Session {
3582
2891
  // missing, so the record must point at a real directory first.
3583
2892
  await this.recreateOwningWorktreeForRestore(workspace, workspace.branch);
3584
2893
  }
3585
- await this.ensureWorkspaceRecordUnarchived(workspace);
2894
+ await this.workspaceProvisioning.ensureWorkspaceRecordUnarchived(workspace);
3586
2895
  await this.emitWorkspaceUpdatesForWorkspaceIds([workspace.workspaceId]);
3587
2896
  }
3588
2897
  async recreateOwningWorktreeForRestore(workspace, branch) {
@@ -3635,26 +2944,6 @@ export class Session {
3635
2944
  });
3636
2945
  }
3637
2946
  }
3638
- async ensureWorkspaceRecordUnarchived(workspace) {
3639
- const project = await this.projectRegistry.get(workspace.projectId);
3640
- if (!workspace.archivedAt && (!project || !project.archivedAt)) {
3641
- return workspace;
3642
- }
3643
- const timestamp = new Date().toISOString();
3644
- let unarchivedWorkspace = workspace;
3645
- if (workspace.archivedAt) {
3646
- unarchivedWorkspace = { ...workspace, archivedAt: null, updatedAt: timestamp };
3647
- await this.workspaceRegistry.upsert(unarchivedWorkspace);
3648
- }
3649
- if (project?.archivedAt) {
3650
- await this.projectRegistry.upsert({
3651
- ...project,
3652
- archivedAt: null,
3653
- updatedAt: timestamp,
3654
- });
3655
- }
3656
- return unarchivedWorkspace;
3657
- }
3658
2947
  async createPaseoWorktree(input, options) {
3659
2948
  const result = await createPaseoWorktree(input, {
3660
2949
  github: this.github,
@@ -3666,8 +2955,8 @@ export class Session {
3666
2955
  workspaceGitService: this.workspaceGitService,
3667
2956
  });
3668
2957
  void Promise.all([
3669
- this.notifyGitMutation(input.cwd, "create-worktree"),
3670
- this.notifyGitMutation(result.worktree.worktreePath, "create-worktree"),
2958
+ this.gitMutation.notifyGitMutation(input.cwd, "create-worktree"),
2959
+ this.gitMutation.notifyGitMutation(result.worktree.worktreePath, "create-worktree"),
3671
2960
  ]).catch((error) => {
3672
2961
  this.sessionLogger.warn({ err: error, cwd: input.cwd, worktreePath: result.worktree.worktreePath }, "Failed to warm git snapshots after creating worktree");
3673
2962
  });
@@ -3691,10 +2980,7 @@ export class Session {
3691
2980
  workspaceRegistry: this.workspaceRegistry,
3692
2981
  });
3693
2982
  if (!existingWorkspace) {
3694
- const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
3695
- if (watchTarget) {
3696
- this.removeWorkspaceGitSubscription(watchTarget.cwd);
3697
- }
2983
+ this.workspaceGitObserver.removeForWorkspaceId(workspaceId);
3698
2984
  return;
3699
2985
  }
3700
2986
  if (!existingWorkspace.archivedAt) {
@@ -3716,9 +3002,8 @@ export class Session {
3716
3002
  // store is keyed by the opaque workspace id. Each cleanup uses its own key so an
3717
3003
  // opaque id is never resolved as a filesystem path.
3718
3004
  async teardownArchivedWorkspace(input) {
3719
- await this.removeWorkspaceGitWatchTarget(input.cwd);
3005
+ this.workspaceGitObserver.removeForCwd(input.cwd);
3720
3006
  this.scriptRuntimeStore?.removeForWorkspace(input.workspaceId);
3721
- this.removeWorkspaceGitSubscription(input.cwd);
3722
3007
  }
3723
3008
  async reconcileAndEmitWorkspaceUpdates() {
3724
3009
  if (!this.workspaceUpdatesSubscription) {
@@ -3793,10 +3078,10 @@ export class Session {
3793
3078
  ? workspace
3794
3079
  : null;
3795
3080
  if (options?.dedupeGitState &&
3796
- this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)) {
3081
+ this.workspaceGitObserver.shouldSkipUpdate(workspaceId, nextWorkspace)) {
3797
3082
  continue;
3798
3083
  }
3799
- this.recordWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
3084
+ this.workspaceGitObserver.recordDescriptorState(workspaceId, nextWorkspace);
3800
3085
  if (!nextWorkspace) {
3801
3086
  subscription.lastEmittedByWorkspaceId.delete(workspaceId);
3802
3087
  this.bufferOrEmitWorkspaceUpdate(subscription, await this.buildWorkspaceRemoveUpdatePayload(workspaceId, options?.removedProjectId));
@@ -3818,16 +3103,6 @@ export class Session {
3818
3103
  void this.reconcileAndEmitWorkspaceUpdates();
3819
3104
  }
3820
3105
  }
3821
- recordWorkspaceGitDescriptorState(workspaceId, nextWorkspace) {
3822
- const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
3823
- if (watchTarget && this.onBranchChanged) {
3824
- const newBranchName = nextWorkspace?.name ?? null;
3825
- if (newBranchName !== watchTarget.lastBranchName) {
3826
- this.onBranchChanged(workspaceId, watchTarget.lastBranchName, newBranchName);
3827
- }
3828
- }
3829
- this.rememberWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
3830
- }
3831
3106
  async buildWorkspaceRemoveUpdatePayload(workspaceId, removedProjectId) {
3832
3107
  if (removedProjectId) {
3833
3108
  return { kind: "remove", id: workspaceId, removedProjectId };
@@ -3876,12 +3151,10 @@ export class Session {
3876
3151
  const subscriptionId = resolveSubscriptionId(request.subscribe, requestedSubscriptionId);
3877
3152
  try {
3878
3153
  if (subscriptionId) {
3879
- this.agentUpdatesSubscription = {
3154
+ this.agentUpdates.beginSubscription({
3880
3155
  subscriptionId,
3881
3156
  filter: request.filter,
3882
- isBootstrapping: true,
3883
- pendingUpdatesByAgentId: new Map(),
3884
- };
3157
+ });
3885
3158
  }
3886
3159
  const payload = await this.listFetchAgentsEntries(request);
3887
3160
  const snapshotUpdatedAtByAgentId = new Map();
@@ -3899,13 +3172,13 @@ export class Session {
3899
3172
  ...payload,
3900
3173
  },
3901
3174
  });
3902
- if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3903
- this.flushBootstrappedAgentUpdates({ snapshotUpdatedAtByAgentId });
3175
+ if (subscriptionId) {
3176
+ this.agentUpdates.flushBootstrapped(subscriptionId, { snapshotUpdatedAtByAgentId });
3904
3177
  }
3905
3178
  }
3906
3179
  catch (error) {
3907
- if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3908
- this.agentUpdatesSubscription = null;
3180
+ if (subscriptionId) {
3181
+ this.agentUpdates.clearSubscription(subscriptionId);
3909
3182
  }
3910
3183
  const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
3911
3184
  const message = error instanceof Error ? error.message : "Failed to fetch agents";
@@ -4004,7 +3277,7 @@ export class Session {
4004
3277
  };
4005
3278
  }
4006
3279
  const payload = await this.listFetchWorkspacesEntries(request);
4007
- this.syncWorkspaceGitObservers(payload.entries);
3280
+ this.workspaceGitObserver.syncObservers(payload.entries);
4008
3281
  this.sessionLogger.debug({
4009
3282
  requestId: request.requestId,
4010
3283
  subscriptionId,
@@ -4246,7 +3519,7 @@ export class Session {
4246
3519
  for (const workspaceRecord of await this.workspaceRegistry.list()) {
4247
3520
  workspacesBefore.set(workspaceRecord.workspaceId, workspaceRecord);
4248
3521
  }
4249
- const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
3522
+ const workspace = await this.workspaceProvisioning.findOrCreateWorkspaceForDirectory(cwd);
4250
3523
  const project = await this.projectRegistry.get(workspace.projectId);
4251
3524
  await this.syncWorkspaceGitObserverForWorkspace(workspace);
4252
3525
  const descriptor = await this.describeWorkspaceRecord(workspace);
@@ -4315,7 +3588,7 @@ export class Session {
4315
3588
  for (const project of await this.projectRegistry.list()) {
4316
3589
  projectsBefore.set(project.projectId, project);
4317
3590
  }
4318
- const project = await this.findOrCreateProjectForDirectory(cwd);
3591
+ const project = await this.workspaceProvisioning.findOrCreateProjectForDirectory(cwd);
4319
3592
  this.sessionLogger.info({
4320
3593
  requestedCwd,
4321
3594
  resolvedCwd: cwd,
@@ -4345,98 +3618,13 @@ export class Session {
4345
3618
  });
4346
3619
  }
4347
3620
  }
3621
+ // Named accessor: the workspace descriptor builder and the git-watch test both read a workspace's
3622
+ // scripts snapshot through here; the workspace-scripts module owns the payload assembly.
4348
3623
  buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory) {
4349
- if (!this.serviceProxy || !this.scriptRuntimeStore) {
4350
- return [];
4351
- }
4352
- return buildWorkspaceScriptPayloads({
4353
- workspaceId,
4354
- workspaceDirectory,
4355
- paseoConfig: readPaseoConfigForProjection(workspaceDirectory, this.sessionLogger),
4356
- serviceProxy: this.serviceProxy,
4357
- runtimeStore: this.scriptRuntimeStore,
4358
- daemonPort: this.getDaemonTcpPort?.() ?? null,
4359
- serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
4360
- gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspaceDirectory),
4361
- resolveHealth: this.resolveScriptHealth ?? undefined,
4362
- });
3624
+ return this.workspaceScripts.buildSnapshot(workspaceId, workspaceDirectory);
4363
3625
  }
4364
- resolveWorkspaceScriptGitMetadata(workspaceDirectory) {
4365
- const snapshot = this.workspaceGitService.peekSnapshot(workspaceDirectory);
4366
- if (!snapshot) {
4367
- return undefined;
4368
- }
4369
- return {
4370
- projectSlug: deriveProjectSlug(workspaceDirectory, snapshot.git.isGit ? snapshot.git.remoteUrl : null),
4371
- currentBranch: snapshot.git.currentBranch,
4372
- };
4373
- }
4374
- emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory) {
4375
- this.emit({
4376
- type: "script_status_update",
4377
- payload: {
4378
- workspaceId,
4379
- scripts: this.buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory),
4380
- },
4381
- });
4382
- }
4383
- async handleStartWorkspaceScriptRequest(request) {
4384
- try {
4385
- if (!this.terminalManager || !this.serviceProxy || !this.scriptRuntimeStore) {
4386
- throw new Error("Workspace scripts are not available on this daemon");
4387
- }
4388
- const workspace = await this.workspaceRegistry.get(request.workspaceId);
4389
- if (!workspace) {
4390
- throw new Error(`Workspace not found: ${request.workspaceId}`);
4391
- }
4392
- const gitMetadata = await this.workspaceGitService.getWorkspaceGitMetadata(workspace.cwd);
4393
- const serviceResult = await spawnWorkspaceScript({
4394
- repoRoot: workspace.cwd,
4395
- workspaceId: workspace.workspaceId,
4396
- projectSlug: gitMetadata.projectSlug,
4397
- branchName: gitMetadata.currentBranch,
4398
- scriptName: request.scriptName,
4399
- daemonPort: this.getDaemonTcpPort?.() ?? null,
4400
- daemonListenHost: this.getDaemonTcpHost?.() ?? null,
4401
- serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
4402
- serviceProxy: this.serviceProxy,
4403
- runtimeStore: this.scriptRuntimeStore,
4404
- terminalManager: this.terminalManager,
4405
- logger: this.sessionLogger,
4406
- onLifecycleChanged: () => {
4407
- this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
4408
- },
4409
- });
4410
- this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
4411
- this.emit({
4412
- type: "start_workspace_script_response",
4413
- payload: {
4414
- requestId: request.requestId,
4415
- workspaceId: request.workspaceId,
4416
- scriptName: request.scriptName,
4417
- terminalId: serviceResult.terminalId,
4418
- error: null,
4419
- },
4420
- });
4421
- }
4422
- catch (error) {
4423
- const message = error instanceof Error ? error.message : "Failed to start workspace script";
4424
- this.sessionLogger.error({
4425
- err: error,
4426
- workspaceId: request.workspaceId,
4427
- scriptName: request.scriptName,
4428
- }, "Failed to start workspace script");
4429
- this.emit({
4430
- type: "start_workspace_script_response",
4431
- payload: {
4432
- requestId: request.requestId,
4433
- workspaceId: request.workspaceId,
4434
- scriptName: request.scriptName,
4435
- terminalId: null,
4436
- error: message,
4437
- },
4438
- });
4439
- }
3626
+ handleStartWorkspaceScriptRequest(request) {
3627
+ return this.workspaceScripts.start(request);
4440
3628
  }
4441
3629
  // COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
4442
3630
  async handleLegacyListAvailableEditorsRequest(request) {
@@ -4489,7 +3677,7 @@ export class Session {
4489
3677
  getDaemonTcpHost: this.getDaemonTcpHost,
4490
3678
  serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
4491
3679
  onScriptsChanged: (workspaceId, workspaceDirectory) => {
4492
- this.emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory);
3680
+ this.workspaceScripts.emitStatusUpdate(workspaceId, workspaceDirectory);
4493
3681
  },
4494
3682
  }, input, options);
4495
3683
  }
@@ -5097,29 +4285,16 @@ export class Session {
5097
4285
  this.unsubscribeAgentEvents();
5098
4286
  this.unsubscribeAgentEvents = null;
5099
4287
  }
4288
+ this.agentUpdates.dispose();
5100
4289
  if (this.unsubscribeTerminalWorkspaceContributionEvents) {
5101
4290
  this.unsubscribeTerminalWorkspaceContributionEvents();
5102
4291
  this.unsubscribeTerminalWorkspaceContributionEvents = null;
5103
4292
  }
5104
4293
  this.providerCatalogSession.dispose();
5105
4294
  await this.voiceSession.cleanup();
5106
- // Close MCP clients
5107
- if (this.agentMcpClient) {
5108
- try {
5109
- await this.agentMcpClient.close();
5110
- }
5111
- catch (error) {
5112
- this.sessionLogger.error({ err: error }, "Failed to close Agent MCP client");
5113
- }
5114
- this.agentMcpClient = null;
5115
- this.agentTools = null;
5116
- }
5117
4295
  this.terminalController.dispose();
5118
4296
  this.checkoutSession.cleanup();
5119
- for (const unsubscribe of this.workspaceGitSubscriptions.values()) {
5120
- unsubscribe();
5121
- }
5122
- this.workspaceGitSubscriptions.clear();
4297
+ this.workspaceGitObserver.dispose();
5123
4298
  }
5124
4299
  }
5125
4300
  //# sourceMappingURL=session.js.map