@byok-sdk/client 0.4.0 → 0.4.1

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 CHANGED
@@ -70,4 +70,4 @@ OS-backed refresh-token custody, a PKCE desktop Google OAuth flow, exact domain
70
70
  policy, a real read-only Gmail metadata adapter, and a closed metadata-only MCP
71
71
  result.
72
72
 
73
- MIT licensed. Node.js 22.19.0 or newer.
73
+ MIT licensed. Node.js 22.22.0 or newer.
@@ -97,7 +97,13 @@ export declare class ClaudeProcessClient {
97
97
  * post-mortem without separate log scraping.
98
98
  */
99
99
  recordUnmappedFrame(label: string): void;
100
- /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
100
+ /**
101
+ * Immediate process-tree termination request. `dispose()` is the settlement
102
+ * receipt, so this stays fire-and-forget: an interrupt must not block on a
103
+ * terminator. A request that could not be spawned is left unrecorded, so
104
+ * `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
105
+ * swallowing it here loses nothing.
106
+ */
101
107
  kill(): void;
102
108
  waitClosed(): Promise<void>;
103
109
  dispose(): Promise<void>;
@@ -77,6 +77,11 @@ export declare class CodexProcessRunner {
77
77
  * cleanly resumable afterward via `codex exec resume` (no corruption from
78
78
  * killing mid-turn). `taskkill /T /F` on Windows, mirroring
79
79
  * `../pi/rpc-client.ts`'s own cross-platform convention.
80
+ *
81
+ * Fire-and-forget by design: an interrupt must not block on a terminator,
82
+ * and `dispose()` is the settlement receipt. A request that could not be
83
+ * spawned is left unrecorded, so `dispose()` re-issues it and raises the
84
+ * typed `stage:'signal'` failure — swallowing it here loses nothing.
80
85
  */
81
86
  kill(): void;
82
87
  dispose(): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { execFile, spawn, spawnSync } from 'child_process';
1
+ import { execFile, spawn } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import { promises, existsSync, readFileSync, realpathSync } from 'fs';
4
4
  import path3 from 'path';
@@ -128,12 +128,12 @@ function resolvePiBin() {
128
128
  }
129
129
  } catch (cause) {
130
130
  throw new Error(
131
- `Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`,
131
+ `Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.22+ pi sidecar`,
132
132
  { cause }
133
133
  );
134
134
  }
135
135
  throw new Error(
136
- `Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`
136
+ `Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.22+ pi sidecar`
137
137
  );
138
138
  }
139
139
 
@@ -338,11 +338,54 @@ var AsyncQueue = class {
338
338
  };
339
339
  }
340
340
  };
341
+
342
+ // src/adapters/taskkill-pid-set.ts
343
+ var INTEGER_PATTERN = /\d+/g;
344
+ function isCandidatePid(value) {
345
+ return Number.isSafeInteger(value) && value > 0;
346
+ }
347
+ function walkTaskkillPidSet(text, rootPid, excludedPids = []) {
348
+ const excluded = new Set(excludedPids);
349
+ const accepted = /* @__PURE__ */ new Set();
350
+ if (isCandidatePid(rootPid)) accepted.add(rootPid);
351
+ const lines = text.split(/\r?\n/).map((line) => {
352
+ const pids = [];
353
+ for (const match of line.matchAll(INTEGER_PATTERN)) {
354
+ const pid = Number(match[0]);
355
+ if (isCandidatePid(pid) && !excluded.has(pid)) pids.push(pid);
356
+ }
357
+ return pids;
358
+ });
359
+ let changed = true;
360
+ while (changed) {
361
+ changed = false;
362
+ for (const pids of lines) {
363
+ if (!pids.some((pid) => accepted.has(pid))) continue;
364
+ for (const pid of pids) {
365
+ if (accepted.has(pid)) continue;
366
+ accepted.add(pid);
367
+ changed = true;
368
+ }
369
+ }
370
+ }
371
+ return accepted;
372
+ }
373
+
374
+ // src/adapters/process-tree.ts
341
375
  var DEFAULT_TERM_GRACE_MS = 750;
342
376
  var DEFAULT_KILL_GRACE_MS = 2e3;
343
377
  var POLL_MS = 20;
344
- var terminationRequested = /* @__PURE__ */ new WeakSet();
345
- var terminationRequestFailed = /* @__PURE__ */ new WeakSet();
378
+ var terminationState = /* @__PURE__ */ new WeakMap();
379
+ function stateFor(child) {
380
+ const existing = terminationState.get(child);
381
+ if (existing) return existing;
382
+ const created = { requested: false, acceptedPids: /* @__PURE__ */ new Set() };
383
+ terminationState.set(child, created);
384
+ return created;
385
+ }
386
+ function defaultKill(pid, signal) {
387
+ process.kill(pid, signal);
388
+ }
346
389
  function withOwnedProcessTree(options) {
347
390
  return {
348
391
  ...options,
@@ -360,9 +403,9 @@ function positivePid(child, label) {
360
403
  }
361
404
  return pid;
362
405
  }
363
- function groupExists(pid, label) {
406
+ function groupExists(pid, label, kill) {
364
407
  try {
365
- process.kill(-pid, 0);
408
+ kill(-pid, 0);
366
409
  return true;
367
410
  } catch (cause) {
368
411
  const code = cause.code;
@@ -374,9 +417,23 @@ function groupExists(pid, label) {
374
417
  }, { cause });
375
418
  }
376
419
  }
377
- function signalGroup(pid, signal, label) {
420
+ function processExists(pid, label, kill) {
378
421
  try {
379
- process.kill(-pid, signal);
422
+ kill(pid, 0);
423
+ return true;
424
+ } catch (cause) {
425
+ const code = cause.code;
426
+ if (code === "ESRCH") return false;
427
+ if (code === "EPERM") return true;
428
+ throw new RuntimeDisposalFailure({
429
+ stage: "quiescence",
430
+ reason: `${label} runtime process ${pid} state could not be verified`
431
+ }, { cause });
432
+ }
433
+ }
434
+ function signalGroup(pid, signal, label, kill) {
435
+ try {
436
+ kill(-pid, signal);
380
437
  } catch (cause) {
381
438
  const code = cause.code;
382
439
  if (code === "ESRCH" || code === "EPERM") return;
@@ -386,6 +443,39 @@ function signalGroup(pid, signal, label) {
386
443
  }, { cause });
387
444
  }
388
445
  }
446
+ async function runTaskkill(pid, options) {
447
+ const spawnFn = options.spawnFn ?? spawn;
448
+ return new Promise((resolve, reject) => {
449
+ const signalFailure = (cause) => {
450
+ reject(new RuntimeDisposalFailure({
451
+ stage: "signal",
452
+ reason: `${options.label} runtime process tree termination could not be requested`
453
+ }, { cause }));
454
+ };
455
+ let taskkill;
456
+ try {
457
+ taskkill = spawnFn("taskkill", ["/PID", String(pid), "/T", "/F"], {
458
+ windowsHide: true,
459
+ stdio: ["ignore", "pipe", "pipe"]
460
+ });
461
+ } catch (cause) {
462
+ signalFailure(cause);
463
+ return;
464
+ }
465
+ const chunks = [];
466
+ taskkill.stdout?.on("data", (chunk) => chunks.push(chunk));
467
+ taskkill.stderr?.on("data", (chunk) => chunks.push(chunk));
468
+ taskkill.once("error", signalFailure);
469
+ taskkill.once("close", () => resolve(Buffer.concat(chunks).toString("latin1")));
470
+ });
471
+ }
472
+ function liveAcceptedPids(accepted, label, kill) {
473
+ const live = [];
474
+ for (const pid of accepted) {
475
+ if (processExists(pid, label, kill)) live.push(pid);
476
+ }
477
+ return live;
478
+ }
389
479
  async function waitUntil(predicate, timeoutMs) {
390
480
  const deadline = Date.now() + timeoutMs;
391
481
  while (predicate()) {
@@ -423,29 +513,27 @@ async function waitWithDeadline(promise, timeoutMs) {
423
513
  );
424
514
  });
425
515
  }
426
- function requestOwnedProcessTreeTermination(options) {
427
- if (options.isClosed()) return;
516
+ async function requestOwnedProcessTreeTermination(options) {
517
+ const platform = options.platform ?? process.platform;
518
+ if (platform !== "win32" && options.isClosed()) return;
428
519
  const pid = positivePid(options.child, options.label);
429
520
  if (pid === void 0) return;
430
- if (process.platform === "win32") {
431
- const result = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true });
432
- if (result.error) {
433
- throw new RuntimeDisposalFailure({
434
- stage: "signal",
435
- reason: `${options.label} runtime process tree could not be terminated`
436
- }, { cause: result.error });
437
- }
438
- terminationRequested.add(options.child);
439
- if (result.status !== 0) terminationRequestFailed.add(options.child);
521
+ if (platform === "win32") {
522
+ const output = await runTaskkill(pid, options);
523
+ const state = stateFor(options.child);
524
+ for (const walked of walkTaskkillPidSet(output, pid, [process.pid])) state.acceptedPids.add(walked);
525
+ state.requested = true;
440
526
  return;
441
527
  }
442
- signalGroup(pid, "SIGTERM", options.label);
443
- terminationRequested.add(options.child);
528
+ signalGroup(pid, "SIGTERM", options.label, options.killFn ?? defaultKill);
529
+ stateFor(options.child).requested = true;
444
530
  }
445
531
  async function disposeOwnedProcessTree(options) {
446
532
  const pid = positivePid(options.child, options.label);
447
533
  const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
448
534
  const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
535
+ const platform = options.platform ?? process.platform;
536
+ const kill = options.killFn ?? defaultKill;
449
537
  if (pid === void 0) {
450
538
  if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
451
539
  throw new RuntimeDisposalFailure({
@@ -453,27 +541,50 @@ async function disposeOwnedProcessTree(options) {
453
541
  reason: `${options.label} runtime process did not settle after spawn failure`
454
542
  });
455
543
  }
456
- if (process.platform === "win32") {
457
- if (!options.isClosed() && !terminationRequested.has(options.child)) requestOwnedProcessTreeTermination(options);
458
- if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
459
- if (terminationRequestFailed.has(options.child)) {
544
+ if (platform === "win32") {
545
+ if (!terminationState.get(options.child)?.requested) {
546
+ await requestOwnedProcessTreeTermination(options);
547
+ }
548
+ const accepted = terminationState.get(options.child)?.acceptedPids ?? /* @__PURE__ */ new Set();
549
+ const deadline = Date.now() + killGraceMs;
550
+ const resweepAt = Date.now() + Math.floor(killGraceMs / 2);
551
+ let reswept = false;
552
+ let live = liveAcceptedPids(accepted, options.label, kill);
553
+ while (live.length > 0) {
554
+ if (Date.now() >= deadline) {
555
+ throw new RuntimeDisposalFailure({
556
+ stage: "quiescence",
557
+ reason: `${options.label} runtime process tree did not quiesce: ${live.length} of ${accepted.size} walked process ids were still alive at the disposal deadline`
558
+ });
559
+ }
560
+ if (!reswept && Date.now() >= resweepAt) {
561
+ reswept = true;
562
+ for (const livePid of live) {
563
+ for (const walked of walkTaskkillPidSet(await runTaskkill(livePid, options), livePid, [process.pid])) {
564
+ accepted.add(walked);
565
+ }
566
+ }
567
+ }
568
+ await new Promise((resolve) => {
569
+ setTimeout(resolve, POLL_MS);
570
+ });
571
+ live = liveAcceptedPids(accepted, options.label, kill);
572
+ }
573
+ if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
460
574
  throw new RuntimeDisposalFailure({
461
- stage: "signal",
462
- reason: `${options.label} runtime process tree could not be terminated`
575
+ stage: "quiescence",
576
+ reason: `${options.label} runtime root did not emit close after its process tree quiesced`
463
577
  });
464
578
  }
465
- throw new RuntimeDisposalFailure({
466
- stage: "quiescence",
467
- reason: `${options.label} runtime process tree did not close before the disposal deadline`
468
- });
579
+ return;
469
580
  }
470
- if (groupExists(pid, options.label) && !terminationRequested.has(options.child)) {
471
- signalGroup(pid, "SIGTERM", options.label);
472
- terminationRequested.add(options.child);
581
+ if (groupExists(pid, options.label, kill) && !terminationState.get(options.child)?.requested) {
582
+ signalGroup(pid, "SIGTERM", options.label, kill);
583
+ stateFor(options.child).requested = true;
473
584
  }
474
- if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
475
- signalGroup(pid, "SIGKILL", options.label);
476
- if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
585
+ if (!await waitUntil(() => groupExists(pid, options.label, kill), termGraceMs)) {
586
+ signalGroup(pid, "SIGKILL", options.label, kill);
587
+ if (!await waitUntil(() => groupExists(pid, options.label, kill), killGraceMs)) {
477
588
  throw new RuntimeDisposalFailure({
478
589
  stage: "quiescence",
479
590
  reason: `${options.label} runtime process group remained live after SIGKILL`
@@ -571,9 +682,16 @@ var PiRpcClient = class {
571
682
  );
572
683
  }
573
684
  }
574
- /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
685
+ /**
686
+ * Immediate process-tree termination request. `dispose()` is the settlement
687
+ * receipt, so this stays fire-and-forget: an interrupt must not block on a
688
+ * terminator. A request that could not be spawned is left unrecorded, so
689
+ * `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
690
+ * swallowing it here loses nothing.
691
+ */
575
692
  kill() {
576
- requestOwnedProcessTreeTermination(this.processTreeOptions());
693
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
694
+ });
577
695
  }
578
696
  waitClosed() {
579
697
  return this.closedPromise;
@@ -1376,9 +1494,16 @@ var ClaudeProcessClient = class {
1376
1494
  );
1377
1495
  }
1378
1496
  }
1379
- /** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
1497
+ /**
1498
+ * Immediate process-tree termination request. `dispose()` is the settlement
1499
+ * receipt, so this stays fire-and-forget: an interrupt must not block on a
1500
+ * terminator. A request that could not be spawned is left unrecorded, so
1501
+ * `dispose()` re-issues it and raises the typed `stage:'signal'` failure —
1502
+ * swallowing it here loses nothing.
1503
+ */
1380
1504
  kill() {
1381
- requestOwnedProcessTreeTermination(this.processTreeOptions());
1505
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
1506
+ });
1382
1507
  }
1383
1508
  waitClosed() {
1384
1509
  return this.closedPromise;
@@ -2143,9 +2268,15 @@ var CodexProcessRunner = class {
2143
2268
  * cleanly resumable afterward via `codex exec resume` (no corruption from
2144
2269
  * killing mid-turn). `taskkill /T /F` on Windows, mirroring
2145
2270
  * `../pi/rpc-client.ts`'s own cross-platform convention.
2271
+ *
2272
+ * Fire-and-forget by design: an interrupt must not block on a terminator,
2273
+ * and `dispose()` is the settlement receipt. A request that could not be
2274
+ * spawned is left unrecorded, so `dispose()` re-issues it and raises the
2275
+ * typed `stage:'signal'` failure — swallowing it here loses nothing.
2146
2276
  */
2147
2277
  kill() {
2148
- requestOwnedProcessTreeTermination(this.processTreeOptions());
2278
+ void requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
2279
+ });
2149
2280
  }
2150
2281
  dispose() {
2151
2282
  if (!this.disposalAttempt) {