Skip to content

v0.22.1.0 fix: configurable prompt-send wait for slow remote agents - #73

Open
avinashjoshi wants to merge 7 commits into
mainfrom
configurable-prompt-wait
Open

avinashjoshi wants to merge 7 commits into
mainfrom
configurable-prompt-wait

Conversation

@avinashjoshi

Copy link
Copy Markdown
Owner

Summary

Fix — canopy new --on <host> --prompt "..." no longer times out waiting for Claude:

  • The prompt-send wait for Claude to become ready had a hardcoded 5-second budget (internal/workspace/initprompt.go's phaseBudget const). Claude reliably takes longer than that to start on a fresh remote host, so canopy new --on tower --prompt "..." would create the workspace fine but fail to deliver the prompt with Phase 1 timeout, leaving you to attach and retype it by hand — an already-tracked bug (TODOS.md, filed 2026-05-12).
  • Replaced with promptPhaseBudget(): an explicit CANOPY_PROMPT_PHASE_BUDGET override, falling through to 15s when CANOPY_REMOTE_DISPATCH is set (exported unconditionally by buildRemoteScript on every --on <host> dispatch, since the whole canopy new process — including this wait's tmux calls — runs on the remote host itself), falling through to the original 5s local default otherwise.
  • Malformed or non-positive override values (CANOPY_PROMPT_PHASE_BUDGET=0s/-1s/a typo) fall through to the next tier with a warning logged, rather than silently being honored (which would skip the poll loop entirely) or erroring.

Refactor — inspired by comparing canopy's architecture to herdr's:

  • The two near-duplicate hand-rolled poll loops in awaitClaudeReady (Phase 1: race trust-dialog-or-ready; Phase 2: wait for ready post-dismiss) are now one shared awaitPaneOutput primitive — a generic poll-until-match-or-timeout call, in the spirit of herdr's socket-API PaneWaitForOutputParams. Canopy doesn't need herdr's client/server split to get the same benefit, since --on dispatch already runs the whole process where the pane lives.
  • Incidental fix: Phase 2 now gets Phase 1's pre-existing "one more check right at the deadline" flake guard, which it previously lacked.

Bug found and fixed mid-ship, by /document-release's own doc-accuracy audit:

  • The override was initially local-only: SSH doesn't forward client env vars on its own, and buildRemoteScript only exported CANOPY_REMOTE_DISPATCH, never CANOPY_PROMPT_PHASE_BUDGET. Setting the override before a --on <host> dispatch would silently do nothing — exactly the escape hatch a user reaches for when the 15s remote default still isn't enough. Fixed by forwarding a locally-set override into the remote script's environment (shell-quoted), mirroring the existing CANOPY_REMOTE_DISPATCH export. Caught before merge, not shipped broken.

Test Coverage

19 new/modified tests across internal/workspace/initprompt_test.go and cmd/canopy/new_test.go. AI-assessed coverage on the core initprompt.go/new.go diff started at 63% (10/16 branches; target 80%, minimum 60%), then closed to ~94% (15/16) by adding targeted tests for: context-already-done-at-loop-start, the grace-period-catches-a-late-match success path, both awaitClaudeReady phase-timeout paths propagating correctly, transient capture-pane failures mid-poll (pane dies, loop keeps retrying rather than panicking), and the buildRemoteScript env-export assertions (including the mid-ship CANOPY_PROMPT_PHASE_BUDGET-forwarding fix, with a dedicated shell-quoting test for values containing metacharacters).

One gap left deliberately open and documented, not silently skipped: awaitClaudeReady's trust-dismiss SendKeyName failure branch requires killing a real tmux pane in the sub-millisecond window between "capture matched trust text" and "SendKeyName call fires" — an unwinnable race with no synchronization point in the code under test, and this project has no mocking convention to fake it cleanly (confirmed no existing test file in the repo mocks tmux.Client).

Pre-Landing Review

Checklist pass: no findings (Go CLI backend diff — most checklist categories are SQL/Rails/Node-oriented and don't apply; no SQL, no HTML rendering, no shell injection surface in the one new static export line).

Specialists dispatched (diff-scope detector reported no backend/frontend/API/migration signals for this Go diff, so only the always-on pair ran): Testing (3 findings) + Maintainability (2 findings) — all 5 informational, all auto-fixed:

  • Missing test: malformed-override-falls-through-to-remote-dispatch-tier (not past it) — added.
  • Missing test: promptPhaseBudget didn't guard/test zero-or-negative override durations (ParseDuration accepts "0s" without erroring) — added a d > 0 guard + tests.
  • Missing test: awaitPaneOutput's transient capture-failure tolerance (pane dies mid-poll) — added.
  • Duplicated CANOPY_REMOTE_DISPATCH string literal across two files — extracted to a shared workspace.EnvRemoteDispatch constant.
  • Bare numeric literals for the 5s/15s budget tiers — extracted to named constants.

Adversarial review (Claude subagent; Codex CLI unavailable in this sandbox — mise/aqua install failure, no working fallback binary): Recommendation was "ship as-is." One cheap fixable finding applied: malformed/zero/negative CANOPY_PROMPT_PHASE_BUDGET values now log a warning instead of silently falling through with no diagnostic trail. Two investigate-only findings accepted as low-severity, self-inflicted config footguns (no ceiling on an absurd override value; CANOPY_REMOTE_DISPATCH is a bare env var with no collision guard, worst case only ever widens a timeout) — not fixed, consistent with the "ship as-is" recommendation.

Design Review

No frontend files changed — design review skipped.

Eval Results

No prompt-related files changed — evals skipped.

Plan Completion

Plan: linear-hopping-thunder.md (this branch's own plan, approved via plan mode).

  • 16 DONE: awaitPaneOutput extraction with the exact planned signature, Phase 1/Phase 2 both routed through it, promptPhaseBudget()'s 3-tier resolution, CANOPY_REMOTE_DISPATCH export, TUI's in-process flow correctly left untouched, TODOS.md entry resolved, all planned tests present.
  • 1 CHANGED: two extra integration tests (TestAwaitClaudeReady_ImmediatelyReady/TrustDialogThenReady) beyond the plan's minimum test list — additive coverage, not a deviation.
  • 0 NOT DONE, 0 unverifiable.
  • (The mid-ship CANOPY_PROMPT_PHASE_BUDGET-forwarding fix and its doc corrections were not part of the original plan — they're a bug the ship process itself caught and fixed before merge.)

Documentation

  • docs/remote-workspaces.md — the --prompt example now documents the 15s remote / 5s local default wait and the CANOPY_PROMPT_PHASE_BUDGET override (including that it's forwarded to --on <host> dispatch).
  • CHANGELOG.md / TODOS.md — describe the shipped behavior accurately (initially overclaimed remote support for the override before the forwarding fix landed mid-ship; corrected in the same PR).
  • README.md / CLAUDE.md / CONTRIBUTING.md / docs/architecture.md — reviewed, no changes needed.

TODOS

  • Marked complete: "Configurable prompt-send timeout for slow remote agents" (P2, added 2026-05-12) → ✅ SHIPPED v0.22.1.0.
  • No new open items — the one TODO briefly filed mid-ship for the remote-forwarding gap was resolved in this same PR rather than deferred, so it was removed rather than left open.

Test plan

  • go build ./... clean
  • go vet ./... clean
  • go test ./... — full suite green
  • go test -race ./... on both the local toolchain (go1.27.1) and CI's exact toolchain (go1.26.2, via GOTOOLCHAIN=go1.26.2) — green on both, no races
  • All new/modified tests independently verified passing across multiple repeated runs (-count=3) to rule out timing flakiness in the real-tmux integration tests

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ

avinashjoshi and others added 7 commits September 3, 2026 12:36
canopy new --on tower --prompt "..." reliably timed out with "Phase 1
timeout" on remote hosts, since the wait for Claude to become ready
before sending the prompt had a hardcoded 5-second budget and Claude
often takes longer than that to start remotely. The workspace was
created fine; only prompt delivery failed.

promptPhaseBudget() replaces the const: an explicit
CANOPY_PROMPT_PHASE_BUDGET override, falling through to 15s when
CANOPY_REMOTE_DISPATCH is set (the entire canopy new process runs on
the remote host for --on, so this poll loop's tmux calls already
execute there), falling through to the original 5s local default
otherwise. Malformed or non-positive override values fall through
with a warning logged, rather than being silently honored or erroring.

Also extracts the two near-duplicate Phase 1/Phase 2 poll loops in
awaitClaudeReady into one shared awaitPaneOutput primitive, inspired
by herdr's PaneWaitForOutputParams socket-API primitive (a generic
poll-until-match-or-timeout call) — canopy doesn't need herdr's
client/server split to get the same benefit, since --on already runs
the whole process where the pane lives. Incidental fix: Phase 2 now
gets Phase 1's existing "one more check right at the deadline"
flake guard, which it previously lacked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ
buildRemoteScript now exports workspace.EnvRemoteDispatch=1
unconditionally on every `canopy new --on <host>` dispatch, which
promptPhaseBudget (previous commit) reads to pick the longer default
prompt-send timeout on remote hosts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ
Adds a note to the --prompt example in docs/remote-workspaces.md
explaining the 15s remote / 5s local default and the
CANOPY_PROMPT_PHASE_BUDGET override introduced by the v0.22.1.0 fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ
…spatch

The doc-review pass (Claude subagent, codex CLI unavailable in this
sandbox) caught a real gap: CHANGELOG.md, TODOS.md, and the
remote-workspaces.md example I'd just added all claimed
CANOPY_PROMPT_PHASE_BUDGET overrides the remote-dispatch wait when set
on the invoking laptop. It doesn't — buildRemoteScript only forwards
CANOPY_REMOTE_DISPATCH into the remote script, never
CANOPY_PROMPT_PHASE_BUDGET, and plain ssh doesn't forward client env
vars. The override only reaches local (non---on) workspace creation
today.

Corrected the three docs to describe the actual (local-only) behavior
and filed a TODOS.md item with a fix sketch for forwarding the var to
remote dispatch, mirroring the existing EnvRemoteDispatch export.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ
The /document-release doc audit caught a real gap: SSH doesn't forward
client env vars on its own, and buildRemoteScript only exported
CANOPY_REMOTE_DISPATCH into the remote script, never
CANOPY_PROMPT_PHASE_BUDGET. Setting the override before `canopy new
--on <host> --prompt "..."` silently did nothing — the exact escape
hatch a user reaches for when the 15s remote default still isn't
enough would quietly not apply.

buildRemoteScript now forwards a locally-set CANOPY_PROMPT_PHASE_BUDGET
into the remote script's environment (shell-quoted) alongside
CANOPY_REMOTE_DISPATCH, mirroring the existing pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ
The /document-release pass (previous commits) correctly flagged that
CHANGELOG.md, TODOS.md, and docs/remote-workspaces.md all overclaimed
remote support for the override before the actual forwarding fix
landed. Now that cmd/canopy/new.go forwards it (see the preceding
commit), restore the docs to describe the working behavior instead of
the now-fixed gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ
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