fix(custom-model): keep the in-use picker honest about down endpoints and exited panes

Review round on PR #757. The route turns EVERY upstream failure into
isLlamaSwap: false, so the old line silently promised an unreachable
endpoint was merely mute, and the live-session check missed a null pid
(the PTY exited, the same test keep-warm applies).

- Require pid !== null in _getCustomModelInUseBySession, pinned by a
  'process exited' case in the marks-nothing table
- Reword #customModelPickUnknown to claim only that nothing was
  reported, true for a down endpoint too; drop the 'stays quiet for a
  failed probe' promise from changeset, docs and comments
- architecture-invariants: add the In-use tier, both pin fences
  (customModel not displayModel; stopped/null-pid/paneExit never
  count) and the unknown-line rule
- Mirror the wording through i18n (key + zh-CN), index.html fixture,
  styles.css comment, and the run-menu tests
This commit is contained in:
Devvyn committed 2026-11-02 19:15:37 +08:00
1 parent 4623ee0d12
commit 4aeab0d9ce
8 files changed
+62 -40

No files matched your search

+2 -2
View File
@@ -629,8 +629,8 @@
'Currently loaded': '当前已加载',
'In use by a session': '会话正在使用',
'Last used': '上次使用',
'Nothing is marked: this endpoint cannot report which model it has loaded, and no running session uses one. Pick the model to launch, or start a session on this endpoint first.':
'未标记任何模型:此端点无法报告它当前加载了哪个模型,并且没有运行中的会话正在使用它。请选择要启动的模型,或先在该端点上启动一个会话。',
'Nothing is marked: this endpoint did not report which model it has loaded, and none of your running sessions uses one. Pick the model to launch.':
'未标记任何模型:此端点没有报告它已加载了哪个模型,你运行中的会话也没有在使用它。请选择要启动的模型。',
'That endpoint no longer exists': '该端点已不存在',
'No models discovered for this endpoint yet': '此端点尚未发现任何模型',
// Model picker search box (index.html placeholder/aria-label and the no-matches line)
+1 -1
View File
@@ -1045,7 +1045,7 @@
answered that it is not llama-swap (see _openCustomModelPickModal): it says out
loud WHY nothing is marked, in the user's language, rather than leaving an
alphabetical wall that looks like the question was ignored. -->
<p class="form-hint custom-model-pick-unknown" id="customModelPickUnknown" role="status" hidden>Nothing is marked: this endpoint cannot report which model it has loaded, and no running session uses one. Pick the model to launch, or start a session on this endpoint first.</p>
<p class="form-hint custom-model-pick-unknown" id="customModelPickUnknown" role="status" hidden>Nothing is marked: this endpoint did not report which model it has loaded, and none of your running sessions uses one. Pick the model to launch.</p>
<div id="customModelPickList" class="run-mode-custom-models"></div>
</div>
</div>
+21 -13
View File
@@ -1011,10 +1011,13 @@ Object.assign(CodemanApp.prototype, {
* `_watchLlamaSwapLoading`'s own check.
*
* Resolves the raw route payload rather than a model id so the caller can tell the
* three answers apart: llama-swap naming a model, an endpoint that positively cannot
* say (safe to explain in the UI), and no answer at all (timeout/unreachable, which
* resolves `null` and must NEVER be described as "cannot report", since the endpoint
* may well have been asleep rather than mute). Never throws: a failed probe should
* three answers apart: llama-swap naming a model; `isLlamaSwap: false`, which is what
* the route answers for EVERY failure it catches (a server without a `/running`
* route, a refused or unresolvable endpoint, a 401 from a wrong key) — safe to
* explain in the UI as long as the wording also fits an endpoint that is down; and
* `null`, only when Codeman's own request to the route failed or the client-side
* bound below won the race, which stays silent, since the endpoint may merely have
* been asleep. Never throws: a failed probe should
* just skip promotion, not break the picker.
*
* ⚠️ Client-side bounded to ~800ms via Promise.race, on top of (never instead of) the
@@ -1061,10 +1064,12 @@ Object.assign(CodemanApp.prototype, {
* is display text scraped off a CLI's footer or statusline for a session header (pane
* derived, `source: 'screen'`), and promoting a row because a terminal printed a
* similar string is worse than showing nothing.
* ⚠️ Requires a live pane: `status: 'stopped'` or a recorded `paneExit` means nothing
* is on the endpoint any more, and a dead session's stale pin is exactly what an "in
* use" claim must not resurrect. `session:updated` keeps it current, so a session
* cleared back to the CLI's own backend or moved to another endpoint stops counting.
* ⚠️ Requires a live pane: `status: 'stopped'`, a null `pid` (the PTY exited; the
* same live-pane test keep-warm applies, `src/prompt-cache-keepwarm.ts`) or a recorded
* `paneExit` means nothing is on the endpoint any more, and a dead session's stale pin
* is exactly what an "in use" claim must not resurrect. `session:updated` keeps it
* current, so a session cleared back to the CLI's own backend or moved to another
* endpoint stops counting.
* ⚠️ A since-deleted endpoint simply matches no host, so no extra liveness check is
* needed here.
*/
@@ -1073,7 +1078,7 @@ Object.assign(CodemanApp.prototype, {
if (models.length === 0) return null;
let newest = null;
for (const [, session] of this.sessions || []) {
if (!session || session.status === 'stopped' || session.paneExit) continue;
if (!session || session.status === 'stopped' || session.pid === null || session.paneExit) continue;
const pinned = session.customModel;
if (!pinned || pinned.endpointId !== host.id) continue;
if (!models.includes(pinned.modelId)) continue; // undiscovered since: no row to promote
@@ -1129,10 +1134,13 @@ Object.assign(CodemanApp.prototype, {
`${cliLabel} → ${host.label} — ${(host.models || []).length} models discovered.`;
// A plain static line (its markup lives in index.html), shown when NOTHING is known
// about any row: 40 alphabetical rows with no mark on any of them otherwise read as
// the app ignoring the question. It only asserts what the probe actually answered:
// `isLlamaSwap: false` is a real "I cannot tell you", where a probe that timed out or
// failed (`runningStatus === null`) is not, the box may merely be asleep, so an
// unreachable endpoint stays silent instead of explaining something it does not know.
// the app ignoring the question. `isLlamaSwap: false` is what the route answers for
// every failure it catches: a server that cannot report AND a down or refused one
// alike (`getLlamaSwapStatus`, custom-model-routes.ts), so the line's wording
// promises only that nothing was reported, which stays true either way. A probe
// that never got an answer at all (`runningStatus === null`: Codeman's own request
// failed or the 800ms bound won) keeps the line hidden, the box may merely be
// asleep.
// A remembered id that has since vanished from host.models is no promotion: count
// only a promoted id that is actually a row, or a stale "last used" would both skip
// tagging any row and hide this line, leaving 40 unmarked rows with no explanation.
+3 -3
View File
@@ -7824,9 +7824,9 @@ html[data-skin="og"] .welcome-primary:hover {
padding: 1rem 0;
}
/* "This endpoint cannot report which model it has loaded": the same `.form-hint`
`display: block` beats `[hidden]` trap the two rules below fix for the no-matches
line and the filtered rows, so the attribute needs the same re-assertion. */
/* "Nothing is marked: this endpoint did not report which model it has loaded": the same
`.form-hint` `display: block` beats `[hidden]` trap the two rules below fix for the
no-matches line and the filtered rows, so the attribute needs the same re-assertion. */
#customModelPickModal .custom-model-pick-unknown {
padding-bottom: 0.15rem;
}