@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.7
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/CHANGELOG.md +52 -0
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/settings-schema.ts +0 -10
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -564
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +74 -89
- package/src/eval/py/index.ts +1 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +95 -7
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +93 -5
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/prompts/tools/eval.md +14 -27
- package/src/session/agent-session.ts +0 -1
- package/src/session/history-storage.ts +77 -19
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +86 -5
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/eval.ts +1 -1
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
- /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
package/src/tools/eval.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const EVAL_DEFAULT_PREVIEW_LINES = 10;
|
|
|
26
26
|
|
|
27
27
|
export const evalSchema = Type.Object({
|
|
28
28
|
input: Type.String({
|
|
29
|
-
description:
|
|
29
|
+
description: 'eval input as a sequence of `*** Cell <lang>:"title"` cell headers followed by code',
|
|
30
30
|
}),
|
|
31
31
|
});
|
|
32
32
|
export type EvalToolParams = Static<typeof evalSchema>;
|
package/src/web/search/index.ts
CHANGED
|
@@ -136,6 +136,7 @@ function formatForLLM(response: SearchResponse): string {
|
|
|
136
136
|
async function executeSearch(
|
|
137
137
|
_toolCallId: string,
|
|
138
138
|
params: SearchQueryParams,
|
|
139
|
+
signal?: AbortSignal,
|
|
139
140
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
140
141
|
const providers =
|
|
141
142
|
params.provider && params.provider !== "auto"
|
|
@@ -165,6 +166,7 @@ async function executeSearch(
|
|
|
165
166
|
maxOutputTokens: params.max_tokens,
|
|
166
167
|
numSearchResults: params.num_search_results,
|
|
167
168
|
temperature: params.temperature,
|
|
169
|
+
signal,
|
|
168
170
|
});
|
|
169
171
|
|
|
170
172
|
const text = formatForLLM(response);
|
|
@@ -221,11 +223,11 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
|
|
|
221
223
|
async execute(
|
|
222
224
|
_toolCallId: string,
|
|
223
225
|
params: SearchToolParams,
|
|
224
|
-
|
|
226
|
+
signal?: AbortSignal,
|
|
225
227
|
_onUpdate?: AgentToolUpdateCallback<SearchRenderDetails>,
|
|
226
228
|
_context?: AgentToolContext,
|
|
227
229
|
): Promise<AgentToolResult<SearchRenderDetails>> {
|
|
228
|
-
return executeSearch(_toolCallId, params);
|
|
230
|
+
return executeSearch(_toolCallId, params, signal);
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
|
|
@@ -241,9 +243,9 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
|
|
|
241
243
|
params: SearchToolParams,
|
|
242
244
|
_onUpdate,
|
|
243
245
|
_ctx: CustomToolContext,
|
|
244
|
-
|
|
246
|
+
signal?: AbortSignal,
|
|
245
247
|
) {
|
|
246
|
-
return executeSearch(toolCallId, params);
|
|
248
|
+
return executeSearch(toolCallId, params, signal);
|
|
247
249
|
},
|
|
248
250
|
|
|
249
251
|
renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
|
package/src/cli/jupyter-cli.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Jupyter CLI command handlers.
|
|
3
|
-
*
|
|
4
|
-
* Handles `omp jupyter` subcommand for managing the shared Python gateway.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
import { getGatewayStatus, shutdownSharedGateway } from "../eval/py/gateway-coordinator";
|
|
10
|
-
|
|
11
|
-
export type JupyterAction = "kill" | "status";
|
|
12
|
-
|
|
13
|
-
export interface JupyterCommandArgs {
|
|
14
|
-
action: JupyterAction;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function parseJupyterArgs(args: string[]): JupyterCommandArgs | undefined {
|
|
18
|
-
if (args.length === 0 || args[0] !== "jupyter") {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const action = args[1] as JupyterAction | undefined;
|
|
23
|
-
if (!action || !["kill", "status"].includes(action)) {
|
|
24
|
-
return { action: "status" };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return { action };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function runJupyterCommand(cmd: JupyterCommandArgs): Promise<void> {
|
|
31
|
-
switch (cmd.action) {
|
|
32
|
-
case "kill":
|
|
33
|
-
await runKill();
|
|
34
|
-
break;
|
|
35
|
-
case "status":
|
|
36
|
-
await runStatus();
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function runKill(): Promise<void> {
|
|
42
|
-
const status = await getGatewayStatus();
|
|
43
|
-
|
|
44
|
-
if (!status.active) {
|
|
45
|
-
console.log(chalk.dim("No Jupyter gateway is running"));
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.log(`Killing Jupyter gateway (PID ${status.pid})...`);
|
|
50
|
-
await shutdownSharedGateway();
|
|
51
|
-
console.log(chalk.green("Jupyter gateway stopped"));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function runStatus(): Promise<void> {
|
|
55
|
-
const status = await getGatewayStatus();
|
|
56
|
-
|
|
57
|
-
if (!status.active) {
|
|
58
|
-
console.log(chalk.dim("No Jupyter gateway is running"));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log(chalk.bold("Jupyter Gateway Status\n"));
|
|
63
|
-
console.log(` ${chalk.green("●")} Running`);
|
|
64
|
-
console.log(` PID: ${status.pid}`);
|
|
65
|
-
console.log(` URL: ${status.url}`);
|
|
66
|
-
if (status.uptime !== null) {
|
|
67
|
-
console.log(` Uptime: ${formatUptime(status.uptime)}`);
|
|
68
|
-
}
|
|
69
|
-
if (status.pythonPath) {
|
|
70
|
-
console.log(` Python: ${status.pythonPath}`);
|
|
71
|
-
}
|
|
72
|
-
if (status.venvPath) {
|
|
73
|
-
console.log(` Venv: ${status.venvPath}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function formatUptime(ms: number): string {
|
|
78
|
-
const seconds = Math.floor(ms / 1000);
|
|
79
|
-
const minutes = Math.floor(seconds / 60);
|
|
80
|
-
const hours = Math.floor(minutes / 60);
|
|
81
|
-
|
|
82
|
-
if (hours > 0) {
|
|
83
|
-
return `${hours}h ${minutes % 60}m`;
|
|
84
|
-
}
|
|
85
|
-
if (minutes > 0) {
|
|
86
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
87
|
-
}
|
|
88
|
-
return `${seconds}s`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function printJupyterHelp(): void {
|
|
92
|
-
console.log(`${chalk.bold(`${APP_NAME} jupyter`)} - Manage the shared Jupyter gateway
|
|
93
|
-
|
|
94
|
-
${chalk.bold("Usage:")}
|
|
95
|
-
${APP_NAME} jupyter <command>
|
|
96
|
-
|
|
97
|
-
${chalk.bold("Commands:")}
|
|
98
|
-
status Show gateway status (default)
|
|
99
|
-
kill Stop the running gateway
|
|
100
|
-
|
|
101
|
-
${chalk.bold("Examples:")}
|
|
102
|
-
${APP_NAME} jupyter # Show status
|
|
103
|
-
${APP_NAME} jupyter status # Show status
|
|
104
|
-
${APP_NAME} jupyter kill # Stop the gateway
|
|
105
|
-
`);
|
|
106
|
-
}
|
package/src/commands/jupyter.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manage the shared Jupyter gateway.
|
|
3
|
-
*/
|
|
4
|
-
import { Args, Command } from "@oh-my-pi/pi-utils/cli";
|
|
5
|
-
import { type JupyterAction, type JupyterCommandArgs, runJupyterCommand } from "../cli/jupyter-cli";
|
|
6
|
-
import { initTheme } from "../modes/theme/theme";
|
|
7
|
-
|
|
8
|
-
const ACTIONS: JupyterAction[] = ["kill", "status"];
|
|
9
|
-
|
|
10
|
-
export default class Jupyter extends Command {
|
|
11
|
-
static description = "Manage the shared Jupyter gateway";
|
|
12
|
-
|
|
13
|
-
static args = {
|
|
14
|
-
action: Args.string({
|
|
15
|
-
description: "Jupyter action",
|
|
16
|
-
required: false,
|
|
17
|
-
options: ACTIONS,
|
|
18
|
-
}),
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
async run(): Promise<void> {
|
|
22
|
-
const { args } = await this.parse(Jupyter);
|
|
23
|
-
const action = (args.action ?? "status") as JupyterAction;
|
|
24
|
-
|
|
25
|
-
const cmd: JupyterCommandArgs = {
|
|
26
|
-
action,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
await initTheme();
|
|
30
|
-
await runJupyterCommand(cmd);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export function getAbortReason(signal: AbortSignal | undefined, fallbackReason: string): Error {
|
|
2
|
-
if (signal?.reason instanceof Error) return signal.reason;
|
|
3
|
-
if (typeof signal?.reason === "string" && signal.reason.length > 0) {
|
|
4
|
-
return new Error(signal.reason);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
return new Error(fallbackReason);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function createCancellationError(name: "AbortError" | "TimeoutError", message: string): Error {
|
|
11
|
-
const error = new Error(message);
|
|
12
|
-
error.name = name;
|
|
13
|
-
return error;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function getExecutionCancellationError(
|
|
17
|
-
result: { timedOut?: boolean },
|
|
18
|
-
signal: AbortSignal | undefined,
|
|
19
|
-
fallbackReason: string,
|
|
20
|
-
): Error {
|
|
21
|
-
if (signal?.aborted) {
|
|
22
|
-
return getAbortReason(signal, fallbackReason);
|
|
23
|
-
}
|
|
24
|
-
if (result.timedOut) {
|
|
25
|
-
return createCancellationError("TimeoutError", fallbackReason);
|
|
26
|
-
}
|
|
27
|
-
return createCancellationError("AbortError", fallbackReason);
|
|
28
|
-
}
|
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { createServer } from "node:net";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { Process } from "@oh-my-pi/pi-natives";
|
|
5
|
-
import { getPythonGatewayDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import type { Subprocess } from "bun";
|
|
7
|
-
import { Settings } from "../../config/settings";
|
|
8
|
-
import { getOrCreateSnapshot } from "../../utils/shell-snapshot";
|
|
9
|
-
import { filterEnv, resolvePythonRuntime } from "./runtime";
|
|
10
|
-
|
|
11
|
-
const GATEWAY_INFO_FILE = "gateway.json";
|
|
12
|
-
const GATEWAY_LOCK_FILE = "gateway.lock";
|
|
13
|
-
const GATEWAY_STARTUP_TIMEOUT_MS = 30000;
|
|
14
|
-
const GATEWAY_LOCK_TIMEOUT_MS = GATEWAY_STARTUP_TIMEOUT_MS + 5000;
|
|
15
|
-
const GATEWAY_LOCK_RETRY_MS = 50;
|
|
16
|
-
const GATEWAY_LOCK_STALE_MS = GATEWAY_STARTUP_TIMEOUT_MS * 2;
|
|
17
|
-
const GATEWAY_LOCK_HEARTBEAT_MS = 5000;
|
|
18
|
-
const HEALTH_CHECK_TIMEOUT_MS = 3000;
|
|
19
|
-
|
|
20
|
-
export interface GatewayInfo {
|
|
21
|
-
url: string;
|
|
22
|
-
pid: number;
|
|
23
|
-
startedAt: number;
|
|
24
|
-
pythonPath?: string;
|
|
25
|
-
venvPath?: string | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface GatewayLockInfo {
|
|
29
|
-
pid: number;
|
|
30
|
-
startedAt: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface AcquireResult {
|
|
34
|
-
url: string;
|
|
35
|
-
isShared: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let localGatewayProcess: Subprocess | null = null;
|
|
39
|
-
let localGatewayUrl: string | null = null;
|
|
40
|
-
let isCoordinatorInitialized = false;
|
|
41
|
-
|
|
42
|
-
async function allocatePort(): Promise<number> {
|
|
43
|
-
const { promise, resolve, reject } = Promise.withResolvers<number>();
|
|
44
|
-
const server = createServer();
|
|
45
|
-
server.unref();
|
|
46
|
-
server.on("error", reject);
|
|
47
|
-
server.listen(0, "127.0.0.1", () => {
|
|
48
|
-
const address = server.address();
|
|
49
|
-
if (address && typeof address === "object") {
|
|
50
|
-
const port = address.port;
|
|
51
|
-
server.close((err: Error | null | undefined) => {
|
|
52
|
-
if (err) {
|
|
53
|
-
reject(err);
|
|
54
|
-
} else {
|
|
55
|
-
resolve(port);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
} else {
|
|
59
|
-
server.close();
|
|
60
|
-
reject(new Error("Failed to allocate port"));
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return promise;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getGatewayDir(): string {
|
|
68
|
-
return getPythonGatewayDir();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getGatewayInfoPath(): string {
|
|
72
|
-
return path.join(getGatewayDir(), GATEWAY_INFO_FILE);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getGatewayLockPath(): string {
|
|
76
|
-
return path.join(getGatewayDir(), GATEWAY_LOCK_FILE);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function writeLockInfo(lockPath: string): Promise<void> {
|
|
80
|
-
const payload: GatewayLockInfo = { pid: process.pid, startedAt: Date.now() };
|
|
81
|
-
try {
|
|
82
|
-
await Bun.write(lockPath, JSON.stringify(payload));
|
|
83
|
-
} catch {
|
|
84
|
-
// Ignore lock write failures
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function readLockInfo(lockPath: string): Promise<GatewayLockInfo | null> {
|
|
89
|
-
try {
|
|
90
|
-
const raw = await Bun.file(lockPath).text();
|
|
91
|
-
const parsed = JSON.parse(raw) as Partial<GatewayLockInfo>;
|
|
92
|
-
if (typeof parsed.pid === "number" && Number.isFinite(parsed.pid)) {
|
|
93
|
-
return { pid: parsed.pid, startedAt: typeof parsed.startedAt === "number" ? parsed.startedAt : 0 };
|
|
94
|
-
}
|
|
95
|
-
} catch {
|
|
96
|
-
// Ignore parse errors
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function ensureGatewayDir(): Promise<void> {
|
|
102
|
-
const dir = getGatewayDir();
|
|
103
|
-
await fs.promises.mkdir(dir, { recursive: true });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
|
|
107
|
-
await ensureGatewayDir();
|
|
108
|
-
const lockPath = getGatewayLockPath();
|
|
109
|
-
const start = Date.now();
|
|
110
|
-
while (true) {
|
|
111
|
-
let fd: fs.promises.FileHandle | undefined;
|
|
112
|
-
try {
|
|
113
|
-
fd = await fs.promises.open(lockPath, "wx");
|
|
114
|
-
let heartbeatRunning = true;
|
|
115
|
-
const heartbeat = (async () => {
|
|
116
|
-
while (heartbeatRunning) {
|
|
117
|
-
await Bun.sleep(GATEWAY_LOCK_HEARTBEAT_MS);
|
|
118
|
-
if (!heartbeatRunning) break;
|
|
119
|
-
try {
|
|
120
|
-
const now = new Date();
|
|
121
|
-
await fs.promises.utimes(lockPath, now, now);
|
|
122
|
-
} catch {
|
|
123
|
-
// Ignore heartbeat errors
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
})();
|
|
127
|
-
try {
|
|
128
|
-
await writeLockInfo(lockPath);
|
|
129
|
-
return await handler();
|
|
130
|
-
} finally {
|
|
131
|
-
heartbeatRunning = false;
|
|
132
|
-
void heartbeat.catch(() => {}); // Don't await - let it die naturally
|
|
133
|
-
try {
|
|
134
|
-
await fd.close();
|
|
135
|
-
await fs.promises.unlink(lockPath);
|
|
136
|
-
} catch {
|
|
137
|
-
// Ignore lock cleanup errors
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} catch (err) {
|
|
141
|
-
const error = err as NodeJS.ErrnoException;
|
|
142
|
-
if (error.code === "EEXIST") {
|
|
143
|
-
let removedStale = false;
|
|
144
|
-
try {
|
|
145
|
-
const lockStat = await fs.promises.stat(lockPath);
|
|
146
|
-
const lockInfo = await readLockInfo(lockPath);
|
|
147
|
-
const lockPid = lockInfo?.pid;
|
|
148
|
-
const lockAgeMs = lockInfo?.startedAt ? Date.now() - lockInfo.startedAt : Date.now() - lockStat.mtimeMs;
|
|
149
|
-
const staleByTime = lockAgeMs > GATEWAY_LOCK_STALE_MS;
|
|
150
|
-
const staleByPid = lockPid !== undefined && !procmgr.isPidRunning(lockPid);
|
|
151
|
-
const staleByMissingPid = lockPid === undefined && staleByTime;
|
|
152
|
-
if (staleByPid || staleByMissingPid) {
|
|
153
|
-
await fs.promises.unlink(lockPath);
|
|
154
|
-
removedStale = true;
|
|
155
|
-
logger.warn("Removed stale shared gateway lock", { path: lockPath, pid: lockPid });
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
// Ignore stat errors; keep waiting
|
|
159
|
-
}
|
|
160
|
-
if (!removedStale) {
|
|
161
|
-
if (Date.now() - start > GATEWAY_LOCK_TIMEOUT_MS) {
|
|
162
|
-
throw new Error("Timed out waiting for shared gateway lock");
|
|
163
|
-
}
|
|
164
|
-
await Bun.sleep(GATEWAY_LOCK_RETRY_MS);
|
|
165
|
-
}
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
throw err;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function readGatewayInfo(): Promise<GatewayInfo | null> {
|
|
174
|
-
const infoPath = getGatewayInfoPath();
|
|
175
|
-
try {
|
|
176
|
-
const content = await Bun.file(infoPath).text();
|
|
177
|
-
const parsed = JSON.parse(content) as Partial<GatewayInfo>;
|
|
178
|
-
|
|
179
|
-
if (typeof parsed.url !== "string" || typeof parsed.pid !== "number" || typeof parsed.startedAt !== "number") {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
url: parsed.url,
|
|
184
|
-
pid: parsed.pid,
|
|
185
|
-
startedAt: parsed.startedAt,
|
|
186
|
-
pythonPath: typeof parsed.pythonPath === "string" ? parsed.pythonPath : undefined,
|
|
187
|
-
venvPath: typeof parsed.venvPath === "string" || parsed.venvPath === null ? parsed.venvPath : undefined,
|
|
188
|
-
};
|
|
189
|
-
} catch (err) {
|
|
190
|
-
if (isEnoent(err)) return null;
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function writeGatewayInfo(info: GatewayInfo): Promise<void> {
|
|
196
|
-
const infoPath = getGatewayInfoPath();
|
|
197
|
-
const tempPath = `${infoPath}.tmp`;
|
|
198
|
-
await Bun.write(tempPath, JSON.stringify(info, null, 2));
|
|
199
|
-
await fs.promises.rename(tempPath, infoPath);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function clearGatewayInfo(): Promise<void> {
|
|
203
|
-
const infoPath = getGatewayInfoPath();
|
|
204
|
-
try {
|
|
205
|
-
await fs.promises.unlink(infoPath);
|
|
206
|
-
} catch {
|
|
207
|
-
// Ignore errors on cleanup (file may not exist)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function isGatewayHealthy(url: string): Promise<boolean> {
|
|
212
|
-
try {
|
|
213
|
-
const response = await fetch(`${url}/api/kernelspecs`, {
|
|
214
|
-
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
|
|
215
|
-
});
|
|
216
|
-
return response.ok;
|
|
217
|
-
} catch {
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function isGatewayAlive(info: GatewayInfo): Promise<boolean> {
|
|
223
|
-
if (!procmgr.isPidRunning(info.pid)) return false;
|
|
224
|
-
return await isGatewayHealthy(info.url);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function startGatewayProcess(
|
|
228
|
-
cwd: string,
|
|
229
|
-
): Promise<{ url: string; pid: number; pythonPath: string; venvPath: string | null }> {
|
|
230
|
-
const settings = await Settings.init();
|
|
231
|
-
const { shell, env } = settings.getShellConfig();
|
|
232
|
-
const filteredEnv = filterEnv(env);
|
|
233
|
-
const runtime = resolvePythonRuntime(cwd, filteredEnv);
|
|
234
|
-
const snapshotPath = await getOrCreateSnapshot(shell, env).catch((err: unknown) => {
|
|
235
|
-
logger.warn("Failed to resolve shell snapshot for shared Python gateway", {
|
|
236
|
-
error: err instanceof Error ? err.message : String(err),
|
|
237
|
-
});
|
|
238
|
-
return null;
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const kernelEnv: Record<string, string | undefined> = {
|
|
242
|
-
...runtime.env,
|
|
243
|
-
PYTHONUNBUFFERED: "1",
|
|
244
|
-
PI_SHELL_SNAPSHOT: snapshotPath ?? undefined,
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const gatewayPort = await allocatePort();
|
|
248
|
-
const gatewayUrl = `http://127.0.0.1:${gatewayPort}`;
|
|
249
|
-
|
|
250
|
-
const gatewayProcess = Bun.spawn(
|
|
251
|
-
[
|
|
252
|
-
runtime.pythonPath,
|
|
253
|
-
"-m",
|
|
254
|
-
"kernel_gateway",
|
|
255
|
-
"--KernelGatewayApp.ip=127.0.0.1",
|
|
256
|
-
`--KernelGatewayApp.port=${gatewayPort}`,
|
|
257
|
-
"--KernelGatewayApp.port_retries=0",
|
|
258
|
-
"--KernelGatewayApp.allow_origin=*",
|
|
259
|
-
"--JupyterApp.answer_yes=true",
|
|
260
|
-
],
|
|
261
|
-
{
|
|
262
|
-
cwd,
|
|
263
|
-
stdin: "ignore",
|
|
264
|
-
stdout: "pipe",
|
|
265
|
-
stderr: "pipe",
|
|
266
|
-
windowsHide: true,
|
|
267
|
-
detached: true,
|
|
268
|
-
env: kernelEnv,
|
|
269
|
-
},
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
let exited = false;
|
|
273
|
-
gatewayProcess.exited
|
|
274
|
-
.catch(() => {})
|
|
275
|
-
.then(() => {
|
|
276
|
-
exited = true;
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const startTime = Date.now();
|
|
280
|
-
while (Date.now() - startTime < GATEWAY_STARTUP_TIMEOUT_MS) {
|
|
281
|
-
if (exited) {
|
|
282
|
-
throw new Error("Gateway process exited during startup");
|
|
283
|
-
}
|
|
284
|
-
if (await isGatewayHealthy(gatewayUrl)) {
|
|
285
|
-
localGatewayProcess = gatewayProcess;
|
|
286
|
-
localGatewayUrl = gatewayUrl;
|
|
287
|
-
return {
|
|
288
|
-
url: gatewayUrl,
|
|
289
|
-
pid: gatewayProcess.pid,
|
|
290
|
-
pythonPath: runtime.pythonPath,
|
|
291
|
-
venvPath: runtime.venvPath ?? null,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
await Bun.sleep(100);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
gatewayProcess.kill();
|
|
298
|
-
throw new Error("Gateway startup timeout");
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async function killGateway(pid: number, context: string): Promise<void> {
|
|
302
|
-
try {
|
|
303
|
-
await Process.fromPid(pid)?.terminate();
|
|
304
|
-
} catch (err) {
|
|
305
|
-
logger.warn("Failed to kill shared gateway process", {
|
|
306
|
-
error: err instanceof Error ? err.message : String(err),
|
|
307
|
-
pid,
|
|
308
|
-
context,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export async function acquireSharedGateway(cwd: string): Promise<AcquireResult | null> {
|
|
314
|
-
try {
|
|
315
|
-
return await withGatewayLock(async () => {
|
|
316
|
-
const existingInfo = await logger.time("acquireSharedGateway:readInfo", readGatewayInfo);
|
|
317
|
-
if (existingInfo) {
|
|
318
|
-
if (await logger.time("acquireSharedGateway:isAlive", isGatewayAlive, existingInfo)) {
|
|
319
|
-
localGatewayUrl = existingInfo.url;
|
|
320
|
-
isCoordinatorInitialized = true;
|
|
321
|
-
logger.debug("Reusing global Python gateway", { url: existingInfo.url });
|
|
322
|
-
return { url: existingInfo.url, isShared: true };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
logger.debug("Cleaning up stale gateway info", { pid: existingInfo.pid });
|
|
326
|
-
if (procmgr.isPidRunning(existingInfo.pid)) {
|
|
327
|
-
await killGateway(existingInfo.pid, "stale");
|
|
328
|
-
}
|
|
329
|
-
await clearGatewayInfo();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const { url, pid, pythonPath, venvPath } = await logger.time(
|
|
333
|
-
"acquireSharedGateway:startGateway",
|
|
334
|
-
startGatewayProcess,
|
|
335
|
-
cwd,
|
|
336
|
-
);
|
|
337
|
-
const info: GatewayInfo = {
|
|
338
|
-
url,
|
|
339
|
-
pid,
|
|
340
|
-
startedAt: Date.now(),
|
|
341
|
-
pythonPath,
|
|
342
|
-
venvPath,
|
|
343
|
-
};
|
|
344
|
-
await writeGatewayInfo(info);
|
|
345
|
-
isCoordinatorInitialized = true;
|
|
346
|
-
logger.debug("Started global Python gateway", { url, pid });
|
|
347
|
-
return { url, isShared: true };
|
|
348
|
-
});
|
|
349
|
-
} catch (err) {
|
|
350
|
-
logger.warn("Failed to acquire shared gateway, falling back to local", {
|
|
351
|
-
error: err instanceof Error ? err.message : String(err),
|
|
352
|
-
});
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export async function releaseSharedGateway(): Promise<void> {
|
|
358
|
-
if (!isCoordinatorInitialized) return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
export async function getSharedGatewayUrl(): Promise<string | null> {
|
|
362
|
-
if (localGatewayUrl) return localGatewayUrl;
|
|
363
|
-
return (await readGatewayInfo())?.url ?? null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export async function isSharedGatewayActive(): Promise<boolean> {
|
|
367
|
-
return (await getGatewayStatus()).active;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export interface GatewayStatus {
|
|
371
|
-
active: boolean;
|
|
372
|
-
url: string | null;
|
|
373
|
-
pid: number | null;
|
|
374
|
-
uptime: number | null;
|
|
375
|
-
pythonPath: string | null;
|
|
376
|
-
venvPath: string | null;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export async function getGatewayStatus(): Promise<GatewayStatus> {
|
|
380
|
-
const info = await readGatewayInfo();
|
|
381
|
-
if (!info) {
|
|
382
|
-
return {
|
|
383
|
-
active: false,
|
|
384
|
-
url: null,
|
|
385
|
-
pid: null,
|
|
386
|
-
uptime: null,
|
|
387
|
-
pythonPath: null,
|
|
388
|
-
venvPath: null,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
const active = procmgr.isPidRunning(info.pid);
|
|
392
|
-
return {
|
|
393
|
-
active,
|
|
394
|
-
url: info.url,
|
|
395
|
-
pid: info.pid,
|
|
396
|
-
uptime: active ? Date.now() - info.startedAt : null,
|
|
397
|
-
pythonPath: info.pythonPath ?? null,
|
|
398
|
-
venvPath: info.venvPath ?? null,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export async function shutdownSharedGateway(): Promise<void> {
|
|
403
|
-
try {
|
|
404
|
-
await withGatewayLock(async () => {
|
|
405
|
-
const info = await readGatewayInfo();
|
|
406
|
-
if (!info) return;
|
|
407
|
-
if (procmgr.isPidRunning(info.pid)) {
|
|
408
|
-
await killGateway(info.pid, "shutdown");
|
|
409
|
-
}
|
|
410
|
-
await clearGatewayInfo();
|
|
411
|
-
});
|
|
412
|
-
} catch (err) {
|
|
413
|
-
logger.warn("Failed to shutdown shared gateway", {
|
|
414
|
-
error: err instanceof Error ? err.message : String(err),
|
|
415
|
-
});
|
|
416
|
-
} finally {
|
|
417
|
-
if (localGatewayProcess) {
|
|
418
|
-
await killGateway(localGatewayProcess.pid, "shutdown-local");
|
|
419
|
-
}
|
|
420
|
-
localGatewayProcess = null;
|
|
421
|
-
localGatewayUrl = null;
|
|
422
|
-
isCoordinatorInitialized = false;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
File without changes
|
|
File without changes
|