Skip to content

[compass] Fix: a seat_type call Windows refuses partway loses the count of what was typed - #23

Merged
skulitom merged 3 commits into
mainfrom
compass/anode-fix-2026-10-06-typing-send-failure
Oct 8, 2026
Merged

skulitom merged 3 commits into
mainfrom
compass/anode-fix-2026-10-06-typing-send-failure

Conversation

@skulitom

@skulitom skulitom commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

The report

Compass's Anode inbox (ANODE-BUGS.md), from compass-merge's second review of #18:

  • B-021: a SendInput failure partway through seat_type loses the count of what was typed.
  • B-020 (its docs half): the docs said a stopping seat ends seat_type with typing_stopped, which in practice it doesn't.

B-021: root cause and fix

Cause. TextTyping.Type called typist.Send with no catch. InputInjector.Send throws in two cases:

  • an InvalidOperationException when the seat's input is blocked (WindowAccess.InputBlockReason());
  • a Win32Exception when SendInput accepts fewer events than it was given.

The exception left Type, and the seat pipe's handler turned it into a bare "{type}: {message}" reply, with no errorCode, typed, total or nextIndex. The agent couldn't tell how much text had reached the field before retrying, and a retry could type it twice. #18's 45-second calls made the window wider.

Fix.

  • InputInjector.TypingStep sends each step of typing: a character's events, or an Enter's or Tab's press and release.
    • A blocked seat or a short SendInput count becomes a TextTyping.Refused.
    • It carries PartlySent when part of the step went out first: an Enter whose press went out but whose release was refused, or some of a character's events.
    • Other input operations keep Send and its errors unchanged. SendCounted is the shared part, and it still reads the Win32 error right after SendInput.
  • TextTyping.Type catches only Refused. It ends the call like the other stops: ok: false, errorCode: "typing_stopped", and the exact typed, total and nextIndex.
    • It adds partlySent: true only when part of that character went out. The message then says it may already have taken effect, as a pressed Enter does.
    • A key left down that way stays in HeldKeys, and Anode releases it when the lease ends, as before.
    • Any other exception, ObjectDisposedException included, is a fault in Anode and still surfaces.
  • The reply never repeats the text. It names the refusal with Windows' or Anode's own message.

Not every refusal can be reported. Windows may drop input that an elevated window doesn't accept without an error. TROUBLESHOOTING and PROTOCOL now say so, instead of offering an elevated window as an example.

B-020: docs only

The daemon sends shutdown on its one shared seat connection. A running input.text holds that connection, so shutdown times out unsent after 1.5 s. The daemon then closes the pipe, and the sign-out ends the seat host.

The docs now say a seat that stops during a call ends it with a lost connection or typing_stopped. The viewer disconnects first, so input may be refused before the pipe closes. Either way the sign-out closes the program, so don't send the rest.

The cancel reason now names the request's time first, which is what cancels typing in practice, and still mentions a stopping seat. #18's CHANGELOG entry and a SeatHost.cs comment are corrected too.

How I tested it

Second review

Codex is at its usage limit until 9 October, so fresh read-only Claude subagents reviewed.

First pass, on 8a58f77: problems found. All fixed in 371437c:

  • P1: an elevated window is not a reported refusal (UIPI drops input silently), and the UAC example was unverified. Both examples are gone, and the docs say silent drops happen.
  • P2: the cancel reason still blamed a stopping seat. It now names the request's time.
  • P2: on stop, the viewer disconnects first, so typing may answer typing_stopped. The docs are hedged.
  • P2: partlySent was true on every refusal. It is now computed where the events are sent, and only Refused is caught.
  • P3: held-key wording, gaps in the check, and the doc comment. All fixed.

Second pass, on 371437c: no P1 or P2. It confirmed several points:

  • other input operations behave and fail exactly as before;
  • the Win32 error is still read right after SendInput;
  • the partlySent flag is right in every case: a block, a refused press, a refused release, and 0 or some of a character's or surrogate pair's events;
  • the checks fail against 8a58f77's behaviour and don't flake.

Its P3s were fixed in fd2356c: the elevated-window note now says "may", the cancel reason names both causes, a stop at the first character says "Nothing was sent.", and a SeatHost comment is corrected. One P3 is left: the checks pass Refused with a preset flag, so TypeUnit's own flag logic (pressed, sent > 0) runs only in a seat. It is listed below.

CI: green on 8a58f77, 371437c and fd2356c.

All four open fix PRs merged together on main (#20, #21, #23, #24, locally): 95/95 quick checks, documentation consistent.

Still to check in a real seat

  1. Type a few lines into Notepad, then cause a refusal partway through (this also exercises TypeUnit's flag logic). For example, have a typed Enter open something that takes the input desktop, such as a UAC prompt. The reply should be typing_stopped with the right count, and partlySent only when an Enter was cut off.
  2. Type into an elevated window, and note whether the reply is ok with the text lost, or typing_stopped. The docs say it may be either.

🤖 Generated with Claude Code

TextTyping.Type called typist.Send with no catch. InputInjector.Send throws
when the seat's input is blocked or SendInput accepts fewer events than it was
given, for example once a typed Enter opened a UAC prompt or an elevated window.
The exception left Type, and the pipe handler turned it into a bare
"{type}: {message}" reply with no typed, total or nextIndex, so an agent could
not tell how much of the text had reached the field before it retried.

A refusal now ends the call like the other stops, with the exact count,
errorCode typing_stopped and partlySent: true, since the refused character
(an Enter pressed but not released, say) may have arrived in part. Other
exceptions still surface as before.

The docs no longer say a stopping seat ends typing with typing_stopped
(B-020): the daemon's shutdown waits behind the running input.text on the
shared seat connection, so the caller sees the connection close, and the
sign-out closes the program anyway.

Reported in Compass's Anode inbox as B-021 and B-020 (the second review of #18).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

skulitom and others added 2 commits October 6, 2026 23:55
Review follow-up. Typing catches only TextTyping.Refused now, which the
typing path in InputInjector throws: TypingStep sends one step, and a block or
a short SendInput count becomes Refused, with PartlySent when an earlier part
of the step (an Enter's press) or some of its events went out. Any other
exception, ObjectDisposedException included, still surfaces as a fault.

The docs no longer name an elevated window as a refusal: Windows drops input
an elevated window doesn't take without reporting it, which TROUBLESHOOTING
and PROTOCOL now say. A seat that stops during a call may answer with a lost
connection or typing_stopped; either way the program is closed. The cancel
reason names the request's time, which is what cancels typing in practice.
The check covers refusals at the first character, at an ordinary one and at
a key whose release was refused, and that the reply never repeats the text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review follow-up (P3s): Windows may drop input to an elevated window
without an error, rather than always does; the cancel reason names the
request's time or a stopping seat, since a shutdown on another connection
cancels the same token; a stop at the first character says "Nothing was
sent."; and the SeatHost comment names the request's deadline, not the lease's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@skulitom

skulitom commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

Compass review: ready to merge

Tested by compass-merge on 2026-10-07 on head fd2356c, with the current main (7f14875, after tonight's merges of #20, #21 and #22) merged in locally:

  • FOCUS test command (scripts\build.ps1 -QuickTest): 93/93 quick checks pass, documentation check consistent. With [compass] Fix: a request queued behind a long seat operation loses its reply and detaches the seat #24 merged in as well: 95/95.
  • CI: build green on this head.
  • Review (the whole diff): a refused SendInput or blocked input in a typing step becomes TextTyping.Refused, and Type answers typing_stopped with exact typed/total/nextIndex. partlySent is set only when part of the character went out: an Enter press whose release was refused, or 1 of 2 events. Other exceptions still surface, and the new check proves it. Send() behaves as before. The docs' "Anode releases the key when the lease ends" matches EndLease → ReleaseHeld(), since a failed release leaves the key in HeldKeys.
  • Second review: compass-anode-fixer's two fresh read-only Claude subagent passes (Codex is at its usage limit). The P1 and the P2s are fixed in 371437c, and the P3s in fd2356c.

Not merged tonight only because of the five-merges-per-run cap (#20, #21, #22 and PixelForge #4 and #5 were older). The next compass-merge run merges it if nothing changes.

@skulitom
skulitom merged commit 1a41f41 into main Oct 8, 2026
1 check passed
@skulitom
skulitom deleted the compass/anode-fix-2026-10-06-typing-send-failure branch October 8, 2026 00:21
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.

1 participant