feat(webui): reintroduce opt-in AI purpose titles for tool calls#103989
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8724051654
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| resolveAgentConfig(cfg, agentId)?.utilityModel?.trim() || | ||
| cfg.agents?.defaults?.utilityModel?.trim(), |
There was a problem hiding this comment.
Honor empty utility-model opt-outs
In configs where an agent sets utilityModel: "" (or an empty per-agent value to override a default utility model), this || treats the empty string as absent and either falls back to agents.defaults.utilityModel or to the Luna default in generateMissingTitles. That bypasses the existing utility-model contract in src/agents/utility-model.ts that an empty value is an explicit opt-out, so enabling gateway.controlUi.toolTitles can still send tool arguments to a utility/OpenAI model for agents that explicitly disabled utility routing; use readUtilityModelSetting or otherwise preserve the disabled state before preparing a title model.
Useful? React with 👍 / 👎.
|
Codex review: needs real behavior proof before merge. Reviewed July 10, 2026, 10:14 PM ET / July 11, 2026, 02:14 UTC. Summary PR surface: Source +801, Tests +460, Docs +4, Other +40. Total +1305 across 24 files. Reproducibility: yes. for the patch defects: the stale-cache, global-failure, profile-routing, and pre-redaction truncation paths are directly reproducible from the exact merged source. The requested capability itself is a feature rather than a broken-behavior reproduction. Review metrics: 3 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Keep the feature default-off, scope all browser state to the active client/session/agent, preserve provider-correct credential profiles, redact before the first truncation boundary, and prove fresh and switched-gateway behavior before explicit maintainer acceptance. Do we have a high-confidence way to reproduce the issue? Yes for the patch defects: the stale-cache, global-failure, profile-routing, and pre-redaction truncation paths are directly reproducible from the exact merged source. The requested capability itself is a feature rather than a broken-behavior reproduction. Is this the best way to solve the issue? No, not yet. A default-off opt-in is the narrowest plausible product shape, but the submitted implementation still violates client-context, credential, and redaction boundaries and lacks exact-head live proof. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against b7156ce68884. Label changesLabel justifications:
Evidence reviewedPR surface: Source +801, Tests +460, Docs +4, Other +40. Total +1305 across 24 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (5 earlier review cycles)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 346002d9e7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| pendingKeys.has(request.key) || | ||
| failedKeys.has(request.key) || | ||
| queue.has(request.key) |
There was a problem hiding this comment.
Notify every pane waiting on a title
When two split panes render the same eligible tool call before the first title request completes, the second pane hits pendingKeys/queue.has and returns without recording its own notify callback. The title is then stored globally, but only the first queued item's head.notify?.() runs, so the other pane's guarded chat subtree never re-renders and keeps the deterministic row until some unrelated update occurs. Track waiters per key or notify all affected panes when a shared title lands.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a20e719d2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const titles = await generateToolCallTitles({ | ||
| cfg: sessionCfg, | ||
| agentId: sessionAgentId, | ||
| sessionPrimaryProvider: sessionModel.provider, | ||
| items: params.items, |
There was a problem hiding this comment.
Preserve session auth profiles for title calls
When a session is using a session-scoped auth profile (entry.authProfileOverride, e.g. a model selection with @profile), this call only forwards the resolved provider. generateToolCallTitles then prepares the utility model without the session profile, while the normal run path does carry authProfileOverride into model execution, so enabling tool titles can send tool arguments through the provider's default credential/account or fail even though the chat session itself has working auth. Please thread the loaded session profile through the title generation path before making the utility-model call.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 821b7fceb0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const resolvedRef = resolveUtilityModelRefForAgent({ | ||
| cfg: params.cfg, | ||
| agentId: params.agentId, | ||
| primaryProvider: params.sessionPrimaryProvider, | ||
| }); |
There was a problem hiding this comment.
Enforce the same-provider title route
When an agent has an explicit utilityModel on a different provider than the session primary, resolveUtilityModelRefForAgent returns that explicit ref before using sessionPrimaryProvider, so this new path sends tool names and bounded inputs to a provider the session was not using despite the in-code/docs guarantee that title generation stays on the session provider. In that configuration, either reject/skip cross-provider utility refs for tool titles or update the contract so operators know enabling this can add a new egress destination for tool arguments.
Useful? React with 👍 / 👎.
| const cached = titlesByKey.get(request.key); | ||
| if (cached) { | ||
| return cached; |
There was a problem hiding this comment.
Clear cached titles when the gateway disables them
Because cached titles are returned before checking the current gateway/client disabled state, a browser that previously viewed an enabled gateway can reconnect to, or switch to, a gateway with gateway.controlUi.toolTitles off and still render the old AI title for the same tool input without ever probing chat.toolTitles for the disabled response. This breaks the opt-out/deterministic rendering contract after restarts or config changes; clear/key the client cache on client changes or suppress cached titles once the active gateway is disabled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7151d6f859
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const modelRef = params.sessionAuthProfile | ||
| ? `${strippedRef}@${params.sessionAuthProfile}` | ||
| : resolvedRef; |
There was a problem hiding this comment.
Drop stale agent-profile suffixes for session overrides
When a session switches to another provider without an authProfileOverride, this branch keeps resolvedRef verbatim. resolveUtilityModelRefForAgent can append the agent primary model's trailing @profile even when primaryProvider was supplied, so an agent default like openai/gpt@work plus a session override to Anthropic will prepare anthropic/<utility>@work even though the chat session itself uses the default Anthropic credential. Fresh evidence beyond the earlier session-profile issue: the fixed sessionAuthProfile branch rewrites profiles, but the no-session-profile branch still preserves the stale agent-primary profile.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a69d824c57
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| !activeSessionKey || | ||
| titlesByKey.has(request.key) || | ||
| pendingKeys.has(request.key) || | ||
| failedKeys.has(request.key) || |
There was a problem hiding this comment.
Scope failed title keys to the active fetch context
When one request returns no title for an item, this process-global failedKeys guard blocks the same tool-name/input digest for every later client, session, and agent. For example, if agent A has no usable utility route (or a transient disconnect/older gateway makes the batch fail), the key is marked failed; opening the same command in agent B after reconnect or after fixing config will return here and never ask the gateway that could now generate the title, leaving that call stuck on deterministic labels until a page reload. Key failures by client/session/agent or clear them when the fetch context changes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 56edd037e3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const modelRef = params.sessionAuthProfile | ||
| ? `${strippedRef}@${params.sessionAuthProfile}` | ||
| : resolvedRef; |
There was a problem hiding this comment.
Avoid applying session profiles to other providers
Fresh evidence beyond the earlier session-profile comments: the new rewrite now applies the session auth profile even when the resolved utility ref is an explicit different provider. In a session using authProfileOverride: "work" for Anthropic with utilityModel: "openai/gpt-5.6-luna", this turns the utility ref into openai/gpt-5.6-luna@work; prepareSimpleCompletionModelForAgent then treats that suffix as a locked profile, and the profile-id path in src/agents/model-auth.ts:1149 resolves it before generic OpenAI fallback, so title generation can fail or attempt the wrong credential despite the docs allowing explicit cross-provider utility models. Only replace the embedded profile for same-provider/derived refs, or preserve the explicit utility model's own profile.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1edb9b08a2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // args can carry raw tokens/signed URLs, and titles must not become a new | ||
| // secret egress path. Redaction runs in tools mode regardless of logging | ||
| // config. | ||
| const input = redactToolPayloadText(item.input.slice(0, TOOL_TITLE_INPUT_MAX_CHARS)); |
There was a problem hiding this comment.
Redact tool-title input before truncating
When a secret-bearing tool input crosses the 2,000-character cutoff, slicing first can turn a registered secret or prefix-pattern token into an unrecognized fragment before redactToolPayloadText runs, so that fragment can still be cached and sent to the utility model despite the redaction contract. The protocol already bounds each input, so redact the received string first and then truncate the redacted result (and keep the UI cap aligned) to avoid boundary leaks.
Useful? React with 👍 / 👎.
Restores the chat.toolTitles path removed in #103821, gated behind the new gateway.controlUi.toolTitles opt-in (default false) so tool rendering stays fully deterministic with no background model calls unless an operator enables it. Disabled gateways answer { titles: {}, disabled: true } without loading the completion runtime, and clients stop asking for the session. When enabled, titles use canonical utility-model routing: an explicit utilityModel (operator-chosen provider, like every utility task), else the session provider's declared small-model default, honoring per-session model overrides and auth profiles; utilityModel "" disables titles and malformed refs fail closed — never the primary model. Tool inputs are redacted with the tools-mode redactor before cache keys or prompts, caller ids are bounded and never reach the model, and results cache in the per-agent SQLite cache_entries so repeat views never re-bill. Also completes two crestodian model-input mock factories that leaked into sibling tests under shared-registry CI shards. Fixes #103987
1edb9b0 to
3560594
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eae25df377
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (command.length < MIN_COMMAND_CHARS_FOR_TITLE) { | ||
| return null; | ||
| } | ||
| const input = command.slice(0, MAX_TITLE_INPUT_CHARS); |
There was a problem hiding this comment.
Redact UI tool-title inputs before slicing
Fresh evidence beyond the earlier server-side truncation concern: this browser path still slices the raw command before the gateway redactor sees it. When a secret-bearing command crosses the 2,000-character UI cutoff (for example an Authorization token beginning near the boundary), only an unrecognizable prefix is sent in chat.toolTitles, so redactToolPayloadText cannot match the full token and that fragment can still be cached and sent to the utility model; redact before this slice or send the bounded raw input to the gateway for redact-then-truncate handling.
Useful? React with 👍 / 👎.
|
Merged via squash.
|
…nclaw#103989) * feat(webui): reintroduce opt-in AI purpose titles for tool calls Restores the chat.toolTitles path removed in openclaw#103821, gated behind the new gateway.controlUi.toolTitles opt-in (default false) so tool rendering stays fully deterministic with no background model calls unless an operator enables it. Disabled gateways answer { titles: {}, disabled: true } without loading the completion runtime, and clients stop asking for the session. When enabled, titles use canonical utility-model routing: an explicit utilityModel (operator-chosen provider, like every utility task), else the session provider's declared small-model default, honoring per-session model overrides and auth profiles; utilityModel "" disables titles and malformed refs fail closed — never the primary model. Tool inputs are redacted with the tools-mode redactor before cache keys or prompts, caller ids are bounded and never reach the model, and results cache in the per-agent SQLite cache_entries so repeat views never re-bill. Also completes two crestodian model-input mock factories that leaked into sibling tests under shared-registry CI shards. Fixes openclaw#103987 * fix(webui): redact tool-title inputs before truncation
What Problem This Solves
Fixes #103987.
PR #103748 shipped AI purpose titles for tool calls; #103821 removed them because the feature was always-on — rendering a chat could trigger model calls, route tool arguments to a provider, spend utility-model tokens, and persist a cache purely for decorative labels. The titles themselves remain wanted (#103554): long agent sessions read far better when complex calls say "Ran linked list tests" instead of a raw command.
Why This Change Was Made
Reintroduces the titles path with the #103821 objections answered by an explicit opt-in: default behavior is exactly what main ships today — fully deterministic tool rendering with zero background model calls.
User Impact
gateway.controlUi.toolTitlesconfig key, defaultfalse(schema, types, help/labels/tags, and configuration-reference docs included).chat.toolTitlesanswers{ titles: {}, disabled: true }without loading the completion runtime, and the Control UI stops asking for the rest of the session (one lightweight probe per connection, no retries).resolveUtilityModelRefForAgent): the agent'sutilityModel, else the primary provider's declared small-model default (OpenAI →gpt-5.6-luna, Anthropic →claude-haiku-4-5) — same provider as the session, so no new egress destination — and the documentedutilityModel: ""opt-out disables titles like every other utility task. Titles never fall back to the primary model. Results cache in the per-agent SQLitecache_entries(scopetool-call-titles), so repeat views never re-bill. All errors fail closed to deterministic labels.Protocol surface is additive:
ChatToolTitlesParams/ChatToolTitlesResult(result gains thedisabledflag), validator,operator.writedescriptor (spends tokens on cache misses when enabled), regenerateddist/protocol.schema.json+ SwiftGatewayModels.swift.Evidence
src/gateway/chat-tool-titles.test.ts(title generation, SQLite cache hit, fail-closed preparation, canonical utility-model ref selection, disabled/no-default skip) andui/src/pages/chat/tool-titles.test.ts(eligibility, digest stability, session/agent capture across pane reconfiguration, and the new disabled-gateway stop test), plus the adjacent renderer/protocol/config suites (chat-tool-cards,chat-message,chat-thread, gateway-protocol index, config-misc) — 210+ assertions locally, full gates re-run by PR CI.protocol:checkparity).