Repository navigation
Make timed button presses cancellation-safe #39
Description
User and workflow
An agent or game loop starts a bounded button press, then needs to stop that action when its task is cancelled, its target changes, or the user takes control. A finite durationMs currently runs to completion; the caller cannot pass cancellation into the runtime.
Direct evidence
- Codex issue #34316, opened 2026-07-20, reports an emulator missing near-instant key presses because it samples the key state periodically; the report asks for a held input with a longer duration.
- Codex issue #44074, opened 2026-09-09, reports that a game control required a sustained press and explicitly asks for owned inputs to be released on cancellation, failure, navigation, target loss, or session teardown. Its author measured 1.2–3.8 ms key-down intervals in that Computer Use session; this is not a timing guarantee for other systems.
These are Computer Use reports, not OpenController incidents. They show two concrete game/emulator workflows and a cleanup need; they do not establish prevalence.
Repository gap and interpretation
packages/core/src/controller.ts exposes finite-duration press calls, while packages/core/src/runtime.ts waits for the full duration before its release call. The options in packages/core/src/types.ts have no AbortSignal. docs/safety.md describes neutral-on-error and neutral-on-disconnect, but not cancellation of one in-flight timed press. This differs from CLI cleanup in #20, negative-duration validation in #34, and timing measurement in #28.
Proposed outcome
Add cancellation to finite-duration button presses only. Define cancellation at the adapter boundary as follows:
- If the signal is already aborted when
pressis called, reject before queueing and make no adapter call. - If the command is queued but has not started, recheck the signal when it reaches the queue; reject without dispatch if aborted.
- Once
adapter.send(press)has been called, the existing adapter interface cannot interrupt that promise. If the signal aborts while it is pending, wait for it to settle. If it resolves, treat the press as accepted by the adapter and issue one matching release before rejecting with the cancellation error. If it rejects, retain the adapter send failure in the cancellation error and mark the handoff outcome adapter-defined/unknown; do not claim the host did or did not observe a partial effect. - If the press send resolved before abort, issue one matching release and then reject with the cancellation error. A shared completion latch must ensure an abort racing the duration timer cannot produce two release calls. If timer expiry wins and its release has started, completion belongs to the timer path; a later abort does not initiate a second release.
- Export a stable
TimedPressAbortErrorfor cancellation rejection. It must exposeabortReason, a primarycause, and optionalpressSendError,releaseError, andneutralizationErrorfields. When cancellation occurs duringadapter.send(press)and that send rejects, use the send failure ascauseand also retain the abort reason. When cancellation release fails, use the abort reason ascauseand preserve the release failure separately. ControllerRuntime.sendmay run best-effort neutralization underneutralOnError. If that neutralization fails after a cancellation error, attach the failure asneutralizationErrorand rethrow the cancellation error; do not let it replace the abort, press-send, or release failure. This special aggregation is limited to the timed-press cancellation path and must not silently change unrelated command error precedence.
All guarantees stop at adapter promises: a resolved adapter.send means only that the adapter call resolved. It does not prove that an OS, device, browser, or game observed the press or release.
Acceptance criteria
- Add an optional
AbortSignalto the options form of finite-durationcontroller.presswithout changing the numeric-duration overload. Cancellation applies to positivedurationMspresses; preserve the current zero-duration behavior and do not imply that aborting a zero-duration press releases it. - Test already-aborted and queued-but-not-dispatched signals; both send no press to the adapter.
- Test abort while
adapter.send(press)is pending: cancellation waits for settle, releases once after a successful send, and preserves the adapter error if send rejects. - Test abort after press-send resolution and verify one release call occurs before rejection.
- Test timer/abort races and verify there is exactly one release call.
- Test cancellation-release failure and abort while the initial adapter send rejects;
TimedPressAbortErrormust retain the abort reason and every applicable adapter failure in the documented fields. - With
neutralOnErrorenabled, test that a failing best-effortforceNeutralafter cancellation is added asneutralizationErrorwithout replacing the cancellation error or earlier failures. Keep existing error precedence for non-cancellation commands unchanged. - Preserve normal duration/release behavior when not aborted and preserve persistent
setButtonsemantics. - Document adapter-boundary semantics and explicitly state that adapter acceptance is not host/game observation.
Tests and validation
Use a controllable test adapter whose send and state-sync/neutralization promises can be paused and resolved/rejected. Cover pre-queue, queued, in-flight, accepted, timer-race, release-failure, and release-plus-neutralization-failure cases in a new focused packages/core/src/tests/timed-press-cancellation.test.ts file, separate from controller.test.ts and the hub/profile lanes. Assert the stable cancellation error fields and unchanged error behavior for an unrelated command failure. Run core tests, typecheck, and the relevant public API build.
Non-goals
- No keyboard or mouse injection, game/window targeting, minimum press duration, or frame-accuracy promise.
- No cancellation for indefinite
setButton, sticks, triggers, combos, or multi-command action maps. - No attempt to abort an adapter's already-pending I/O, and no guarantee about a host/game consuming input.
- No change to CLI native-test cleanup or general error-neutralization policy.
Confidence and limits
Medium. Two detailed dated reports describe timing-sensitive emulator/game input, and the API currently has no per-call cancellation. Both reports concern another product; no OpenController user report demonstrates this need.
Ownership and dependencies
Lane: packages/core runtime/API, tests, and docs/safety.md. Independent of CLI cleanup and adapter-capability work. One agent can own this API slice; no shared example files are required.
Activity