Skip to content

mx_set_displayname() should return the refreshed client #18

Open

Description

@TroyHernandez

mx_set_displayname() wraps its request in mx_with_relogin(), so a rejected token is refreshed and persisted. But it discards what the wrapper returns:

mx_set_displayname <- function(client, name, save = TRUE) {
    ...
    mx_with_relogin(client, function(cl) {
        mx.api::mx_set_displayname(mx_client_session(cl), name)
    }, save = save)
    invisible(TRUE)          # <- the refreshed client is dropped here
}

A caller therefore has no way to learn that its client is now stale. It keeps the rejected token in memory while the live one sits on disk.

Why this bit

corteza's model badge renames the bot on every /model switch, so it calls this on a path where token rotation is expected. The acknowledgement that follows the rename went out on the rejected token, and because chat.api::chat_send() does no relogin of its own, the failure died in a best-effort tryCatch with nothing logged. The user saw a /model switch that silently never acknowledged.

It took several passes to find, because the token is copied into a number of derived objects and refreshing one leaves the others stale. corteza now works around it by re-reading its config from disk after any attempted rename:

tryCatch(mx.client::mx_set_displayname(matrix_client(cfg), name),
         error = function(e) NULL)
# Re-read whether or not the rename succeeded; disk is authoritative.
invisible(tryCatch(matrix_load_config(), error = function(e) cfg))

That works but it is a disk round-trip standing in for a return value.

Proposal

Return the client, matching what mx_with_relogin() already hands back:

res <- mx_with_relogin(client, function(cl) {
    mx.api::mx_set_displayname(mx_client_session(cl), name)
    cl
}, save = save)
invisible(res)

Callers that ignore the value are unaffected, since it is invisible. invisible(TRUE) carries no information a caller can act on, so nothing depends on the current return.

Worth checking at the same time

Two subtleties this surfaced, either of which would make a good doc addition:

  1. mx_with_relogin() persists before it retries. So a relogin whose retry then fails (rate limit, transient 5xx) still leaves the live token on disk even though the call threw. Any caller that only adopts a refreshed client on success discards it. That is not obvious from outside and it is worth stating in ?mx_with_relogin.

  2. Other wrappers may have the same shape. Anything else in mx.client that goes through mx_with_relogin() and returns a bare TRUE has this problem too, and is worth a sweep.

Activity

  1. TroyHernandez commented on Aug 5, 2026

    @TroyHernandez
    ContributorAuthor

    Swept the package for the same shape — a mx_with_relogin() wrapper that returns a bare TRUE — and mx_set_displayname() in R/profile.R:33 is the only one. Every other caller of mx_with_relogin() passes its return value through.

    So this is a single-site fix, not a pattern to unwind.

  2. TroyHernandez commented on Aug 5, 2026

    @TroyHernandez
    ContributorAuthor

    Correction to the proposal above: returning the client is necessary but not sufficient.

    res <- mx_with_relogin(client, function(cl) {
        mx.api::mx_set_displayname(mx_client_session(cl), name)
        cl
    }, save = save)
    invisible(res)

    That works when the retry succeeds. When the retry fails -- rate limit, transient 5xx -- mx_with_relogin() propagates the error, so there is no return value at all. The refreshed client is already persisted to disk at that point, but a caller has no way to reach it through the return.

    So corteza's unconditional disk reload after any attempted rename is not a workaround to retire once this lands. It is load-bearing on the error path:

    tryCatch(mx.client::mx_set_displayname(matrix_client(cfg), name),
             error = function(e) NULL)
    # Re-read whether or not the rename succeeded; disk is authoritative.
    invisible(tryCatch(matrix_load_config(), error = function(e) cfg))

    Flagging it because the obvious reading of this issue is "mx.client returns the client, so corteza can drop the reload" -- and doing that would reintroduce the original silent-drop bug on exactly the path that is hardest to reproduce.

    What a complete fix needs

    mx_with_relogin() has to expose the refreshed client on the error path too. Options, roughly in order of how much they disturb the existing contract:

    1. Attach it to the condition. Add the refreshed client as a field on the re-thrown error, so a handler can recover it. Non-breaking for callers that ignore conditions.
    2. Out-parameter. Accept an environment and write the refreshed client into it before retrying. Ugly but explicit, and works regardless of how the retry ends.
    3. Return a result object instead of throwing. list(ok =, value =, client =). Cleanest semantics, most disruptive to existing callers.

    Worth documenting the persist-before-retry ordering in ?mx_with_relogin either way, since that is the detail that makes the error path recoverable at all and it is invisible from outside.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions