@raingor/pi-web-switch 0.6.0 → 0.6.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server/pi-reader.ts +28 -15
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@raingor/pi-web-switch",
3
3
  "private": false,
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "type": "module",
6
6
  "main": "dist/index.html",
7
7
  "description": "Web UI for pi coding agent — live configuration management, session browser, and memory viewer",
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, readdirSync, existsSync, statSync, unlinkSync, writeFileSync, mkdirSync, renameSync, chmodSync } from "fs";
2
2
  import { homedir, platform } from "os";
3
3
  import { join, resolve, dirname, relative, sep } from "path";
4
- import { spawnSync } from "child_process";
4
+ import { spawnSync, spawn } from "child_process";
5
5
  import { DatabaseSync } from "node:sqlite";
6
6
 
7
7
  const PI_DIR = join(homedir(), ".pi", "agent");
@@ -1868,26 +1868,39 @@ export async function optimizeMemory(): Promise<OptimizeMemoryResult> {
1868
1868
  ? cfg.consolidationTimeoutMs
1869
1869
  : 600000;
1870
1870
 
1871
+ // IMPORTANT: use async spawn, NOT spawnSync. This runs inside the Vite dev
1872
+ // server's Node process; a multi-minute spawnSync would block the event loop,
1873
+ // freeze the HMR WebSocket heartbeat, and make the browser's Vite client
1874
+ // force a full page reload when the socket reconnects. Async spawn keeps the
1875
+ // loop free so /api/pi/memory/status polling and HMR stay responsive.
1871
1876
  try {
1872
- const out = spawnSync(bin, args, {
1873
- encoding: "utf8",
1874
- timeout: timeoutMs,
1875
- maxBuffer: 1024 * 1024 * 8,
1877
+ const status2: { code: number | null; killed: boolean; error?: Error } = await new Promise((resolvePromise) => {
1878
+ const child = spawn(bin, args, { stdio: ["ignore", "ignore", "ignore"] });
1879
+ let settled = false;
1880
+ const finish = (r: { code: number | null; killed: boolean; error?: Error }) => {
1881
+ if (settled) return;
1882
+ settled = true;
1883
+ clearTimeout(killTimer);
1884
+ resolvePromise(r);
1885
+ };
1886
+ const killTimer = setTimeout(() => {
1887
+ child.kill("SIGKILL");
1888
+ finish({ code: null, killed: true });
1889
+ }, timeoutMs);
1890
+ child.on("error", (error) => finish({ code: null, killed: false, error }));
1891
+ child.on("close", (code) => finish({ code, killed: false }));
1876
1892
  });
1893
+
1877
1894
  const after = memoryFileSizes();
1878
1895
  const freedBytes = Object.values(before).reduce((a, b) => a + b, 0)
1879
1896
  - Object.values(after).reduce((a, b) => a + b, 0);
1880
- if (out.error) {
1881
- const killed = (out.error as NodeJS.ErrnoException).code === "ETIMEDOUT";
1882
- return {
1883
- success: false,
1884
- before,
1885
- after,
1886
- freedBytes,
1887
- message: killed ? `consolidation timed out after ${Math.round(timeoutMs / 1000)}s` : String(out.error.message),
1888
- };
1897
+ if (status2.error) {
1898
+ return { success: false, before, after, freedBytes, message: String(status2.error.message) };
1899
+ }
1900
+ if (status2.killed) {
1901
+ return { success: false, before, after, freedBytes, message: `consolidation timed out after ${Math.round(timeoutMs / 1000)}s` };
1889
1902
  }
1890
- return { success: out.status === 0, before, after, freedBytes, message: out.status === 0 ? undefined : `exit ${out.status}` };
1903
+ return { success: status2.code === 0, before, after, freedBytes, message: status2.code === 0 ? undefined : `exit ${status2.code}` };
1891
1904
  } catch (e) {
1892
1905
  const after = memoryFileSizes();
1893
1906
  return { success: false, before, after, freedBytes: 0, message: e instanceof Error ? e.message : String(e) };