pi-orchestrator-config 4.2.0 → 4.3.2
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 +76 -67
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +42 -3
- package/agents/github-expert.md +11 -0
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +563 -102
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +15 -9
- package/extensions/orchestrator/dreaming.ts +32 -25
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +80 -27
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +78 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +57 -33
- package/extensions/orchestrator/project-settings.ts +333 -127
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +63 -15
- package/extensions/orchestrator/rule-placeholders.ts +411 -2
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +157 -59
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +96 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +9 -3
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +12 -1
- package/rules/20-code-review-loop.md +52 -25
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +563 -0
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- package/extensions/coms/coms-net.ts +0 -1589
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
|
+
import * as net from "node:net";
|
|
12
13
|
import * as path from "node:path";
|
|
13
14
|
import { createRequire } from "node:module";
|
|
14
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import { hyperlink } from "@earendil-works/pi-tui";
|
|
16
17
|
import {
|
|
17
|
-
createLogger,
|
|
18
18
|
checkHealth,
|
|
19
19
|
ensureUiBuilt,
|
|
20
20
|
spawnDaemon,
|
|
@@ -25,16 +25,21 @@ import {
|
|
|
25
25
|
readLockfile,
|
|
26
26
|
removeLockfile,
|
|
27
27
|
} from "../shared/daemon-manager.js";
|
|
28
|
+
import { createLogger } from "../shared/logger.js";
|
|
28
29
|
import { setupHeartbeat, setupReconnectPoller } from "../shared/ws-client.js";
|
|
29
30
|
import { getSetting } from "../orchestrator/project-settings.js";
|
|
31
|
+
import { shouldSkipOneshotRegister } from "../shared/oneshot.js";
|
|
32
|
+
import { evaluateSpawnLock } from "./spawn-lock.js";
|
|
33
|
+
|
|
34
|
+
/** Re-export for callers/tests that import from pidiff. */
|
|
35
|
+
export { evaluateSpawnLock } from "./spawn-lock.js";
|
|
30
36
|
|
|
31
37
|
const RECONNECT_INTERVAL_MS = 5000;
|
|
32
38
|
const ICON_DIFF = "";
|
|
33
39
|
|
|
34
|
-
const log = createLogger(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
40
|
+
const log = createLogger("pidiff");
|
|
41
|
+
/** String logger for daemon-manager / ws-client APIs that take `(msg: string) => void`. */
|
|
42
|
+
const debugLog = (msg: string) => log.debug(msg);
|
|
38
43
|
|
|
39
44
|
function isGitRepo(cwd: string): boolean {
|
|
40
45
|
try { execFileSync("git", ["rev-parse", "--git-dir"], { cwd, stdio: "ignore" }); return true; } catch { return false; }
|
|
@@ -48,8 +53,35 @@ function getBranch(cwd: string): string {
|
|
|
48
53
|
} catch { return ""; }
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
/** Return true if port can be bound on 127.0.0.1. */
|
|
57
|
+
function isPortFree(port: number): Promise<boolean> {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const srv = net.createServer();
|
|
60
|
+
srv.once("error", () => { try { srv.close(); } catch {} resolve(false); });
|
|
61
|
+
srv.listen(port, "127.0.0.1", () => { srv.close(() => resolve(true)); });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Reuse preferred port if free; otherwise allocate a new one. */
|
|
66
|
+
async function resolveSpawnPort(preferred: number | null | undefined): Promise<number> {
|
|
67
|
+
if (!preferred) {
|
|
68
|
+
const port = await findFreePort();
|
|
69
|
+
log.debug(`allocated new port: ${port}`);
|
|
70
|
+
return port;
|
|
71
|
+
}
|
|
72
|
+
if (await isPortFree(preferred)) {
|
|
73
|
+
log.debug(`reusing lockfile port: ${preferred}`);
|
|
74
|
+
return preferred;
|
|
75
|
+
}
|
|
76
|
+
log.debug(`lockfile port ${preferred} in use, allocating new port`);
|
|
77
|
+
const port = await findFreePort();
|
|
78
|
+
log.debug(`allocated new port: ${port}`);
|
|
79
|
+
return port;
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
export function registerPidiff(pi: ExtensionAPI): void {
|
|
52
83
|
if (process.env.PI_SUBAGENT_CHILD === "1") return;
|
|
84
|
+
if (shouldSkipOneshotRegister(log)) return;
|
|
53
85
|
|
|
54
86
|
const projectCwd = process.cwd();
|
|
55
87
|
const pidiffDisabled = !getSetting(projectCwd, "pidiff_enable");
|
|
@@ -96,21 +128,21 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
96
128
|
}
|
|
97
129
|
|
|
98
130
|
function doSpawn(port: number, cwd: string): void {
|
|
99
|
-
ensureUiBuilt(import.meta.url, "pidiff-ui",
|
|
131
|
+
ensureUiBuilt(import.meta.url, "pidiff-ui", debugLog);
|
|
100
132
|
spawnDaemon({
|
|
101
133
|
serverScript: "pidiff-server.ts",
|
|
102
134
|
logFile: path.join(process.env.HOME || "/tmp", ".pi", "pidiff-server.log"),
|
|
103
135
|
env: { PI_PIDIFF_PORT: String(port), PI_PIDIFF_CWD: cwd },
|
|
104
|
-
log,
|
|
136
|
+
log: debugLog,
|
|
105
137
|
});
|
|
106
138
|
}
|
|
107
139
|
|
|
108
140
|
async function connect(ctx: any) {
|
|
109
|
-
log(`connect() called, connected=${connected}, connecting=${connecting}`);
|
|
141
|
+
log.debug(`connect() called, connected=${connected}, connecting=${connecting}`);
|
|
110
142
|
if (connected || connecting || shuttingDown) return;
|
|
111
143
|
connecting = true;
|
|
112
144
|
lastCtx = ctx;
|
|
113
|
-
if (!lockDir) { log("connect: lockDir not set (session_start not fired yet)"); connecting = false; return; }
|
|
145
|
+
if (!lockDir) { log.debug("connect: lockDir not set (session_start not fired yet)"); connecting = false; return; }
|
|
114
146
|
|
|
115
147
|
// Try to discover port from lockfile
|
|
116
148
|
let port = activePort;
|
|
@@ -123,12 +155,12 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
123
155
|
let running = false;
|
|
124
156
|
if (port) {
|
|
125
157
|
running = await checkHealth(port);
|
|
126
|
-
log(`lockfile port=${port}, healthy=${running}`);
|
|
158
|
+
log.debug(`lockfile port=${port}, healthy=${running}`);
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
if (!running) {
|
|
130
162
|
if (spawning) {
|
|
131
|
-
log("daemon already spawning, waiting...");
|
|
163
|
+
log.debug("daemon already spawning, waiting...");
|
|
132
164
|
connecting = false;
|
|
133
165
|
return;
|
|
134
166
|
}
|
|
@@ -138,8 +170,17 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
138
170
|
try {
|
|
139
171
|
fs.writeFileSync(spawnLock, String(process.pid), { flag: "wx" });
|
|
140
172
|
} catch {
|
|
141
|
-
// Another session is already spawning —
|
|
142
|
-
|
|
173
|
+
// Another session is already spawning — check if spawner is still alive
|
|
174
|
+
const lockDecision = evaluateSpawnLock(spawnLock, getSetting(ctx.cwd, "pidiff_stale_lock_timeout_ms"));
|
|
175
|
+
if (lockDecision.action === "recover" || lockDecision.action === "recover_pid_reuse") {
|
|
176
|
+
log.info("stale_spawn_lock", lockDecision.reason);
|
|
177
|
+
try { fs.unlinkSync(spawnLock); } catch {}
|
|
178
|
+
connecting = false;
|
|
179
|
+
setTimeout(() => connect(ctx), 500);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Spawner is alive — wait for it
|
|
183
|
+
log.debug("another session is spawning pidiff, waiting for lockfile...");
|
|
143
184
|
for (let i = 0; i < 30; i++) {
|
|
144
185
|
await new Promise(r => setTimeout(r, 1000));
|
|
145
186
|
const lock = readLockfile(lockDir);
|
|
@@ -156,12 +197,12 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
156
197
|
|
|
157
198
|
spawning = true;
|
|
158
199
|
try {
|
|
159
|
-
log("spawning daemon...");
|
|
160
|
-
port
|
|
161
|
-
|
|
200
|
+
log.debug("spawning daemon...");
|
|
201
|
+
// Reuse lockfile port if available — keeps URL stable across reload/restart
|
|
202
|
+
port = await resolveSpawnPort(port);
|
|
162
203
|
doSpawn(port, ctx.cwd);
|
|
163
|
-
writeLockfile(lockDir, port, null,
|
|
164
|
-
const ready = await waitForDaemon(port,
|
|
204
|
+
writeLockfile(lockDir, port, null, debugLog);
|
|
205
|
+
const ready = await waitForDaemon(port, getSetting(process.cwd(), "pidiff_daemon_startup_timeout_s"), debugLog);
|
|
165
206
|
if (!ready) { connecting = false; return; }
|
|
166
207
|
} finally {
|
|
167
208
|
spawning = false;
|
|
@@ -174,11 +215,11 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
174
215
|
try {
|
|
175
216
|
const _require = createRequire(import.meta.url);
|
|
176
217
|
const WebSocket = _require("ws");
|
|
177
|
-
log(`connecting WebSocket to port ${activePort}...`);
|
|
218
|
+
log.debug(`connecting WebSocket to port ${activePort}...`);
|
|
178
219
|
const wsClient = new WebSocket(`ws://127.0.0.1:${activePort}/ws/pi`);
|
|
179
220
|
|
|
180
221
|
wsClient.on("open", () => {
|
|
181
|
-
log("WebSocket connected");
|
|
222
|
+
log.debug("WebSocket connected");
|
|
182
223
|
ws = wsClient;
|
|
183
224
|
connected = true;
|
|
184
225
|
connecting = false;
|
|
@@ -197,14 +238,14 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
197
238
|
}));
|
|
198
239
|
} catch {
|
|
199
240
|
// ctx may be stale if session was replaced during WebSocket connect
|
|
200
|
-
log("skipped registration — ctx is stale");
|
|
241
|
+
log.debug("skipped registration — ctx is stale");
|
|
201
242
|
}
|
|
202
243
|
|
|
203
244
|
// Keepalive + dead connection detection
|
|
204
245
|
if (cleanupHeartbeat) cleanupHeartbeat();
|
|
205
246
|
cleanupHeartbeat = setupHeartbeat({
|
|
206
247
|
ws: wsClient,
|
|
207
|
-
log,
|
|
248
|
+
log: debugLog,
|
|
208
249
|
onDead: () => {
|
|
209
250
|
connected = false; ws = null;
|
|
210
251
|
if (!shuttingDown && lastCtx) setTimeout(() => connect(lastCtx), 1000);
|
|
@@ -216,7 +257,7 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
216
257
|
try {
|
|
217
258
|
const parsed = JSON.parse(data.toString());
|
|
218
259
|
if (parsed.type === "publish-review" && parsed.comments?.length > 0) {
|
|
219
|
-
log(`review received: ${parsed.comments.length} comments`);
|
|
260
|
+
log.debug(`review received: ${parsed.comments.length} comments`);
|
|
220
261
|
const review = {
|
|
221
262
|
source: "pidiff",
|
|
222
263
|
type: "code-review",
|
|
@@ -235,25 +276,26 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
235
276
|
const message = `## pidiff: Code Review Comments\n\n\`\`\`json\n${JSON.stringify(review, null, 2)}\n\`\`\`\n\nPlease address these review comments.`;
|
|
236
277
|
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
237
278
|
}
|
|
238
|
-
} catch (e: any) { log(`message handler error: ${e.message}`); }
|
|
279
|
+
} catch (e: any) { log.debug(`message handler error: ${e.message}`); }
|
|
239
280
|
});
|
|
240
281
|
|
|
241
282
|
wsClient.on("close", () => {
|
|
242
|
-
log("WebSocket closed");
|
|
283
|
+
log.debug("WebSocket closed");
|
|
243
284
|
connected = false; ws = null; connecting = false;
|
|
244
285
|
if (!shuttingDown) {
|
|
245
286
|
setTimeout(() => { if (lastCtx && !shuttingDown) connect(lastCtx); }, RECONNECT_INTERVAL_MS);
|
|
246
287
|
}
|
|
247
288
|
});
|
|
248
289
|
|
|
249
|
-
wsClient.on("error", (e: Error) => { log(`WebSocket error: ${e.message}`); });
|
|
290
|
+
wsClient.on("error", (e: Error) => { log.debug(`WebSocket error: ${e.message}`); });
|
|
250
291
|
} catch (e: any) {
|
|
251
|
-
log(`connect error: ${e.message}`);
|
|
292
|
+
log.debug(`connect error: ${e.message}`);
|
|
252
293
|
connecting = false;
|
|
253
294
|
}
|
|
254
295
|
}
|
|
255
296
|
|
|
256
297
|
// /pidiff command
|
|
298
|
+
let pidiffCmdHandler: any;
|
|
257
299
|
pi.registerCommand("pidiff", {
|
|
258
300
|
description: "Manage pidiff server — /pidiff start|stop|restart|status",
|
|
259
301
|
getArgumentCompletions: (prefix: string) => {
|
|
@@ -265,7 +307,7 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
265
307
|
];
|
|
266
308
|
return items.filter(i => i.value.startsWith(prefix.toLowerCase()));
|
|
267
309
|
},
|
|
268
|
-
handler: async (args, ctx) => {
|
|
310
|
+
handler: pidiffCmdHandler = async (args: any, ctx: any) => {
|
|
269
311
|
lastCtx = ctx;
|
|
270
312
|
const cmd = (args || "").trim().toLowerCase();
|
|
271
313
|
|
|
@@ -273,8 +315,8 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
273
315
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
274
316
|
connected = false;
|
|
275
317
|
const pidFile = path.join(lockDir, "pidiff.pid");
|
|
276
|
-
killDaemonByPid(pidFile,
|
|
277
|
-
removeLockfile(lockDir,
|
|
318
|
+
killDaemonByPid(pidFile, debugLog);
|
|
319
|
+
removeLockfile(lockDir, debugLog);
|
|
278
320
|
activePort = 0;
|
|
279
321
|
if (ctx.hasUI) { ctx.ui.setStatus("8-diff", undefined); ctx.ui.notify("pidiff server stopped", "info"); }
|
|
280
322
|
return;
|
|
@@ -288,11 +330,12 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
288
330
|
if (!connected) connect(ctx);
|
|
289
331
|
return;
|
|
290
332
|
}
|
|
291
|
-
|
|
333
|
+
// Reuse lockfile port to keep URL stable — fall back if taken
|
|
334
|
+
const port = await resolveSpawnPort(lock?.port);
|
|
292
335
|
doSpawn(port, ctx.cwd);
|
|
293
|
-
writeLockfile(lockDir, port, null,
|
|
336
|
+
writeLockfile(lockDir, port, null, debugLog);
|
|
294
337
|
if (ctx.hasUI) ctx.ui.notify("Starting pidiff server...", "info");
|
|
295
|
-
if (await waitForDaemon(port,
|
|
338
|
+
if (await waitForDaemon(port, getSetting(process.cwd(), "pidiff_daemon_startup_timeout_s"), debugLog)) {
|
|
296
339
|
activePort = port;
|
|
297
340
|
connect(ctx);
|
|
298
341
|
if (ctx.hasUI) ctx.ui.notify(`pidiff started at http://localhost:${activePort}`, "info");
|
|
@@ -306,14 +349,18 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
306
349
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
307
350
|
connected = false;
|
|
308
351
|
const pidFile = path.join(lockDir, "pidiff.pid");
|
|
309
|
-
|
|
310
|
-
|
|
352
|
+
// Reuse existing port to keep URL stable (read before removeLockfile)
|
|
353
|
+
const lock = readLockfile(lockDir);
|
|
354
|
+
const preferredPort = activePort || lock?.port || 0;
|
|
355
|
+
killDaemonByPid(pidFile, debugLog);
|
|
356
|
+
removeLockfile(lockDir, debugLog);
|
|
311
357
|
await new Promise(r => setTimeout(r, 1000));
|
|
312
|
-
|
|
358
|
+
// Verify preferred port is free after kill — fall back if taken by another process
|
|
359
|
+
const port = await resolveSpawnPort(preferredPort || null);
|
|
313
360
|
doSpawn(port, ctx.cwd);
|
|
314
|
-
writeLockfile(lockDir, port, null,
|
|
361
|
+
writeLockfile(lockDir, port, null, debugLog);
|
|
315
362
|
if (ctx.hasUI) ctx.ui.notify("Restarting pidiff server...", "info");
|
|
316
|
-
if (await waitForDaemon(port,
|
|
363
|
+
if (await waitForDaemon(port, getSetting(process.cwd(), "pidiff_daemon_startup_timeout_s"), debugLog)) {
|
|
317
364
|
activePort = port;
|
|
318
365
|
connect(ctx);
|
|
319
366
|
if (ctx.hasUI) ctx.ui.notify(`pidiff restarted at http://localhost:${activePort}`, "info");
|
|
@@ -333,6 +380,17 @@ export function registerPidiff(pi: ExtensionAPI): void {
|
|
|
333
380
|
},
|
|
334
381
|
});
|
|
335
382
|
|
|
383
|
+
// Expose handler to pidash for browser command dispatch
|
|
384
|
+
try {
|
|
385
|
+
if ((globalThis as any).__pidiff_pidash_listener) {
|
|
386
|
+
try { pi.events.removeListener("pidash:request-commands", (globalThis as any).__pidiff_pidash_listener); } catch {}
|
|
387
|
+
}
|
|
388
|
+
const registerWithPidash = () => pi.events.emit("pidash:register-command", { name: "pidiff", handler: pidiffCmdHandler });
|
|
389
|
+
(globalThis as any).__pidiff_pidash_listener = registerWithPidash;
|
|
390
|
+
registerWithPidash();
|
|
391
|
+
pi.events.on("pidash:request-commands", registerWithPidash);
|
|
392
|
+
} catch {}
|
|
393
|
+
|
|
336
394
|
// Session lifecycle
|
|
337
395
|
pi.on("session_start", async (_event, ctx) => {
|
|
338
396
|
lastCtx = ctx;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn-lock evaluation for pidiff daemon startup races.
|
|
3
|
+
* No pi-tui / ExtensionAPI deps — unit tests can import directly.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import { createLogger } from "../shared/logger.js";
|
|
7
|
+
|
|
8
|
+
const log = createLogger("pidiff");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Evaluate a stale spawn lockfile and decide the recovery action.
|
|
12
|
+
* Exported for testing.
|
|
13
|
+
*/
|
|
14
|
+
export function evaluateSpawnLock(
|
|
15
|
+
lockPath: string,
|
|
16
|
+
staleTimeoutMs: number,
|
|
17
|
+
): { action: "wait" | "recover" | "recover_pid_reuse"; reason: string } {
|
|
18
|
+
log.debug("evaluate_spawn_lock", "lockPath", lockPath, "staleTimeoutMs", staleTimeoutMs);
|
|
19
|
+
const effectiveTimeout = Math.max(staleTimeoutMs, 1000); // Floor at 1s to prevent thrashing
|
|
20
|
+
let result: { action: "wait" | "recover" | "recover_pid_reuse"; reason: string };
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(lockPath, "utf-8").trim();
|
|
24
|
+
const spawnerPid = /^\d+$/.test(content) ? parseInt(content, 10) : NaN;
|
|
25
|
+
|
|
26
|
+
if (!spawnerPid || isNaN(spawnerPid)) {
|
|
27
|
+
// No valid PID — check age
|
|
28
|
+
const lockAge = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
29
|
+
if (lockAge > effectiveTimeout) {
|
|
30
|
+
result = { action: "recover", reason: `no valid PID, age ${Math.round(lockAge / 1000)}s > timeout` };
|
|
31
|
+
} else {
|
|
32
|
+
result = { action: "wait", reason: "no valid PID but lock is young" };
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
let isAlive = false;
|
|
36
|
+
try {
|
|
37
|
+
process.kill(spawnerPid, 0);
|
|
38
|
+
isAlive = true;
|
|
39
|
+
} catch (e: any) {
|
|
40
|
+
// EPERM = process exists but we can't signal it — treat as alive
|
|
41
|
+
isAlive = e?.code === "EPERM";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isAlive) {
|
|
45
|
+
result = { action: "recover", reason: `PID ${spawnerPid} is dead` };
|
|
46
|
+
} else {
|
|
47
|
+
// PID alive — check for PID reuse (lock age > timeout)
|
|
48
|
+
const lockAge = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
49
|
+
if (lockAge > effectiveTimeout) {
|
|
50
|
+
result = { action: "recover_pid_reuse", reason: `PID ${spawnerPid} alive but lock age ${Math.round(lockAge / 1000)}s > timeout (likely PID reuse)` };
|
|
51
|
+
} else {
|
|
52
|
+
result = { action: "wait", reason: `PID ${spawnerPid} alive, lock age ${Math.round(lockAge / 1000)}s within threshold` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
result = { action: "recover", reason: "cannot read/stat lockfile" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
log.debug("evaluate_spawn_lock", result.action, result.reason);
|
|
61
|
+
return result;
|
|
62
|
+
}
|