synabun 2026.4.26-beta.6 → 2026.4.26-beta.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.
@@ -4509,12 +4509,36 @@ function stopOpencodeServer() {
4509
4509
  stopOpencodeSSERelay();
4510
4510
  const managedPid = readOpencodeManagedPid();
4511
4511
  const procPid = _ocpProc?.pid || null;
4512
+ const isWin = process.platform === 'win32';
4513
+
4514
+ // On Windows `process.kill(pid, 'SIGTERM')` is unreliable for non-Node
4515
+ // children (cmd.exe shells, opencode native binaries). Use taskkill /T /F
4516
+ // to walk the process tree and force-terminate every descendant.
4517
+ // This is the recurring source of EBUSY errors during `npm install -g
4518
+ // synabun@latest` — opencode-ai's `serve` process survives parent SynaBun
4519
+ // exit and keeps file handles inside neural-interface/ open.
4520
+ const winKill = (pid) => {
4521
+ try {
4522
+ spawn('taskkill', ['/pid', String(pid), '/T', '/F'], {
4523
+ shell: true, windowsHide: true, stdio: 'ignore',
4524
+ });
4525
+ } catch {}
4526
+ };
4527
+
4512
4528
  if (_ocpProc && !_ocpProc.killed) {
4513
- try { _ocpProc.kill(); } catch {}
4529
+ if (isWin && procPid) {
4530
+ winKill(procPid);
4531
+ } else {
4532
+ try { _ocpProc.kill(); } catch {}
4533
+ }
4514
4534
  _ocpProc = null;
4515
4535
  }
4516
4536
  if (managedPid && managedPid !== procPid) {
4517
- signalPids([managedPid], 'SIGTERM');
4537
+ if (isWin) {
4538
+ winKill(managedPid);
4539
+ } else {
4540
+ signalPids([managedPid], 'SIGTERM');
4541
+ }
4518
4542
  }
4519
4543
  clearOpencodeManagedPid();
4520
4544
  _ocpReady = false;
@@ -23587,6 +23611,21 @@ function saveSessionSnapshot() {
23587
23611
  setInterval(saveSessionSnapshot, SESSION_SNAPSHOT_INTERVAL_MS);
23588
23612
 
23589
23613
  // Also try to save on clean shutdown (works with Ctrl+C, not window close)
23590
- process.on('SIGINT', () => { stopOpencodeServer(); saveSessionSnapshot(); process.exit(0); });
23591
- process.on('SIGTERM', () => { stopOpencodeServer(); saveSessionSnapshot(); process.exit(0); });
23592
- process.on('exit', () => { stopOpencodeServer(); saveSessionSnapshot(); });
23614
+ // Manual termination paths (Ctrl+C, kill, parent shell exit) all flow through
23615
+ // gracefulShutdown — same teardown the in-app restart/shutdown/update paths
23616
+ // use. Without this, hand-killed servers still leave orphaned opencode +
23617
+ // agent children holding file handles, which blocks `npm install -g synabun`.
23618
+ process.on('SIGINT', async () => {
23619
+ console.log('\n[server] SIGINT received.');
23620
+ await gracefulShutdown('sigint');
23621
+ process.exit(0);
23622
+ });
23623
+ process.on('SIGTERM', async () => {
23624
+ console.log('\n[server] SIGTERM received.');
23625
+ await gracefulShutdown('sigterm');
23626
+ process.exit(0);
23627
+ });
23628
+ // `exit` fires after process.exit() — only safe for synchronous final tasks.
23629
+ // gracefulShutdown is async so we cannot await here; just snapshot + ocp stop
23630
+ // best-effort. The async paths above are the primary teardown.
23631
+ process.on('exit', () => { try { stopOpencodeServer(); } catch {} try { saveSessionSnapshot(); } catch {} });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.26-beta.6",
3
+ "version": "2026.4.26-beta.7",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",
package/preuninstall.js CHANGED
@@ -69,6 +69,80 @@ async function tryGracefulServerShutdown() {
69
69
  }
70
70
  }
71
71
 
72
+ // ── Step 1b: kill any orphaned processes holding file handles ──
73
+ //
74
+ // Even after a graceful server shutdown (or when no server is running),
75
+ // orphaned children can survive: opencode-ai sidecars spawned via cmd.exe,
76
+ // MCP servers (claude-memory-mcp) spawned by Claude Code instances rather
77
+ // than by SynaBun itself, agent processes, etc. On Windows these orphans
78
+ // keep file handles inside the install dir open, causing EBUSY when npm
79
+ // tries to rename it.
80
+ //
81
+ // We nuke any process whose command line references the SynaBun install
82
+ // path. This catches every orphan regardless of how they got that way.
83
+ // Skips the current PID (we are the preuninstall process itself).
84
+
85
+ killOrphansInsideInstall();
86
+
87
+ function killOrphansInsideInstall() {
88
+ const installPath = __dirname;
89
+ const selfPid = process.pid;
90
+ const plat = platform();
91
+
92
+ if (plat === 'win32') {
93
+ // PowerShell: Get-CimInstance returns every process with CommandLine.
94
+ // Filter for our install path (escape backslashes for the -like wildcard).
95
+ // Stop-Process -Force kills synchronously; -ErrorAction SilentlyContinue
96
+ // suppresses "process already exited" noise.
97
+ const escaped = installPath.replace(/\\/g, '\\\\').replace(/'/g, `''`);
98
+ const ps = `
99
+ $ErrorActionPreference = 'SilentlyContinue';
100
+ Get-CimInstance Win32_Process |
101
+ Where-Object {
102
+ $_.ProcessId -ne ${selfPid} -and (
103
+ ($_.CommandLine -and $_.CommandLine -like '*${escaped}*') -or
104
+ ($_.ExecutablePath -and $_.ExecutablePath -like '${escaped}*')
105
+ )
106
+ } |
107
+ ForEach-Object {
108
+ Write-Host (' Killing orphan PID ' + $_.ProcessId + ' (' + $_.Name + ')');
109
+ Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
110
+ }
111
+ `.trim();
112
+ try {
113
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, {
114
+ stdio: 'inherit',
115
+ timeout: 8000,
116
+ });
117
+ } catch {
118
+ // PowerShell unavailable, blocked, or timed out. Fall through.
119
+ }
120
+ } else {
121
+ // Unix: pgrep -f matches against the full command line. -P 1 skips
122
+ // self/children of init we shouldn't touch. We just enumerate then
123
+ // SIGKILL each one that isn't us.
124
+ try {
125
+ const out = execSync(`pgrep -f "${installPath.replace(/[/.*?+^$|(){}\\\[\]]/g, '\\$&')}"`, {
126
+ encoding: 'utf8',
127
+ timeout: 5000,
128
+ }).trim();
129
+ const pids = out.split('\n')
130
+ .map(s => Number(s.trim()))
131
+ .filter(n => Number.isFinite(n) && n > 0 && n !== selfPid);
132
+ for (const pid of pids) {
133
+ try { process.kill(pid, 'SIGKILL'); } catch {}
134
+ }
135
+ } catch {
136
+ // No matches or pgrep missing — fine.
137
+ }
138
+ }
139
+
140
+ // Brief grace for OS to release file handles after killing.
141
+ // Synchronous busy-wait so we run before the rmSync calls below.
142
+ const until = Date.now() + 1500;
143
+ while (Date.now() < until) { /* spin */ }
144
+ }
145
+
72
146
  // ── Step 2: remove nested node_modules ──
73
147
 
74
148
  const dirs = [