v0.22.1.0 fix: configurable prompt-send wait for slow remote agents - #73
Open
avinashjoshi wants to merge 7 commits into
Open
avinashjoshi wants to merge 7 commits into
avinashjoshi wants to merge 7 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix —
canopy new --on <host> --prompt "..."no longer times out waiting for Claude:internal/workspace/initprompt.go'sphaseBudgetconst). Claude reliably takes longer than that to start on a fresh remote host, socanopy new --on tower --prompt "..."would create the workspace fine but fail to deliver the prompt withPhase 1 timeout, leaving you to attach and retype it by hand — an already-tracked bug (TODOS.md, filed 2026-05-12).promptPhaseBudget(): an explicitCANOPY_PROMPT_PHASE_BUDGEToverride, falling through to 15s whenCANOPY_REMOTE_DISPATCHis set (exported unconditionally bybuildRemoteScripton every--on <host>dispatch, since the wholecanopy newprocess — including this wait's tmux calls — runs on the remote host itself), falling through to the original 5s local default otherwise.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:
awaitClaudeReady(Phase 1: race trust-dialog-or-ready; Phase 2: wait for ready post-dismiss) are now one sharedawaitPaneOutputprimitive — a generic poll-until-match-or-timeout call, in the spirit of herdr's socket-APIPaneWaitForOutputParams. Canopy doesn't need herdr's client/server split to get the same benefit, since--ondispatch already runs the whole process where the pane lives.Bug found and fixed mid-ship, by
/document-release's own doc-accuracy audit:buildRemoteScriptonly exportedCANOPY_REMOTE_DISPATCH, neverCANOPY_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 existingCANOPY_REMOTE_DISPATCHexport. Caught before merge, not shipped broken.Test Coverage
19 new/modified tests across
internal/workspace/initprompt_test.goandcmd/canopy/new_test.go. AI-assessed coverage on the coreinitprompt.go/new.godiff 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, bothawaitClaudeReadyphase-timeout paths propagating correctly, transient capture-pane failures mid-poll (pane dies, loop keeps retrying rather than panicking), and thebuildRemoteScriptenv-export assertions (including the mid-shipCANOPY_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-dismissSendKeyNamefailure 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 mockstmux.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
exportline).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:
promptPhaseBudgetdidn't guard/test zero-or-negative override durations (ParseDurationaccepts"0s"without erroring) — added ad > 0guard + tests.awaitPaneOutput's transient capture-failure tolerance (pane dies mid-poll) — added.CANOPY_REMOTE_DISPATCHstring literal across two files — extracted to a sharedworkspace.EnvRemoteDispatchconstant.Adversarial review (Claude subagent; Codex CLI unavailable in this sandbox —
mise/aquainstall failure, no working fallback binary): Recommendation was "ship as-is." One cheap fixable finding applied: malformed/zero/negativeCANOPY_PROMPT_PHASE_BUDGETvalues 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_DISPATCHis 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).awaitPaneOutputextraction with the exact planned signature, Phase 1/Phase 2 both routed through it,promptPhaseBudget()'s 3-tier resolution,CANOPY_REMOTE_DISPATCHexport, TUI's in-process flow correctly left untouched, TODOS.md entry resolved, all planned tests present.TestAwaitClaudeReady_ImmediatelyReady/TrustDialogThenReady) beyond the plan's minimum test list — additive coverage, not a deviation.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
--promptexample now documents the 15s remote / 5s local default wait and theCANOPY_PROMPT_PHASE_BUDGEToverride (including that it's forwarded to--on <host>dispatch).TODOS
✅ SHIPPED v0.22.1.0.Test plan
go build ./...cleango vet ./...cleango test ./...— full suite greengo test -race ./...on both the local toolchain (go1.27.1) and CI's exact toolchain (go1.26.2, viaGOTOOLCHAIN=go1.26.2) — green on both, no races-count=3) to rule out timing flakiness in the real-tmux integration tests🤖 Generated with Claude Code
https://claude.ai/code/session_01L9xb577XwZaxM2uSm8xTgQ