Skip to content

feat: split session_timeout into access_timeout + completion_timeout#21

Open
synacktraa wants to merge 9 commits into
masterfrom
feat/timeout-redesign
Open

feat: split session_timeout into access_timeout + completion_timeout#21
synacktraa wants to merge 9 commits into
masterfrom
feat/timeout-redesign

Conversation

@synacktraa

Copy link
Copy Markdown
Owner

Why

One session_timeout knob did two unrelated jobs — orphan defense (operator never connects) and human work budget (operator connected but slow). Late clicks silently shrank the work window. Operators couldn't tell whether they ran out of time because nobody clicked or because the human took too long.

What changes

  • ServerConfig and per-call kwargs each gain access_timeout (start → first WS connect) and completion_timeout (first connect → detection match). None per-call inherits from ServerConfig; None on ServerConfig means truly no timeout.
  • Handoff.wait_for_completion resolves the two timers and races detection-match against both deadlines via _await_timeout_cause, returning the cause name or None for a clean match.
  • Handoff.run gains trigger_timeout (rename of timeout) plus the two new kwargs, which forward into wait_for_completion on match.
  • HandoffResult.timeout_cause: Literal["access", "completion"] | None tells callers which timer fired (or that detection matched cleanly).
  • Late operator clicks are rejected at the WS upgrade with code 1008 reason access_timeout_expired; the wrapper renders the expired card on that close code.
  • Wrapper toolbar gains a completion_deadline_ms-driven countdown pill, switching to a warning style under 60s. The deadline is anchored once on first connect so the countdown is reconnect-safe.

Defaults

  • ServerConfig.access_timeout = 600.0 (10 min)
  • ServerConfig.completion_timeout = 1800.0 (30 min)
  • Handoff.run(trigger_timeout=30.0) unchanged from the prior timeout=.

Breaking

  • ServerConfig.session_timeout removed.
  • ServerConfig.completion_timeout (previously a deprecated alias) reclaimed with new semantics — per-call work budget, not total lifetime.
  • Handoff.run(timeout=...) removed; pass trigger_timeout=....
  • Passing any removed kwarg raises TypeError at construction.

synacktraa and others added 9 commits June 30, 2026 15:51
The single session_timeout knob conflated orphan defense (operator
never connects) with the human's work budget (operator connected
but is taking too long). With one number both jobs share, late
clicks silently shrink the work window — invisible to the
operator.

Replace with two timers, each at both ServerConfig (house default)
and per-call (override) layers:

  access_timeout     — start of wait → first WS connect.
                       Defends against orphaned sessions.
  completion_timeout — first WS connect → detection match.
                       Bounds work time.

None per-call inherits from ServerConfig; None on ServerConfig
means truly no timeout. Defaults: 600s access, 1800s completion.

In handoff.py, the single asyncio.wait_for becomes a three-way
race (_await_timeout_cause) between detection-match, access
deadline, and completion deadline. Returns the cause name
("access" / "completion") or None for a clean match — fed
directly into HandoffResult.timeout_cause for diagnostics.

Lazy listener install (substrate-URL leak defense) moves to a
side task armed on first connect; the race itself starts
immediately so access_timeout begins counting from the call,
not from connect.

Handoff.run(timeout=...) renamed to trigger_timeout=...; the new
access_timeout / completion_timeout kwargs are forwarded into
wait_for_completion on match.

WS upgrade rejects late operator clicks with 1008
access_timeout_expired once the access timer fires; the wrapper
handles that close code → expired card.

session_state WS message gains completion_deadline_ms (epoch
millis) anchored on first accept, reused across reconnects so
the wrapper countdown is reconnect-safe.

Breaking: ServerConfig.session_timeout and the deprecated
ServerConfig.completion_timeout alias are removed. Anyone still
passing them gets TypeError from the dataclass at construction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renders the remaining completion_timeout in the toolbar against
the absolute completion_deadline_ms from the session_state WS
message. Switches to a warning style under 60s; hides when no
deadline is set; reconnect-safe because the deadline is absolute.

Handles the new WS close code (1008 access_timeout_expired) by
rendering the expired card on load — the operator clicked past
the access window.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Same countdown + expired-card handling as the screencast
template, ported to the passthrough proxy wrapper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update the How-it-works and ServerConfig snippets to the new
access_timeout / completion_timeout names. trigger_timeout
replaces timeout on Handoff.run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…opping/local.py

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…opping/using_kernel.py

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…off/in_daytona.py

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rename timeout to trigger_timeout, and rewire the error message
to read result.timeout_cause instead of the removed
handoff.server.completion_timeout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
examples/**/temp* — local scratch test scripts that shouldn't
land in the repo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant