@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.
- package/dist/server/server/agent/agent-manager.d.ts +11 -3
- package/dist/server/server/agent/agent-manager.js +96 -24
- package/dist/server/server/agent/agent-prompt.d.ts +1 -1
- package/dist/server/server/agent/agent-prompt.js +3 -10
- package/dist/server/server/agent/agent-sdk-types.d.ts +20 -9
- package/dist/server/server/agent/create-agent/create.d.ts +2 -0
- package/dist/server/server/agent/create-agent/create.js +8 -7
- package/dist/server/server/agent/lifecycle-command.d.ts +15 -1
- package/dist/server/server/agent/lifecycle-command.js +9 -2
- package/dist/server/server/agent/mcp-server.js +254 -115
- package/dist/server/server/agent/provider-notices.d.ts +3 -0
- package/dist/server/server/agent/provider-notices.js +5 -0
- package/dist/server/server/agent/provider-registry.d.ts +8 -3
- package/dist/server/server/agent/provider-registry.js +58 -25
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +3 -0
- package/dist/server/server/agent/provider-snapshot-manager.js +37 -16
- package/dist/server/server/agent/providers/acp-agent.d.ts +5 -3
- package/dist/server/server/agent/providers/acp-agent.js +32 -19
- package/dist/server/server/agent/providers/claude/agent.d.ts +2 -2
- package/dist/server/server/agent/providers/claude/agent.js +261 -167
- package/dist/server/server/agent/providers/claude/models.js +7 -3
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +6 -4
- package/dist/server/server/agent/providers/codex-app-server-agent.js +48 -25
- package/dist/server/server/agent/providers/copilot-acp-agent.js +4 -31
- package/dist/server/server/agent/providers/diagnostic-utils.d.ts +9 -0
- package/dist/server/server/agent/providers/diagnostic-utils.js +188 -0
- package/dist/server/server/agent/providers/generic-acp-agent.d.ts +0 -1
- package/dist/server/server/agent/providers/generic-acp-agent.js +2 -108
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -3
- package/dist/server/server/agent/providers/mock-load-test-agent.js +5 -5
- package/dist/server/server/agent/providers/mock-slow-provider.d.ts +2 -3
- package/dist/server/server/agent/providers/mock-slow-provider.js +3 -6
- package/dist/server/server/agent/providers/opencode/server-manager.d.ts +29 -2
- package/dist/server/server/agent/providers/opencode/server-manager.js +83 -17
- package/dist/server/server/agent/providers/opencode-agent.d.ts +6 -3
- package/dist/server/server/agent/providers/opencode-agent.js +61 -107
- package/dist/server/server/agent/providers/pi/agent.d.ts +2 -3
- package/dist/server/server/agent/providers/pi/agent.js +11 -63
- package/dist/server/server/agent/providers/pi/cli-runtime.js +2 -2
- package/dist/server/server/agent/providers/pi/runtime.d.ts +1 -1
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +1 -1
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +1 -1
- package/dist/server/server/bootstrap.d.ts +2 -0
- package/dist/server/server/bootstrap.js +32 -2
- package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
- package/dist/server/server/managed-processes/managed-processes.js +326 -0
- package/dist/server/server/resolve-worktree-creation-intent.d.ts +3 -0
- package/dist/server/server/resolve-worktree-creation-intent.js +3 -3
- package/dist/server/server/session/agent-config/agent-config-session.d.ts +50 -0
- package/dist/server/server/session/agent-config/agent-config-session.js +98 -0
- package/dist/server/server/session/chat/chat-schedule-loop-session.d.ts +120 -0
- package/dist/server/server/session/chat/chat-schedule-loop-session.js +489 -0
- package/dist/server/server/session/checkout/checkout-session.d.ts +142 -0
- package/dist/server/server/session/checkout/checkout-session.js +925 -0
- package/dist/server/server/session/daemon/daemon-session.d.ts +50 -0
- package/dist/server/server/session/daemon/daemon-session.js +98 -0
- package/dist/server/server/session/files/workspace-files-session.d.ts +43 -0
- package/dist/server/server/session/files/workspace-files-session.js +218 -0
- package/dist/server/server/session/project-config/project-config-session.d.ts +34 -0
- package/dist/server/server/session/project-config/project-config-session.js +125 -0
- package/dist/server/server/session/provider/provider-catalog-session.d.ts +74 -0
- package/dist/server/server/session/provider/provider-catalog-session.js +339 -0
- package/dist/server/server/session/voice/voice-session.d.ts +166 -0
- package/dist/server/server/session/voice/voice-session.js +893 -0
- package/dist/server/server/{voice → session/voice}/voice-turn-controller.d.ts +2 -2
- package/dist/server/server/{voice → session/voice}/voice-turn-controller.js +2 -2
- package/dist/server/server/session.d.ts +23 -207
- package/dist/server/server/session.js +2319 -5102
- package/dist/server/server/speech/providers/openai/runtime.js +3 -4
- package/dist/server/server/websocket-server.d.ts +1 -0
- package/dist/server/server/websocket-server.js +11 -0
- package/dist/server/server/workspace-archive-service.js +2 -3
- package/dist/server/server/workspace-directory.js +5 -5
- package/dist/server/server/workspace-reconciliation-service.js +2 -2
- package/dist/server/server/worktree-core.d.ts +1 -0
- package/dist/server/server/worktree-core.js +5 -1
- package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
- package/dist/server/services/quota-fetcher/manifest.js +47 -0
- package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
- package/dist/server/services/quota-fetcher/provider.js +2 -0
- package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
- package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
- package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
- package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
- package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
- package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
- package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
- package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
- package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
- package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
- package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
- package/dist/server/services/quota-fetcher/service.d.ts +28 -0
- package/dist/server/services/quota-fetcher/service.js +58 -0
- package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
- package/dist/server/services/quota-fetcher/usage.js +49 -0
- package/dist/server/utils/checkout-git.d.ts +6 -0
- package/dist/server/utils/directory-suggestions.js +98 -2
- 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>;
|
|
@@ -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
|
|
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
|
-
|
|
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
|