The route dialog offers a Daybreak select (Off / On) only when the
installed model_map backend declares the `daybreak` route key and this
account's Codex catalog lists Daybreak for the chosen model (the same
capability the main model menu's Daybreak switch uses). An older plugin
that would ignore the key never gets the control.
- Switching to a model or provider without Daybreak clears the opt-in.
- A stored opt-in the plugin cannot run, or whose model is no longer
listed, stays visible with a warning so it can be turned off.
- Turning it off removes the key; the route list marks Daybreak routes.
- Strings in every locale.
Classic CLI gains a Codex-style backtrack picker. Esc Esc on an idle, empty composer with no
overlay (or /rewind) lists earlier prompts newest first with a preview and turn number.
- Enter: prefill the prompt for editing. Nothing is cut until the edit is submitted; the
submission rides to the process loop as a BacktrackResend, which re-validates the picked
turn (same session, same text) and then runs the same durable rewind /undo uses. Esc Esc,
Ctrl+C, an empty submit, or submitting a slash/! command cancels and cuts nothing.
- f: fork. Runs the existing /branch path, then rewinds only the CHILD to just before the
picked prompt and prefills it; the parent's rows stay active.
- /rewind N and /rewind fork N do the same without the picker.
Esc Esc with a draft still discards it; mid-turn Esc Esc is unchanged.
undo_last's rewind body moves into CLISessionMixin._rewind_to_user_ordinal (shared by /undo
and the picker; same compaction-carrier handling, agent patching and memory rewound hook).
The picker lives in a new hermes_cli/cli_backtrack.py mixin; cli_tui_mixin.py is untouched.
CommandDef rewind is cli_only (desktop: terminal); desktop registry and parity table regenerated.
The /rewind picker (Esc Esc on an idle, empty composer) gains Codex-style
backtrack navigation:
- f = Fork from here: a `session.mutate branch` through the last stored row
before the picked prompt (the same primitive Desktop's message-level branch
sends), switch to the child, prefill the prompt unsent. The original session
is untouched. The first prompt has nothing before it, so it opens a plain new
session with the prompt prefilled.
- The list shows a preview (first lines + prompt number) of the selection.
- While the list is open the transcript scrolls to the selected prompt and
marks it, so Up/Down jumps between earlier user messages.
The keys this branch added to the TUI catalog (locales/_keys.tui.json) had no
translation in the bundled per-language packs, so
test_bundled_tui_pack_mirrors_tui_key_export failed for every language.
The Ink TUI could only undo or retry the newest turn. `/rewind` (and Esc Esc on an idle,
empty composer) now lists every earlier text prompt; Enter opens the picked one for editing and
Enter again cuts the session back to just before it and sends the edit. Nothing is cut until the
edit is sent, and Esc steps back (edit -> list -> closed).
The cut is the same revision-fenced `session.mutate rewind` that /undo and /retry use, aimed at
the picked row: the owner's form of the `prompt.submit truncate_before_row_id` Desktop's
edit-and-resend sends. A media turn keeps its number but is not offered (no lossless replay,
the /retry rule). Salvages the /rewind UX of #25074 / #23445 onto main's rewind primitives.
Approval prompts gain "No, and tell Hermes what to do instead" wherever Deny is offered. Picking
it keeps the command held while the user types; Enter steers the instruction into the running
turn first (so it rides on the blocked tool result), then answers the gateway with a plain deny.
A refused steer (the turn already ended) keeps the words as the next prompt, the /steer fallback.
Esc on the typing row returns to the choices; Esc on the choices still denies outright.
Co-authored-by: SaguaroDev <74339271+SaguaroDev@users.noreply.github.com>
The nine slashCmd.ops.pluginsUpdate keys the TUI update-all route added were in the en source and
the key snapshot but missing from the 16 locales/*.tui.yaml packs (test_i18n_tui_packs).
The Plugins tab header gets an "Update all" button (plugins.manage
update_all: the unattended path; widened pins and caution scans come
back as needs review and are named in the toast) and an Auto-update
switch for the selected profile (plugins.manage auto_update, i.e.
plugins.auto_apply). The switch hides on an older backend that lacks
the action. Copy in all eight overlays; locale keys regenerated.
The manage_connections MCP card asked the user to Install / Enable / Authorize
a server by name only. Each MCP target now carries a backend-built `launch`
line ({kind: stdio|remote, target}) in connection.request: the catalog
manifest's transport for install, the configured mcp_servers entry for
enable/authorize. The card renders it under the row as plain text
("Runs: npx -y ...", "Connects to: https://...") until the row resolves.
The line is scrubbed in the backend (tools/connectors/mcp_launch.py): values
of secret-named flags (--api-key=..., --token ...), credential query params
and URL userinfo, long opaque URL path segments, then the standard egress
redaction; control characters are flattened and the text is capped at 400
chars after scrubbing. The env map and headers are never part of it. The
model's tool result drops the field.
i18n: launchCommand / launchRemote in all nine locales, _keys.desktop.json
regenerated. Gateway contract regenerated.
A TUI attached with HERMES_TUI_GATEWAY_URL to a listener that is not a
session authority (an older or remote gateway) fell back to the legacy
session.save RPC for every /save. That RPC ignores the format, filename
and redact arguments and writes default-named, unredacted JSON on the
gateway host, so `/save md notes.md redact` silently produced a raw JSON
copy, secrets included, on another machine.
Match the CLI (_save_on_older_gateway) and Desktop (saveLegacyRefused):
only a plain /save (json) may use the legacy RPC, and its notice now says
the file is raw JSON on the gateway host. A redact flag, a filename or a
non-JSON format is refused with a message naming why and what to do.
The /save help line now lists the arguments.
Feishu already rendered exec approvals and update prompts as interactive
cards, but the clarify tool fell back to the base numbered-text prompt and
finite-choice slash commands (/reasoning, /fast, usage-limit switch) fell
back to text status cards. This implements the generic adapter contracts
from BasePlatformAdapter instead of a Feishu-only path:
- send_clarify(): multiple-choice questions become a card with one numbered
button per option plus "Other (type answer)"; a tap answers through
resolve_gateway_clarify and the callback response settles the card inline
for every client; "Other" calls mark_awaiting_text. Open-ended questions
use the base text prompt. Answers come from the pending clarify entry, so
a tap after a timeout or /new reports the prompt expired.
- retire_clarify_card(): the gateway's hook for a clarify that ended without
a tap (timeout, reset, superseding prose, typed answer) PATCHes the card
button-less; the card is a shared card (update_multi) so the update
reaches every recipient.
- send_choice_picker(): one-tap picker card; the tap settles the card and
runs on_choice_selected on the adapter loop, then sends its confirmation.
- Every click passes the approval path's _validate_card_action (operator
authorization incl. the gateway pairing check, callback-vs-card chat
match); state pops are first-writer-wins, so a double or stale tap is
refused.
Behaviour lives in plugins/platforms/feishu/adapter_choice_cards.py (mixin);
adapter.py stays at main's length (card-action routing became a table).
New platform.feishu.clarify/picker strings in all 17 locales, reusing each
locale's existing Telegram/Discord wording.
A large paste collapses to a [[ … [N lines] … ]] chip, and until now the
user could neither see nor change what was behind it, and the sent message
showed only the chip.
Composer: Ctrl+P expands the chip under the cursor (or the last one) back
into ordinary editable text; the composer shows a window of rows that
follows the cursor so a 60-line paste never pushes the caret off screen.
Ctrl+P again, or Enter, folds the span back into a chip whose payload is
the text as edited, so what the model receives is the edited full text,
through the same expandTokens path as before.
Transcript: a sent user row keeps showing the chip and gets one accordion
per paste ("Pasted text #1 · 60 lines"). Click toggles one; Ctrl+P with no
chip in the composer toggles them all. Payloads ride from submit to the
row through a small FIFO keyed by chip label, so a busy-time row painted
later still gets its own payload.
Community feedback comparing against other agent TUIs asked for /new to open a
fresh session without pausing the one that is busy. On main /new refused mid-turn
("interrupt the current turn before trying to switch sessions").
- /new while a turn runs starts a new LIVE session (the switcher's `+ new` path):
nothing is closed or interrupted, so no destructive confirm. The old session
keeps its turn and stays in the session switcher (Ctrl+X). /new on an idle
session and /clear are unchanged (the idle one is closed and stays resumable,
so live agents do not pile up one per /new).
- Approval/clarify/sudo/secret/vault requests from a session that is not on
screen no longer open over the focused session's view: they stay open on the
backend and come back as open_requests when that session is activated.
- The live-session poll announces a background session this view showed when
its turn finishes or it starts waiting for an answer.
With a Bot group room in front, the panel targets the primary chat behind
it (consistent with the meter's numbers), but the dialog only said "this
chat". The confirm title now carries the target chat's title, falling back
to the generic question for untitled chats. New key questionNamed in en,
types and all 8 overlays; _keys.desktop.json regenerated.
Trim #148607 to the minimal mechanism. The context panel's Compact
conversation button now confirms through the shared confirm() dialog and
then calls the session delegate's executeSlash('/compress', ...), the same
entry the overflow card's Compress button already uses. Owner routing,
stale-runtime recovery, transcript replacement, progress notices, the
deferred-completion edge and the in-flight coalesce all stay in slash.ts.
Drops the parallel compress-session module, the controller action, the
terminal-observer store and the slash.ts rewrite (~1.9k lines), and keeps
the panel copy in all locales.
The chat is captured at click time and re-checked after the confirmation
resolves (focused runtime id, busy, already compacting), so switching chats
or starting a reply while the dialog is open compacts nothing.
VS Code's terminal draws images only with terminal.integrated.enableImages
on, and nothing in the environment says whether it is. `auto` therefore
used half-blocks there even for users who had turned images on.
At startup (same pre-prompt_toolkit window as the light-mode OSC 11 probe)
the classic CLI now sends one XTSMGRAPHICS sixel query fenced by DA1. Only
VS Code's image addon answers XTSMGRAPHICS, so a reply means images are on
and `auto` uses the full-resolution iTerm2 path; DA1 alone means off.
Timeout, measured live in VS Code 1.145 (30 runs per deadline): the reply
lands in ~7 ms; at 8 ms some runs missed it and the late reply leaked into
the next read, at 10 ms and above every run was right with no leak. The
deadline is 50 ms for headroom; the read returns as soon as the DA1 fence
arrives, so a normal start pays ~7-10 ms. Skipped over SSH (a remote round
trip can exceed the deadline and leak into the prompt) and on Windows.
When the probe says images are off, roughly one launch in five shows a tip
instead of the random one: "VS Code can show full-resolution images here:
turn on terminal.integrated.enableImages in settings." A pinned
display.inline_images value skips both the probe and the tip.
- locales/_keys.desktop.json: drop settings.model.tasks.skills_hub.{label,hint}.
The committed key list mirrors the English catalog
(apps/desktop/scripts/i18n-keys.test.mjs runs `--check`), and the rows were
removed from en_aux_tasks.ts.
- zh-Hans configuring-models still had the Skills Hub row claiming that
`hermes skills search` uses the slot; the developer-guide provider-runtime
lists (EN + zh) still named "skills hub operations" as an aux task.
- Pin the removal: no built-in surface (DEFAULT_CONFIG, the `hermes model`
picker, the dashboard REST allowlist, the web and desktop rows) offers the
slot, and a config that still carries an auxiliary.skills_hub block loads.
Refs #40703, #142403
The gateway session line printed "◆ Context: 128K tokens (detected)" for a Bedrock
model no source knew. _resolve_gateway_model_context recognised only the generic 256K
DEFAULT_FALLBACK_CONTEXT as a default, so Bedrock's static 128K was reported as
detected. The CLI surfaces showed the number with no source at all.
agent/context_pin.py already labels a model.context_length pin "(pinned)" on those
surfaces (#66168). This commit extends the same helper with a "fallback" marker:
- is_fallback_context_length(window, model=, provider=, base_url=) is True only when
the window equals the resolver's default for that route and no local source knows
the model. On Bedrock the default is BEDROCK_DEFAULT_CONTEXT_LENGTH, and the checks
are the table/family step, a provider-confirmed cache row and model_overrides. The
generic route uses DEFAULT_FALLBACK_CONTEXT, the catalog, models.dev and
custom_providers. It does only local reads, memoised for 60s because the status bar
repaints often. It fails open and never probes.
- Labelled surfaces: the gateway session line (via context_source "default", now
worded "fallback, not a detected limit"), CLI and gateway /usage, the wide status
bar (new cli.status_bar.ctx_fallback key in all 17 locales), the welcome banner,
and the CLI and gateway /model switch summary.
No new config key. The override is unchanged: model.context_length, or
model_overrides.<provider>.<model>.context_window.
- On a Portal-managed agent (connector /manage/access/list answers
managedBy: portal) a code minted with the gateway bearer carries no owner, so
its redeem binds no account (gateway-gateway scopeService: a managed agent
takes only the Portal setting). The gateway now refuses to mint there
(managed_by_portal) and the Channels card on both surfaces lists the linked
accounts read-only with a 'Link accounts in Nous Portal' button.
- Success is a new platformUserId in the refreshed list, never an 'inactive'
code before expiry: 'inactive' also means used by an already-linked account
or unknown. That case now shows a neutral note; expiry still says expired.
- Desktop keys the link card by connection + profile + platform, so a code,
poll or account list never carries over to another backend.
- A platform the relay switches off but the shared bot does not carry (Slack,
Matrix outside GATEWAY_RELAY_PLATFORMS) now reads 'Managed by Team Gateway'
with the toggle disabled and the opt-out explained, matching the dashboard
(#141175); before, its Enable toggle saved and reverted at every restart.
- The link calls carry the settings profile so a profile pointed at another
host links against that host.
On a Hermes Cloud instance with Team Gateway, every direct messaging adapter is
switched off at gateway start (config_env._relay) and the connector's shared bot
is the only way Telegram, Discord and Slack reach the agent. The Channels page
and the Desktop messaging page still offered "Create with QR" and bot-token
setup, which make a bot the relay never runs: users finished setup, restarted,
and the bot never answered (#141546, #138101).
The instance now mints the connector's one-time `/link <code>` itself:
- gateway/relay/link.py calls /manage/link, /manage/link/state,
/manage/link-status and /manage/unlink with the per-gateway bearer, and
builds the shared bot's DM link from GATEWAY_RELAY_BOT_IDS.
- The per-gateway secret lives only in the gateway process, so the gateway
answers a new `relay-link` control-socket verb and the dashboard routes
(/api/messaging/relay*) go through it.
- /api/messaging/platforms reports relay_fronted / relay_exclusive per platform.
- Dashboard Channels and Desktop Messaging show a Team Gateway card (link
button, QR of the bot, the exact message to send, linked accounts with
Unlink) and hide the direct setup that cannot work under an exclusive relay.
The pane's new strings (settings.gateway.webAccess.startElapsed and startStages.*) were missing from the generated locales/_keys.desktop.json, which scripts/i18n-keys.test.mjs checks against the English catalog. Regenerated with npm run i18n:keys.
OpenAI Daybreak (Trusted Access for Cyber) reduces cyber refusals for
approved accounts. Codex's model picker has a Daybreak toggle; Hermes had
no way to request it.
The signed-in account's Codex /models catalog already lists, per model,
which cyber programs that account may use
(available_access_programs.cyber). Hermes reads that catalog for Fast and
Ultrafast; it now keeps the cyber list from the same fetch.
- agent.daybreak (default false) is one profile-wide switch, like Codex's.
- Desktop shows a Daybreak switch in a model's options submenu, next to
Fast, only when this account's catalog lists daybreak_blue for that
model. Unknown or unreachable catalog: hidden.
- Each Responses request adds access_programs.cyber=daybreak_blue only
when the switch is on AND the request's credential and model are listed.
Checked per request, so a /model switch, credential rotation or expired
catalog never carries it to a model or account without it.
- The gateway re-reads the switch every turn, so a flip applies to the
next turn of a running chat without rebuilding its agent.
- The opt-in codex app-server runtime does not send it, so the switch is
hidden there.
Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
Review P3-4: an SSH teardown now keeps the tab and printed a hardcoded English
line beside the localized gateway messages. Move it to
rightSidebar.terminalDisconnectedBody in en and all eight overlays.
A terminal tab now records the gateway (connection id, plus the profile the
SSH tunnel is keyed by) it was opened on, and its PTY spawns on THAT host
whichever gateway the window shows later. Building on #143491's start-payload
gateway, the window-backend route now only serves tabs that recorded none.
- New tabs take the gateway picked in Sessions at first start; URL/token and
cloud gateways have no shell transport, so their tabs are This device and
say so. The + in the terminal rail gets a chevron (and a right-click menu)
to open a terminal on any SSH gateway or This device, side by side.
- Each tab shows where it runs: a gateway glyph on its rail square and
"zsh · NAS" in its tooltip/aria label. This device stays unlabeled until
tabs run on more than one host.
- Restored tabs reopen on their recorded gateway. A removed gateway, or one
that lost its shell transport, ends in an exited tab that says so instead
of a local shell under the remote's name.
- An SSH teardown (gateway edited or removed) leaves its tabs disconnected
("press Enter") instead of closing them; Enter re-resolves the tab's own
gateway. The output gate keeps the first exit verdict so the kill's own
exit cannot overwrite it.
- The resolver moves out of main.ts into electron/terminal-gateway-target.ts;
the window-route lookup reuses it.
Tab ownership and persistence follow #85241's TerminalEntry shape.
Co-authored-by: Addel Hamoudhy <ahamoudhy@gmail.com>
Adds the user-facing half of the per-skill lock (hermes curator lock):
- PUT /api/skills/lock {name, locked, profile?}: resolves the skill and
keys the record by its directory (what the write guards resolve), in
the selected profile; 404 for an unknown skill, 500 when the sidecar
write fails.
- GET /api/skills rows carry locked (best-effort; a corrupt sidecar
lists everything unlocked instead of failing the list).
- Desktop: Lock/Locked text toggle with a state-specific hint in the
skill detail pane for every provenance (optimistic, rolls back on
error), plus a lock glyph on locked rows (list rows, cards, detail
header). Strings in all bundled locales.
Desktop treated registered gateways as one-at-a-time: picking another gateway
ran a "switch" (barrier, session-list wipe, a "Switch to This device? This
opens a fresh session on this computer..." confirm), and a gateway that was
unreachable when Desktop started never showed up in the All profiles session
list after it came back.
- store/gateway-persistence: every registered gateway's home socket is dialed
at launch, pinned against the idle pruner (retainGatewayForRelay now takes
includeLocal so This device is kept too when a remote is primary), redialed
every 30 s while down, and the session list is re-pulled when one connects.
- selectConnection is a live activation like a profile pick: the target is
opened first (a dead gateway fails there and touches nothing), then the old
runtime is unbound and a fresh draft started synchronously right before the
target is published (#93937 invariant kept). No barrier, no list wipe, no
machine-context reset; an in-flight Settings apply is superseded.
- This device prompts only when picking it would start the installer; the
switchDevice* copy is gone and "Switch to" reads "Open". Other gateways'
rail squares use the normal unselected weight instead of an "at rest" dim.
- Docs: multi-connection-desktop.md describes always-connected gateways.
The Ink approval panel showed the first 10 wrapped lines and said the rest
was "(full text above)", which was not true: a dangerous line 11 never
reached the screen. The panel now pages the command with v (wrapping back
to the top) and says which lines are on screen and how to see the rest.
prompt.approval.moreLines is replaced by pageMore/pageEnd in every bundled
TUI pack, so no translation keeps the false claim.
- Classic CLI (and the Desktop slash worker, which runs this handler): `/model reset` switches
back, session-scoped (never --global, even under model.persist_switch_by_default), to
config.yaml's model read fresh at the command, through the normal typed path (guards, confirm,
session-row persist), and drops an armed --once restore. Already on the default: says so; no
model.default: says there is nothing to reset to.
- The /model args_hint lists `reset`.
- TUI: a `/model reset` queued mid-turn shows the default it will adopt, not the word "reset",
and skips the pick-time selection guard (adopting the configured default is not a new pick).
- cli.model.reset_none / reset_no_default in all 17 locales; slash-command and messaging docs.
- Tests: parser invariants, gateway reset of a store-only override after a restart, TUI reset
that adopts config without re-pinning.
A session /model override persists (gateway session store, TUI/Desktop session row) and
shadows every later change to the configured default. Until now the only way back was /new,
which also drops the conversation.
`/model reset` (no flags) clears the override and returns the session to the configured default:
- shared parser: `reset` is its own request (`ModelSwitchRequest.is_reset`); combined with any
flag it is rejected (`MODEL_SWITCH_ERR_RESET_WITH_FLAGS`). Not aliased to "default" (MoA preset).
- messaging gateway: clears the in-memory and persisted override and any armed --once restore,
evicts the cached agent, leaves a next-turn model note; channel_overrides > config.yaml applies.
- TUI gateway (`config.set model`, Desktop's slash mirror): drops the pin and the one-turn
restore, adopts config.yaml through the config-sync switch (no new pin, never written back) and
persists the row so session.resume cannot restore the old pin.
- gateway.model.usage_reset / reset_done / reset_none in all 17 locales.
Re-ported onto the split /model modules (gateway/slash_commands_model.py,
tui_gateway/model_switch.py) for the current main.
The backend sentence, the TUI/dashboard hint and the Desktop/Webapp card name the Hermes desktop
app and `hermes model` -> Local models instead of the Desktop app alone; the card says Retry only
while the server can still come up (retryable), and the Python hint keys its no-retry wording on
the surface's code and retryable flag rather than rewriting the text. The localRuntimeUnavailable
TUI keys are translated into every bundled locales/*.tui.yaml pack.
local_runtime_unavailable gets its own card copy in every locale instead of the
'custom model server' endpoint body. The card is shared with the Webapp, so it names
no pane; the Desktop notice keeps Settings → Providers → Local models beside its button.
The Webapp marked a blocked or finished session only with a mark in the tab
title, and its notify bridge asked for notification permission from inside
the event (no user gesture, so browsers silently refuse) and ignored clicks.
- While the tab is hidden or unfocused, every event that passes the existing
native-notification gate (Settings > Notifications, per kind) alternates the
tab title with the notification's title and badges the favicon until the
user returns to the tab.
- The bridge shows a browser Notification only when permission is granted;
permission is asked from a click (new Settings row, or Send test), never on
load. Over plain HTTP (no secure context) the row says so and the title
flash carries the signal alone.
- Clicking a browser notification focuses the tab and opens the session
through the existing onFocusSession path Desktop's OS notifications use.
A job whose newest ledger attempt is claimed/running shows Stop in its header. A click calls
POST /api/cron/jobs/{id}/cancel and holds "Stopping…" while the view polls the job list until that
attempt is terminal, then toasts "Run cancelled" (or that the run finished first / nothing was in
flight). A cron.changed refresh makes a run started elsewhere show its Stop too, and while this
window's own Run-now is waiting the run counts as live until an attempt claimed since the click has
finished, so the button never flickers back.
The editor gains "Missed runs" (use the default / run once when back / skip them), sent only when
the user changed it; the dashboard create endpoint now accepts catch_up as update already did.
Strings live in types_cron_control.ts plus one <lang>_cron_control.ts per locale.
The inline error card on a failed turn is rebuilt to the Error System
Overhaul design (Figma "Error handling" frame + Nous DS buttons / Copy):
- Severity: blocking (user can't keep chatting until they act; rust tint
and icon) vs degraded (chat still works; white card, grey icon, close X).
errorCardSeverity() derives it from the card's main action.
- One main action and at most one second option per failure
(errorCardActions / CODE_ACTIONS), rendered as Nous DS secondary + ghost
buttons. New actions: Continue (truncated), Open provider settings
(privacy block), setup guides for TLS / local servers, Open model
settings (agent init). Plan usage limits keep switch-to-model / credits.
- "View error details" toggle reveals the raw error in a DS Copy-style
slab with a copy glyph, with Copy details / Open logs / Send diagnostics
right below it.
- Copy rewritten: titles of at most 5 words that name the provider or
model, one-to-two-sentence bodies that say the fix. Translated into all
8 desktop locales.
- One event, one surface: no error toast for the session on screen (the
card says it); out of credits is the composer banner, and the card only
speaks when that banner isn't up for the chat. Background sessions keep
their toasts.
Three follow-ups to the install dialog holding an install to the commit it showed.
- Catalog picks. The backend reloads the live catalog entry at Install, so a
re-pin between the dialog's inspection and the click installed (and, through
the one checkout, published the desktop half of) a tree nobody reviewed;
the renderer sent no inspected_sha for catalog sources and neither the
catalog install nor reuse of a disabled tree read it. Now the catalog
install compares the commit it actually checked out with the inspected sha
peeled through the clone (annotated-tag pins still install), a disabled tree
is reused only at that commit (or its recorded tag pin), and catalog dialogs
send it: the backend's inspect sha in the Webapp, the shown reviewed pin on
native Desktop (whose Electron probe reads the branch tip). After a re-pin
refusal the native dialog re-reads the catalog for the new pin. The record
stays the catalog's pinned record. Electron's desktop clone uses the same
reviewed commit and peels a tag pin.
- Older backends. A backend that refuses inspected_sha at admission is no
longer retried without it behind the user's back: the dialog explains that
this backend cannot guarantee the reviewed version is what gets installed
(update it for that guarantee) and installs only on an explicit
"Install anyway" (profile kept); Cancel installs nothing.
- Partial installs. Native Desktop on a remote backend could install the agent
half at commit A, fail the desktop half (source moved), re-review to B and
let a second Install put the desktop half down at B beside the agent half at
A. That is now reported as a partial install (agent half in at A, desktop
half not); Install stays disabled until Force reinstall, which replaces both
halves with the version shown.
Item 1 of the review is only partly fixed by the stale-click commit
before this one. Clearing `_backend_settled` lets the settle observer
retire the card, but the click still edited the same card with
"Approval expired — command was not run" through its own interaction
response. Nothing serializes that edit with the observer's. If the
click's edit lands last, it hides the real outcome and reattaches the
disabled controls. Discord serializes the interaction payload before
awaiting the response, so a later embed change can't repair it.
When a click on a card with a request id resolves nothing, that request
has already settled through another path. Since the earlier commits, its
observer is always registered (it is registered before the send, and a
request gone before that already retires the card). So the click now
only answers the clicker privately ("no longer pending") and leaves the
card edit to the observer.
A click whose lookup raises recorded nothing, and its request may still
be pending. It used to stamp the card "expired — not run" and disable it
for good, and that edit could race the observer too. Now it re-opens the
card for a retry, tells only the clicker ("Couldn't record your choice.
Click again, or answer with /approve or /deny." — a new string in every
locale), and leaves the final edit to the observer. A card without a
request id has no observer, so its no-op click keeps the old "expired"
edit.
Tests:
- The stale-click regression now asserts that the click never edits the
card. It fails on the previous commit.
- A new test drives a failed lookup on a really pending request with its
observer attached: the card stays live, and a later text answer
retires it with the real outcome.
- A third test pins the expired edit on a card with no request id.
The card registered its settle observer only after `channel.send`
returned. If the request ended during the send (wait timed out, /stop,
session closed, withdrawn), the registration found no entry and the card
retired with the default reason "resolved". The card then said "This
approval has already been resolved" for a command that never ran.
Build the view first and register the observer before sending. A reason
that arrives while the card is still in flight is held on the view
(`pending_retire`) and applied as soon as the card exists. Only a
request that was already gone before the card was built still has an
unknown outcome. That card now says "⌛ This approval is no longer
pending (it timed out or was answered elsewhere)", a new outcome-neutral
string, translated in every locale. It no longer claims either
"resolved" or "not run".
Building the view outside `_build` also removes the `view_holder` list
that only existed to get the view back out. The view never depended on
the channel.
The test now covers the three orderings (after, during, before the send)
and asserts the footer. Two of them fail on the previous commit.
Review on #145574:
- The Settings summary labelled the unsaved memory.provider draft "Active
memory provider" even when GET /api/memory named another one. The draft
now shows as "Selected memory provider" (not active yet, naming what
sessions keep using), and the active row and recall copy come only from
status.active. A config autosave that touches `memory` refetches.
- Readiness refreshes after an inline credential save, a full-config modal
save and OAuth completion ($memoryStatusRevision); a failed status read
shows "Memory status unavailable" with Retry instead of dropping the badge.
- The built-in row reflects memory.memory_enabled / user_profile_enabled in
Settings ("On by default" where the config isn't loaded), and recall is
claimed only for a ready provider. Web dashboard copy drops "always on".
- isExternalMemoryProvider matches the backend's full sentinel set,
including `default`.
Refs #144648 (WA-22)
Refs #145175