@getpaseo/server 0.1.4 → 0.1.6
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/client/daemon-client-terminal-stream-manager.d.ts.map +1 -1
- package/dist/server/client/daemon-client-terminal-stream-manager.js +4 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -1
- package/dist/server/client/daemon-client.d.ts +20 -15
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +35 -23
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-management-mcp.d.ts +2 -0
- package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
- package/dist/server/server/agent/agent-management-mcp.js +29 -4
- package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +3 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +27 -5
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +14 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/mcp-server.d.ts +2 -0
- package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
- package/dist/server/server/agent/mcp-server.js +30 -5
- package/dist/server/server/agent/mcp-server.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +42 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/agent/timeline-append.d.ts +10 -0
- package/dist/server/server/agent/timeline-append.d.ts.map +1 -0
- package/dist/server/server/agent/timeline-append.js +27 -0
- package/dist/server/server/agent/timeline-append.js.map +1 -0
- package/dist/server/server/bootstrap.d.ts.map +1 -1
- package/dist/server/server/bootstrap.js +3 -0
- package/dist/server/server/bootstrap.js.map +1 -1
- package/dist/server/server/persisted-config.d.ts +8 -8
- package/dist/server/server/session.d.ts +19 -6
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +373 -171
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/worktree-bootstrap.d.ts +29 -0
- package/dist/server/server/worktree-bootstrap.d.ts.map +1 -0
- package/dist/server/server/worktree-bootstrap.js +407 -0
- package/dist/server/server/worktree-bootstrap.js.map +1 -0
- package/dist/server/shared/binary-mux.d.ts.map +1 -1
- package/dist/server/shared/binary-mux.js +13 -0
- package/dist/server/shared/binary-mux.js.map +1 -1
- package/dist/server/shared/messages.d.ts +2051 -2493
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +55 -25
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/tool-call-display.d.ts.map +1 -1
- package/dist/server/shared/tool-call-display.js +4 -0
- package/dist/server/shared/tool-call-display.js.map +1 -1
- package/dist/server/terminal/terminal-manager.d.ts +11 -0
- package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
- package/dist/server/terminal/terminal-manager.js +75 -24
- package/dist/server/terminal/terminal-manager.js.map +1 -1
- package/dist/server/terminal/terminal.d.ts +1 -0
- package/dist/server/terminal/terminal.d.ts.map +1 -1
- package/dist/server/terminal/terminal.js +54 -6
- package/dist/server/terminal/terminal.js.map +1 -1
- package/dist/server/utils/worktree.d.ts +32 -0
- package/dist/server/utils/worktree.d.ts.map +1 -1
- package/dist/server/utils/worktree.js +127 -6
- package/dist/server/utils/worktree.js.map +1 -1
- package/package.json +2 -2
|
@@ -17,13 +17,15 @@ import { experimental_createMCPClient } from "ai";
|
|
|
17
17
|
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
|
18
18
|
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
|
19
19
|
import { toAgentPayload } from "./agent/agent-projections.js";
|
|
20
|
+
import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
|
|
20
21
|
import { projectTimelineRows } from "./agent/timeline-projection.js";
|
|
21
22
|
import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
|
|
22
23
|
import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
|
|
23
24
|
import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
|
|
24
25
|
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
|
|
25
26
|
import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
|
|
26
|
-
import {
|
|
27
|
+
import { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
|
|
28
|
+
import { createAgentWorktree, runAsyncWorktreeBootstrap, } from "./worktree-bootstrap.js";
|
|
27
29
|
import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
|
|
28
30
|
import { getProjectIcon } from "../utils/project-icon.js";
|
|
29
31
|
import { expandTilde } from "../utils/path.js";
|
|
@@ -37,8 +39,6 @@ const pendingAgentInitializations = new Map();
|
|
|
37
39
|
let restartRequested = false;
|
|
38
40
|
const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
|
|
39
41
|
const RESTART_EXIT_DELAY_MS = 250;
|
|
40
|
-
const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
|
|
41
|
-
const MAX_AGENTS_PER_PROJECT = 5;
|
|
42
42
|
const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
|
|
43
43
|
const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
|
|
44
44
|
const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
|
|
@@ -90,17 +90,17 @@ function deriveRemoteProjectKey(remoteUrl) {
|
|
|
90
90
|
}
|
|
91
91
|
return `remote:${cleanedHost}/${cleanedPath}`;
|
|
92
92
|
}
|
|
93
|
-
function deriveProjectGroupingKey(
|
|
94
|
-
const remoteKey = deriveRemoteProjectKey(remoteUrl);
|
|
93
|
+
function deriveProjectGroupingKey(options) {
|
|
94
|
+
const remoteKey = deriveRemoteProjectKey(options.remoteUrl);
|
|
95
95
|
if (remoteKey) {
|
|
96
96
|
return remoteKey;
|
|
97
97
|
}
|
|
98
98
|
const worktreeMarker = ".paseo/worktrees/";
|
|
99
|
-
const idx = cwd.indexOf(worktreeMarker);
|
|
99
|
+
const idx = options.cwd.indexOf(worktreeMarker);
|
|
100
100
|
if (idx !== -1) {
|
|
101
|
-
return cwd.slice(0, idx).replace(/\/$/, "");
|
|
101
|
+
return options.cwd.slice(0, idx).replace(/\/$/, "");
|
|
102
102
|
}
|
|
103
|
-
return cwd;
|
|
103
|
+
return options.cwd;
|
|
104
104
|
}
|
|
105
105
|
function deriveProjectGroupingName(projectKey) {
|
|
106
106
|
const githubRemotePrefix = "remote:github.com/";
|
|
@@ -110,6 +110,13 @@ function deriveProjectGroupingName(projectKey) {
|
|
|
110
110
|
const segments = projectKey.split(/[\\/]/).filter(Boolean);
|
|
111
111
|
return segments[segments.length - 1] || projectKey;
|
|
112
112
|
}
|
|
113
|
+
class SessionRequestError extends Error {
|
|
114
|
+
constructor(code, message) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.code = code;
|
|
117
|
+
this.name = "SessionRequestError";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
113
120
|
const PCM_SAMPLE_RATE = 16000;
|
|
114
121
|
const PCM_CHANNELS = 1;
|
|
115
122
|
const PCM_BITS_PER_SAMPLE = 16;
|
|
@@ -209,10 +216,12 @@ export class Session {
|
|
|
209
216
|
this.agentTools = null;
|
|
210
217
|
this.unsubscribeAgentEvents = null;
|
|
211
218
|
this.agentUpdatesSubscription = null;
|
|
212
|
-
this.projectPlacementCache = new Map();
|
|
213
219
|
this.clientActivity = null;
|
|
214
220
|
this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
|
|
221
|
+
this.subscribedTerminalDirectories = new Set();
|
|
222
|
+
this.unsubscribeTerminalsChanged = null;
|
|
215
223
|
this.terminalSubscriptions = new Map();
|
|
224
|
+
this.terminalExitSubscriptions = new Map();
|
|
216
225
|
this.terminalStreams = new Map();
|
|
217
226
|
this.terminalStreamByTerminalId = new Map();
|
|
218
227
|
this.nextTerminalStreamId = 1;
|
|
@@ -232,6 +241,9 @@ export class Session {
|
|
|
232
241
|
this.agentStorage = agentStorage;
|
|
233
242
|
this.createAgentMcpTransport = createAgentMcpTransport;
|
|
234
243
|
this.terminalManager = terminalManager;
|
|
244
|
+
if (this.terminalManager) {
|
|
245
|
+
this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => this.handleTerminalsChanged(event));
|
|
246
|
+
}
|
|
235
247
|
this.voiceAgentMcpStdio = voice?.voiceAgentMcpStdio ?? null;
|
|
236
248
|
const configuredModelsDir = dictation?.localModels?.modelsDir?.trim();
|
|
237
249
|
this.localSpeechModelsDir =
|
|
@@ -619,26 +631,16 @@ export class Session {
|
|
|
619
631
|
const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome })
|
|
620
632
|
.then((status) => this.toProjectCheckoutLite(cwd, status))
|
|
621
633
|
.catch(() => this.buildFallbackProjectCheckout(cwd));
|
|
622
|
-
const projectKey = deriveProjectGroupingKey(
|
|
634
|
+
const projectKey = deriveProjectGroupingKey({
|
|
635
|
+
cwd,
|
|
636
|
+
remoteUrl: checkout.remoteUrl,
|
|
637
|
+
});
|
|
623
638
|
return {
|
|
624
639
|
projectKey,
|
|
625
640
|
projectName: deriveProjectGroupingName(projectKey),
|
|
626
641
|
checkout,
|
|
627
642
|
};
|
|
628
643
|
}
|
|
629
|
-
getProjectPlacement(cwd) {
|
|
630
|
-
const now = Date.now();
|
|
631
|
-
const cached = this.projectPlacementCache.get(cwd);
|
|
632
|
-
if (cached && cached.expiresAt > now) {
|
|
633
|
-
return cached.promise;
|
|
634
|
-
}
|
|
635
|
-
const promise = this.buildProjectPlacement(cwd);
|
|
636
|
-
this.projectPlacementCache.set(cwd, {
|
|
637
|
-
expiresAt: now + PROJECT_PLACEMENT_CACHE_TTL_MS,
|
|
638
|
-
promise,
|
|
639
|
-
});
|
|
640
|
-
return promise;
|
|
641
|
-
}
|
|
642
644
|
async forwardAgentUpdate(agent) {
|
|
643
645
|
try {
|
|
644
646
|
const subscription = this.agentUpdatesSubscription;
|
|
@@ -648,7 +650,7 @@ export class Session {
|
|
|
648
650
|
const payload = await this.buildAgentPayload(agent);
|
|
649
651
|
const matches = this.matchesAgentFilter(payload, subscription.filter);
|
|
650
652
|
if (matches) {
|
|
651
|
-
const project = await this.
|
|
653
|
+
const project = await this.buildProjectPlacement(payload.cwd);
|
|
652
654
|
this.emit({
|
|
653
655
|
type: "agent_update",
|
|
654
656
|
payload: { kind: "upsert", agent: payload, project },
|
|
@@ -680,10 +682,7 @@ export class Session {
|
|
|
680
682
|
this.handleAudioPlayed(msg.id);
|
|
681
683
|
break;
|
|
682
684
|
case "fetch_agents_request":
|
|
683
|
-
await this.handleFetchAgents(msg
|
|
684
|
-
break;
|
|
685
|
-
case "fetch_agents_grouped_by_project_request":
|
|
686
|
-
await this.handleFetchAgentsGroupedByProject(msg.requestId, msg.filter);
|
|
685
|
+
await this.handleFetchAgents(msg);
|
|
687
686
|
break;
|
|
688
687
|
case "fetch_agent_request":
|
|
689
688
|
await this.handleFetchAgent(msg.agentId, msg.requestId);
|
|
@@ -868,6 +867,12 @@ export class Session {
|
|
|
868
867
|
case "register_push_token":
|
|
869
868
|
this.handleRegisterPushToken(msg.token);
|
|
870
869
|
break;
|
|
870
|
+
case "subscribe_terminals_request":
|
|
871
|
+
this.handleSubscribeTerminalsRequest(msg);
|
|
872
|
+
break;
|
|
873
|
+
case "unsubscribe_terminals_request":
|
|
874
|
+
this.handleUnsubscribeTerminalsRequest(msg);
|
|
875
|
+
break;
|
|
871
876
|
case "list_terminals_request":
|
|
872
877
|
await this.handleListTerminalsRequest(msg);
|
|
873
878
|
break;
|
|
@@ -1546,7 +1551,10 @@ export class Session {
|
|
|
1546
1551
|
}
|
|
1547
1552
|
const prompt = this.buildAgentPrompt(text, images);
|
|
1548
1553
|
try {
|
|
1549
|
-
this.agentManager.recordUserMessage(agentId, text, {
|
|
1554
|
+
this.agentManager.recordUserMessage(agentId, text, {
|
|
1555
|
+
messageId,
|
|
1556
|
+
emitState: false,
|
|
1557
|
+
});
|
|
1550
1558
|
}
|
|
1551
1559
|
catch (error) {
|
|
1552
1560
|
this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
|
|
@@ -1606,7 +1614,22 @@ export class Session {
|
|
|
1606
1614
|
});
|
|
1607
1615
|
}
|
|
1608
1616
|
if (worktreeConfig) {
|
|
1609
|
-
void
|
|
1617
|
+
void runAsyncWorktreeBootstrap({
|
|
1618
|
+
agentId: snapshot.id,
|
|
1619
|
+
worktree: worktreeConfig,
|
|
1620
|
+
terminalManager: this.terminalManager,
|
|
1621
|
+
appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
|
|
1622
|
+
agentManager: this.agentManager,
|
|
1623
|
+
agentId: snapshot.id,
|
|
1624
|
+
item,
|
|
1625
|
+
}),
|
|
1626
|
+
emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
|
|
1627
|
+
agentManager: this.agentManager,
|
|
1628
|
+
agentId: snapshot.id,
|
|
1629
|
+
item,
|
|
1630
|
+
}),
|
|
1631
|
+
logger: this.sessionLogger,
|
|
1632
|
+
});
|
|
1610
1633
|
}
|
|
1611
1634
|
this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
|
|
1612
1635
|
}
|
|
@@ -1776,12 +1799,11 @@ export class Session {
|
|
|
1776
1799
|
throw new Error("A branch name is required when creating a worktree.");
|
|
1777
1800
|
}
|
|
1778
1801
|
this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
|
|
1779
|
-
const createdWorktree = await
|
|
1802
|
+
const createdWorktree = await createAgentWorktree({
|
|
1780
1803
|
branchName: targetBranch,
|
|
1781
1804
|
cwd,
|
|
1782
1805
|
baseBranch: normalized.baseBranch,
|
|
1783
1806
|
worktreeSlug: normalized.worktreeSlug ?? targetBranch,
|
|
1784
|
-
runSetup: false,
|
|
1785
1807
|
paseoHome: this.paseoHome,
|
|
1786
1808
|
});
|
|
1787
1809
|
cwd = createdWorktree.worktreePath;
|
|
@@ -1805,100 +1827,6 @@ export class Session {
|
|
|
1805
1827
|
worktreeConfig,
|
|
1806
1828
|
};
|
|
1807
1829
|
}
|
|
1808
|
-
async runAsyncWorktreeSetup(agentId, worktree) {
|
|
1809
|
-
const callId = uuidv4();
|
|
1810
|
-
let results = [];
|
|
1811
|
-
try {
|
|
1812
|
-
const started = await this.safeAppendTimelineItem(agentId, {
|
|
1813
|
-
type: "tool_call",
|
|
1814
|
-
name: "paseo_worktree_setup",
|
|
1815
|
-
callId,
|
|
1816
|
-
status: "running",
|
|
1817
|
-
detail: {
|
|
1818
|
-
type: "unknown",
|
|
1819
|
-
input: {
|
|
1820
|
-
worktreePath: worktree.worktreePath,
|
|
1821
|
-
branchName: worktree.branchName,
|
|
1822
|
-
},
|
|
1823
|
-
output: null,
|
|
1824
|
-
},
|
|
1825
|
-
error: null,
|
|
1826
|
-
});
|
|
1827
|
-
if (!started) {
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
results = await runWorktreeSetupCommands({
|
|
1831
|
-
worktreePath: worktree.worktreePath,
|
|
1832
|
-
branchName: worktree.branchName,
|
|
1833
|
-
cleanupOnFailure: false,
|
|
1834
|
-
});
|
|
1835
|
-
await this.safeAppendTimelineItem(agentId, {
|
|
1836
|
-
type: "tool_call",
|
|
1837
|
-
name: "paseo_worktree_setup",
|
|
1838
|
-
callId,
|
|
1839
|
-
status: "completed",
|
|
1840
|
-
detail: {
|
|
1841
|
-
type: "unknown",
|
|
1842
|
-
input: {
|
|
1843
|
-
worktreePath: worktree.worktreePath,
|
|
1844
|
-
branchName: worktree.branchName,
|
|
1845
|
-
},
|
|
1846
|
-
output: {
|
|
1847
|
-
worktreePath: worktree.worktreePath,
|
|
1848
|
-
commands: results.map((result) => ({
|
|
1849
|
-
command: result.command,
|
|
1850
|
-
cwd: result.cwd,
|
|
1851
|
-
exitCode: result.exitCode,
|
|
1852
|
-
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
|
1853
|
-
})),
|
|
1854
|
-
},
|
|
1855
|
-
},
|
|
1856
|
-
error: null,
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
catch (error) {
|
|
1860
|
-
if (error instanceof WorktreeSetupError) {
|
|
1861
|
-
results = error.results;
|
|
1862
|
-
}
|
|
1863
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1864
|
-
await this.safeAppendTimelineItem(agentId, {
|
|
1865
|
-
type: "tool_call",
|
|
1866
|
-
name: "paseo_worktree_setup",
|
|
1867
|
-
callId,
|
|
1868
|
-
status: "failed",
|
|
1869
|
-
detail: {
|
|
1870
|
-
type: "unknown",
|
|
1871
|
-
input: {
|
|
1872
|
-
worktreePath: worktree.worktreePath,
|
|
1873
|
-
branchName: worktree.branchName,
|
|
1874
|
-
},
|
|
1875
|
-
output: {
|
|
1876
|
-
worktreePath: worktree.worktreePath,
|
|
1877
|
-
commands: results.map((result) => ({
|
|
1878
|
-
command: result.command,
|
|
1879
|
-
cwd: result.cwd,
|
|
1880
|
-
exitCode: result.exitCode,
|
|
1881
|
-
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
|
1882
|
-
})),
|
|
1883
|
-
},
|
|
1884
|
-
},
|
|
1885
|
-
error: { message },
|
|
1886
|
-
});
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
async safeAppendTimelineItem(agentId, item) {
|
|
1890
|
-
try {
|
|
1891
|
-
await this.agentManager.appendTimelineItem(agentId, item);
|
|
1892
|
-
return true;
|
|
1893
|
-
}
|
|
1894
|
-
catch (error) {
|
|
1895
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1896
|
-
if (message.includes("Unknown agent")) {
|
|
1897
|
-
return false;
|
|
1898
|
-
}
|
|
1899
|
-
throw error;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
1830
|
async handleListProviderModelsRequest(msg) {
|
|
1903
1831
|
const fetchedAt = new Date().toISOString();
|
|
1904
1832
|
try {
|
|
@@ -3672,64 +3600,230 @@ export class Session {
|
|
|
3672
3600
|
}
|
|
3673
3601
|
return this.buildStoredAgentPayload(record);
|
|
3674
3602
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3603
|
+
normalizeFetchAgentsSort(sort) {
|
|
3604
|
+
const fallback = [
|
|
3605
|
+
{ key: "updated_at", direction: "desc" },
|
|
3606
|
+
];
|
|
3607
|
+
if (!sort || sort.length === 0) {
|
|
3608
|
+
return fallback;
|
|
3609
|
+
}
|
|
3610
|
+
const deduped = [];
|
|
3611
|
+
const seen = new Set();
|
|
3612
|
+
for (const entry of sort) {
|
|
3613
|
+
if (seen.has(entry.key)) {
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
seen.add(entry.key);
|
|
3617
|
+
deduped.push(entry);
|
|
3682
3618
|
}
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3619
|
+
return deduped.length > 0 ? deduped : fallback;
|
|
3620
|
+
}
|
|
3621
|
+
getStatusPriority(agent) {
|
|
3622
|
+
const requiresAttention = agent.requiresAttention ?? false;
|
|
3623
|
+
const attentionReason = agent.attentionReason ?? null;
|
|
3624
|
+
if (requiresAttention && attentionReason === "permission") {
|
|
3625
|
+
return 0;
|
|
3626
|
+
}
|
|
3627
|
+
if (agent.status === "error" || attentionReason === "error") {
|
|
3628
|
+
return 1;
|
|
3629
|
+
}
|
|
3630
|
+
if (agent.status === "running") {
|
|
3631
|
+
return 2;
|
|
3689
3632
|
}
|
|
3633
|
+
if (agent.status === "initializing") {
|
|
3634
|
+
return 3;
|
|
3635
|
+
}
|
|
3636
|
+
return 4;
|
|
3690
3637
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3638
|
+
getFetchAgentsSortValue(entry, key) {
|
|
3639
|
+
switch (key) {
|
|
3640
|
+
case "status_priority":
|
|
3641
|
+
return this.getStatusPriority(entry.agent);
|
|
3642
|
+
case "created_at":
|
|
3643
|
+
return Date.parse(entry.agent.createdAt);
|
|
3644
|
+
case "updated_at":
|
|
3645
|
+
return Date.parse(entry.agent.updatedAt);
|
|
3646
|
+
case "title":
|
|
3647
|
+
return entry.agent.title?.toLocaleLowerCase() ?? "";
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
compareSortValues(left, right) {
|
|
3651
|
+
if (left === right) {
|
|
3652
|
+
return 0;
|
|
3653
|
+
}
|
|
3654
|
+
if (left === null) {
|
|
3655
|
+
return -1;
|
|
3656
|
+
}
|
|
3657
|
+
if (right === null) {
|
|
3658
|
+
return 1;
|
|
3659
|
+
}
|
|
3660
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
3661
|
+
return left < right ? -1 : 1;
|
|
3662
|
+
}
|
|
3663
|
+
return String(left).localeCompare(String(right));
|
|
3664
|
+
}
|
|
3665
|
+
compareFetchAgentsEntries(left, right, sort) {
|
|
3666
|
+
for (const spec of sort) {
|
|
3667
|
+
const leftValue = this.getFetchAgentsSortValue(left, spec.key);
|
|
3668
|
+
const rightValue = this.getFetchAgentsSortValue(right, spec.key);
|
|
3669
|
+
const base = this.compareSortValues(leftValue, rightValue);
|
|
3670
|
+
if (base === 0) {
|
|
3671
|
+
continue;
|
|
3712
3672
|
}
|
|
3713
|
-
|
|
3673
|
+
return spec.direction === "asc" ? base : -base;
|
|
3674
|
+
}
|
|
3675
|
+
return left.agent.id.localeCompare(right.agent.id);
|
|
3676
|
+
}
|
|
3677
|
+
encodeFetchAgentsCursor(entry, sort) {
|
|
3678
|
+
const values = {};
|
|
3679
|
+
for (const spec of sort) {
|
|
3680
|
+
values[spec.key] = this.getFetchAgentsSortValue(entry, spec.key);
|
|
3681
|
+
}
|
|
3682
|
+
return Buffer.from(JSON.stringify({
|
|
3683
|
+
sort,
|
|
3684
|
+
values,
|
|
3685
|
+
id: entry.agent.id,
|
|
3686
|
+
}), "utf8").toString("base64url");
|
|
3687
|
+
}
|
|
3688
|
+
decodeFetchAgentsCursor(cursor, sort) {
|
|
3689
|
+
let parsed;
|
|
3690
|
+
try {
|
|
3691
|
+
parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
3692
|
+
}
|
|
3693
|
+
catch {
|
|
3694
|
+
throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
|
|
3695
|
+
}
|
|
3696
|
+
if (!parsed || typeof parsed !== "object") {
|
|
3697
|
+
throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
|
|
3698
|
+
}
|
|
3699
|
+
const payload = parsed;
|
|
3700
|
+
if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
|
|
3701
|
+
throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
|
|
3702
|
+
}
|
|
3703
|
+
if (!payload.values || typeof payload.values !== "object") {
|
|
3704
|
+
throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
|
|
3705
|
+
}
|
|
3706
|
+
const cursorSort = [];
|
|
3707
|
+
for (const item of payload.sort) {
|
|
3708
|
+
if (!item ||
|
|
3709
|
+
typeof item !== "object" ||
|
|
3710
|
+
typeof item.key !== "string" ||
|
|
3711
|
+
typeof item.direction !== "string") {
|
|
3712
|
+
throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
|
|
3713
|
+
}
|
|
3714
|
+
const key = item.key;
|
|
3715
|
+
const direction = item.direction;
|
|
3716
|
+
if ((key !== "status_priority" &&
|
|
3717
|
+
key !== "created_at" &&
|
|
3718
|
+
key !== "updated_at" &&
|
|
3719
|
+
key !== "title") ||
|
|
3720
|
+
(direction !== "asc" && direction !== "desc")) {
|
|
3721
|
+
throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
|
|
3722
|
+
}
|
|
3723
|
+
cursorSort.push({ key, direction });
|
|
3724
|
+
}
|
|
3725
|
+
if (cursorSort.length !== sort.length ||
|
|
3726
|
+
cursorSort.some((entry, index) => entry.key !== sort[index]?.key ||
|
|
3727
|
+
entry.direction !== sort[index]?.direction)) {
|
|
3728
|
+
throw new SessionRequestError("invalid_cursor", "fetch_agents cursor does not match current sort");
|
|
3729
|
+
}
|
|
3730
|
+
return {
|
|
3731
|
+
sort: cursorSort,
|
|
3732
|
+
values: payload.values,
|
|
3733
|
+
id: payload.id,
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
compareEntryWithCursor(entry, cursor, sort) {
|
|
3737
|
+
for (const spec of sort) {
|
|
3738
|
+
const leftValue = this.getFetchAgentsSortValue(entry, spec.key);
|
|
3739
|
+
const rightValue = cursor.values[spec.key] !== undefined ? cursor.values[spec.key] ?? null : null;
|
|
3740
|
+
const base = this.compareSortValues(leftValue, rightValue);
|
|
3741
|
+
if (base === 0) {
|
|
3714
3742
|
continue;
|
|
3715
3743
|
}
|
|
3716
|
-
|
|
3744
|
+
return spec.direction === "asc" ? base : -base;
|
|
3745
|
+
}
|
|
3746
|
+
return entry.agent.id.localeCompare(cursor.id);
|
|
3747
|
+
}
|
|
3748
|
+
async listFetchAgentsEntries(request) {
|
|
3749
|
+
const filter = request.filter;
|
|
3750
|
+
const sort = this.normalizeFetchAgentsSort(request.sort);
|
|
3751
|
+
const includeArchived = filter?.includeArchived ?? false;
|
|
3752
|
+
let agents = await this.listAgentPayloads({
|
|
3753
|
+
labels: filter?.labels,
|
|
3754
|
+
});
|
|
3755
|
+
if (!includeArchived) {
|
|
3756
|
+
agents = agents.filter((agent) => !agent.archivedAt);
|
|
3717
3757
|
}
|
|
3718
|
-
|
|
3758
|
+
if (filter?.statuses && filter.statuses.length > 0) {
|
|
3759
|
+
const statuses = new Set(filter.statuses);
|
|
3760
|
+
agents = agents.filter((agent) => statuses.has(agent.status));
|
|
3761
|
+
}
|
|
3762
|
+
if (typeof filter?.requiresAttention === "boolean") {
|
|
3763
|
+
agents = agents.filter((agent) => (agent.requiresAttention ?? false) === filter.requiresAttention);
|
|
3764
|
+
}
|
|
3765
|
+
const placementByCwd = new Map();
|
|
3766
|
+
const getPlacement = (cwd) => {
|
|
3767
|
+
const existing = placementByCwd.get(cwd);
|
|
3768
|
+
if (existing) {
|
|
3769
|
+
return existing;
|
|
3770
|
+
}
|
|
3771
|
+
const placementPromise = this.buildProjectPlacement(cwd);
|
|
3772
|
+
placementByCwd.set(cwd, placementPromise);
|
|
3773
|
+
return placementPromise;
|
|
3774
|
+
};
|
|
3775
|
+
let entries = await Promise.all(agents.map(async (agent) => ({
|
|
3776
|
+
agent,
|
|
3777
|
+
project: await getPlacement(agent.cwd),
|
|
3778
|
+
})));
|
|
3779
|
+
if (filter?.projectKeys && filter.projectKeys.length > 0) {
|
|
3780
|
+
const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
|
|
3781
|
+
entries = entries.filter((entry) => projectKeys.has(entry.project.projectKey));
|
|
3782
|
+
}
|
|
3783
|
+
entries.sort((left, right) => this.compareFetchAgentsEntries(left, right, sort));
|
|
3784
|
+
const cursorToken = request.page?.cursor;
|
|
3785
|
+
if (cursorToken) {
|
|
3786
|
+
const cursor = this.decodeFetchAgentsCursor(cursorToken, sort);
|
|
3787
|
+
entries = entries.filter((entry) => this.compareEntryWithCursor(entry, cursor, sort) > 0);
|
|
3788
|
+
}
|
|
3789
|
+
const limit = request.page?.limit ?? entries.length;
|
|
3790
|
+
const pagedEntries = entries.slice(0, limit);
|
|
3791
|
+
const hasMore = entries.length > limit;
|
|
3792
|
+
const nextCursor = hasMore && pagedEntries.length > 0
|
|
3793
|
+
? this.encodeFetchAgentsCursor(pagedEntries[pagedEntries.length - 1], sort)
|
|
3794
|
+
: null;
|
|
3795
|
+
return {
|
|
3796
|
+
entries: pagedEntries,
|
|
3797
|
+
pageInfo: {
|
|
3798
|
+
nextCursor,
|
|
3799
|
+
prevCursor: request.page?.cursor ?? null,
|
|
3800
|
+
hasMore,
|
|
3801
|
+
},
|
|
3802
|
+
};
|
|
3719
3803
|
}
|
|
3720
|
-
async
|
|
3804
|
+
async handleFetchAgents(request) {
|
|
3721
3805
|
try {
|
|
3722
|
-
const
|
|
3806
|
+
const payload = await this.listFetchAgentsEntries(request);
|
|
3723
3807
|
this.emit({
|
|
3724
|
-
type: "
|
|
3725
|
-
payload: {
|
|
3808
|
+
type: "fetch_agents_response",
|
|
3809
|
+
payload: {
|
|
3810
|
+
requestId: request.requestId,
|
|
3811
|
+
...payload,
|
|
3812
|
+
},
|
|
3726
3813
|
});
|
|
3727
3814
|
}
|
|
3728
3815
|
catch (error) {
|
|
3729
|
-
|
|
3816
|
+
const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
|
|
3817
|
+
const message = error instanceof Error ? error.message : "Failed to fetch agents";
|
|
3818
|
+
this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
|
|
3730
3819
|
this.emit({
|
|
3731
|
-
type: "
|
|
3732
|
-
payload: {
|
|
3820
|
+
type: "rpc_error",
|
|
3821
|
+
payload: {
|
|
3822
|
+
requestId: request.requestId,
|
|
3823
|
+
requestType: request.type,
|
|
3824
|
+
error: message,
|
|
3825
|
+
code,
|
|
3826
|
+
},
|
|
3733
3827
|
});
|
|
3734
3828
|
}
|
|
3735
3829
|
}
|
|
@@ -3845,7 +3939,10 @@ export class Session {
|
|
|
3845
3939
|
await this.ensureAgentLoaded(agentId);
|
|
3846
3940
|
await this.interruptAgentIfRunning(agentId);
|
|
3847
3941
|
try {
|
|
3848
|
-
this.agentManager.recordUserMessage(agentId, msg.text, {
|
|
3942
|
+
this.agentManager.recordUserMessage(agentId, msg.text, {
|
|
3943
|
+
messageId: msg.messageId,
|
|
3944
|
+
emitState: false,
|
|
3945
|
+
});
|
|
3849
3946
|
}
|
|
3850
3947
|
catch (error) {
|
|
3851
3948
|
this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
|
|
@@ -4502,10 +4599,19 @@ export class Session {
|
|
|
4502
4599
|
await this.disableVoiceModeForActiveAgent(true);
|
|
4503
4600
|
this.isVoiceMode = false;
|
|
4504
4601
|
// Unsubscribe from all terminals
|
|
4602
|
+
if (this.unsubscribeTerminalsChanged) {
|
|
4603
|
+
this.unsubscribeTerminalsChanged();
|
|
4604
|
+
this.unsubscribeTerminalsChanged = null;
|
|
4605
|
+
}
|
|
4606
|
+
this.subscribedTerminalDirectories.clear();
|
|
4505
4607
|
for (const unsubscribe of this.terminalSubscriptions.values()) {
|
|
4506
4608
|
unsubscribe();
|
|
4507
4609
|
}
|
|
4508
4610
|
this.terminalSubscriptions.clear();
|
|
4611
|
+
for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
|
|
4612
|
+
unsubscribeExit();
|
|
4613
|
+
}
|
|
4614
|
+
this.terminalExitSubscriptions.clear();
|
|
4509
4615
|
this.detachAllTerminalStreams({ emitExit: false });
|
|
4510
4616
|
for (const target of this.checkoutDiffTargets.values()) {
|
|
4511
4617
|
this.closeCheckoutDiffWatchTarget(target);
|
|
@@ -4516,6 +4622,96 @@ export class Session {
|
|
|
4516
4622
|
// ============================================================================
|
|
4517
4623
|
// Terminal Handlers
|
|
4518
4624
|
// ============================================================================
|
|
4625
|
+
ensureTerminalExitSubscription(terminal) {
|
|
4626
|
+
if (this.terminalExitSubscriptions.has(terminal.id)) {
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
const unsubscribeExit = terminal.onExit(() => {
|
|
4630
|
+
this.handleTerminalExited(terminal.id);
|
|
4631
|
+
});
|
|
4632
|
+
this.terminalExitSubscriptions.set(terminal.id, unsubscribeExit);
|
|
4633
|
+
}
|
|
4634
|
+
handleTerminalExited(terminalId) {
|
|
4635
|
+
const unsubscribeExit = this.terminalExitSubscriptions.get(terminalId);
|
|
4636
|
+
if (unsubscribeExit) {
|
|
4637
|
+
unsubscribeExit();
|
|
4638
|
+
this.terminalExitSubscriptions.delete(terminalId);
|
|
4639
|
+
}
|
|
4640
|
+
const unsubscribe = this.terminalSubscriptions.get(terminalId);
|
|
4641
|
+
if (unsubscribe) {
|
|
4642
|
+
try {
|
|
4643
|
+
unsubscribe();
|
|
4644
|
+
}
|
|
4645
|
+
catch (error) {
|
|
4646
|
+
this.sessionLogger.warn({ err: error, terminalId }, "Failed to unsubscribe terminal after process exit");
|
|
4647
|
+
}
|
|
4648
|
+
this.terminalSubscriptions.delete(terminalId);
|
|
4649
|
+
}
|
|
4650
|
+
const streamId = this.terminalStreamByTerminalId.get(terminalId);
|
|
4651
|
+
if (typeof streamId === "number") {
|
|
4652
|
+
this.detachTerminalStream(streamId, { emitExit: true });
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
emitTerminalsChangedSnapshot(input) {
|
|
4656
|
+
this.emit({
|
|
4657
|
+
type: "terminals_changed",
|
|
4658
|
+
payload: {
|
|
4659
|
+
cwd: input.cwd,
|
|
4660
|
+
terminals: input.terminals,
|
|
4661
|
+
},
|
|
4662
|
+
});
|
|
4663
|
+
}
|
|
4664
|
+
handleTerminalsChanged(event) {
|
|
4665
|
+
if (!this.subscribedTerminalDirectories.has(event.cwd)) {
|
|
4666
|
+
return;
|
|
4667
|
+
}
|
|
4668
|
+
this.emitTerminalsChangedSnapshot({
|
|
4669
|
+
cwd: event.cwd,
|
|
4670
|
+
terminals: event.terminals.map((terminal) => ({
|
|
4671
|
+
id: terminal.id,
|
|
4672
|
+
name: terminal.name,
|
|
4673
|
+
})),
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4676
|
+
handleSubscribeTerminalsRequest(msg) {
|
|
4677
|
+
this.subscribedTerminalDirectories.add(msg.cwd);
|
|
4678
|
+
void this.emitInitialTerminalsChangedSnapshot(msg.cwd);
|
|
4679
|
+
}
|
|
4680
|
+
handleUnsubscribeTerminalsRequest(msg) {
|
|
4681
|
+
this.subscribedTerminalDirectories.delete(msg.cwd);
|
|
4682
|
+
}
|
|
4683
|
+
async emitInitialTerminalsChangedSnapshot(cwd) {
|
|
4684
|
+
if (!this.terminalManager || !this.subscribedTerminalDirectories.has(cwd)) {
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
const hadDirectoryBeforeSubscribe = this.terminalManager
|
|
4688
|
+
.listDirectories()
|
|
4689
|
+
.includes(cwd);
|
|
4690
|
+
try {
|
|
4691
|
+
const terminals = await this.terminalManager.getTerminals(cwd);
|
|
4692
|
+
for (const terminal of terminals) {
|
|
4693
|
+
this.ensureTerminalExitSubscription(terminal);
|
|
4694
|
+
}
|
|
4695
|
+
// New directories auto-create Terminal 1, which already emits through
|
|
4696
|
+
// terminal-manager change listeners.
|
|
4697
|
+
if (!hadDirectoryBeforeSubscribe) {
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
if (!this.subscribedTerminalDirectories.has(cwd)) {
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
this.emitTerminalsChangedSnapshot({
|
|
4704
|
+
cwd,
|
|
4705
|
+
terminals: terminals.map((terminal) => ({
|
|
4706
|
+
id: terminal.id,
|
|
4707
|
+
name: terminal.name,
|
|
4708
|
+
})),
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4711
|
+
catch (error) {
|
|
4712
|
+
this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4519
4715
|
async handleListTerminalsRequest(msg) {
|
|
4520
4716
|
if (!this.terminalManager) {
|
|
4521
4717
|
this.emit({
|
|
@@ -4530,6 +4726,9 @@ export class Session {
|
|
|
4530
4726
|
}
|
|
4531
4727
|
try {
|
|
4532
4728
|
const terminals = await this.terminalManager.getTerminals(msg.cwd);
|
|
4729
|
+
for (const terminal of terminals) {
|
|
4730
|
+
this.ensureTerminalExitSubscription(terminal);
|
|
4731
|
+
}
|
|
4533
4732
|
this.emit({
|
|
4534
4733
|
type: "list_terminals_response",
|
|
4535
4734
|
payload: {
|
|
@@ -4568,6 +4767,7 @@ export class Session {
|
|
|
4568
4767
|
cwd: msg.cwd,
|
|
4569
4768
|
name: msg.name,
|
|
4570
4769
|
});
|
|
4770
|
+
this.ensureTerminalExitSubscription(session);
|
|
4571
4771
|
this.emit({
|
|
4572
4772
|
type: "create_terminal_response",
|
|
4573
4773
|
payload: {
|
|
@@ -4615,6 +4815,7 @@ export class Session {
|
|
|
4615
4815
|
});
|
|
4616
4816
|
return;
|
|
4617
4817
|
}
|
|
4818
|
+
this.ensureTerminalExitSubscription(session);
|
|
4618
4819
|
// Unsubscribe from previous subscription if any
|
|
4619
4820
|
const existing = this.terminalSubscriptions.get(msg.terminalId);
|
|
4620
4821
|
if (existing) {
|
|
@@ -4660,6 +4861,7 @@ export class Session {
|
|
|
4660
4861
|
this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
|
|
4661
4862
|
return;
|
|
4662
4863
|
}
|
|
4864
|
+
this.ensureTerminalExitSubscription(session);
|
|
4663
4865
|
session.send(msg.message);
|
|
4664
4866
|
}
|
|
4665
4867
|
async handleKillTerminalRequest(msg) {
|