@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
@@ -21,6 +21,13 @@ interface ProjectedWindowSelection {
21
21
  minSeq: number | null;
22
22
  maxSeq: number | null;
23
23
  }
24
+ export interface ProjectedTimelinePageSelection {
25
+ entries: TimelineProjectionEntry[];
26
+ startSeq: number | null;
27
+ endSeq: number | null;
28
+ hasOlder: boolean;
29
+ hasNewer: boolean;
30
+ }
24
31
  export declare function projectTimelineRows(input: {
25
32
  rows: readonly AgentTimelineRow[];
26
33
  mode: TimelineProjectionMode;
@@ -34,8 +41,17 @@ export declare function selectTimelineWindowByProjectedLimit(input: {
34
41
  rows: readonly AgentTimelineRow[];
35
42
  direction: TimelineLimitDirection;
36
43
  limit: number;
37
- collapseToolLifecycle?: boolean;
38
44
  }): ProjectedWindowSelection;
45
+ export declare function selectProjectedTimelinePage(input: {
46
+ rows: readonly AgentTimelineRow[];
47
+ bounds?: {
48
+ minSeq: number;
49
+ maxSeq: number;
50
+ };
51
+ direction: TimelineLimitDirection;
52
+ cursorSeq?: number;
53
+ limit?: number;
54
+ }): ProjectedTimelinePageSelection;
39
55
  /**
40
56
  * Apply a projected-count limit to a flat AgentTimelineItem[] without seq metadata.
41
57
  * Used by callers that only have items in hand (e.g. MCP tools reading
@@ -188,9 +188,8 @@ export function projectTimelineRows(input) {
188
188
  export function selectTimelineWindowByProjectedLimit(input) {
189
189
  const { rows, direction } = input;
190
190
  const limit = Math.max(0, Math.floor(input.limit));
191
- const collapseTools = input.collapseToolLifecycle ?? true;
192
191
  const canonical = makeCanonicalEntries(rows);
193
- const projectedAll = mergeReasoningChunks(mergeAssistantChunks(collapseTools ? collapseToolLifecycle(canonical) : canonical));
192
+ const projectedAll = mergeReasoningChunks(mergeAssistantChunks(collapseToolLifecycle(canonical)));
194
193
  if (projectedAll.length === 0) {
195
194
  return {
196
195
  projectedEntries: [],
@@ -232,23 +231,21 @@ export function selectTimelineWindowByProjectedLimit(input) {
232
231
  };
233
232
  let { minSeq, maxSeq } = computeWindowBounds(projectedEntries);
234
233
  let expandedEntries = projectedEntries;
235
- if (collapseTools) {
236
- // Expand to include any projected entries that overlap the selected
237
- // canonical range. Tool lifecycle collapse can produce non-monotonic
238
- // seqEnd values, which would otherwise create cursor gaps.
239
- for (let iteration = 0; iteration < projectedAll.length + 1; iteration += 1) {
240
- const overlapping = projectedAll.filter((entry) => entry.seqStart <= maxSeq && entry.seqEnd >= minSeq);
241
- const nextBounds = computeWindowBounds(overlapping);
242
- if (overlapping.length === expandedEntries.length &&
243
- nextBounds.minSeq === minSeq &&
244
- nextBounds.maxSeq === maxSeq) {
245
- expandedEntries = overlapping;
246
- break;
247
- }
234
+ // Expand to include any projected entries that overlap the selected canonical
235
+ // range. Tool lifecycle collapse can produce non-monotonic seqEnd values,
236
+ // which would otherwise create cursor gaps.
237
+ for (let iteration = 0; iteration < projectedAll.length + 1; iteration += 1) {
238
+ const overlapping = projectedAll.filter((entry) => entry.seqStart <= maxSeq && entry.seqEnd >= minSeq);
239
+ const nextBounds = computeWindowBounds(overlapping);
240
+ if (overlapping.length === expandedEntries.length &&
241
+ nextBounds.minSeq === minSeq &&
242
+ nextBounds.maxSeq === maxSeq) {
248
243
  expandedEntries = overlapping;
249
- minSeq = nextBounds.minSeq;
250
- maxSeq = nextBounds.maxSeq;
244
+ break;
251
245
  }
246
+ expandedEntries = overlapping;
247
+ minSeq = nextBounds.minSeq;
248
+ maxSeq = nextBounds.maxSeq;
252
249
  }
253
250
  const selectedRows = rows.filter((row) => row.seq >= minSeq && row.seq <= maxSeq);
254
251
  return {
@@ -258,6 +255,74 @@ export function selectTimelineWindowByProjectedLimit(input) {
258
255
  maxSeq: Number.isFinite(maxSeq) ? maxSeq : null,
259
256
  };
260
257
  }
258
+ function getTimelineBounds(rows) {
259
+ const first = rows[0];
260
+ const last = rows[rows.length - 1];
261
+ if (!first || !last) {
262
+ return null;
263
+ }
264
+ return { minSeq: first.seq, maxSeq: last.seq };
265
+ }
266
+ function selectEntriesOverlappingSeqRange(input) {
267
+ return input.entries.filter((entry) => entry.seqStart <= input.endSeq && entry.seqEnd >= input.startSeq);
268
+ }
269
+ export function selectProjectedTimelinePage(input) {
270
+ const limit = input.limit === undefined ? 0 : Math.max(0, Math.floor(input.limit));
271
+ const bounds = input.bounds ?? getTimelineBounds(input.rows);
272
+ const projectedAll = projectTimelineRows({ rows: input.rows, mode: "projected" });
273
+ if (projectedAll.length === 0 || !bounds) {
274
+ return {
275
+ entries: [],
276
+ startSeq: null,
277
+ endSeq: null,
278
+ hasOlder: false,
279
+ hasNewer: false,
280
+ };
281
+ }
282
+ if (input.direction === "tail") {
283
+ const selected = selectTimelineWindowByProjectedLimit({
284
+ rows: input.rows,
285
+ direction: "tail",
286
+ limit,
287
+ });
288
+ return {
289
+ entries: selected.projectedEntries,
290
+ startSeq: selected.minSeq,
291
+ endSeq: selected.maxSeq,
292
+ hasOlder: selected.minSeq !== null && selected.minSeq > bounds.minSeq,
293
+ hasNewer: false,
294
+ };
295
+ }
296
+ let startSeq;
297
+ let endSeq;
298
+ if (input.direction === "after") {
299
+ const cursorSeq = input.cursorSeq ?? bounds.minSeq - 1;
300
+ startSeq = Math.max(bounds.minSeq, cursorSeq + 1);
301
+ endSeq = limit === 0 ? bounds.maxSeq : Math.min(bounds.maxSeq, cursorSeq + limit);
302
+ }
303
+ else {
304
+ const cursorSeq = input.cursorSeq ?? bounds.maxSeq + 1;
305
+ endSeq = Math.min(bounds.maxSeq, cursorSeq - 1);
306
+ startSeq = limit === 0 ? bounds.minSeq : Math.max(bounds.minSeq, cursorSeq - limit);
307
+ }
308
+ if (startSeq > endSeq) {
309
+ return {
310
+ entries: [],
311
+ startSeq: null,
312
+ endSeq: null,
313
+ hasOlder: startSeq > bounds.minSeq,
314
+ hasNewer: endSeq < bounds.maxSeq,
315
+ };
316
+ }
317
+ const entries = selectEntriesOverlappingSeqRange({ entries: projectedAll, startSeq, endSeq });
318
+ return {
319
+ entries,
320
+ startSeq,
321
+ endSeq,
322
+ hasOlder: startSeq > bounds.minSeq,
323
+ hasNewer: endSeq < bounds.maxSeq,
324
+ };
325
+ }
261
326
  export function selectItemsByProjectedLimit(input) {
262
327
  const rows = input.items.map((item, index) => ({
263
328
  seq: index + 1,
@@ -10,6 +10,7 @@ import type { TerminalManager } from "../../terminal/terminal-manager.js";
10
10
  import { isPaseoOwnedWorktreeCwd } from "../../utils/worktree.js";
11
11
  export interface AutoArchiveArchiveOptions {
12
12
  paseoHome: string;
13
+ worktreesRoot?: string;
13
14
  daemonConfigStore: DaemonConfigStore;
14
15
  workspaceGitService: WorkspaceGitServiceImpl;
15
16
  github: GitHubService;
@@ -37,13 +37,17 @@ export async function archiveIfSafe(input) {
37
37
  if (snapshot.git.isDirty === true || (snapshot.git.aheadOfOrigin ?? 0) > 0) {
38
38
  return;
39
39
  }
40
- const ownership = await deps.isPaseoOwnedWorktreeCwd(cwd, { paseoHome: options.paseoHome });
40
+ const ownership = await deps.isPaseoOwnedWorktreeCwd(cwd, {
41
+ paseoHome: options.paseoHome,
42
+ worktreesRoot: options.worktreesRoot,
43
+ });
41
44
  if (!ownership.allowed) {
42
45
  return;
43
46
  }
44
47
  try {
45
48
  await deps.archivePaseoWorktree({
46
49
  paseoHome: options.paseoHome,
50
+ worktreesRoot: options.worktreesRoot,
47
51
  github: options.github,
48
52
  workspaceGitService: options.workspaceGitService,
49
53
  agentManager: options.agentManager,
@@ -64,6 +68,7 @@ export async function archiveIfSafe(input) {
64
68
  targetPath: cwd,
65
69
  repoRoot: ownership.repoRoot ?? null,
66
70
  worktreesRoot: ownership.worktreeRoot,
71
+ worktreesBaseRoot: options.worktreesRoot,
67
72
  requestId: "auto-archive-on-merge",
68
73
  });
69
74
  log.info({ cwd }, "Auto-archived worktree after PR merge");
@@ -21,7 +21,7 @@ import type { PushNotificationSender } from "./push/notifications.js";
21
21
  import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js";
22
22
  import type { AgentProviderRuntimeSettingsMap, ProviderOverride } from "./agent/provider-launch-config.js";
23
23
  import type { PersistedConfig } from "./persisted-config.js";
24
- import { ScriptRouteStore } from "./script-proxy.js";
24
+ import { type ServiceProxySubsystem } from "./service-proxy.js";
25
25
  import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
26
26
  import { type HostnamesConfig } from "./hostnames.js";
27
27
  import { type DaemonAuthConfig } from "./auth.js";
@@ -49,6 +49,7 @@ export type DaemonLifecycleIntent = {
49
49
  export interface PaseoDaemonConfig {
50
50
  listen: string;
51
51
  paseoHome: string;
52
+ worktreesRoot?: string;
52
53
  corsAllowedOrigins: string[];
53
54
  allowedHosts?: HostnamesConfig;
54
55
  hostnames?: HostnamesConfig;
@@ -66,6 +67,10 @@ export interface PaseoDaemonConfig {
66
67
  relayPublicEndpoint?: string;
67
68
  relayUseTls?: boolean;
68
69
  relayPublicUseTls?: boolean;
70
+ serviceProxy?: {
71
+ publicBaseUrl: string | null;
72
+ standaloneListen: string | null;
73
+ };
69
74
  appBaseUrl?: string;
70
75
  auth?: DaemonAuthConfig;
71
76
  openai?: PaseoOpenAIConfig;
@@ -93,7 +98,7 @@ export interface PaseoDaemon {
93
98
  agentManager: AgentManager;
94
99
  agentStorage: AgentStorage;
95
100
  terminalManager: TerminalManager;
96
- scriptRouteStore: ScriptRouteStore;
101
+ serviceProxy: ServiceProxySubsystem;
97
102
  scriptRuntimeStore: WorkspaceScriptRuntimeStore;
98
103
  start(): Promise<void>;
99
104
  stop(): Promise<void>;
@@ -99,7 +99,7 @@ import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
99
99
  import { startRelayTransport } from "./relay-transport.js";
100
100
  import { getOrCreateServerId } from "./server-id.js";
101
101
  import { resolveDaemonVersion } from "./daemon-version.js";
102
- import { ScriptRouteStore, createScriptProxyMiddleware, createScriptProxyUpgradeHandler, } from "./script-proxy.js";
102
+ import { createServiceProxySubsystem } from "./service-proxy.js";
103
103
  import { ScriptHealthMonitor } from "./script-health-monitor.js";
104
104
  import { createScriptStatusEmitter } from "./script-status-projection.js";
105
105
  import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
@@ -187,30 +187,42 @@ export async function createPaseoDaemon(config, rootLogger) {
187
187
  const app = express();
188
188
  let boundListenTarget = null;
189
189
  let workspaceRegistry = null;
190
- const scriptRouteStore = new ScriptRouteStore();
190
+ const serviceProxyPublicBaseUrl = config.serviceProxy?.publicBaseUrl
191
+ ? config.serviceProxy.publicBaseUrl
192
+ : null;
193
+ const serviceProxy = createServiceProxySubsystem({
194
+ logger,
195
+ publicBaseUrl: serviceProxyPublicBaseUrl,
196
+ });
191
197
  const scriptRuntimeStore = new WorkspaceScriptRuntimeStore();
192
198
  const configuredHostnames = config.hostnames ?? config.allowedHosts;
193
199
  let wsServer = null;
200
+ let serviceProxyListenTarget = null;
194
201
  const scriptHealthMonitor = new ScriptHealthMonitor({
195
- routeStore: scriptRouteStore,
202
+ serviceProxy,
196
203
  onChange: createScriptStatusEmitter({
197
204
  sessions: () => wsServer?.listActiveSessions().map((session) => ({
198
205
  emit: (message) => session.emitServerMessage(message),
199
206
  })) ?? [],
200
- routeStore: scriptRouteStore,
207
+ serviceProxy,
201
208
  runtimeStore: scriptRuntimeStore,
202
209
  daemonPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
203
210
  resolveWorkspaceDirectory: async (workspaceId) => (await workspaceRegistry?.get(workspaceId))?.cwd ?? null,
204
211
  logger,
212
+ serviceProxyPublicBaseUrl,
205
213
  }),
206
214
  });
207
215
  const handleBranchChange = createBranchChangeRouteHandler({
208
- routeStore: scriptRouteStore,
216
+ serviceProxy,
209
217
  onRoutesChanged: (workspaceId) => {
210
218
  scriptHealthMonitor.invalidateWorkspace(workspaceId);
211
219
  },
212
220
  logger,
213
221
  });
222
+ // Service proxy classifies service hosts before daemon auth/route fallthrough.
223
+ // Registered service hosts proxy directly; known service namespaces without a
224
+ // route return 404 and never reach daemon APIs.
225
+ app.use(serviceProxy.middleware());
214
226
  // Host allowlist / DNS rebinding protection (vite-like semantics).
215
227
  // For non-TCP (unix sockets), skip host validation.
216
228
  if (listenTarget.type === "tcp") {
@@ -254,10 +266,6 @@ export async function createPaseoDaemon(config, rootLogger) {
254
266
  app.use(createRequireBearerMiddleware(config.auth, (context) => {
255
267
  logger.warn(context, "Rejected HTTP request with invalid daemon password");
256
268
  }));
257
- // Script proxy — intercepts requests for registered *.localhost hostnames
258
- // and forwards them to the corresponding local script port. Placed after
259
- // host/CORS/auth checks but before the rest of the routes.
260
- app.use(createScriptProxyMiddleware({ routeStore: scriptRouteStore, logger }));
261
269
  // Serve static files from public directory
262
270
  app.use("/public", express.static(staticDir));
263
271
  // Middleware
@@ -331,11 +339,10 @@ export async function createPaseoDaemon(config, rootLogger) {
331
339
  // VoiceAssistantWebSocketServer attaches its own "upgrade" listener so that
332
340
  // script-bound upgrades are forwarded first. The handler is a no-op for
333
341
  // requests that don't match a registered script route.
334
- const scriptProxyUpgradeHandler = createScriptProxyUpgradeHandler({
335
- routeStore: scriptRouteStore,
336
- logger,
337
- });
338
- httpServer.on("upgrade", scriptProxyUpgradeHandler);
342
+ httpServer.on("upgrade", serviceProxy.upgradeHandler({ passthroughUnknown: true }));
343
+ if (config.serviceProxy?.standaloneListen) {
344
+ serviceProxyListenTarget = parseListenString(config.serviceProxy.standaloneListen);
345
+ }
339
346
  const agentStorage = new AgentStorage(config.agentStoragePath, logger);
340
347
  const projectRegistry = new FileBackedProjectRegistry(path.join(config.paseoHome, "projects", "projects.json"), logger);
341
348
  workspaceRegistry = new FileBackedWorkspaceRegistry(path.join(config.paseoHome, "projects", "workspaces.json"), logger);
@@ -348,6 +355,7 @@ export async function createPaseoDaemon(config, rootLogger) {
348
355
  const workspaceGitService = new WorkspaceGitServiceImpl({
349
356
  logger,
350
357
  paseoHome: config.paseoHome,
358
+ worktreesRoot: config.worktreesRoot,
351
359
  deps: {
352
360
  github,
353
361
  },
@@ -467,6 +475,7 @@ export async function createPaseoDaemon(config, rootLogger) {
467
475
  };
468
476
  setupAutoArchiveOnMerge({
469
477
  paseoHome: config.paseoHome,
478
+ worktreesRoot: config.worktreesRoot,
470
479
  daemonConfigStore,
471
480
  workspaceGitService,
472
481
  github,
@@ -501,6 +510,7 @@ export async function createPaseoDaemon(config, rootLogger) {
501
510
  createPaseoWorktree: async (input, serviceOptions) => {
502
511
  return createPaseoWorktreeWorkflow({
503
512
  paseoHome: config.paseoHome,
513
+ worktreesRoot: config.worktreesRoot,
504
514
  createPaseoWorktree: async (workflowInput, workflowOptions) => {
505
515
  return createRegisteredPaseoWorktree(workflowInput, {
506
516
  github,
@@ -530,14 +540,16 @@ export async function createPaseoDaemon(config, rootLogger) {
530
540
  sessionLogger: logger,
531
541
  terminalManager,
532
542
  archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
533
- scriptRouteStore,
543
+ serviceProxy,
534
544
  scriptRuntimeStore,
535
545
  getDaemonTcpPort: () => boundListenTarget?.type === "tcp" ? boundListenTarget.port : null,
536
546
  getDaemonTcpHost: () => boundListenTarget?.type === "tcp" ? boundListenTarget.host : null,
547
+ serviceProxyPublicBaseUrl,
537
548
  onScriptsChanged: null,
538
549
  }, input, serviceOptions);
539
550
  },
540
551
  paseoHome: config.paseoHome,
552
+ worktreesRoot: config.worktreesRoot,
541
553
  callerAgentId,
542
554
  enableVoiceTools: false,
543
555
  resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
@@ -652,112 +664,136 @@ export async function createPaseoDaemon(config, rootLogger) {
652
664
  logger.info({ elapsed: elapsed() }, "Speech service created");
653
665
  logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening");
654
666
  const start = async () => {
655
- // Start main HTTP server
656
- await new Promise((resolve, reject) => {
657
- const onError = (err) => {
658
- httpServer.off("listening", onListening);
659
- reject(err);
660
- };
661
- const onListening = () => {
662
- httpServer.off("error", onError);
663
- const logAndResolve = async () => {
664
- boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
665
- const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
666
- agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
667
- agentManager.setMcpBaseUrl(agentMcpBaseUrl);
668
- daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
669
- agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
670
- });
671
- daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
672
- agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
673
- });
674
- const relayEnabled = config.relayEnabled ?? true;
675
- const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
676
- const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
677
- const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
678
- const relayPublicUseTls = config.relayPublicUseTls ?? relayUseTls;
679
- const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
680
- if (boundListenTarget.type === "tcp") {
681
- logger.info({
682
- host: boundListenTarget.host,
683
- port: boundListenTarget.port,
684
- authRequired: !!config.auth?.password,
685
- elapsed: elapsed(),
686
- }, `Server listening on http://${boundListenTarget.host}:${boundListenTarget.port}`);
687
- }
688
- else {
689
- logger.info({
690
- path: boundListenTarget.path,
691
- authRequired: !!config.auth?.password,
692
- elapsed: elapsed(),
693
- }, `Server listening on ${boundListenTarget.path}`);
694
- }
695
- if (config.auth?.password) {
696
- logger.info("Daemon password authentication enabled");
697
- }
698
- wsServer = new VoiceAssistantWebSocketServer(httpServer, logger, serverId, agentManager, agentStorage, downloadTokenStore, config.paseoHome, daemonConfigStore, mcpBaseUrl, { allowedOrigins, hostnames: configuredHostnames }, config.auth, speechService, terminalManager, {
699
- finalTimeoutMs: config.dictationFinalTimeoutMs,
700
- }, daemonVersion, (intent) => {
701
- try {
702
- config.onLifecycleIntent?.(intent);
667
+ let mainStarted = false;
668
+ try {
669
+ if (serviceProxyListenTarget) {
670
+ const boundServiceProxyTarget = await serviceProxy.startStandalone({
671
+ listenTarget: serviceProxyListenTarget,
672
+ });
673
+ serviceProxyListenTarget = boundServiceProxyTarget;
674
+ logger.info({
675
+ listen: formatListenTarget(serviceProxyListenTarget),
676
+ publicBaseUrl: serviceProxyPublicBaseUrl,
677
+ elapsed: elapsed(),
678
+ }, "Service proxy listening");
679
+ }
680
+ // Start main HTTP server
681
+ await new Promise((resolve, reject) => {
682
+ const onError = (err) => {
683
+ httpServer.off("listening", onListening);
684
+ reject(err);
685
+ };
686
+ const onListening = () => {
687
+ httpServer.off("error", onError);
688
+ mainStarted = true;
689
+ const logAndResolve = async () => {
690
+ boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
691
+ const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
692
+ agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
693
+ agentManager.setMcpBaseUrl(agentMcpBaseUrl);
694
+ daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
695
+ agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
696
+ });
697
+ daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
698
+ agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
699
+ });
700
+ const relayEnabled = config.relayEnabled ?? true;
701
+ const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
702
+ const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
703
+ const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
704
+ const relayPublicUseTls = config.relayPublicUseTls ?? relayUseTls;
705
+ const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
706
+ if (boundListenTarget.type === "tcp") {
707
+ logger.info({
708
+ host: boundListenTarget.host,
709
+ port: boundListenTarget.port,
710
+ authRequired: !!config.auth?.password,
711
+ elapsed: elapsed(),
712
+ }, `Server listening on http://${boundListenTarget.host}:${boundListenTarget.port}`);
703
713
  }
704
- catch (error) {
705
- logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent");
714
+ else {
715
+ logger.info({
716
+ path: boundListenTarget.path,
717
+ authRequired: !!config.auth?.password,
718
+ elapsed: elapsed(),
719
+ }, `Server listening on ${boundListenTarget.path}`);
706
720
  }
707
- }, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager, scriptRouteStore, scriptRuntimeStore, handleBranchChange, () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null), () => (boundListenTarget?.type === "tcp" ? boundListenTarget.host : null), (hostname) => scriptHealthMonitor.getHealthForHostname(hostname), workspaceGitService, github, config.pushNotificationSender, providerSnapshotManager, {
708
- listen: formatListenTarget(boundListenTarget ?? listenTarget),
709
- relay: {
710
- enabled: relayEnabled,
711
- endpoint: relayEndpoint,
712
- publicEndpoint: relayPublicEndpoint,
713
- useTls: relayUseTls,
714
- publicUseTls: relayPublicUseTls,
715
- },
716
- });
717
- if (relayEnabled) {
718
- const offer = await createConnectionOfferV2({
719
- serverId,
720
- daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
721
+ if (config.auth?.password) {
722
+ logger.info("Daemon password authentication enabled");
723
+ }
724
+ wsServer = new VoiceAssistantWebSocketServer(httpServer, logger, serverId, agentManager, agentStorage, downloadTokenStore, config.paseoHome, daemonConfigStore, mcpBaseUrl, { allowedOrigins, hostnames: configuredHostnames }, config.auth, speechService, terminalManager, {
725
+ finalTimeoutMs: config.dictationFinalTimeoutMs,
726
+ }, daemonVersion, (intent) => {
727
+ try {
728
+ config.onLifecycleIntent?.(intent);
729
+ }
730
+ catch (error) {
731
+ logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent");
732
+ }
733
+ }, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager, serviceProxy, scriptRuntimeStore, handleBranchChange, () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null), () => (boundListenTarget?.type === "tcp" ? boundListenTarget.host : null), (hostname) => scriptHealthMonitor.getHealthForHostname(hostname), workspaceGitService, github, config.pushNotificationSender, providerSnapshotManager, {
734
+ listen: formatListenTarget(boundListenTarget ?? listenTarget),
735
+ worktreesRoot: config.worktreesRoot,
721
736
  relay: {
722
- endpoint: relayPublicEndpoint,
723
- useTls: relayPublicUseTls,
737
+ enabled: relayEnabled,
738
+ endpoint: relayEndpoint,
739
+ publicEndpoint: relayPublicEndpoint,
740
+ useTls: relayUseTls,
741
+ publicUseTls: relayPublicUseTls,
724
742
  },
725
- });
726
- encodeOfferToFragmentUrl({ offer, appBaseUrl });
727
- relayTransport?.stop().catch(() => undefined);
728
- relayTransport = startRelayTransport({
729
- logger,
730
- attachSocket: (ws, metadata) => {
731
- if (!wsServer) {
732
- throw new Error("WebSocket server not initialized");
733
- }
734
- return wsServer.attachExternalSocket(ws, metadata);
735
- },
736
- relayEndpoint,
737
- relayUseTls,
738
- serverId,
739
- daemonKeyPair: daemonKeyPair.keyPair,
740
- });
741
- }
743
+ }, serviceProxyPublicBaseUrl);
744
+ if (relayEnabled) {
745
+ const offer = await createConnectionOfferV2({
746
+ serverId,
747
+ daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
748
+ relay: {
749
+ endpoint: relayPublicEndpoint,
750
+ useTls: relayPublicUseTls,
751
+ },
752
+ });
753
+ encodeOfferToFragmentUrl({ offer, appBaseUrl });
754
+ relayTransport?.stop().catch(() => undefined);
755
+ relayTransport = startRelayTransport({
756
+ logger,
757
+ attachSocket: (ws, metadata) => {
758
+ if (!wsServer) {
759
+ throw new Error("WebSocket server not initialized");
760
+ }
761
+ return wsServer.attachExternalSocket(ws, metadata);
762
+ },
763
+ relayEndpoint,
764
+ relayUseTls,
765
+ serverId,
766
+ daemonKeyPair: daemonKeyPair.keyPair,
767
+ });
768
+ }
769
+ };
770
+ logAndResolve().then(resolve, reject);
742
771
  };
743
- logAndResolve().then(resolve, reject);
744
- };
745
- httpServer.once("error", onError);
746
- httpServer.once("listening", onListening);
747
- if (listenTarget.type === "tcp") {
748
- httpServer.listen(listenTarget.port, listenTarget.host);
749
- }
750
- else {
751
- if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
752
- unlinkSync(listenTarget.path);
772
+ httpServer.once("error", onError);
773
+ httpServer.once("listening", onListening);
774
+ if (listenTarget.type === "tcp") {
775
+ httpServer.listen(listenTarget.port, listenTarget.host);
753
776
  }
754
- httpServer.listen(listenTarget.path);
777
+ else {
778
+ if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
779
+ unlinkSync(listenTarget.path);
780
+ }
781
+ httpServer.listen(listenTarget.path);
782
+ }
783
+ });
784
+ // Start speech service after listening so synchronous Sherpa native
785
+ // model loading doesn't block the server from accepting connections.
786
+ speechService.start();
787
+ scriptHealthMonitor.start();
788
+ }
789
+ catch (error) {
790
+ await serviceProxy.stopStandalone().catch(() => undefined);
791
+ if (mainStarted) {
792
+ httpServer.closeAllConnections();
793
+ await new Promise((resolve) => httpServer.close(() => resolve()));
755
794
  }
756
- });
757
- // Start speech service after listening so synchronous Sherpa native
758
- // model loading doesn't block the server from accepting connections.
759
- speechService.start();
760
- scriptHealthMonitor.start();
795
+ throw error;
796
+ }
761
797
  };
762
798
  const stop = async () => {
763
799
  scriptHealthMonitor.stop();
@@ -773,6 +809,7 @@ export async function createPaseoDaemon(config, rootLogger) {
773
809
  if (wsServer) {
774
810
  await wsServer.close();
775
811
  }
812
+ await serviceProxy.stopStandalone();
776
813
  // Force-drop remaining sockets so httpServer.close() resolves promptly.
777
814
  // We've already closed wsServer (which sent ws-layer close frames) and
778
815
  // stopped every other service, so anything still attached is a TCP
@@ -794,7 +831,7 @@ export async function createPaseoDaemon(config, rootLogger) {
794
831
  agentManager,
795
832
  agentStorage,
796
833
  terminalManager,
797
- scriptRouteStore,
834
+ serviceProxy,
798
835
  scriptRuntimeStore,
799
836
  start,
800
837
  stop,