aicodeman 1.30.0 → 1.31.0

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 (173) hide show
  1. package/dist/config/cli-registry/schema.d.ts +8 -0
  2. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  3. package/dist/config/cli-registry/schema.js +29 -0
  4. package/dist/config/cli-registry/schema.js.map +1 -1
  5. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  6. package/dist/config/cli-registry/stock.js +62 -7
  7. package/dist/config/cli-registry/stock.js.map +1 -1
  8. package/dist/config/cli-registry/types.d.ts +62 -0
  9. package/dist/config/cli-registry/types.d.ts.map +1 -1
  10. package/dist/config/remote-wake-limits.d.ts +23 -0
  11. package/dist/config/remote-wake-limits.d.ts.map +1 -0
  12. package/dist/config/remote-wake-limits.js +23 -0
  13. package/dist/config/remote-wake-limits.js.map +1 -0
  14. package/dist/custom-model-hosts.d.ts +32 -0
  15. package/dist/custom-model-hosts.d.ts.map +1 -1
  16. package/dist/custom-model-hosts.js.map +1 -1
  17. package/dist/custom-model-injection-apply.d.ts +29 -1
  18. package/dist/custom-model-injection-apply.d.ts.map +1 -1
  19. package/dist/custom-model-injection-apply.js +191 -5
  20. package/dist/custom-model-injection-apply.js.map +1 -1
  21. package/dist/custom-model-injection.d.ts +35 -9
  22. package/dist/custom-model-injection.d.ts.map +1 -1
  23. package/dist/custom-model-injection.js +37 -16
  24. package/dist/custom-model-injection.js.map +1 -1
  25. package/dist/mux-interface.d.ts +12 -0
  26. package/dist/mux-interface.d.ts.map +1 -1
  27. package/dist/remote-hosts.d.ts +20 -0
  28. package/dist/remote-hosts.d.ts.map +1 -1
  29. package/dist/remote-hosts.js +33 -0
  30. package/dist/remote-hosts.js.map +1 -1
  31. package/dist/remote-wake.d.ts +446 -0
  32. package/dist/remote-wake.d.ts.map +1 -0
  33. package/dist/remote-wake.js +880 -0
  34. package/dist/remote-wake.js.map +1 -0
  35. package/dist/session-env-clamp.d.ts +12 -7
  36. package/dist/session-env-clamp.d.ts.map +1 -1
  37. package/dist/session-env-clamp.js +12 -7
  38. package/dist/session-env-clamp.js.map +1 -1
  39. package/dist/session-submit-verifier.d.ts +44 -0
  40. package/dist/session-submit-verifier.d.ts.map +1 -0
  41. package/dist/session-submit-verifier.js +122 -0
  42. package/dist/session-submit-verifier.js.map +1 -0
  43. package/dist/session.d.ts +9 -0
  44. package/dist/session.d.ts.map +1 -1
  45. package/dist/session.js +42 -1
  46. package/dist/session.js.map +1 -1
  47. package/dist/tmux-manager.d.ts.map +1 -1
  48. package/dist/tmux-manager.js +9 -0
  49. package/dist/tmux-manager.js.map +1 -1
  50. package/dist/types/session.d.ts +26 -0
  51. package/dist/types/session.d.ts.map +1 -1
  52. package/dist/types/session.js.map +1 -1
  53. package/dist/web/public/admin-ui.js.gz +0 -0
  54. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  55. package/dist/web/public/app.8df358cd.js +38 -0
  56. package/dist/web/public/app.8df358cd.js.br +0 -0
  57. package/dist/web/public/app.8df358cd.js.gz +0 -0
  58. package/dist/web/public/approvals-ui.js.gz +0 -0
  59. package/dist/web/public/{constants.258b140f.js → constants.85ed12f3.js} +74 -0
  60. package/dist/web/public/constants.85ed12f3.js.br +0 -0
  61. package/dist/web/public/constants.85ed12f3.js.gz +0 -0
  62. package/dist/web/public/cron-ui.js.gz +0 -0
  63. package/dist/web/public/entrance-animations.js.gz +0 -0
  64. package/dist/web/public/home-sessions.js.gz +0 -0
  65. package/dist/web/public/host-wake-ui.js +440 -0
  66. package/dist/web/public/host-wake-ui.js.br +0 -0
  67. package/dist/web/public/host-wake-ui.js.gz +0 -0
  68. package/dist/web/public/i18n.83924614.js +1 -0
  69. package/dist/web/public/i18n.83924614.js.br +0 -0
  70. package/dist/web/public/i18n.83924614.js.gz +0 -0
  71. package/dist/web/public/image-input.cd4b97c4.js.gz +0 -0
  72. package/dist/web/public/index.html +186 -10
  73. package/dist/web/public/index.html.br +0 -0
  74. package/dist/web/public/index.html.gz +0 -0
  75. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  76. package/dist/web/public/keyboard-accessory.2cf04f17.js.gz +0 -0
  77. package/dist/web/public/mobile-handlers.6f354a87.js.gz +0 -0
  78. package/dist/web/public/mobile-overview.js.gz +0 -0
  79. package/dist/web/public/mobile.6caaa28a.css.gz +0 -0
  80. package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
  81. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  82. package/dist/web/public/{panels-ui.5b07ad14.js → panels-ui.28df5053.js} +5 -5
  83. package/dist/web/public/panels-ui.28df5053.js.br +0 -0
  84. package/dist/web/public/panels-ui.28df5053.js.gz +0 -0
  85. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  86. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  87. package/dist/web/public/readmymind-ui.js.gz +0 -0
  88. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  89. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  90. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  91. package/dist/web/public/session-lineage.js.gz +0 -0
  92. package/dist/web/public/session-ui.d4aea71a.js +88 -0
  93. package/dist/web/public/session-ui.d4aea71a.js.br +0 -0
  94. package/dist/web/public/session-ui.d4aea71a.js.gz +0 -0
  95. package/dist/web/public/settings-ui.4762f480.js +78 -0
  96. package/dist/web/public/settings-ui.4762f480.js.br +0 -0
  97. package/dist/web/public/settings-ui.4762f480.js.gz +0 -0
  98. package/dist/web/public/styles.e34fea04.css +1 -0
  99. package/dist/web/public/styles.e34fea04.css.br +0 -0
  100. package/dist/web/public/styles.e34fea04.css.gz +0 -0
  101. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  102. package/dist/web/public/sw.js.gz +0 -0
  103. package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
  104. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  105. package/dist/web/public/terminal-ui.35812aa2.js +2 -0
  106. package/dist/web/public/terminal-ui.35812aa2.js.br +0 -0
  107. package/dist/web/public/terminal-ui.35812aa2.js.gz +0 -0
  108. package/dist/web/public/ultracode-panel.js.gz +0 -0
  109. package/dist/web/public/ultracode-windows.js.gz +0 -0
  110. package/dist/web/public/upload.html.gz +0 -0
  111. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  112. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  113. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  114. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  115. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  116. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  117. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  118. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  119. package/dist/web/public/vendor/xterm.css.gz +0 -0
  120. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  121. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  122. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  123. package/dist/web/public/webview-tabs.js.gz +0 -0
  124. package/dist/web/routes/custom-model-routes.d.ts +173 -0
  125. package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
  126. package/dist/web/routes/custom-model-routes.js +628 -11
  127. package/dist/web/routes/custom-model-routes.js.map +1 -1
  128. package/dist/web/routes/index.d.ts +1 -1
  129. package/dist/web/routes/index.d.ts.map +1 -1
  130. package/dist/web/routes/index.js +1 -1
  131. package/dist/web/routes/index.js.map +1 -1
  132. package/dist/web/routes/session-routes.d.ts +6 -1
  133. package/dist/web/routes/session-routes.d.ts.map +1 -1
  134. package/dist/web/routes/session-routes.js +448 -17
  135. package/dist/web/routes/session-routes.js.map +1 -1
  136. package/dist/web/schemas.d.ts +15 -0
  137. package/dist/web/schemas.d.ts.map +1 -1
  138. package/dist/web/schemas.js +76 -0
  139. package/dist/web/schemas.js.map +1 -1
  140. package/dist/web/server.d.ts +21 -0
  141. package/dist/web/server.d.ts.map +1 -1
  142. package/dist/web/server.js +130 -5
  143. package/dist/web/server.js.map +1 -1
  144. package/dist/web/sse-events.d.ts +25 -2
  145. package/dist/web/sse-events.d.ts.map +1 -1
  146. package/dist/web/sse-events.js +30 -3
  147. package/dist/web/sse-events.js.map +1 -1
  148. package/package.json +1 -1
  149. package/skills/codeman/SKILL.md +75 -26
  150. package/skills/codeman/preamble.sh +66 -19
  151. package/skills/codeman/reference/recipes.md +1 -1
  152. package/dist/web/public/app.6d2dc4e8.js +0 -38
  153. package/dist/web/public/app.6d2dc4e8.js.br +0 -0
  154. package/dist/web/public/app.6d2dc4e8.js.gz +0 -0
  155. package/dist/web/public/constants.258b140f.js.br +0 -0
  156. package/dist/web/public/constants.258b140f.js.gz +0 -0
  157. package/dist/web/public/i18n.5f897ed5.js +0 -1
  158. package/dist/web/public/i18n.5f897ed5.js.br +0 -0
  159. package/dist/web/public/i18n.5f897ed5.js.gz +0 -0
  160. package/dist/web/public/panels-ui.5b07ad14.js.br +0 -0
  161. package/dist/web/public/panels-ui.5b07ad14.js.gz +0 -0
  162. package/dist/web/public/session-ui.42b81477.js +0 -77
  163. package/dist/web/public/session-ui.42b81477.js.br +0 -0
  164. package/dist/web/public/session-ui.42b81477.js.gz +0 -0
  165. package/dist/web/public/settings-ui.58f1d756.js +0 -67
  166. package/dist/web/public/settings-ui.58f1d756.js.br +0 -0
  167. package/dist/web/public/settings-ui.58f1d756.js.gz +0 -0
  168. package/dist/web/public/styles.e6edfb0b.css +0 -1
  169. package/dist/web/public/styles.e6edfb0b.css.br +0 -0
  170. package/dist/web/public/styles.e6edfb0b.css.gz +0 -0
  171. package/dist/web/public/terminal-ui.4f8d5820.js +0 -2
  172. package/dist/web/public/terminal-ui.4f8d5820.js.br +0 -0
  173. package/dist/web/public/terminal-ui.4f8d5820.js.gz +0 -0
@@ -42,6 +42,7 @@ import { hostname as getHostname, uptime as osUptime } from 'node:os';
42
42
  import { looksLikeHostReboot, newestPersistedActivity, planRebootRestore } from '../reboot-restore.js';
43
43
  import { rebootRestoreRegistry } from './reboot-restore-registry.js';
44
44
  import { dataPath, getDataDir, CODEMAN_INSTANCE } from '../config/instance.js';
45
+ import { readRemoteHosts, rehydrateRemoteHostFields } from '../remote-hosts.js';
45
46
  import { normalizeBasePath, stripBasePath, joinBasePath } from '../config/base-path.js';
46
47
  import { GLYPH, palette } from '../cli-style.js';
47
48
  import { getHookSecret } from '../config/hook-secret.js';
@@ -58,7 +59,7 @@ import { subagentWatcher, } from '../subagent-watcher.js';
58
59
  import { imageWatcher } from '../image-watcher.js';
59
60
  import { workflowRunWatcher, summarizeRun } from '../workflow-run-watcher.js';
60
61
  import { attachmentRegistry, buildFileThumbnailRoute, registerExternalAttachment } from '../attachment-registry.js';
61
- import { getCli } from '../config/cli-registry/registry.js';
62
+ import { getCli, enabledClis } from '../config/cli-registry/registry.js';
62
63
  import { readCustomModelHosts } from '../custom-model-hosts.js';
63
64
  import { applyCustomModelInjection, customModelConfigDir, removeConfigDir } from '../custom-model-injection-apply.js';
64
65
  import { registerGeneratedArtifactAttachment } from '../generated-artifact-attachments.js';
@@ -113,7 +114,7 @@ import { isMultiUserMode } from '../config/multiuser.js';
113
114
  import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '../user-store.js';
114
115
  import { installRouteErrorHandler } from './route-error-handler.js';
115
116
  import { isExplicitlyEnabled, isLoopbackBindHost, buildHostPolicy } from './network-auth-policy.js';
116
- import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerCustomModelRoutes, tryWebviewRefererFallback, } from './routes/index.js';
117
+ import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerCustomModelRoutes, refreshAllCustomModelHosts, readCustomModelEndpointsEnabled, closeAllLlamaSwapLogTails, detectCustomModelSwapDisplacements, pruneIdleLlamaSwapLogTails, tryWebviewRefererFallback, } from './routes/index.js';
117
118
  import { isLostWebviewFrameNavigation } from './webview-proxy.js';
118
119
  import { CronService } from '../cron/cron-service.js';
119
120
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -122,9 +123,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
122
123
  // while capping growth of `sseClientsById` and blocking pathological inputs.
123
124
  const SSE_CLIENT_ID_RE = /^[A-Za-z0-9_-]{8,64}$/;
124
125
  const CODEX_USAGE_POLL_INTERVAL_MS = 5 * 60_000;
126
+ const CUSTOM_MODEL_REDISCOVER_INTERVAL_MS = 5 * 60_000;
127
+ // Much shorter than the model-LIST refresh above on purpose: this catches an actual
128
+ // eviction (a session's model no longer loaded, silently swapped out by another
129
+ // session's use), which the user wants to know about promptly, not once every 5
130
+ // minutes. Cheap either way — one /running GET per distinct endpoint with at least
131
+ // one live custom-model session, not per session.
132
+ const CUSTOM_MODEL_SWAP_CHECK_INTERVAL_MS = 20_000;
125
133
  function escapeHtmlText(value) {
126
134
  return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
127
135
  }
136
+ /**
137
+ * Escapes a JSON string for safe embedding as the body of an inline `<script>`
138
+ * tag: `<` becomes the six-character sequence `<`, which both a JSON
139
+ * parser and a plain JS string literal decode back to `<` (both treat
140
+ * `\uXXXX` identically), but which can never itself form the two literal
141
+ * characters `<` `/` a browser's HTML tokenizer looks for to end the tag. A
142
+ * value containing a literal `</script>` would otherwise close the tag early
143
+ * and turn the rest of the document into inert script-body text. Exported so
144
+ * it unit-tests without constructing a WebServer (which needs a real tmux).
145
+ */
146
+ export function escapeScriptJson(json) {
147
+ return json.replace(/</g, '\\u003c');
148
+ }
128
149
  import { SESSIONS_LIST_CACHE_TTL, SCHEDULED_CLEANUP_INTERVAL, SCHEDULED_RUN_MAX_AGE, SSE_HEARTBEAT_INTERVAL, SESSION_LIMIT_WAIT_MS, ITERATION_PAUSE_MS, STATS_COLLECTION_INTERVAL_MS, INACTIVITY_TIMEOUT_MS, CRON_TICK_INTERVAL, } from '../config/server-timing.js';
129
150
  /**
130
151
  * Get or generate a self-signed TLS certificate for HTTPS.
@@ -163,8 +184,18 @@ export class WebServer extends EventEmitter {
163
184
  // Store session listener references for explicit cleanup (prevents memory leaks)
164
185
  sessionListenerRefs = new Map();
165
186
  scheduledRuns = new Map();
187
+ /** De-dupe state for the swap-displacement sweep — see detectCustomModelSwapDisplacements. */
188
+ _customModelDisplacedNotified = new Set();
166
189
  /** Cron service (assigned in setupRoutes). */
167
190
  cronService;
191
+ /**
192
+ * Wake-on-LAN registry, returned by `registerSessionRoutes`. Held for its LIFETIME
193
+ * only — `drop()` on session cleanup, `stop()` on shutdown. Waking from here would
194
+ * re-wake a host on every timer tick (the invariant `remote-wake.ts` documents), so
195
+ * the wiring guard in `test/remote-wake.test.ts` pins that this file calls nothing
196
+ * but `drop`/`stop` on it.
197
+ */
198
+ remoteWake = null;
168
199
  sse;
169
200
  store = getStore();
170
201
  tabLayouts;
@@ -902,7 +933,9 @@ export class WebServer extends EventEmitter {
902
933
  registerStatusTelemetryRoutes(this.app, ctx);
903
934
  registerSystemRoutes(this.app, ctx);
904
935
  registerCaseRoutes(this.app, ctx);
905
- registerSessionRoutes(this.app, ctx);
936
+ // The registry's lifetime is the server's: it drops per-session wake state on every
937
+ // cleanup path and resolves in-flight wakes on shutdown.
938
+ this.remoteWake = registerSessionRoutes(this.app, ctx);
906
939
  registerRespawnRoutes(this.app, ctx);
907
940
  registerRalphRoutes(this.app, ctx);
908
941
  registerPlanRoutes(this.app, ctx);
@@ -1027,7 +1060,7 @@ export class WebServer extends EventEmitter {
1027
1060
  return undefined;
1028
1061
  }
1029
1062
  try {
1030
- return applyCustomModelInjection(entry, endpoint, saved.modelId, session.id)?.envOverrides;
1063
+ return applyCustomModelInjection(entry, endpoint, saved.modelId, session.id, endpoint.modelContextLengths?.[saved.modelId])?.envOverrides;
1031
1064
  }
1032
1065
  catch (err) {
1033
1066
  console.warn('[WebServer] Failed to rebuild custom-model env on recovery:', err);
@@ -1112,6 +1145,9 @@ export class WebServer extends EventEmitter {
1112
1145
  if (session) {
1113
1146
  session.ralphTracker.stopWatchingFixPlan();
1114
1147
  }
1148
+ // Custom Model Endpoint Profiles: drop this session's swap-displacement notify flag
1149
+ // (see _checkCustomModelSwapDisplacements below) so it can't linger in that Set forever.
1150
+ this._customModelDisplacedNotified.delete(sessionId);
1115
1151
  // Kill all subagents spawned by this session (scoped to sessionId to avoid cross-session kills)
1116
1152
  if (session && killMux) {
1117
1153
  try {
@@ -1251,6 +1287,11 @@ export class WebServer extends EventEmitter {
1251
1287
  sessionWaits.notifySignal(sessionId, 'exit');
1252
1288
  sessionWaits.cancelAll(sessionId);
1253
1289
  approvalInbox.resolveForSession(sessionId, 'session_ended');
1290
+ // Wake state goes with the session on EVERY cleanup path (delete routes, the cron
1291
+ // and admin paths, scheduled-run teardown, error paths) — that is why it lives here
1292
+ // rather than in the two delete routes, where it left an entry behind, including up
1293
+ // to 4 KB of the user's buffered keystrokes.
1294
+ this.remoteWake?.drop(sessionId);
1254
1295
  this.broadcast(SseEvent.SessionDeleted, { id: sessionId });
1255
1296
  }
1256
1297
  async renderIndexHtml(soloSessionId) {
@@ -1368,6 +1409,20 @@ export class WebServer extends EventEmitter {
1368
1409
  git: isGitAvailable(),
1369
1410
  };
1370
1411
  html = html.replace('</head>', `<script>window.__codemanCliAvailable=${JSON.stringify(available)};</script>\n</head>`);
1412
+ // Which run modes the Run-menu picker (docs/custom-model-endpoints-plan.md) may
1413
+ // generate an entry for: read generically off the registry's `capabilities`
1414
+ // (never an id list here) so a CLI whose customModelInjection lands later shows
1415
+ // up in the picker with no frontend change, and one that ships `unsupported`
1416
+ // (antigravity, and `shell`'s `kind !== 'agent'`) never does.
1417
+ const customModelClis = enabledClis()
1418
+ .filter((entry) => entry.kind === 'agent' && entry.capabilities.customModelInjection.kind !== 'unsupported')
1419
+ .map((entry) => ({ id: entry.id, label: entry.label }));
1420
+ // Unlike the boolean-only __codemanCliAvailable above, this payload carries
1421
+ // `label`, a string a user's own clis.json can set (CliEntry.label, up to 60
1422
+ // chars) — see escapeScriptJson's own doc comment for why that needs escaping
1423
+ // and __codemanCliAvailable's booleans never did.
1424
+ const customModelClisJson = escapeScriptJson(JSON.stringify(customModelClis));
1425
+ html = html.replace('</head>', `<script>window.__codemanCustomModelClis=${customModelClisJson};</script>\n</head>`);
1371
1426
  }
1372
1427
  if (!soloSessionId && process.env.CODEMAN_GESTURE === '1') {
1373
1428
  html = html.replace('</head>', `<script>window.__codemanGestureAvailable=true;</script>\n</head>`);
@@ -2019,11 +2074,20 @@ export class WebServer extends EventEmitter {
2019
2074
  'scheduled:',
2020
2075
  'team:',
2021
2076
  'case:',
2077
+ 'remote:',
2078
+ 'custom-model:',
2022
2079
  ];
2023
2080
  if (SESSION_PREFIXES.some((p) => event.startsWith(p))) {
2024
2081
  const d = (data ?? {});
2025
2082
  const sessionId = d.sessionId ?? d.id ?? d.session?.id;
2026
2083
  const owner = sessionId ? this.sessions.get(sessionId)?.owner : undefined;
2084
+ // `remote:hostWaking` / `remote:hostWakeFailed` for a create/attach wake have no
2085
+ // session yet (nothing exists until the host is up), so the registry names the
2086
+ // requesting user instead; the payload carries `hostId`/`label`, which non-admins
2087
+ // are not shown elsewhere. No session and no requester: admins only (fail closed).
2088
+ if (!sessionId && event.startsWith('remote:') && d.username) {
2089
+ return { username: d.username, sessionScoped: true };
2090
+ }
2027
2091
  return { owner, sessionScoped: true };
2028
2092
  }
2029
2093
  // #20/#38: clipboard:write writes into the receiver's OS clipboard — route it to
@@ -2343,6 +2407,55 @@ export class WebServer extends EventEmitter {
2343
2407
  description: 'Codex plan-usage refresh',
2344
2408
  });
2345
2409
  }
2410
+ // Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md): keeps
2411
+ // each saved endpoint's discovered model list current with no manual
2412
+ // "Discover" click, so a model added on the server side (or one that drops
2413
+ // off) shows up in the Run-menu picker within one cycle. Best-effort per
2414
+ // endpoint (refreshAllCustomModelHosts skips one that's unreachable rather
2415
+ // than failing the sweep) and off in tests for the same reason the Codex
2416
+ // poll above is — no real network to hit, no server instance to keep alive.
2417
+ if (!this.testMode) {
2418
+ this.cleanup.setInterval(() => {
2419
+ // Reads the setting fresh on every tick, same reasoning as
2420
+ // readPlanUsageTelemetryEnabled() beside it: a live toggle takes effect
2421
+ // on the very next cycle, not just at server boot, and turning the
2422
+ // feature off actually stops the polling instead of only hiding the UI.
2423
+ void readCustomModelEndpointsEnabled()
2424
+ .then((enabled) => {
2425
+ if (!enabled)
2426
+ return;
2427
+ return refreshAllCustomModelHosts();
2428
+ })
2429
+ .catch((err) => {
2430
+ console.error('[custom-model] periodic re-discovery failed:', getErrorMessage(err));
2431
+ });
2432
+ }, CUSTOM_MODEL_REDISCOVER_INTERVAL_MS, { description: 'custom model endpoint re-discovery' });
2433
+ }
2434
+ // Custom Model Endpoint Profiles: the swap-conflict check on the apply/create routes
2435
+ // only ever runs at THAT session's own launch/apply moment — it cannot catch a LATER
2436
+ // eviction triggered by a different session's normal use, since llama-swap has no push
2437
+ // notification of its own and only swaps in response to a real inference request
2438
+ // (confirmed live: a session created while nothing else conflicted at that instant can
2439
+ // still get silently displaced afterward). This periodic sweep is what catches that
2440
+ // case after the fact and tells the displaced session's user, rather than leaving them
2441
+ // to discover it only when their next prompt behaves unexpectedly.
2442
+ if (!this.testMode) {
2443
+ this.cleanup.setInterval(() => {
2444
+ detectCustomModelSwapDisplacements(this.sessions.values(), this._customModelDisplacedNotified)
2445
+ .then((displacements) => {
2446
+ for (const displacement of displacements) {
2447
+ this.broadcast(SseEvent.CustomModelSwappedOut, displacement);
2448
+ }
2449
+ })
2450
+ .catch((err) => {
2451
+ console.error('[custom-model] swap-displacement check failed:', getErrorMessage(err));
2452
+ });
2453
+ // Same cadence, unrelated concern: close any /api/events tail (see
2454
+ // getLatestLlamaSwapLogLine) nothing has polled in a while, so a loading banner
2455
+ // that finished (or was abandoned) doesn't leave a connection open forever.
2456
+ pruneIdleLlamaSwapLogTails();
2457
+ }, CUSTOM_MODEL_SWAP_CHECK_INTERVAL_MS, { description: 'custom model swap-displacement check' });
2458
+ }
2346
2459
  // Start scheduled runs cleanup timer
2347
2460
  this.cleanup.setInterval(() => {
2348
2461
  this.cleanupScheduledRuns();
@@ -2718,6 +2831,9 @@ export class WebServer extends EventEmitter {
2718
2831
  console.log(`[Server] Found ${alive.length + discovered.length} alive mux session(s) from previous run`);
2719
2832
  // For each alive mux session, create a Session object if it doesn't exist
2720
2833
  const muxSessions = this.mux.getSessions();
2834
+ // Host-level config lives in remote-hosts.json, not in the persisted session
2835
+ // snapshot, so refresh the fields that only exist there (see the helper).
2836
+ const remoteHostsById = new Map((await readRemoteHosts(getDataDir())).map((host) => [host.id, host]));
2721
2837
  for (const muxSession of muxSessions) {
2722
2838
  if (!this.sessions.has(muxSession.sessionId)) {
2723
2839
  // Restore session settings from state.json (single source of truth)
@@ -2792,7 +2908,9 @@ export class WebServer extends EventEmitter {
2792
2908
  // respawn rebuilds a LOCAL command, breaking the pane and silently
2793
2909
  // erasing `remote` from state.json on the next persist. mux-sessions.json
2794
2910
  // round-trips MuxSession.remote; state.json carries SessionState.remote.
2795
- remote: muxSession.remote ?? savedState?.remote,
2911
+ // Host-level fields are refreshed from remote-hosts.json on top, or a
2912
+ // field added to the host config after launch would never arrive.
2913
+ remote: rehydrateRemoteHostFields(muxSession.remote ?? savedState?.remote, remoteHostsById),
2796
2914
  // Docker metadata round-trips the same way (mux-sessions.json carries
2797
2915
  // MuxSession.docker; state.json carries SessionState.docker), so recovery
2798
2916
  // rebuilds the `docker exec` launch instead of a broken local command.
@@ -3137,6 +3255,9 @@ export class WebServer extends EventEmitter {
3137
3255
  // port against the next start — the exact EADDRINUSE this feature already
3138
3256
  // got wrong once.
3139
3257
  void stopDeepSeekWeb();
3258
+ // Same teardown rule: the per-endpoint llama-swap log tails are otherwise closed
3259
+ // only by the periodic idle sweep, whose interval is disposed just below.
3260
+ closeAllLlamaSwapLogTails();
3140
3261
  // Dispose all managed timers (intervals + resettable timeouts)
3141
3262
  this.cleanup.dispose();
3142
3263
  // Gracefully close all SSE connections and clear batching state
@@ -3146,6 +3267,10 @@ export class WebServer extends EventEmitter {
3146
3267
  // response), so without this a 10-minute wait holds shutdown open.
3147
3268
  sessionWaits.cancelEverything();
3148
3269
  approvalInbox.stop();
3270
+ // Same reason as `cancelEverything` above: an in-flight wake is awaited by a request,
3271
+ // and `app.close()` (the last line of this method) does not abort in-flight requests —
3272
+ // so without this a restart during a wake waits out the readiness poll.
3273
+ this.remoteWake?.stop();
3149
3274
  this.lastRecordedTokens.clear();
3150
3275
  // Stop multiplexer and flush pending saves
3151
3276
  this.mux.destroy();