aicodeman 1.47.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.
- package/README.md +15 -3
- package/dist/cli-notify.d.ts +34 -0
- package/dist/cli-notify.d.ts.map +1 -0
- package/dist/cli-notify.js +100 -0
- package/dist/cli-notify.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/cli-registry/patterns.d.ts +1 -1
- package/dist/config/cli-registry/patterns.d.ts.map +1 -1
- package/dist/config/cli-registry/patterns.js +4 -0
- package/dist/config/cli-registry/patterns.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +7 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +67 -3
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +114 -31
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +51 -21
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/foreign-tmux.d.ts +48 -0
- package/dist/config/foreign-tmux.d.ts.map +1 -0
- package/dist/config/foreign-tmux.js +48 -0
- package/dist/config/foreign-tmux.js.map +1 -0
- package/dist/config/path-probe.d.ts +6 -0
- package/dist/config/path-probe.d.ts.map +1 -1
- package/dist/config/path-probe.js +6 -0
- package/dist/config/path-probe.js.map +1 -1
- package/dist/config-backup.d.ts +1 -1
- package/dist/config-backup.d.ts.map +1 -1
- package/dist/config-backup.js +2 -0
- package/dist/config-backup.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +32 -8
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js +10 -0
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/custom-model-injection-apply.d.ts +21 -5
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +121 -27
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/custom-model-injection.d.ts +6 -0
- package/dist/custom-model-injection.d.ts.map +1 -1
- package/dist/custom-model-injection.js +92 -10
- package/dist/custom-model-injection.js.map +1 -1
- package/dist/discord-bridge.d.ts +338 -0
- package/dist/discord-bridge.d.ts.map +1 -0
- package/dist/discord-bridge.js +1256 -0
- package/dist/discord-bridge.js.map +1 -0
- package/dist/docker-hosts.d.ts +8 -1
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +5 -5
- package/dist/docker-hosts.js.map +1 -1
- package/dist/edit-history-local-source.d.ts +83 -0
- package/dist/edit-history-local-source.d.ts.map +1 -0
- package/dist/edit-history-local-source.js +198 -0
- package/dist/edit-history-local-source.js.map +1 -0
- package/dist/edit-history-parse.d.ts +114 -0
- package/dist/edit-history-parse.d.ts.map +1 -0
- package/dist/edit-history-parse.js +312 -0
- package/dist/edit-history-parse.js.map +1 -0
- package/dist/edit-history-store.d.ts +209 -0
- package/dist/edit-history-store.d.ts.map +1 -0
- package/dist/edit-history-store.js +599 -0
- package/dist/edit-history-store.js.map +1 -0
- package/dist/foreign-tmux-discovery.d.ts +140 -0
- package/dist/foreign-tmux-discovery.d.ts.map +1 -0
- package/dist/foreign-tmux-discovery.js +366 -0
- package/dist/foreign-tmux-discovery.js.map +1 -0
- package/dist/foreign-tmux.d.ts +381 -0
- package/dist/foreign-tmux.d.ts.map +1 -0
- package/dist/foreign-tmux.js +775 -0
- package/dist/foreign-tmux.js.map +1 -0
- package/dist/git-clone.d.ts +6 -1
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +79 -14
- package/dist/git-clone.js.map +1 -1
- package/dist/git-fetch-scheduler.d.ts +2 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -1
- package/dist/git-fetch-scheduler.js +1 -1
- package/dist/git-fetch-scheduler.js.map +1 -1
- package/dist/git-fetch.d.ts +1 -1
- package/dist/git-fetch.d.ts.map +1 -1
- package/dist/git-workspace-status.d.ts +2 -2
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/hooks-config.d.ts +2 -1
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +2 -1
- package/dist/hooks-config.js.map +1 -1
- package/dist/image-watcher.js +4 -4
- package/dist/image-watcher.js.map +1 -1
- package/dist/live-worktree.d.ts +53 -0
- package/dist/live-worktree.d.ts.map +1 -0
- package/dist/live-worktree.js +97 -0
- package/dist/live-worktree.js.map +1 -0
- package/dist/mux-interface.d.ts +22 -2
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/reboot-restore.d.ts +1 -1
- package/dist/reboot-restore.d.ts.map +1 -1
- package/dist/reboot-restore.js +9 -0
- package/dist/reboot-restore.js.map +1 -1
- package/dist/remote-files.d.ts +150 -5
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +505 -7
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-hosts.d.ts +15 -9
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +42 -20
- package/dist/remote-hosts.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +23 -8
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-env-clamp.d.ts +1 -1
- package/dist/session-env-clamp.d.ts.map +1 -1
- package/dist/session-env-clamp.js +4 -3
- package/dist/session-env-clamp.js.map +1 -1
- package/dist/session.d.ts +161 -24
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +337 -37
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +114 -12
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +318 -41
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/discord.d.ts +55 -0
- package/dist/types/discord.d.ts.map +1 -0
- package/dist/types/discord.js +18 -0
- package/dist/types/discord.js.map +1 -0
- package/dist/types/foreign-tmux.d.ts +101 -0
- package/dist/types/foreign-tmux.d.ts.map +1 -0
- package/dist/types/foreign-tmux.js +27 -0
- package/dist/types/foreign-tmux.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +119 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/update.d.ts +4 -1
- package/dist/types/update.d.ts.map +1 -1
- package/dist/web/case-path.d.ts +2 -0
- package/dist/web/case-path.d.ts.map +1 -1
- package/dist/web/case-path.js +3 -3
- package/dist/web/case-path.js.map +1 -1
- package/dist/web/edit-history-sync.d.ts +178 -0
- package/dist/web/edit-history-sync.d.ts.map +1 -0
- package/dist/web/edit-history-sync.js +461 -0
- package/dist/web/edit-history-sync.js.map +1 -0
- package/dist/web/launch-defaults.d.ts +1 -1
- package/dist/web/launch-defaults.js +2 -2
- package/dist/web/launch-defaults.js.map +1 -1
- package/dist/web/linked-cases-writer.d.ts +43 -0
- package/dist/web/linked-cases-writer.d.ts.map +1 -0
- package/dist/web/linked-cases-writer.js +107 -0
- package/dist/web/linked-cases-writer.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +10 -1
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +10 -2
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +13 -0
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.fe8a6b4c.js → app.9ea37070.js} +6 -6
- package/dist/web/public/app.9ea37070.js.br +0 -0
- package/dist/web/public/app.9ea37070.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
- package/dist/web/public/constants.782170e1.js.br +0 -0
- package/dist/web/public/constants.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +643 -0
- package/dist/web/public/edit-history-ui.js.br +0 -0
- package/dist/web/public/edit-history-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/foreign-sessions.js +476 -0
- package/dist/web/public/foreign-sessions.js.br +0 -0
- package/dist/web/public/foreign-sessions.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +5 -3
- package/dist/web/public/git-status-ui.js.br +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.a5e82c95.js +2 -0
- package/dist/web/public/i18n.a5e82c95.js.br +0 -0
- package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +183 -18
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.ff64fafc.js → keyboard-accessory.b941676c.js} +75 -2
- package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
- package/dist/web/public/keyboard-accessory.b941676c.js.gz +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +10 -0
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.67c8eb5b.css +1 -0
- package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
- package/dist/web/public/mobile.67c8eb5b.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.4a56ee63.js → panels-ui.8bcd4473.js} +5 -5
- package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
- package/dist/web/public/panels-ui.8bcd4473.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/{respawn-ui.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
- package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
- package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/security-notices.json +1 -21
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.426fb671.js → session-ui.a7c8c095.js} +30 -30
- package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
- package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
- package/dist/web/public/settings-ui.18f92052.js +106 -0
- package/dist/web/public/settings-ui.18f92052.js.br +0 -0
- package/dist/web/public/settings-ui.18f92052.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.6a718331.css +1 -0
- package/dist/web/public/styles.6a718331.css.br +0 -0
- package/dist/web/public/styles.6a718331.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +2 -2
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.a7883640.js +4 -0
- package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
- package/dist/web/public/terminal-ui.a7883640.js.gz +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -8
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +165 -50
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +5 -1
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
- package/dist/web/routes/config-backup-routes.js +4 -1
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/custom-model-routes.d.ts +7 -2
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +51 -6
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.d.ts +25 -0
- package/dist/web/routes/discord-routes.d.ts.map +1 -0
- package/dist/web/routes/discord-routes.js +206 -0
- package/dist/web/routes/discord-routes.js.map +1 -0
- package/dist/web/routes/edit-history-routes.d.ts +43 -0
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
- package/dist/web/routes/edit-history-routes.js +107 -0
- package/dist/web/routes/edit-history-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +273 -102
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +7 -2
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/mux-routes.d.ts +10 -3
- package/dist/web/routes/mux-routes.d.ts.map +1 -1
- package/dist/web/routes/mux-routes.js +59 -1
- package/dist/web/routes/mux-routes.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +22 -0
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
- package/dist/web/routes/readmymind-routes.js +3 -2
- package/dist/web/routes/readmymind-routes.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +24 -0
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +21 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +338 -55
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +4 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +92 -2
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +111 -10
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/self-update.d.ts +9 -0
- package/dist/web/self-update.d.ts.map +1 -1
- package/dist/web/self-update.js +19 -3
- package/dist/web/self-update.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +15 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +56 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +285 -17
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +13 -0
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +16 -8
- package/dist/web/session-wait-registry.js.map +1 -1
- package/package.json +2 -2
- package/skills/codeman/SKILL.md +20 -4
- package/dist/web/public/app.fe8a6b4c.js.br +0 -0
- package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/i18n.a618ef4e.js +0 -2
- package/dist/web/public/i18n.a618ef4e.js.br +0 -0
- package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +0 -1
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/session-ui.426fb671.js.br +0 -0
- package/dist/web/public/session-ui.426fb671.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +0 -106
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/styles.4f0c67a0.css +0 -1
- package/dist/web/public/styles.4f0c67a0.css.br +0 -0
- package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
- package/dist/web/public/terminal-ui.7e787829.js +0 -4
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* auto-clear, auto-compact, image watcher, flicker filter, and logout.
|
|
5
5
|
*/
|
|
6
6
|
import { claudeProjectDirName } from '../../case-rename.js';
|
|
7
|
-
import { join, dirname, extname, basename, resolve as resolvePath } from 'node:path';
|
|
7
|
+
import { join, dirname, extname, basename, isAbsolute, resolve as resolvePath } from 'node:path';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { existsSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { execFile } from 'node:child_process';
|
|
@@ -14,13 +14,18 @@ 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';
|
|
20
24
|
import { htmlViewCapabilities } from '../../html-view-capabilities.js';
|
|
21
25
|
import { CreateSessionSchema, SessionNameSchema, SessionColorSchema, RunPromptSchema, SessionInputWithLimitSchema, ResizeSchema, AutoClearSchema, AutoCompactSchema, AutoResumeSchema, KeepWarmSchema, GitFetchSettingsSchema, PinSessionSchema, ImageWatcherSchema, FlickerFilterSchema, QuickRunSchema, QuickStartSchema, InteractiveStartSchema, SessionOrderUpdateSchema, SessionWaitQuerySchema, SessionWaitOutputQuerySchema, CustomModelSelectionSchema, } from '../schemas.js';
|
|
22
|
-
import { readCustomModelHosts } from '../../custom-model-hosts.js';
|
|
26
|
+
import { readCustomModelHosts, resolveCustomModelContextLength } from '../../custom-model-hosts.js';
|
|
23
27
|
import { applyCustomModelInjection, removeConfigDir } from '../../custom-model-injection-apply.js';
|
|
28
|
+
import { buildCustomModelInjection } from '../../custom-model-injection.js';
|
|
24
29
|
import { getLlamaSwapStatus, triggerLlamaSwapLoad, exceedsSafeContextFloor, CLAUDE_MIN_SAFE_CONTEXT_TOKENS, } from './custom-model-routes.js';
|
|
25
30
|
import { matchesPattern } from '../../config/cli-registry/patterns.js';
|
|
26
31
|
import { ownerLayoutKey } from '../../tab-layout-persistence.js';
|
|
@@ -75,6 +80,41 @@ const CLAUDE_WORKTREE_PATH = /^(.*)\/\.claude\/worktrees\/([^/]+)\/?$/;
|
|
|
75
80
|
function landedInGroup(layout, sessionId) {
|
|
76
81
|
return !!layout?.groups.some((group) => group.refs.some((ref) => ref.kind === 'session' && ref.id === sessionId));
|
|
77
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Guard a session-supplied configDir home BEFORE `applyCustomModelInjection` runs.
|
|
85
|
+
*
|
|
86
|
+
* The apply resolves the CLI's real home from this session's env override and then runs
|
|
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.
|
|
93
|
+
*
|
|
94
|
+
* Probe the session's own home with `probePathKind(..., { pastCap: true })` first: on
|
|
95
|
+
* `'unknown'` (a dead mount that never answered) hand the apply a lookup that hides that
|
|
96
|
+
* one env var, so linking falls through to the server's own environment and the unresponsive
|
|
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.
|
|
99
|
+
*/
|
|
100
|
+
export async function guardSessionConfigDirHome(entry, baseLookup, probeOptions = { pastCap: true }) {
|
|
101
|
+
const recipe = entry.capabilities.customModelInjection;
|
|
102
|
+
const dirEnvVar = recipe?.kind === 'env'
|
|
103
|
+
? recipe.configDirVar
|
|
104
|
+
: recipe?.kind === 'configDir' && recipe.linkFromRealHome?.length
|
|
105
|
+
? recipe.dirEnvVar
|
|
106
|
+
: undefined;
|
|
107
|
+
if (!dirEnvVar)
|
|
108
|
+
return baseLookup;
|
|
109
|
+
const supplied = baseLookup(dirEnvVar)?.trim();
|
|
110
|
+
if (!supplied || !isAbsolute(supplied))
|
|
111
|
+
return baseLookup;
|
|
112
|
+
if ((await probePathKind(supplied, probeOptions)) === 'unknown') {
|
|
113
|
+
console.warn(`[custom-model] ${dirEnvVar} (${supplied}) did not answer; linking from the server's own home instead`);
|
|
114
|
+
return (envVar) => (envVar === dirEnvVar ? undefined : baseLookup(envVar));
|
|
115
|
+
}
|
|
116
|
+
return baseLookup;
|
|
117
|
+
}
|
|
78
118
|
/**
|
|
79
119
|
* The case a POST /api/sessions `workingDir` belongs to, for its default session
|
|
80
120
|
* group: a direct child of the caller's case space, or (admin / single-user, as
|
|
@@ -143,7 +183,7 @@ const MOUSE_TRACKING_PATTERN = /\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[h
|
|
|
143
183
|
* - `strip-full` (claude/codex/gemini): alt-screen toggles, `3J` and mouse DECSETs.
|
|
144
184
|
* xterm obeys the toggles by switching to its scrollback-less alt buffer and wiping
|
|
145
185
|
* saved lines, so history disappeared on tab switch.
|
|
146
|
-
* - `strip-mux-and-mouse` (
|
|
186
|
+
* - `strip-mux-and-mouse` (tmux-backed): alt-screen toggles and mouse DECSETs.
|
|
147
187
|
* A replayed tracking enable parks xterm in report mode, where a drag goes to the CLI
|
|
148
188
|
* instead of selecting text (and Ctrl+C without a selection is opencode's app_exit).
|
|
149
189
|
* `3J` stays: a TUI is not a `clear` consumer.
|
|
@@ -1088,8 +1128,9 @@ options = {}) {
|
|
|
1088
1128
|
const globalNice = await ctx.getGlobalNiceConfig();
|
|
1089
1129
|
const modelConfig = await ctx.getModelConfig();
|
|
1090
1130
|
const mode = body.mode || 'claude';
|
|
1091
|
-
// Synced App Settings launch defaults (capabilities.launchDefaults, codex's model
|
|
1092
|
-
// effort today) fill the CLI's own config object where the caller
|
|
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.
|
|
1093
1134
|
// Local launches only: a remote attach runs whatever the remote pane already runs.
|
|
1094
1135
|
const launchBody = remote ? body : await applyLaunchDefaults(mode, body);
|
|
1095
1136
|
// Where a model override comes from is a capability, and the three answers are
|
|
@@ -1172,6 +1213,169 @@ options = {}) {
|
|
|
1172
1213
|
...(body.tabGroupId || landedInGroup(tabLayout, session.id) ? { tabLayout } : {}),
|
|
1173
1214
|
};
|
|
1174
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
|
+
}
|
|
1175
1379
|
// ========== Rename Session ==========
|
|
1176
1380
|
app.put('/api/sessions/:id/name', async (req) => {
|
|
1177
1381
|
const { id } = req.params;
|
|
@@ -1225,6 +1429,10 @@ options = {}) {
|
|
|
1225
1429
|
if (session.remote || session.docker) {
|
|
1226
1430
|
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Custom model endpoints are not supported for remote (SSH) or Docker sessions yet');
|
|
1227
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
|
+
}
|
|
1228
1436
|
if (session.isBusy()) {
|
|
1229
1437
|
return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
|
|
1230
1438
|
}
|
|
@@ -1247,8 +1455,29 @@ options = {}) {
|
|
|
1247
1455
|
if (!endpoint) {
|
|
1248
1456
|
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Model endpoint not found');
|
|
1249
1457
|
}
|
|
1250
|
-
|
|
1458
|
+
// Discovery's per-model figure wins; the endpoint's user-set default covers models
|
|
1459
|
+
// whose size discovery could not determine (a gateway that states none, say). With
|
|
1460
|
+
// neither, the existing "no override, CLI assumes its own default" behaviour stands.
|
|
1461
|
+
const contextLength = resolveCustomModelContextLength(endpoint, body.modelId);
|
|
1251
1462
|
const slotCount = endpoint.modelSlotCounts?.[body.modelId];
|
|
1463
|
+
// A CLI whose config alone cannot select the model also gets its `model` launch param
|
|
1464
|
+
// forced (pi/omp `custom/<id>`, grok's block name, and codex now carries its bare
|
|
1465
|
+
// selected id too). The argv engine DROPS a token that
|
|
1466
|
+
// fails its pattern rather than quoting it, which would silently launch the CLI on its
|
|
1467
|
+
// own default provider again, so refuse an id the pattern cannot carry up front.
|
|
1468
|
+
// The check runs on the PURE build result, before applyCustomModelInjection below:
|
|
1469
|
+
// the forced launch id only comes out of that build, and writing first would mean a
|
|
1470
|
+
// refused id already replaced the config dir a live codex session uses as CODEX_HOME.
|
|
1471
|
+
// It also runs before the two confirmations, so nobody agrees to evict a model only
|
|
1472
|
+
// to be told the id was never usable.
|
|
1473
|
+
const modelSpec = entry.launch.params.model;
|
|
1474
|
+
const injection = buildCustomModelInjection(entry, endpoint, body.modelId, contextLength);
|
|
1475
|
+
const forcedLaunchModel = injection.kind === 'unsupported' ? undefined : injection.launchModel;
|
|
1476
|
+
if (forcedLaunchModel !== undefined &&
|
|
1477
|
+
modelSpec?.type === 'token' &&
|
|
1478
|
+
!matchesPattern(modelSpec.pattern, forcedLaunchModel)) {
|
|
1479
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(body.modelId)} cannot be passed to ${session.mode} on its command line`);
|
|
1480
|
+
}
|
|
1252
1481
|
// Some CLIs (today: only claude) carry enough of their own fixed system-prompt/tool-
|
|
1253
1482
|
// schema overhead that a small enough real context guarantees a first-message failure
|
|
1254
1483
|
// no matter what CLAUDE_CODE_MAX_CONTEXT_TOKENS says — confirmed live at ~36.4K tokens
|
|
@@ -1306,21 +1535,10 @@ options = {}) {
|
|
|
1306
1535
|
return { requiresConfirmation: true, currentlyLoadedModel: currentlyLoaded, affectedSessions };
|
|
1307
1536
|
}
|
|
1308
1537
|
}
|
|
1309
|
-
|
|
1310
|
-
// forced (pi/omp `custom/<id>`, grok's block name). The argv engine DROPS a token that
|
|
1311
|
-
// fails its pattern rather than quoting it, which would silently launch the CLI on its
|
|
1312
|
-
// own default provider again, so refuse an id the pattern cannot carry up front.
|
|
1313
|
-
const modelSpec = entry.launch.params.model;
|
|
1314
|
-
const applied = applyCustomModelInjection(entry, endpoint, body.modelId, session.id, contextLength, session.workingDir);
|
|
1538
|
+
const applied = applyCustomModelInjection(entry, endpoint, body.modelId, session.id, contextLength, session.workingDir, await guardSessionConfigDirHome(entry, (envVar) => session.ownEnvValue(envVar)));
|
|
1315
1539
|
if (!applied) {
|
|
1316
1540
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `${session.mode} has no known custom-model mechanism`);
|
|
1317
1541
|
}
|
|
1318
|
-
if (applied.launchModel !== undefined &&
|
|
1319
|
-
modelSpec?.type === 'token' &&
|
|
1320
|
-
!matchesPattern(modelSpec.pattern, applied.launchModel)) {
|
|
1321
|
-
removeConfigDir(applied.configDir);
|
|
1322
|
-
return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(body.modelId)} cannot be passed to ${session.mode} on its command line`);
|
|
1323
|
-
}
|
|
1324
1542
|
const { previousConfigDir } = session.setCustomModel({
|
|
1325
1543
|
endpointId: endpoint.id,
|
|
1326
1544
|
modelId: body.modelId,
|
|
@@ -1328,6 +1546,7 @@ options = {}) {
|
|
|
1328
1546
|
envKeys: applied.envKeys,
|
|
1329
1547
|
configDir: applied.configDir,
|
|
1330
1548
|
launchModel: applied.launchModel,
|
|
1549
|
+
homeEnvVar: applied.homeEnvVar,
|
|
1331
1550
|
}, applied.envOverrides);
|
|
1332
1551
|
// Clean up the OLD config dir on disk, unless the new one happens to reuse the same
|
|
1333
1552
|
// path (same session, configDir kind again) — never delete the dir we just wrote.
|
|
@@ -1361,7 +1580,9 @@ options = {}) {
|
|
|
1361
1580
|
// Called for its existence/ownership 404 side effect only — demoteOrRemoveSession
|
|
1362
1581
|
// below re-looks-up the record by id, so the returned SessionState is unused here.
|
|
1363
1582
|
findPersistedSessionOrFail(ctx.store, id, req);
|
|
1364
|
-
|
|
1583
|
+
// The edit history goes with the record (same rule as `cleanupSession`).
|
|
1584
|
+
if (ctx.store.demoteOrRemoveSession(id) === 'removed')
|
|
1585
|
+
ctx.forgetEditHistory(id);
|
|
1365
1586
|
// A detached session kept its agent inbox (server.ts _doCleanupSession drops it
|
|
1366
1587
|
// only on killMux); retiring the row is the delete, so the mail goes now.
|
|
1367
1588
|
agentInbox.drop(id);
|
|
@@ -1465,6 +1686,11 @@ options = {}) {
|
|
|
1465
1686
|
const { id } = req.params;
|
|
1466
1687
|
const { prompt } = parseBody(RunPromptSchema, req.body);
|
|
1467
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
|
+
}
|
|
1468
1694
|
if (session.isBusy()) {
|
|
1469
1695
|
return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
|
|
1470
1696
|
}
|
|
@@ -1505,6 +1731,10 @@ options = {}) {
|
|
|
1505
1731
|
// about shell, and they disagree upstream too — reconciling them is a behaviour change
|
|
1506
1732
|
// and belongs in its own PR, not in a refactor that is meant to change nothing.
|
|
1507
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 &&
|
|
1508
1738
|
ctx.store.getConfig().ralphEnabled &&
|
|
1509
1739
|
!session.ralphTracker.autoEnableDisabled) {
|
|
1510
1740
|
autoConfigureRalph(session, session.workingDir, ctx);
|
|
@@ -2372,7 +2602,10 @@ options = {}) {
|
|
|
2372
2602
|
* effort: the tab rename has already happened and must not fail on this.
|
|
2373
2603
|
*/
|
|
2374
2604
|
async function syncClaudeTitle(session) {
|
|
2375
|
-
if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' ||
|
|
2605
|
+
if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' ||
|
|
2606
|
+
session.remote ||
|
|
2607
|
+
session.docker ||
|
|
2608
|
+
session.isAdopted)
|
|
2376
2609
|
return;
|
|
2377
2610
|
try {
|
|
2378
2611
|
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
|
|
@@ -2451,15 +2684,24 @@ options = {}) {
|
|
|
2451
2684
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2452
2685
|
return candidates[0] ?? null;
|
|
2453
2686
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2687
|
+
// The body of GET /api/sessions/:id/last-response, so the Discord bridge reads the same transcript
|
|
2688
|
+
// the response viewer does (per-CLI reader, pane fallback) without a second copy of this logic.
|
|
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
|
+
}
|
|
2457
2700
|
// Codex sessions don't write to ~/.claude/projects — their transcripts
|
|
2458
2701
|
// live in ~/.codex/sessions/**. Branch to a Codex-specific reader so the
|
|
2459
2702
|
// response-viewer works for Codex panes too.
|
|
2460
2703
|
if (getCli(session.mode)?.capabilities.transcript === 'codex-rollout') {
|
|
2461
|
-
|
|
2462
|
-
return await readCodexLastResponse(session, codexQuery.context === 'full');
|
|
2704
|
+
return await readCodexLastResponse(session, context === 'full');
|
|
2463
2705
|
}
|
|
2464
2706
|
// DeepSeek Harness writes a real structured transcript under
|
|
2465
2707
|
// `$DSH_HOME/sessions/**`, so read that rather than segmenting the pane.
|
|
@@ -2477,8 +2719,7 @@ options = {}) {
|
|
|
2477
2719
|
// would starve on an answer that exists. Those configurations keep the
|
|
2478
2720
|
// pane segmenter below: coarse, but the real conversation.
|
|
2479
2721
|
if (getCli(session.mode)?.capabilities.transcript === 'deepseek-zstd' && !session.docker && !session.remote) {
|
|
2480
|
-
const
|
|
2481
|
-
const full = deepSeekQuery.context === 'full';
|
|
2722
|
+
const full = context === 'full';
|
|
2482
2723
|
const transcript = await readDeepSeekLastResponse(session, { blocks: full });
|
|
2483
2724
|
if (transcript) {
|
|
2484
2725
|
return {
|
|
@@ -2495,7 +2736,6 @@ options = {}) {
|
|
|
2495
2736
|
// the pane IS the transcript for these CLIs. Codex is already handled above,
|
|
2496
2737
|
// where a real rollout file is the better source.
|
|
2497
2738
|
if (isExternalCliTranscriptMode(session.mode)) {
|
|
2498
|
-
const externalQuery = req.query;
|
|
2499
2739
|
// A repainting full-screen TUI (copilot) streams absolute cursor moves, not lines, so the
|
|
2500
2740
|
// buffer cannot be segmented; read the pane's visible screen instead, and fall back to the
|
|
2501
2741
|
// buffer when there is no tmux pane to ask (direct PTY, tests).
|
|
@@ -2507,7 +2747,7 @@ options = {}) {
|
|
|
2507
2747
|
text: getLastTranscriptResponse(blocks),
|
|
2508
2748
|
timestamp: '',
|
|
2509
2749
|
hasContext: blocks.length > 0,
|
|
2510
|
-
messages:
|
|
2750
|
+
messages: context === 'full' ? blocks : undefined,
|
|
2511
2751
|
};
|
|
2512
2752
|
}
|
|
2513
2753
|
// Scan ~/.claude/projects/*/ for the transcript file
|
|
@@ -2528,11 +2768,10 @@ options = {}) {
|
|
|
2528
2768
|
void persistDockerCaseClaudeSessionId(CODEMAN_CONFIG_DIR, session.docker.containerName, activeId).catch(() => { });
|
|
2529
2769
|
}
|
|
2530
2770
|
}
|
|
2531
|
-
const query = req.query;
|
|
2532
2771
|
// `turn` is the brief view's context: the last ANSWERED turn's assistant
|
|
2533
2772
|
// messages, so a multi-row answer is not reduced to its final row. `text`
|
|
2534
2773
|
// stays the last assistant row in every mode (agent pollers hash it).
|
|
2535
|
-
const wantsMessages =
|
|
2774
|
+
const wantsMessages = context === 'full' || context === 'turn';
|
|
2536
2775
|
const claudeSessionId = session.claudeSessionId || session.id;
|
|
2537
2776
|
const transcript = await findClaudeTranscript(projectsDir, claudeSessionId, session.id);
|
|
2538
2777
|
if (!transcript) {
|
|
@@ -2547,7 +2786,7 @@ options = {}) {
|
|
|
2547
2786
|
try {
|
|
2548
2787
|
const content = await fs.readFile(transcript.path, 'utf8');
|
|
2549
2788
|
const parsed = parseClaudeResponseTranscript(content, wantsMessages);
|
|
2550
|
-
if (
|
|
2789
|
+
if (context === 'turn') {
|
|
2551
2790
|
return {
|
|
2552
2791
|
text: parsed.text,
|
|
2553
2792
|
timestamp: parsed.timestamp,
|
|
@@ -2559,6 +2798,12 @@ options = {}) {
|
|
|
2559
2798
|
catch {
|
|
2560
2799
|
return wantsMessages ? { text: '', timestamp: '', messages: [] } : { text: '', timestamp: '' };
|
|
2561
2800
|
}
|
|
2801
|
+
};
|
|
2802
|
+
ctx.lastResponseFor = lastResponseFor;
|
|
2803
|
+
app.get('/api/sessions/:id/last-response', async (req) => {
|
|
2804
|
+
const { id } = req.params;
|
|
2805
|
+
const session = findSessionOrFail(ctx, id, req);
|
|
2806
|
+
return lastResponseFor(session, req.query.context);
|
|
2562
2807
|
});
|
|
2563
2808
|
function isCodexInjectedContext(text) {
|
|
2564
2809
|
return (/^# AGENTS\.md instructions\b/i.test(text) ||
|
|
@@ -3120,6 +3365,13 @@ options = {}) {
|
|
|
3120
3365
|
const { id } = req.params;
|
|
3121
3366
|
const body = parseBody(AutoClearSchema, req.body, 'Invalid request body');
|
|
3122
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
|
+
}
|
|
3123
3375
|
session.setAutoClear(body.enabled, body.threshold);
|
|
3124
3376
|
persistAndBroadcastSession(ctx, session);
|
|
3125
3377
|
return {
|
|
@@ -3137,6 +3389,13 @@ options = {}) {
|
|
|
3137
3389
|
const { id } = req.params;
|
|
3138
3390
|
const body = parseBody(AutoCompactSchema, req.body, 'Invalid request body');
|
|
3139
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
|
+
}
|
|
3140
3399
|
session.setAutoCompact(body.enabled, body.threshold, body.prompt);
|
|
3141
3400
|
persistAndBroadcastSession(ctx, session);
|
|
3142
3401
|
return {
|
|
@@ -3188,8 +3447,8 @@ options = {}) {
|
|
|
3188
3447
|
const { id } = req.params;
|
|
3189
3448
|
const body = parseBody(GitFetchSettingsSchema, req.body, 'Invalid request body');
|
|
3190
3449
|
const session = findSessionOrFail(ctx, id, req);
|
|
3191
|
-
if (session.remote || session.docker) {
|
|
3192
|
-
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git fetch does not run for remote or
|
|
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');
|
|
3193
3452
|
}
|
|
3194
3453
|
const enabled = 'enabled' in body ? (body.enabled ?? undefined) : session.gitFetchEnabled;
|
|
3195
3454
|
const intervalSeconds = 'intervalSeconds' in body ? (body.intervalSeconds ?? undefined) : session.gitFetchIntervalSeconds;
|
|
@@ -3210,6 +3469,13 @@ options = {}) {
|
|
|
3210
3469
|
const { id } = req.params;
|
|
3211
3470
|
const body = parseBody(AutoResumeSchema, req.body, 'Invalid request body');
|
|
3212
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
|
+
}
|
|
3213
3479
|
session.setAutoResume(body.enabled);
|
|
3214
3480
|
persistAndBroadcastSession(ctx, session);
|
|
3215
3481
|
return {
|
|
@@ -3271,6 +3537,10 @@ options = {}) {
|
|
|
3271
3537
|
if (body.enabled && session.remote) {
|
|
3272
3538
|
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Image watching does not run for remote sessions');
|
|
3273
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
|
+
}
|
|
3274
3544
|
if (body.enabled) {
|
|
3275
3545
|
imageWatcher.watchSession(session.id, session.workingDir);
|
|
3276
3546
|
}
|
|
@@ -3786,12 +4056,12 @@ options = {}) {
|
|
|
3786
4056
|
// unknown endpoint, a model id the CLI's argv pattern can't carry) rather than trusting
|
|
3787
4057
|
// a lighter version of them, since this is the same server-side authority reached a
|
|
3788
4058
|
// different way, not a separate, less-checked path.
|
|
3789
|
-
|
|
3790
|
-
// Only the INJECTED keys (never the caller's envOverrides merged in)
|
|
3791
|
-
//
|
|
3792
|
-
//
|
|
3793
|
-
//
|
|
3794
|
-
//
|
|
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.
|
|
3795
4065
|
let qsCustomModelAppliedEnvOverrides;
|
|
3796
4066
|
let qsCustomModelLaunchModel;
|
|
3797
4067
|
let qsCustomModelSessionId;
|
|
@@ -3808,8 +4078,21 @@ options = {}) {
|
|
|
3808
4078
|
const cmEndpoint = cmHosts.find((h) => h.id === customModel.endpointId);
|
|
3809
4079
|
if (!cmEndpoint)
|
|
3810
4080
|
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Model endpoint not found');
|
|
3811
|
-
|
|
4081
|
+
// Per-model discovery figure first, user-set endpoint default second, nothing third
|
|
4082
|
+
// (see the dedicated route's own comment above).
|
|
4083
|
+
const cmContextLength = resolveCustomModelContextLength(cmEndpoint, customModel.modelId);
|
|
3812
4084
|
const cmSlotCount = cmEndpoint.modelSlotCounts?.[customModel.modelId];
|
|
4085
|
+
// See the dedicated route: refused on the pure build, before either confirmation, so
|
|
4086
|
+
// nobody agrees to evict a model for an id that could never launch, and before anything
|
|
4087
|
+
// is written.
|
|
4088
|
+
const cmModelSpec = cmEntry.launch.params.model;
|
|
4089
|
+
const cmInjection = buildCustomModelInjection(cmEntry, cmEndpoint, customModel.modelId, cmContextLength);
|
|
4090
|
+
const cmForcedLaunchModel = cmInjection.kind === 'unsupported' ? undefined : cmInjection.launchModel;
|
|
4091
|
+
if (cmForcedLaunchModel !== undefined &&
|
|
4092
|
+
cmModelSpec?.type === 'token' &&
|
|
4093
|
+
!matchesPattern(cmModelSpec.pattern, cmForcedLaunchModel)) {
|
|
4094
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(customModel.modelId)} cannot be passed to ${mode} on its command line`);
|
|
4095
|
+
}
|
|
3813
4096
|
// See the dedicated route's own comment for the full reasoning: some CLIs' own fixed
|
|
3814
4097
|
// overhead can exceed a small enough real context on the very first message,
|
|
3815
4098
|
// regardless of contextLengthVar. Warn before creating a session that's certain to
|
|
@@ -3861,18 +4144,10 @@ options = {}) {
|
|
|
3861
4144
|
// with, not a placeholder: `new Session({ id: ... })` accepts an explicit id for
|
|
3862
4145
|
// exactly this reason.
|
|
3863
4146
|
qsCustomModelSessionId = randomUUID();
|
|
3864
|
-
const cmApplied = applyCustomModelInjection(cmEntry, cmEndpoint, customModel.modelId, qsCustomModelSessionId, cmContextLength, workspacePath);
|
|
4147
|
+
const cmApplied = applyCustomModelInjection(cmEntry, cmEndpoint, customModel.modelId, qsCustomModelSessionId, cmContextLength, workspacePath, await guardSessionConfigDirHome(cmEntry, (envVar) => qsGatedEnvOverrides?.[envVar]));
|
|
3865
4148
|
if (!cmApplied) {
|
|
3866
4149
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `${mode} has no known custom-model mechanism`);
|
|
3867
4150
|
}
|
|
3868
|
-
const cmModelSpec = cmEntry.launch.params.model;
|
|
3869
|
-
if (cmApplied.launchModel !== undefined &&
|
|
3870
|
-
cmModelSpec?.type === 'token' &&
|
|
3871
|
-
!matchesPattern(cmModelSpec.pattern, cmApplied.launchModel)) {
|
|
3872
|
-
removeConfigDir(cmApplied.configDir);
|
|
3873
|
-
return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(customModel.modelId)} cannot be passed to ${mode} on its command line`);
|
|
3874
|
-
}
|
|
3875
|
-
qsCustomModelEnvOverrides = { ...qsGatedEnvOverrides, ...cmApplied.envOverrides };
|
|
3876
4151
|
qsCustomModelAppliedEnvOverrides = cmApplied.envOverrides;
|
|
3877
4152
|
qsCustomModelLaunchModel = cmApplied.launchModel;
|
|
3878
4153
|
qsCustomModelBookkeeping = {
|
|
@@ -3882,6 +4157,7 @@ options = {}) {
|
|
|
3882
4157
|
envKeys: cmApplied.envKeys,
|
|
3883
4158
|
configDir: cmApplied.configDir,
|
|
3884
4159
|
launchModel: cmApplied.launchModel,
|
|
4160
|
+
homeEnvVar: cmApplied.homeEnvVar,
|
|
3885
4161
|
};
|
|
3886
4162
|
// Actually kick off llama-swap's load now — see the dedicated apply route's own
|
|
3887
4163
|
// comment on triggerLlamaSwapLoad for why this can't just wait on the launched CLI's
|
|
@@ -3912,7 +4188,7 @@ options = {}) {
|
|
|
3912
4188
|
copilotConfig: mode === 'copilot' ? qsGatedCopilotConfig : undefined,
|
|
3913
4189
|
deepSeekConfig: mode === 'deepseek' ? qsGatedDeepSeekConfig : undefined,
|
|
3914
4190
|
ompConfig: qsResolvedOmpConfig,
|
|
3915
|
-
envOverrides:
|
|
4191
|
+
envOverrides: qsGatedEnvOverrides,
|
|
3916
4192
|
effort,
|
|
3917
4193
|
advisorModel,
|
|
3918
4194
|
remote,
|
|
@@ -3941,10 +4217,10 @@ options = {}) {
|
|
|
3941
4217
|
}
|
|
3942
4218
|
const session = new Session(qsSessionOptions);
|
|
3943
4219
|
// Records the selection for session.customModel/getCustomModelForPersist() and future
|
|
3944
|
-
// clear/switch calls
|
|
3945
|
-
// the launch above (
|
|
3946
|
-
// this is
|
|
3947
|
-
// 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).
|
|
3948
4224
|
if (qsCustomModelBookkeeping) {
|
|
3949
4225
|
session.setCustomModel(qsCustomModelBookkeeping, qsCustomModelAppliedEnvOverrides);
|
|
3950
4226
|
}
|
|
@@ -5016,6 +5292,13 @@ options = {}) {
|
|
|
5016
5292
|
reply.code(400);
|
|
5017
5293
|
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Prompt uploads are not supported for remote (SSH) sessions');
|
|
5018
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
|
+
}
|
|
5019
5302
|
// The upload is written under session.workingDir (ensureUploadDir below), and
|
|
5020
5303
|
// a live session's workspace can sit on a network mount that stopped answering.
|
|
5021
5304
|
// Probe it bounded before the first disk touch, as #516 requires for a path the
|