@getpaseo/server 0.1.88 → 0.1.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/server/agent/agent-manager.js +4 -1
- package/dist/server/server/agent/agent-prompt.js +4 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
- package/dist/server/server/agent/agent-storage.d.ts +22 -22
- package/dist/server/server/agent/agent-storage.js +2 -9
- package/dist/server/server/agent/create-agent/create.d.ts +2 -0
- package/dist/server/server/agent/create-agent/create.js +26 -7
- 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/create-agent-mode.d.ts +3 -8
- package/dist/server/server/agent/create-agent-mode.js +16 -2
- package/dist/server/server/agent/import-sessions.js +1 -1
- package/dist/server/server/agent/mcp-server.d.ts +1 -0
- package/dist/server/server/agent/mcp-server.js +113 -70
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +2 -1
- package/dist/server/server/agent/provider-snapshot-manager.js +18 -2
- package/dist/server/server/agent/providers/acp-agent.d.ts +3 -3
- package/dist/server/server/agent/providers/acp-agent.js +18 -13
- package/dist/server/server/agent/providers/codex-app-server-agent.js +16 -22
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.js +69 -2
- package/dist/server/server/agent/providers/opencode-agent.js +19 -8
- 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.js +30 -1
- package/dist/server/server/atomic-file.d.ts +3 -0
- package/dist/server/server/atomic-file.js +19 -0
- 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 +10 -2
- package/dist/server/server/bootstrap.d.ts +7 -2
- package/dist/server/server/bootstrap.js +154 -115
- package/dist/server/server/chat/chat-service.js +2 -4
- package/dist/server/server/config.js +41 -0
- package/dist/server/server/daemon-keypair.js +2 -2
- package/dist/server/server/loop-service.d.ts +26 -22
- package/dist/server/server/loop-service.js +27 -9
- 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 +84 -28
- package/dist/server/server/persisted-config.js +20 -3
- package/dist/server/server/pid-lock.d.ts +2 -2
- package/dist/server/server/private-files.d.ts +0 -1
- package/dist/server/server/private-files.js +0 -5
- package/dist/server/server/schedule/service.d.ts +6 -0
- package/dist/server/server/schedule/service.js +41 -18
- package/dist/server/server/schedule/store.js +3 -2
- 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/server-id.js +3 -3
- 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 +12 -18
- package/dist/server/server/session.js +206 -117
- package/dist/server/server/speech/providers/local/worker-client.js +1 -11
- package/dist/server/server/websocket-server.d.ts +7 -4
- package/dist/server/server/websocket-server.js +9 -4
- package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
- package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
- package/dist/server/server/workspace-directory.d.ts +8 -0
- package/dist/server/server/workspace-directory.js +141 -11
- 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-registry.js +2 -6
- 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/utils/checkout-git.d.ts +6 -3
- package/dist/server/utils/checkout-git.js +40 -38
- package/dist/server/utils/worktree.d.ts +17 -12
- package/dist/server/utils/worktree.js +39 -22
- package/dist/src/server/persisted-config.js +20 -3
- package/dist/src/server/private-files.js +0 -5
- package/package.json +9 -7
- package/dist/server/server/editor-targets.d.ts +0 -18
- package/dist/server/server/editor-targets.js +0 -109
- package/dist/server/utils/script-hostname.d.ts +0 -8
- package/dist/server/utils/script-hostname.js +0 -14
|
@@ -202,7 +202,7 @@ function requireWebSocketServices(params) {
|
|
|
202
202
|
* WebSocket server that only accepts sockets + parses/forwards messages to the session layer.
|
|
203
203
|
*/
|
|
204
204
|
export class VoiceAssistantWebSocketServer {
|
|
205
|
-
constructor(server, logger, serverId, agentManager, agentStorage, downloadTokenStore, paseoHome, daemonConfigStore, mcpBaseUrl, wsConfig, auth, speech, terminalManager, dictation, daemonVersion, onLifecycleIntent, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager,
|
|
205
|
+
constructor(server, logger, serverId, agentManager, agentStorage, downloadTokenStore, paseoHome, daemonConfigStore, mcpBaseUrl, wsConfig, auth, speech, terminalManager, dictation, daemonVersion, onLifecycleIntent, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager, serviceProxy, scriptRuntimeStore, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, resolveScriptHealth, workspaceGitService, github, pushNotificationSender, providerSnapshotManager, daemonRuntimeConfig, serviceProxyPublicBaseUrl) {
|
|
206
206
|
this.pendingConnections = new Map();
|
|
207
207
|
this.sessions = new Map();
|
|
208
208
|
this.externalSessionsByKey = new Map();
|
|
@@ -238,6 +238,7 @@ export class VoiceAssistantWebSocketServer {
|
|
|
238
238
|
this.workspaceGitService = workspaceGitService ?? createFallbackWorkspaceGitService();
|
|
239
239
|
this.downloadTokenStore = downloadTokenStore;
|
|
240
240
|
this.paseoHome = paseoHome;
|
|
241
|
+
this.worktreesRoot = daemonRuntimeConfig?.worktreesRoot;
|
|
241
242
|
this.daemonConfigStore = daemonConfigStore;
|
|
242
243
|
this.mcpBaseUrl = mcpBaseUrl;
|
|
243
244
|
this.assignOptionalServices({
|
|
@@ -245,11 +246,12 @@ export class VoiceAssistantWebSocketServer {
|
|
|
245
246
|
terminalManager,
|
|
246
247
|
dictation,
|
|
247
248
|
onLifecycleIntent,
|
|
248
|
-
|
|
249
|
+
serviceProxy,
|
|
249
250
|
scriptRuntimeStore,
|
|
250
251
|
onBranchChanged,
|
|
251
252
|
getDaemonTcpPort,
|
|
252
253
|
getDaemonTcpHost,
|
|
254
|
+
serviceProxyPublicBaseUrl,
|
|
253
255
|
resolveScriptHealth,
|
|
254
256
|
});
|
|
255
257
|
if (!providerSnapshotManager) {
|
|
@@ -286,11 +288,12 @@ export class VoiceAssistantWebSocketServer {
|
|
|
286
288
|
this.terminalManager = params.terminalManager ?? null;
|
|
287
289
|
this.dictation = params.dictation ?? null;
|
|
288
290
|
this.onLifecycleIntent = params.onLifecycleIntent ?? null;
|
|
289
|
-
this.
|
|
291
|
+
this.serviceProxy = params.serviceProxy ?? null;
|
|
290
292
|
this.scriptRuntimeStore = params.scriptRuntimeStore ?? null;
|
|
291
293
|
this.onBranchChanged = params.onBranchChanged ?? null;
|
|
292
294
|
this.getDaemonTcpPort = params.getDaemonTcpPort ?? null;
|
|
293
295
|
this.getDaemonTcpHost = params.getDaemonTcpHost ?? null;
|
|
296
|
+
this.serviceProxyPublicBaseUrl = params.serviceProxyPublicBaseUrl ?? null;
|
|
294
297
|
this.resolveScriptHealth = params.resolveScriptHealth ?? null;
|
|
295
298
|
}
|
|
296
299
|
createWebSocketServer(server, wsConfig, auth) {
|
|
@@ -550,6 +553,7 @@ export class VoiceAssistantWebSocketServer {
|
|
|
550
553
|
downloadTokenStore: this.downloadTokenStore,
|
|
551
554
|
pushTokenStore: this.pushTokenStore,
|
|
552
555
|
paseoHome: this.paseoHome,
|
|
556
|
+
worktreesRoot: this.worktreesRoot,
|
|
553
557
|
agentManager: this.agentManager,
|
|
554
558
|
agentStorage: this.agentStorage,
|
|
555
559
|
projectRegistry: this.projectRegistry,
|
|
@@ -567,12 +571,13 @@ export class VoiceAssistantWebSocketServer {
|
|
|
567
571
|
tts: () => this.speech?.resolveTts() ?? null,
|
|
568
572
|
terminalManager: this.terminalManager,
|
|
569
573
|
providerSnapshotManager: this.providerSnapshotManager,
|
|
570
|
-
|
|
574
|
+
serviceProxy: this.serviceProxy ?? undefined,
|
|
571
575
|
scriptRuntimeStore: this.scriptRuntimeStore ?? undefined,
|
|
572
576
|
workspaceSetupSnapshots: this.workspaceSetupSnapshots,
|
|
573
577
|
onBranchChanged: this.onBranchChanged ?? undefined,
|
|
574
578
|
getDaemonTcpPort: this.getDaemonTcpPort ?? undefined,
|
|
575
579
|
getDaemonTcpHost: this.getDaemonTcpHost ?? undefined,
|
|
580
|
+
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
|
|
576
581
|
resolveScriptHealth: this.resolveScriptHealth ?? undefined,
|
|
577
582
|
voice: {
|
|
578
583
|
turnDetection: () => this.speech?.resolveTurnDetection() ?? null,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure dedupe decision for the bootstrap flush.
|
|
3
|
+
*
|
|
4
|
+
* During the bootstrap window the session buffers workspace updates that
|
|
5
|
+
* race with the initial `fetch_workspaces_response`. The flush step decides
|
|
6
|
+
* which buffered updates still carry new information and which are
|
|
7
|
+
* redundant with what the client just received in the snapshot.
|
|
8
|
+
*
|
|
9
|
+
* Returns `true` (emit) when ANY of:
|
|
10
|
+
* - the status changed from the snapshot
|
|
11
|
+
* - the statusEnteredAt changed from the snapshot (including the
|
|
12
|
+
* null↔value transition that the unmask case produces)
|
|
13
|
+
* - the update's activityAtMs is strictly newer than the snapshot's
|
|
14
|
+
* - the snapshot has no activityAtMs and the update has one (new activity
|
|
15
|
+
* where there was none)
|
|
16
|
+
*
|
|
17
|
+
* Returns `false` (drop) when the status pair is unchanged AND the update
|
|
18
|
+
* is not strictly newer than the snapshot in activity. The both-null
|
|
19
|
+
* activity case falls through to drop — there is genuinely no new info.
|
|
20
|
+
*/
|
|
21
|
+
export interface BootstrapUpdateSnapshot {
|
|
22
|
+
status: string;
|
|
23
|
+
statusEnteredAt: string | null;
|
|
24
|
+
activityAtMs: number | null;
|
|
25
|
+
}
|
|
26
|
+
export interface BootstrapUpdateCheckInput {
|
|
27
|
+
/** Snapshot captured from the fetch_workspaces_response. `null` means
|
|
28
|
+
* the workspace was not in the snapshot (first-time subscription). */
|
|
29
|
+
snapshot: BootstrapUpdateSnapshot | null;
|
|
30
|
+
/** Pending update buffered during the bootstrap window. */
|
|
31
|
+
update: BootstrapUpdateSnapshot;
|
|
32
|
+
}
|
|
33
|
+
export declare function shouldEmitPendingBootstrapUpdate(input: BootstrapUpdateCheckInput): boolean;
|
|
34
|
+
//# sourceMappingURL=workspace-bootstrap-dedupe.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function shouldEmitPendingBootstrapUpdate(input) {
|
|
2
|
+
const { snapshot, update } = input;
|
|
3
|
+
if (!snapshot) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
if (snapshot.status !== update.status) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
const snapshotEnteredAt = snapshot.statusEnteredAt ?? null;
|
|
10
|
+
const updateEnteredAt = update.statusEnteredAt ?? null;
|
|
11
|
+
if (snapshotEnteredAt !== updateEnteredAt) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
// Status pair is unchanged. The only remaining signal is activity.
|
|
15
|
+
if (update.activityAtMs === null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (snapshot.activityAtMs === null) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return update.activityAtMs > snapshot.activityAtMs;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=workspace-bootstrap-dedupe.js.map
|
|
@@ -44,6 +44,12 @@ export declare function summarizeFetchWorkspacesEntries(entries: Iterable<FetchW
|
|
|
44
44
|
export declare class WorkspaceDirectory {
|
|
45
45
|
private readonly deps;
|
|
46
46
|
private readonly archivingByWorkspaceId;
|
|
47
|
+
/**
|
|
48
|
+
* Per-workspace last-seen winning bucket + entered-at. Persists across
|
|
49
|
+
* `buildDescriptorMap` calls inside the daemon process; reset on cold start.
|
|
50
|
+
* Server-internal; never crosses the wire.
|
|
51
|
+
*/
|
|
52
|
+
private readonly bucketHistoryByWorkspaceId;
|
|
47
53
|
private readonly pager;
|
|
48
54
|
constructor(deps: WorkspaceDirectoryDeps);
|
|
49
55
|
markArchiving(workspaceIds: Iterable<string>, archivingAt: string): void;
|
|
@@ -52,6 +58,8 @@ export declare class WorkspaceDirectory {
|
|
|
52
58
|
includeGitData: boolean;
|
|
53
59
|
workspaceIds?: Iterable<string>;
|
|
54
60
|
}): Promise<Map<string, WorkspaceDescriptorPayload>>;
|
|
61
|
+
private resolveStatusEnteredAt;
|
|
62
|
+
private findNewestAgentTimestampInBucket;
|
|
55
63
|
resolveRegisteredWorkspaceIdForCwd(cwd: string, workspaces: PersistedWorkspaceRecord[]): string;
|
|
56
64
|
listDescriptors(): Promise<WorkspaceDescriptorPayload[]>;
|
|
57
65
|
matchesFilter(input: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { sep } from "node:path";
|
|
3
3
|
import { deriveAgentStateBucket, getWorkspaceStateBucketPriority, } from "@getpaseo/protocol/agent-state-bucket";
|
|
4
|
+
import { getParentAgentIdFromLabels, isDelegatedAgent } from "@getpaseo/protocol/agent-labels";
|
|
4
5
|
import { SortablePager } from "./pagination/sortable-pager.js";
|
|
5
6
|
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
|
|
6
7
|
const FETCH_WORKSPACES_SORT_KEYS = [
|
|
@@ -34,6 +35,12 @@ export class WorkspaceDirectory {
|
|
|
34
35
|
constructor(deps) {
|
|
35
36
|
this.deps = deps;
|
|
36
37
|
this.archivingByWorkspaceId = new Map();
|
|
38
|
+
/**
|
|
39
|
+
* Per-workspace last-seen winning bucket + entered-at. Persists across
|
|
40
|
+
* `buildDescriptorMap` calls inside the daemon process; reset on cold start.
|
|
41
|
+
* Server-internal; never crosses the wire.
|
|
42
|
+
*/
|
|
43
|
+
this.bucketHistoryByWorkspaceId = new Map();
|
|
37
44
|
this.pager = new SortablePager({
|
|
38
45
|
validKeys: FETCH_WORKSPACES_SORT_KEYS,
|
|
39
46
|
defaultSort: [{ key: "activity_at", direction: "desc" }],
|
|
@@ -92,14 +99,31 @@ export class WorkspaceDirectory {
|
|
|
92
99
|
archivingAt: this.archivingByWorkspaceId.get(workspaceId) ?? null,
|
|
93
100
|
});
|
|
94
101
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
const activeAgents = agents.filter((agent) => !agent.archivedAt && this.deps.isProviderVisibleToClient(agent.provider));
|
|
103
|
+
const activeAgentsById = new Map(activeAgents.map((agent) => [agent.id, agent]));
|
|
104
|
+
for (const agent of activeAgents) {
|
|
105
|
+
let workspaceAgent = agent;
|
|
106
|
+
let bucket;
|
|
107
|
+
if (isDelegatedAgent(agent)) {
|
|
108
|
+
if (agent.status !== "running") {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const parentAgent = resolveDelegationRootAgent(agent, activeAgentsById);
|
|
112
|
+
if (!parentAgent) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
workspaceAgent = parentAgent;
|
|
116
|
+
bucket = "running";
|
|
98
117
|
}
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
else {
|
|
119
|
+
bucket = deriveAgentStateBucket({
|
|
120
|
+
status: agent.status,
|
|
121
|
+
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
|
|
122
|
+
requiresAttention: agent.requiresAttention,
|
|
123
|
+
attentionReason: agent.attentionReason ?? null,
|
|
124
|
+
});
|
|
101
125
|
}
|
|
102
|
-
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(
|
|
126
|
+
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(workspaceAgent.cwd));
|
|
103
127
|
if (workspaceId === undefined) {
|
|
104
128
|
continue;
|
|
105
129
|
}
|
|
@@ -107,17 +131,104 @@ export class WorkspaceDirectory {
|
|
|
107
131
|
if (!existing) {
|
|
108
132
|
continue;
|
|
109
133
|
}
|
|
110
|
-
|
|
134
|
+
if (getWorkspaceStateBucketPriority(bucket) < getWorkspaceStateBucketPriority(existing.status)) {
|
|
135
|
+
existing.status = bucket;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Resolve the workspace-level `statusEnteredAt` (see aggregate semantics
|
|
139
|
+
// on `resolveStatusEnteredAt`).
|
|
140
|
+
const nowIso = new Date().toISOString();
|
|
141
|
+
for (const [workspaceId, descriptor] of descriptorsByWorkspaceId) {
|
|
142
|
+
const contributingAgents = agents.filter((agent) => !agent.archivedAt &&
|
|
143
|
+
this.deps.isProviderVisibleToClient(agent.provider) &&
|
|
144
|
+
workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd)) === workspaceId);
|
|
145
|
+
const result = this.resolveStatusEnteredAt({
|
|
146
|
+
workspaceId,
|
|
147
|
+
winningBucket: descriptor.status,
|
|
148
|
+
contributingAgents,
|
|
149
|
+
previous: this.bucketHistoryByWorkspaceId.get(workspaceId) ?? null,
|
|
150
|
+
nowIso,
|
|
151
|
+
});
|
|
152
|
+
descriptor.statusEnteredAt = result.statusEnteredAt;
|
|
153
|
+
if (result.recordUpdate) {
|
|
154
|
+
this.bucketHistoryByWorkspaceId.set(workspaceId, result.recordUpdate);
|
|
155
|
+
}
|
|
156
|
+
else if (result.recordDelete) {
|
|
157
|
+
this.bucketHistoryByWorkspaceId.delete(workspaceId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return descriptorsByWorkspaceId;
|
|
161
|
+
}
|
|
162
|
+
// Aggregate the workspace-level `statusEnteredAt` from its contributing
|
|
163
|
+
// agents. Aggregate semantics:
|
|
164
|
+
// - winning bucket = highest-priority across contributing agents;
|
|
165
|
+
// - entry time = best-effort timestamp from agents in the winning bucket;
|
|
166
|
+
// - priority unmasking: when the winning bucket transitions (e.g. a
|
|
167
|
+
// higher-priority bucket cleared), the new entry time is "now";
|
|
168
|
+
// - same-bucket emits reuse the previous entered-at;
|
|
169
|
+
// - empty workspaces that never had contributing agents get
|
|
170
|
+
// `statusEnteredAt: null`.
|
|
171
|
+
// - when archived agents leave a previously active workspace empty, keep
|
|
172
|
+
// the previous done timestamp or stamp the transition to done now.
|
|
173
|
+
resolveStatusEnteredAt(params) {
|
|
174
|
+
const { winningBucket, contributingAgents, previous, nowIso } = params;
|
|
175
|
+
if (contributingAgents.length === 0) {
|
|
176
|
+
if (!previous) {
|
|
177
|
+
return { statusEnteredAt: null };
|
|
178
|
+
}
|
|
179
|
+
const enteredAt = previous.bucket === "done" ? previous.enteredAt : nowIso;
|
|
180
|
+
return {
|
|
181
|
+
statusEnteredAt: enteredAt,
|
|
182
|
+
recordUpdate: { bucket: "done", enteredAt },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (!previous) {
|
|
186
|
+
const newestInWinningBucket = this.findNewestAgentTimestampInBucket(contributingAgents, winningBucket);
|
|
187
|
+
const enteredAt = newestInWinningBucket ?? nowIso;
|
|
188
|
+
return {
|
|
189
|
+
statusEnteredAt: enteredAt,
|
|
190
|
+
recordUpdate: { bucket: winningBucket, enteredAt },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (previous.bucket !== winningBucket) {
|
|
194
|
+
return {
|
|
195
|
+
statusEnteredAt: nowIso,
|
|
196
|
+
recordUpdate: { bucket: winningBucket, enteredAt: nowIso },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
statusEnteredAt: previous.enteredAt,
|
|
201
|
+
recordUpdate: previous,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Best-effort newest timestamp across contributing agents whose derived
|
|
205
|
+
// bucket matches `winningBucket`. Uses available agent fields:
|
|
206
|
+
// - `attentionTimestamp` when attention is set (covers attention/failed)
|
|
207
|
+
// - `updatedAt` as a general fallback for any bucket
|
|
208
|
+
// Returns `null` if no matching agent has a parseable timestamp.
|
|
209
|
+
findNewestAgentTimestampInBucket(contributingAgents, winningBucket) {
|
|
210
|
+
const candidates = contributingAgents
|
|
211
|
+
.filter((agent) => {
|
|
212
|
+
const derived = deriveAgentStateBucket({
|
|
111
213
|
status: agent.status,
|
|
112
214
|
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
|
|
113
215
|
requiresAttention: agent.requiresAttention,
|
|
114
216
|
attentionReason: agent.attentionReason ?? null,
|
|
115
217
|
});
|
|
116
|
-
|
|
117
|
-
|
|
218
|
+
return derived === winningBucket;
|
|
219
|
+
})
|
|
220
|
+
.map((agent) => {
|
|
221
|
+
// Prefer attentionTimestamp when the agent has attention set — this is
|
|
222
|
+
// the most accurate "entered current status" signal.
|
|
223
|
+
if (agent.attentionTimestamp) {
|
|
224
|
+
return agent.attentionTimestamp;
|
|
118
225
|
}
|
|
119
|
-
|
|
120
|
-
|
|
226
|
+
// Fall back to updatedAt as a general proxy for recent activity.
|
|
227
|
+
return agent.updatedAt;
|
|
228
|
+
})
|
|
229
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
230
|
+
.sort();
|
|
231
|
+
return candidates.at(-1) ?? null;
|
|
121
232
|
}
|
|
122
233
|
resolveRegisteredWorkspaceIdForCwd(cwd, workspaces) {
|
|
123
234
|
const normalizedCwd = normalizeWorkspaceId(cwd);
|
|
@@ -211,4 +322,23 @@ export class WorkspaceDirectory {
|
|
|
211
322
|
};
|
|
212
323
|
}
|
|
213
324
|
}
|
|
325
|
+
function resolveDelegationRootAgent(agent, activeAgentsById) {
|
|
326
|
+
const seen = new Set([agent.id]);
|
|
327
|
+
let current = agent;
|
|
328
|
+
while (true) {
|
|
329
|
+
const parentAgentId = getParentAgentIdFromLabels(current.labels);
|
|
330
|
+
if (!parentAgentId) {
|
|
331
|
+
return current;
|
|
332
|
+
}
|
|
333
|
+
if (seen.has(parentAgentId)) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const parent = activeAgentsById.get(parentAgentId);
|
|
337
|
+
if (!parent) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
seen.add(parentAgentId);
|
|
341
|
+
current = parent;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
214
344
|
//# sourceMappingURL=workspace-directory.js.map
|
|
@@ -150,11 +150,13 @@ interface WorkspaceGitServiceDependencies {
|
|
|
150
150
|
interface WorkspaceGitServiceOptions {
|
|
151
151
|
logger: pino.Logger;
|
|
152
152
|
paseoHome: string;
|
|
153
|
+
worktreesRoot?: string;
|
|
153
154
|
deps?: Partial<WorkspaceGitServiceDependencies>;
|
|
154
155
|
}
|
|
155
156
|
export declare class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|
156
157
|
private readonly logger;
|
|
157
158
|
private readonly paseoHome;
|
|
159
|
+
private readonly worktreesRoot;
|
|
158
160
|
private readonly deps;
|
|
159
161
|
private readonly snapshotUpdatedListeners;
|
|
160
162
|
private readonly workspaceTargets;
|
|
@@ -219,6 +221,7 @@ export declare class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|
|
219
221
|
private scheduleWorkspaceRefresh;
|
|
220
222
|
private startWorkspaceSubscriptionTimers;
|
|
221
223
|
private updateGitHubPollForTarget;
|
|
224
|
+
private resolveGitHubPollTarget;
|
|
222
225
|
private stopGitHubPollForTarget;
|
|
223
226
|
private addWorkingTreeWatcher;
|
|
224
227
|
private ensureLinuxRepoTreeWatchers;
|
|
@@ -71,6 +71,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
71
71
|
this.checkoutDiffCache = new LRUCache({ max: WORKSPACE_GIT_CHECKOUT_DIFF_CACHE_MAX });
|
|
72
72
|
this.logger = options.logger.child({ module: "workspace-git-service" });
|
|
73
73
|
this.paseoHome = options.paseoHome;
|
|
74
|
+
this.worktreesRoot = options.worktreesRoot;
|
|
74
75
|
this.deps = resolveWorkspaceGitServiceDeps(options.deps);
|
|
75
76
|
}
|
|
76
77
|
registerWorkspace(params, listener) {
|
|
@@ -112,6 +113,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
112
113
|
try {
|
|
113
114
|
const status = await this.deps.getCheckoutStatus(normalizedCwd, {
|
|
114
115
|
paseoHome: this.paseoHome,
|
|
116
|
+
worktreesRoot: this.worktreesRoot,
|
|
115
117
|
logger: this.logger,
|
|
116
118
|
});
|
|
117
119
|
if (!status.isGit) {
|
|
@@ -152,7 +154,10 @@ export class WorkspaceGitServiceImpl {
|
|
|
152
154
|
const normalizedCwd = normalizeWorkspaceId(cwd);
|
|
153
155
|
const normalizedOptions = this.normalizeCheckoutDiffOptions(options);
|
|
154
156
|
const key = this.buildCheckoutDiffCacheKey(normalizedCwd, normalizedOptions);
|
|
155
|
-
return this.readAuxiliaryCache(this.checkoutDiffCache, key, readOptions, () => this.deps.getCheckoutDiff(normalizedCwd, normalizedOptions, {
|
|
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
|
-
|
|
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
|
|
680
|
-
const
|
|
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 (!
|
|
689
|
+
if (!pollTarget || remoteUrl === null || !hasGitHubRemote) {
|
|
683
690
|
this.stopGitHubPollForTarget(target);
|
|
684
691
|
return;
|
|
685
692
|
}
|
|
686
|
-
|
|
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.
|
|
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({
|
|
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.
|
|
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 = {
|
|
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
|
-
|
|
1112
|
+
const pollTarget = this.resolveGitHubPollTarget(target);
|
|
1113
|
+
if (!pollTarget) {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
return buildWorkspaceGitHubPollKey(git.remoteUrl, pollTarget);
|
|
1079
1117
|
}
|
|
1080
1118
|
rememberGitHubSnapshot(target, github, options) {
|
|
1081
1119
|
if (target.closed || this.workspaceTargets.get(target.cwd) !== target) {
|
|
@@ -1317,6 +1355,9 @@ function buildGitHubSnapshotFromStatus(status) {
|
|
|
1317
1355
|
error: null,
|
|
1318
1356
|
};
|
|
1319
1357
|
}
|
|
1358
|
+
function buildWorkspaceGitHubPollKey(remoteUrl, target) {
|
|
1359
|
+
return JSON.stringify([remoteUrl, target.headRef, target.headRepositoryOwner ?? null]);
|
|
1360
|
+
}
|
|
1320
1361
|
async function runGitFetch(cwd) {
|
|
1321
1362
|
await runGitCommand(["fetch", "origin", "--prune"], {
|
|
1322
1363
|
cwd,
|
|
@@ -39,6 +39,7 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
|
|
|
39
39
|
updatedAt: z.ZodString;
|
|
40
40
|
archivedAt: z.ZodNullable<z.ZodString>;
|
|
41
41
|
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
workspaceId: string;
|
|
42
43
|
cwd: string;
|
|
43
44
|
createdAt: string;
|
|
44
45
|
updatedAt: string;
|
|
@@ -46,8 +47,8 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
|
|
|
46
47
|
kind: "worktree" | "local_checkout" | "directory";
|
|
47
48
|
projectId: string;
|
|
48
49
|
displayName: string;
|
|
49
|
-
workspaceId: string;
|
|
50
50
|
}, {
|
|
51
|
+
workspaceId: string;
|
|
51
52
|
cwd: string;
|
|
52
53
|
createdAt: string;
|
|
53
54
|
updatedAt: string;
|
|
@@ -55,7 +56,6 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
|
|
|
55
56
|
kind: "worktree" | "local_checkout" | "directory";
|
|
56
57
|
projectId: string;
|
|
57
58
|
displayName: string;
|
|
58
|
-
workspaceId: string;
|
|
59
59
|
}>;
|
|
60
60
|
export type PersistedProjectRecord = z.infer<typeof PersistedProjectRecordSchema>;
|
|
61
61
|
export type PersistedWorkspaceRecord = z.infer<typeof PersistedWorkspaceRecordSchema>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { promises as fs } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import { z } from "zod";
|
|
3
|
+
import { writeJsonFileAtomic } from "./atomic-file.js";
|
|
5
4
|
const PersistedProjectRecordSchema = z.object({
|
|
6
5
|
projectId: z.string(),
|
|
7
6
|
rootPath: z.string(),
|
|
@@ -110,10 +109,7 @@ class FileBackedRegistry {
|
|
|
110
109
|
}
|
|
111
110
|
async persist() {
|
|
112
111
|
const records = Array.from(this.cache.values());
|
|
113
|
-
await
|
|
114
|
-
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
115
|
-
await fs.writeFile(tempPath, JSON.stringify(records, null, 2), "utf8");
|
|
116
|
-
await fs.rename(tempPath, this.filePath);
|
|
112
|
+
await writeJsonFileAtomic(this.filePath, records);
|
|
117
113
|
}
|
|
118
114
|
async enqueuePersist() {
|
|
119
115
|
const nextPersist = this.persistQueue.then(() => this.persist());
|
|
@@ -8,6 +8,7 @@ export interface BuildWorkspaceServiceEnvOptions {
|
|
|
8
8
|
branchName: string | null;
|
|
9
9
|
daemonPort: number | null | undefined;
|
|
10
10
|
daemonListenHost: string | null | undefined;
|
|
11
|
+
serviceProxyPublicBaseUrl?: string | null;
|
|
11
12
|
peers: readonly WorkspaceServicePeer[];
|
|
12
13
|
}
|
|
13
14
|
export declare function normalizeServiceEnvName(scriptName: string): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
49
|
+
return projectServiceProxyUrls({
|
|
46
50
|
projectSlug: options.projectSlug,
|
|
47
51
|
branchName: options.branchName,
|
|
48
52
|
scriptName: options.scriptName,
|
|
49
|
-
|
|
50
|
-
|
|
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;
|