Skip to content

feat(computer): add --char-delay for lossy keyboard relays (#262)#289

Merged
muqsitnawaz merged 1 commit into
mainfrom
feat-262-char-delay
Jun 15, 2026
Merged

feat(computer): add --char-delay for lossy keyboard relays (#262)#289
muqsitnawaz merged 1 commit into
mainfrom
feat-262-char-delay

Conversation

@muqsitnawaz

Copy link
Copy Markdown
Contributor

Closes #262.

Problem

Events.typeText hardcoded the inter-character sleep at Thread.sleep(forTimeInterval: 0.004). During the #258 acceptance run against a Parallels Windows 11 guest, a 28-char type-text stream dropped everything after char 11 mid-stream (daemon reported frontmost: true, ok: true — the drop happened inside the guest's keyboard relay). A slower inter-char rate recovers it.

Change (3 surfaces)

  1. Daemon (Swift)packages/computer-helper/Sources/ComputerHelper/Events.swift: parse optional char_delay_ms from the type_text RPC params (via the existing Params.intOpt helper), clamp to [1, 250]ms, default stays 4ms (backward compatible). Replaced Thread.sleep(forTimeInterval: 0.004) with Thread.sleep(forTimeInterval: Double(charDelayMs)/1000.0).
  2. CLI (TypeScript)src/commands/computer-actions.ts: added --char-delay <ms> to agents computer type-text, parsed to int, clamped CLI-side via the new pure clampCharDelay helper (defense in depth) and forwarded as char_delay_ms in the RPC payload. Returns undefined when unset so the daemon applies its own 4ms default.
  3. Clamp enforced on both sides — daemon min(250, max(1, … ?? 4)); CLI clampCharDelay mirrors [1, 250] and truncates fractional input.

Tests

src/commands/computer-actions.test.ts — 6 new clampCharDelay cases (default/unset, typical 25, floor, ceiling, boundaries, fractional+NaN). Full file: 36 tests pass. No Swift XCTest target exists in Package.swift, so the clamp is covered CLI-side plus the real-flow timing run below.

Verified

  • swift build (daemon) — compiles clean (one pre-existing unrelated deprecation warning).
  • bun run build (TS) — tsc passes.
  • bunx vitest run src/commands/computer-actions.test.ts — 36/36 pass.
  • agents computer type-text --help renders --char-delay <ms>.
  • Real-flow timing driving the freshly-built daemon (trust_status: trusted:true) into a focused TextEdit, 18-char string:
    • char_delay_ms=1 → 34ms
    • char_delay_ms=250 → 4530ms (≈ 18 × 250)
    • char_delay_ms=5000 → 4533ms — clamped to 250 (would be ~90s unclamped)
    • All three: daemon returned {"ok":true,"chars":18,"frontmost":true}.

This proves parse + clamp + delay-applied through the new daemon code.

Needs the reporter's VM

The issue's acceptance criterion — ~10 consecutive 28+-char type-text runs into a Parallels guest with --char-delay 25 delivering without drops — requires the reporter's Parallels Windows 11 guest. That is not reproducible on this host; the drop is inside the guest's keyboard relay. The mechanism (slower inter-char rate via --char-delay) is implemented and verified locally.

Follow-up (separate repo)

The skill-doc note — recommending --char-delay 25 (or higher) for lossy relays / VM guests in skills/computer/SKILL.md — lives in the .agents-system repo (gh:phnx-labs/.agents-system), not this one. Tracked as a follow-up there; intentionally not touched here.

type-text hardcoded a 4ms inter-character sleep. Into a Parallels/VM
guest's keyboard relay, a 28-char stream dropped everything after char
11 under a transient focus blip. Add an optional per-character delay so
callers can slow the stream for lossy relays.

- Daemon (Events.typeText): parse optional char_delay_ms, clamp to
  [1, 250]ms, default 4 (backward compatible); replace the hardcoded
  0.004 with Double(charDelayMs)/1000.0.
- CLI (computer type-text): --char-delay <ms>, parsed to int, clamped
  CLI-side (defense in depth) and forwarded as char_delay_ms.
- Tests: clampCharDelay unit tests (default/floor/ceiling/boundary).

Verified end-to-end driving the freshly-built daemon into TextEdit:
char_delay_ms=1 -> 34ms, =250 -> 4530ms, =5000 -> 4533ms (clamped to
250). The 10-run Parallels-guest acceptance needs the reporter's VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@prix-cloud

prix-cloud Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Ready to merge

Build: tsc clean — zero errors, zero new warnings.
Tests: 36/36 pass in computer-actions.test.ts. Full suite: 2360 passed, 60 skipped, 1 pre-existing failure in src/lib/browser/port.test.ts (environment-bound lsof/listener check, unrelated to this PR). CI confirms build (Node 22 + 24), gitleaks, and test all green.


Instructions read

AGENTS.md at repo root. Build command: bun install && bun run build && bun test. Ran exactly that.


What works well

Dual-side clamp is the right design. The CLI-side clampCharDelay (computer-actions.ts:105–108) and the daemon-side min(250, max(1, … ?? 4)) (Events.swift:54) are independent guards that agree on [1, 250]ms. If someone calls the daemon directly over the RPC socket with an out-of-range value, it's still safe. "Defense in depth" is correctly applied here, not cargo-culted.

Backward compatibility is provably preserved. When --char-delay is omitted, clampCharDelay(undefined) returns undefined (computer-actions.ts:106), so char_delay_ms is never forwarded in the RPC payload (computer-actions.ts:356). The daemon's ?? 4 default (Events.swift:54) applies, producing identical behavior to the pre-PR code.

Params.intOpt is the right helper. Confirmed in RPC.swift:138 — it accepts Int or Double (JSON number either way), returns nil for absent/wrong-type values. Correct choice vs. the throwing Params.int.

Test coverage is comprehensive. The 6 clampCharDelay cases (computer-actions.test.ts:168–195) hit every branch: undefined passthrough, in-range passthrough, floor clamp (0 and -100), ceiling clamp (1000), exact boundaries (1 and 250), and fractional+NaN. Nothing to add.


One tradeoff worth documenting (not a blocker)

At --char-delay 250, a string longer than ~120 chars will exceed the 30-second RPC timeout (computer-rpc.ts:227: RPC_TIMEOUT_MS = 30_000) before all characters are delivered. 250ms × 120 chars = 30s. The PR is solving for 28-char streams into a Parallels guest (recommended --char-delay 25, per the PR description), where the worst case is 700ms — well within the limit. For pathological combinations (very long string + max delay), the caller gets an rpc_timeout error, which is recoverable. The skill-doc follow-up (gh:phnx-labs/.agents-system) is the right place to document the tradeoff.


Things to verify manually

The 10-run Parallels Windows 11 acceptance (28+ chars, --char-delay 25, no drops) requires the reporter's VM. The mechanism is correct and locally verified — this is the expected remaining gap the PR author already called out.


Reviewed by Code Reviewer — actually ran the build and tests on this branch.

@muqsitnawaz muqsitnawaz merged commit 8f13985 into main Jun 15, 2026
5 checks passed
@muqsitnawaz muqsitnawaz deleted the feat-262-char-delay branch June 15, 2026 05:18
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.

computer: type-text per-char delay param for lossy keyboard relays (VM guests)

1 participant