@songsid/agend 2.1.5-beta.1 → 2.1.5-beta.2

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.
@@ -976,6 +976,18 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
976
976
  /** Invalidate work queued before a user cancel and wake idle-gate waiters. */
977
977
  private cancelPendingDeliveries;
978
978
  private activeLogin;
979
+ /**
980
+ * Web-terminal login (v2.1.5 default, `login.mode: web`). The relay code
981
+ * below (activeLogin/LoginSession) is `login.mode: relay`, kept for one
982
+ * release as the rollback lever and removed in 2.1.6.
983
+ */
984
+ private loginController;
985
+ /**
986
+ * One login/install window fleet-wide. Claimed synchronously before the
987
+ * first await by web login, relay login and install alike (sol B1).
988
+ */
989
+ private readonly loginWindow;
990
+ private get webLogin();
979
991
  /** Post the backend chooser for a bare `/login`. Caller enforces admin. */
980
992
  promptLoginBackends(chat: {
981
993
  adapter: ChannelAdapter;
@@ -1042,11 +1054,16 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1042
1054
  adapterId: string;
1043
1055
  chatId: string;
1044
1056
  threadId?: string;
1057
+ userId?: string;
1045
1058
  }, opts?: {
1046
1059
  skipAuthCheck?: boolean;
1060
+ tokenPresent?: boolean;
1047
1061
  }): Promise<string | null>;
1062
+ private startRelayClaimed;
1048
1063
  /** Create the login window and session (pre-check already settled). */
1049
1064
  private launchLoginSession;
1065
+ /** Fleet shutdown: end any web/relay login or install window and wait for its confirmed teardown. */
1066
+ private shutdownLoginWindows;
1050
1067
  /** `/login code <text>` — paste admin-supplied text into the login window. */
1051
1068
  loginSubmitInput(text: string): Promise<string>;
1052
1069
  /** `/login cancel` — abort the active session and remove its window. */
@@ -1068,6 +1085,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1068
1085
  private handleLoginBackendSelect;
1069
1086
  /** Re-login confirmation (auth pre-check said credentials still work). */
1070
1087
  private handleLoginConfirm;
1088
+ /** "Resend token" button (web login): only the requester may press it; the token never enters the channel. */
1089
+ private handleLoginTokenResend;
1071
1090
  /** Kiro provider button → drive the CLI's arrow-key selector. */
1072
1091
  private handleLoginMenuSelect;
1073
1092
  private activeInstall;
@@ -48,6 +48,8 @@ import { handleViewRequest, isViewPath } from "./view-api.js";
48
48
  import { handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
49
49
  import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES, checkAuthStatus } from "./login-flows.js";
50
50
  import { LoginSession } from "./login-manager.js";
51
+ import { LoginController, LOGIN_TOKEN_RESEND_PREFIX } from "./login-controller.js";
52
+ import { LoginWindowLock } from "./login-window-lock.js";
51
53
  import { handleSettingsRequest } from "./settings-api.js";
52
54
  import { setLocale, detectLocale, getLocale, t } from "./locale.js";
53
55
  import { handleAgentRequest } from "./agent-endpoint.js";
@@ -1965,6 +1967,8 @@ export class FleetManager {
1965
1967
  }
1966
1968
  /** Start all instances from fleet config */
1967
1969
  async startAll(configPath) {
1970
+ this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
1971
+ this.loginController?.reopen();
1968
1972
  const startupStartedAt = Date.now();
1969
1973
  FleetManager.signalTarget = this;
1970
1974
  this.startupComplete = false;
@@ -2735,6 +2739,8 @@ export class FleetManager {
2735
2739
  return;
2736
2740
  if (await this.handleLoginConfirm(data, adapterId, this.adapter ?? undefined))
2737
2741
  return;
2742
+ if (await this.handleLoginTokenResend(data, adapterId, this.adapter ?? undefined))
2743
+ return;
2738
2744
  if (await this.handleInstallLoginConfirm(data, adapterId, this.adapter ?? undefined))
2739
2745
  return;
2740
2746
  if (await this.handleClearConfirmation(data, adapterId, this.adapter ?? undefined))
@@ -3135,6 +3141,8 @@ export class FleetManager {
3135
3141
  return;
3136
3142
  if (await this.handleLoginConfirm(data, adapterId, adapter))
3137
3143
  return;
3144
+ if (await this.handleLoginTokenResend(data, adapterId, adapter))
3145
+ return;
3138
3146
  if (await this.handleInstallLoginConfirm(data, adapterId, adapter))
3139
3147
  return;
3140
3148
  if (await this.handleClearConfirmation(data, adapterId, adapter))
@@ -6859,6 +6867,40 @@ export class FleetManager {
6859
6867
  // repairs every instance of that backend, and instance delivery, pane-state
6860
6868
  // detection, tool progress, and mcp_proxy_reply never observe login output.
6861
6869
  activeLogin = null;
6870
+ /**
6871
+ * Web-terminal login (v2.1.5 default, `login.mode: web`). The relay code
6872
+ * below (activeLogin/LoginSession) is `login.mode: relay`, kept for one
6873
+ * release as the rollback lever and removed in 2.1.6.
6874
+ */
6875
+ loginController = null;
6876
+ /**
6877
+ * One login/install window fleet-wide. Claimed synchronously before the
6878
+ * first await by web login, relay login and install alike (sol B1).
6879
+ */
6880
+ loginWindow = new LoginWindowLock();
6881
+ get webLogin() {
6882
+ if (!this.loginController) {
6883
+ this.loginController = new LoginController({
6884
+ logger: this.logger,
6885
+ fleetConfig: () => this.fleetConfig,
6886
+ isFleetAdmin: (userId, adapterId) => this.isFleetAdmin(userId, adapterId),
6887
+ eventLog: () => this.eventLog,
6888
+ recoverBackendInstances: backend => this.recoverBackendInstances(backend),
6889
+ claimWindow: backend => this.loginWindow.tryClaim("web", backend),
6890
+ releaseWindow: claim => { this.loginWindow.release(claim); },
6891
+ isClaimCurrent: claim => this.loginWindow.isCurrent(claim),
6892
+ windowBusyMessage: () => this.loginWindow.busyMessage(),
6893
+ postButtons: async ({ prefix, instanceName, chat, message, choices, expiredText }) => {
6894
+ await this.postNonceButtonPrompt({
6895
+ prefix, alertType: "login", instanceName,
6896
+ adapter: chat.adapter, adapterId: chat.adapterId, chatId: chat.chatId, threadId: chat.threadId,
6897
+ message, choices, expiredText,
6898
+ });
6899
+ },
6900
+ });
6901
+ }
6902
+ return this.loginController;
6903
+ }
6862
6904
  /** Post the backend chooser for a bare `/login`. Caller enforces admin. */
6863
6905
  async promptLoginBackends(chat) {
6864
6906
  const configured = new Set();
@@ -7034,21 +7076,40 @@ export class FleetManager {
7034
7076
  * posted instead (auth still valid — see the pre-check below).
7035
7077
  */
7036
7078
  async startLoginSession(backendArg, chat, opts = {}) {
7079
+ if (this.webLogin.mode() === "web")
7080
+ return this.webLogin.start(backendArg, chat, opts);
7081
+ // ── legacy relay mode (login.mode: relay) — removed in 2.1.6 ──
7037
7082
  const backend = LOGIN_BACKEND_ALIASES[backendArg.toLowerCase()] ?? backendArg.toLowerCase();
7038
7083
  const flow = LOGIN_FLOWS[backend];
7039
7084
  if (!flow)
7040
7085
  return t("login.unsupported", backendArg);
7041
- if (this.activeLogin)
7042
- return t("login.busy", this.activeLogin.backend);
7043
- // Install sessions share the window namespace — never run both at once.
7044
- if (this.activeInstall)
7045
- return t("install.busy");
7086
+ // Declined in every mode — never a silent relay fallback (e.g. Antigravity).
7087
+ if (flow.remoteLogin === "unsupported")
7088
+ return t("login.remote_unsupported_agent_cli", backend, flow.command);
7089
+ // Reserve the window before the pre-check await. The claim is owned by this
7090
+ // region until it is transferred to launchLoginSession; any other exit
7091
+ // (buttons only, throw, shutdown) releases it.
7092
+ const claim = this.loginWindow.tryClaim("relay", backend);
7093
+ if (!claim)
7094
+ return this.loginWindow.busyMessage();
7095
+ let transferred = false;
7096
+ try {
7097
+ return await this.startRelayClaimed(flow, backend, chat, opts, claim, () => { transferred = true; });
7098
+ }
7099
+ finally {
7100
+ if (!transferred)
7101
+ this.loginWindow.release(claim);
7102
+ }
7103
+ }
7104
+ async startRelayClaimed(flow, backend, chat, opts, claim, markTransferred) {
7046
7105
  // Token-free pre-check (5s cap): re-login while auth still works is
7047
7106
  // usually a mistake, so it needs a confirmed click. An invalid OR
7048
7107
  // uncertain result (timeout, missing binary) proceeds straight to login —
7049
7108
  // an unreliable probe must never block the re-login the admin asked for.
7050
7109
  if (!opts.skipAuthCheck && flow.authCheck) {
7051
7110
  const status = await checkAuthStatus(flow.authCheck);
7111
+ if (!this.loginWindow.isCurrent(claim))
7112
+ return t("login.web_shutting_down"); // fleet shut down while we probed
7052
7113
  if (status === "valid") {
7053
7114
  await this.postNonceButtonPrompt({
7054
7115
  prefix: LOGIN_CONFIRM_CALLBACK_PREFIX,
@@ -7068,18 +7129,27 @@ export class FleetManager {
7068
7129
  return null;
7069
7130
  }
7070
7131
  }
7071
- return this.launchLoginSession(flow, backend, chat);
7132
+ markTransferred(); // launchLoginSession owns the claim from here
7133
+ return this.launchLoginSession(flow, backend, chat, claim);
7072
7134
  }
7073
7135
  /** Create the login window and session (pre-check already settled). */
7074
- async launchLoginSession(flow, backend, chat) {
7075
- if (this.activeLogin)
7076
- return t("login.busy", this.activeLogin.backend);
7077
- // Install sessions share the window namespace — never run both at once.
7078
- if (this.activeInstall)
7079
- return t("install.busy");
7080
- const sessionName = getTmuxSession();
7081
- await TmuxManager.ensureSession(sessionName);
7082
- const tmux = new TmuxManager(sessionName, "");
7136
+ async launchLoginSession(flow, backend, chat, claim) {
7137
+ // Owns `claim`: released on any failure before the session is published,
7138
+ // and by onDone afterwards. A shutdown during ensureSession stops us.
7139
+ let tmux;
7140
+ try {
7141
+ const sessionName = getTmuxSession();
7142
+ await TmuxManager.ensureSession(sessionName);
7143
+ if (!this.loginWindow.isCurrent(claim)) {
7144
+ this.loginWindow.release(claim);
7145
+ return t("login.web_shutting_down");
7146
+ }
7147
+ tmux = new TmuxManager(sessionName, "");
7148
+ }
7149
+ catch (err) {
7150
+ this.loginWindow.release(claim);
7151
+ return t("login.failed", backend, err.message);
7152
+ }
7083
7153
  const session = new LoginSession(flow, tmux, {
7084
7154
  onMenu: async (options) => {
7085
7155
  await this.postNonceButtonPrompt({
@@ -7101,8 +7171,12 @@ export class FleetManager {
7101
7171
  onNeedInput: async (promptExcerpt) => {
7102
7172
  await chat.adapter.sendText(chat.chatId, t("login.need_input", backend, promptExcerpt), { threadId: chat.threadId }).catch(() => { });
7103
7173
  },
7104
- onDone: async ({ ok, detail }) => {
7174
+ onDone: async ({ ok, detail, cleanupFailed }) => {
7105
7175
  this.activeLogin = null;
7176
+ this.loginWindow.release(claim);
7177
+ if (cleanupFailed) {
7178
+ await chat.adapter.sendText(chat.chatId, t("login.web_cleanup_failed", backend), { threadId: chat.threadId }).catch(() => { });
7179
+ }
7106
7180
  let text;
7107
7181
  if (ok) {
7108
7182
  const { woken, restarted } = await this.recoverBackendInstances(backend);
@@ -7128,12 +7202,68 @@ export class FleetManager {
7128
7202
  }
7129
7203
  catch (err) {
7130
7204
  this.activeLogin = null;
7131
- return t("login.failed", backend, err.message);
7205
+ this.loginWindow.release(claim);
7206
+ const text = t("login.failed", backend, err.message);
7207
+ return err.cleanupFailed ? `${text}\n${t("login.web_cleanup_failed", backend)}` : text;
7208
+ }
7209
+ if (session.state === "done") {
7210
+ // Cancelled (user or shutdown) while starting: start() joined the
7211
+ // teardown, so nothing is left. onDone already released the claim.
7212
+ this.activeLogin = null;
7213
+ this.loginWindow.release(claim);
7214
+ return this.loginWindow.isClosed ? t("login.web_shutting_down") : t("login.cancelled", backend);
7215
+ }
7216
+ if (!this.loginWindow.isCurrent(claim)) {
7217
+ await session.cancel("cancelled").catch(() => { });
7218
+ this.activeLogin = null;
7219
+ this.loginWindow.release(claim);
7220
+ return t("login.web_shutting_down");
7132
7221
  }
7133
7222
  return t("login.started", backend);
7134
7223
  }
7224
+ /** Fleet shutdown: end any web/relay login or install window and wait for its confirmed teardown. */
7225
+ async shutdownLoginWindows() {
7226
+ // Close the lock FIRST: in-flight starts parked in a pre-check or
7227
+ // ensureSession observe !isCurrent when they resume and stop; no new
7228
+ // window can be claimed while we stop.
7229
+ this.loginWindow.close();
7230
+ // Completion semantics live INSIDE the sessions: each awaits its own
7231
+ // confirmed cleanup, every tmux op on that path has a hard per-op bound
7232
+ // (web: abort ≤10 s + kill ≤31 s; legacy: create/list/kill ≤10 s each,
7233
+ // duplicates killed in parallel). This outer deadline is only a loud last
7234
+ // resort so `agend stop` cannot hang forever on a wedged tmux; reaching it
7235
+ // is an ERROR, logged and recorded, and it releases nothing re-claimable
7236
+ // (the lock stays closed, the controller keeps its entry). The timer is
7237
+ // cleared on success so a long-lived process never fires it spuriously.
7238
+ const SHUTDOWN_DEADLINE_MS = 120_000;
7239
+ const bounded = (p, what) => {
7240
+ if (!p)
7241
+ return Promise.resolve();
7242
+ let timer;
7243
+ const deadline = new Promise(r => {
7244
+ timer = setTimeout(() => {
7245
+ this.logger.error({ what, deadlineMs: SHUTDOWN_DEADLINE_MS }, "login window teardown still in flight at the shutdown deadline — a dedicated tmux server may survive; check `tmux -L agend-term-* ls`");
7246
+ this.eventLog?.insert("login", "login_window_shutdown_deadline", { what });
7247
+ r();
7248
+ }, SHUTDOWN_DEADLINE_MS);
7249
+ timer.unref?.();
7250
+ });
7251
+ return Promise.race([p.then(() => undefined, () => this.logger.warn({ what }, "login window shutdown failed")), deadline])
7252
+ .finally(() => { if (timer)
7253
+ clearTimeout(timer); });
7254
+ };
7255
+ // "cancelled" is the detail both legacy onDone handlers keep quiet about —
7256
+ // a stopping fleet must not announce "login failed — fleet shutdown".
7257
+ await Promise.all([
7258
+ bounded(this.loginController?.shutdown(), "web-login"),
7259
+ bounded(this.activeLogin?.session.cancel("cancelled"), "relay-login"),
7260
+ bounded(this.activeInstall?.session.cancel("cancelled"), "install"),
7261
+ ]);
7262
+ }
7135
7263
  /** `/login code <text>` — paste admin-supplied text into the login window. */
7136
7264
  async loginSubmitInput(text) {
7265
+ if (this.loginController?.isActive())
7266
+ return t("login.web_code_not_needed");
7137
7267
  if (!this.activeLogin)
7138
7268
  return t("login.no_session");
7139
7269
  const ok = await this.activeLogin.session.submitInput(text);
@@ -7141,6 +7271,8 @@ export class FleetManager {
7141
7271
  }
7142
7272
  /** `/login cancel` — abort the active session and remove its window. */
7143
7273
  async cancelLoginSession() {
7274
+ if (this.loginController?.isActive())
7275
+ return this.loginController.cancel();
7144
7276
  if (!this.activeLogin)
7145
7277
  return t("login.no_session");
7146
7278
  const backend = this.activeLogin.backend;
@@ -7214,6 +7346,7 @@ export class FleetManager {
7214
7346
  adapterId: entry.adapterId,
7215
7347
  chatId: entry.chatId,
7216
7348
  threadId: entry.threadId,
7349
+ userId: data.userId,
7217
7350
  });
7218
7351
  if (text)
7219
7352
  await entry.adapter.sendText(entry.chatId, text, { threadId: entry.threadId }).catch(() => { });
@@ -7221,7 +7354,7 @@ export class FleetManager {
7221
7354
  }
7222
7355
  /** Re-login confirmation (auth pre-check said credentials still work). */
7223
7356
  async handleLoginConfirm(data, callbackAdapterId, receivingAdapter) {
7224
- const claimed = this.consumeNonceCallback(LOGIN_CONFIRM_CALLBACK_PREFIX, /^login-confirm:([0-9a-f]+):(go|cancel)$/, data, callbackAdapterId, receivingAdapter);
7357
+ const claimed = this.consumeNonceCallback(LOGIN_CONFIRM_CALLBACK_PREFIX, /^login-confirm:([0-9a-f]+):(go|go-relogin|cancel)$/, data, callbackAdapterId, receivingAdapter);
7225
7358
  if (claimed === null)
7226
7359
  return false;
7227
7360
  if (claimed === "consumed")
@@ -7238,11 +7371,24 @@ export class FleetManager {
7238
7371
  adapterId: entry.adapterId,
7239
7372
  chatId: entry.chatId,
7240
7373
  threadId: entry.threadId,
7241
- }, { skipAuthCheck: true });
7374
+ userId: data.userId,
7375
+ }, { skipAuthCheck: true, tokenPresent: action === "go-relogin" });
7242
7376
  if (text)
7243
7377
  await entry.adapter.sendText(entry.chatId, text, { threadId: entry.threadId }).catch(() => { });
7244
7378
  return true;
7245
7379
  }
7380
+ /** "Resend token" button (web login): only the requester may press it; the token never enters the channel. */
7381
+ async handleLoginTokenResend(data, callbackAdapterId, receivingAdapter) {
7382
+ const claimed = this.consumeNonceCallback(LOGIN_TOKEN_RESEND_PREFIX, /^login-token:([0-9a-f]+):(resend)$/, data, callbackAdapterId, receivingAdapter);
7383
+ if (claimed === null)
7384
+ return false;
7385
+ if (claimed === "consumed")
7386
+ return true;
7387
+ const { entry } = claimed;
7388
+ const text = await this.webLogin.resendToken(data.userId);
7389
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, text);
7390
+ return true;
7391
+ }
7246
7392
  /** Kiro provider button → drive the CLI's arrow-key selector. */
7247
7393
  async handleLoginMenuSelect(data, callbackAdapterId, receivingAdapter) {
7248
7394
  const claimed = this.consumeNonceCallback(LOGIN_MENU_CALLBACK_PREFIX, /^login-menu:([0-9a-f]+):(\d)$/, data, callbackAdapterId, receivingAdapter);
@@ -7272,13 +7418,25 @@ export class FleetManager {
7272
7418
  return t("install.unsupported", backendArg);
7273
7419
  if (checkBinaryInstalled(info.binary))
7274
7420
  return t("install.already", backend, info.binary);
7275
- if (this.activeInstall)
7276
- return t("install.busy");
7277
- if (this.activeLogin)
7278
- return t("login.busy", this.activeLogin.backend);
7279
- const sessionName = getTmuxSession();
7280
- await TmuxManager.ensureSession(sessionName);
7281
- const tmux = new TmuxManager(sessionName, "");
7421
+ // Reserve the fleet-wide window before the first await (shared with web/relay
7422
+ // login). Owned by this method until the session is published.
7423
+ const claim = this.loginWindow.tryClaim("install", backend);
7424
+ if (!claim)
7425
+ return this.loginWindow.busyMessage();
7426
+ let tmux;
7427
+ try {
7428
+ const sessionName = getTmuxSession();
7429
+ await TmuxManager.ensureSession(sessionName);
7430
+ if (!this.loginWindow.isCurrent(claim)) {
7431
+ this.loginWindow.release(claim);
7432
+ return t("login.web_shutting_down");
7433
+ }
7434
+ tmux = new TmuxManager(sessionName, "");
7435
+ }
7436
+ catch (err) {
7437
+ this.loginWindow.release(claim);
7438
+ return t("install.failed", backend, err.message);
7439
+ }
7282
7440
  // A synthetic login flow: success is decided by the installer's exit code
7283
7441
  // (LoginSession treats a clean exit as success), never by pane text — and
7284
7442
  // installer output that happens to contain a URL must not be forwarded as
@@ -7293,8 +7451,12 @@ export class FleetManager {
7293
7451
  onMenu: () => { },
7294
7452
  onAuthHint: () => { },
7295
7453
  onNeedInput: () => { },
7296
- onDone: async ({ ok, detail }) => {
7454
+ onDone: async ({ ok, detail, cleanupFailed }) => {
7297
7455
  this.activeInstall = null;
7456
+ this.loginWindow.release(claim);
7457
+ if (cleanupFailed) {
7458
+ await chat.adapter.sendText(chat.chatId, t("login.web_cleanup_failed", backend), { threadId: chat.threadId }).catch(() => { });
7459
+ }
7298
7460
  if (!ok) {
7299
7461
  // A cancel is user-initiated — the cancel command's own reply already
7300
7462
  // said so; a second message here would be a duplicate.
@@ -7336,7 +7498,20 @@ export class FleetManager {
7336
7498
  }
7337
7499
  catch (err) {
7338
7500
  this.activeInstall = null;
7339
- return t("install.failed", backend, err.message);
7501
+ this.loginWindow.release(claim);
7502
+ const text = t("install.failed", backend, err.message);
7503
+ return err.cleanupFailed ? `${text}\n${t("login.web_cleanup_failed", backend)}` : text;
7504
+ }
7505
+ if (session.state === "done") {
7506
+ this.activeInstall = null;
7507
+ this.loginWindow.release(claim);
7508
+ return this.loginWindow.isClosed ? t("login.web_shutting_down") : t("install.cancelled", backend);
7509
+ }
7510
+ if (!this.loginWindow.isCurrent(claim)) {
7511
+ await session.cancel("cancelled").catch(() => { });
7512
+ this.activeInstall = null;
7513
+ this.loginWindow.release(claim);
7514
+ return t("login.web_shutting_down");
7340
7515
  }
7341
7516
  return t("install.started", backend);
7342
7517
  }
@@ -7377,6 +7552,7 @@ export class FleetManager {
7377
7552
  adapterId: entry.adapterId,
7378
7553
  chatId: entry.chatId,
7379
7554
  threadId: entry.threadId,
7555
+ userId: data.userId,
7380
7556
  });
7381
7557
  if (text)
7382
7558
  await entry.adapter.sendText(entry.chatId, text, { threadId: entry.threadId }).catch(() => { });
@@ -7388,7 +7564,7 @@ export class FleetManager {
7388
7564
  await data.respond(t("permission.denied"));
7389
7565
  return;
7390
7566
  }
7391
- const chat = { adapter, adapterId, chatId: data.channelId };
7567
+ const chat = { adapter, adapterId, chatId: data.channelId, userId: data.userId };
7392
7568
  if (data.options?.cancel === true) {
7393
7569
  await data.respond(await this.cancelLoginSession());
7394
7570
  return;
@@ -9255,6 +9431,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
9255
9431
  clearInterval(this.watchdogTimer);
9256
9432
  this.watchdogTimer = null;
9257
9433
  }
9434
+ // A login/install window is a dedicated tmux server with its own TTL
9435
+ // timer and HTTP listener living in THIS process: without an explicit
9436
+ // shutdown it would outlive us as an owner-less login CLI (sol B3).
9437
+ await this.shutdownLoginWindows();
9258
9438
  // Cancel adapter retry timers
9259
9439
  for (const state of this.adapterState.values()) {
9260
9440
  if (state.retryTimer) {