@getpaseo/server 0.1.87 → 0.1.89

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 (69) hide show
  1. package/dist/server/server/agent/agent-manager.js +4 -1
  2. package/dist/server/server/agent/agent-storage.d.ts +22 -22
  3. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  4. package/dist/server/server/agent/create-agent/create.js +16 -5
  5. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
  6. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
  7. package/dist/server/server/agent/mcp-server.d.ts +1 -0
  8. package/dist/server/server/agent/mcp-server.js +137 -63
  9. package/dist/server/server/agent/mcp-shared.d.ts +1 -0
  10. package/dist/server/server/agent/providers/pi/agent.js +13 -0
  11. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
  12. package/dist/server/server/agent/timeline-projection.d.ts +17 -1
  13. package/dist/server/server/agent/timeline-projection.js +82 -17
  14. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
  15. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
  16. package/dist/server/server/bootstrap.d.ts +7 -2
  17. package/dist/server/server/bootstrap.js +152 -115
  18. package/dist/server/server/config.js +41 -0
  19. package/dist/server/server/loop-service.d.ts +22 -22
  20. package/dist/server/server/package-version.d.ts +2 -2
  21. package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
  22. package/dist/server/server/paseo-worktree-archive-service.js +28 -9
  23. package/dist/server/server/persisted-config.d.ts +89 -33
  24. package/dist/server/server/persisted-config.js +17 -0
  25. package/dist/server/server/pid-lock.d.ts +2 -2
  26. package/dist/server/server/schedule/cron.js +52 -5
  27. package/dist/server/server/script-health-monitor.d.ts +4 -4
  28. package/dist/server/server/script-health-monitor.js +6 -6
  29. package/dist/server/server/script-proxy.d.ts +2 -39
  30. package/dist/server/server/script-proxy.js +1 -244
  31. package/dist/server/server/script-route-branch-handler.d.ts +2 -2
  32. package/dist/server/server/script-route-branch-handler.js +3 -37
  33. package/dist/server/server/script-status-projection.d.ts +6 -4
  34. package/dist/server/server/script-status-projection.js +85 -37
  35. package/dist/server/server/service-proxy.d.ts +237 -0
  36. package/dist/server/server/service-proxy.js +714 -0
  37. package/dist/server/server/session.d.ts +11 -4
  38. package/dist/server/server/session.js +96 -99
  39. package/dist/server/server/websocket-server.d.ts +7 -4
  40. package/dist/server/server/websocket-server.js +9 -4
  41. package/dist/server/server/workspace-directory.js +4 -0
  42. package/dist/server/server/workspace-git-service.d.ts +3 -0
  43. package/dist/server/server/workspace-git-service.js +53 -12
  44. package/dist/server/server/workspace-registry.d.ts +2 -2
  45. package/dist/server/server/workspace-service-env.d.ts +1 -0
  46. package/dist/server/server/workspace-service-env.js +23 -18
  47. package/dist/server/server/worktree/commands.d.ts +2 -0
  48. package/dist/server/server/worktree/commands.js +4 -1
  49. package/dist/server/server/worktree-bootstrap.d.ts +4 -3
  50. package/dist/server/server/worktree-bootstrap.js +14 -13
  51. package/dist/server/server/worktree-core.d.ts +1 -0
  52. package/dist/server/server/worktree-core.js +2 -0
  53. package/dist/server/server/worktree-session.d.ts +6 -2
  54. package/dist/server/server/worktree-session.js +3 -0
  55. package/dist/server/services/github-service.d.ts +1 -0
  56. package/dist/server/services/github-service.js +7 -1
  57. package/dist/server/terminal/terminal-manager.js +11 -1
  58. package/dist/server/terminal/terminal-session-controller.d.ts +3 -1
  59. package/dist/server/terminal/terminal-session-controller.js +22 -12
  60. package/dist/server/terminal/terminal.d.ts +1 -0
  61. package/dist/server/terminal/terminal.js +34 -0
  62. package/dist/server/utils/checkout-git.d.ts +6 -2
  63. package/dist/server/utils/checkout-git.js +136 -54
  64. package/dist/server/utils/worktree.d.ts +17 -12
  65. package/dist/server/utils/worktree.js +39 -22
  66. package/dist/src/server/persisted-config.js +17 -0
  67. package/package.json +5 -5
  68. package/dist/server/utils/script-hostname.d.ts +0 -8
  69. package/dist/server/utils/script-hostname.js +0 -14
@@ -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>;
@@ -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;
@@ -17,6 +17,7 @@ export async function createPaseoWorktreeCommand(dependencies, input) {
17
17
  ...input,
18
18
  runSetup: false,
19
19
  paseoHome: input.paseoHome ?? dependencies.paseoHome,
20
+ worktreesRoot: input.worktreesRoot ?? dependencies.worktreesRoot,
20
21
  });
21
22
  return { ok: true, createdWorktree };
22
23
  }
@@ -32,6 +33,7 @@ export async function archivePaseoWorktreeCommand(dependencies, input) {
32
33
  const resolvedTarget = await resolveArchiveTarget(dependencies, input);
33
34
  const ownership = await isPaseoOwnedWorktreeCwd(resolvedTarget.targetPath, {
34
35
  paseoHome: dependencies.paseoHome,
36
+ worktreesRoot: dependencies.worktreesRoot,
35
37
  });
36
38
  if (!ownership.allowed) {
37
39
  return {
@@ -46,6 +48,7 @@ export async function archivePaseoWorktreeCommand(dependencies, input) {
46
48
  targetPath: resolvedTarget.targetPath,
47
49
  repoRoot,
48
50
  worktreesRoot: ownership.worktreeRoot,
51
+ worktreesBaseRoot: dependencies.worktreesRoot,
49
52
  requestId: input.requestId,
50
53
  });
51
54
  return {
@@ -78,7 +81,7 @@ async function resolveArchiveTarget(dependencies, input) {
78
81
  throw new Error("worktreePath, worktreeSlug, or repoRoot+branchName is required");
79
82
  }
80
83
  async function resolveWorktreeSlugPath(dependencies, repoRoot, worktreeSlug) {
81
- const worktreesRoot = await getPaseoWorktreesRoot(repoRoot, dependencies.paseoHome);
84
+ const worktreesRoot = await getPaseoWorktreesRoot(repoRoot, dependencies.paseoHome, dependencies.worktreesRoot);
82
85
  return join(worktreesRoot, worktreeSlug);
83
86
  }
84
87
  //# sourceMappingURL=commands.js.map
@@ -1,7 +1,7 @@
1
1
  import type { Logger } from "pino";
2
2
  import type { TerminalManager } from "../terminal/terminal-manager.js";
3
3
  import { runWorktreeSetupCommands, type WorktreeConfig, type WorktreeSetupCommandResult } from "../utils/worktree.js";
4
- import { type ScriptRouteStore } from "./script-proxy.js";
4
+ import { type ServiceProxySubsystem } from "./service-proxy.js";
5
5
  import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
6
6
  import type { AgentTimelineItem, ToolCallDetail } from "./agent/agent-sdk-types.js";
7
7
  export interface WorktreeBootstrapTerminalResult {
@@ -58,7 +58,8 @@ interface SpawnWorkspaceScriptOptions {
58
58
  scriptName: string;
59
59
  daemonPort?: number | null;
60
60
  daemonListenHost?: string | null;
61
- routeStore: ScriptRouteStore;
61
+ serviceProxyPublicBaseUrl?: string | null;
62
+ serviceProxy: ServiceProxySubsystem;
62
63
  runtimeStore: WorkspaceScriptRuntimeStore;
63
64
  terminalManager: TerminalManager;
64
65
  logger?: Logger;
@@ -67,7 +68,7 @@ interface SpawnWorkspaceScriptOptions {
67
68
  export declare function spawnWorkspaceScript(options: SpawnWorkspaceScriptOptions): Promise<WorktreeScriptResult>;
68
69
  export declare function teardownWorktreeScripts(options: {
69
70
  hostnames: string[];
70
- routeStore: ScriptRouteStore;
71
+ serviceProxy: Pick<ServiceProxySubsystem, "removeServiceRoutesByHostnames">;
71
72
  logger: Logger;
72
73
  }): void;
73
74
  export {};
@@ -1,7 +1,6 @@
1
1
  import { v4 as uuidv4 } from "uuid";
2
- import { buildScriptHostname } from "../utils/script-hostname.js";
3
2
  import { getScriptConfigs, getWorktreeTerminalSpecs, isServiceScript, paseoConfigParseError, processCarriageReturns, readPaseoConfig, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
4
- import { findFreePort } from "./script-proxy.js";
3
+ import { findFreePort } from "./service-proxy.js";
5
4
  import { assertNoServiceEnvNameCollisions, buildWorkspaceServiceEnv, } from "./workspace-service-env.js";
6
5
  import { ensureWorkspaceServicePortPlan, requirePlannedWorkspaceServicePort, refreshWorkspaceServicePort, } from "./workspace-service-port-registry.js";
7
6
  const MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES = 64 * 1024;
@@ -501,8 +500,7 @@ export async function runAsyncWorktreeBootstrap(options) {
501
500
  await runWorktreeTerminalBootstrap(options, runtimeEnv);
502
501
  }
503
502
  async function setupServiceScriptRoute(params) {
504
- const { scriptConfigs, config, scriptName, projectSlug, branchName, workspaceId, daemonPort, daemonListenHost, existingRuntimeEntry, routeStore, } = params;
505
- const hostname = buildScriptHostname({ projectSlug, branchName, scriptName });
503
+ const { scriptConfigs, config, scriptName, projectSlug, branchName, workspaceId, daemonPort, daemonListenHost, serviceProxyPublicBaseUrl, existingRuntimeEntry, serviceProxy, } = params;
506
504
  const serviceDeclarations = [];
507
505
  for (const [configuredScriptName, scriptConfig] of scriptConfigs) {
508
506
  if (isServiceScript(scriptConfig)) {
@@ -538,16 +536,18 @@ async function setupServiceScriptRoute(params) {
538
536
  branchName,
539
537
  daemonPort,
540
538
  daemonListenHost,
539
+ serviceProxyPublicBaseUrl,
541
540
  peers,
542
541
  });
543
- routeStore.registerRoute({
544
- hostname,
542
+ const registeredRoute = serviceProxy.registerWorkspaceService({
545
543
  port,
546
544
  workspaceId,
547
545
  projectSlug,
546
+ branchName,
548
547
  scriptName,
548
+ publicBaseUrl: serviceProxyPublicBaseUrl ?? null,
549
549
  });
550
- return { hostname, port, env };
550
+ return { hostname: registeredRoute.hostname, port, env };
551
551
  }
552
552
  async function acquireWorkspaceScriptTerminal(params) {
553
553
  const { serviceScript, existingRuntimeEntry, terminalManager, repoRoot, scriptName, env } = params;
@@ -565,7 +565,7 @@ async function acquireWorkspaceScriptTerminal(params) {
565
565
  return { terminal, reusableTerminal };
566
566
  }
567
567
  export async function spawnWorkspaceScript(options) {
568
- const { repoRoot, workspaceId, projectSlug, branchName, scriptName, daemonPort, daemonListenHost, routeStore, runtimeStore, terminalManager, logger, onLifecycleChanged, } = options;
568
+ const { repoRoot, workspaceId, projectSlug, branchName, scriptName, daemonPort, daemonListenHost, serviceProxyPublicBaseUrl, serviceProxy, runtimeStore, terminalManager, logger, onLifecycleChanged, } = options;
569
569
  const configResult = readPaseoConfig(repoRoot);
570
570
  if (!configResult.ok) {
571
571
  throw paseoConfigParseError(configResult);
@@ -598,8 +598,9 @@ export async function spawnWorkspaceScript(options) {
598
598
  workspaceId,
599
599
  daemonPort,
600
600
  daemonListenHost,
601
+ serviceProxyPublicBaseUrl,
601
602
  existingRuntimeEntry,
602
- routeStore,
603
+ serviceProxy,
603
604
  });
604
605
  hostname = serviceSetup.hostname;
605
606
  port = serviceSetup.port;
@@ -631,7 +632,7 @@ export async function spawnWorkspaceScript(options) {
631
632
  disposeLifecycleListeners?.();
632
633
  disposeLifecycleListeners = null;
633
634
  if (input.removeRoute && hostname) {
634
- routeStore.removeRouteForWorkspaceScript({ workspaceId, scriptName });
635
+ serviceProxy.removeWorkspaceService({ workspaceId, scriptName });
635
636
  }
636
637
  runtimeStore.set({
637
638
  workspaceId,
@@ -689,7 +690,7 @@ export async function spawnWorkspaceScript(options) {
689
690
  catch (error) {
690
691
  disposeLifecycleListeners?.();
691
692
  if (routeRegistered && hostname) {
692
- routeStore.removeRoute(hostname);
693
+ serviceProxy.removeServiceRoutesByHostnames([hostname]);
693
694
  }
694
695
  if (runtimeRegistered) {
695
696
  runtimeStore.remove({ workspaceId, scriptName });
@@ -707,9 +708,9 @@ export async function spawnWorkspaceScript(options) {
707
708
  }
708
709
  }
709
710
  export function teardownWorktreeScripts(options) {
710
- const { hostnames, routeStore, logger } = options;
711
+ const { hostnames, serviceProxy, logger } = options;
712
+ serviceProxy.removeServiceRoutesByHostnames(hostnames);
711
713
  for (const hostname of hostnames) {
712
- routeStore.removeRoute(hostname);
713
714
  logger.info({ hostname }, "Removed script proxy route");
714
715
  }
715
716
  }
@@ -11,6 +11,7 @@ export interface CreateWorktreeCoreInput {
11
11
  githubPrNumber?: number;
12
12
  firstAgentContext?: FirstAgentContext;
13
13
  paseoHome?: string;
14
+ worktreesRoot?: string;
14
15
  runSetup?: boolean;
15
16
  }
16
17
  export interface CreateWorktreeCoreDeps {
@@ -54,6 +54,7 @@ export async function createWorktreeCore(input, deps) {
54
54
  slug: normalizedSlug,
55
55
  repoRoot,
56
56
  paseoHome: input.paseoHome,
57
+ worktreesRoot: input.worktreesRoot,
57
58
  });
58
59
  if (existingWorktree) {
59
60
  return { worktree: existingWorktree, intent, repoRoot, created: false };
@@ -65,6 +66,7 @@ export async function createWorktreeCore(input, deps) {
65
66
  source: intent,
66
67
  runSetup: input.runSetup ?? true,
67
68
  paseoHome: input.paseoHome,
69
+ worktreesRoot: input.worktreesRoot,
68
70
  }),
69
71
  intent,
70
72
  repoRoot,
@@ -5,7 +5,7 @@ import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
5
5
  import type { WorkspaceGitService } from "./workspace-git-service.js";
6
6
  import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
7
7
  import type { TerminalManager } from "../terminal/terminal-manager.js";
8
- import type { ScriptRouteStore } from "./script-proxy.js";
8
+ import type { ServiceProxySubsystem } from "./service-proxy.js";
9
9
  import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
10
10
  import type { GitHubService } from "../services/github-service.js";
11
11
  import type { CheckoutExistingBranchResult } from "../utils/checkout-git.js";
@@ -31,6 +31,7 @@ type AgentWorktreeSetupTimelineWriter = (input: {
31
31
  }) => Promise<boolean>;
32
32
  interface BuildAgentSessionConfigDependencies {
33
33
  paseoHome?: string;
34
+ worktreesRoot?: string;
34
35
  sessionLogger: Logger;
35
36
  workspaceGitService?: WorkspaceGitService;
36
37
  createPaseoWorktree: (input: CreatePaseoWorktreeInput, options?: {
@@ -47,6 +48,7 @@ interface BuildAgentSessionConfigDependencies {
47
48
  }
48
49
  interface CreatePaseoWorktreeInBackgroundDependencies {
49
50
  paseoHome?: string;
51
+ worktreesRoot?: string;
50
52
  emitWorkspaceUpdateForCwd: (cwd: string, options?: {
51
53
  dedupeGitState?: boolean;
52
54
  }) => Promise<void>;
@@ -55,10 +57,11 @@ interface CreatePaseoWorktreeInBackgroundDependencies {
55
57
  sessionLogger: Logger;
56
58
  terminalManager: TerminalManager | null;
57
59
  archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
58
- scriptRouteStore: ScriptRouteStore | null;
60
+ serviceProxy: ServiceProxySubsystem | null;
59
61
  scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
60
62
  getDaemonTcpPort: (() => number | null) | null;
61
63
  getDaemonTcpHost: (() => string | null) | null;
64
+ serviceProxyPublicBaseUrl?: string | null;
62
65
  onScriptsChanged: ((workspaceId: string, workspaceDirectory: string) => void) | null;
63
66
  }
64
67
  interface CreatePaseoWorktreeWorkflowDependencies extends CreatePaseoWorktreeInBackgroundDependencies {
@@ -100,6 +103,7 @@ interface HandleWorkspaceSetupStatusRequestDependencies {
100
103
  }
101
104
  interface HandleCreatePaseoWorktreeRequestDependencies {
102
105
  paseoHome?: string;
106
+ worktreesRoot?: string;
103
107
  describeWorkspaceRecord: (result: CreatePaseoWorktreeResult) => Promise<WorkspaceDescriptorPayload>;
104
108
  emit: EmitSessionMessage;
105
109
  sessionLogger: Logger;
@@ -41,6 +41,7 @@ export async function buildAgentSessionConfig(dependencies, config, gitOptions,
41
41
  firstAgentContext,
42
42
  runSetup: false,
43
43
  paseoHome: dependencies.paseoHome,
44
+ worktreesRoot: dependencies.worktreesRoot,
44
45
  }, {
45
46
  resolveDefaultBranch: normalized.baseBranch
46
47
  ? async () => normalized.baseBranch
@@ -241,6 +242,7 @@ export async function handleCreatePaseoWorktreeRequest(dependencies, request) {
241
242
  try {
242
243
  const commandResult = await createPaseoWorktreeCommand({
243
244
  paseoHome: dependencies.paseoHome,
245
+ worktreesRoot: dependencies.worktreesRoot,
244
246
  createPaseoWorktreeWorkflow: dependencies.createPaseoWorktreeWorkflow,
245
247
  }, {
246
248
  cwd: request.cwd,
@@ -304,6 +306,7 @@ export async function createPaseoWorktreeWorkflow(dependencies, input, options)
304
306
  ...input,
305
307
  runSetup: false,
306
308
  paseoHome: input.paseoHome ?? dependencies.paseoHome,
309
+ worktreesRoot: input.worktreesRoot ?? dependencies.worktreesRoot,
307
310
  }, options?.resolveDefaultBranch
308
311
  ? { resolveDefaultBranch: options.resolveDefaultBranch }
309
312
  : undefined);
@@ -228,6 +228,7 @@ export interface GitHubService {
228
228
  retainCurrentPullRequestStatusPoll?(options: {
229
229
  cwd: string;
230
230
  headRef: string;
231
+ headRepositoryOwner?: string;
231
232
  onStatus?: (status: GitHubCurrentPullRequestStatus | null) => void;
232
233
  onError?: (error: unknown) => void;
233
234
  }): {
@@ -425,7 +425,10 @@ export function createGitHubService(options = {}) {
425
425
  return buildCacheKey({
426
426
  cwd: target.cwd,
427
427
  method: "getCurrentPullRequestStatus",
428
- args: { headRef: target.headRef },
428
+ args: {
429
+ headRef: target.headRef,
430
+ headRepositoryOwner: target.headRepositoryOwner,
431
+ },
429
432
  });
430
433
  }
431
434
  function updatePollTargetAfterSuccess(update) {
@@ -465,6 +468,7 @@ export function createGitHubService(options = {}) {
465
468
  await api.getCurrentPullRequestStatus({
466
469
  cwd: target.cwd,
467
470
  headRef: target.headRef,
471
+ headRepositoryOwner: target.headRepositoryOwner,
468
472
  reason: "self-heal-github",
469
473
  });
470
474
  }
@@ -601,6 +605,7 @@ export function createGitHubService(options = {}) {
601
605
  updatePollTargetAfterSuccess({
602
606
  cwd: input.cwd,
603
607
  headRef: input.headRef,
608
+ headRepositoryOwner: input.headRepositoryOwner,
604
609
  status,
605
610
  notify: input.reason === "self-heal-github",
606
611
  });
@@ -795,6 +800,7 @@ export function createGitHubService(options = {}) {
795
800
  target = {
796
801
  cwd: input.cwd,
797
802
  headRef: input.headRef,
803
+ headRepositoryOwner: input.headRepositoryOwner,
798
804
  retainCount: 0,
799
805
  timer: null,
800
806
  latestStatus: null,
@@ -1,6 +1,7 @@
1
1
  import { createTerminal, } from "./terminal.js";
2
2
  import { captureTerminalLines } from "./terminal-capture.js";
3
3
  import { resolve, sep, win32, posix } from "node:path";
4
+ import { isSameOrDescendantPath } from "../server/path-utils.js";
4
5
  export function createTerminalManager() {
5
6
  const terminalsByCwd = new Map();
6
7
  const terminalsById = new Map();
@@ -99,7 +100,16 @@ export function createTerminalManager() {
99
100
  return {
100
101
  async getTerminals(cwd) {
101
102
  assertAbsolutePath(cwd);
102
- return terminalsByCwd.get(cwd) ?? [];
103
+ // Terminals are bucketed by exact cwd, but an agent can open a terminal in
104
+ // a subdirectory of the workspace. A query for the workspace root must
105
+ // surface those too, so aggregate every bucket at or below `cwd`.
106
+ const sessions = [];
107
+ for (const [bucketCwd, bucketSessions] of terminalsByCwd) {
108
+ if (isSameOrDescendantPath(cwd, bucketCwd)) {
109
+ sessions.push(...bucketSessions);
110
+ }
111
+ }
112
+ return sessions;
103
113
  },
104
114
  async createTerminal(options) {
105
115
  assertAbsolutePath(options.cwd);
@@ -9,6 +9,7 @@ export interface TerminalSessionControllerOptions {
9
9
  hasBinaryChannel: () => boolean;
10
10
  isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
11
11
  sessionLogger: pino.Logger;
12
+ clientSupportsWrapReflow?: () => boolean;
12
13
  }
13
14
  export interface TerminalSessionControllerMetrics {
14
15
  directorySubscriptionCount: number;
@@ -21,6 +22,7 @@ export declare class TerminalSessionController {
21
22
  private readonly hasBinaryChannel;
22
23
  private readonly isPathWithinRoot;
23
24
  private readonly sessionLogger;
25
+ private readonly clientSupportsWrapReflow;
24
26
  private readonly subscribedDirectories;
25
27
  private unsubscribeTerminalsChanged;
26
28
  private readonly exitSubscriptions;
@@ -45,7 +47,7 @@ export declare class TerminalSessionController {
45
47
  private handleTerminalsChanged;
46
48
  private handleSubscribeTerminalsRequest;
47
49
  private handleUnsubscribeTerminalsRequest;
48
- private emitInitialTerminalsChangedSnapshot;
50
+ private emitTerminalsSnapshotForRoot;
49
51
  private handleListTerminalsRequest;
50
52
  private getAllTerminalSessions;
51
53
  private handleCreateTerminalRequest;