@fitchmultz/pi-subagents 0.45.0 → 0.46.0

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.
@@ -8,8 +8,9 @@ import { getBrokerSocketPath } from "./broker/paths.ts";
8
8
  import { loadConfig, sameListener, type Config, type Identity } from "./bridge-config.ts";
9
9
  import { ASK_TIMEOUT, Fault, fingerprint, log } from "./bridge-protocol.ts";
10
10
  import { Session } from "./bridge-session.ts";
11
- import { operationFor, routeFor } from "./bridge-routes.ts";
11
+ import { operationFor, routeFor, type Operation } from "./bridge-routes.ts";
12
12
  import { reply } from "./bridge-http.ts";
13
+ import { Launcher, sameIdentity } from "./bridge-launch.ts";
13
14
 
14
15
  const MAX_CONNECTIONS = 128;
15
16
 
@@ -24,6 +25,7 @@ class Bridge {
24
25
  private readonly configPath: string;
25
26
  private readonly sessions = new Map<string, Session>();
26
27
  private readonly losses = new Map<string, number>();
28
+ private readonly launcher = new Launcher();
27
29
  private readonly sockets = new Set<Socket>();
28
30
  private readonly connections = new Map<TLSSocket, Connection>();
29
31
  private readonly server: Server;
@@ -88,7 +90,7 @@ class Bridge {
88
90
  }
89
91
  private allowed(identity: Identity): boolean {
90
92
  const current = this.config.clients.get(identity.fingerprint256);
91
- return !this.stopping && current?.name === identity.name && current.cwd === identity.cwd;
93
+ return !this.stopping && sameIdentity(identity, current);
92
94
  }
93
95
  private lost(fp: string, count = 1): void {
94
96
  if (count > 0 && this.config.clients.has(fp)) {
@@ -181,8 +183,9 @@ class Bridge {
181
183
  });
182
184
  });
183
185
  process.on("SIGHUP", () => this.reload());
184
- process.once("SIGINT", () => this.stop());
185
- process.once("SIGTERM", () => this.stop());
186
+ // Retain listeners during dispatch: selected SDK dependencies use signal-exit's listener check.
187
+ process.on("SIGINT", () => this.stop());
188
+ process.on("SIGTERM", () => this.stop());
186
189
  }
187
190
  private reload(): void {
188
191
  if (this.stopping) {
@@ -195,9 +198,13 @@ class Bridge {
195
198
  }
196
199
  this.config = next;
197
200
  this.revokeRemoved();
201
+ this.launcher.revoke(next.clients).catch(() => log("launch_cleanup", { result: "failed" }));
198
202
  log("allowlist_reloaded", { result: "accepted", clients: next.clients.size });
199
203
  } catch {
200
204
  this.config = { ...this.config, clients: new Map() };
205
+ this.launcher
206
+ .revoke(this.config.clients)
207
+ .catch(() => log("launch_cleanup", { result: "failed" }));
201
208
  for (const session of this.sessions.values()) {
202
209
  session.close(
203
210
  new Fault(
@@ -253,7 +260,11 @@ class Bridge {
253
260
  for (const socket of this.sockets) {
254
261
  socket.destroy();
255
262
  }
256
- await Promise.all([...this.handlers, ...sessions.map((session) => session.settled())]);
263
+ await Promise.all([
264
+ ...this.handlers,
265
+ ...sessions.map((session) => session.settled()),
266
+ this.launcher.shutdown(),
267
+ ]);
257
268
  log("shutdown", { result: "closed" });
258
269
  }
259
270
  private admission(req: IncomingMessage): {
@@ -317,14 +328,16 @@ class Bridge {
317
328
  const signal = controller.signal;
318
329
  req.once("aborted", abort);
319
330
  res.once("close", onClose);
320
- setDeadline(route === "POST /v1/ask" ? ASK_TIMEOUT + 15000 : 15000);
331
+ const duration = route === "POST /v1/ask" ? ASK_TIMEOUT + 15000 : 15000;
332
+ setDeadline(route === "POST /v1/start" ? 75000 : duration);
321
333
  const operation = await operationFor(req, route, signal);
322
334
  if (operation.route === "POST /v1/ask") {
323
335
  setDeadline(operation.timeoutMs + 15000);
324
336
  }
325
- const data = await session.execute(operation, {
337
+ const data = await this.execute(operation, session, {
326
338
  signal,
327
339
  requestId,
340
+ expires,
328
341
  onPeer: (id) => {
329
342
  peer = id;
330
343
  },
@@ -351,6 +364,38 @@ class Bridge {
351
364
  }
352
365
  }
353
366
  }
367
+ private async execute(
368
+ operation: Operation,
369
+ session: Readonly<Session>,
370
+ context: {
371
+ readonly signal: AbortSignal;
372
+ readonly requestId: string;
373
+ readonly expires: number;
374
+ readonly onPeer: (id: string) => void;
375
+ },
376
+ ) {
377
+ switch (operation.route) {
378
+ case "POST /v1/start":
379
+ return this.launcher.start(operation.name, {
380
+ identity: session.identity,
381
+ expires: context.expires,
382
+ signal: context.signal,
383
+ peers: (signal) => session.connectedPeers(signal),
384
+ });
385
+ case "GET /v1/sessions":
386
+ return this.launcher.list(session.identity);
387
+ case "POST /v1/stop":
388
+ return this.launcher.stop(session.identity, operation.sessionId);
389
+ case "GET /v1/list":
390
+ case "GET /v1/inbox":
391
+ case "POST /v1/register":
392
+ case "POST /v1/ask":
393
+ case "POST /v1/send":
394
+ case "POST /v1/reply":
395
+ case "POST /v1/ack":
396
+ return session.execute(operation, context);
397
+ }
398
+ }
354
399
  }
355
400
 
356
401
  function main(): void {
@@ -371,7 +416,7 @@ function main(): void {
371
416
  new Bridge(loadConfig(path), path).start();
372
417
  } catch {
373
418
  process.stderr.write(
374
- "Bridge startup failed: check config schema, file ownership/mode 0600, absolute TLS/cwd paths, and TLS credentials.\n",
419
+ "Bridge startup failed: check config schema, file ownership/mode 0600, absolute TLS/cwd paths, TLS credentials, and launch policy (0700 sessionDir, Pi packageRoot, extension paths).\n",
375
420
  );
376
421
  process.exitCode = 1;
377
422
  }
@@ -0,0 +1,13 @@
1
+ import * as path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import type * as NativeSession from "@earendil-works/pi-coding-agent";
4
+
5
+ export async function loadNativeSession(packageRoot: string): Promise<typeof NativeSession> {
6
+ // The trusted selected Pi package supplies this exact SDK declaration contract.
7
+ // A computed URL is required: Jiti rewrites literal SDK imports to its own host graph.
8
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
9
+ const sessionModule: typeof NativeSession = await import(
10
+ pathToFileURL(path.join(packageRoot, "dist/index.js")).href
11
+ );
12
+ return sessionModule;
13
+ }
@@ -1,13 +1,6 @@
1
- import * as path from "node:path";
2
- import { pathToFileURL } from "node:url";
3
- import type * as NativeSession from "@earendil-works/pi-coding-agent";
4
1
  import { requirePiPackageRoot } from "../runs/shared/pi-spawn.ts";
2
+ import { loadNativeSession } from "./native-session-loader.ts";
5
3
 
6
4
  // Detached Node runners do not get Pi's extension-loader package aliases.
7
- const selectedSessionURL = pathToFileURL(path.join(requirePiPackageRoot(), "dist/index.js")).href;
8
- // The trusted selected Pi package supplies this exact SDK declaration contract.
9
- // A computed URL is required: Jiti rewrites literal SDK imports to its own host graph.
10
- // oxlint-disable-next-line typescript/no-unsafe-assignment
11
- const sessionModule: typeof NativeSession = await import(selectedSessionURL);
12
5
  export const { buildSessionContext, parseSessionEntries, migrateSessionEntries, SessionManager } =
13
- sessionModule;
6
+ await loadNativeSession(requirePiPackageRoot());