Skip to content

Cross-machine handoff can still false-fail: sync host's global 60s responder timeout breaks the "reported failure = actually cancelled" contract #859

Description

@arul28

Principle (the real bug class)

ADE must never tell the user an operation failed while that operation keeps running to a later success. Either:

  1. Timeout = cancellation — when a timeout error is surfaced, the underlying operation is actually aborted (no surprise session/state appearing later), or
  2. Honest late-success reconciliation — if the operation cannot be cancelled, the UI must not claim failure; it says the operation is still finishing and reconciles the late result as the expected outcome.

ADE-122 (PR #858) fixed this for local handoffs by extending the requester timeouts (120s daemon action / 150s IPC) and mapping residual transport timeouts to honest "still finishing in the background" copy + delayed session-list refreshes. One path was intentionally left out of scope:

Remaining gap: cross-machine ("Send to machine") accept path

The sync host/responder wraps every incoming remote command in a single global timeout: DEFAULT_SYNC_MESSAGE_TIMEOUT_MS = 60_000 in apps/ade-cli/src/services/sync/syncHostService.ts (~L244; raced around handleMessagehandleCommandexecutor.execute, ~L4358 / ~L4709-4720 / ~L5710-5716). The chat.handoffSession / chat.prepareCrossMachineHandoff / accept-side materialization executors registered in syncRemoteCommandService.ts (~L3768) run inside that 60s envelope.

A cross-machine handoff accept that takes the destination >60s (large fork capsule inflate + provider-file materialization + first-message dispatch on slow hardware) hits exactly the ADE-122 movie again: the sender is told the handoff failed, the destination keeps materializing (the accept path is deliberately idempotent/resumable — forkMaterializedAt + deterministic session id), and a "surprise" session appears later on the destination machine.

Note the requester side is already patient (desktop→remote prepareCrossMachineHandoff requester = 120s in remoteConnectionPool.ts ~L127; CLI default 600s), so today the responder's 60s is the binding constraint — the tightest timeout in the chain is a shared global that individual long-running commands cannot opt out of.

Suggested direction

  • Add a per-command timeout override map to the sync responder (mirroring LONG_RUNNING_LOCAL_RUNTIME_ACTION_TIMEOUTS in localRuntimeConnectionPool.ts) and give the two handoff commands ≥120s, keeping the 60s default for everything else. Keep the invariant requester > responder ≥ inner-work bound.
  • OR (stronger, per the principle): make the responder timeout actually cancel the executor (AbortSignal through the command executors) so a timeout reply is truthful.
  • Either way, the sender-side error copy for a cross-machine timeout should follow the ADE-122 pattern: if the destination may still complete (idempotent accept), say so instead of claiming failure.
  • Regression test: a slow (>timeout) fake executor for chat.handoffSession on the host must either be cancelled or be reported as "still completing", never as a hard failure followed by silent success.

Context

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions