@volter/supercode-teams 0.2.95 → 0.2.97

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.
@@ -1,7 +1,9 @@
1
1
  // An upgrade replaces the code a long-running connector runs (its native binary, this package) under it. Its readers
2
2
  // go on answering from the code they loaded, so every reading it serves is the old version's until something
3
- // restarts it. This watches the files that code came from and says, once, when one of them changed.
4
- import { statSync } from 'node:fs';
3
+ // restarts it. This watches the files that code came from and says, once, when one of them changed; and it lets the
4
+ // service's connector take back the socket from a linkless daemon started while it was between restarts.
5
+ import { readFileSync, statSync } from 'node:fs';
6
+ import { connect } from 'node:net';
5
7
  import { fileURLToPath } from 'node:url';
6
8
 
7
9
  const CODE_WATCH_MS = 60_000;
@@ -24,3 +26,27 @@ export function watchCodeChange(files, onChange) {
24
26
  timer.unref?.();
25
27
  return () => clearInterval(timer);
26
28
  }
29
+
30
+ const alive = (pid) => { try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
31
+
32
+ /**
33
+ * Stop the daemon `record` (its machine.json) names when it has no server link (`server: null`, a standalone
34
+ * `machine start`) and is not this process, and wait up to 15 s for it to go. A linked daemon is left alone.
35
+ */
36
+ export async function retireLinklessDaemon(record) {
37
+ let daemon;
38
+ try { daemon = JSON.parse(readFileSync(record, 'utf8')); } catch { return; }
39
+ const pid = Number(daemon?.pid);
40
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid || daemon.server || !alive(pid)) return;
41
+ // The record outlives a crash and its pid can be reused: only a daemon that answers on the socket is stopped.
42
+ const answers = await new Promise((resolve) => {
43
+ if (typeof daemon.socket !== 'string') return resolve(false);
44
+ const probe = connect(daemon.socket);
45
+ probe.once('connect', () => { probe.destroy(); resolve(true); });
46
+ probe.once('error', () => resolve(false));
47
+ });
48
+ if (!answers) return;
49
+ process.stderr.write(`supercode teams: a machine daemon with no server link (pid ${pid}) holds this machine's socket; asking it to stop\n`);
50
+ try { process.kill(pid, 'SIGTERM'); } catch { return; }
51
+ for (let waited = 0; waited < 15_000 && alive(pid); waited += 250) await new Promise((resolve) => setTimeout(resolve, 250));
52
+ }
package/bin/workspace.mjs CHANGED
@@ -18,7 +18,7 @@ import { loadOrCreateKeyPair, publicKeyId } from '@volter/teams/identity';
18
18
  import { duration, parseArgs, positiveInteger, requireArgs, usage } from './workspace/args.mjs';
19
19
  import { reserveArtifactDirectory, reserveProtected, writeProtected } from './workspace/files.mjs';
20
20
  import { acquireLifecycle, activeSocket, lifecycleHeld, requestStop } from './workspace/lifecycle.mjs';
21
- import { watchCodeChange } from './workspace/upgrade-watch.mjs';
21
+ import { retireLinklessDaemon, watchCodeChange } from './workspace/upgrade-watch.mjs';
22
22
  import { volterDeviceSignIn } from './workspace/volter-device.mjs';
23
23
  import { loginSummary, signInToTeams } from './workspace/volter-login.mjs';
24
24
  import { readSharedSignIn, signOut } from '@volter/identity';
@@ -736,6 +736,34 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
736
736
  // The connector is the service's daemon and keeps serving; a daemon that took its socket path is the one to stop.
737
737
  machine.on('superseded', (path) => process.stderr.write(`supercode teams: another machine daemon replaced this connector's socket at ${path}; stop it (supercode teams status)\n`));
738
738
  const stopAll = async () => { await machine.stop().catch(() => {}); await connector.stop(); await core.close(); await largeCore.close(); await slowCore.close(); await liveCore.close(); await lifecycle.close(); state.update(current => current ? ({ ...current, process: undefined }) : null); };
739
+ // A daemon with no server link holding the socket is a session's auto-start while this service was between
740
+ // restarts (from a supercode old enough not to wait for the service): it yields to the service's connector, which
741
+ // otherwise failed on EADDRINUSE at every restart and left this machine unlinked.
742
+ if (saved.service) await retireLinklessDaemon(paths(daemonHome).machine);
743
+ try { await connector.start({ roots: () => saved.roots }); await machine.start(); } catch (error) { await stopAll(); throw error; }
744
+ output({ version: 1, context: scope(context), data: { machine: saved.machine, source: saved.source, roots: saved.roots.map(({ path: _path, ...item }) => item), socket: machine.socketPath, foreground: true } });
745
+ // Run by a service, the connector moves to an upgrade on its own: its readers would otherwise answer from the code
746
+ // they loaded until something restarted it (after 0.5.51 this Mac's went on reading a Codex turn waiting on an
747
+ // approval as busy). The move is the install, run by the upgraded CLI, because an upgrade can change the service
748
+ // unit too (a launchd job without ProcessType ran at background priority and missed discovery's bound): it rewrites
749
+ // a changed unit and restarts this connector under it. It runs in a session of its own, so a service manager
750
+ // stopping this connector cannot stop it half done. On Windows the service runs a per-version copy, so the upgraded
751
+ // CLI is the `supercode` on PATH (whose install stops this connector; the task's repeat starts the new copy).
752
+ // An unchanged unit leaves this connector running, and it then restarts itself: every service manager here
753
+ // restarts a connector that exits non-zero.
754
+ if (saved.service) {
755
+ const shim = process.platform === 'win32' ? (() => { try { return execFileSync('where', ['supercode'], { encoding: 'utf8', windowsHide: true }).split(/\r?\n/).find(Boolean); } catch { return null; } })() : null;
756
+ watchCodeChange([process.env.SUPERCODE_BIN, new URL('../package.json', import.meta.url), shim], (changed) => {
757
+ process.stderr.write(`supercode teams: ${changed} changed under this connector (an upgrade); moving to the new code\n`);
758
+ const cli = process.platform === 'win32' ? 'supercode' : process.env.SUPERCODE_BIN;
759
+ spawn(cli, ['teams', 'connect', '--install', '--context', contextName, '--cwd', path], { shell: process.platform === 'win32', detached: true, stdio: 'ignore', windowsHide: true }).unref();
760
+ if (process.platform !== 'win32') setTimeout(() => { process.exitCode = 75; void stopConnector?.(); }, 60_000);
761
+ });
762
+ }
763
+ // A daemon with no server link holding the socket is a session's auto-start while this service was between
764
+ // restarts (from a supercode old enough not to wait for the service): it yields to the service's connector, which
765
+ // otherwise failed on EADDRINUSE at every restart and left this machine unlinked.
766
+ if (saved.service) await retireLinklessDaemon(paths(daemonHome).machine);
739
767
  try { await connector.start({ roots: () => saved.roots }); await machine.start(); } catch (error) { await stopAll(); throw error; }
740
768
  output({ version: 1, context: scope(context), data: { machine: saved.machine, source: saved.source, roots: saved.roots.map(({ path: _path, ...item }) => item), socket: machine.socketPath, foreground: true } });
741
769
  // Run by a service, the connector moves to an upgrade on its own: its readers would otherwise answer from the code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.95",
3
+ "version": "0.2.97",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",