Skip to content

Make timed button presses cancellation-safe #39

Closed

Description

@SYMBaiEX

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:

  1. If the signal is already aborted when press is called, reject before queueing and make no adapter call.
  2. If the command is queued but has not started, recheck the signal when it reaches the queue; reject without dispatch if aborted.
  3. 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.
  4. 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.
  5. Export a stable TimedPressAbortError for cancellation rejection. It must expose abortReason, a primary cause, and optional pressSendError, releaseError, and neutralizationError fields. When cancellation occurs during adapter.send(press) and that send rejects, use the send failure as cause and also retain the abort reason. When cancellation release fails, use the abort reason as cause and preserve the release failure separately.
  6. ControllerRuntime.send may run best-effort neutralization under neutralOnError. If that neutralization fails after a cancellation error, attach the failure as neutralizationError and 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 AbortSignal to the options form of finite-duration controller.press without changing the numeric-duration overload. Cancellation applies to positive durationMs presses; 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; TimedPressAbortError must retain the abort reason and every applicable adapter failure in the documented fields.
  • With neutralOnError enabled, test that a failing best-effort forceNeutral after cancellation is added as neutralizationError without 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 setButton semantics.
  • 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    cycle-7Tracked work for OpenController AI and Gaming Improvement Cycle 7cycle-7-candidateEvidence-backed candidate prepared for Cycle 7 reviewenhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions