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.
- package/README.md +12 -3
- package/dist/config/cli-registry/schema.d.ts +1 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +5 -1
- 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 +71 -5
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +4 -4
- 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/custom-model-injection-apply.d.ts +4 -2
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +35 -21
- package/dist/custom-model-injection-apply.js.map +1 -1
- 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/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-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/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 +12 -1
- 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 +19 -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 +91 -21
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +190 -33
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +74 -12
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +256 -41
- package/dist/tmux-manager.js.map +1 -1
- 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 +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +106 -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/edit-history-sync.d.ts +27 -7
- package/dist/web/edit-history-sync.d.ts.map +1 -1
- package/dist/web/edit-history-sync.js +35 -19
- package/dist/web/edit-history-sync.js.map +1 -1
- 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 +4 -1
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +4 -2
- package/dist/web/paste-image-gc.js.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.963b5ba8.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.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +27 -14
- 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 +36 -17
- 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.d4ec5957.js → keyboard-accessory.b941676c.js} +3 -1
- package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
- package/dist/web/public/{keyboard-accessory.d4ec5957.js.gz → 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.5ef2ce85.css → mobile.67c8eb5b.css} +1 -1
- package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
- package/dist/web/public/{mobile.5ef2ce85.css.gz → 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.b100727d.js → panels-ui.8bcd4473.js} +4 -4
- 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.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.5ac82f2c.js → session-ui.a7c8c095.js} +11 -11
- 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.d1d4fb99.js → settings-ui.18f92052.js} +3 -3
- 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.77dc538f.css → styles.6a718331.css} +1 -1
- package/dist/web/public/styles.6a718331.css.br +0 -0
- package/dist/web/public/{styles.77dc538f.css.gz → 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.d7c75ce8.js → terminal-ui.a7883640.js} +2 -2
- 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 -9
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +72 -53
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.js +1 -1
- package/dist/web/routes/discord-routes.js.map +1 -1
- package/dist/web/routes/edit-history-routes.d.ts +2 -2
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -1
- package/dist/web/routes/edit-history-routes.js +4 -3
- package/dist/web/routes/edit-history-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +213 -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/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 +11 -8
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +262 -29
- 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 +3 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +24 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +34 -9
- 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/server.d.ts +16 -3
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +84 -24
- 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.963b5ba8.js.br +0 -0
- package/dist/web/public/app.963b5ba8.js.gz +0 -0
- package/dist/web/public/i18n.33f930d7.js +0 -2
- package/dist/web/public/i18n.33f930d7.js.br +0 -0
- package/dist/web/public/i18n.33f930d7.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
- package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
- package/dist/web/public/panels-ui.b100727d.js.br +0 -0
- package/dist/web/public/panels-ui.b100727d.js.gz +0 -0
- package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
- package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
- package/dist/web/public/styles.77dc538f.css.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
- 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`,
|
|
84
|
-
* `linkFromRealHome` recipe). A path on an
|
|
85
|
-
* stat wait forever, and this runs INSIDE a
|
|
86
|
-
* web server (CLAUDE.md, the #516 rule on
|
|
87
|
-
* the apply, which already refuses to join
|
|
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
|
-
|
|
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,
|
|
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` (
|
|
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
|
|
1122
|
-
// 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.
|
|
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.
|
|
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' ||
|
|
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
|
|
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
|
-
|
|
3834
|
-
// Only the INJECTED keys (never the caller's envOverrides merged in)
|
|
3835
|
-
//
|
|
3836
|
-
//
|
|
3837
|
-
//
|
|
3838
|
-
//
|
|
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:
|
|
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
|
|
3995
|
-
// the launch above (
|
|
3996
|
-
// this is
|
|
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
|