Commit 5123707
fix(live-voice,computer-use): narration budget, AX tree size, and the scripting story (#42806)
* fix(live-voice): give progress narration a budget it can actually meet
A live-voice turn that runs tools speaks short progress updates into the
dead air between them. The budget for generating that text was 1500ms,
which sits at the edge of the narrator model's real roundtrip: across one
three-minute computer-use turn, 9 of 22 attempts (41%) aborted at the
deadline, every one of them at totalMs 1500-1503 against timeoutMs 1500.
A budget that only sometimes covers the roundtrip does not make narration
faster, it thins it out - and it thins out precisely where the updates are
worth most, because the beats that take longest to generate are the ones
with the longest silence behind them. This is the failure mode the sibling
endpointDecisionTimeoutMs already documents. The budget is explicitly not
latency-critical (it speaks into dead air), so raise it to 5000ms, still
under the 6000ms minGapMs so an update that spends the whole budget lands
before the next one is due.
Two things hid this. The timeout aborted with kind "voice_session_aborted",
which reads in the logs as the session dying once per update rather than
one beat going unspoken, so give it its own kind. And every drop logged the
same line whatever the cause, so name the outcome (timed_out /
session_ended / error) as one greppable field. Neither the module nor the
log says anything on the success path - the spoken count lives in the
live-voice-metrics `progressUpdatesSpoken` field - so a drop rate was only
ever legible by pairing the two, which is how a 41% rate went unnoticed.
Also reflows one pre-existing over-long assertion in conversation-error
test so the touched file passes the formatting hook.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(live-voice): state the spoken-update count in the log even at zero
The turn log carried progressUpdatesSpoken only when it was above zero, so
the one value that distinguishes "this turn was asked for fewer updates and
correctly spoke none" from "narration failed every attempt" was the one
value the log never showed. Both render as an absent field.
That is not hypothetical: reading these logs, an absent field was twice read
as "narration never ran" on a turn that had in fact suppressed updates
exactly as the caller asked out loud.
Only the log changes. The telemetry payload keeps the omission, because
there the absence means something different and deliberate - a turn that
never engaged the feature is unchanged on the wire (see optionalTurnFields,
whose contract the existing "omits endpoint and progress fields" test
pins).
No new test: the log line is one field on a module-level logger, and
mocking that module here risks the cross-file mock leakage this repo
already has trouble with. The wire-shape guard that could actually regress
is the existing test, and it still passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(computer-use): leave scrolled-away rows out of the AX tree
A scroll view shows part of its contents and hides the rest, but the
accessibility tree reports a scrolled-away row at the frame it would have
if it were on screen. The CU tree walked the raw element tree and listed
every one of them, so a Finder window of ~700 files came back as ~735
interactive rows spanning y=224 to y=15460 on an 1117pt screen - roughly 45
of them actually drawn, the other ~94% reported at real-looking positions
with nothing there. At ~20 tokens a line that is ~15k tokens per step, and
since 42762 every action returns one.
The fix is already in this file: flattenClipped carries each ancestor's
crop down the tree and its doc comment states this exact problem, but it
had one caller (the coachmark measurement path in main.swift) and the tree
the model reads was not it. Thread the same AXClip.narrowed through
collectFormatted and drop what the crop leaves nothing of, reusing
AXDisplayMatch.frame for the geometric test so "any overlap counts" has one
definition rather than two - a row half out of the pane stays listed.
The count is reported rather than silently dropped. A list quietly reduced
to its visible rows reads as a short list, and the model needs to know the
rest are reachable by scrolling.
A tree with nothing cropping in it keeps a nil clip the whole way down and
is reported exactly as before, so apps that scroll nothing are unaffected.
Not unit-tested: formatAXTree lives in MacHelperExecutable, which has a
top-level main.swift and no test target. The geometric rule it now leans on
is covered in MacHelperCoreTests (AXClipTests already asserts a
scrolled-away row fails standsOn its pane's clip). Verify in the dev app
against a long Finder list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(computer-use): teach the scripting dictionary before the menu click
The scripting section lists three things AppleScript is for - "a menu item
(<worked example>), an app's own scripting dictionary, or window management"
- and then spends the rest of the paragraph on the menu item. The dictionary
gets four words and no example. Models copy the example.
They did. Asked to open Finder, go to the Desktop and sort by name, a voice
turn reached for computer_use_run_applescript exactly as told and used the
one pattern it had been shown: System Events clicking Go > Desktop. It then
fell back to two raw clicks on the column header to sort. Finder has a
scripting dictionary in which that whole task is four lines, and the run
cost an app open, a 7,497-element accessibility walk and two clicks instead.
So lead with the dictionary and give it the worked example, and demote
System Events to what it actually is: the fallback for apps with no
dictionary entry for what you need, and UI automation in a script's clothes
- still dependent on the menu sitting where you expect and the app being
frontmost. The iMovie menu example stays, one rung down.
The tool description carried the same ordering ("menu items, an app's
scripting dictionary, windows"), so both copies of it move too - the
manifest-regression test pins them to each other.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf(computer-use): stop walking rows a scroll view is not showing
Clipping the serialized tree cut what the model reads; it did not cut what
the helper does to produce it. The walk still visited every element: the
Finder observation that printed 735 rows enumerated 7,497 elements and took
28.5 seconds, against 39-653 elements for every other step in the same
session. The cost scales with how many files are in the folder rather than
with how many are on screen, and each skipped element is ~9 AX attribute
reads over Mach IPC plus its whole subtree.
So carry the same AXClip down the walk and skip children their scrolling
ancestor leaves nothing of, paying one frame read to save the rest.
Off by default, and behind a flag rather than applied to the enumerator at
large, because the other two callers need the opposite. `ax.locate` (the
coachmark path) exists to answer "where is this element", and a scrolled-away
element has to be in the tree for `flattenClipped` to answer "it exists and
is not on screen" - dropped during the walk, that answer becomes "not
found", which puts a coachmark somewhere else entirely. Only the CU
observation path, whose tree is a list of things to act on right now, opts
in.
Two deliberate asymmetries with the formatter's clipping:
- What the walk skips never reaches the formatter to be counted, so the
count is passed across and summed. Otherwise the "scrolled out of view"
note - the thing that tells the model to scroll rather than conclude the
list is short - would silently go to zero.
- The walk does not reuse AXDisplayMatch's test, where an element with no
area is on nothing. That is right for one being listed and wrong for one
being walked, since skipping here discards the whole subtree under it, and
a wrapper reporting no frame (web content does) must be descended into
rather than judged.
Not unit-tested, same reason as the formatter change: MacHelperExecutable
has a top-level main.swift and no test target. Worth QAing against both a
long Finder list and a scrolled web page, and worth re-checking a coachmark
lands correctly since that path deliberately did not change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: drop the em dashes this branch added
AGENTS.md bans them outright, and the accessibility-tree note was the strict
case: a string the model reads, written in punctuation the assistant's own
prompt forbids. Replaced with a colon there and with periods or a colon in
the four comments.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent a170659 commit 5123707
15 files changed
Lines changed: 207 additions & 32 deletions
File tree
- assistant/src
- __tests__
- calls
- __tests__
- config
- bundled-skills/computer-use
- schemas
- __tests__
- live-voice
- tools/computer-use
- util
- clients
- docs/src/app/docs/_components
- macos/native/mac-helper/Sources/MacHelperExecutable/ComputerUse
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1021 | 1021 | | |
1022 | 1022 | | |
1023 | 1023 | | |
1024 | | - | |
| 1024 | + | |
1025 | 1025 | | |
1026 | 1026 | | |
1027 | 1027 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1490 | 1490 | | |
1491 | 1491 | | |
1492 | 1492 | | |
| 1493 | + | |
1493 | 1494 | | |
1494 | 1495 | | |
1495 | 1496 | | |
| |||
1727 | 1728 | | |
1728 | 1729 | | |
1729 | 1730 | | |
1730 | | - | |
| 1731 | + | |
| 1732 | + | |
| 1733 | + | |
1731 | 1734 | | |
1732 | 1735 | | |
1733 | 1736 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
188 | 211 | | |
189 | 212 | | |
190 | 213 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
168 | 171 | | |
169 | 172 | | |
170 | 173 | | |
| |||
260 | 263 | | |
261 | 264 | | |
262 | 265 | | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
263 | 275 | | |
264 | 276 | | |
265 | 277 | | |
| 278 | + | |
266 | 279 | | |
267 | 280 | | |
268 | 281 | | |
| |||
Lines changed: 26 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
50 | 69 | | |
51 | 70 | | |
52 | 71 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
269 | 269 | | |
270 | 270 | | |
271 | 271 | | |
272 | | - | |
| 272 | + | |
273 | 273 | | |
274 | 274 | | |
275 | 275 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
78 | | - | |
| 78 | + | |
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
| 77 | + | |
78 | 78 | | |
79 | | - | |
| 79 | + | |
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
511 | 511 | | |
512 | 512 | | |
513 | 513 | | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
514 | 523 | | |
515 | 524 | | |
516 | 525 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
352 | 352 | | |
353 | 353 | | |
354 | 354 | | |
355 | | - | |
| 355 | + | |
356 | 356 | | |
357 | 357 | | |
358 | 358 | | |
| |||
0 commit comments