switchroom 0.19.33 โ 0.19.35
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/dist/agent-scheduler/index.js +1 -0
- package/dist/auth-broker/index.js +11088 -10989
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +10947 -10361
- package/dist/host-control/main.js +9512 -9410
- package/dist/vault/approvals/kernel-server.js +454 -423
- package/dist/vault/broker/server.js +1658 -1636
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +172 -27
- package/telegram-plugin/gateway/gateway.ts +5 -2
- package/telegram-plugin/gateway/model-command.ts +245 -23
- package/telegram-plugin/tests/model-command.test.ts +415 -4
- package/vendor/hindsight-memory/scripts/subagent_retain.py +103 -7
- package/vendor/hindsight-memory/scripts/tests/test_subagent_retain_learnings.py +377 -0
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type DiscoverResult,
|
|
34
34
|
type ModelPickerOption,
|
|
35
35
|
} from '../../src/agents/model-picker.js'
|
|
36
|
+
import { assessThinkingEffortRisk } from '../../src/config/thinking-effort-risk.js'
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Aliases the claude CLI resolves natively (`claude --help`: "an alias for
|
|
@@ -50,6 +51,34 @@ import {
|
|
|
50
51
|
*/
|
|
51
52
|
export const MODEL_ALIASES = ['opus', 'sonnet', 'haiku', 'fable', 'default'] as const
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Short SWITCHROOM-side spellings for pinned **Anthropic** model ids. These are
|
|
56
|
+
* expanded to the full `claude-*` id on the `/model` path BEFORE any
|
|
57
|
+
* Claude-vs-external classification runs, so what reaches the `.session-model`
|
|
58
|
+
* carrier (and therefore `claude --model`) is always the canonical id โ the CLI
|
|
59
|
+
* never sees the short spelling.
|
|
60
|
+
*
|
|
61
|
+
* Deliberately NOT `SR_MODEL_ALIASES`: that map means "external / OpenRouter /
|
|
62
|
+
* the Anthropic OAuth header is NOT forwarded". These targets are Anthropic
|
|
63
|
+
* OAuth-passthrough models, so putting them there would flip the
|
|
64
|
+
* header-forwarding + external-billing classification and surface them under the
|
|
65
|
+
* "๐ External models" keyboard page with an `srFriendlyLabel`.
|
|
66
|
+
*
|
|
67
|
+
* Also NOT `MODEL_ALIASES`: that list is the aliases the claude CLI resolves
|
|
68
|
+
* *itself*, and its members double as FAMILY tokens in `modelFamilyToken` /
|
|
69
|
+
* `canonicalClaudeToken` / `servedModelMatchesRequested`. A pinned-id shortcut
|
|
70
|
+
* is neither.
|
|
71
|
+
*
|
|
72
|
+
* NB the repo prefers family aliases over pinned ids (an alias tracks the
|
|
73
|
+
* current flagship, a pinned id goes stale) โ these exist because an operator
|
|
74
|
+
* sometimes wants a specific pinned Opus, and typing `claude-opus-4-8` on a
|
|
75
|
+
* phone is hostile. Keys are matched case-insensitively.
|
|
76
|
+
*/
|
|
77
|
+
export const CLAUDE_MODEL_ALIASES: Record<string, string> = {
|
|
78
|
+
opus48: 'claude-opus-4-8',
|
|
79
|
+
'opus-4-8': 'claude-opus-4-8',
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
/**
|
|
54
83
|
* Shape gate for the model argument. This string is typed literally
|
|
55
84
|
* into the agent's tmux pane, so the gate is strict by construction:
|
|
@@ -76,11 +105,15 @@ export function isSrModel(name: string): boolean {
|
|
|
76
105
|
* True when `name` is a Claude model โ either a well-known alias or a
|
|
77
106
|
* full `claude-*` id (including `[1m]` variants). This is intentionally
|
|
78
107
|
* broader than MODEL_ALIASES: any `claude-โฆ` string from claude's own
|
|
79
|
-
* picker (e.g. `claude-opus-4-8`) qualifies
|
|
108
|
+
* picker (e.g. `claude-opus-4-8`) qualifies, as does a switchroom-side short
|
|
109
|
+
* spelling of a pinned Claude id (CLAUDE_MODEL_ALIASES, e.g. `opus48`) โ those
|
|
110
|
+
* name an Anthropic OAuth-passthrough model, so a classification consumer must
|
|
111
|
+
* never mistake one for an external/OpenRouter route.
|
|
80
112
|
*/
|
|
81
113
|
export function isClaudeModel(name: string): boolean {
|
|
82
114
|
const lower = name.toLowerCase()
|
|
83
115
|
if ((MODEL_ALIASES as readonly string[]).includes(lower)) return true
|
|
116
|
+
if (lower in CLAUDE_MODEL_ALIASES) return true
|
|
84
117
|
return lower.startsWith('claude-')
|
|
85
118
|
}
|
|
86
119
|
|
|
@@ -621,7 +654,7 @@ export function planModelCommand(
|
|
|
621
654
|
): ModelCommandDisposition {
|
|
622
655
|
if (parsed.kind === 'show' && ctx.menuEnabled) return { kind: 'menu' }
|
|
623
656
|
if (parsed.kind === 'set' && isModelCommandBusy(ctx)) {
|
|
624
|
-
return { kind: 'queue', target:
|
|
657
|
+
return { kind: 'queue', target: expandModelAlias(parsed.model) }
|
|
625
658
|
}
|
|
626
659
|
return { kind: 'apply', parsed }
|
|
627
660
|
}
|
|
@@ -662,6 +695,24 @@ export interface ModelCommandDeps {
|
|
|
662
695
|
* rendered as "default".
|
|
663
696
|
*/
|
|
664
697
|
getConfiguredModel: () => string | null
|
|
698
|
+
/**
|
|
699
|
+
* The agent's cascade-resolved `thinking_effort` from `switchroom agent list`
|
|
700
|
+
* โ the value start.sh bakes into `--effort` on the relaunch this command is
|
|
701
|
+
* about to schedule. Null when unset / unreadable (treated as the scaffold
|
|
702
|
+
* floor `low`, i.e. safe).
|
|
703
|
+
*
|
|
704
|
+
* Needed because a `/model` switch is SESSION-scoped and therefore invisible
|
|
705
|
+
* to `switchroom doctor`, whose `thinking_effort ร adaptive model` check
|
|
706
|
+
* (`src/cli/doctor.ts:572`) reads the CONFIGURED `model:` out of
|
|
707
|
+
* switchroom.yaml. Switching the live session onto a pinned Opus 4.x id โ by
|
|
708
|
+
* typing it, by a `CLAUDE_MODEL_ALIASES` shortcut, or by tapping the
|
|
709
|
+
* "Opus 4.8" button โ lands the exact (pinned Opus 4.x, effort > low)
|
|
710
|
+
* combination doctor exists to warn about, with doctor none the wiser. So the
|
|
711
|
+
* assessment is re-run HERE, at switch time, against the effort the relaunch
|
|
712
|
+
* will actually use. A relaunch writes no `.session-effort` carrier, so any
|
|
713
|
+
* live `/effort` override is shed and the configured value is the right input.
|
|
714
|
+
*/
|
|
715
|
+
getConfiguredEffort: () => string | null
|
|
665
716
|
escapeHtml: (s: string) => string
|
|
666
717
|
preBlock: (s: string) => string
|
|
667
718
|
/**
|
|
@@ -721,14 +772,34 @@ export interface ModelCommandReply {
|
|
|
721
772
|
const PERSIST_NOTE =
|
|
722
773
|
'_A `/model` switch relaunches the session (~30s) on the chosen model. Session-only โ reverts to the configured \`model:\` on the next restart. \`/model default\` reverts now. Live scrollback is replaced by a fresh session; memory and the handoff briefing carry the context. To change the default permanently, set \`model:\` in switchroom.yaml._'
|
|
723
774
|
|
|
775
|
+
/**
|
|
776
|
+
* Discoverability line for CLAUDE_MODEL_ALIASES, grouped by target so several
|
|
777
|
+
* spellings of one id read as one entry (`opus48` ยท `opus-4-8` โ `claude-opus-4-8`).
|
|
778
|
+
* Derived from the map, never hand-listed, so a new shortcut shows up in the
|
|
779
|
+
* help and dashboard text for free.
|
|
780
|
+
*/
|
|
781
|
+
function claudeAliasHints(prefix: string): string {
|
|
782
|
+
const byTarget = new Map<string, string[]>()
|
|
783
|
+
for (const [alias, target] of Object.entries(CLAUDE_MODEL_ALIASES)) {
|
|
784
|
+
const spellings = byTarget.get(target) ?? []
|
|
785
|
+
spellings.push(alias)
|
|
786
|
+
byTarget.set(target, spellings)
|
|
787
|
+
}
|
|
788
|
+
return [...byTarget]
|
|
789
|
+
.map(([target, spellings]) => `${spellings.map(a => `\`${prefix}${a}\``).join(' ยท ')} โ \`${target}\``)
|
|
790
|
+
.join('; ')
|
|
791
|
+
}
|
|
792
|
+
|
|
724
793
|
function helpText(deps: ModelCommandDeps, reason?: string): ModelCommandReply {
|
|
725
794
|
const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map(a => `\`${a}\``).join(' ยท ')
|
|
795
|
+
const claudeAliasExamples = claudeAliasHints('')
|
|
726
796
|
const lines: string[] = []
|
|
727
797
|
if (reason) lines.push(`โ ๏ธ ${deps.escapeHtml(reason)}`)
|
|
728
798
|
lines.push(
|
|
729
799
|
'**/model** โ show or switch the Claude model',
|
|
730
800
|
'\`/model\` โ show the configured model',
|
|
731
801
|
`\`/model <name>\` โ switch the live session (${MODEL_ALIASES.map(a => `\`${a}\``).join(' ยท ')} or a full model id)`,
|
|
802
|
+
`_Pinned Claude shortcuts:_ ${claudeAliasExamples}`,
|
|
732
803
|
`_OpenRouter shortcuts:_ ${srAliasExamples}`,
|
|
733
804
|
'_Every switch relaunches the session (~30s) on the chosen model โ Claude and OpenRouter (sr-\\*) alike._',
|
|
734
805
|
PERSIST_NOTE,
|
|
@@ -751,6 +822,7 @@ export async function handleModelCommand(
|
|
|
751
822
|
`**Model โ ${deps.escapeHtml(deps.getAgentName())}**`,
|
|
752
823
|
`Configured: \`${deps.escapeHtml(shown)}\``,
|
|
753
824
|
`Switch the live session: ${MODEL_ALIASES.map(a => `\`/model ${a}\``).join(' ยท ')}`,
|
|
825
|
+
`Pinned Claude shortcuts: ${claudeAliasHints('/model ')}`,
|
|
754
826
|
`OpenRouter shortcuts: ${srAliasExamples}`,
|
|
755
827
|
'or \`/model <full-model-id>\`',
|
|
756
828
|
PERSIST_NOTE,
|
|
@@ -765,8 +837,9 @@ export async function handleModelCommand(
|
|
|
765
837
|
return helpText(deps, `not a valid model name: ${parsed.model}`)
|
|
766
838
|
}
|
|
767
839
|
|
|
768
|
-
// Expand short aliases
|
|
769
|
-
|
|
840
|
+
// Expand short aliases BEFORE any Claude-vs-external decision below:
|
|
841
|
+
// `opus48`/`opus-4-8` โ `claude-opus-4-8`, `flash` โ `sr-gemini-2.5-flash`, etc.
|
|
842
|
+
const model = expandModelAlias(parsed.model)
|
|
770
843
|
|
|
771
844
|
// Busy gate: a switch RELAUNCHES the session, which is unsafe mid-turn (it
|
|
772
845
|
// would tear down the live turn). The gateway's mid-turn path ACKs + queues +
|
|
@@ -822,16 +895,70 @@ function relaunchErrorReply(
|
|
|
822
895
|
* silently serves the fallback. Say so IMMEDIATELY in the switch ack, and
|
|
823
896
|
* point at the first-reply tripwire that will catch it. Aliases and sr-* ids
|
|
824
897
|
* are vouched by their own gates (MODEL_ALIASES / the LiteLLM route probe),
|
|
825
|
-
* so only typed `claude-*` ids carry the caveat
|
|
898
|
+
* so only typed `claude-*` ids carry the caveat โ and a CLAUDE_MODEL_ALIASES
|
|
899
|
+
* TARGET is vouched by the same curation gate that makes it offline-trustable
|
|
900
|
+
* (isOfflineTrustedModelToken), so it is exempt too; warning "can't be
|
|
901
|
+
* validated" on a shortcut switchroom itself curates would contradict that.
|
|
902
|
+
*
|
|
903
|
+
* The exemption compares `canonicalModelToken(model)` โ the SAME form
|
|
904
|
+
* `expandModelAlias` emits โ so the token that reaches `claude --model` and the
|
|
905
|
+
* token this gate judges can never disagree. Comparing a lowercased form against
|
|
906
|
+
* a verbatim-emitted one is exactly how `/model CLAUDE-OPUS-4-8` used to launch
|
|
907
|
+
* an uncanonical id under a clean green ack.
|
|
908
|
+
*
|
|
909
|
+
* What the exemption costs, stated honestly: the ONLY remaining signal that an
|
|
910
|
+
* exempted target silently fell back is the first-reply divergence tripwire
|
|
911
|
+
* (`servedModelMatchesRequested`). The post-boot confirmation card is NOT a
|
|
912
|
+
* second signal for a pinned id whose FAMILY equals the configured default's:
|
|
913
|
+
* `modelFamilyToken('claude-opus-4-8')` is `'opus'`, so on an `opus`-default
|
|
914
|
+
* agent `classifyModelSwitchConfirmation` returns `default`, not `not-applied`
|
|
915
|
+
* (see #4005 โ a pre-existing property of the family reduction, reachable
|
|
916
|
+
* before this map existed by typing the full id).
|
|
917
|
+
* Exported for tests.
|
|
826
918
|
*/
|
|
827
919
|
export function unvalidatedIdCaveat(
|
|
828
920
|
deps: Pick<ModelCommandDeps, 'escapeHtml'>,
|
|
829
921
|
model: string,
|
|
830
922
|
): string | null {
|
|
831
|
-
|
|
923
|
+
const lower = canonicalModelToken(model).toLowerCase()
|
|
924
|
+
if (!lower.startsWith('claude-')) return null
|
|
925
|
+
if (Object.values(CLAUDE_MODEL_ALIASES).includes(lower)) return null
|
|
832
926
|
return `_\`${deps.escapeHtml(model)}\` can't be validated before launch โ if it isn't a real Claude model id, claude will silently serve the configured fallback model instead. I check the first reply and will warn if that happens._`
|
|
833
927
|
}
|
|
834
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Switch-time surfacing of the #1978 adaptive-thinking risk (`thinking_effort`
|
|
931
|
+
* above `low` on a PINNED Opus 4.x id).
|
|
932
|
+
*
|
|
933
|
+
* `assessThinkingEffortRisk` had exactly ONE consumer โ `switchroom doctor`
|
|
934
|
+
* (`src/cli/doctor.ts:572`) โ which reads the CONFIGURED `model:` from
|
|
935
|
+
* switchroom.yaml. A `/model` switch is session-scoped and never touches that
|
|
936
|
+
* file, so every route onto a pinned Opus 4.x (typed full id, a
|
|
937
|
+
* `CLAUDE_MODEL_ALIASES` shortcut, or the one-tap "Opus 4.8" button) reached the
|
|
938
|
+
* risky combination with no warning anywhere. This is the second consumer, on
|
|
939
|
+
* the path that actually creates the combination.
|
|
940
|
+
*
|
|
941
|
+
* Advisory only โ it never rewrites anyone's effort. Returns null when the combo
|
|
942
|
+
* is safe (effort unset/`low`, or any non-Opus-4.x model, including Opus 5 and
|
|
943
|
+
* the bare `opus` alias).
|
|
944
|
+
*/
|
|
945
|
+
export function thinkingEffortCaveat(
|
|
946
|
+
deps: Pick<ModelCommandDeps, 'escapeHtml' | 'getConfiguredEffort'>,
|
|
947
|
+
model: string,
|
|
948
|
+
): string | null {
|
|
949
|
+
let effort: string | null
|
|
950
|
+
try {
|
|
951
|
+
effort = deps.getConfiguredEffort()
|
|
952
|
+
} catch {
|
|
953
|
+
// The effort probe shells out to `switchroom agent list`; a transient
|
|
954
|
+
// failure must never block or fail the switch it is only annotating.
|
|
955
|
+
return null
|
|
956
|
+
}
|
|
957
|
+
const risk = assessThinkingEffortRisk(model, effort ?? undefined)
|
|
958
|
+
if (!risk.risky || !risk.reason) return null
|
|
959
|
+
return `โ ๏ธ _${deps.escapeHtml(risk.reason)}_`
|
|
960
|
+
}
|
|
961
|
+
|
|
835
962
|
/** Schedule a carrier relaunch onto `model`, returning the deterministic ack. */
|
|
836
963
|
async function scheduleRelaunchReply(
|
|
837
964
|
deps: ModelCommandDeps,
|
|
@@ -844,8 +971,14 @@ async function scheduleRelaunchReply(
|
|
|
844
971
|
return relaunchErrorReply(deps, model, err)
|
|
845
972
|
}
|
|
846
973
|
const caveat = unvalidatedIdCaveat(deps, model)
|
|
974
|
+
const effortRisk = thinkingEffortCaveat(deps, model)
|
|
847
975
|
return {
|
|
848
|
-
text: [
|
|
976
|
+
text: [
|
|
977
|
+
switchingLine(deps, model),
|
|
978
|
+
...(caveat ? [caveat] : []),
|
|
979
|
+
...(effortRisk ? [effortRisk] : []),
|
|
980
|
+
PERSIST_NOTE,
|
|
981
|
+
].join('\n'),
|
|
849
982
|
html: true,
|
|
850
983
|
}
|
|
851
984
|
}
|
|
@@ -934,15 +1067,28 @@ export const MODEL_CALLBACK_PAGE_MAIN = 'mdl:page:main'
|
|
|
934
1067
|
export type ModelMenuPage = 'main' | 'external'
|
|
935
1068
|
|
|
936
1069
|
/**
|
|
937
|
-
* Static Claude
|
|
938
|
-
* own `/model` picker (deps.discover)
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
1070
|
+
* Static Claude `--model` TOKENS appended to the scraped Claude group โ extra
|
|
1071
|
+
* keyboard rows for targets the claude CLI's own `/model` picker (deps.discover)
|
|
1072
|
+
* does not list. Each renders as a button that switches via the carrier relaunch
|
|
1073
|
+
* (MODEL_CALLBACK_ALIAS โ scheduleModelRelaunch, rev 5).
|
|
1074
|
+
*
|
|
1075
|
+
* NB "token", not "alias": members are not required to be CLI-resolvable aliases
|
|
1076
|
+
* and are treated opaquely by both call sites (they are only concatenated onto
|
|
1077
|
+
* the `mdl:alias:` callback prefix, whose wire format predates this list). The
|
|
1078
|
+
* two kinds present today:
|
|
1079
|
+
* - `fable` โ a genuine alias the CLI resolves itself; the picker just omits
|
|
1080
|
+
* it. Boots via the LiteLLM router repoint in start.sh (see the fable case).
|
|
1081
|
+
* - `claude-opus-4-8` โ a full PINNED id, not an alias. Rendered because the
|
|
1082
|
+
* picker doesn't offer it and typing it on a phone is hostile.
|
|
1083
|
+
* Extend with either kind; a member only has to be a legal `claude --model` arg.
|
|
943
1084
|
*/
|
|
944
|
-
export const
|
|
945
|
-
{
|
|
1085
|
+
export const EXTRA_CLAUDE_MENU_TOKENS: ReadonlyArray<{ token: string; label: string }> = [
|
|
1086
|
+
{ token: 'fable', label: 'Fable' },
|
|
1087
|
+
// Pinned Opus 4.8. The button carries the CANONICAL id (not the `opus48`
|
|
1088
|
+
// shortcut) so the tap needs no expansion to be recognized by an older
|
|
1089
|
+
// gateway, and so `canonicalClaudeToken` resolves it directly. Typed
|
|
1090
|
+
// `/model opus48` / `/model opus-4-8` land on the same target.
|
|
1091
|
+
{ token: 'claude-opus-4-8', label: 'Opus 4.8' },
|
|
946
1092
|
]
|
|
947
1093
|
|
|
948
1094
|
/**
|
|
@@ -1039,13 +1185,69 @@ export function isOfflineTrustedModelToken(token: string): boolean {
|
|
|
1039
1185
|
const lower = token.toLowerCase()
|
|
1040
1186
|
if ((MODEL_ALIASES as readonly string[]).includes(lower)) return true
|
|
1041
1187
|
if (lower in SR_MODEL_ALIASES) return true
|
|
1042
|
-
|
|
1188
|
+
if (Object.values(SR_MODEL_ALIASES).includes(lower)) return true
|
|
1189
|
+
// Curated pinned-Claude shortcuts and their targets. Both spellings are
|
|
1190
|
+
// trustable for the same reason the sr-* alias TARGETS are: the id is
|
|
1191
|
+
// switchroom-curated, not a hand-typed guess. A queue that already expanded
|
|
1192
|
+
// (planModelCommand) persists the VALUE, an alias tap persists the KEY โ so
|
|
1193
|
+
// both sides must be accepted or `/model opus48` would survive a boot only
|
|
1194
|
+
// on one of the two paths.
|
|
1195
|
+
if (lower in CLAUDE_MODEL_ALIASES) return true
|
|
1196
|
+
return Object.values(CLAUDE_MODEL_ALIASES).includes(lower)
|
|
1043
1197
|
}
|
|
1044
1198
|
|
|
1045
1199
|
export function expandSrAlias(arg: string): string {
|
|
1046
1200
|
return SR_MODEL_ALIASES[arg.toLowerCase()] ?? arg
|
|
1047
1201
|
}
|
|
1048
1202
|
|
|
1203
|
+
/**
|
|
1204
|
+
* The ONE canonical form of a model token โ what `claude --model` is handed and
|
|
1205
|
+
* what every downstream gate compares against.
|
|
1206
|
+
*
|
|
1207
|
+
* Two normalizations, both narrow:
|
|
1208
|
+
* - **trim** โ unconditional. `unvalidatedIdCaveat` already trimmed while
|
|
1209
|
+
* expansion did not, so the two disagreed on a padded token.
|
|
1210
|
+
* - **lowercase, `claude-*` ONLY.** Every real Anthropic model id is lowercase
|
|
1211
|
+
* and a phone autocapitalizes, so `/model Claude-Opus-4-8` used to be launched
|
|
1212
|
+
* VERBATIM as an unvalidated id while the caveat exemption (which lowercases)
|
|
1213
|
+
* suppressed the warning โ a clean green ack for a token the caveat was
|
|
1214
|
+
* written for. Non-`claude-` tokens are left alone: `sr-*` ids are matched
|
|
1215
|
+
* case-insensitively by their own maps, and rewriting an arbitrary external
|
|
1216
|
+
* id's case is not ours to do.
|
|
1217
|
+
*
|
|
1218
|
+
* Applied by `expandModelAlias`, so canonicalization happens on every `/model`
|
|
1219
|
+
* path (typed apply, mid-turn queue, menu tap, queued-across-restart persist)
|
|
1220
|
+
* whether or not the token hit a shortcut map โ one canonical form flows to
|
|
1221
|
+
* `claude --model`, and `unvalidatedIdCaveat` sees exactly that form.
|
|
1222
|
+
*/
|
|
1223
|
+
export function canonicalModelToken(arg: string): string {
|
|
1224
|
+
const trimmed = arg.trim()
|
|
1225
|
+
const lower = trimmed.toLowerCase()
|
|
1226
|
+
return lower.startsWith('claude-') ? lower : trimmed
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** Expand a short pinned-Claude spelling (case-insensitive) to its full `claude-*` id. */
|
|
1230
|
+
export function expandClaudeAlias(arg: string): string {
|
|
1231
|
+
return CLAUDE_MODEL_ALIASES[arg.trim().toLowerCase()] ?? arg
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* The ONE expansion every `/model` argument goes through, on every path that
|
|
1236
|
+
* consumes a user-supplied token (typed apply, mid-turn queue, menu tap,
|
|
1237
|
+
* queued-across-restart persist). Claude shortcuts resolve first, then sr-*
|
|
1238
|
+
* shortcuts; the two key sets are disjoint by construction (a Claude shortcut
|
|
1239
|
+
* never expands to an `sr-*` id and vice versa), so order only documents intent.
|
|
1240
|
+
* The result is `canonicalModelToken`-normalized, so a miss can no longer emit a
|
|
1241
|
+
* verbatim mixed-case `claude-*` id that the caveat exemption then mis-matches.
|
|
1242
|
+
*
|
|
1243
|
+
* Expanding HERE โ before `isClaudeModel` / `isSrModel` / `externalModelNames`
|
|
1244
|
+
* ever see the token โ is what keeps a pinned-Claude shortcut on the Anthropic
|
|
1245
|
+
* OAuth passthrough instead of being misread as an external route.
|
|
1246
|
+
*/
|
|
1247
|
+
export function expandModelAlias(arg: string): string {
|
|
1248
|
+
return canonicalModelToken(expandSrAlias(expandClaudeAlias(arg)))
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1049
1251
|
export function srFriendlyLabel(srName: string): string {
|
|
1050
1252
|
return SR_MODEL_LABELS[srName] ?? srName.replace(/^sr-/, '').replace(/-/g, ' ')
|
|
1051
1253
|
}
|
|
@@ -1161,6 +1363,13 @@ function busyStaticMenu(
|
|
|
1161
1363
|
callback_data: `${MODEL_CALLBACK_ALIAS}${alias}`,
|
|
1162
1364
|
}])
|
|
1163
1365
|
}
|
|
1366
|
+
// Same extra Claude rows the live keyboard renders (Fable, pinned Opus 4.8),
|
|
1367
|
+
// minus any already covered by a MODEL_ALIASES row above โ otherwise a model
|
|
1368
|
+
// is selectable when idle but vanishes from the mid-turn quick list.
|
|
1369
|
+
for (const { token, label } of EXTRA_CLAUDE_MENU_TOKENS) {
|
|
1370
|
+
if ((MODEL_ALIASES as readonly string[]).includes(token.toLowerCase())) continue
|
|
1371
|
+
rows.push([{ text: label, callback_data: `${MODEL_CALLBACK_ALIAS}${token}` }])
|
|
1372
|
+
}
|
|
1164
1373
|
rows.push([{ text: 'Default (configured)', callback_data: `${MODEL_CALLBACK_ALIAS}default` }])
|
|
1165
1374
|
if (externalNames.length > 0) {
|
|
1166
1375
|
rows.push([{ text: '๐ External models โธ', callback_data: MODEL_CALLBACK_PAGE_EXTERNAL }])
|
|
@@ -1230,16 +1439,16 @@ function mainPageKeyboard(
|
|
|
1230
1439
|
}])
|
|
1231
1440
|
}
|
|
1232
1441
|
|
|
1233
|
-
// Static Claude
|
|
1234
|
-
// scraped Claude options already include a matching row,
|
|
1235
|
-
// static one too.
|
|
1236
|
-
for (const {
|
|
1442
|
+
// Static Claude tokens the CLI picker omits (Fable, pinned Opus 4.8).
|
|
1443
|
+
// Deduped: if the scraped Claude options already include a matching row,
|
|
1444
|
+
// don't render the static one too.
|
|
1445
|
+
for (const { token, label } of EXTRA_CLAUDE_MENU_TOKENS) {
|
|
1237
1446
|
const already = claudeOptions.some(
|
|
1238
1447
|
(o) => o.label.toLowerCase() === label.toLowerCase() ||
|
|
1239
|
-
o.label.toLowerCase() ===
|
|
1448
|
+
o.label.toLowerCase() === token.toLowerCase(),
|
|
1240
1449
|
)
|
|
1241
1450
|
if (already) continue
|
|
1242
|
-
rows.push([{ text: label, callback_data: `${MODEL_CALLBACK_ALIAS}${
|
|
1451
|
+
rows.push([{ text: label, callback_data: `${MODEL_CALLBACK_ALIAS}${token}` }])
|
|
1243
1452
|
}
|
|
1244
1453
|
|
|
1245
1454
|
if (hasExternal) {
|
|
@@ -1434,9 +1643,17 @@ export async function handleModelMenuCallback(
|
|
|
1434
1643
|
token = data.slice(MODEL_CALLBACK_SELECT.length)
|
|
1435
1644
|
label = token
|
|
1436
1645
|
}
|
|
1646
|
+
// Shape gate FIRST, on the RAW callback payload. `expandModelAlias` trims,
|
|
1647
|
+
// so gating after it would let a whitespace-padded payload through a regex
|
|
1648
|
+
// that exists precisely to guarantee one whitespace-free token reaches the
|
|
1649
|
+
// tmux pane. Order: shape โ expand โ recognition.
|
|
1437
1650
|
if (!isValidModelArg(token)) {
|
|
1438
1651
|
return { answer: 'Invalid model name', reply: await buildModelMenu(deps) }
|
|
1439
1652
|
}
|
|
1653
|
+
// Same expansion the typed path applies, before the recognition gate: a
|
|
1654
|
+
// callback minted by an older gateway (or a hand-crafted `mdl:alias:opus48`)
|
|
1655
|
+
// must resolve to the canonical id rather than be rejected as unrecognized.
|
|
1656
|
+
token = expandModelAlias(token)
|
|
1440
1657
|
// N1: reject a token we don't recognize (a stale OLD-gateway `mdl:s:<hex>`
|
|
1441
1658
|
// callback, an unmapped SELECT row, or garbage). Relaunching onto it would
|
|
1442
1659
|
// write a carrier claude silently falls back from (--fallback-model). Re-render
|
|
@@ -1498,11 +1715,16 @@ async function menuRelaunchOutcome(
|
|
|
1498
1715
|
}
|
|
1499
1716
|
}
|
|
1500
1717
|
const friendly = isDefault ? 'the configured default' : label
|
|
1718
|
+
// Same #1978 switch-time guard the typed path carries โ the one-tap
|
|
1719
|
+
// "Opus 4.8" button is the EASIEST route into (pinned Opus 4.x, effort > low)
|
|
1720
|
+
// and doctor cannot see a session-scoped switch. `/model default` reverts to
|
|
1721
|
+
// the configured model, which doctor already covers, so it is not assessed.
|
|
1722
|
+
const effortRisk = isDefault ? null : thinkingEffortCaveat(deps, token)
|
|
1501
1723
|
return {
|
|
1502
1724
|
answer: `Switching to ${isDefault ? 'default' : label} โ relaunching (~30s)`,
|
|
1503
1725
|
reply: await menuWithBannerStatic(
|
|
1504
1726
|
deps,
|
|
1505
|
-
`๐ Switching session to **${deps.escapeHtml(friendly)}** โ relaunching (~30s)
|
|
1727
|
+
`๐ Switching session to **${deps.escapeHtml(friendly)}** โ relaunching (~30s).${effortRisk ? `\n${effortRisk}` : ''}\n${PERSIST_NOTE}`,
|
|
1506
1728
|
),
|
|
1507
1729
|
}
|
|
1508
1730
|
}
|