synabun 2026.4.26-beta.11 → 2026.4.26-beta.12

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.
@@ -1594,11 +1594,15 @@ async function gracefulShutdown(reason = 'shutdown') {
1594
1594
 
1595
1595
  try { closeDb(); } catch {}
1596
1596
 
1597
- // 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
1598
- // Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
1599
- // not be reachable through the registries above. The taskkill /T flag walks
1600
- // the process tree and terminates every descendant.
1601
- if (process.platform === 'win32') {
1597
+ // 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
1598
+ // Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
1599
+ // not be reachable through the registries above. The taskkill /T flag walks
1600
+ // the process tree and terminates every descendant.
1601
+ //
1602
+ // Do not run this for click-to-update. The updater terminal is deliberately
1603
+ // launched before this server exits; taskkill /T would kill that brand-new
1604
+ // terminal too, which looks like a brief blink and no update on Windows.
1605
+ if (process.platform === 'win32' && reason !== 'run-update') {
1602
1606
  try {
1603
1607
  // Use spawn(detached) so taskkill survives our own exit and finishes
1604
1608
  // killing children even if we exit before it completes.
@@ -1635,9 +1639,45 @@ app.post('/api/server/shutdown', async (req, res) => {
1635
1639
  }, 200);
1636
1640
  });
1637
1641
 
1638
- function shellSingleQuote(s) {
1639
- return `'${String(s).replace(/'/g, `'\\''`)}'`;
1640
- }
1642
+ function shellSingleQuote(s) {
1643
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
1644
+ }
1645
+
1646
+ function collectWindowsDescendantPids(rootPid) {
1647
+ if (process.platform !== 'win32' || !rootPid) return [];
1648
+ try {
1649
+ const script = [
1650
+ `$root=${Number(rootPid)}`,
1651
+ '$procs=Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId',
1652
+ '$seen=@{}',
1653
+ '$front=@($root)',
1654
+ '$out=@()',
1655
+ 'while ($front.Count -gt 0) {',
1656
+ ' $next=@()',
1657
+ ' foreach ($p in $procs) {',
1658
+ ' if ($front -contains $p.ParentProcessId -and -not $seen.ContainsKey($p.ProcessId)) {',
1659
+ ' $seen[$p.ProcessId]=$true',
1660
+ ' $out += $p.ProcessId',
1661
+ ' $next += $p.ProcessId',
1662
+ ' }',
1663
+ ' }',
1664
+ ' $front=$next',
1665
+ '}',
1666
+ '$out -join ","',
1667
+ ].join('; ');
1668
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
1669
+ encoding: 'utf8',
1670
+ windowsHide: true,
1671
+ timeout: 5000,
1672
+ });
1673
+ const out = result.status === 0 ? String(result.stdout || '').trim() : '';
1674
+ return out
1675
+ ? out.split(',').map(v => Number(v)).filter(pid => Number.isFinite(pid) && pid > 0 && pid !== rootPid)
1676
+ : [];
1677
+ } catch {
1678
+ return [];
1679
+ }
1680
+ }
1641
1681
 
1642
1682
  function cleanupOldUpdaterStages(baseDir) {
1643
1683
  try {
@@ -1653,7 +1693,7 @@ function cleanupOldUpdaterStages(baseDir) {
1653
1693
  } catch {}
1654
1694
  }
1655
1695
 
1656
- function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
1696
+ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
1657
1697
  const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
1658
1698
  if (!existsSync(sourceUpdater)) {
1659
1699
  throw new Error(`updater.mjs not found at ${sourceUpdater}. SynaBun install may be corrupt - reinstall manually: ${installPlan.displayCommand}`);
@@ -1678,20 +1718,22 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
1678
1718
  displayCommand: installPlan.displayCommand,
1679
1719
  source: installPlan.source,
1680
1720
  current: installPlan.current,
1681
- target: installPlan.target,
1682
- channel: installPlan.channel,
1683
- };
1721
+ target: installPlan.target,
1722
+ channel: installPlan.channel,
1723
+ cleanupPids,
1724
+ };
1684
1725
  writeFileSync(payloadPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
1685
1726
 
1686
- if (process.platform === 'win32') {
1687
- writeFileSync(runnerPath, [
1688
- '@echo off',
1689
- 'setlocal enableextensions',
1690
- 'title SynaBun Updater',
1691
- 'cd /d "%~dp0"',
1692
- 'set "EXIT_CODE=0"',
1693
- 'set "NODE_BIN=node"',
1694
- 'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
1727
+ if (process.platform === 'win32') {
1728
+ const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
1729
+ writeFileSync(runnerPath, [
1730
+ '@echo off',
1731
+ 'setlocal enableextensions',
1732
+ 'title SynaBun Updater',
1733
+ 'cd /d "%~dp0"',
1734
+ 'set "EXIT_CODE=0"',
1735
+ `set "NODE_BIN=${nodeBin}"`,
1736
+ 'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
1695
1737
  '"%NODE_BIN%" "%~dp0updater.mjs" --payload "%~dp0payload.json" --no-hold',
1696
1738
  'set "EXIT_CODE=%ERRORLEVEL%"',
1697
1739
  'echo.',
@@ -1761,7 +1803,8 @@ app.post('/api/system/run-update', async (req, res) => {
1761
1803
 
1762
1804
  const plat = process.platform;
1763
1805
  const serverPid = process.pid;
1764
- const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart });
1806
+ const cleanupPids = collectWindowsDescendantPids(serverPid);
1807
+ const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
1765
1808
 
1766
1809
  let spawned = null;
1767
1810
  let launchError = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.26-beta.11",
3
+ "version": "2026.4.26-beta.12",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",
package/updater.mjs CHANGED
@@ -20,6 +20,7 @@ const legacyTag = sanitizeTag(args.tag || 'latest');
20
20
  const installSpec = sanitizeInstallSpec(payload.installSpec || args['install-spec'] || `synabun@${legacyTag}`);
21
21
  const displayCommand = payload.displayCommand || `npm i -g ${installSpec}`;
22
22
  const serverPid = toPid(payload.serverPid ?? args.pid);
23
+ const cleanupPids = sanitizePidList(payload.cleanupPids).filter(pid => pid !== serverPid && pid !== process.pid);
23
24
  const autoRestart = payload.autoRestart != null ? !!payload.autoRestart : !!args.restart;
24
25
  const safeCwd = payload.cwd || process.cwd();
25
26
  const noHold = !!args['no-hold'];
@@ -46,8 +47,8 @@ try {
46
47
  if (exited) {
47
48
  console.log(' Server exited.\n');
48
49
  } else {
49
- console.warn(' Server still alive after 30s - forcing kill.\n');
50
- forceKillTree(serverPid);
50
+ console.warn(' Server still alive after 30s - forcing server process kill.\n');
51
+ forceKillPid(serverPid);
51
52
  await sleep(1500);
52
53
  }
53
54
  } else {
@@ -55,7 +56,15 @@ try {
55
56
  }
56
57
 
57
58
  await sleep(1500);
58
- if (isWin && serverPid) forceKillTree(serverPid);
59
+ if (cleanupPids.length) {
60
+ console.log(`Cleaning up ${cleanupPids.length} pre-existing child process${cleanupPids.length === 1 ? '' : 'es'}...`);
61
+ for (const pid of cleanupPids) {
62
+ if (!isPidAlive(pid)) continue;
63
+ forceKillTree(pid);
64
+ }
65
+ await sleep(1000);
66
+ console.log(' Cleanup complete.\n');
67
+ }
59
68
 
60
69
  console.log(`Running: ${displayCommand}\n`);
61
70
  console.log(HR);
@@ -143,6 +152,19 @@ function toPid(raw) {
143
152
  return Number.isFinite(pid) && pid > 0 ? pid : null;
144
153
  }
145
154
 
155
+ function sanitizePidList(raw) {
156
+ if (!Array.isArray(raw)) return [];
157
+ const out = [];
158
+ const seen = new Set();
159
+ for (const item of raw) {
160
+ const pid = toPid(item);
161
+ if (!pid || seen.has(pid)) continue;
162
+ seen.add(pid);
163
+ out.push(pid);
164
+ }
165
+ return out;
166
+ }
167
+
146
168
  async function waitForExit(pid, timeoutMs) {
147
169
  const deadline = Date.now() + timeoutMs;
148
170
  while (Date.now() < deadline) {
@@ -176,6 +198,20 @@ function forceKillTree(pid) {
176
198
  } catch {}
177
199
  }
178
200
 
201
+ function forceKillPid(pid) {
202
+ try {
203
+ if (isWin) {
204
+ spawnSync('taskkill', ['/pid', String(pid), '/F'], {
205
+ shell: true,
206
+ windowsHide: true,
207
+ stdio: 'ignore',
208
+ });
209
+ } else {
210
+ try { process.kill(pid, 'SIGKILL'); } catch {}
211
+ }
212
+ } catch {}
213
+ }
214
+
179
215
  function printFixTips(command) {
180
216
  console.error('Common fixes:');
181
217
  if (isWin) {