@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.
- package/dist/server/server/agent/agent-manager.js +4 -1
- package/dist/server/server/agent/agent-storage.d.ts +22 -22
- package/dist/server/server/agent/create-agent/create.d.ts +2 -0
- package/dist/server/server/agent/create-agent/create.js +16 -5
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
- package/dist/server/server/agent/mcp-server.d.ts +1 -0
- package/dist/server/server/agent/mcp-server.js +137 -63
- package/dist/server/server/agent/mcp-shared.d.ts +1 -0
- package/dist/server/server/agent/providers/pi/agent.js +13 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
- package/dist/server/server/agent/timeline-projection.d.ts +17 -1
- package/dist/server/server/agent/timeline-projection.js +82 -17
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
- package/dist/server/server/bootstrap.d.ts +7 -2
- package/dist/server/server/bootstrap.js +152 -115
- package/dist/server/server/config.js +41 -0
- package/dist/server/server/loop-service.d.ts +22 -22
- package/dist/server/server/package-version.d.ts +2 -2
- package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
- package/dist/server/server/paseo-worktree-archive-service.js +28 -9
- package/dist/server/server/persisted-config.d.ts +89 -33
- package/dist/server/server/persisted-config.js +17 -0
- package/dist/server/server/pid-lock.d.ts +2 -2
- package/dist/server/server/schedule/cron.js +52 -5
- package/dist/server/server/script-health-monitor.d.ts +4 -4
- package/dist/server/server/script-health-monitor.js +6 -6
- package/dist/server/server/script-proxy.d.ts +2 -39
- package/dist/server/server/script-proxy.js +1 -244
- package/dist/server/server/script-route-branch-handler.d.ts +2 -2
- package/dist/server/server/script-route-branch-handler.js +3 -37
- package/dist/server/server/script-status-projection.d.ts +6 -4
- package/dist/server/server/script-status-projection.js +85 -37
- package/dist/server/server/service-proxy.d.ts +237 -0
- package/dist/server/server/service-proxy.js +714 -0
- package/dist/server/server/session.d.ts +11 -4
- package/dist/server/server/session.js +96 -99
- package/dist/server/server/websocket-server.d.ts +7 -4
- package/dist/server/server/websocket-server.js +9 -4
- package/dist/server/server/workspace-directory.js +4 -0
- package/dist/server/server/workspace-git-service.d.ts +3 -0
- package/dist/server/server/workspace-git-service.js +53 -12
- package/dist/server/server/workspace-registry.d.ts +2 -2
- package/dist/server/server/workspace-service-env.d.ts +1 -0
- package/dist/server/server/workspace-service-env.js +23 -18
- package/dist/server/server/worktree/commands.d.ts +2 -0
- package/dist/server/server/worktree/commands.js +4 -1
- package/dist/server/server/worktree-bootstrap.d.ts +4 -3
- package/dist/server/server/worktree-bootstrap.js +14 -13
- package/dist/server/server/worktree-core.d.ts +1 -0
- package/dist/server/server/worktree-core.js +2 -0
- package/dist/server/server/worktree-session.d.ts +6 -2
- package/dist/server/server/worktree-session.js +3 -0
- package/dist/server/services/github-service.d.ts +1 -0
- package/dist/server/services/github-service.js +7 -1
- package/dist/server/terminal/terminal-manager.js +11 -1
- package/dist/server/terminal/terminal-session-controller.d.ts +3 -1
- package/dist/server/terminal/terminal-session-controller.js +22 -12
- package/dist/server/terminal/terminal.d.ts +1 -0
- package/dist/server/terminal/terminal.js +34 -0
- package/dist/server/utils/checkout-git.d.ts +6 -2
- package/dist/server/utils/checkout-git.js +136 -54
- package/dist/server/utils/worktree.d.ts +17 -12
- package/dist/server/utils/worktree.js +39 -22
- package/dist/src/server/persisted-config.js +17 -0
- package/package.json +5 -5
- package/dist/server/utils/script-hostname.d.ts +0 -8
- 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(
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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, {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
202
|
+
serviceProxy,
|
|
196
203
|
onChange: createScriptStatusEmitter({
|
|
197
204
|
sessions: () => wsServer?.listActiveSessions().map((session) => ({
|
|
198
205
|
emit: (message) => session.emitServerMessage(message),
|
|
199
206
|
})) ?? [],
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
705
|
-
logger.
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
723
|
-
|
|
737
|
+
enabled: relayEnabled,
|
|
738
|
+
endpoint: relayEndpoint,
|
|
739
|
+
publicEndpoint: relayPublicEndpoint,
|
|
740
|
+
useTls: relayUseTls,
|
|
741
|
+
publicUseTls: relayPublicUseTls,
|
|
724
742
|
},
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
834
|
+
serviceProxy,
|
|
798
835
|
scriptRuntimeStore,
|
|
799
836
|
start,
|
|
800
837
|
stop,
|