Principle (the real bug class)
ADE must never tell the user an operation failed while that operation keeps running to a later success. Either:
- Timeout = cancellation — when a timeout error is surfaced, the underlying operation is actually aborted (no surprise session/state appearing later), or
- 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 handleMessage → handleCommand → executor.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
Principle (the real bug class)
ADE must never tell the user an operation failed while that operation keeps running to a later success. Either:
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_000inapps/ade-cli/src/services/sync/syncHostService.ts(~L244; raced aroundhandleMessage→handleCommand→executor.execute, ~L4358 / ~L4709-4720 / ~L5710-5716). Thechat.handoffSession/chat.prepareCrossMachineHandoff/ accept-side materialization executors registered insyncRemoteCommandService.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
prepareCrossMachineHandoffrequester = 120s inremoteConnectionPool.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
LONG_RUNNING_LOCAL_RUNTIME_ACTION_TIMEOUTSinlocalRuntimeConnectionPool.ts) and give the two handoff commands ≥120s, keeping the 60s default for everything else. Keep the invariant requester > responder ≥ inner-work bound.chat.handoffSessionon the host must either be cancelled or be reported as "still completing", never as a hard failure followed by silent success.Context
AgentChatPane) matches the two local transport wrapper messages; a sync-responder timeout has a different error shape and will currently surface as a raw hard failure.