pipane 0.1.5 → 0.1.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/README.md +45 -1
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/bin/pipane-rendezvous.js +21 -0
- package/bin/pipane.js +21 -1
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/__vite-browser-external-DtLbhLGx.js +1 -0
- package/dist/client/assets/app-runtime-Rw-O-AwW.js +1 -0
- package/dist/client/assets/backend-landing-page-B7SbyIXQ.js +1 -0
- package/dist/client/assets/index-DwbBcYUf.js +2 -0
- package/dist/client/assets/index-iblQYBAl.css +1 -0
- package/dist/client/assets/main-LGItV9Aj.js +2807 -0
- package/dist/client/assets/pairing-page-D-WxlWZR.js +1 -0
- package/dist/client/assets/remote-backend-manager-BWuGh30I.js +1 -0
- package/dist/client/assets/rendezvous-trust-api-C2Zob5i4.js +4 -0
- package/dist/client/assets/webrtc-frame-transport-BijNnA7N.js +1 -0
- package/dist/client/assets/ws-agent-adapter-BRysf-Y7.js +6 -0
- package/dist/client/index.html +2 -2
- package/dist/server/rendezvous/rendezvous-hub.js +426 -0
- package/dist/server/rendezvous/server.js +247 -0
- package/dist/server/rendezvous/trust-store.js +432 -0
- package/dist/server/server/auth-guard.js +61 -0
- package/dist/server/server/backend-connection-authorizer.js +29 -0
- package/dist/server/server/backend-identity.js +167 -0
- package/dist/server/server/backend-protocol-handler.js +132 -0
- package/dist/server/server/backend-trust-store.js +157 -0
- package/dist/server/server/backend-webrtc.js +239 -0
- package/dist/server/server/bundled-extensions.js +6 -0
- package/dist/server/server/conversation-file-access.js +97 -0
- package/dist/server/server/extension-status.js +31 -0
- package/dist/server/server/frame-connection.js +48 -0
- package/dist/server/server/frame-router.js +45 -0
- package/dist/server/server/ice-servers.js +24 -0
- package/dist/server/server/local-backend-api.js +389 -0
- package/dist/server/server/local-settings.js +28 -0
- package/dist/server/server/pi-rpc-protocol.js +407 -0
- package/dist/server/server/pi-runtime.js +1 -1
- package/dist/server/server/process-pool.js +179 -136
- package/dist/server/server/rendezvous-client.js +282 -0
- package/dist/server/server/rest-api.js +121 -231
- package/dist/server/server/server.js +188 -134
- package/dist/server/server/session-actor.js +202 -0
- package/dist/server/server/session-index.js +111 -18
- package/dist/server/server/session-jsonl.js +128 -8
- package/dist/server/server/session-path.js +145 -0
- package/dist/server/server/session-registry.js +67 -0
- package/dist/server/server/update-api.js +28 -0
- package/dist/server/server/update-check.js +33 -13
- package/dist/server/server/update-manager.js +231 -0
- package/dist/server/server/worktree-name.js +124 -0
- package/dist/server/server/ws-handler.js +784 -478
- package/dist/server/shared/backend-api.js +1 -0
- package/dist/server/shared/backend-protocol.js +164 -0
- package/dist/server/shared/jsonl-sync.js +41 -0
- package/dist/server/shared/node-trust-crypto.js +61 -0
- package/dist/server/shared/rendezvous-protocol.js +243 -0
- package/dist/server/shared/rpc-timeouts.js +4 -0
- package/dist/server/shared/thinking-levels.js +94 -0
- package/dist/server/shared/tool-runtime.js +1 -0
- package/dist/server/shared/trust-protocol.js +97 -0
- package/dist/server/shared/updates.js +4 -0
- package/dist/server/shared/ws-protocol.js +473 -0
- package/docs/protocol.md +127 -0
- package/extensions/canvas.ts +1 -1
- package/package.json +42 -17
- package/dist/client/assets/_node-stub_node_crypto-C_7epg3G.js +0 -1
- package/dist/client/assets/_node-stub_node_fs-B-VOzeXW.js +0 -1
- package/dist/client/assets/_node-stub_node_http-CROXaVGJ.js +0 -1
- package/dist/client/assets/_node-stub_node_os-TTKJha15.js +0 -1
- package/dist/client/assets/_node-stub_node_path-DJCJiKv7.js +0 -1
- package/dist/client/assets/_node-stub_stream-DDexIdn_.js +0 -1
- package/dist/client/assets/index-BtDAXAo1.css +0 -1
- package/dist/client/assets/index-CAQqlK61.js +0 -131
- package/dist/client/assets/index-CJEeBmBj.js +0 -3
- package/dist/client/assets/index-DUEhKmPQ.js +0 -1
- package/dist/client/assets/index-XEVKscP-.js +0 -4443
- package/dist/client/assets/index-woI4agCy.js +0 -45
- package/dist/server/server/attached-session.js +0 -209
- package/dist/server/server/load-trace-store.js +0 -48
- package/dist/server/server/session-lifecycle.js +0 -155
- package/patches/@mariozechner+pi-web-ui+0.55.3.patch +0 -3437
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CWD-aware process pool for pi RPC processes.
|
|
2
|
+
* CWD-aware, leased process pool for pi RPC processes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* operate in the correct project directory.
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* - Lazy spawning per-cwd with configurable pre-warming
|
|
11
|
-
* - Readiness check via get_state RPC (replaces setTimeout(500) hack)
|
|
12
|
-
* - Dead process cleanup on exit
|
|
13
|
-
* - Global process cap
|
|
4
|
+
* The pool is the sole owner of process availability. Callers receive an
|
|
5
|
+
* idempotent lease instead of passing a separate busy set, so a process is
|
|
6
|
+
* reserved atomically before control returns to asynchronous application code.
|
|
14
7
|
*/
|
|
15
8
|
import { spawn } from "node:child_process";
|
|
16
|
-
import * as readline from "node:readline";
|
|
17
9
|
import { existsSync } from "node:fs";
|
|
10
|
+
import { attachStrictJsonlReader, decodePiRpcLine, encodePiRpcCommand, } from "./pi-rpc-protocol.js";
|
|
11
|
+
/** Exclusive ownership of one pooled process. */
|
|
12
|
+
export class RpcProcessLease {
|
|
13
|
+
process;
|
|
14
|
+
pool;
|
|
15
|
+
_released = false;
|
|
16
|
+
constructor(pool, process) {
|
|
17
|
+
this.pool = pool;
|
|
18
|
+
this.process = process;
|
|
19
|
+
}
|
|
20
|
+
get released() { return this._released; }
|
|
21
|
+
release() {
|
|
22
|
+
this.pool.releaseLease(this);
|
|
23
|
+
}
|
|
24
|
+
/** @internal */
|
|
25
|
+
markReleased() {
|
|
26
|
+
this._released = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
18
29
|
export class ProcessPool {
|
|
19
|
-
/** All live processes, keyed by cwd */
|
|
20
30
|
pools = new Map();
|
|
31
|
+
leases = new Map();
|
|
32
|
+
decommissioning = new Set();
|
|
33
|
+
finalizedProcesses = new WeakSet();
|
|
21
34
|
nextProcId = 0;
|
|
22
35
|
spawnConfig;
|
|
23
36
|
maxProcesses;
|
|
24
37
|
prewarmCount;
|
|
25
38
|
rpcTimeout;
|
|
26
39
|
onProcessExit;
|
|
40
|
+
eventListeners = new Set();
|
|
27
41
|
constructor(spawnConfig, options) {
|
|
28
42
|
this.spawnConfig = spawnConfig;
|
|
29
43
|
this.maxProcesses = options?.maxProcesses ?? 6;
|
|
@@ -31,40 +45,50 @@ export class ProcessPool {
|
|
|
31
45
|
this.rpcTimeout = options?.rpcTimeout ?? 30000;
|
|
32
46
|
this.onProcessExit = options?.onProcessExit;
|
|
33
47
|
}
|
|
34
|
-
/** Get the total count of live processes across all cwds */
|
|
35
48
|
get totalProcesses() {
|
|
36
49
|
let count = 0;
|
|
37
50
|
for (const procs of this.pools.values()) {
|
|
38
|
-
count += procs.filter((
|
|
51
|
+
count += procs.filter((proc) => proc.process.exitCode === null).length;
|
|
39
52
|
}
|
|
40
53
|
return count;
|
|
41
54
|
}
|
|
42
|
-
|
|
55
|
+
subscribeEvents(listener) {
|
|
56
|
+
this.eventListeners.add(listener);
|
|
57
|
+
return () => this.eventListeners.delete(listener);
|
|
58
|
+
}
|
|
59
|
+
emitEvent(proc, event) {
|
|
60
|
+
for (const listener of this.eventListeners) {
|
|
61
|
+
try {
|
|
62
|
+
listener(proc, event);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error(`[pool] pi#${proc.id} event listener failed:`, err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
43
69
|
getAllProcesses() {
|
|
44
70
|
const all = [];
|
|
45
|
-
for (const procs of this.pools.values())
|
|
71
|
+
for (const procs of this.pools.values())
|
|
46
72
|
all.push(...procs);
|
|
47
|
-
}
|
|
48
73
|
return all;
|
|
49
74
|
}
|
|
50
|
-
|
|
75
|
+
isLeased(proc) {
|
|
76
|
+
return this.leases.has(proc);
|
|
77
|
+
}
|
|
78
|
+
isDecommissioning(proc) {
|
|
79
|
+
return this.decommissioning.has(proc);
|
|
80
|
+
}
|
|
51
81
|
getRecentStderr(proc, maxLines = 12) {
|
|
52
82
|
if (!proc?.recentStderr?.length)
|
|
53
83
|
return [];
|
|
54
84
|
return proc.recentStderr.slice(Math.max(0, proc.recentStderr.length - maxLines));
|
|
55
85
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Spawn a new RPC process for the given cwd.
|
|
58
|
-
* Does not wait for readiness — call waitForReady() separately.
|
|
59
|
-
*/
|
|
60
86
|
spawn(cwd) {
|
|
61
87
|
if (!existsSync(cwd)) {
|
|
62
88
|
throw new Error(`Cannot spawn pi process: directory does not exist: ${cwd}`);
|
|
63
89
|
}
|
|
64
90
|
const procId = ++this.nextProcId;
|
|
65
91
|
console.log(`[pool] Spawning pi process #${procId} (cwd: ${cwd})...`);
|
|
66
|
-
// Strip NODE_ENV from the child environment so tools spawned by pi
|
|
67
|
-
// (e.g. bash) don't inherit pipane's "production" setting.
|
|
68
92
|
const { NODE_ENV: _, ...parentEnv } = process.env;
|
|
69
93
|
const baseArgs = typeof this.spawnConfig.baseArgs === "function"
|
|
70
94
|
? this.spawnConfig.baseArgs()
|
|
@@ -90,71 +114,46 @@ export class ProcessPool {
|
|
|
90
114
|
recentStderr.splice(0, recentStderr.length - 60);
|
|
91
115
|
}
|
|
92
116
|
});
|
|
93
|
-
const rl = readline.createInterface({ input: child.stdout, terminal: false });
|
|
94
117
|
const proc = {
|
|
95
118
|
id: procId,
|
|
96
119
|
cwd,
|
|
97
120
|
process: child,
|
|
98
|
-
|
|
121
|
+
stopReadingStdout: () => { },
|
|
99
122
|
pendingRequests: new Map(),
|
|
100
123
|
requestId: 0,
|
|
101
124
|
lastResponseTime: Date.now(),
|
|
102
125
|
recentStderr,
|
|
103
126
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
data = JSON.parse(line);
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
127
|
+
proc.stopReadingStdout = attachStrictJsonlReader(child.stdout, (line) => {
|
|
128
|
+
const decoded = decodePiRpcLine(line);
|
|
129
|
+
if (!decoded.ok) {
|
|
130
|
+
console.warn(`[pool] pi#${proc.id} ignored invalid RPC output: ${decoded.error.message}`);
|
|
111
131
|
return;
|
|
112
132
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
proc.pendingRequests.
|
|
133
|
+
const message = decoded.value;
|
|
134
|
+
if (message.type === "response") {
|
|
135
|
+
const pending = message.id ? proc.pendingRequests.get(message.id) : undefined;
|
|
136
|
+
if (!pending || !message.id)
|
|
137
|
+
return;
|
|
138
|
+
proc.pendingRequests.delete(message.id);
|
|
116
139
|
proc.lastResponseTime = Date.now();
|
|
117
|
-
pending.
|
|
140
|
+
if (message.command !== pending.command) {
|
|
141
|
+
pending.reject(new Error(`Mismatched RPC response for ${pending.command}: received ${message.command}`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
pending.resolve(message);
|
|
145
|
+
return;
|
|
118
146
|
}
|
|
147
|
+
this.emitEvent(proc, message);
|
|
119
148
|
});
|
|
120
149
|
child.on("error", (err) => {
|
|
121
150
|
console.error(`[pool] pi#${proc.id} spawn error: ${err.message}`);
|
|
122
|
-
|
|
123
|
-
const poolForCwd = this.pools.get(cwd);
|
|
124
|
-
if (poolForCwd) {
|
|
125
|
-
const idx = poolForCwd.indexOf(proc);
|
|
126
|
-
if (idx !== -1)
|
|
127
|
-
poolForCwd.splice(idx, 1);
|
|
128
|
-
if (poolForCwd.length === 0)
|
|
129
|
-
this.pools.delete(cwd);
|
|
130
|
-
}
|
|
131
|
-
// Reject any pending requests
|
|
132
|
-
for (const [, pending] of proc.pendingRequests) {
|
|
133
|
-
pending.reject(new Error(`pi process #${proc.id} failed to spawn: ${err.message}`));
|
|
134
|
-
}
|
|
135
|
-
proc.pendingRequests.clear();
|
|
136
|
-
this.onProcessExit?.(proc);
|
|
151
|
+
this.finalizeProcess(proc, new Error(`pi process #${proc.id} failed to spawn: ${err.message}`));
|
|
137
152
|
});
|
|
138
153
|
child.on("exit", (code) => {
|
|
139
154
|
console.log(`[pool] pi#${proc.id} exited (code ${code})`);
|
|
140
|
-
|
|
141
|
-
const poolForCwd = this.pools.get(cwd);
|
|
142
|
-
if (poolForCwd) {
|
|
143
|
-
const idx = poolForCwd.indexOf(proc);
|
|
144
|
-
if (idx !== -1)
|
|
145
|
-
poolForCwd.splice(idx, 1);
|
|
146
|
-
if (poolForCwd.length === 0)
|
|
147
|
-
this.pools.delete(cwd);
|
|
148
|
-
}
|
|
149
|
-
// Reject any pending requests
|
|
150
|
-
for (const [, pending] of proc.pendingRequests) {
|
|
151
|
-
pending.reject(new Error(`pi process #${proc.id} exited unexpectedly (code ${code})`));
|
|
152
|
-
}
|
|
153
|
-
proc.pendingRequests.clear();
|
|
154
|
-
// Notify caller
|
|
155
|
-
this.onProcessExit?.(proc);
|
|
155
|
+
this.finalizeProcess(proc, new Error(`pi process #${proc.id} exited unexpectedly (code ${code})`));
|
|
156
156
|
});
|
|
157
|
-
// Add to pool
|
|
158
157
|
let poolForCwd = this.pools.get(cwd);
|
|
159
158
|
if (!poolForCwd) {
|
|
160
159
|
poolForCwd = [];
|
|
@@ -164,10 +163,29 @@ export class ProcessPool {
|
|
|
164
163
|
console.log(`[pool] pi#${procId} spawned (total: ${this.totalProcesses})`);
|
|
165
164
|
return proc;
|
|
166
165
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
finalizeProcess(proc, error) {
|
|
167
|
+
if (this.finalizedProcesses.has(proc))
|
|
168
|
+
return;
|
|
169
|
+
this.finalizedProcesses.add(proc);
|
|
170
|
+
const poolForCwd = this.pools.get(proc.cwd);
|
|
171
|
+
if (poolForCwd) {
|
|
172
|
+
const index = poolForCwd.indexOf(proc);
|
|
173
|
+
if (index !== -1)
|
|
174
|
+
poolForCwd.splice(index, 1);
|
|
175
|
+
if (poolForCwd.length === 0)
|
|
176
|
+
this.pools.delete(proc.cwd);
|
|
177
|
+
}
|
|
178
|
+
const lease = this.leases.get(proc);
|
|
179
|
+
if (lease)
|
|
180
|
+
lease.markReleased();
|
|
181
|
+
this.leases.delete(proc);
|
|
182
|
+
this.decommissioning.delete(proc);
|
|
183
|
+
proc.stopReadingStdout();
|
|
184
|
+
for (const pending of proc.pendingRequests.values())
|
|
185
|
+
pending.reject(error);
|
|
186
|
+
proc.pendingRequests.clear();
|
|
187
|
+
this.onProcessExit?.(proc);
|
|
188
|
+
}
|
|
171
189
|
async waitForReady(proc, timeoutMs = 5000) {
|
|
172
190
|
try {
|
|
173
191
|
await this.sendRpc(proc, { type: "get_state" }, timeoutMs);
|
|
@@ -177,116 +195,141 @@ export class ProcessPool {
|
|
|
177
195
|
return false;
|
|
178
196
|
}
|
|
179
197
|
}
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
* An "idle" process is one that's alive and not in the busySet.
|
|
183
|
-
*/
|
|
184
|
-
acquire(cwd, busySet) {
|
|
198
|
+
/** Atomically reserve a matching process, spawning one when capacity allows. */
|
|
199
|
+
acquire(cwd) {
|
|
185
200
|
const poolForCwd = this.pools.get(cwd);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
// Check global cap
|
|
192
|
-
if (this.totalProcesses >= this.maxProcesses) {
|
|
201
|
+
const idle = poolForCwd?.find((proc) => this.isAvailable(proc));
|
|
202
|
+
if (idle)
|
|
203
|
+
return this.reserve(idle);
|
|
204
|
+
if (this.totalProcesses >= this.maxProcesses)
|
|
193
205
|
return null;
|
|
194
|
-
|
|
195
|
-
return this.spawn(cwd);
|
|
206
|
+
return this.reserve(this.spawn(cwd));
|
|
196
207
|
}
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
* need a specific cwd.
|
|
200
|
-
*/
|
|
201
|
-
getAny(busySet) {
|
|
202
|
-
// Prefer idle
|
|
208
|
+
/** Reserve any idle live process, regardless of cwd. */
|
|
209
|
+
acquireAny() {
|
|
203
210
|
for (const procs of this.pools.values()) {
|
|
204
|
-
const idle = procs.find((
|
|
211
|
+
const idle = procs.find((proc) => this.isAvailable(proc));
|
|
205
212
|
if (idle)
|
|
206
|
-
return idle;
|
|
207
|
-
}
|
|
208
|
-
// Fall back to any live process
|
|
209
|
-
for (const procs of this.pools.values()) {
|
|
210
|
-
const live = procs.find((p) => p.process.exitCode === null);
|
|
211
|
-
if (live)
|
|
212
|
-
return live;
|
|
213
|
+
return this.reserve(idle);
|
|
213
214
|
}
|
|
214
215
|
return null;
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
isAvailable(proc) {
|
|
218
|
+
return proc.process.exitCode === null
|
|
219
|
+
&& !this.leases.has(proc)
|
|
220
|
+
&& !this.decommissioning.has(proc);
|
|
221
|
+
}
|
|
222
|
+
reserve(proc) {
|
|
223
|
+
if (!this.isAvailable(proc))
|
|
224
|
+
throw new Error(`pi process #${proc.id} is not available`);
|
|
225
|
+
const lease = new RpcProcessLease(this, proc);
|
|
226
|
+
this.leases.set(proc, lease);
|
|
227
|
+
return lease;
|
|
228
|
+
}
|
|
229
|
+
/** @internal Called only by RpcProcessLease.release(). */
|
|
230
|
+
releaseLease(lease) {
|
|
231
|
+
if (lease.released)
|
|
232
|
+
return;
|
|
233
|
+
const proc = lease.process;
|
|
234
|
+
if (this.leases.get(proc) !== lease) {
|
|
235
|
+
lease.markReleased();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.leases.delete(proc);
|
|
239
|
+
lease.markReleased();
|
|
240
|
+
if (this.decommissioning.has(proc) && proc.process.exitCode === null && !proc.process.killed) {
|
|
241
|
+
// Keep the marker until the exit event removes the process. Otherwise a
|
|
242
|
+
// concurrent acquisition could lease the dying process before SIGTERM lands.
|
|
243
|
+
console.log(`[pool] Decommissioning pi#${proc.id} after completed operation`);
|
|
244
|
+
proc.process.kill("SIGTERM");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
decommission(proc) {
|
|
248
|
+
if (proc.process.exitCode !== null || this.decommissioning.has(proc))
|
|
249
|
+
return "ignored";
|
|
250
|
+
this.decommissioning.add(proc);
|
|
251
|
+
if (this.leases.has(proc))
|
|
252
|
+
return "draining";
|
|
253
|
+
proc.process.kill("SIGTERM");
|
|
254
|
+
return "killed";
|
|
255
|
+
}
|
|
256
|
+
/** Evict an idle process from a different cwd to free capacity. */
|
|
257
|
+
evictIdleDifferentCwd(targetCwd) {
|
|
221
258
|
for (const [cwd, procs] of this.pools) {
|
|
222
259
|
if (cwd === targetCwd)
|
|
223
260
|
continue;
|
|
224
|
-
const victim = procs.find((
|
|
261
|
+
const victim = procs.find((proc) => this.isAvailable(proc));
|
|
225
262
|
if (!victim)
|
|
226
263
|
continue;
|
|
227
264
|
console.log(`[pool] Evicting idle pi#${victim.id} from cwd ${cwd} to make room for ${targetCwd}`);
|
|
228
|
-
|
|
265
|
+
this.decommission(victim);
|
|
229
266
|
return victim;
|
|
230
267
|
}
|
|
231
268
|
return null;
|
|
232
269
|
}
|
|
233
|
-
/**
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
270
|
+
/** Immediately kill one process while keeping it unavailable until exit. */
|
|
271
|
+
forceKill(proc) {
|
|
272
|
+
if (proc.process.exitCode !== null)
|
|
273
|
+
return false;
|
|
274
|
+
this.decommissioning.add(proc);
|
|
275
|
+
return proc.process.kill("SIGKILL");
|
|
276
|
+
}
|
|
277
|
+
/** Kill idle processes now and mark leased processes for kill on release. */
|
|
278
|
+
decommissionAll() {
|
|
279
|
+
let killed = 0;
|
|
280
|
+
let draining = 0;
|
|
281
|
+
for (const proc of this.getAllProcesses()) {
|
|
282
|
+
const result = this.decommission(proc);
|
|
283
|
+
if (result === "killed")
|
|
284
|
+
killed += 1;
|
|
285
|
+
if (result === "draining")
|
|
286
|
+
draining += 1;
|
|
287
|
+
}
|
|
288
|
+
return { killed, draining };
|
|
289
|
+
}
|
|
242
290
|
async prewarm(cwd) {
|
|
243
|
-
const existing = this.pools.get(cwd)?.filter((
|
|
291
|
+
const existing = this.pools.get(cwd)?.filter((proc) => proc.process.exitCode === null).length ?? 0;
|
|
244
292
|
const needed = Math.min(this.prewarmCount, this.maxProcesses) - existing;
|
|
245
293
|
for (let i = 0; i < needed; i++) {
|
|
246
294
|
if (this.totalProcesses >= this.maxProcesses)
|
|
247
295
|
break;
|
|
248
296
|
const proc = this.spawn(cwd);
|
|
249
|
-
if (i < needed - 1)
|
|
250
|
-
// Wait for this process to be ready before spawning the next one
|
|
297
|
+
if (i < needed - 1)
|
|
251
298
|
await this.waitForReady(proc);
|
|
252
|
-
}
|
|
253
299
|
}
|
|
254
300
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Send an RPC command to a process and wait for a response.
|
|
257
|
-
*/
|
|
258
301
|
sendRpc(proc, command, timeoutMs) {
|
|
259
302
|
if (!proc.process || proc.process.exitCode !== null) {
|
|
260
303
|
return Promise.reject(new Error("RPC process is dead"));
|
|
261
304
|
}
|
|
262
305
|
const timeout = timeoutMs ?? this.rpcTimeout;
|
|
263
306
|
const id = `req_${++proc.requestId}`;
|
|
264
|
-
const fullCommand = { ...command, id };
|
|
265
307
|
return new Promise((resolve, reject) => {
|
|
266
308
|
const timer = setTimeout(() => {
|
|
267
309
|
proc.pendingRequests.delete(id);
|
|
310
|
+
// A timed-out RPC may still be executing. Never return that process to
|
|
311
|
+
// the idle pool where a new session command could overlap with it.
|
|
312
|
+
this.decommission(proc);
|
|
268
313
|
reject(new Error(`Timeout waiting for RPC response to ${command.type}`));
|
|
269
314
|
}, timeout);
|
|
270
315
|
proc.pendingRequests.set(id, {
|
|
271
|
-
|
|
316
|
+
command: command.type,
|
|
317
|
+
resolve: (response) => {
|
|
272
318
|
clearTimeout(timer);
|
|
273
|
-
resolve(
|
|
319
|
+
resolve(response);
|
|
274
320
|
},
|
|
275
|
-
reject: (
|
|
321
|
+
reject: (error) => {
|
|
276
322
|
clearTimeout(timer);
|
|
277
|
-
reject(
|
|
323
|
+
reject(error);
|
|
278
324
|
},
|
|
279
325
|
});
|
|
280
|
-
proc.process.stdin.write(
|
|
326
|
+
proc.process.stdin.write(encodePiRpcCommand(command, id));
|
|
281
327
|
});
|
|
282
328
|
}
|
|
283
|
-
/**
|
|
284
|
-
* Send an RPC command and throw if it fails.
|
|
285
|
-
*/
|
|
286
329
|
async sendRpcChecked(proc, command) {
|
|
287
330
|
const response = await this.sendRpc(proc, command);
|
|
288
|
-
if (!response
|
|
289
|
-
throw new Error(response
|
|
331
|
+
if (!response.success) {
|
|
332
|
+
throw new Error(response.error || `RPC command failed: ${command.type}`);
|
|
290
333
|
}
|
|
291
334
|
return response;
|
|
292
335
|
}
|