@getpaseo/server 0.1.88 → 0.1.90

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 (94) hide show
  1. package/dist/server/server/agent/agent-manager.js +4 -1
  2. package/dist/server/server/agent/agent-prompt.js +4 -1
  3. package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
  4. package/dist/server/server/agent/agent-storage.d.ts +22 -22
  5. package/dist/server/server/agent/agent-storage.js +2 -9
  6. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  7. package/dist/server/server/agent/create-agent/create.js +26 -7
  8. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
  9. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
  10. package/dist/server/server/agent/create-agent-mode.d.ts +3 -8
  11. package/dist/server/server/agent/create-agent-mode.js +16 -2
  12. package/dist/server/server/agent/import-sessions.js +1 -1
  13. package/dist/server/server/agent/mcp-server.d.ts +1 -0
  14. package/dist/server/server/agent/mcp-server.js +113 -70
  15. package/dist/server/server/agent/provider-snapshot-manager.d.ts +2 -1
  16. package/dist/server/server/agent/provider-snapshot-manager.js +18 -2
  17. package/dist/server/server/agent/providers/acp-agent.d.ts +3 -3
  18. package/dist/server/server/agent/providers/acp-agent.js +18 -13
  19. package/dist/server/server/agent/providers/codex-app-server-agent.js +16 -22
  20. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
  21. package/dist/server/server/agent/providers/mock-load-test-agent.js +69 -2
  22. package/dist/server/server/agent/providers/opencode-agent.js +19 -8
  23. package/dist/server/server/agent/providers/pi/agent.js +13 -0
  24. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
  25. package/dist/server/server/agent/timeline-projection.js +30 -1
  26. package/dist/server/server/atomic-file.d.ts +3 -0
  27. package/dist/server/server/atomic-file.js +19 -0
  28. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
  29. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +10 -2
  30. package/dist/server/server/bootstrap.d.ts +7 -2
  31. package/dist/server/server/bootstrap.js +154 -115
  32. package/dist/server/server/chat/chat-service.js +2 -4
  33. package/dist/server/server/config.js +41 -0
  34. package/dist/server/server/daemon-keypair.js +2 -2
  35. package/dist/server/server/loop-service.d.ts +26 -22
  36. package/dist/server/server/loop-service.js +27 -9
  37. package/dist/server/server/package-version.d.ts +2 -2
  38. package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
  39. package/dist/server/server/paseo-worktree-archive-service.js +28 -9
  40. package/dist/server/server/persisted-config.d.ts +84 -28
  41. package/dist/server/server/persisted-config.js +20 -3
  42. package/dist/server/server/pid-lock.d.ts +2 -2
  43. package/dist/server/server/private-files.d.ts +0 -1
  44. package/dist/server/server/private-files.js +0 -5
  45. package/dist/server/server/schedule/service.d.ts +6 -0
  46. package/dist/server/server/schedule/service.js +41 -18
  47. package/dist/server/server/schedule/store.js +3 -2
  48. package/dist/server/server/script-health-monitor.d.ts +4 -4
  49. package/dist/server/server/script-health-monitor.js +6 -6
  50. package/dist/server/server/script-proxy.d.ts +2 -39
  51. package/dist/server/server/script-proxy.js +1 -244
  52. package/dist/server/server/script-route-branch-handler.d.ts +2 -2
  53. package/dist/server/server/script-route-branch-handler.js +3 -37
  54. package/dist/server/server/script-status-projection.d.ts +6 -4
  55. package/dist/server/server/script-status-projection.js +85 -37
  56. package/dist/server/server/server-id.js +3 -3
  57. package/dist/server/server/service-proxy.d.ts +237 -0
  58. package/dist/server/server/service-proxy.js +714 -0
  59. package/dist/server/server/session.d.ts +12 -18
  60. package/dist/server/server/session.js +206 -117
  61. package/dist/server/server/speech/providers/local/worker-client.js +1 -11
  62. package/dist/server/server/websocket-server.d.ts +7 -4
  63. package/dist/server/server/websocket-server.js +9 -4
  64. package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
  65. package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
  66. package/dist/server/server/workspace-directory.d.ts +8 -0
  67. package/dist/server/server/workspace-directory.js +141 -11
  68. package/dist/server/server/workspace-git-service.d.ts +3 -0
  69. package/dist/server/server/workspace-git-service.js +53 -12
  70. package/dist/server/server/workspace-registry.d.ts +2 -2
  71. package/dist/server/server/workspace-registry.js +2 -6
  72. package/dist/server/server/workspace-service-env.d.ts +1 -0
  73. package/dist/server/server/workspace-service-env.js +23 -18
  74. package/dist/server/server/worktree/commands.d.ts +2 -0
  75. package/dist/server/server/worktree/commands.js +4 -1
  76. package/dist/server/server/worktree-bootstrap.d.ts +4 -3
  77. package/dist/server/server/worktree-bootstrap.js +14 -13
  78. package/dist/server/server/worktree-core.d.ts +1 -0
  79. package/dist/server/server/worktree-core.js +2 -0
  80. package/dist/server/server/worktree-session.d.ts +6 -2
  81. package/dist/server/server/worktree-session.js +3 -0
  82. package/dist/server/services/github-service.d.ts +1 -0
  83. package/dist/server/services/github-service.js +7 -1
  84. package/dist/server/utils/checkout-git.d.ts +6 -3
  85. package/dist/server/utils/checkout-git.js +40 -38
  86. package/dist/server/utils/worktree.d.ts +17 -12
  87. package/dist/server/utils/worktree.js +39 -22
  88. package/dist/src/server/persisted-config.js +20 -3
  89. package/dist/src/server/private-files.js +0 -5
  90. package/package.json +9 -7
  91. package/dist/server/server/editor-targets.d.ts +0 -18
  92. package/dist/server/server/editor-targets.js +0 -109
  93. package/dist/server/utils/script-hostname.d.ts +0 -8
  94. package/dist/server/utils/script-hostname.js +0 -14
@@ -202,7 +202,7 @@ function requireWebSocketServices(params) {
202
202
  * WebSocket server that only accepts sockets + parses/forwards messages to the session layer.
203
203
  */
204
204
  export class VoiceAssistantWebSocketServer {
205
- constructor(server, logger, serverId, agentManager, agentStorage, downloadTokenStore, paseoHome, daemonConfigStore, mcpBaseUrl, wsConfig, auth, speech, terminalManager, dictation, daemonVersion, onLifecycleIntent, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager, scriptRouteStore, scriptRuntimeStore, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, resolveScriptHealth, workspaceGitService, github, pushNotificationSender, providerSnapshotManager, daemonRuntimeConfig) {
205
+ constructor(server, logger, serverId, agentManager, agentStorage, downloadTokenStore, paseoHome, daemonConfigStore, mcpBaseUrl, wsConfig, auth, speech, terminalManager, dictation, daemonVersion, onLifecycleIntent, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager, serviceProxy, scriptRuntimeStore, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, resolveScriptHealth, workspaceGitService, github, pushNotificationSender, providerSnapshotManager, daemonRuntimeConfig, serviceProxyPublicBaseUrl) {
206
206
  this.pendingConnections = new Map();
207
207
  this.sessions = new Map();
208
208
  this.externalSessionsByKey = new Map();
@@ -238,6 +238,7 @@ export class VoiceAssistantWebSocketServer {
238
238
  this.workspaceGitService = workspaceGitService ?? createFallbackWorkspaceGitService();
239
239
  this.downloadTokenStore = downloadTokenStore;
240
240
  this.paseoHome = paseoHome;
241
+ this.worktreesRoot = daemonRuntimeConfig?.worktreesRoot;
241
242
  this.daemonConfigStore = daemonConfigStore;
242
243
  this.mcpBaseUrl = mcpBaseUrl;
243
244
  this.assignOptionalServices({
@@ -245,11 +246,12 @@ export class VoiceAssistantWebSocketServer {
245
246
  terminalManager,
246
247
  dictation,
247
248
  onLifecycleIntent,
248
- scriptRouteStore,
249
+ serviceProxy,
249
250
  scriptRuntimeStore,
250
251
  onBranchChanged,
251
252
  getDaemonTcpPort,
252
253
  getDaemonTcpHost,
254
+ serviceProxyPublicBaseUrl,
253
255
  resolveScriptHealth,
254
256
  });
255
257
  if (!providerSnapshotManager) {
@@ -286,11 +288,12 @@ export class VoiceAssistantWebSocketServer {
286
288
  this.terminalManager = params.terminalManager ?? null;
287
289
  this.dictation = params.dictation ?? null;
288
290
  this.onLifecycleIntent = params.onLifecycleIntent ?? null;
289
- this.scriptRouteStore = params.scriptRouteStore ?? null;
291
+ this.serviceProxy = params.serviceProxy ?? null;
290
292
  this.scriptRuntimeStore = params.scriptRuntimeStore ?? null;
291
293
  this.onBranchChanged = params.onBranchChanged ?? null;
292
294
  this.getDaemonTcpPort = params.getDaemonTcpPort ?? null;
293
295
  this.getDaemonTcpHost = params.getDaemonTcpHost ?? null;
296
+ this.serviceProxyPublicBaseUrl = params.serviceProxyPublicBaseUrl ?? null;
294
297
  this.resolveScriptHealth = params.resolveScriptHealth ?? null;
295
298
  }
296
299
  createWebSocketServer(server, wsConfig, auth) {
@@ -550,6 +553,7 @@ export class VoiceAssistantWebSocketServer {
550
553
  downloadTokenStore: this.downloadTokenStore,
551
554
  pushTokenStore: this.pushTokenStore,
552
555
  paseoHome: this.paseoHome,
556
+ worktreesRoot: this.worktreesRoot,
553
557
  agentManager: this.agentManager,
554
558
  agentStorage: this.agentStorage,
555
559
  projectRegistry: this.projectRegistry,
@@ -567,12 +571,13 @@ export class VoiceAssistantWebSocketServer {
567
571
  tts: () => this.speech?.resolveTts() ?? null,
568
572
  terminalManager: this.terminalManager,
569
573
  providerSnapshotManager: this.providerSnapshotManager,
570
- scriptRouteStore: this.scriptRouteStore ?? undefined,
574
+ serviceProxy: this.serviceProxy ?? undefined,
571
575
  scriptRuntimeStore: this.scriptRuntimeStore ?? undefined,
572
576
  workspaceSetupSnapshots: this.workspaceSetupSnapshots,
573
577
  onBranchChanged: this.onBranchChanged ?? undefined,
574
578
  getDaemonTcpPort: this.getDaemonTcpPort ?? undefined,
575
579
  getDaemonTcpHost: this.getDaemonTcpHost ?? undefined,
580
+ serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
576
581
  resolveScriptHealth: this.resolveScriptHealth ?? undefined,
577
582
  voice: {
578
583
  turnDetection: () => this.speech?.resolveTurnDetection() ?? null,
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pure dedupe decision for the bootstrap flush.
3
+ *
4
+ * During the bootstrap window the session buffers workspace updates that
5
+ * race with the initial `fetch_workspaces_response`. The flush step decides
6
+ * which buffered updates still carry new information and which are
7
+ * redundant with what the client just received in the snapshot.
8
+ *
9
+ * Returns `true` (emit) when ANY of:
10
+ * - the status changed from the snapshot
11
+ * - the statusEnteredAt changed from the snapshot (including the
12
+ * null↔value transition that the unmask case produces)
13
+ * - the update's activityAtMs is strictly newer than the snapshot's
14
+ * - the snapshot has no activityAtMs and the update has one (new activity
15
+ * where there was none)
16
+ *
17
+ * Returns `false` (drop) when the status pair is unchanged AND the update
18
+ * is not strictly newer than the snapshot in activity. The both-null
19
+ * activity case falls through to drop — there is genuinely no new info.
20
+ */
21
+ export interface BootstrapUpdateSnapshot {
22
+ status: string;
23
+ statusEnteredAt: string | null;
24
+ activityAtMs: number | null;
25
+ }
26
+ export interface BootstrapUpdateCheckInput {
27
+ /** Snapshot captured from the fetch_workspaces_response. `null` means
28
+ * the workspace was not in the snapshot (first-time subscription). */
29
+ snapshot: BootstrapUpdateSnapshot | null;
30
+ /** Pending update buffered during the bootstrap window. */
31
+ update: BootstrapUpdateSnapshot;
32
+ }
33
+ export declare function shouldEmitPendingBootstrapUpdate(input: BootstrapUpdateCheckInput): boolean;
34
+ //# sourceMappingURL=workspace-bootstrap-dedupe.d.ts.map
@@ -0,0 +1,23 @@
1
+ export function shouldEmitPendingBootstrapUpdate(input) {
2
+ const { snapshot, update } = input;
3
+ if (!snapshot) {
4
+ return true;
5
+ }
6
+ if (snapshot.status !== update.status) {
7
+ return true;
8
+ }
9
+ const snapshotEnteredAt = snapshot.statusEnteredAt ?? null;
10
+ const updateEnteredAt = update.statusEnteredAt ?? null;
11
+ if (snapshotEnteredAt !== updateEnteredAt) {
12
+ return true;
13
+ }
14
+ // Status pair is unchanged. The only remaining signal is activity.
15
+ if (update.activityAtMs === null) {
16
+ return false;
17
+ }
18
+ if (snapshot.activityAtMs === null) {
19
+ return true;
20
+ }
21
+ return update.activityAtMs > snapshot.activityAtMs;
22
+ }
23
+ //# sourceMappingURL=workspace-bootstrap-dedupe.js.map
@@ -44,6 +44,12 @@ export declare function summarizeFetchWorkspacesEntries(entries: Iterable<FetchW
44
44
  export declare class WorkspaceDirectory {
45
45
  private readonly deps;
46
46
  private readonly archivingByWorkspaceId;
47
+ /**
48
+ * Per-workspace last-seen winning bucket + entered-at. Persists across
49
+ * `buildDescriptorMap` calls inside the daemon process; reset on cold start.
50
+ * Server-internal; never crosses the wire.
51
+ */
52
+ private readonly bucketHistoryByWorkspaceId;
47
53
  private readonly pager;
48
54
  constructor(deps: WorkspaceDirectoryDeps);
49
55
  markArchiving(workspaceIds: Iterable<string>, archivingAt: string): void;
@@ -52,6 +58,8 @@ export declare class WorkspaceDirectory {
52
58
  includeGitData: boolean;
53
59
  workspaceIds?: Iterable<string>;
54
60
  }): Promise<Map<string, WorkspaceDescriptorPayload>>;
61
+ private resolveStatusEnteredAt;
62
+ private findNewestAgentTimestampInBucket;
55
63
  resolveRegisteredWorkspaceIdForCwd(cwd: string, workspaces: PersistedWorkspaceRecord[]): string;
56
64
  listDescriptors(): Promise<WorkspaceDescriptorPayload[]>;
57
65
  matchesFilter(input: {
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { sep } from "node:path";
3
3
  import { deriveAgentStateBucket, getWorkspaceStateBucketPriority, } from "@getpaseo/protocol/agent-state-bucket";
4
+ import { getParentAgentIdFromLabels, isDelegatedAgent } from "@getpaseo/protocol/agent-labels";
4
5
  import { SortablePager } from "./pagination/sortable-pager.js";
5
6
  import { normalizeWorkspaceId } from "./workspace-registry-model.js";
6
7
  const FETCH_WORKSPACES_SORT_KEYS = [
@@ -34,6 +35,12 @@ export class WorkspaceDirectory {
34
35
  constructor(deps) {
35
36
  this.deps = deps;
36
37
  this.archivingByWorkspaceId = new Map();
38
+ /**
39
+ * Per-workspace last-seen winning bucket + entered-at. Persists across
40
+ * `buildDescriptorMap` calls inside the daemon process; reset on cold start.
41
+ * Server-internal; never crosses the wire.
42
+ */
43
+ this.bucketHistoryByWorkspaceId = new Map();
37
44
  this.pager = new SortablePager({
38
45
  validKeys: FETCH_WORKSPACES_SORT_KEYS,
39
46
  defaultSort: [{ key: "activity_at", direction: "desc" }],
@@ -92,14 +99,31 @@ export class WorkspaceDirectory {
92
99
  archivingAt: this.archivingByWorkspaceId.get(workspaceId) ?? null,
93
100
  });
94
101
  }
95
- for (const agent of agents) {
96
- if (agent.archivedAt) {
97
- continue;
102
+ const activeAgents = agents.filter((agent) => !agent.archivedAt && this.deps.isProviderVisibleToClient(agent.provider));
103
+ const activeAgentsById = new Map(activeAgents.map((agent) => [agent.id, agent]));
104
+ for (const agent of activeAgents) {
105
+ let workspaceAgent = agent;
106
+ let bucket;
107
+ if (isDelegatedAgent(agent)) {
108
+ if (agent.status !== "running") {
109
+ continue;
110
+ }
111
+ const parentAgent = resolveDelegationRootAgent(agent, activeAgentsById);
112
+ if (!parentAgent) {
113
+ continue;
114
+ }
115
+ workspaceAgent = parentAgent;
116
+ bucket = "running";
98
117
  }
99
- if (!this.deps.isProviderVisibleToClient(agent.provider)) {
100
- continue;
118
+ else {
119
+ bucket = deriveAgentStateBucket({
120
+ status: agent.status,
121
+ pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
122
+ requiresAttention: agent.requiresAttention,
123
+ attentionReason: agent.attentionReason ?? null,
124
+ });
101
125
  }
102
- const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd));
126
+ const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(workspaceAgent.cwd));
103
127
  if (workspaceId === undefined) {
104
128
  continue;
105
129
  }
@@ -107,17 +131,104 @@ export class WorkspaceDirectory {
107
131
  if (!existing) {
108
132
  continue;
109
133
  }
110
- const bucket = deriveAgentStateBucket({
134
+ if (getWorkspaceStateBucketPriority(bucket) < getWorkspaceStateBucketPriority(existing.status)) {
135
+ existing.status = bucket;
136
+ }
137
+ }
138
+ // Resolve the workspace-level `statusEnteredAt` (see aggregate semantics
139
+ // on `resolveStatusEnteredAt`).
140
+ const nowIso = new Date().toISOString();
141
+ for (const [workspaceId, descriptor] of descriptorsByWorkspaceId) {
142
+ const contributingAgents = agents.filter((agent) => !agent.archivedAt &&
143
+ this.deps.isProviderVisibleToClient(agent.provider) &&
144
+ workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd)) === workspaceId);
145
+ const result = this.resolveStatusEnteredAt({
146
+ workspaceId,
147
+ winningBucket: descriptor.status,
148
+ contributingAgents,
149
+ previous: this.bucketHistoryByWorkspaceId.get(workspaceId) ?? null,
150
+ nowIso,
151
+ });
152
+ descriptor.statusEnteredAt = result.statusEnteredAt;
153
+ if (result.recordUpdate) {
154
+ this.bucketHistoryByWorkspaceId.set(workspaceId, result.recordUpdate);
155
+ }
156
+ else if (result.recordDelete) {
157
+ this.bucketHistoryByWorkspaceId.delete(workspaceId);
158
+ }
159
+ }
160
+ return descriptorsByWorkspaceId;
161
+ }
162
+ // Aggregate the workspace-level `statusEnteredAt` from its contributing
163
+ // agents. Aggregate semantics:
164
+ // - winning bucket = highest-priority across contributing agents;
165
+ // - entry time = best-effort timestamp from agents in the winning bucket;
166
+ // - priority unmasking: when the winning bucket transitions (e.g. a
167
+ // higher-priority bucket cleared), the new entry time is "now";
168
+ // - same-bucket emits reuse the previous entered-at;
169
+ // - empty workspaces that never had contributing agents get
170
+ // `statusEnteredAt: null`.
171
+ // - when archived agents leave a previously active workspace empty, keep
172
+ // the previous done timestamp or stamp the transition to done now.
173
+ resolveStatusEnteredAt(params) {
174
+ const { winningBucket, contributingAgents, previous, nowIso } = params;
175
+ if (contributingAgents.length === 0) {
176
+ if (!previous) {
177
+ return { statusEnteredAt: null };
178
+ }
179
+ const enteredAt = previous.bucket === "done" ? previous.enteredAt : nowIso;
180
+ return {
181
+ statusEnteredAt: enteredAt,
182
+ recordUpdate: { bucket: "done", enteredAt },
183
+ };
184
+ }
185
+ if (!previous) {
186
+ const newestInWinningBucket = this.findNewestAgentTimestampInBucket(contributingAgents, winningBucket);
187
+ const enteredAt = newestInWinningBucket ?? nowIso;
188
+ return {
189
+ statusEnteredAt: enteredAt,
190
+ recordUpdate: { bucket: winningBucket, enteredAt },
191
+ };
192
+ }
193
+ if (previous.bucket !== winningBucket) {
194
+ return {
195
+ statusEnteredAt: nowIso,
196
+ recordUpdate: { bucket: winningBucket, enteredAt: nowIso },
197
+ };
198
+ }
199
+ return {
200
+ statusEnteredAt: previous.enteredAt,
201
+ recordUpdate: previous,
202
+ };
203
+ }
204
+ // Best-effort newest timestamp across contributing agents whose derived
205
+ // bucket matches `winningBucket`. Uses available agent fields:
206
+ // - `attentionTimestamp` when attention is set (covers attention/failed)
207
+ // - `updatedAt` as a general fallback for any bucket
208
+ // Returns `null` if no matching agent has a parseable timestamp.
209
+ findNewestAgentTimestampInBucket(contributingAgents, winningBucket) {
210
+ const candidates = contributingAgents
211
+ .filter((agent) => {
212
+ const derived = deriveAgentStateBucket({
111
213
  status: agent.status,
112
214
  pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
113
215
  requiresAttention: agent.requiresAttention,
114
216
  attentionReason: agent.attentionReason ?? null,
115
217
  });
116
- if (getWorkspaceStateBucketPriority(bucket) < getWorkspaceStateBucketPriority(existing.status)) {
117
- existing.status = bucket;
218
+ return derived === winningBucket;
219
+ })
220
+ .map((agent) => {
221
+ // Prefer attentionTimestamp when the agent has attention set — this is
222
+ // the most accurate "entered current status" signal.
223
+ if (agent.attentionTimestamp) {
224
+ return agent.attentionTimestamp;
118
225
  }
119
- }
120
- return descriptorsByWorkspaceId;
226
+ // Fall back to updatedAt as a general proxy for recent activity.
227
+ return agent.updatedAt;
228
+ })
229
+ .filter((value) => typeof value === "string" && value.length > 0)
230
+ .sort();
231
+ return candidates.at(-1) ?? null;
121
232
  }
122
233
  resolveRegisteredWorkspaceIdForCwd(cwd, workspaces) {
123
234
  const normalizedCwd = normalizeWorkspaceId(cwd);
@@ -211,4 +322,23 @@ export class WorkspaceDirectory {
211
322
  };
212
323
  }
213
324
  }
325
+ function resolveDelegationRootAgent(agent, activeAgentsById) {
326
+ const seen = new Set([agent.id]);
327
+ let current = agent;
328
+ while (true) {
329
+ const parentAgentId = getParentAgentIdFromLabels(current.labels);
330
+ if (!parentAgentId) {
331
+ return current;
332
+ }
333
+ if (seen.has(parentAgentId)) {
334
+ return null;
335
+ }
336
+ const parent = activeAgentsById.get(parentAgentId);
337
+ if (!parent) {
338
+ return null;
339
+ }
340
+ seen.add(parentAgentId);
341
+ current = parent;
342
+ }
343
+ }
214
344
  //# sourceMappingURL=workspace-directory.js.map
@@ -150,11 +150,13 @@ interface WorkspaceGitServiceDependencies {
150
150
  interface WorkspaceGitServiceOptions {
151
151
  logger: pino.Logger;
152
152
  paseoHome: string;
153
+ worktreesRoot?: string;
153
154
  deps?: Partial<WorkspaceGitServiceDependencies>;
154
155
  }
155
156
  export declare class WorkspaceGitServiceImpl implements WorkspaceGitService {
156
157
  private readonly logger;
157
158
  private readonly paseoHome;
159
+ private readonly worktreesRoot;
158
160
  private readonly deps;
159
161
  private readonly snapshotUpdatedListeners;
160
162
  private readonly workspaceTargets;
@@ -219,6 +221,7 @@ export declare class WorkspaceGitServiceImpl implements WorkspaceGitService {
219
221
  private scheduleWorkspaceRefresh;
220
222
  private startWorkspaceSubscriptionTimers;
221
223
  private updateGitHubPollForTarget;
224
+ private resolveGitHubPollTarget;
222
225
  private stopGitHubPollForTarget;
223
226
  private addWorkingTreeWatcher;
224
227
  private ensureLinuxRepoTreeWatchers;
@@ -71,6 +71,7 @@ export class WorkspaceGitServiceImpl {
71
71
  this.checkoutDiffCache = new LRUCache({ max: WORKSPACE_GIT_CHECKOUT_DIFF_CACHE_MAX });
72
72
  this.logger = options.logger.child({ module: "workspace-git-service" });
73
73
  this.paseoHome = options.paseoHome;
74
+ this.worktreesRoot = options.worktreesRoot;
74
75
  this.deps = resolveWorkspaceGitServiceDeps(options.deps);
75
76
  }
76
77
  registerWorkspace(params, listener) {
@@ -112,6 +113,7 @@ export class WorkspaceGitServiceImpl {
112
113
  try {
113
114
  const status = await this.deps.getCheckoutStatus(normalizedCwd, {
114
115
  paseoHome: this.paseoHome,
116
+ worktreesRoot: this.worktreesRoot,
115
117
  logger: this.logger,
116
118
  });
117
119
  if (!status.isGit) {
@@ -152,7 +154,10 @@ export class WorkspaceGitServiceImpl {
152
154
  const normalizedCwd = normalizeWorkspaceId(cwd);
153
155
  const normalizedOptions = this.normalizeCheckoutDiffOptions(options);
154
156
  const key = this.buildCheckoutDiffCacheKey(normalizedCwd, normalizedOptions);
155
- return this.readAuxiliaryCache(this.checkoutDiffCache, key, readOptions, () => this.deps.getCheckoutDiff(normalizedCwd, normalizedOptions, { paseoHome: this.paseoHome }));
157
+ return this.readAuxiliaryCache(this.checkoutDiffCache, key, readOptions, () => this.deps.getCheckoutDiff(normalizedCwd, normalizedOptions, {
158
+ paseoHome: this.paseoHome,
159
+ worktreesRoot: this.worktreesRoot,
160
+ }));
156
161
  }
157
162
  normalizeCheckoutDiffOptions(options) {
158
163
  return {
@@ -221,6 +226,7 @@ export class WorkspaceGitServiceImpl {
221
226
  return this.readAuxiliaryCache(this.worktreeListCache, key, options, () => this.deps.listPaseoWorktrees({
222
227
  cwd: repoRoot,
223
228
  paseoHome: this.paseoHome,
229
+ worktreesRoot: this.worktreesRoot,
224
230
  }));
225
231
  }
226
232
  async resolveRepoRoot(cwd, options) {
@@ -380,7 +386,7 @@ export class WorkspaceGitServiceImpl {
380
386
  debounceTimer: null,
381
387
  selfHealTimer: null,
382
388
  githubPollSubscription: null,
383
- githubPollHeadRef: null,
389
+ githubPollKey: null,
384
390
  refreshState: { status: "idle" },
385
391
  latestGit: null,
386
392
  latestGitLoadedAtMs: null,
@@ -676,21 +682,26 @@ export class WorkspaceGitServiceImpl {
676
682
  this.stopGitHubPollForTarget(target);
677
683
  return;
678
684
  }
679
- const headRef = git.currentBranch;
680
- const hasGitHubRemote = target.cachedGitHubRemote?.remoteUrl === git.remoteUrl &&
685
+ const pollTarget = this.resolveGitHubPollTarget(target);
686
+ const remoteUrl = git.remoteUrl;
687
+ const hasGitHubRemote = target.cachedGitHubRemote?.remoteUrl === remoteUrl &&
681
688
  target.cachedGitHubRemote.identity !== null;
682
- if (!headRef || !hasGitHubRemote) {
689
+ if (!pollTarget || remoteUrl === null || !hasGitHubRemote) {
683
690
  this.stopGitHubPollForTarget(target);
684
691
  return;
685
692
  }
686
- if (target.githubPollHeadRef === headRef && target.githubPollSubscription) {
693
+ const pollKey = buildWorkspaceGitHubPollKey(remoteUrl, pollTarget);
694
+ if (target.githubPollKey === pollKey && target.githubPollSubscription) {
687
695
  return;
688
696
  }
689
697
  this.stopGitHubPollForTarget(target);
690
- target.githubPollHeadRef = headRef;
698
+ target.githubPollKey = pollKey;
691
699
  target.githubPollSubscription = this.deps.github.retainCurrentPullRequestStatusPoll({
692
700
  cwd: target.cwd,
693
- headRef,
701
+ headRef: pollTarget.headRef,
702
+ ...(pollTarget.headRepositoryOwner
703
+ ? { headRepositoryOwner: pollTarget.headRepositoryOwner }
704
+ : {}),
694
705
  onStatus: (status) => {
695
706
  if (!this.isActiveObservedWorkspaceTarget(target)) {
696
707
  return;
@@ -700,14 +711,33 @@ export class WorkspaceGitServiceImpl {
700
711
  });
701
712
  },
702
713
  onError: (error) => {
703
- this.logger.warn({ err: error, cwd: target.cwd, headRef, reason: "self-heal-github" }, "Failed to run GitHub self-heal refresh");
714
+ this.logger.warn({
715
+ err: error,
716
+ cwd: target.cwd,
717
+ headRef: pollTarget.headRef,
718
+ headRepositoryOwner: pollTarget.headRepositoryOwner,
719
+ reason: "self-heal-github",
720
+ }, "Failed to run GitHub self-heal refresh");
704
721
  },
705
722
  });
706
723
  }
724
+ resolveGitHubPollTarget(target) {
725
+ const git = target.latestGit;
726
+ if (!git?.currentBranch) {
727
+ return null;
728
+ }
729
+ const lookupTarget = target.latestFacts?.isGit && target.latestFacts.currentBranch === git.currentBranch
730
+ ? target.latestFacts.pullRequestLookupTarget
731
+ : null;
732
+ if (lookupTarget) {
733
+ return lookupTarget;
734
+ }
735
+ return { headRef: git.currentBranch };
736
+ }
707
737
  stopGitHubPollForTarget(target) {
708
738
  target.githubPollSubscription?.unsubscribe();
709
739
  target.githubPollSubscription = null;
710
- target.githubPollHeadRef = null;
740
+ target.githubPollKey = null;
711
741
  }
712
742
  addWorkingTreeWatcher(target, watchPath, shouldTryRecursive) {
713
743
  if (target.watchedPaths.has(watchPath)) {
@@ -998,7 +1028,11 @@ export class WorkspaceGitServiceImpl {
998
1028
  target.lastShellOutAtMs = now.getTime();
999
1029
  const cwd = target.cwd;
1000
1030
  const previousGitHubPollKey = this.getGitHubPollKey(target);
1001
- const baseContext = { paseoHome: this.paseoHome, logger: this.logger };
1031
+ const baseContext = {
1032
+ paseoHome: this.paseoHome,
1033
+ worktreesRoot: this.worktreesRoot,
1034
+ logger: this.logger,
1035
+ };
1002
1036
  const facts = await this.loadCheckoutFacts(target, {
1003
1037
  ...baseContext,
1004
1038
  allowRecent: !request.force,
@@ -1075,7 +1109,11 @@ export class WorkspaceGitServiceImpl {
1075
1109
  if (!githubRemote || githubRemote.remoteUrl !== git.remoteUrl || !githubRemote.identity) {
1076
1110
  return null;
1077
1111
  }
1078
- return JSON.stringify([git.remoteUrl, git.currentBranch]);
1112
+ const pollTarget = this.resolveGitHubPollTarget(target);
1113
+ if (!pollTarget) {
1114
+ return null;
1115
+ }
1116
+ return buildWorkspaceGitHubPollKey(git.remoteUrl, pollTarget);
1079
1117
  }
1080
1118
  rememberGitHubSnapshot(target, github, options) {
1081
1119
  if (target.closed || this.workspaceTargets.get(target.cwd) !== target) {
@@ -1317,6 +1355,9 @@ function buildGitHubSnapshotFromStatus(status) {
1317
1355
  error: null,
1318
1356
  };
1319
1357
  }
1358
+ function buildWorkspaceGitHubPollKey(remoteUrl, target) {
1359
+ return JSON.stringify([remoteUrl, target.headRef, target.headRepositoryOwner ?? null]);
1360
+ }
1320
1361
  async function runGitFetch(cwd) {
1321
1362
  await runGitCommand(["fetch", "origin", "--prune"], {
1322
1363
  cwd,
@@ -39,6 +39,7 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
39
39
  updatedAt: z.ZodString;
40
40
  archivedAt: z.ZodNullable<z.ZodString>;
41
41
  }, "strip", z.ZodTypeAny, {
42
+ workspaceId: string;
42
43
  cwd: string;
43
44
  createdAt: string;
44
45
  updatedAt: string;
@@ -46,8 +47,8 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
46
47
  kind: "worktree" | "local_checkout" | "directory";
47
48
  projectId: string;
48
49
  displayName: string;
49
- workspaceId: string;
50
50
  }, {
51
+ workspaceId: string;
51
52
  cwd: string;
52
53
  createdAt: string;
53
54
  updatedAt: string;
@@ -55,7 +56,6 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
55
56
  kind: "worktree" | "local_checkout" | "directory";
56
57
  projectId: string;
57
58
  displayName: string;
58
- workspaceId: string;
59
59
  }>;
60
60
  export type PersistedProjectRecord = z.infer<typeof PersistedProjectRecordSchema>;
61
61
  export type PersistedWorkspaceRecord = z.infer<typeof PersistedWorkspaceRecordSchema>;
@@ -1,7 +1,6 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { promises as fs } from "node:fs";
3
- import path from "node:path";
4
2
  import { z } from "zod";
3
+ import { writeJsonFileAtomic } from "./atomic-file.js";
5
4
  const PersistedProjectRecordSchema = z.object({
6
5
  projectId: z.string(),
7
6
  rootPath: z.string(),
@@ -110,10 +109,7 @@ class FileBackedRegistry {
110
109
  }
111
110
  async persist() {
112
111
  const records = Array.from(this.cache.values());
113
- await fs.mkdir(path.dirname(this.filePath), { recursive: true });
114
- const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
115
- await fs.writeFile(tempPath, JSON.stringify(records, null, 2), "utf8");
116
- await fs.rename(tempPath, this.filePath);
112
+ await writeJsonFileAtomic(this.filePath, records);
117
113
  }
118
114
  async enqueuePersist() {
119
115
  const nextPersist = this.persistQueue.then(() => this.persist());
@@ -8,6 +8,7 @@ export interface BuildWorkspaceServiceEnvOptions {
8
8
  branchName: string | null;
9
9
  daemonPort: number | null | undefined;
10
10
  daemonListenHost: string | null | undefined;
11
+ serviceProxyPublicBaseUrl?: string | null;
11
12
  peers: readonly WorkspaceServicePeer[];
12
13
  }
13
14
  export declare function normalizeServiceEnvName(scriptName: string): string;
@@ -1,4 +1,4 @@
1
- import { buildScriptHostname } from "../utils/script-hostname.js";
1
+ import { projectServiceProxyUrls } from "./service-proxy.js";
2
2
  export function normalizeServiceEnvName(scriptName) {
3
3
  return scriptName
4
4
  .toUpperCase()
@@ -16,24 +16,28 @@ export function buildWorkspaceServiceEnv(options) {
16
16
  HOST: resolveServiceBindHost(options.daemonListenHost),
17
17
  PASEO_PORT: String(selfPeer.port),
18
18
  };
19
- if (options.daemonPort !== null && options.daemonPort !== undefined) {
20
- env.PASEO_URL = buildServiceProxyUrl({
21
- projectSlug: options.projectSlug,
22
- branchName: options.branchName,
23
- scriptName: options.scriptName,
24
- daemonPort: options.daemonPort,
25
- });
19
+ const selfProxyUrl = buildServiceProxyUrl({
20
+ projectSlug: options.projectSlug,
21
+ branchName: options.branchName,
22
+ scriptName: options.scriptName,
23
+ daemonPort: options.daemonPort,
24
+ serviceProxyPublicBaseUrl: options.serviceProxyPublicBaseUrl,
25
+ });
26
+ if (selfProxyUrl) {
27
+ env.PASEO_URL = selfProxyUrl;
26
28
  }
27
29
  for (const peer of options.peers) {
28
30
  const envName = normalizeServiceEnvName(peer.scriptName);
29
31
  env[`PASEO_SERVICE_${envName}_PORT`] = String(peer.port);
30
- if (options.daemonPort !== null && options.daemonPort !== undefined) {
31
- env[`PASEO_SERVICE_${envName}_URL`] = buildServiceProxyUrl({
32
- projectSlug: options.projectSlug,
33
- branchName: options.branchName,
34
- scriptName: peer.scriptName,
35
- daemonPort: options.daemonPort,
36
- });
32
+ const peerProxyUrl = buildServiceProxyUrl({
33
+ projectSlug: options.projectSlug,
34
+ branchName: options.branchName,
35
+ scriptName: peer.scriptName,
36
+ daemonPort: options.daemonPort,
37
+ serviceProxyPublicBaseUrl: options.serviceProxyPublicBaseUrl,
38
+ });
39
+ if (peerProxyUrl) {
40
+ env[`PASEO_SERVICE_${envName}_URL`] = peerProxyUrl;
37
41
  }
38
42
  }
39
43
  return env;
@@ -42,12 +46,13 @@ export function resolveServiceBindHost(daemonListenHost) {
42
46
  return isLoopbackListenHost(daemonListenHost) ? "127.0.0.1" : "0.0.0.0";
43
47
  }
44
48
  function buildServiceProxyUrl(options) {
45
- const hostname = buildScriptHostname({
49
+ return projectServiceProxyUrls({
46
50
  projectSlug: options.projectSlug,
47
51
  branchName: options.branchName,
48
52
  scriptName: options.scriptName,
49
- });
50
- return `http://${hostname}:${options.daemonPort}`;
53
+ daemonPort: options.daemonPort,
54
+ publicBaseUrl: options.serviceProxyPublicBaseUrl,
55
+ }).proxyUrl;
51
56
  }
52
57
  function isLoopbackListenHost(host) {
53
58
  if (!host) {
@@ -13,10 +13,12 @@ export declare function listPaseoWorktreesCommand(dependencies: ListPaseoWorktre
13
13
  type CreatePaseoWorktreeWorkflow<Result extends CreatePaseoWorktreeResult> = (input: CreatePaseoWorktreeInput) => Promise<Result>;
14
14
  export interface CreatePaseoWorktreeCommandDependencies<Result extends CreatePaseoWorktreeResult = CreatePaseoWorktreeResult> {
15
15
  paseoHome?: string;
16
+ worktreesRoot?: string;
16
17
  createPaseoWorktreeWorkflow?: CreatePaseoWorktreeWorkflow<Result>;
17
18
  }
18
19
  export type CreatePaseoWorktreeCommandInput = Omit<CreatePaseoWorktreeInput, "paseoHome" | "runSetup"> & {
19
20
  paseoHome?: string;
21
+ worktreesRoot?: string;
20
22
  };
21
23
  export type CreatePaseoWorktreeCommandResult<Result extends CreatePaseoWorktreeResult> = {
22
24
  ok: true;