@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
@@ -90,13 +90,16 @@ export declare class VoiceAssistantWebSocketServer {
90
90
  private serverCapabilities;
91
91
  private readonly runtimeMetrics;
92
92
  private runtimeMetricsInterval;
93
+ private eventLoopDelayMonitor;
93
94
  private unsubscribeSpeechReadiness;
94
95
  private unsubscribeDaemonConfigChange;
96
+ private unsubscribeTerminalActivity;
95
97
  constructor(server: HTTPServer, logger: pino.Logger, serverId: string, agentManager: AgentManager, agentStorage: AgentStorage, downloadTokenStore: DownloadTokenStore, paseoHome: string, daemonConfigStore: DaemonConfigStore, mcpBaseUrl: string | null, wsConfig: WebSocketServerConfig, auth?: DaemonAuthConfig, speech?: SpeechService | null, terminalManager?: TerminalManager | null, dictation?: {
96
98
  finalTimeoutMs?: number;
97
99
  }, daemonVersion?: string, onLifecycleIntent?: (intent: SessionLifecycleIntent) => void, projectRegistry?: ProjectRegistry, workspaceRegistry?: WorkspaceRegistry, chatService?: FileBackedChatService, loopService?: LoopService, scheduleService?: ScheduleService, checkoutDiffManager?: CheckoutDiffManager, serviceProxy?: ServiceProxySubsystem | null, scriptRuntimeStore?: WorkspaceScriptRuntimeStore | null, onBranchChanged?: (workspaceId: string, oldBranch: string | null, newBranch: string | null) => void, getDaemonTcpPort?: () => number | null, getDaemonTcpHost?: () => string | null, resolveScriptHealth?: (hostname: string) => ScriptHealthState | null, workspaceGitService?: WorkspaceGitService, github?: GitHubService, pushNotificationSender?: PushNotificationSender, providerSnapshotManager?: ProviderSnapshotManager, daemonRuntimeConfig?: {
98
100
  listen: string | null;
99
101
  worktreesRoot?: string;
102
+ appBaseUrl?: string;
100
103
  relay: {
101
104
  enabled: boolean;
102
105
  endpoint: string;
@@ -108,6 +111,7 @@ export declare class VoiceAssistantWebSocketServer {
108
111
  private assignOptionalServices;
109
112
  private createWebSocketServer;
110
113
  private startRuntimeMetricsInterval;
114
+ private snapshotEventLoopDelay;
111
115
  private verifyWsUpgrade;
112
116
  private attachAuthenticatedSocket;
113
117
  broadcast(message: WSOutboundMessage): void;
@@ -150,6 +154,7 @@ export declare class VoiceAssistantWebSocketServer {
150
154
  private flushRuntimeMetrics;
151
155
  private getClientActivityState;
152
156
  private broadcastAgentAttention;
157
+ private broadcastTerminalAttention;
153
158
  }
154
159
  export {};
155
160
  //# sourceMappingURL=websocket-server.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { WebSocketServer } from "ws";
2
2
  import { basename, join } from "path";
3
3
  import { hostname as getHostname } from "node:os";
4
+ import { monitorEventLoopDelay } from "node:perf_hooks";
4
5
  import { WSInboundMessageSchema, wrapSessionMessage, } from "./messages.js";
5
6
  import { asUint8Array, decodeBinaryFrame } from "@getpaseo/protocol/binary-frames/index";
6
7
  import { isHostnameAllowed } from "./hostnames.js";
@@ -8,12 +9,26 @@ import { Session } from "./session.js";
8
9
  import { buildWorkspaceGitMetadataFromSnapshot } from "./workspace-git-metadata.js";
9
10
  import { PushTokenStore } from "./push/token-store.js";
10
11
  import { createPushNotificationSender } from "./push/notifications.js";
11
- import { computeNotificationPlan } from "./agent-attention-policy.js";
12
+ import { computeNotificationPlan, isPushEligibleAttentionReason, } from "./agent-attention-policy.js";
12
13
  import { buildAgentAttentionNotificationPayload, findLatestPermissionRequest, } from "@getpaseo/protocol/agent-attention-notification";
13
14
  import { createGitHubService } from "../services/github-service.js";
14
15
  import { extractWsBearerProtocol, extractWsBearerToken, isBearerTokenValid, } from "./auth.js";
15
16
  import { WebSocketRuntimeMetricsWindow, } from "./websocket/runtime-metrics.js";
16
17
  const WS_CLOSE_DAEMON_AUTH_FAILED = 4401;
18
+ function resolveTerminalAttentionReason(input) {
19
+ if (input.attentionReason === "finished")
20
+ return "finished";
21
+ if (input.attentionReason === "needs_input")
22
+ return "needs_input";
23
+ if (input.state === "attention")
24
+ return "needs_input";
25
+ if (input.previousState === "working" && input.state === "idle")
26
+ return "finished";
27
+ return null;
28
+ }
29
+ function terminalAttentionTitle(reason) {
30
+ return reason === "needs_input" ? "Terminal needs input" : "Terminal finished";
31
+ }
17
32
  function createFallbackWorkspaceGitSnapshot(cwd) {
18
33
  return {
19
34
  cwd,
@@ -213,8 +228,10 @@ export class VoiceAssistantWebSocketServer {
213
228
  this.workspaceSetupSnapshots = new Map();
214
229
  this.runtimeMetrics = new WebSocketRuntimeMetricsWindow();
215
230
  this.runtimeMetricsInterval = null;
231
+ this.eventLoopDelayMonitor = null;
216
232
  this.unsubscribeSpeechReadiness = null;
217
233
  this.unsubscribeDaemonConfigChange = null;
234
+ this.unsubscribeTerminalActivity = null;
218
235
  this.logger = logger.child({ module: "websocket-server" });
219
236
  this.serverId = serverId;
220
237
  if (typeof daemonVersion !== "string" || daemonVersion.trim().length === 0) {
@@ -288,6 +305,27 @@ export class VoiceAssistantWebSocketServer {
288
305
  assignOptionalServices(params) {
289
306
  this.speech = params.speech ?? null;
290
307
  this.terminalManager = params.terminalManager ?? null;
308
+ if (this.terminalManager) {
309
+ this.unsubscribeTerminalActivity = this.terminalManager.subscribeTerminalActivity((event) => {
310
+ const reason = resolveTerminalAttentionReason({
311
+ attentionReason: event.activity?.attentionReason,
312
+ previousState: event.previous?.state ?? null,
313
+ state: event.activity?.state ?? null,
314
+ });
315
+ if (!reason) {
316
+ return;
317
+ }
318
+ void this.broadcastTerminalAttention({
319
+ terminalId: event.terminalId,
320
+ cwd: event.cwd,
321
+ ...(event.workspaceId ? { workspaceId: event.workspaceId } : {}),
322
+ terminalName: event.name,
323
+ reason,
324
+ }).catch((err) => {
325
+ this.logger.warn({ err, terminalId: event.terminalId }, "Failed to broadcast terminal attention");
326
+ });
327
+ });
328
+ }
291
329
  this.dictation = params.dictation ?? null;
292
330
  this.onLifecycleIntent = params.onLifecycleIntent ?? null;
293
331
  this.serviceProxy = params.serviceProxy ?? null;
@@ -315,12 +353,30 @@ export class VoiceAssistantWebSocketServer {
315
353
  return wss;
316
354
  }
317
355
  startRuntimeMetricsInterval() {
356
+ this.eventLoopDelayMonitor = monitorEventLoopDelay({ resolution: 10 });
357
+ this.eventLoopDelayMonitor.enable();
318
358
  const runtimeMetricsInterval = setInterval(() => {
319
359
  this.flushRuntimeMetrics();
320
360
  }, WS_RUNTIME_METRICS_FLUSH_MS);
321
361
  this.runtimeMetricsInterval = runtimeMetricsInterval;
322
362
  runtimeMetricsInterval.unref?.();
323
363
  }
364
+ // Main-loop stall visibility: terminal frames and agent traffic share one event
365
+ // loop, so delay percentiles here are the ground truth for "the daemon is busy".
366
+ snapshotEventLoopDelay() {
367
+ const monitor = this.eventLoopDelayMonitor;
368
+ if (!monitor) {
369
+ return null;
370
+ }
371
+ const toMs = (nanoseconds) => Math.round(nanoseconds / 1e5) / 10;
372
+ const snapshot = {
373
+ p50Ms: toMs(monitor.percentile(50)),
374
+ p99Ms: toMs(monitor.percentile(99)),
375
+ maxMs: toMs(monitor.max),
376
+ };
377
+ monitor.reset();
378
+ return snapshot;
379
+ }
324
380
  verifyWsUpgrade(req, allowedOrigins, hostnames, callback) {
325
381
  const requestMetadata = extractSocketRequestMetadata(req);
326
382
  const origin = requestMetadata.origin;
@@ -393,11 +449,15 @@ export class VoiceAssistantWebSocketServer {
393
449
  this.unsubscribeSpeechReadiness = null;
394
450
  this.unsubscribeDaemonConfigChange?.();
395
451
  this.unsubscribeDaemonConfigChange = null;
452
+ this.unsubscribeTerminalActivity?.();
453
+ this.unsubscribeTerminalActivity = null;
396
454
  if (this.runtimeMetricsInterval) {
397
455
  clearInterval(this.runtimeMetricsInterval);
398
456
  this.runtimeMetricsInterval = null;
399
457
  }
400
458
  this.flushRuntimeMetrics({ final: true });
459
+ this.eventLoopDelayMonitor?.disable();
460
+ this.eventLoopDelayMonitor = null;
401
461
  const uniqueConnections = new Set([
402
462
  ...this.sessions.values(),
403
463
  ...this.externalSessionsByKey.values(),
@@ -548,6 +608,22 @@ export class VoiceAssistantWebSocketServer {
548
608
  }
549
609
  this.sendBinaryToConnection(connection, frame);
550
610
  },
611
+ getTransportBufferedAmount: () => {
612
+ if (!connection) {
613
+ return null;
614
+ }
615
+ // Relay-attached sockets are a WebSocketLike that doesn't expose
616
+ // bufferedAmount. Return null when no socket gives a signal so the
617
+ // terminal fallback can't mistake "no signal" for "client keeping up";
618
+ // a direct ws reports its real buffered bytes (0 when drained).
619
+ let maxBuffered = null;
620
+ for (const socket of connection.sockets) {
621
+ if (typeof socket.bufferedAmount === "number") {
622
+ maxBuffered = Math.max(maxBuffered ?? 0, socket.bufferedAmount);
623
+ }
624
+ }
625
+ return maxBuffered;
626
+ },
551
627
  onLifecycleIntent: (intent) => {
552
628
  this.onLifecycleIntent?.(intent);
553
629
  },
@@ -729,6 +805,8 @@ export class VoiceAssistantWebSocketServer {
729
805
  rewind: true,
730
806
  // COMPAT(checkoutRefresh): added in v0.1.86, remove gate after 2026-11-29.
731
807
  checkoutRefresh: true,
808
+ // COMPAT(workspaceMultiplicity): added in v0.1.97, drop the gate when floor >= v0.1.97
809
+ workspaceMultiplicity: true,
732
810
  },
733
811
  };
734
812
  }
@@ -1164,6 +1242,7 @@ export class VoiceAssistantWebSocketServer {
1164
1242
  outboundAgentStreamAgentsTop: runtimeMetrics.outboundAgentStreamAgentsTop,
1165
1243
  outboundBinaryFrameTypesTop: runtimeMetrics.outboundBinaryFrameTypesTop,
1166
1244
  bufferedAmount: runtimeMetrics.bufferedAmount,
1245
+ eventLoopDelay: this.snapshotEventLoopDelay(),
1167
1246
  runtime: sessionMetrics,
1168
1247
  latency: runtimeMetrics.latency,
1169
1248
  agents: agentSnapshot,
@@ -1175,12 +1254,14 @@ export class VoiceAssistantWebSocketServer {
1175
1254
  return {
1176
1255
  appVisible: false,
1177
1256
  focusedAgentId: null,
1257
+ focusedTerminalId: null,
1178
1258
  lastActivityAtMs: null,
1179
1259
  };
1180
1260
  }
1181
1261
  return {
1182
1262
  appVisible: activity.appVisible,
1183
1263
  focusedAgentId: activity.focusedAgentId,
1264
+ focusedTerminalId: activity.focusedTerminalId,
1184
1265
  lastActivityAtMs: activity.lastActivityAt.getTime(),
1185
1266
  };
1186
1267
  }
@@ -1205,8 +1286,8 @@ export class VoiceAssistantWebSocketServer {
1205
1286
  });
1206
1287
  const plan = computeNotificationPlan({
1207
1288
  allStates,
1208
- agentId: params.agentId,
1209
- reason: params.reason,
1289
+ focusTarget: { kind: "agent", id: params.agentId },
1290
+ pushEligible: isPushEligibleAttentionReason(params.reason),
1210
1291
  nowMs,
1211
1292
  });
1212
1293
  if (plan.shouldPush) {
@@ -1235,6 +1316,59 @@ export class VoiceAssistantWebSocketServer {
1235
1316
  this.sendToClient(ws, message);
1236
1317
  }
1237
1318
  }
1319
+ async broadcastTerminalAttention(params) {
1320
+ const clientEntries = [];
1321
+ for (const [ws, connection] of this.sessions) {
1322
+ clientEntries.push({
1323
+ ws,
1324
+ state: this.getClientActivityState(connection.session),
1325
+ });
1326
+ }
1327
+ const allStates = clientEntries.map((e) => e.state);
1328
+ const nowMs = Date.now();
1329
+ const workspaceId = params.workspaceId;
1330
+ const plan = computeNotificationPlan({
1331
+ allStates,
1332
+ focusTarget: { kind: "terminal", id: params.terminalId },
1333
+ pushEligible: true,
1334
+ nowMs,
1335
+ });
1336
+ const title = terminalAttentionTitle(params.reason);
1337
+ const body = params.terminalName;
1338
+ if (plan.shouldPush) {
1339
+ void this.pushNotificationSender
1340
+ .send({
1341
+ title,
1342
+ body,
1343
+ data: {
1344
+ serverId: this.serverId,
1345
+ terminalId: params.terminalId,
1346
+ cwd: params.cwd,
1347
+ ...(workspaceId ? { workspaceId } : {}),
1348
+ },
1349
+ })
1350
+ .catch((err) => {
1351
+ this.logger.warn({ err, terminalId: params.terminalId }, "Failed to send push notification");
1352
+ });
1353
+ }
1354
+ for (const [clientIndex, { ws }] of clientEntries.entries()) {
1355
+ const shouldNotify = clientIndex === plan.inAppRecipientIndex;
1356
+ const message = wrapSessionMessage({
1357
+ type: "terminal_attention_required",
1358
+ payload: {
1359
+ serverId: this.serverId,
1360
+ terminalId: params.terminalId,
1361
+ cwd: params.cwd,
1362
+ ...(workspaceId ? { workspaceId } : {}),
1363
+ reason: params.reason,
1364
+ title,
1365
+ body,
1366
+ shouldNotify,
1367
+ },
1368
+ });
1369
+ this.sendToClient(ws, message);
1370
+ }
1371
+ }
1238
1372
  }
1239
1373
  function extractSocketRequestMetadata(request) {
1240
1374
  if (!request || typeof request !== "object") {
@@ -1,8 +1,65 @@
1
- import type { PersistedWorkspaceRecord, ProjectRegistry, WorkspaceRegistry } from "./workspace-registry.js";
1
+ import type { Logger } from "pino";
2
+ import type { AgentManager } from "./agent/agent-manager.js";
3
+ import type { AgentStorage } from "./agent/agent-storage.js";
4
+ import type { WorkspaceGitService } from "./workspace-git-service.js";
5
+ import type { GitHubService } from "../services/github-service.js";
6
+ import type { TerminalManager } from "../terminal/terminal-manager.js";
7
+ import type { PersistedWorkspaceRecord, WorkspaceRegistry } from "./workspace-registry.js";
8
+ export interface ActiveWorkspaceRef {
9
+ workspaceId: string;
10
+ cwd: string;
11
+ kind?: "local_checkout" | "worktree" | "directory";
12
+ }
13
+ export interface ArchiveDependencies {
14
+ paseoHome?: string;
15
+ paseoWorktreesBaseRoot?: string;
16
+ github: GitHubService;
17
+ workspaceGitService: Pick<WorkspaceGitService, "getSnapshot">;
18
+ agentManager: Pick<AgentManager, "listAgents" | "archiveAgent" | "archiveSnapshot">;
19
+ agentStorage: Pick<AgentStorage, "list">;
20
+ findWorkspaceIdForCwd: (cwd: string) => Promise<string | null>;
21
+ listActiveWorkspaces: () => Promise<ActiveWorkspaceRef[]>;
22
+ archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
23
+ emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds: Iterable<string>) => Promise<void>;
24
+ markWorkspaceArchiving: (workspaceIds: Iterable<string>, archivingAt: string) => void;
25
+ clearWorkspaceArchiving: (workspaceIds: Iterable<string>) => void;
26
+ killTerminalsForWorkspace: (workspaceId: string) => Promise<void>;
27
+ sessionLogger?: Logger;
28
+ }
29
+ export interface KillTerminalsForWorkspaceDependencies {
30
+ detachTerminalStream?: (terminalId: string, options: {
31
+ emitExit: boolean;
32
+ }) => void;
33
+ sessionLogger: Logger;
34
+ terminalManager: TerminalManager | null;
35
+ }
36
+ export type ArchiveScope = {
37
+ kind: "workspace";
38
+ workspaceId: string;
39
+ } | {
40
+ kind: "worktree";
41
+ targetPath: string;
42
+ };
43
+ export interface ArchiveResult {
44
+ archivedAgentIds: string[];
45
+ archivedWorkspaceIds: string[];
46
+ removedDirectory: boolean;
47
+ }
48
+ export interface ArchiveByScopeRequest {
49
+ scope: ArchiveScope;
50
+ repoRoot: string | null;
51
+ repoWorktreesRoot?: string;
52
+ paseoWorktreesBaseRoot?: string;
53
+ requestId: string;
54
+ }
55
+ export declare function resolveWorkspaceIdAtPath(dependencies: Pick<ArchiveDependencies, "findWorkspaceIdForCwd" | "listActiveWorkspaces">, targetPath: string): Promise<string | null>;
56
+ export declare function archiveByScope(dependencies: ArchiveDependencies, request: ArchiveByScopeRequest): Promise<ArchiveResult>;
57
+ export type ArchiveWorkspaceContentsDependencies = Pick<ArchiveDependencies, "agentManager" | "agentStorage" | "killTerminalsForWorkspace" | "sessionLogger">;
58
+ export declare function archiveWorkspaceContents(dependencies: ArchiveWorkspaceContentsDependencies, workspaceId: string): Promise<Set<string>>;
59
+ export declare function killTerminalsForWorkspace(dependencies: KillTerminalsForWorkspaceDependencies, workspaceId: string): Promise<void>;
2
60
  export declare function archivePersistedWorkspaceRecord(input: {
3
61
  workspaceId: string;
4
- workspaceRegistry: Pick<WorkspaceRegistry, "get" | "list" | "archive">;
5
- projectRegistry: Pick<ProjectRegistry, "archive">;
62
+ workspaceRegistry: Pick<WorkspaceRegistry, "get" | "archive">;
6
63
  archivedAt?: string;
7
64
  }): Promise<PersistedWorkspaceRecord | null>;
8
65
  //# sourceMappingURL=workspace-archive-service.d.ts.map
@@ -1,3 +1,220 @@
1
+ import { resolve } from "node:path";
2
+ import { deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, WorktreeTeardownError, } from "../utils/worktree.js";
3
+ export async function resolveWorkspaceIdAtPath(dependencies, targetPath) {
4
+ const targetDir = resolve(targetPath);
5
+ const activeWorkspaces = await dependencies.listActiveWorkspaces();
6
+ const exactMatches = activeWorkspaces.filter((workspace) => resolve(workspace.cwd) === targetDir);
7
+ const worktreeMatch = exactMatches.find((workspace) => workspace.kind === "worktree");
8
+ if (worktreeMatch) {
9
+ return worktreeMatch.workspaceId;
10
+ }
11
+ return dependencies.findWorkspaceIdForCwd(targetPath);
12
+ }
13
+ // THE single archive entry. Resolves the in-scope record set, tears each down
14
+ // (agents + terminals + record), then removes the backing directory iff it is
15
+ // Paseo-owned AND no active workspace still references it.
16
+ export async function archiveByScope(dependencies, request) {
17
+ const { targetDir, targetWorkspaceIds } = await resolveArchiveTargets(dependencies, request.scope, request.paseoWorktreesBaseRoot);
18
+ if (targetWorkspaceIds.length > 0) {
19
+ dependencies.markWorkspaceArchiving(targetWorkspaceIds, new Date().toISOString());
20
+ }
21
+ let removedDirectory = false;
22
+ try {
23
+ if (targetWorkspaceIds.length > 0) {
24
+ await dependencies.emitWorkspaceUpdatesForWorkspaceIds(targetWorkspaceIds);
25
+ }
26
+ const { archivedAgents, archivedWorkspaceIds } = await archiveTargetRecords(dependencies, targetWorkspaceIds, request.requestId);
27
+ if (request.repoRoot) {
28
+ try {
29
+ await dependencies.workspaceGitService.getSnapshot(request.repoRoot, {
30
+ force: true,
31
+ reason: "archive-worktree",
32
+ });
33
+ }
34
+ catch (error) {
35
+ dependencies.sessionLogger?.warn({ err: error, cwd: request.repoRoot, requestId: request.requestId }, "Failed to force-refresh workspace git snapshot after archiving");
36
+ }
37
+ }
38
+ if (targetDir !== null) {
39
+ removedDirectory = await maybeRemoveDirectory(dependencies, request, targetDir, archivedWorkspaceIds);
40
+ }
41
+ return {
42
+ archivedAgentIds: Array.from(archivedAgents),
43
+ archivedWorkspaceIds,
44
+ removedDirectory,
45
+ };
46
+ }
47
+ finally {
48
+ if (targetWorkspaceIds.length > 0) {
49
+ dependencies.clearWorkspaceArchiving(targetWorkspaceIds);
50
+ await dependencies.emitWorkspaceUpdatesForWorkspaceIds(targetWorkspaceIds);
51
+ }
52
+ }
53
+ }
54
+ async function resolveArchiveTargets(dependencies, scope, paseoWorktreesBaseRoot) {
55
+ const activeWorkspaces = await dependencies.listActiveWorkspaces();
56
+ if (scope.kind === "workspace") {
57
+ const workspaceId = scope.workspaceId;
58
+ const record = activeWorkspaces.find((workspace) => workspace.workspaceId === workspaceId);
59
+ if (!record) {
60
+ dependencies.sessionLogger?.warn({ workspaceId }, "Workspace not found for archive-by-scope; skipping");
61
+ return { targetDir: null, targetWorkspaceIds: [] };
62
+ }
63
+ return { targetDir: resolve(record.cwd), targetWorkspaceIds: [workspaceId] };
64
+ }
65
+ let targetPath = scope.targetPath;
66
+ const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
67
+ paseoHome: dependencies.paseoHome,
68
+ worktreesRoot: paseoWorktreesBaseRoot ?? dependencies.paseoWorktreesBaseRoot,
69
+ });
70
+ if (resolvedWorktree) {
71
+ targetPath = resolvedWorktree.worktreePath;
72
+ }
73
+ const targetDir = resolve(targetPath);
74
+ const targetWorkspaceIds = activeWorkspaces
75
+ .filter((workspace) => resolve(workspace.cwd) === targetDir)
76
+ .map((workspace) => workspace.workspaceId);
77
+ return { targetDir, targetWorkspaceIds };
78
+ }
79
+ async function archiveTargetRecords(dependencies, targetWorkspaceIds, requestId) {
80
+ const archivedAgents = new Set();
81
+ const archivedWorkspaceIds = [];
82
+ const results = await Promise.allSettled(targetWorkspaceIds.map(async (workspaceId) => {
83
+ const agents = await archiveWorkspaceContents(dependencies, workspaceId);
84
+ await dependencies.archiveWorkspaceRecord(workspaceId);
85
+ return { workspaceId, agents };
86
+ }));
87
+ for (const result of results) {
88
+ if (result.status === "fulfilled") {
89
+ archivedWorkspaceIds.push(result.value.workspaceId);
90
+ for (const agentId of result.value.agents) {
91
+ archivedAgents.add(agentId);
92
+ }
93
+ }
94
+ else {
95
+ dependencies.sessionLogger?.warn({ err: result.reason, requestId }, "archiveByScope workspace teardown failed; continuing");
96
+ }
97
+ }
98
+ return { archivedAgents, archivedWorkspaceIds };
99
+ }
100
+ async function maybeRemoveDirectory(dependencies, request, targetDir, archivedWorkspaceIds) {
101
+ const ownership = await isPaseoOwnedWorktreeCwd(targetDir, {
102
+ paseoHome: dependencies.paseoHome,
103
+ worktreesRoot: request.paseoWorktreesBaseRoot ?? dependencies.paseoWorktreesBaseRoot,
104
+ });
105
+ if (!ownership.allowed) {
106
+ return false;
107
+ }
108
+ const remainingActive = await dependencies.listActiveWorkspaces();
109
+ if (!isDirectoryUnreferenced(remainingActive, targetDir, new Set(archivedWorkspaceIds))) {
110
+ return false;
111
+ }
112
+ try {
113
+ await deletePaseoWorktree({
114
+ cwd: request.repoRoot,
115
+ worktreePath: targetDir,
116
+ worktreesRoot: request.repoWorktreesRoot ?? ownership.worktreeRoot,
117
+ paseoHome: dependencies.paseoHome,
118
+ worktreesBaseRoot: request.paseoWorktreesBaseRoot ?? dependencies.paseoWorktreesBaseRoot,
119
+ });
120
+ dependencies.github.invalidate({ cwd: targetDir });
121
+ return true;
122
+ }
123
+ catch (error) {
124
+ if (error instanceof WorktreeTeardownError) {
125
+ dependencies.sessionLogger?.warn({ err: error, targetPath: targetDir, requestId: request.requestId }, "Worktree disk removal failed during archive; workspace already archived");
126
+ return false;
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+ // Tears down everything OWNED by a single workspace record: its live agents,
132
+ // its persisted-but-not-running agent snapshots, and its terminals. Scoped by
133
+ // workspaceId so a sibling workspace sharing the same directory is untouched.
134
+ // Returns the set of archived agent ids.
135
+ export async function archiveWorkspaceContents(dependencies, workspaceId) {
136
+ const archivedAgents = new Set();
137
+ const liveAgents = dependencies.agentManager
138
+ .listAgents()
139
+ .filter((agent) => agent.workspaceId === workspaceId);
140
+ for (const agent of liveAgents) {
141
+ archivedAgents.add(agent.id);
142
+ }
143
+ let storedRecords = [];
144
+ try {
145
+ storedRecords = await dependencies.agentStorage.list();
146
+ }
147
+ catch (error) {
148
+ dependencies.sessionLogger?.warn({ err: error, workspaceId }, "Failed to list stored agents during workspace archive; continuing");
149
+ }
150
+ const liveAgentIds = new Set(liveAgents.map((agent) => agent.id));
151
+ const matchingStoredRecords = storedRecords.filter((record) => record.workspaceId === workspaceId);
152
+ for (const record of matchingStoredRecords) {
153
+ archivedAgents.add(record.id);
154
+ }
155
+ const archivedAt = new Date().toISOString();
156
+ const archiveResults = await Promise.allSettled([
157
+ ...liveAgents.map((agent) => dependencies.agentManager.archiveAgent(agent.id)),
158
+ ...matchingStoredRecords
159
+ .filter((record) => !liveAgentIds.has(record.id) && !record.archivedAt)
160
+ .map((record) => dependencies.agentManager.archiveSnapshot(record.id, archivedAt)),
161
+ dependencies.killTerminalsForWorkspace(workspaceId),
162
+ ]);
163
+ for (const result of archiveResults) {
164
+ if (result.status === "rejected") {
165
+ dependencies.sessionLogger?.warn({ err: result.reason, workspaceId }, "Workspace archive teardown step failed; continuing");
166
+ }
167
+ }
168
+ return archivedAgents;
169
+ }
170
+ // EXACTLY one last-reference predicate in the module. True when, after archiving
171
+ // the in-scope records, no active workspace still points at targetDir. Derived
172
+ // from records each call — no stored counter.
173
+ function isDirectoryUnreferenced(activeWorkspaces, targetDir, archivedWorkspaceIds) {
174
+ const target = resolve(targetDir);
175
+ return !activeWorkspaces.some((workspace) => !archivedWorkspaceIds.has(workspace.workspaceId) && resolve(workspace.cwd) === target);
176
+ }
177
+ export async function killTerminalsForWorkspace(dependencies, workspaceId) {
178
+ const terminalManager = dependencies.terminalManager;
179
+ if (!terminalManager) {
180
+ return;
181
+ }
182
+ const terminalIds = [];
183
+ const terminalLists = await Promise.all(terminalManager.listDirectories().map(async (terminalCwd) => {
184
+ try {
185
+ return await terminalManager.getTerminals(terminalCwd, { workspaceId });
186
+ }
187
+ catch (error) {
188
+ dependencies.sessionLogger.warn({ err: error, cwd: terminalCwd }, "Failed to enumerate workspace terminals during archive");
189
+ return [];
190
+ }
191
+ }));
192
+ for (const terminals of terminalLists) {
193
+ for (const terminal of terminals) {
194
+ if (terminal.workspaceId === workspaceId) {
195
+ terminalIds.push(terminal.id);
196
+ }
197
+ }
198
+ }
199
+ if (terminalIds.length === 0) {
200
+ return;
201
+ }
202
+ await Promise.allSettled(terminalIds.map(async (terminalId) => {
203
+ try {
204
+ dependencies.detachTerminalStream?.(terminalId, { emitExit: true });
205
+ await terminalManager.killTerminalAndWait(terminalId, {
206
+ gracefulTimeoutMs: 2000,
207
+ forceTimeoutMs: 1500,
208
+ });
209
+ }
210
+ catch (error) {
211
+ dependencies.sessionLogger.warn({ err: error, terminalId }, "Terminal kill escalation failed during archive; proceeding anyway");
212
+ }
213
+ }));
214
+ }
215
+ // Archiving the last workspace of a project leaves the project as a first-class
216
+ // empty project — it persists until explicitly removed, so we never archive the
217
+ // parent project here.
1
218
  export async function archivePersistedWorkspaceRecord(input) {
2
219
  const existingWorkspace = await input.workspaceRegistry.get(input.workspaceId);
3
220
  if (!existingWorkspace) {
@@ -8,10 +225,6 @@ export async function archivePersistedWorkspaceRecord(input) {
8
225
  }
9
226
  const archivedAt = input.archivedAt ?? new Date().toISOString();
10
227
  await input.workspaceRegistry.archive(input.workspaceId, archivedAt);
11
- const activeSiblings = (await input.workspaceRegistry.list()).filter((workspace) => workspace.projectId === existingWorkspace.projectId && !workspace.archivedAt);
12
- if (activeSiblings.length === 0) {
13
- await input.projectRegistry.archive(existingWorkspace.projectId, archivedAt);
14
- }
15
228
  return existingWorkspace;
16
229
  }
17
230
  //# sourceMappingURL=workspace-archive-service.js.map
@@ -1,6 +1,7 @@
1
1
  import type pino from "pino";
2
2
  import type { AgentSnapshotPayload, SessionInboundMessage, SessionOutboundMessage, WorkspaceDescriptorPayload } from "./messages.js";
3
3
  import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
4
+ import { type TerminalActivity } from "@getpaseo/protocol/terminal-activity";
4
5
  type FetchWorkspacesRequestMessage = Extract<SessionInboundMessage, {
5
6
  type: "fetch_workspaces_request";
6
7
  }>;
@@ -10,6 +11,7 @@ type FetchWorkspacesResponsePayload = Extract<SessionOutboundMessage, {
10
11
  }>["payload"];
11
12
  type FetchWorkspacesResponseEntry = FetchWorkspacesResponsePayload["entries"][number];
12
13
  type FetchWorkspacesResponsePageInfo = FetchWorkspacesResponsePayload["pageInfo"];
14
+ type WorkspaceProjectDescriptor = FetchWorkspacesResponsePayload["emptyProjects"][number];
13
15
  export type WorkspaceUpdatesFilter = FetchWorkspacesRequestFilter;
14
16
  export interface WorkspaceDirectoryDeps {
15
17
  logger: pino.Logger;
@@ -20,6 +22,11 @@ export interface WorkspaceDirectoryDeps {
20
22
  list(): Promise<PersistedWorkspaceRecord[]>;
21
23
  };
22
24
  listAgentPayloads(): Promise<AgentSnapshotPayload[]>;
25
+ listTerminalActivityContributions(): Promise<Array<{
26
+ cwd: string;
27
+ workspaceId?: string;
28
+ activity: TerminalActivity | null;
29
+ }>>;
23
30
  isProviderVisibleToClient(provider: string): boolean;
24
31
  buildWorkspaceDescriptor(input: {
25
32
  workspace: PersistedWorkspaceRecord;
@@ -41,6 +48,14 @@ export declare function summarizeFetchWorkspacesEntries(entries: Iterable<FetchW
41
48
  activityAt: string | null;
42
49
  }>;
43
50
  };
51
+ /**
52
+ * Git facts (branch, diff, dirty, PR) belong to a checkout on disk, not to a
53
+ * workspace identity. Every workspace whose own cwd is that checkout re-derives
54
+ * its git facts from the same folder. This returns the ids of those workspaces
55
+ * so a git change can fan out to all of them. This is git-fact display, NOT
56
+ * ownership: do not use it to decide which workspace owns an arbitrary path.
57
+ */
58
+ export declare function workspaceIdsOnCheckout(workspaces: Iterable<PersistedWorkspaceRecord>, cwd: string): string[];
44
59
  export declare class WorkspaceDirectory {
45
60
  private readonly deps;
46
61
  private readonly archivingByWorkspaceId;
@@ -58,9 +73,11 @@ export declare class WorkspaceDirectory {
58
73
  includeGitData: boolean;
59
74
  workspaceIds?: Iterable<string>;
60
75
  }): Promise<Map<string, WorkspaceDescriptorPayload>>;
76
+ private applyAgentBucketContributions;
77
+ private applyTerminalContributions;
61
78
  private resolveStatusEnteredAt;
62
- private findNewestAgentTimestampInBucket;
63
- resolveRegisteredWorkspaceIdForCwd(cwd: string, workspaces: PersistedWorkspaceRecord[]): string;
79
+ private findNewestTimestampInBucket;
80
+ listEmptyProjects(): Promise<WorkspaceProjectDescriptor[]>;
64
81
  listDescriptors(): Promise<WorkspaceDescriptorPayload[]>;
65
82
  matchesFilter(input: {
66
83
  workspace: WorkspaceDescriptorPayload;
@@ -68,6 +85,7 @@ export declare class WorkspaceDirectory {
68
85
  }): boolean;
69
86
  listFetchEntries(request: FetchWorkspacesRequestMessage): Promise<{
70
87
  entries: FetchWorkspacesResponseEntry[];
88
+ emptyProjects: WorkspaceProjectDescriptor[];
71
89
  pageInfo: FetchWorkspacesResponsePageInfo;
72
90
  }>;
73
91
  }