@getpaseo/server 0.1.97 → 0.1.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +11 -3
  2. package/dist/server/server/agent/agent-manager.js +94 -22
  3. package/dist/server/server/agent/agent-prompt.d.ts +1 -1
  4. package/dist/server/server/agent/agent-prompt.js +3 -10
  5. package/dist/server/server/agent/agent-sdk-types.d.ts +9 -3
  6. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  7. package/dist/server/server/agent/create-agent/create.js +8 -7
  8. package/dist/server/server/agent/lifecycle-command.d.ts +15 -1
  9. package/dist/server/server/agent/lifecycle-command.js +9 -2
  10. package/dist/server/server/agent/mcp-server.js +254 -115
  11. package/dist/server/server/agent/provider-notices.d.ts +3 -0
  12. package/dist/server/server/agent/provider-notices.js +5 -0
  13. package/dist/server/server/agent/provider-registry.d.ts +2 -0
  14. package/dist/server/server/agent/provider-registry.js +10 -3
  15. package/dist/server/server/agent/provider-snapshot-manager.d.ts +3 -0
  16. package/dist/server/server/agent/provider-snapshot-manager.js +11 -2
  17. package/dist/server/server/agent/providers/claude/agent.js +257 -143
  18. package/dist/server/server/agent/providers/claude/models.js +7 -3
  19. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +4 -3
  20. package/dist/server/server/agent/providers/codex-app-server-agent.js +43 -1
  21. package/dist/server/server/agent/providers/copilot-acp-agent.js +4 -1
  22. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +9 -0
  23. package/dist/server/server/agent/providers/diagnostic-utils.js +188 -0
  24. package/dist/server/server/agent/providers/mock-slow-provider.js +1 -1
  25. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +29 -2
  26. package/dist/server/server/agent/providers/opencode/server-manager.js +83 -17
  27. package/dist/server/server/agent/providers/opencode-agent.d.ts +2 -0
  28. package/dist/server/server/agent/providers/opencode-agent.js +14 -9
  29. package/dist/server/server/agent/providers/pi/agent.js +27 -14
  30. package/dist/server/server/bootstrap.d.ts +2 -0
  31. package/dist/server/server/bootstrap.js +32 -2
  32. package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
  33. package/dist/server/server/managed-processes/managed-processes.js +326 -0
  34. package/dist/server/server/resolve-worktree-creation-intent.d.ts +3 -0
  35. package/dist/server/server/resolve-worktree-creation-intent.js +3 -3
  36. package/dist/server/server/session.d.ts +12 -1
  37. package/dist/server/server/session.js +230 -40
  38. package/dist/server/server/speech/providers/openai/runtime.js +3 -4
  39. package/dist/server/server/websocket-server.d.ts +1 -0
  40. package/dist/server/server/websocket-server.js +11 -0
  41. package/dist/server/server/workspace-archive-service.js +2 -3
  42. package/dist/server/server/workspace-directory.js +5 -5
  43. package/dist/server/server/workspace-reconciliation-service.js +2 -2
  44. package/dist/server/server/worktree-core.d.ts +1 -0
  45. package/dist/server/server/worktree-core.js +5 -1
  46. package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
  47. package/dist/server/services/quota-fetcher/manifest.js +47 -0
  48. package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
  49. package/dist/server/services/quota-fetcher/provider.js +2 -0
  50. package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
  51. package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
  52. package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
  53. package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
  54. package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
  55. package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
  56. package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
  57. package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
  58. package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
  59. package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
  60. package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
  61. package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
  62. package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
  63. package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
  64. package/dist/server/services/quota-fetcher/service.d.ts +28 -0
  65. package/dist/server/services/quota-fetcher/service.js +58 -0
  66. package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
  67. package/dist/server/services/quota-fetcher/usage.js +49 -0
  68. package/dist/server/utils/directory-suggestions.js +98 -2
  69. package/package.json +5 -5
@@ -21,8 +21,9 @@ import { CodexAppServerClient, parseCodexThreadForkResponse, parseCodexThreadRol
21
21
  import { revertCodexConversation } from "./codex/rewind.js";
22
22
  import { renderProviderImageOutputAsAssistantMarkdown, } from "./provider-image-output.js";
23
23
  import { normalizeProviderReplayTimestamp } from "../provider-history-timestamps.js";
24
- import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, resolveBinaryVersion, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
24
+ import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, buildCommandResolutionDiagnosticRows, resolveBinaryVersion, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
25
25
  import { runProviderTurn } from "./provider-runner.js";
26
+ import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../provider-notices.js";
26
27
  function assertChildWithPipes(child) {
27
28
  if (!child.stdin || !child.stdout || !child.stderr) {
28
29
  throw new Error("Child process did not expose stdio pipes");
@@ -31,6 +32,10 @@ function assertChildWithPipes(child) {
31
32
  function isRecord(value) {
32
33
  return value != null && typeof value === "object" && !Array.isArray(value);
33
34
  }
35
+ function isCodexAlreadyUnarchivedError(error, threadId) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ return message.includes(`no archived rollout found for thread id ${threadId}`);
38
+ }
34
39
  const TURN_START_TIMEOUT_MS = 90 * 1000;
35
40
  const INTERRUPT_TIMEOUT_MS = 2000;
36
41
  const CODEX_PROVIDER = "codex";
@@ -2759,6 +2764,9 @@ export class CodexAppServerAgentSession {
2759
2764
  validateCodexMode(modeId);
2760
2765
  this.currentMode = modeId;
2761
2766
  this.cachedRuntimeInfo = null;
2767
+ if (this.activeForegroundTurnId) {
2768
+ return SETTING_APPLIES_NEXT_TURN_NOTICE;
2769
+ }
2762
2770
  }
2763
2771
  async setModel(modelId) {
2764
2772
  this.config.model = modelId ?? undefined;
@@ -2772,6 +2780,9 @@ export class CodexAppServerAgentSession {
2772
2780
  this.config.thinkingOptionId = normalizeCodexThinkingOptionId(thinkingOptionId);
2773
2781
  this.refreshResolvedCollaborationMode();
2774
2782
  this.cachedRuntimeInfo = null;
2783
+ if (this.activeForegroundTurnId) {
2784
+ return SETTING_APPLIES_NEXT_TURN_NOTICE;
2785
+ }
2775
2786
  }
2776
2787
  async setFeature(featureId, value) {
2777
2788
  if (featureId === "fast_mode") {
@@ -4406,6 +4417,34 @@ export class CodexAppServerAgentClient {
4406
4417
  await client.dispose();
4407
4418
  }
4408
4419
  }
4420
+ async unarchiveNativeSession(handle) {
4421
+ const threadId = handle.nativeHandle ?? handle.sessionId;
4422
+ if (!threadId)
4423
+ return;
4424
+ const child = await this.spawnAppServer();
4425
+ const client = new CodexAppServerClient(child, this.logger);
4426
+ try {
4427
+ await client.request("initialize", buildCodexAppServerInitializeParams());
4428
+ client.notify("initialized", {});
4429
+ try {
4430
+ await client.request("thread/unarchive", { threadId });
4431
+ }
4432
+ catch (error) {
4433
+ if (!isCodexAlreadyUnarchivedError(error, threadId)) {
4434
+ throw error;
4435
+ }
4436
+ try {
4437
+ await client.request("thread/read", { threadId });
4438
+ }
4439
+ catch {
4440
+ throw error;
4441
+ }
4442
+ }
4443
+ }
4444
+ finally {
4445
+ await client.dispose();
4446
+ }
4447
+ }
4409
4448
  async isAvailable() {
4410
4449
  const launch = await resolveCodexLaunch(this.runtimeSettings);
4411
4450
  const availability = await checkCodexLaunchAvailable(launch);
@@ -4417,6 +4456,9 @@ export class CodexAppServerAgentClient {
4417
4456
  const availability = await checkCodexLaunchAvailable(launch);
4418
4457
  const available = availability.available;
4419
4458
  const entries = [
4459
+ ...(await buildCommandResolutionDiagnosticRows(launch, {
4460
+ knownBinaryNames: ["codex"],
4461
+ })),
4420
4462
  ...(await buildBinaryDiagnosticRows(launch, availability)),
4421
4463
  ];
4422
4464
  let status = formatDiagnosticStatus(available);
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { checkProviderLaunchAvailable, resolveProviderLaunch, } from "../provider-launch-config.js";
3
3
  import { ACPAgentClient, } from "./acp-agent.js";
4
- import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
4
+ import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, buildCommandResolutionDiagnosticRows, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
5
5
  const COPILOT_CAPABILITIES = {
6
6
  supportsStreaming: true,
7
7
  supportsSessionPersistence: true,
@@ -93,6 +93,9 @@ export class CopilotACPAgentClient extends ACPAgentClient {
93
93
  }
94
94
  return {
95
95
  diagnostic: formatProviderDiagnostic("Copilot", [
96
+ ...(await buildCommandResolutionDiagnosticRows(launch, {
97
+ knownBinaryNames: ["copilot"],
98
+ })),
96
99
  ...(await buildBinaryDiagnosticRows(launch, availability)),
97
100
  { label: "Models", value: modelsValue },
98
101
  { label: "Status", value: status },
@@ -21,6 +21,15 @@ export interface BinaryDiagnosticRowsOptions {
21
21
  binaryLabel?: string;
22
22
  versionCommand?: BinaryDiagnosticVersionCommand;
23
23
  }
24
+ export interface CommandResolutionDiagnosticRowsOptions {
25
+ knownBinaryNames: readonly string[];
26
+ includeCommandProbes?: boolean;
27
+ pathValue?: string;
28
+ pathext?: string;
29
+ platform?: NodeJS.Platform;
30
+ shell?: string;
31
+ }
32
+ export declare function buildCommandResolutionDiagnosticRows(launch: ResolvedProviderLaunch, options: CommandResolutionDiagnosticRowsOptions): Promise<DiagnosticEntry[]>;
24
33
  export declare function buildBinaryDiagnosticRows(launch: ResolvedProviderLaunch, availability: ProviderLaunchAvailability, options?: BinaryDiagnosticRowsOptions): Promise<DiagnosticEntry[]>;
25
34
  export declare function formatConfiguredCommand(defaultArgv: readonly string[], runtimeSettings?: ProviderRuntimeSettings): string;
26
35
  //# sourceMappingURL=diagnostic-utils.d.ts.map
@@ -1,3 +1,6 @@
1
+ import { constants } from "node:fs";
2
+ import { access, stat } from "node:fs/promises";
3
+ import path from "node:path";
1
4
  import { createProviderEnvSpec, } from "../provider-launch-config.js";
2
5
  import { execCommand } from "../../../utils/spawn.js";
3
6
  export function formatProviderDiagnostic(providerName, entries) {
@@ -114,6 +117,191 @@ export async function resolveBinaryVersion(binaryPath) {
114
117
  return `error: ${toDiagnosticErrorMessage(error)}`;
115
118
  }
116
119
  }
120
+ const COMMAND_PROBE_TIMEOUT_MS = 3000;
121
+ const COMMAND_PROBE_MAX_BUFFER = 32 * 1024;
122
+ function resolvePlatform(options) {
123
+ return options?.platform ?? process.platform;
124
+ }
125
+ function resolvePathValue(options) {
126
+ return options?.pathValue ?? process.env["PATH"] ?? process.env["Path"] ?? "";
127
+ }
128
+ function resolveShellValue(options) {
129
+ if (options?.shell) {
130
+ return options.shell;
131
+ }
132
+ if (resolvePlatform(options) === "win32") {
133
+ return process.env["ComSpec"] ?? "cmd.exe";
134
+ }
135
+ return process.env["SHELL"] ?? "/bin/sh";
136
+ }
137
+ async function isExecutableFile(filePath, platform) {
138
+ try {
139
+ const candidate = await stat(filePath);
140
+ if (!candidate.isFile()) {
141
+ return false;
142
+ }
143
+ if (platform === "win32") {
144
+ return true;
145
+ }
146
+ await access(filePath, constants.X_OK);
147
+ return true;
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ function resolveSearchableNames(binaryNames) {
154
+ return binaryNames.filter((binaryName) => binaryName.trim().length > 0 && !binaryName.includes("/") && !binaryName.includes("\\"));
155
+ }
156
+ function resolveWindowsPathExt(options) {
157
+ const value = options.pathext ?? process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD";
158
+ return value
159
+ .split(";")
160
+ .map((extension) => extension.trim())
161
+ .filter(Boolean)
162
+ .map((extension) => (extension.startsWith(".") ? extension : `.${extension}`));
163
+ }
164
+ function resolveBinaryCandidateNames(binaryName, options) {
165
+ if (resolvePlatform(options) !== "win32" || path.win32.extname(binaryName)) {
166
+ return [binaryName];
167
+ }
168
+ return [binaryName, ...resolveWindowsPathExt(options).map((extension) => binaryName + extension)];
169
+ }
170
+ async function formatPathMatches(options) {
171
+ const binaryNames = options.knownBinaryNames;
172
+ const searchableNames = resolveSearchableNames(binaryNames);
173
+ if (searchableNames.length === 0) {
174
+ return "not checked";
175
+ }
176
+ const pathDelimiter = resolvePlatform(options) === "win32" ? ";" : path.delimiter;
177
+ const pathEntries = resolvePathValue(options).split(pathDelimiter).filter(Boolean);
178
+ const matches = [];
179
+ const seen = new Set();
180
+ const platform = resolvePlatform(options);
181
+ for (const directory of pathEntries) {
182
+ for (const binaryName of searchableNames) {
183
+ for (const candidateName of resolveBinaryCandidateNames(binaryName, options)) {
184
+ const candidate = path.join(directory, candidateName);
185
+ if (seen.has(candidate)) {
186
+ continue;
187
+ }
188
+ seen.add(candidate);
189
+ if (await isExecutableFile(candidate, platform)) {
190
+ matches.push(candidate);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ return matches.length > 0 ? matches.join("\n ") : "none";
196
+ }
197
+ function shellToken(value) {
198
+ return `'${value.replace(/'/g, "'\\''")}'`;
199
+ }
200
+ function formatCommandProbeOutput(stdout, stderr) {
201
+ const sections = [];
202
+ const trimmedStdout = truncateForDiagnostic(stdout);
203
+ const trimmedStderr = truncateForDiagnostic(stderr);
204
+ if (trimmedStdout.length > 0) {
205
+ sections.push(trimmedStdout);
206
+ }
207
+ if (trimmedStderr.length > 0) {
208
+ sections.push(`stderr: ${trimmedStderr}`);
209
+ }
210
+ return sections.length > 0 ? sections.join("\n") : "(no output)";
211
+ }
212
+ function formatCommandProbeError(error) {
213
+ return toDiagnosticErrorMessage(error);
214
+ }
215
+ async function runCommandProbe(command, args) {
216
+ try {
217
+ const { stdout, stderr } = await execCommand(command, args, {
218
+ timeout: COMMAND_PROBE_TIMEOUT_MS,
219
+ killSignal: "SIGKILL",
220
+ maxBuffer: COMMAND_PROBE_MAX_BUFFER,
221
+ });
222
+ return formatCommandProbeOutput(stdout, stderr);
223
+ }
224
+ catch (error) {
225
+ return formatCommandProbeError(error);
226
+ }
227
+ }
228
+ async function buildPosixCommandProbeRows(binaryName) {
229
+ const shell = resolveShellValue();
230
+ const typeCommand = `type -a ${shellToken(binaryName)}`;
231
+ return [
232
+ {
233
+ label: `which -a ${binaryName}`,
234
+ value: await runCommandProbe("/usr/bin/which", ["-a", binaryName]),
235
+ },
236
+ {
237
+ label: `${path.basename(shell)} -lc type -a ${binaryName}`,
238
+ value: await runCommandProbe(shell, ["-lc", typeCommand]),
239
+ },
240
+ ];
241
+ }
242
+ async function buildWindowsCommandProbeRows(binaryName) {
243
+ const powershellCommand = [
244
+ "$ErrorActionPreference = 'Continue';",
245
+ `Get-Command -All ${JSON.stringify(binaryName)} |`,
246
+ "Select-Object CommandType,Source,Name,Definition |",
247
+ "Format-List",
248
+ ].join(" ");
249
+ return [
250
+ {
251
+ label: `where.exe ${binaryName}`,
252
+ value: await runCommandProbe("where.exe", [binaryName]),
253
+ },
254
+ {
255
+ label: `powershell Get-Command -All ${binaryName}`,
256
+ value: await runCommandProbe("powershell.exe", [
257
+ "-NoProfile",
258
+ "-ExecutionPolicy",
259
+ "Bypass",
260
+ "-Command",
261
+ powershellCommand,
262
+ ]),
263
+ },
264
+ ];
265
+ }
266
+ async function buildCommandProbeRows(binaryNames) {
267
+ const searchableNames = resolveSearchableNames(binaryNames);
268
+ if (searchableNames.length === 0) {
269
+ return [];
270
+ }
271
+ const rows = [];
272
+ for (const binaryName of searchableNames) {
273
+ rows.push(...(process.platform === "win32"
274
+ ? await buildWindowsCommandProbeRows(binaryName)
275
+ : await buildPosixCommandProbeRows(binaryName)));
276
+ }
277
+ return rows;
278
+ }
279
+ export async function buildCommandResolutionDiagnosticRows(launch, options) {
280
+ const includeCommandProbes = options.includeCommandProbes ?? true;
281
+ return [
282
+ {
283
+ label: "Command source",
284
+ value: launch.source,
285
+ },
286
+ {
287
+ label: "Configured command",
288
+ value: [launch.command, ...launch.args].join(" "),
289
+ },
290
+ {
291
+ label: "Daemon PATH",
292
+ value: truncateForDiagnostic(resolvePathValue(options)) || "(empty)",
293
+ },
294
+ {
295
+ label: "Daemon shell",
296
+ value: resolveShellValue(options),
297
+ },
298
+ {
299
+ label: "PATH matches",
300
+ value: await formatPathMatches(options),
301
+ },
302
+ ...(includeCommandProbes ? await buildCommandProbeRows(options.knownBinaryNames) : []),
303
+ ];
304
+ }
117
305
  async function resolveCommandVersion(invocation) {
118
306
  try {
119
307
  const { stdout, stderr } = await execCommand(invocation.command, invocation.args, {
@@ -19,7 +19,7 @@ export class MockSlowProviderClient {
19
19
  this.capabilities = CAPABILITIES;
20
20
  }
21
21
  async isAvailable() {
22
- return true;
22
+ return process.env.PASEO_ENABLE_MOCK_SLOW === "true";
23
23
  }
24
24
  listModels(_options) {
25
25
  return neverResolves();
@@ -1,5 +1,8 @@
1
1
  import type { ChildProcess } from "node:child_process";
2
2
  import type { Logger } from "pino";
3
+ import { type SpawnProcessOptions } from "../../../../utils/spawn.js";
4
+ import { type ProcessTerminator } from "../../../../utils/tree-kill.js";
5
+ import type { ManagedProcessRegistry } from "../../../managed-processes/managed-processes.js";
3
6
  import { type ProviderRuntimeSettings } from "../../provider-launch-config.js";
4
7
  export interface OpenCodeServerAcquisition {
5
8
  server: {
@@ -24,6 +27,22 @@ export interface OpenCodeServerGeneration {
24
27
  url: string;
25
28
  refCount: number;
26
29
  retired: boolean;
30
+ managedProcessId?: string;
31
+ }
32
+ export type OpenCodePortAllocator = () => Promise<number>;
33
+ export type OpenCodeCommandPrefixResolver = () => Promise<{
34
+ command: string;
35
+ args: string[];
36
+ }>;
37
+ export type OpenCodeServerProcessSpawner = (command: string, args: string[], options: SpawnProcessOptions) => ChildProcess;
38
+ export interface OpenCodeServerManagerOptions {
39
+ logger: Logger;
40
+ runtimeSettings?: ProviderRuntimeSettings;
41
+ managedProcesses?: ManagedProcessRegistry;
42
+ terminateProcess?: ProcessTerminator;
43
+ portAllocator?: OpenCodePortAllocator;
44
+ resolveCommandPrefix?: OpenCodeCommandPrefixResolver;
45
+ spawnServerProcess?: OpenCodeServerProcessSpawner;
27
46
  }
28
47
  export declare class OpenCodeServerManager implements OpenCodeServerManagerLike {
29
48
  private static instance;
@@ -35,8 +54,13 @@ export declare class OpenCodeServerManager implements OpenCodeServerManagerLike
35
54
  private readonly logger;
36
55
  private readonly runtimeSettings?;
37
56
  private readonly runtimeSettingsKey;
38
- private constructor();
39
- static getInstance(logger: Logger, runtimeSettings?: ProviderRuntimeSettings): OpenCodeServerManager;
57
+ private readonly managedProcesses?;
58
+ private readonly terminateProcess;
59
+ private readonly portAllocator;
60
+ private readonly resolveCommandPrefix;
61
+ private readonly spawnServerProcess;
62
+ constructor(options: OpenCodeServerManagerOptions);
63
+ static getInstance(logger: Logger, runtimeSettings?: ProviderRuntimeSettings, options?: Omit<OpenCodeServerManagerOptions, "logger" | "runtimeSettings">): OpenCodeServerManager;
40
64
  private static registerExitHandler;
41
65
  ensureRunning(): Promise<{
42
66
  port: number;
@@ -55,5 +79,8 @@ export declare class OpenCodeServerManager implements OpenCodeServerManagerLike
55
79
  shutdown(): Promise<void>;
56
80
  private cleanupRetiredServers;
57
81
  private killServer;
82
+ private recordManagedServerProcess;
83
+ private removeManagedProcessRecordWhenResolved;
84
+ private removeManagedProcessId;
58
85
  }
59
86
  //# sourceMappingURL=server-manager.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import net from "node:net";
2
+ import os from "node:os";
2
3
  import { findExecutable } from "../../../../executable-resolution/executable-resolution.js";
3
4
  import { spawnProcess } from "../../../../utils/spawn.js";
4
5
  import { terminateWithTreeKill } from "../../../../utils/tree-kill.js";
@@ -6,19 +7,30 @@ import { createProviderEnvSpec, resolveProviderCommandPrefix, } from "../../prov
6
7
  const OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5000;
7
8
  const OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1000;
8
9
  export class OpenCodeServerManager {
9
- constructor(logger, runtimeSettings) {
10
+ constructor(options) {
10
11
  this.currentServer = null;
11
12
  this.retiredServers = new Set();
12
13
  this.startPromise = null;
13
14
  this.forcedRefreshPromise = null;
14
- this.logger = logger;
15
- this.runtimeSettings = runtimeSettings;
16
- this.runtimeSettingsKey = JSON.stringify(runtimeSettings ?? {});
15
+ this.logger = options.logger;
16
+ this.runtimeSettings = options.runtimeSettings;
17
+ this.runtimeSettingsKey = JSON.stringify(this.runtimeSettings ?? {});
18
+ this.managedProcesses = options.managedProcesses;
19
+ this.terminateProcess = options.terminateProcess ?? terminateWithTreeKill;
20
+ this.portAllocator = options.portAllocator ?? findAvailablePort;
21
+ this.resolveCommandPrefix =
22
+ options.resolveCommandPrefix ??
23
+ (() => resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveOpenCodeBinary));
24
+ this.spawnServerProcess = options.spawnServerProcess ?? spawnProcess;
17
25
  }
18
- static getInstance(logger, runtimeSettings) {
26
+ static getInstance(logger, runtimeSettings, options = {}) {
19
27
  const nextSettingsKey = JSON.stringify(runtimeSettings ?? {});
20
28
  if (!OpenCodeServerManager.instance) {
21
- OpenCodeServerManager.instance = new OpenCodeServerManager(logger, runtimeSettings);
29
+ OpenCodeServerManager.instance = new OpenCodeServerManager({
30
+ logger,
31
+ runtimeSettings,
32
+ ...options,
33
+ });
22
34
  OpenCodeServerManager.registerExitHandler();
23
35
  }
24
36
  else if (OpenCodeServerManager.instance.runtimeSettingsKey !== nextSettingsKey) {
@@ -128,11 +140,14 @@ export class OpenCodeServerManager {
128
140
  return server;
129
141
  }
130
142
  async startServer(launchEnv) {
131
- const port = await findAvailablePort();
143
+ const port = await this.portAllocator();
132
144
  const url = `http://127.0.0.1:${port}`;
133
- const launchPrefix = await resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveOpenCodeBinary);
145
+ const launchPrefix = await this.resolveCommandPrefix();
146
+ const serverArgs = [...launchPrefix.args, "serve", "--port", String(port)];
147
+ const serverCwd = os.homedir();
134
148
  return new Promise((resolve, reject) => {
135
- const serverProcess = spawnProcess(launchPrefix.command, [...launchPrefix.args, "serve", "--port", String(port)], {
149
+ const serverProcess = this.spawnServerProcess(launchPrefix.command, serverArgs, {
150
+ cwd: serverCwd,
136
151
  detached: process.platform !== "win32",
137
152
  stdio: ["ignore", "pipe", "pipe"],
138
153
  ...createProviderEnvSpec({
@@ -140,6 +155,12 @@ export class OpenCodeServerManager {
140
155
  overlays: [launchEnv],
141
156
  }),
142
157
  });
158
+ const managedProcessRecord = this.recordManagedServerProcess({
159
+ process: serverProcess,
160
+ command: launchPrefix.command,
161
+ args: serverArgs,
162
+ port,
163
+ });
143
164
  let started = false;
144
165
  let stderrBuffer = "";
145
166
  let stdoutBuffer = "";
@@ -174,13 +195,17 @@ export class OpenCodeServerManager {
174
195
  if (output.includes("listening on") && !started) {
175
196
  started = true;
176
197
  clearTimeout(timeout);
177
- resolve({
178
- process: serverProcess,
179
- port,
180
- url,
181
- refCount: 0,
182
- retired: false,
183
- });
198
+ void (async () => {
199
+ const record = await managedProcessRecord;
200
+ resolve({
201
+ process: serverProcess,
202
+ port,
203
+ url,
204
+ refCount: 0,
205
+ retired: false,
206
+ ...(record ? { managedProcessId: record.id } : {}),
207
+ });
208
+ })();
184
209
  }
185
210
  });
186
211
  serverProcess.stderr?.on("data", (data) => {
@@ -190,10 +215,12 @@ export class OpenCodeServerManager {
190
215
  });
191
216
  serverProcess.on("error", (error) => {
192
217
  clearTimeout(timeout);
218
+ this.removeManagedProcessRecordWhenResolved(managedProcessRecord);
193
219
  const headline = error instanceof Error ? error.message : String(error);
194
220
  reject(new Error(buildStartupErrorMessage(headline)));
195
221
  });
196
222
  serverProcess.on("exit", (code) => {
223
+ this.removeManagedProcessRecordWhenResolved(managedProcessRecord);
197
224
  if (!started) {
198
225
  clearTimeout(timeout);
199
226
  reject(new Error(buildStartupErrorMessage(`OpenCode server exited with code ${code}`)));
@@ -231,7 +258,7 @@ export class OpenCodeServerManager {
231
258
  (server.process.signalCode !== null && server.process.signalCode !== undefined)) {
232
259
  return;
233
260
  }
234
- const result = await terminateWithTreeKill(server.process, {
261
+ const result = await this.terminateProcess(server.process, {
235
262
  gracefulTimeoutMs: OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS,
236
263
  forceTimeoutMs: OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS,
237
264
  onForceSignal: () => {
@@ -241,6 +268,45 @@ export class OpenCodeServerManager {
241
268
  if (result === "kill-timeout") {
242
269
  this.logger.warn({ timeoutMs: OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS }, "OpenCode server did not report exit after SIGKILL");
243
270
  }
271
+ if (server.managedProcessId) {
272
+ await this.removeManagedProcessId(server.managedProcessId);
273
+ server.managedProcessId = undefined;
274
+ }
275
+ }
276
+ async recordManagedServerProcess(options) {
277
+ const pid = options.process.pid;
278
+ if (!this.managedProcesses || typeof pid !== "number" || pid <= 0) {
279
+ return null;
280
+ }
281
+ try {
282
+ return await this.managedProcesses.record({
283
+ owner: { provider: "opencode", kind: "helper-server" },
284
+ pid,
285
+ command: options.command,
286
+ args: options.args,
287
+ metadata: { port: options.port },
288
+ });
289
+ }
290
+ catch (error) {
291
+ this.logger.warn({ err: error, pid, port: options.port }, "Failed to record OpenCode helper process");
292
+ return null;
293
+ }
294
+ }
295
+ removeManagedProcessRecordWhenResolved(record) {
296
+ void record.then((resolved) => {
297
+ if (resolved) {
298
+ return this.removeManagedProcessId(resolved.id);
299
+ }
300
+ return undefined;
301
+ });
302
+ }
303
+ async removeManagedProcessId(id) {
304
+ try {
305
+ await this.managedProcesses?.remove(id);
306
+ }
307
+ catch (error) {
308
+ this.logger.warn({ err: error, id }, "Failed to remove OpenCode helper process record");
309
+ }
244
310
  }
245
311
  }
246
312
  OpenCodeServerManager.instance = null;
@@ -4,6 +4,7 @@ import { type AgentCapabilityFlags, type AgentClient, type AgentCreateSessionOpt
4
4
  import { isDefaultAgentCreateConfigUnattended } from "../create-agent-mode.js";
5
5
  import { type ProviderRuntimeSettings } from "../provider-launch-config.js";
6
6
  import { type OpenCodeRuntime } from "./opencode/runtime.js";
7
+ import type { ManagedProcessRegistry } from "../../managed-processes/managed-processes.js";
7
8
  declare function resolveOpenCodeCreateConfig(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult;
8
9
  declare function isOpenCodeCreateConfigUnattended(input: Parameters<typeof isDefaultAgentCreateConfigUnattended>[0]): boolean;
9
10
  type OpenCodeAgentConfig = AgentSessionConfig & {
@@ -103,6 +104,7 @@ export declare const __openCodeInternals: {
103
104
  };
104
105
  interface OpenCodeAgentClientDeps {
105
106
  runtime?: OpenCodeRuntime;
107
+ managedProcesses?: ManagedProcessRegistry;
106
108
  }
107
109
  export declare class OpenCodeAgentClient implements AgentClient {
108
110
  readonly provider: "opencode";
@@ -11,7 +11,7 @@ import { execCommand } from "../../../utils/spawn.js";
11
11
  import { buildToolCallDisplayModel } from "@getpaseo/protocol/tool-call-display";
12
12
  import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
13
13
  import { OpenCodeServerManager } from "./opencode/server-manager.js";
14
- import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
14
+ import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, buildCommandResolutionDiagnosticRows, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
15
15
  import { runProviderTurn } from "./provider-runner.js";
16
16
  import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
17
17
  import { composeSystemPromptParts } from "../system-prompt.js";
@@ -398,14 +398,14 @@ function mapOpenCodeAgentToMode(agent) {
398
398
  };
399
399
  }
400
400
  function mergeOpenCodeModes(discoveredModes) {
401
- const modesById = new Map(DEFAULT_MODES.map((mode) => [mode.id, mode]));
402
- for (const mode of discoveredModes) {
403
- if (mode.id === OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) {
404
- continue;
405
- }
406
- modesById.set(mode.id, mode);
401
+ const filtered = discoveredModes.filter((mode) => mode.id !== OPENCODE_LEGACY_FULL_ACCESS_MODE_ID);
402
+ // When discovery returns results, trust them exactly — don't inject hardcoded
403
+ // defaults that the user may have intentionally disabled in their OpenCode config.
404
+ // Fall back to DEFAULT_MODES only when discovery produced nothing.
405
+ if (filtered.length > 0) {
406
+ return sortOpenCodeModes(filtered);
407
407
  }
408
- return sortOpenCodeModes(Array.from(modesById.values()));
408
+ return sortOpenCodeModes([...DEFAULT_MODES]);
409
409
  }
410
410
  function sortOpenCodeModes(modes) {
411
411
  const order = new Map(DEFAULT_MODES.map((mode, index) => [mode.id, index]));
@@ -880,7 +880,9 @@ export class OpenCodeAgentClient {
880
880
  this.runtimeSettings = runtimeSettings;
881
881
  this.runtime =
882
882
  deps.runtime ??
883
- new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings));
883
+ new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings, {
884
+ managedProcesses: deps.managedProcesses,
885
+ }));
884
886
  }
885
887
  async createSession(config, launchContext, options) {
886
888
  const openCodeConfig = this.assertConfig(config);
@@ -1146,6 +1148,9 @@ export class OpenCodeAgentClient {
1146
1148
  }
1147
1149
  return {
1148
1150
  diagnostic: formatProviderDiagnostic("OpenCode", [
1151
+ ...(await buildCommandResolutionDiagnosticRows(launch, {
1152
+ knownBinaryNames: ["opencode"],
1153
+ })),
1149
1154
  ...(await buildBinaryDiagnosticRows(launch, availability)),
1150
1155
  { label: "Server", value: serverStatus },
1151
1156
  { label: "Auth", value: authValue },