Skip to content

fix(desktop): keep quoted literals in agent-activity send previews - #6841

Open
baxen wants to merge 2 commits into
mainfrom
ss-dev-01/classifier-quote-context
Open

fix(desktop): keep quoted literals in agent-activity send previews#6841
baxen wants to merge 2 commits into
mainfrom
ss-dev-01/classifier-quote-context

Conversation

@baxen

@baxen baxen commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

The bug

An agent posting ordinary markdown loses the text of its own message in the agent activity view. These two sends differ only in the body:

--content 'Fixed `labelForStatus` in the rail'   ->  preview: null
--content 'Fixed labelForStatus in the rail'     ->  preview: the text

Agents write markdown by default, so the broken case is the common one. This is part (a) of #6834.

Why

The guard is right in intent, but it tests characters instead of meaning. #2201 (c5d00a358) added it so --content "$(cat file)" and "$MESSAGE" could never be displayed as if the unexpanded shell expression were the sent text — that intent is kept, and its tests stay green.

The problem is upstream: tokenizeShellCommand discards quoting. By the time the guard runs, a single-quoted literal backtick — text the shell never touches — is indistinguishable from a real substitution, so both get dropped.

The fix

Preserve quote context through tokenization. Each token now carries hasSubstitution, set only where the shell could actually expand something — $ or a backtick unquoted or inside double quotes. Inside single quotes, or after a backslash, they are literal and the preview survives. The guard reads that flag instead of scanning characters.

Only single quoting is trusted, because only single quoting is literal in every shell BUZZ_SHELL can select. A backslash escape is not portable — verified by running PowerShell 7, where "literal \$MESSAGE" keeps the backslash and expands the variable — so escaped $/backtick stay flagged even though bash reads them as literal. (Codex caught this; see the resolved thread. First push credited \$ as a literal, which would have reintroduced the #2201 failure under BUZZ_SHELL=pwsh.)

The $ rule is deliberately conservative in the same direction: any $ outside single quotes flags the token, including places bash would leave it literal (a trailing $, $ before a space). Showing a shell expression as the sent message is the failure being prevented, so it errs towards dropping the preview.

Going the other way — teaching the classifier the selected shell dialect — is not available: it sees only the command string, with no record of which shell ran it.

tokenizeShellCommand is retained as a string-returning wrapper over the new tokenizeShellCommandTokens, so #2201's tokenizer test still exercises the same code path. It has no other callers in desktop/src.

Verification

Checked against bash, not against a reading of bash. For nine command lines, bash was asked for the real argv entry it passes as --content. Contract: if the shell changed the text, the preview must be null; if it did not, the preview must equal that argv byte for byte.

OK   single-quoted backtick expanded=false flagged=false bashArgv="Fixed `labelForStatus` in the rail" preview="Fixed `labelForStatus` in the rail"
OK   single-quoted dollar   expanded=false flagged=false bashArgv="Costs $5 per run, not $MESSAGE"    preview="Costs $5 per run, not $MESSAGE"
OK   escaped in dquotes*    expanded=false flagged=true  bashArgv="literal $MESSAGE and `code`"       preview=null
OK   --content= sq literal  expanded=false flagged=false bashArgv="use `pnpm test`"                   preview="use `pnpm test`"
OK   mixed literal+var      expanded=true  flagged=true  bashArgv="a `literal` EXPANDED_BY_SHELL"     preview=null
OK   unquoted var           expanded=true  flagged=true  bashArgv="EXPANDED_BY_SHELL"                 preview=null
OK   dquoted var            expanded=true  flagged=true  bashArgv="EXPANDED_BY_SHELL"                 preview=null
OK   real substitution      expanded=true  flagged=true  bashArgv="FILE CONTENTS FROM CAT"            preview=null
OK   plain prose            expanded=false flagged=false bashArgv="Confirmed the mismatch is..."      preview="Confirmed the mismatch is..."

* = intentionally more conservative than bash, per the cross-shell finding above.

An earlier version of this probe read the wrong argv position and failed loudly on 4/9 rather than passing falsely — noting that because a probe that silently agrees is the one to distrust.

Mutants — three, each with a distinct failure set (mutated line printed before each run, so a silently-failed revert can't contaminate the next):

Mutant Result
Revert the fix — restore includes("$") || includes("")` 4 fail: exactly the new quoted-literal tests
isSubstitutionChar always returns false (guard disarmed) 8 fail — four of them #2201's own, proving they are still load-bearing
Single quotes stop protecting (flag inside any quote) 4 fail: the tokenizer test + three quoted-literal tests
Backslash escape credits a literal again (the cross-shell hole) exactly 1 fail — the new escape-form test

Gates at 9dc2ec80d, read in the same shell as git rev-parse HEAD, tree clean:

  • Full desktop suite (not a scoped run): 5516 passing / 0 failing / 81 suites
  • tsc --noEmit, pnpm check, pnpm check:file-sizes all clean. The 2 biome warnings are pre-existing in sidebar/onboarding files this PR does not touch.
  • Push hooks green on both commits: desktop-check, desktop-typecheck, desktop-test (98.76s at head).

Scope — deliberately not fixed here

Both are recorded in #6834 and neither is blocked by this change:

  • Part (b), the rail's missing event-fetch fallback. Gated on an open design question (whether a mid-turn relay post should be a rail step at all), and the rail is not on main.
  • The latent isBuzzMessageSend dot/underscore mismatch. Real, but I could not find a path on main that shows it to a user, so fixing it here would be an unverifiable change.

Refs #6834

An agent posting ordinary markdown lost the text of its own message in the
activity view. `--content 'Fixed `labelForStatus` in the rail'` renders with no
message in it, while the same send in plain prose renders fine.

The cause is that the guard added by #2201 tests characters, not meaning. It
rejects any `--content` containing `$` or a backtick so that
`--content "$(cat file)"` and `"$MESSAGE"` can never be displayed as if the
unexpanded shell expression were the sent text. That intent is right and is kept.
But `tokenizeShellCommand` discards quoting, so by the time the guard runs a
single-quoted literal backtick -- ordinary markdown the shell never touches --
is indistinguishable from a real substitution, and both are dropped. Since
agents write markdown by default, the rejected case is the common one.

Quote context is therefore preserved through tokenization. Each token now
carries `hasSubstitution`, set only where the shell could actually expand
something: `$` and a backtick unquoted or inside double quotes. Inside single
quotes, or after a backslash, they are literal text and the preview survives.
The guard reads that flag instead of scanning characters, so it still rejects
every substitution #2201 was about -- `$(cat file)`, `$MESSAGE`, bare and
prefixed, and a token that concatenates a quoted literal with an unquoted
variable -- and those four tests continue to pin it.

The `$` rule stays deliberately conservative: any unescaped `$` outside single
quotes flags the token, including places bash would leave it literal. Showing a
shell expression as the sent message is the failure being prevented, so it errs
towards dropping the preview.

Verified against bash rather than against a reading of it: for nine command
lines, `bash` was asked for the real argv entry it passes as `--content`. Every
preserved preview is byte-identical to that argv, and every case where the shell
changed the text yields `null`.

Three mutants, each producing a distinct failure set: restoring the character
test fails the four new quoted-literal tests; making substitution tracking never
fire fails eight, four of them #2201's own; and making single quotes stop
protecting fails four. `tokenizeShellCommand` is kept as a string-returning
wrapper so #2201's tokenizer test still exercises the same path.

Not addressed here, and still open in #6834: the rail's missing event-fetch
fallback (part b, gated on a design call) and the latent dot/underscore
`isBuzzMessageSend` mismatch, which has no user-visible path on main.

Desktop suite 5516 passing / 0 failing; typecheck, check and the file-size
ratchet clean.

Refs #6834

Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
@baxen
baxen requested a review from a team as a code owner August 26, 2026 00:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60642ca333

ℹ️ 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".

Comment thread desktop/src/features/agents/ui/agentSessionToolClassifier.ts
Codex found a hole in the previous commit. Backslash-escaping earned literal
credit -- `--content "literal \$MESSAGE"` was displayed as sent text -- on the
assumption that a backslash escape is portable. It is not, and `BUZZ_SHELL`
explicitly supports the shells where it is not (`buzz-dev-mcp/src/shell.rs`
dispatches `-Command` for powershell/pwsh and `/C` for cmd).

Verified by running PowerShell 7 rather than reasoning about it:

    'Fixed `labelForStatus` in the rail'  -> Fixed `labelForStatus` in the rail
    'Costs $5 per run, not $MESSAGE'      -> Costs $5 per run, not $MESSAGE
    "literal \$MESSAGE"                   -> literal \      (variable EXPANDED)

So single quoting is literal in PowerShell too, and the fix this PR exists for
is portable. Backslash is not an escape character there at all: the shell keeps
the backslash and still expands the variable, which is exactly the #2201 failure
the guard prevents, and the old character test returned null for it.

Escaped `$` and backtick therefore stay flagged as substitutions even though
bash reads them as literal. Only single quoting is trusted, because only single
quoting means the same thing in every shell an operator can select. The affected
test now asserts null and says why.

The bash-argv probe still agrees on all nine command lines, with the escape case
marked as intentionally more conservative than bash. Mutating the new guard away
fails exactly one test -- the new one -- so it is not vacuous.

Desktop suite 5516 passing / 0 failing; typecheck, check and the file-size
ratchet clean.

Refs #6834

Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>

@Chessing234 Chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the right diagnosis. the guard was written against characters because tokenizeShellCommand had already thrown away the only thing that could answer the question, and carrying hasSubstitution out of the tokenizer puts the decision where the information is. the test that pins the mixed case — 'a literal '"$MESSAGE" — is the one that convinced me, because that's where a naive per-token flag would have leaked.

rejecting the backslash-escaped form as non-portable is also the right call, and the comment explaining it (bash literal, powershell not) is worth having.

one gap i'd want acknowledged, because the pr reframes what the guard means:

the old code said "reject $ or backtick", which was obviously a heuristic. the new comment says "quoting decides this, not the characters: a single-quoted or escaped $ / backtick is literal text the shell never touched". that reads as a general guarantee, and hasSubstitution still only tracks $ and backticks. the shell rewrites unquoted tokens in several other ways:

--content ~/notes        ->  preview "~/notes",        sent "/Users/me/notes"
--content report_*.md    ->  preview "report_*.md",    sent whatever globbed
--content {a,b}          ->  preview "{a,b}",          sent "a b"

none of those are new — main has the same holes — and they're much rarer in agent output than a markdown backtick, so i don't think they block. but the comment now invites the next reader to trust the flag as "the shell couldn't have changed this", and it doesn't mean that. either narrow the wording to the two constructs it actually tracks, or set the flag for an unquoted leading ~, */?/[, and {, } too — the tokenizer already knows the quote state, so it's cheap.

smaller: const group = tokens[range.groupIndex].value drops the optional access that the line below keeps (tokens[range.verbIndex]?.value ?? "run"). findBuzzCommand presumably guarantees groupIndex is in range, and main indexed it unguarded too, so this isn't a regression — but the two adjacent lines now disagree about whether the range is trustworthy, and one of them will be wrong if findBuzzCommand ever changes.

and worth exporting tokenizeShellCommandTokens with a short doc comment on hasSubstitution saying what it does and does not cover — it's a public API now, and it's the kind of flag someone will reach for from another call site expecting the general guarantee.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants