aicodeman 1.48.0 → 1.49.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 (269) hide show
  1. package/README.md +12 -3
  2. package/dist/config/cli-registry/schema.d.ts +1 -0
  3. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  4. package/dist/config/cli-registry/schema.js +5 -1
  5. package/dist/config/cli-registry/schema.js.map +1 -1
  6. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  7. package/dist/config/cli-registry/stock.js +71 -5
  8. package/dist/config/cli-registry/stock.js.map +1 -1
  9. package/dist/config/cli-registry/types.d.ts +4 -4
  10. package/dist/config/cli-registry/types.d.ts.map +1 -1
  11. package/dist/config/foreign-tmux.d.ts +48 -0
  12. package/dist/config/foreign-tmux.d.ts.map +1 -0
  13. package/dist/config/foreign-tmux.js +48 -0
  14. package/dist/config/foreign-tmux.js.map +1 -0
  15. package/dist/custom-model-injection-apply.d.ts +4 -2
  16. package/dist/custom-model-injection-apply.d.ts.map +1 -1
  17. package/dist/custom-model-injection-apply.js +35 -21
  18. package/dist/custom-model-injection-apply.js.map +1 -1
  19. package/dist/docker-hosts.d.ts +8 -1
  20. package/dist/docker-hosts.d.ts.map +1 -1
  21. package/dist/docker-hosts.js +5 -5
  22. package/dist/docker-hosts.js.map +1 -1
  23. package/dist/foreign-tmux-discovery.d.ts +140 -0
  24. package/dist/foreign-tmux-discovery.d.ts.map +1 -0
  25. package/dist/foreign-tmux-discovery.js +366 -0
  26. package/dist/foreign-tmux-discovery.js.map +1 -0
  27. package/dist/foreign-tmux.d.ts +381 -0
  28. package/dist/foreign-tmux.d.ts.map +1 -0
  29. package/dist/foreign-tmux.js +775 -0
  30. package/dist/foreign-tmux.js.map +1 -0
  31. package/dist/git-fetch-scheduler.d.ts +2 -0
  32. package/dist/git-fetch-scheduler.d.ts.map +1 -1
  33. package/dist/git-fetch-scheduler.js +1 -1
  34. package/dist/git-fetch-scheduler.js.map +1 -1
  35. package/dist/git-fetch.d.ts +1 -1
  36. package/dist/git-fetch.d.ts.map +1 -1
  37. package/dist/git-workspace-status.d.ts +2 -2
  38. package/dist/git-workspace-status.d.ts.map +1 -1
  39. package/dist/hooks-config.d.ts +2 -1
  40. package/dist/hooks-config.d.ts.map +1 -1
  41. package/dist/hooks-config.js +2 -1
  42. package/dist/hooks-config.js.map +1 -1
  43. package/dist/live-worktree.d.ts +53 -0
  44. package/dist/live-worktree.d.ts.map +1 -0
  45. package/dist/live-worktree.js +97 -0
  46. package/dist/live-worktree.js.map +1 -0
  47. package/dist/mux-interface.d.ts +12 -1
  48. package/dist/mux-interface.d.ts.map +1 -1
  49. package/dist/reboot-restore.d.ts +1 -1
  50. package/dist/reboot-restore.d.ts.map +1 -1
  51. package/dist/reboot-restore.js +9 -0
  52. package/dist/reboot-restore.js.map +1 -1
  53. package/dist/remote-files.d.ts +150 -5
  54. package/dist/remote-files.d.ts.map +1 -1
  55. package/dist/remote-files.js +505 -7
  56. package/dist/remote-files.js.map +1 -1
  57. package/dist/remote-hosts.d.ts +15 -9
  58. package/dist/remote-hosts.d.ts.map +1 -1
  59. package/dist/remote-hosts.js +42 -20
  60. package/dist/remote-hosts.js.map +1 -1
  61. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  62. package/dist/session-cli-registry-bridge.js +19 -8
  63. package/dist/session-cli-registry-bridge.js.map +1 -1
  64. package/dist/session-env-clamp.d.ts +1 -1
  65. package/dist/session-env-clamp.d.ts.map +1 -1
  66. package/dist/session-env-clamp.js +4 -3
  67. package/dist/session-env-clamp.js.map +1 -1
  68. package/dist/session.d.ts +91 -21
  69. package/dist/session.d.ts.map +1 -1
  70. package/dist/session.js +190 -33
  71. package/dist/session.js.map +1 -1
  72. package/dist/tmux-manager.d.ts +74 -12
  73. package/dist/tmux-manager.d.ts.map +1 -1
  74. package/dist/tmux-manager.js +256 -41
  75. package/dist/tmux-manager.js.map +1 -1
  76. package/dist/types/foreign-tmux.d.ts +101 -0
  77. package/dist/types/foreign-tmux.d.ts.map +1 -0
  78. package/dist/types/foreign-tmux.js +27 -0
  79. package/dist/types/foreign-tmux.js.map +1 -0
  80. package/dist/types/index.d.ts +1 -0
  81. package/dist/types/index.d.ts.map +1 -1
  82. package/dist/types/index.js +1 -0
  83. package/dist/types/index.js.map +1 -1
  84. package/dist/types/session.d.ts +106 -2
  85. package/dist/types/session.d.ts.map +1 -1
  86. package/dist/types/session.js.map +1 -1
  87. package/dist/types/update.d.ts +4 -1
  88. package/dist/types/update.d.ts.map +1 -1
  89. package/dist/web/edit-history-sync.d.ts +27 -7
  90. package/dist/web/edit-history-sync.d.ts.map +1 -1
  91. package/dist/web/edit-history-sync.js +35 -19
  92. package/dist/web/edit-history-sync.js.map +1 -1
  93. package/dist/web/launch-defaults.d.ts +1 -1
  94. package/dist/web/launch-defaults.js +2 -2
  95. package/dist/web/launch-defaults.js.map +1 -1
  96. package/dist/web/linked-cases-writer.d.ts +43 -0
  97. package/dist/web/linked-cases-writer.d.ts.map +1 -0
  98. package/dist/web/linked-cases-writer.js +107 -0
  99. package/dist/web/linked-cases-writer.js.map +1 -0
  100. package/dist/web/paste-image-gc.d.ts +4 -1
  101. package/dist/web/paste-image-gc.d.ts.map +1 -1
  102. package/dist/web/paste-image-gc.js +4 -2
  103. package/dist/web/paste-image-gc.js.map +1 -1
  104. package/dist/web/public/admin-ui.js.gz +0 -0
  105. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  106. package/dist/web/public/{app.963b5ba8.js → app.9ea37070.js} +6 -6
  107. package/dist/web/public/app.9ea37070.js.br +0 -0
  108. package/dist/web/public/app.9ea37070.js.gz +0 -0
  109. package/dist/web/public/approvals-ui.js.gz +0 -0
  110. package/dist/web/public/constants.782170e1.js.gz +0 -0
  111. package/dist/web/public/cron-ui.js.gz +0 -0
  112. package/dist/web/public/edit-history-ui.js +27 -14
  113. package/dist/web/public/edit-history-ui.js.br +0 -0
  114. package/dist/web/public/edit-history-ui.js.gz +0 -0
  115. package/dist/web/public/entrance-animations.js.gz +0 -0
  116. package/dist/web/public/foreign-sessions.js +476 -0
  117. package/dist/web/public/foreign-sessions.js.br +0 -0
  118. package/dist/web/public/foreign-sessions.js.gz +0 -0
  119. package/dist/web/public/git-status-ui.js +5 -3
  120. package/dist/web/public/git-status-ui.js.br +0 -0
  121. package/dist/web/public/git-status-ui.js.gz +0 -0
  122. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  123. package/dist/web/public/home-sessions.js.gz +0 -0
  124. package/dist/web/public/host-wake-ui.js.gz +0 -0
  125. package/dist/web/public/i18n.a5e82c95.js +2 -0
  126. package/dist/web/public/i18n.a5e82c95.js.br +0 -0
  127. package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
  128. package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
  129. package/dist/web/public/index.html +36 -17
  130. package/dist/web/public/index.html.br +0 -0
  131. package/dist/web/public/index.html.gz +0 -0
  132. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  133. package/dist/web/public/{keyboard-accessory.d4ec5957.js → keyboard-accessory.b941676c.js} +3 -1
  134. package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
  135. package/dist/web/public/{keyboard-accessory.d4ec5957.js.gz → keyboard-accessory.b941676c.js.gz} +0 -0
  136. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  137. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  138. package/dist/web/public/mobile-overview.js +10 -0
  139. package/dist/web/public/mobile-overview.js.br +0 -0
  140. package/dist/web/public/mobile-overview.js.gz +0 -0
  141. package/dist/web/public/{mobile.5ef2ce85.css → mobile.67c8eb5b.css} +1 -1
  142. package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
  143. package/dist/web/public/{mobile.5ef2ce85.css.gz → mobile.67c8eb5b.css.gz} +0 -0
  144. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  145. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  146. package/dist/web/public/{panels-ui.b100727d.js → panels-ui.8bcd4473.js} +4 -4
  147. package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
  148. package/dist/web/public/panels-ui.8bcd4473.js.gz +0 -0
  149. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  150. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  151. package/dist/web/public/readmymind-ui.js.gz +0 -0
  152. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  153. package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
  154. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  155. package/dist/web/public/session-lineage.js.gz +0 -0
  156. package/dist/web/public/{session-ui.5ac82f2c.js → session-ui.a7c8c095.js} +11 -11
  157. package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
  158. package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
  159. package/dist/web/public/{settings-ui.d1d4fb99.js → settings-ui.18f92052.js} +3 -3
  160. package/dist/web/public/settings-ui.18f92052.js.br +0 -0
  161. package/dist/web/public/settings-ui.18f92052.js.gz +0 -0
  162. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  163. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  164. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  165. package/dist/web/public/{styles.77dc538f.css → styles.6a718331.css} +1 -1
  166. package/dist/web/public/styles.6a718331.css.br +0 -0
  167. package/dist/web/public/{styles.77dc538f.css.gz → styles.6a718331.css.gz} +0 -0
  168. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  169. package/dist/web/public/sw.js +2 -2
  170. package/dist/web/public/sw.js.br +0 -0
  171. package/dist/web/public/sw.js.gz +0 -0
  172. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  173. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  174. package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
  175. package/dist/web/public/terminal-split.js.gz +0 -0
  176. package/dist/web/public/terminal-tile.js +2 -2
  177. package/dist/web/public/terminal-tile.js.br +0 -0
  178. package/dist/web/public/terminal-tile.js.gz +0 -0
  179. package/dist/web/public/{terminal-ui.d7c75ce8.js → terminal-ui.a7883640.js} +2 -2
  180. package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
  181. package/dist/web/public/terminal-ui.a7883640.js.gz +0 -0
  182. package/dist/web/public/tile-grid.js.gz +0 -0
  183. package/dist/web/public/ultracode-panel.js.gz +0 -0
  184. package/dist/web/public/ultracode-windows.js.gz +0 -0
  185. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  186. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  187. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  188. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  189. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  190. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  191. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  192. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  193. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  194. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  195. package/dist/web/public/vendor/xterm.css.gz +0 -0
  196. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  197. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  198. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  199. package/dist/web/public/webview-tabs.js.gz +0 -0
  200. package/dist/web/public/whats-new.json +9 -9
  201. package/dist/web/routes/case-routes.d.ts.map +1 -1
  202. package/dist/web/routes/case-routes.js +72 -53
  203. package/dist/web/routes/case-routes.js.map +1 -1
  204. package/dist/web/routes/discord-routes.js +1 -1
  205. package/dist/web/routes/discord-routes.js.map +1 -1
  206. package/dist/web/routes/edit-history-routes.d.ts +2 -2
  207. package/dist/web/routes/edit-history-routes.d.ts.map +1 -1
  208. package/dist/web/routes/edit-history-routes.js +4 -3
  209. package/dist/web/routes/edit-history-routes.js.map +1 -1
  210. package/dist/web/routes/file-routes.d.ts.map +1 -1
  211. package/dist/web/routes/file-routes.js +213 -102
  212. package/dist/web/routes/file-routes.js.map +1 -1
  213. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  214. package/dist/web/routes/git-status-routes.js +7 -2
  215. package/dist/web/routes/git-status-routes.js.map +1 -1
  216. package/dist/web/routes/mux-routes.d.ts +10 -3
  217. package/dist/web/routes/mux-routes.d.ts.map +1 -1
  218. package/dist/web/routes/mux-routes.js +59 -1
  219. package/dist/web/routes/mux-routes.js.map +1 -1
  220. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  221. package/dist/web/routes/ralph-routes.js +22 -0
  222. package/dist/web/routes/ralph-routes.js.map +1 -1
  223. package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
  224. package/dist/web/routes/readmymind-routes.js +3 -2
  225. package/dist/web/routes/readmymind-routes.js.map +1 -1
  226. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  227. package/dist/web/routes/respawn-routes.js +24 -0
  228. package/dist/web/routes/respawn-routes.js.map +1 -1
  229. package/dist/web/routes/session-routes.d.ts +11 -8
  230. package/dist/web/routes/session-routes.d.ts.map +1 -1
  231. package/dist/web/routes/session-routes.js +262 -29
  232. package/dist/web/routes/session-routes.js.map +1 -1
  233. package/dist/web/routes/system-routes.d.ts.map +1 -1
  234. package/dist/web/routes/system-routes.js +3 -2
  235. package/dist/web/routes/system-routes.js.map +1 -1
  236. package/dist/web/schemas.d.ts +24 -1
  237. package/dist/web/schemas.d.ts.map +1 -1
  238. package/dist/web/schemas.js +34 -9
  239. package/dist/web/schemas.js.map +1 -1
  240. package/dist/web/self-update.d.ts +9 -0
  241. package/dist/web/self-update.d.ts.map +1 -1
  242. package/dist/web/self-update.js +19 -3
  243. package/dist/web/self-update.js.map +1 -1
  244. package/dist/web/server.d.ts +16 -3
  245. package/dist/web/server.d.ts.map +1 -1
  246. package/dist/web/server.js +84 -24
  247. package/dist/web/server.js.map +1 -1
  248. package/dist/web/session-wait-registry.d.ts +13 -0
  249. package/dist/web/session-wait-registry.d.ts.map +1 -1
  250. package/dist/web/session-wait-registry.js +16 -8
  251. package/dist/web/session-wait-registry.js.map +1 -1
  252. package/package.json +2 -2
  253. package/skills/codeman/SKILL.md +20 -4
  254. package/dist/web/public/app.963b5ba8.js.br +0 -0
  255. package/dist/web/public/app.963b5ba8.js.gz +0 -0
  256. package/dist/web/public/i18n.33f930d7.js +0 -2
  257. package/dist/web/public/i18n.33f930d7.js.br +0 -0
  258. package/dist/web/public/i18n.33f930d7.js.gz +0 -0
  259. package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
  260. package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
  261. package/dist/web/public/panels-ui.b100727d.js.br +0 -0
  262. package/dist/web/public/panels-ui.b100727d.js.gz +0 -0
  263. package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
  264. package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
  265. package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
  266. package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
  267. package/dist/web/public/styles.77dc538f.css.br +0 -0
  268. package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
  269. package/dist/web/public/terminal-ui.d7c75ce8.js.gz +0 -0
@@ -14,6 +14,10 @@ import { randomBytes, randomUUID } from 'node:crypto';
14
14
  import { isUtf8 } from 'node:buffer';
15
15
  import { performance } from 'node:perf_hooks';
16
16
  import { ApiErrorCode, createErrorResponse, getErrorMessage, } from '../../types.js';
17
+ import { AdoptForeignSessionSchema } from '../schemas.js';
18
+ import { discoverForeignSessions, invalidateForeignCache, readAllDockerCases, readAllRemoteHosts, } from '../../foreign-tmux-discovery.js';
19
+ import { adoptTargetIdentity, foreignTargetKey, foreignViewSessionName } from '../../foreign-tmux.js';
20
+ import { requireAdmin } from '../route-helpers.js';
17
21
  import { GuardedInputBusyError, Session, cliTakesSessionModel, isAltScreenStripMode, isExternalCliMode, isMuxAltScreenOnlyStripMode, isMuxMouseStripMode, } from '../../session.js';
18
22
  import { SseEvent } from '../sse-events.js';
19
23
  import { webviewCapabilities } from '../../webview-capabilities.js';
@@ -80,26 +84,32 @@ function landedInGroup(layout, sessionId) {
80
84
  * Guard a session-supplied configDir home BEFORE `applyCustomModelInjection` runs.
81
85
  *
82
86
  * The apply resolves the CLI's real home from this session's env override and then runs
83
- * synchronous `existsSync`/`mkdirSync` on it (deepseek `DSH_HOME`, and any future
84
- * `linkFromRealHome` recipe). A path on an unreachable network mount makes a synchronous
85
- * stat wait forever, and this runs INSIDE a request handler, so it would freeze the whole
86
- * web server (CLAUDE.md, the #516 rule on user-chosen paths). A relative home is left to
87
- * the apply, which already refuses to join non-absolute values.
87
+ * synchronous `existsSync`/`mkdirSync` on it (deepseek `DSH_HOME`, any future
88
+ * `linkFromRealHome` recipe, and claude's `CLAUDE_CONFIG_DIR` for `projects`). A path on an
89
+ * unreachable network mount makes a synchronous stat wait forever, and this runs INSIDE a
90
+ * request handler, so it would freeze the whole web server (CLAUDE.md, the #516 rule on
91
+ * user-chosen paths). A relative home is left to the apply, which already refuses to join
92
+ * non-absolute values.
88
93
  *
89
94
  * Probe the session's own home with `probePathKind(..., { pastCap: true })` first: on
90
95
  * `'unknown'` (a dead mount that never answered) hand the apply a lookup that hides that
91
96
  * one env var, so linking falls through to the server's own environment and the unresponsive
92
- * path is never touched. Any other entry's lookup passes through untouched.
97
+ * path is never touched. Any other entry's lookup passes through untouched. Boot recovery
98
+ * passes `probeOptions` of `{}`: it walks every saved session, so it keeps the bulk cap.
93
99
  */
94
- export async function guardSessionConfigDirHome(entry, baseLookup) {
100
+ export async function guardSessionConfigDirHome(entry, baseLookup, probeOptions = { pastCap: true }) {
95
101
  const recipe = entry.capabilities.customModelInjection;
96
- if (recipe?.kind !== 'configDir' || !recipe.linkFromRealHome?.length)
102
+ const dirEnvVar = recipe?.kind === 'env'
103
+ ? recipe.configDirVar
104
+ : recipe?.kind === 'configDir' && recipe.linkFromRealHome?.length
105
+ ? recipe.dirEnvVar
106
+ : undefined;
107
+ if (!dirEnvVar)
97
108
  return baseLookup;
98
- const dirEnvVar = recipe.dirEnvVar;
99
109
  const supplied = baseLookup(dirEnvVar)?.trim();
100
110
  if (!supplied || !isAbsolute(supplied))
101
111
  return baseLookup;
102
- if ((await probePathKind(supplied, { pastCap: true })) === 'unknown') {
112
+ if ((await probePathKind(supplied, probeOptions)) === 'unknown') {
103
113
  console.warn(`[custom-model] ${dirEnvVar} (${supplied}) did not answer; linking from the server's own home instead`);
104
114
  return (envVar) => (envVar === dirEnvVar ? undefined : baseLookup(envVar));
105
115
  }
@@ -173,7 +183,7 @@ const MOUSE_TRACKING_PATTERN = /\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[h
173
183
  * - `strip-full` (claude/codex/gemini): alt-screen toggles, `3J` and mouse DECSETs.
174
184
  * xterm obeys the toggles by switching to its scrollback-less alt buffer and wiping
175
185
  * saved lines, so history disappeared on tab switch.
176
- * - `strip-mux-and-mouse` (opencode, tmux-backed): alt-screen toggles and mouse DECSETs.
186
+ * - `strip-mux-and-mouse` (tmux-backed): alt-screen toggles and mouse DECSETs.
177
187
  * A replayed tracking enable parks xterm in report mode, where a drag goes to the CLI
178
188
  * instead of selecting text (and Ctrl+C without a selection is opencode's app_exit).
179
189
  * `3J` stays: a TUI is not a `clear` consumer.
@@ -1118,8 +1128,9 @@ options = {}) {
1118
1128
  const globalNice = await ctx.getGlobalNiceConfig();
1119
1129
  const modelConfig = await ctx.getModelConfig();
1120
1130
  const mode = body.mode || 'claude';
1121
- // Synced App Settings launch defaults (capabilities.launchDefaults, codex's model and
1122
- // effort today) fill the CLI's own config object where the caller left it unset.
1131
+ // Synced App Settings launch defaults (capabilities.launchDefaults, codex's model,
1132
+ // effort and network access today) fill the CLI's own config object where the caller
1133
+ // left it unset.
1123
1134
  // Local launches only: a remote attach runs whatever the remote pane already runs.
1124
1135
  const launchBody = remote ? body : await applyLaunchDefaults(mode, body);
1125
1136
  // Where a model override comes from is a capability, and the three answers are
@@ -1202,6 +1213,169 @@ options = {}) {
1202
1213
  ...(body.tabGroupId || landedInGroup(tabLayout, session.id) ? { tabLayout } : {}),
1203
1214
  };
1204
1215
  });
1216
+ // ========== Adopt a foreign tmux session ==========
1217
+ /**
1218
+ * The wrapper pane for an adopted session runs `tmux attach`, never the agent,
1219
+ * so it needs no workspace, and must not borrow the foreign one. Same value
1220
+ * `resolveMuxAttachCwd()` gives remote and docker panes.
1221
+ */
1222
+ const ADOPTED_WRAPPER_WORKING_DIR = '/tmp';
1223
+ /**
1224
+ * Targets with a wrapper being created right now. The live-session check alone
1225
+ * leaves a gap: between it and `addSession` the route awaits the host registries,
1226
+ * the history config and the owner's layout lock, so two devices opening one row
1227
+ * could each pass it.
1228
+ */
1229
+ const adoptsInFlight = new Set();
1230
+ /**
1231
+ * Wrap a tmux session a HUMAN started (local, in a container, or over ssh) in a
1232
+ * Codeman session, so it appears as a tab and can be driven from the browser.
1233
+ *
1234
+ * Four things make this safe, and each is load-bearing:
1235
+ *
1236
+ * 1. **The body carries only an opaque id.** The socket path, session name and
1237
+ * host are re-resolved by re-running discovery here. A browser therefore
1238
+ * never supplies a fragment of the command we are about to run, which is the
1239
+ * same rule that keeps docker-adopt and remote-attach injection-free.
1240
+ * 2. **The candidate must still exist.** Discovery is re-run rather than cached,
1241
+ * so a session that died between the listing and the click fails with a 404
1242
+ * instead of producing a wrapper attached to nothing.
1243
+ * 3. **One wrapper per target.** Two wrappers on one foreign session would each
1244
+ * create their own grouped view and each think they own the tab; the guard
1245
+ * is here rather than in the button's in-flight lock, which only stops a
1246
+ * double-click on one device.
1247
+ * 4. **Admin-only under multi-user.** Discovery already is (it exposes other
1248
+ * users' processes), and adopting someone's `shell` is arbitrary execution
1249
+ * as the server account, which is exactly what the `can-bypass-permissions`
1250
+ * grant gates elsewhere. The admin gate subsumes it, so there is deliberately
1251
+ * no second grant check here.
1252
+ */
1253
+ app.post('/api/sessions/adopt', async (req, reply) => {
1254
+ if (isMultiUserMode() && !requireAdmin(req, reply))
1255
+ return;
1256
+ const owner = ownerFor(req);
1257
+ const body = parseBody(AdoptForeignSessionSchema, req.body, 'Invalid request body');
1258
+ // Re-resolve rather than trust: point 1 and 2 above.
1259
+ const found = await discoverForeignSessions({
1260
+ local: true,
1261
+ force: true,
1262
+ dockerCases: body.docker ? await readAllDockerCases() : undefined,
1263
+ remoteHosts: body.remote ? await readAllRemoteHosts() : undefined,
1264
+ });
1265
+ const target = found.sessions.find((f) => f.id === body.id);
1266
+ if (!target) {
1267
+ // ⚠️ "Not in the re-resolve" has two very different causes and they must not
1268
+ // be reported as one. The session really being gone is the ordinary case;
1269
+ // the OTHER case is a location we could not reach this time, which on a
1270
+ // flaky link makes a perfectly live remote session read as deleted. Measured
1271
+ // against a real VM whose ssh path dropped ~10% of connections: clicking
1272
+ // Open failed with "no longer there" while the session was sitting right
1273
+ // there. Discovery already knows which it was: it wrote a note, so say so.
1274
+ const reach = found.notes.filter((n) => !/skipped/.test(n));
1275
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, reach.length
1276
+ ? `Could not reach it just now (${reach.join('; ')}). It may still be running, try again.`
1277
+ : 'That tmux session is no longer there. Refresh the list and try again.');
1278
+ }
1279
+ // Point 3: one wrapper per target. Live sessions and not mux records, which
1280
+ // only exist once the first attach starts the pane, so reading those let a
1281
+ // second adopt through before the first tab had started.
1282
+ const targetKey = foreignTargetKey(target);
1283
+ for (const session of ctx.sessions.values()) {
1284
+ if (session.adopt && foreignTargetKey(adoptTargetIdentity(session.adopt)) === targetKey) {
1285
+ return { session: ctx.getSessionStateWithRespawn(session), alreadyAdopted: true };
1286
+ }
1287
+ }
1288
+ if (adoptsInFlight.has(targetKey)) {
1289
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'That tmux session is already being opened');
1290
+ }
1291
+ // After the reuse above, which creates nothing and so must not be refused at capacity.
1292
+ const capMsg = sessionCapacityMessage(ctx.sessions, owner);
1293
+ if (capMsg)
1294
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, capMsg);
1295
+ adoptsInFlight.add(targetKey);
1296
+ try {
1297
+ return await createAdoptedSession(req, target, owner, body);
1298
+ }
1299
+ finally {
1300
+ adoptsInFlight.delete(targetKey);
1301
+ }
1302
+ });
1303
+ /** Everything past the guards in `POST /api/sessions/adopt`, held under its in-flight mark. */
1304
+ async function createAdoptedSession(req, target, owner, body) {
1305
+ // Connection facts are copied onto the session rather than referenced by id:
1306
+ // a wrapper restored after a server restart must be able to rebuild its
1307
+ // command even if the host registry was edited in the meantime.
1308
+ const adopt = {
1309
+ location: target.location,
1310
+ socketPath: target.socketPath,
1311
+ targetSession: target.sessionName,
1312
+ viewSession: '',
1313
+ paneCurrentPath: target.workingDir,
1314
+ };
1315
+ if (target.location === 'docker') {
1316
+ const hosts = await readDockerHosts(CODEMAN_CONFIG_DIR);
1317
+ const host = hosts.find((h) => h.id === target.hostId);
1318
+ if (!target.containerName) {
1319
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Container name missing for a docker candidate');
1320
+ }
1321
+ adopt.docker = {
1322
+ hostId: target.hostId ?? '',
1323
+ label: target.hostLabel ?? target.containerName,
1324
+ engine: host?.engine ?? 'docker',
1325
+ containerName: target.containerName,
1326
+ daemonHost: host?.daemonHost,
1327
+ context: host?.context,
1328
+ };
1329
+ }
1330
+ else if (target.location === 'remote') {
1331
+ const host = (await readRemoteHosts(CODEMAN_CONFIG_DIR)).find((h) => h.id === target.hostId);
1332
+ if (!host)
1333
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Remote host not found');
1334
+ adopt.remote = {
1335
+ hostId: host.id,
1336
+ label: host.label,
1337
+ host: host.host,
1338
+ username: host.username,
1339
+ port: host.port,
1340
+ identityFile: host.identityFile,
1341
+ socksProxy: host.socksProxy,
1342
+ jumpHost: host.jumpHost,
1343
+ extraSshOptions: host.extraSshOptions,
1344
+ };
1345
+ }
1346
+ const adoptHistoryConfig = await ctx.getTerminalHistoryConfig();
1347
+ // ⚠️ The wrapper's `workingDir` is a NEUTRAL LOCAL path, never the foreign
1348
+ // pane's cwd. That cwd is an observation about another machine: it can carry
1349
+ // characters outside SAFE_PATH_PATTERN (`~/c++`, `Program Files (x86)`), which
1350
+ // makes `createSession` throw so the tab never starts at all, and when it is a
1351
+ // container or ssh path every local consumer of `workingDir` (the Files panel,
1352
+ // the boot hook sweep, the fs watchers) reads it as a path on THIS host. The
1353
+ // foreign cwd stays on `adopt.paneCurrentPath`, which is for display only.
1354
+ // `/tmp` is the same neutral cwd `resolveMuxAttachCwd` already gives the pane.
1355
+ const session = new Session({
1356
+ workingDir: ADOPTED_WRAPPER_WORKING_DIR,
1357
+ mode: target.mode,
1358
+ name: body.name || target.sessionName,
1359
+ mux: ctx.mux,
1360
+ useMux: true,
1361
+ tmuxHistoryLimit: adoptHistoryConfig.tmuxHistoryLimit,
1362
+ adopt,
1363
+ owner,
1364
+ parentSessionId: resolveParentSessionId(ctx, req, body.parentSessionId, owner),
1365
+ });
1366
+ // The view session name is derived from the Codeman session id, so it can only
1367
+ // be filled once the Session exists.
1368
+ adopt.viewSession = foreignViewSessionName(session.id);
1369
+ await ctx.addSession(session);
1370
+ ctx.store.incrementSessionsCreated();
1371
+ ctx.persistSessionState(session);
1372
+ await ctx.setupSessionListeners(session);
1373
+ getLifecycleLog().log({ event: 'created', sessionId: session.id, name: session.name });
1374
+ invalidateForeignCache();
1375
+ const lightState = ctx.getSessionStateWithRespawn(session);
1376
+ ctx.broadcast(SseEvent.SessionCreated, lightState);
1377
+ return { session: lightState, adopted: true };
1378
+ }
1205
1379
  // ========== Rename Session ==========
1206
1380
  app.put('/api/sessions/:id/name', async (req) => {
1207
1381
  const { id } = req.params;
@@ -1255,6 +1429,10 @@ options = {}) {
1255
1429
  if (session.remote || session.docker) {
1256
1430
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Custom model endpoints are not supported for remote (SSH) or Docker sessions yet');
1257
1431
  }
1432
+ // Its own check: this restarts the CLI in the pane, and an adopted pane's CLI is not ours to restart.
1433
+ if (session.isAdopted) {
1434
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Custom model endpoints are not available for adopted sessions: Codeman did not start this agent');
1435
+ }
1258
1436
  if (session.isBusy()) {
1259
1437
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
1260
1438
  }
@@ -1357,7 +1535,7 @@ options = {}) {
1357
1535
  return { requiresConfirmation: true, currentlyLoadedModel: currentlyLoaded, affectedSessions };
1358
1536
  }
1359
1537
  }
1360
- const applied = applyCustomModelInjection(entry, endpoint, body.modelId, session.id, contextLength, session.workingDir, await guardSessionConfigDirHome(entry, (envVar) => session.envOverrideValue(envVar)));
1538
+ const applied = applyCustomModelInjection(entry, endpoint, body.modelId, session.id, contextLength, session.workingDir, await guardSessionConfigDirHome(entry, (envVar) => session.ownEnvValue(envVar)));
1361
1539
  if (!applied) {
1362
1540
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `${session.mode} has no known custom-model mechanism`);
1363
1541
  }
@@ -1368,6 +1546,7 @@ options = {}) {
1368
1546
  envKeys: applied.envKeys,
1369
1547
  configDir: applied.configDir,
1370
1548
  launchModel: applied.launchModel,
1549
+ homeEnvVar: applied.homeEnvVar,
1371
1550
  }, applied.envOverrides);
1372
1551
  // Clean up the OLD config dir on disk, unless the new one happens to reuse the same
1373
1552
  // path (same session, configDir kind again) — never delete the dir we just wrote.
@@ -1507,6 +1686,11 @@ options = {}) {
1507
1686
  const { id } = req.params;
1508
1687
  const { prompt } = parseBody(RunPromptSchema, req.body);
1509
1688
  const session = findSessionOrFail(ctx, id, req);
1689
+ // Its own check: `runPrompt` starts a new local claude, and the answer has to say so
1690
+ // here rather than as a `session:error` after a success.
1691
+ if (session.isAdopted) {
1692
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'One-shot prompts are not available for adopted sessions: Codeman did not start this agent');
1693
+ }
1510
1694
  if (session.isBusy()) {
1511
1695
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
1512
1696
  }
@@ -1547,6 +1731,10 @@ options = {}) {
1547
1731
  // about shell, and they disagree upstream too — reconciling them is a behaviour change
1548
1732
  // and belongs in its own PR, not in a refactor that is meant to change nothing.
1549
1733
  if (!isExternalCliMode(session.mode) &&
1734
+ // ⚠️ And never for an adopted session, whose mode can be claude while the
1735
+ // agent in that pane belongs to someone else. Kept as its own clause for
1736
+ // the same reason the respawn and Ralph routes keep theirs separate.
1737
+ !session.isAdopted &&
1550
1738
  ctx.store.getConfig().ralphEnabled &&
1551
1739
  !session.ralphTracker.autoEnableDisabled) {
1552
1740
  autoConfigureRalph(session, session.workingDir, ctx);
@@ -2414,7 +2602,10 @@ options = {}) {
2414
2602
  * effort: the tab rename has already happened and must not fail on this.
2415
2603
  */
2416
2604
  async function syncClaudeTitle(session) {
2417
- if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' || session.remote || session.docker)
2605
+ if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' ||
2606
+ session.remote ||
2607
+ session.docker ||
2608
+ session.isAdopted)
2418
2609
  return;
2419
2610
  try {
2420
2611
  const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
@@ -2496,6 +2687,16 @@ options = {}) {
2496
2687
  // The body of GET /api/sessions/:id/last-response, so the Discord bridge reads the same transcript
2497
2688
  // the response viewer does (per-CLI reader, pane fallback) without a second copy of this logic.
2498
2689
  const lastResponseFor = async (session, context) => {
2690
+ // ⚠️ An adopted session has no transcript Codeman can name. Every reader below pairs one by
2691
+ // the id Codeman launched the CLI with or by the session's folder, and neither means anything
2692
+ // here: the CLI chose its own id, and the folder is the neutral `/tmp`, so the newest file for
2693
+ // that folder would be another conversation's answer shown as this one's. Nothing said yet is
2694
+ // the honest answer.
2695
+ if (session.isAdopted) {
2696
+ return context === 'full' || context === 'turn'
2697
+ ? { text: '', timestamp: '', messages: [] }
2698
+ : { text: '', timestamp: '' };
2699
+ }
2499
2700
  // Codex sessions don't write to ~/.claude/projects — their transcripts
2500
2701
  // live in ~/.codex/sessions/**. Branch to a Codex-specific reader so the
2501
2702
  // response-viewer works for Codex panes too.
@@ -3164,6 +3365,13 @@ options = {}) {
3164
3365
  const { id } = req.params;
3165
3366
  const body = parseBody(AutoClearSchema, req.body, 'Invalid request body');
3166
3367
  const session = findSessionOrFail(ctx, id, req);
3368
+ // ⚠️ Separate from the external-CLI gate on purpose: an adopted session's mode
3369
+ // can perfectly well BE claude, and this drives the pane of a session someone
3370
+ // else is using. Merging the two checks means a future change to the
3371
+ // external-CLI rule silently reopens this one.
3372
+ if (session.isAdopted) {
3373
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Auto-clear is not available for adopted sessions: Codeman did not start this agent and must not drive it');
3374
+ }
3167
3375
  session.setAutoClear(body.enabled, body.threshold);
3168
3376
  persistAndBroadcastSession(ctx, session);
3169
3377
  return {
@@ -3181,6 +3389,13 @@ options = {}) {
3181
3389
  const { id } = req.params;
3182
3390
  const body = parseBody(AutoCompactSchema, req.body, 'Invalid request body');
3183
3391
  const session = findSessionOrFail(ctx, id, req);
3392
+ // ⚠️ Separate from the external-CLI gate on purpose: an adopted session's mode
3393
+ // can perfectly well BE claude, and this drives the pane of a session someone
3394
+ // else is using. Merging the two checks means a future change to the
3395
+ // external-CLI rule silently reopens this one.
3396
+ if (session.isAdopted) {
3397
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Auto-compact is not available for adopted sessions: Codeman did not start this agent and must not drive it');
3398
+ }
3184
3399
  session.setAutoCompact(body.enabled, body.threshold, body.prompt);
3185
3400
  persistAndBroadcastSession(ctx, session);
3186
3401
  return {
@@ -3232,8 +3447,8 @@ options = {}) {
3232
3447
  const { id } = req.params;
3233
3448
  const body = parseBody(GitFetchSettingsSchema, req.body, 'Invalid request body');
3234
3449
  const session = findSessionOrFail(ctx, id, req);
3235
- if (session.remote || session.docker) {
3236
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git fetch does not run for remote or Docker sessions');
3450
+ if (session.remote || session.docker || session.isAdopted) {
3451
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git fetch does not run for remote, Docker or adopted sessions');
3237
3452
  }
3238
3453
  const enabled = 'enabled' in body ? (body.enabled ?? undefined) : session.gitFetchEnabled;
3239
3454
  const intervalSeconds = 'intervalSeconds' in body ? (body.intervalSeconds ?? undefined) : session.gitFetchIntervalSeconds;
@@ -3254,6 +3469,13 @@ options = {}) {
3254
3469
  const { id } = req.params;
3255
3470
  const body = parseBody(AutoResumeSchema, req.body, 'Invalid request body');
3256
3471
  const session = findSessionOrFail(ctx, id, req);
3472
+ // ⚠️ Separate from the external-CLI gate on purpose: an adopted session's mode
3473
+ // can perfectly well BE claude, and this drives the pane of a session someone
3474
+ // else is using. Merging the two checks means a future change to the
3475
+ // external-CLI rule silently reopens this one.
3476
+ if (session.isAdopted) {
3477
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Auto-resume is not available for adopted sessions: Codeman did not start this agent and must not drive it');
3478
+ }
3257
3479
  session.setAutoResume(body.enabled);
3258
3480
  persistAndBroadcastSession(ctx, session);
3259
3481
  return {
@@ -3315,6 +3537,10 @@ options = {}) {
3315
3537
  if (body.enabled && session.remote) {
3316
3538
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Image watching does not run for remote sessions');
3317
3539
  }
3540
+ // An adopted session's folder is the neutral wrapper folder, not the foreign pane's work.
3541
+ if (body.enabled && session.isAdopted) {
3542
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Image watching does not run for adopted sessions');
3543
+ }
3318
3544
  if (body.enabled) {
3319
3545
  imageWatcher.watchSession(session.id, session.workingDir);
3320
3546
  }
@@ -3830,12 +4056,12 @@ options = {}) {
3830
4056
  // unknown endpoint, a model id the CLI's argv pattern can't carry) rather than trusting
3831
4057
  // a lighter version of them, since this is the same server-side authority reached a
3832
4058
  // different way, not a separate, less-checked path.
3833
- let qsCustomModelEnvOverrides = qsGatedEnvOverrides;
3834
- // Only the INJECTED keys (never the caller's envOverrides merged in) — this is what
3835
- // setCustomModel() bookkeeping must be given below. The Session constructor already
3836
- // applies qsCustomModelEnvOverrides (the full merged set) directly; re-merging that
3837
- // full set into setCustomModel() would put CLAUDE_CODE_EFFORT_LEVEL back after the
3838
- // constructor stripped it (see setCustomModel()'s own doc comment in session.ts).
4059
+ //
4060
+ // Only the INJECTED keys (never the caller's envOverrides merged in): setCustomModel()
4061
+ // merges these over what the Session constructor took, which is how it learns which of
4062
+ // the caller's values the selection replaced. Handing the constructor the merged set
4063
+ // would hide them, and giving setCustomModel() that set would put
4064
+ // CLAUDE_CODE_EFFORT_LEVEL back after the constructor stripped it.
3839
4065
  let qsCustomModelAppliedEnvOverrides;
3840
4066
  let qsCustomModelLaunchModel;
3841
4067
  let qsCustomModelSessionId;
@@ -3922,7 +4148,6 @@ options = {}) {
3922
4148
  if (!cmApplied) {
3923
4149
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `${mode} has no known custom-model mechanism`);
3924
4150
  }
3925
- qsCustomModelEnvOverrides = { ...qsGatedEnvOverrides, ...cmApplied.envOverrides };
3926
4151
  qsCustomModelAppliedEnvOverrides = cmApplied.envOverrides;
3927
4152
  qsCustomModelLaunchModel = cmApplied.launchModel;
3928
4153
  qsCustomModelBookkeeping = {
@@ -3932,6 +4157,7 @@ options = {}) {
3932
4157
  envKeys: cmApplied.envKeys,
3933
4158
  configDir: cmApplied.configDir,
3934
4159
  launchModel: cmApplied.launchModel,
4160
+ homeEnvVar: cmApplied.homeEnvVar,
3935
4161
  };
3936
4162
  // Actually kick off llama-swap's load now — see the dedicated apply route's own
3937
4163
  // comment on triggerLlamaSwapLoad for why this can't just wait on the launched CLI's
@@ -3962,7 +4188,7 @@ options = {}) {
3962
4188
  copilotConfig: mode === 'copilot' ? qsGatedCopilotConfig : undefined,
3963
4189
  deepSeekConfig: mode === 'deepseek' ? qsGatedDeepSeekConfig : undefined,
3964
4190
  ompConfig: qsResolvedOmpConfig,
3965
- envOverrides: qsCustomModelEnvOverrides,
4191
+ envOverrides: qsGatedEnvOverrides,
3966
4192
  effort,
3967
4193
  advisorModel,
3968
4194
  remote,
@@ -3991,10 +4217,10 @@ options = {}) {
3991
4217
  }
3992
4218
  const session = new Session(qsSessionOptions);
3993
4219
  // Records the selection for session.customModel/getCustomModelForPersist() and future
3994
- // clear/switch calls — the actual env vars and launch-model config are already part of
3995
- // the launch above (constructor envOverrides, piConfig/grokConfig/ompConfig.model), so
3996
- // this is bookkeeping only, never a restart: setCustomModel() is synchronous state, no
3997
- // tmux IO of its own (see its own doc comment in session.ts).
4220
+ // clear/switch calls, and merges the injected env, which the launch below reads along
4221
+ // with the launch-model config set above (piConfig/grokConfig/ompConfig.model). Nothing
4222
+ // has started yet, so this is never a restart: setCustomModel() is synchronous state,
4223
+ // no tmux IO of its own (see its own doc comment in session.ts).
3998
4224
  if (qsCustomModelBookkeeping) {
3999
4225
  session.setCustomModel(qsCustomModelBookkeeping, qsCustomModelAppliedEnvOverrides);
4000
4226
  }
@@ -5066,6 +5292,13 @@ options = {}) {
5066
5292
  reply.code(400);
5067
5293
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Prompt uploads are not supported for remote (SSH) sessions');
5068
5294
  }
5295
+ // An adopted session has no workspace of ours: its folder is the neutral `/tmp`, shared with
5296
+ // whatever else runs there, and an agent in a container or on an ssh host could not read it.
5297
+ // The foreign pane's own folder is not ours to write into either.
5298
+ if (session.isAdopted) {
5299
+ reply.code(400);
5300
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Prompt uploads are not supported for adopted sessions');
5301
+ }
5069
5302
  // The upload is written under session.workingDir (ensureUploadDir below), and
5070
5303
  // a live session's workspace can sit on a network mount that stopped answering.
5071
5304
  // Probe it bounded before the first disk touch, as #516 requires for a path the