@getpaseo/server 0.1.97 → 0.1.99

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 (101) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +11 -3
  2. package/dist/server/server/agent/agent-manager.js +96 -24
  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 +20 -9
  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 +8 -3
  14. package/dist/server/server/agent/provider-registry.js +58 -25
  15. package/dist/server/server/agent/provider-snapshot-manager.d.ts +3 -0
  16. package/dist/server/server/agent/provider-snapshot-manager.js +37 -16
  17. package/dist/server/server/agent/providers/acp-agent.d.ts +5 -3
  18. package/dist/server/server/agent/providers/acp-agent.js +32 -19
  19. package/dist/server/server/agent/providers/claude/agent.d.ts +2 -2
  20. package/dist/server/server/agent/providers/claude/agent.js +261 -167
  21. package/dist/server/server/agent/providers/claude/models.js +7 -3
  22. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +6 -4
  23. package/dist/server/server/agent/providers/codex-app-server-agent.js +48 -25
  24. package/dist/server/server/agent/providers/copilot-acp-agent.js +4 -31
  25. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +9 -0
  26. package/dist/server/server/agent/providers/diagnostic-utils.js +188 -0
  27. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +0 -1
  28. package/dist/server/server/agent/providers/generic-acp-agent.js +2 -108
  29. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -3
  30. package/dist/server/server/agent/providers/mock-load-test-agent.js +5 -5
  31. package/dist/server/server/agent/providers/mock-slow-provider.d.ts +2 -3
  32. package/dist/server/server/agent/providers/mock-slow-provider.js +3 -6
  33. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +29 -2
  34. package/dist/server/server/agent/providers/opencode/server-manager.js +83 -17
  35. package/dist/server/server/agent/providers/opencode-agent.d.ts +6 -3
  36. package/dist/server/server/agent/providers/opencode-agent.js +61 -107
  37. package/dist/server/server/agent/providers/pi/agent.d.ts +2 -3
  38. package/dist/server/server/agent/providers/pi/agent.js +11 -63
  39. package/dist/server/server/agent/providers/pi/cli-runtime.js +2 -2
  40. package/dist/server/server/agent/providers/pi/runtime.d.ts +1 -1
  41. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +1 -1
  42. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +1 -1
  43. package/dist/server/server/bootstrap.d.ts +2 -0
  44. package/dist/server/server/bootstrap.js +32 -2
  45. package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
  46. package/dist/server/server/managed-processes/managed-processes.js +326 -0
  47. package/dist/server/server/resolve-worktree-creation-intent.d.ts +3 -0
  48. package/dist/server/server/resolve-worktree-creation-intent.js +3 -3
  49. package/dist/server/server/session/agent-config/agent-config-session.d.ts +50 -0
  50. package/dist/server/server/session/agent-config/agent-config-session.js +98 -0
  51. package/dist/server/server/session/chat/chat-schedule-loop-session.d.ts +120 -0
  52. package/dist/server/server/session/chat/chat-schedule-loop-session.js +489 -0
  53. package/dist/server/server/session/checkout/checkout-session.d.ts +142 -0
  54. package/dist/server/server/session/checkout/checkout-session.js +925 -0
  55. package/dist/server/server/session/daemon/daemon-session.d.ts +50 -0
  56. package/dist/server/server/session/daemon/daemon-session.js +98 -0
  57. package/dist/server/server/session/files/workspace-files-session.d.ts +43 -0
  58. package/dist/server/server/session/files/workspace-files-session.js +218 -0
  59. package/dist/server/server/session/project-config/project-config-session.d.ts +34 -0
  60. package/dist/server/server/session/project-config/project-config-session.js +125 -0
  61. package/dist/server/server/session/provider/provider-catalog-session.d.ts +74 -0
  62. package/dist/server/server/session/provider/provider-catalog-session.js +339 -0
  63. package/dist/server/server/session/voice/voice-session.d.ts +166 -0
  64. package/dist/server/server/session/voice/voice-session.js +893 -0
  65. package/dist/server/server/{voice → session/voice}/voice-turn-controller.d.ts +2 -2
  66. package/dist/server/server/{voice → session/voice}/voice-turn-controller.js +2 -2
  67. package/dist/server/server/session.d.ts +23 -207
  68. package/dist/server/server/session.js +2319 -5102
  69. package/dist/server/server/speech/providers/openai/runtime.js +3 -4
  70. package/dist/server/server/websocket-server.d.ts +1 -0
  71. package/dist/server/server/websocket-server.js +11 -0
  72. package/dist/server/server/workspace-archive-service.js +2 -3
  73. package/dist/server/server/workspace-directory.js +5 -5
  74. package/dist/server/server/workspace-reconciliation-service.js +2 -2
  75. package/dist/server/server/worktree-core.d.ts +1 -0
  76. package/dist/server/server/worktree-core.js +5 -1
  77. package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
  78. package/dist/server/services/quota-fetcher/manifest.js +47 -0
  79. package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
  80. package/dist/server/services/quota-fetcher/provider.js +2 -0
  81. package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
  82. package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
  83. package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
  84. package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
  85. package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
  86. package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
  87. package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
  88. package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
  89. package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
  90. package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
  91. package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
  92. package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
  93. package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
  94. package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
  95. package/dist/server/services/quota-fetcher/service.d.ts +28 -0
  96. package/dist/server/services/quota-fetcher/service.js +58 -0
  97. package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
  98. package/dist/server/services/quota-fetcher/usage.js +49 -0
  99. package/dist/server/utils/checkout-git.d.ts +6 -0
  100. package/dist/server/utils/directory-suggestions.js +98 -2
  101. package/package.json +5 -5
@@ -61,7 +61,7 @@ export declare class FakePiSession implements PiRuntimeSession {
61
61
  abort(): Promise<void>;
62
62
  getState(): Promise<PiSessionState>;
63
63
  getMessages(): Promise<PiAgentMessage[]>;
64
- getAvailableModels(): Promise<PiModel[]>;
64
+ getAvailableModels(_timeoutMs?: number): Promise<PiModel[]>;
65
65
  setModel(provider: string, modelId: string): Promise<PiModel>;
66
66
  setThinkingLevel(level: string): Promise<void>;
67
67
  getSessionStats(): Promise<PiSessionStats>;
@@ -100,7 +100,7 @@ export class FakePiSession {
100
100
  async getMessages() {
101
101
  return this.messages;
102
102
  }
103
- async getAvailableModels() {
103
+ async getAvailableModels(_timeoutMs) {
104
104
  return this.models;
105
105
  }
106
106
  async setModel(provider, modelId) {
@@ -25,6 +25,7 @@ import type { AgentProviderRuntimeSettingsMap, ProviderOverride } from "./agent/
25
25
  import type { PersistedConfig } from "./persisted-config.js";
26
26
  import { type ServiceProxySubsystem } from "./service-proxy.js";
27
27
  import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
28
+ import { type ManagedProcessRegistry } from "./managed-processes/managed-processes.js";
28
29
  import { type HostnamesConfig } from "./hostnames.js";
29
30
  import { type DaemonAuthConfig } from "./auth.js";
30
31
  export declare function createTerminalActivityRouteHandler(terminalManager: TerminalManager): express.RequestHandler;
@@ -97,6 +98,7 @@ export interface PaseoDaemonConfig {
97
98
  log?: PersistedConfig["log"];
98
99
  onLifecycleIntent?: (intent: DaemonLifecycleIntent) => void;
99
100
  pushNotificationSender?: PushNotificationSender;
101
+ managedProcesses?: ManagedProcessRegistry;
100
102
  }
101
103
  export interface PaseoDaemon {
102
104
  config: PaseoDaemonConfig;
@@ -52,12 +52,15 @@ export function parseListenString(listen) {
52
52
  }
53
53
  // 6. host:port — TCP
54
54
  if (listen.includes(":")) {
55
- const [host, portStr] = listen.split(":");
55
+ const lastColonIdx = listen.lastIndexOf(":");
56
+ const host = listen.slice(0, lastColonIdx);
57
+ const portStr = listen.slice(lastColonIdx + 1);
56
58
  const parsedPort = parseInt(portStr, 10);
57
59
  if (!Number.isFinite(parsedPort)) {
58
60
  throw new Error(`Invalid port in listen string: ${listen}`);
59
61
  }
60
- return { type: "tcp", host: host || "127.0.0.1", port: parsedPort };
62
+ const cleanHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
63
+ return { type: "tcp", host: cleanHost || "127.0.0.1", port: parsedPort };
61
64
  }
62
65
  throw new Error(`Invalid listen string: ${listen}`);
63
66
  }
@@ -105,6 +108,8 @@ import { createServiceProxySubsystem } from "./service-proxy.js";
105
108
  import { ScriptHealthMonitor } from "./script-health-monitor.js";
106
109
  import { createScriptStatusEmitter } from "./script-status-projection.js";
107
110
  import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
111
+ import { createManagedProcessRegistry, createSystemManagedProcessTable, } from "./managed-processes/managed-processes.js";
112
+ import { terminateWithTreeKill } from "../utils/tree-kill.js";
108
113
  import { isHostnameAllowed } from "./hostnames.js";
109
114
  import { createRequireBearerMiddleware, isAgentMcpRequestAuthorized, } from "./auth.js";
110
115
  const MAX_MCP_DEBUG_BATCH_ITEMS = 10;
@@ -207,6 +212,23 @@ function summarizeAgentMcpDebugBody(body) {
207
212
  ...(body.length > messages.length ? { omitted: body.length - messages.length } : {}),
208
213
  };
209
214
  }
215
+ function createBootstrapManagedProcessRegistry(config, logger) {
216
+ if (config.managedProcesses) {
217
+ return config.managedProcesses;
218
+ }
219
+ return createManagedProcessRegistry({
220
+ paseoHome: config.paseoHome,
221
+ processTable: createSystemManagedProcessTable(),
222
+ terminateProcess: terminateWithTreeKill,
223
+ logger,
224
+ });
225
+ }
226
+ async function reconcileManagedProcessLedger(managedProcesses, logger) {
227
+ const reapResult = await managedProcesses.reapStale();
228
+ if (reapResult.checked > 0 || reapResult.errors.length > 0) {
229
+ logger.info(reapResult, "Managed helper process ledger reconciled");
230
+ }
231
+ }
210
232
  export async function createPaseoDaemon(config, rootLogger) {
211
233
  const logger = rootLogger.child({ module: "bootstrap" });
212
234
  const bootstrapStart = performance.now();
@@ -233,6 +255,13 @@ export async function createPaseoDaemon(config, rootLogger) {
233
255
  }, logger);
234
256
  const serverId = getOrCreateServerId(config.paseoHome, { logger });
235
257
  const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger);
258
+ const managedProcesses = createBootstrapManagedProcessRegistry(config, logger);
259
+ // Reconcile the helper-process ledger in the background so it never blocks the
260
+ // daemon from coming up; terminating a live leftover can take a few seconds.
261
+ // Best-effort, so a failure is logged here rather than crashing startup.
262
+ void reconcileManagedProcessLedger(managedProcesses, logger).catch((error) => {
263
+ logger.warn({ err: error }, "Failed to reconcile managed helper process ledger");
264
+ });
236
265
  let relayTransport = null;
237
266
  const staticDir = config.staticDir;
238
267
  const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
@@ -434,6 +463,7 @@ export async function createPaseoDaemon(config, rootLogger) {
434
463
  runtimeSettings: config.agentProviderSettings,
435
464
  providerOverrides: config.providerOverrides,
436
465
  workspaceGitService,
466
+ managedProcesses,
437
467
  isDev: config.isDev === true,
438
468
  extraClients: config.agentClients,
439
469
  });
@@ -0,0 +1,76 @@
1
+ import type { Logger } from "pino";
2
+ import type { ProcessTerminator, TreeKillTarget } from "../../utils/tree-kill.js";
3
+ export interface ManagedProcessSnapshot {
4
+ pid: number;
5
+ commandLine: string | null;
6
+ startedAt: string | null;
7
+ }
8
+ export type ManagedProcessInspection = {
9
+ status: "alive";
10
+ snapshot: ManagedProcessSnapshot;
11
+ } | {
12
+ status: "not-found";
13
+ } | {
14
+ status: "error";
15
+ error: unknown;
16
+ };
17
+ export interface ManagedProcessTable {
18
+ inspect(pid: number): Promise<ManagedProcessInspection>;
19
+ }
20
+ export interface ManagedProcessCommandRunner {
21
+ exec(command: string, args: string[]): Promise<{
22
+ stdout: string;
23
+ stderr: string;
24
+ }>;
25
+ }
26
+ export interface ManagedProcessOwner {
27
+ provider: string;
28
+ kind: string;
29
+ }
30
+ export interface ManagedProcessRecordInput {
31
+ owner: ManagedProcessOwner;
32
+ pid: number;
33
+ command: string;
34
+ args: string[];
35
+ metadata?: Record<string, unknown>;
36
+ }
37
+ export interface ManagedProcessRecord extends ManagedProcessRecordInput {
38
+ id: string;
39
+ metadata: Record<string, unknown>;
40
+ identity: {
41
+ commandLine: string | null;
42
+ startedAt: string | null;
43
+ };
44
+ createdAt: string;
45
+ }
46
+ export interface ManagedProcessReapResult {
47
+ checked: number;
48
+ dead: number;
49
+ mismatched: number;
50
+ removed: number;
51
+ terminated: number;
52
+ errors: Array<{
53
+ id: string;
54
+ message: string;
55
+ }>;
56
+ }
57
+ export interface ManagedProcessRegistry {
58
+ record(input: ManagedProcessRecordInput): Promise<ManagedProcessRecord>;
59
+ remove(id: string): Promise<void>;
60
+ list(): Promise<ManagedProcessRecord[]>;
61
+ reapStale(): Promise<ManagedProcessReapResult>;
62
+ }
63
+ interface ManagedProcessRegistryOptions {
64
+ paseoHome: string;
65
+ processTable: ManagedProcessTable;
66
+ terminateProcess: ProcessTerminator;
67
+ logger: Logger;
68
+ }
69
+ export declare function createManagedProcessRegistry(options: ManagedProcessRegistryOptions): ManagedProcessRegistry;
70
+ export declare function createSystemManagedProcessTable(options?: {
71
+ platform?: NodeJS.Platform;
72
+ commandRunner?: ManagedProcessCommandRunner;
73
+ }): ManagedProcessTable;
74
+ export declare function createPidTarget(pid: number): TreeKillTarget;
75
+ export {};
76
+ //# sourceMappingURL=managed-processes.d.ts.map
@@ -0,0 +1,326 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { writeJsonFileAtomic } from "../atomic-file.js";
6
+ import { execCommand } from "../../utils/spawn.js";
7
+ const MANAGED_PROCESS_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5000;
8
+ const MANAGED_PROCESS_FORCE_SHUTDOWN_TIMEOUT_MS = 1000;
9
+ const MANAGED_PROCESS_EXIT_POLL_INTERVAL_MS = 50;
10
+ const MANAGED_PROCESS_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
11
+ // `ps -o lstart` emits a fixed-width 24-char ctime stamp, e.g. "Sat Jun 20 10:30:40 2026".
12
+ const POSIX_LSTART_WIDTH = 24;
13
+ const ManagedProcessRecordSchema = z.object({
14
+ id: z.string().min(1),
15
+ owner: z.object({
16
+ provider: z.string().min(1),
17
+ kind: z.string().min(1),
18
+ }),
19
+ pid: z.number().int().positive(),
20
+ command: z.string().min(1),
21
+ args: z.array(z.string()),
22
+ metadata: z.record(z.string(), z.unknown()).default({}),
23
+ identity: z.object({
24
+ commandLine: z.string().nullable(),
25
+ startedAt: z.string().nullable(),
26
+ }),
27
+ createdAt: z.string().min(1),
28
+ });
29
+ const WindowsProcessSnapshotSchema = z.object({
30
+ ProcessId: z.number().int().positive(),
31
+ CommandLine: z.string().nullable().optional(),
32
+ CreationDate: z.string().nullable().optional(),
33
+ });
34
+ export function createManagedProcessRegistry(options) {
35
+ return new FileBackedManagedProcessRegistry(options);
36
+ }
37
+ export function createSystemManagedProcessTable(options) {
38
+ return new SystemManagedProcessTable({
39
+ platform: options?.platform ?? process.platform,
40
+ commandRunner: options?.commandRunner ?? {
41
+ exec: execCommand,
42
+ },
43
+ });
44
+ }
45
+ class SystemManagedProcessTable {
46
+ constructor(options) {
47
+ this.platform = options.platform;
48
+ this.commandRunner = options.commandRunner;
49
+ }
50
+ async inspect(pid) {
51
+ if (!Number.isInteger(pid) || pid <= 0) {
52
+ return { status: "not-found" };
53
+ }
54
+ try {
55
+ return this.platform === "win32"
56
+ ? await this.inspectWindows(pid)
57
+ : await this.inspectPosix(pid);
58
+ }
59
+ catch (error) {
60
+ return { status: "error", error };
61
+ }
62
+ }
63
+ async inspectPosix(pid) {
64
+ let stdout;
65
+ try {
66
+ ({ stdout } = await this.commandRunner.exec("ps", [
67
+ "-ww",
68
+ "-p",
69
+ String(pid),
70
+ "-o",
71
+ "lstart=",
72
+ "-o",
73
+ "command=",
74
+ ]));
75
+ }
76
+ catch (error) {
77
+ // `ps -p <pid>` exits non-zero when no process matches the pid; a numeric
78
+ // exit code means ps ran and found nothing, distinct from ps failing to run.
79
+ return isCommandExitFailure(error) ? { status: "not-found" } : { status: "error", error };
80
+ }
81
+ const line = stdout.trimEnd();
82
+ if (!line) {
83
+ return { status: "not-found" };
84
+ }
85
+ const startedAt = line.slice(0, POSIX_LSTART_WIDTH).trim();
86
+ const commandLine = line.slice(POSIX_LSTART_WIDTH).trim();
87
+ return {
88
+ status: "alive",
89
+ snapshot: {
90
+ pid,
91
+ commandLine: commandLine || null,
92
+ startedAt: startedAt || null,
93
+ },
94
+ };
95
+ }
96
+ async inspectWindows(pid) {
97
+ const command = [
98
+ `$process = Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}';`,
99
+ "if ($process) { $process | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress }",
100
+ ].join(" ");
101
+ const { stdout } = await this.commandRunner.exec("powershell.exe", [
102
+ "-NoProfile",
103
+ "-NonInteractive",
104
+ "-Command",
105
+ command,
106
+ ]);
107
+ const trimmed = stdout.trim();
108
+ if (!trimmed) {
109
+ return { status: "not-found" };
110
+ }
111
+ const parsed = WindowsProcessSnapshotSchema.parse(JSON.parse(trimmed));
112
+ return {
113
+ status: "alive",
114
+ snapshot: {
115
+ pid,
116
+ commandLine: parsed.CommandLine ?? null,
117
+ startedAt: parsed.CreationDate ?? null,
118
+ },
119
+ };
120
+ }
121
+ }
122
+ class FileBackedManagedProcessRegistry {
123
+ constructor(options) {
124
+ this.directory = path.join(options.paseoHome, "runtime", "managed-processes");
125
+ this.processTable = options.processTable;
126
+ this.terminateProcess = options.terminateProcess;
127
+ this.logger = options.logger.child({ module: "managed-processes" });
128
+ }
129
+ async record(input) {
130
+ const inspection = await this.processTable.inspect(input.pid);
131
+ const snapshot = inspection.status === "alive" ? inspection.snapshot : null;
132
+ const record = {
133
+ id: randomUUID(),
134
+ owner: input.owner,
135
+ pid: input.pid,
136
+ command: input.command,
137
+ args: input.args,
138
+ metadata: input.metadata ?? {},
139
+ identity: {
140
+ commandLine: snapshot?.commandLine ?? null,
141
+ startedAt: snapshot?.startedAt ?? null,
142
+ },
143
+ createdAt: new Date().toISOString(),
144
+ };
145
+ await writeJsonFileAtomic(this.recordPath(record.id), record);
146
+ return record;
147
+ }
148
+ async remove(id) {
149
+ await fs.rm(this.recordPath(id), { force: true });
150
+ }
151
+ async list() {
152
+ const entries = await this.readEntries();
153
+ return entries.map((entry) => entry.record);
154
+ }
155
+ async reapStale() {
156
+ const result = {
157
+ checked: 0,
158
+ dead: 0,
159
+ mismatched: 0,
160
+ removed: 0,
161
+ terminated: 0,
162
+ errors: [],
163
+ };
164
+ for (const entry of await this.readEntries()) {
165
+ result.checked += 1;
166
+ try {
167
+ const inspection = await this.processTable.inspect(entry.record.pid);
168
+ if (inspection.status === "not-found") {
169
+ await fs.rm(entry.path, { force: true });
170
+ result.dead += 1;
171
+ result.removed += 1;
172
+ continue;
173
+ }
174
+ if (inspection.status === "error") {
175
+ // Inspection failed, so we cannot tell whether the helper is still
176
+ // alive. Keep the record and retry on the next reconcile rather than
177
+ // orphaning a live process by deleting its record without killing it.
178
+ const message = inspection.error instanceof Error ? inspection.error.message : String(inspection.error);
179
+ result.errors.push({ id: entry.record.id, message });
180
+ this.logger.warn({
181
+ err: inspection.error,
182
+ id: entry.record.id,
183
+ pid: entry.record.pid,
184
+ owner: entry.record.owner,
185
+ }, "Could not inspect managed helper process; leaving record for next reconcile");
186
+ continue;
187
+ }
188
+ const snapshot = inspection.snapshot;
189
+ if (!processIdentityMatches(entry.record, snapshot)) {
190
+ await fs.rm(entry.path, { force: true });
191
+ result.mismatched += 1;
192
+ result.removed += 1;
193
+ continue;
194
+ }
195
+ await this.terminateProcess(createPidTarget(entry.record.pid), {
196
+ gracefulTimeoutMs: MANAGED_PROCESS_GRACEFUL_SHUTDOWN_TIMEOUT_MS,
197
+ forceTimeoutMs: MANAGED_PROCESS_FORCE_SHUTDOWN_TIMEOUT_MS,
198
+ onForceSignal: () => {
199
+ this.logger.warn({
200
+ pid: entry.record.pid,
201
+ owner: entry.record.owner,
202
+ timeoutMs: MANAGED_PROCESS_GRACEFUL_SHUTDOWN_TIMEOUT_MS,
203
+ }, "Managed helper process did not exit after SIGTERM; sending SIGKILL");
204
+ },
205
+ });
206
+ await fs.rm(entry.path, { force: true });
207
+ result.terminated += 1;
208
+ result.removed += 1;
209
+ }
210
+ catch (error) {
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ result.errors.push({ id: entry.record.id, message });
213
+ this.logger.warn({ err: error, id: entry.record.id, pid: entry.record.pid, owner: entry.record.owner }, "Failed to reap managed helper process");
214
+ }
215
+ }
216
+ return result;
217
+ }
218
+ recordPath(id) {
219
+ if (!MANAGED_PROCESS_ID_PATTERN.test(id)) {
220
+ throw new Error(`Invalid managed process record id: ${id}`);
221
+ }
222
+ return path.join(this.directory, `${id}.json`);
223
+ }
224
+ async readEntries() {
225
+ let fileNames;
226
+ try {
227
+ fileNames = await fs.readdir(this.directory);
228
+ }
229
+ catch (error) {
230
+ if (isNodeErrorWithCode(error, "ENOENT")) {
231
+ return [];
232
+ }
233
+ throw error;
234
+ }
235
+ const entries = [];
236
+ for (const fileName of fileNames) {
237
+ if (!fileName.endsWith(".json")) {
238
+ continue;
239
+ }
240
+ const filePath = path.join(this.directory, fileName);
241
+ try {
242
+ const raw = await fs.readFile(filePath, "utf8");
243
+ const parsed = ManagedProcessRecordSchema.parse(JSON.parse(raw));
244
+ entries.push({ path: filePath, record: parsed });
245
+ }
246
+ catch (error) {
247
+ // A single corrupt or partially-written record must not abort the whole
248
+ // reconcile and leave every other leftover un-reaped. Skip it.
249
+ this.logger.warn({ err: error, file: fileName }, "Skipping unreadable managed process record");
250
+ }
251
+ }
252
+ return entries;
253
+ }
254
+ }
255
+ function processIdentityMatches(record, snapshot) {
256
+ if (record.identity.startedAt && snapshot.startedAt) {
257
+ if (record.identity.startedAt !== snapshot.startedAt) {
258
+ return false;
259
+ }
260
+ return snapshot.commandLine ? commandLineMatchesRecord(record, snapshot.commandLine) : true;
261
+ }
262
+ if (record.identity.commandLine && snapshot.commandLine) {
263
+ return (normalizeCommandLine(record.identity.commandLine) ===
264
+ normalizeCommandLine(snapshot.commandLine));
265
+ }
266
+ return snapshot.commandLine ? commandLineMatchesRecord(record, snapshot.commandLine) : false;
267
+ }
268
+ function commandLineMatchesRecord(record, commandLine) {
269
+ // Require the command name and args as one contiguous run, not scattered
270
+ // tokens. Without exact process identity (lstart), a reused PID whose command
271
+ // line merely mentions "opencode", "serve" and the port elsewhere must not be
272
+ // mistaken for our leftover and killed.
273
+ const normalized = normalizeCommandLine(commandLine);
274
+ const commandName = path.basename(record.command).toLowerCase();
275
+ const signature = [commandName, ...record.args].map((token) => token.toLowerCase()).join(" ");
276
+ return normalized.includes(signature);
277
+ }
278
+ function normalizeCommandLine(commandLine) {
279
+ return commandLine.replace(/\s+/g, " ").trim().toLowerCase();
280
+ }
281
+ export function createPidTarget(pid) {
282
+ return {
283
+ pid,
284
+ exitCode: null,
285
+ signalCode: null,
286
+ kill(signal) {
287
+ process.kill(pid, signal);
288
+ return true;
289
+ },
290
+ // The reaper has no ChildProcess handle for a leftover from a previous
291
+ // daemon, so it observes exit by polling the pid. Without this, termination
292
+ // can never see a graceful SIGTERM exit and always waits out the full
293
+ // graceful+force window before escalating to SIGKILL.
294
+ once(_event, listener) {
295
+ const timer = setInterval(() => {
296
+ if (!isProcessAlive(pid)) {
297
+ clearInterval(timer);
298
+ listener();
299
+ }
300
+ }, MANAGED_PROCESS_EXIT_POLL_INTERVAL_MS);
301
+ timer.unref();
302
+ },
303
+ };
304
+ }
305
+ function isProcessAlive(pid) {
306
+ try {
307
+ process.kill(pid, 0);
308
+ return true;
309
+ }
310
+ catch (error) {
311
+ return isNodeErrorWithCode(error, "EPERM");
312
+ }
313
+ }
314
+ function isCommandExitFailure(error) {
315
+ // execFile rejects with a numeric `code` (the process exit status) when the
316
+ // command ran and exited non-zero; a string `code` (e.g. "ENOENT") means it
317
+ // never ran.
318
+ return typeof error?.code === "number";
319
+ }
320
+ function isNodeErrorWithCode(error, code) {
321
+ return (typeof error === "object" &&
322
+ error !== null &&
323
+ "code" in error &&
324
+ error.code === code);
325
+ }
326
+ //# sourceMappingURL=managed-processes.js.map
@@ -3,16 +3,19 @@ import type { WorktreeSource } from "../utils/worktree.js";
3
3
  export type WorktreeCreationIntent = WorktreeSource;
4
4
  export type ResolveWorktreeCreationIntentInput = {
5
5
  worktreeSlug: string;
6
+ branchName?: string;
6
7
  refName?: string;
7
8
  action?: "branch-off";
8
9
  githubPrNumber?: undefined;
9
10
  } | {
10
11
  worktreeSlug?: string;
12
+ branchName?: string;
11
13
  refName?: string;
12
14
  action: "checkout";
13
15
  githubPrNumber?: number;
14
16
  } | {
15
17
  worktreeSlug?: string;
18
+ branchName?: string;
16
19
  refName?: string;
17
20
  action?: undefined;
18
21
  githubPrNumber: number;
@@ -10,7 +10,7 @@ export async function resolveWorktreeCreationIntent(input, repoRoot, deps) {
10
10
  return {
11
11
  kind: "branch-off",
12
12
  baseBranch: input.refName?.trim() || (await resolveDefaultBranch(repoRoot, deps)),
13
- branchName: input.worktreeSlug,
13
+ branchName: input.branchName ?? input.worktreeSlug,
14
14
  };
15
15
  }
16
16
  if (input.action === "checkout") {
@@ -43,13 +43,13 @@ export async function resolveWorktreeCreationIntent(input, repoRoot, deps) {
43
43
  return {
44
44
  kind: "branch-off",
45
45
  baseBranch: input.refName.trim(),
46
- branchName: input.worktreeSlug,
46
+ branchName: input.branchName ?? input.worktreeSlug,
47
47
  };
48
48
  }
49
49
  return {
50
50
  kind: "branch-off",
51
51
  baseBranch: await resolveDefaultBranch(repoRoot, deps),
52
- branchName: input.worktreeSlug,
52
+ branchName: input.branchName ?? input.worktreeSlug,
53
53
  };
54
54
  }
55
55
  async function resolveGitHubPrCheckoutIntent(params) {
@@ -0,0 +1,50 @@
1
+ import type pino from "pino";
2
+ import type { AgentProviderNotice } from "../../agent/agent-sdk-types.js";
3
+ import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js";
4
+ export interface AgentConfigSessionHost {
5
+ emit(msg: SessionOutboundMessage): void;
6
+ }
7
+ /**
8
+ * The per-agent config mutations this subsystem drives. The shell adapts these
9
+ * onto the live AgentManager (mode still routes through setAgentModeCommand);
10
+ * tests wire an in-memory fake. Mode and thinking yield a provider notice; model
11
+ * and feature do not.
12
+ */
13
+ export interface AgentConfigOperations {
14
+ setMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null>;
15
+ setModel(agentId: string, modelId: string | null): Promise<void>;
16
+ setFeature(agentId: string, featureId: string, value: unknown): Promise<void>;
17
+ setThinking(agentId: string, thinkingOptionId: string | null): Promise<AgentProviderNotice | null>;
18
+ }
19
+ export interface AgentConfigSessionOptions {
20
+ host: AgentConfigSessionHost;
21
+ operations: AgentConfigOperations;
22
+ logger: pino.Logger;
23
+ }
24
+ /**
25
+ * A client's per-agent config surface: set mode, model, feature, and thinking
26
+ * option. Each request shares one envelope — log, run the mutation, then emit the
27
+ * accepted response, or on failure emit an activity_log error frame followed by
28
+ * the rejected response. Reaches no state beyond the injected operations and the
29
+ * outbound channel.
30
+ */
31
+ export declare class AgentConfigSession {
32
+ private readonly host;
33
+ private readonly operations;
34
+ private readonly logger;
35
+ constructor(options: AgentConfigSessionOptions);
36
+ handleSetAgentModeRequest(msg: Extract<SessionInboundMessage, {
37
+ type: "set_agent_mode_request";
38
+ }>): Promise<void>;
39
+ handleSetAgentModelRequest(msg: Extract<SessionInboundMessage, {
40
+ type: "set_agent_model_request";
41
+ }>): Promise<void>;
42
+ handleSetAgentFeatureRequest(msg: Extract<SessionInboundMessage, {
43
+ type: "set_agent_feature_request";
44
+ }>): Promise<void>;
45
+ handleSetAgentThinkingRequest(msg: Extract<SessionInboundMessage, {
46
+ type: "set_agent_thinking_request";
47
+ }>): Promise<void>;
48
+ private applyConfigChange;
49
+ }
50
+ //# sourceMappingURL=agent-config-session.d.ts.map