Skip to content

refactor(install): give both pre-agent installers one retry window - #908

Open
tend-agent wants to merge 2 commits into
fix/proxy-uv-install-retry-31298989023from
fix/share-install-retry-window-31300088979
Open

refactor(install): give both pre-agent installers one retry window#908
tend-agent wants to merge 2 commits into
fix/proxy-uv-install-retry-31298989023from
fix/share-install-retry-window-31300088979

Conversation

@tend-agent

Copy link
Copy Markdown
Collaborator

Stacked on #907 — based on its branch, so the diff here is only the extraction. GitHub retargets this as the stack merges.

Problem

Raised in the review of #907: after #906 and #907 land, the same retry policy exists in two files with no shared source of truth. install-claude-binary.sh and install-proxy-uv.sh each carry ATTEMPTS=5, the same 5 * 2 ** (i - 1) + RANDOM % 10 backoff, the same timeout 60, and the same set -o pipefail-inside-bash -c requirement. The next adjustment to the window has to be found and applied twice, and the two rationale comments already differ in wording while describing one policy — which is how the windows drift apart.

The repo already has the mechanism: shared/steps/lib/run-issue.sh is sourced by both rate-limit-preflight.sh and report-failure.sh.

Change

shared/steps/lib/retry.sh holds retry_install LABEL COMMAND — the loop, the backoff, the jitter, and the set -o pipefail that both callers' curl | sh shape depends on. Each caller keeps its own one-line rationale for why it fetches what it fetches; the window's rationale lives once, in the lib.

The review flagged the sandbox boundary as the unverified part, and it is real: install-claude-binary.sh runs its loop inside sudo -u "$SANDBOX" env … bash <<'EOF', and nothing grants the sandbox UID read access to the action's own checkout — setup-sandbox.sh grants traversal (o+x) on the workspace's ancestors only, and the action lives elsewhere under _actions/. So the lib is not sourced by path from inside the heredoc. It is concatenated onto the front of the script the sandbox bash reads from stdin:

cat "${SCRIPT_DIR}/lib/retry.sh" - <<'EOF' \
  | sudo -u "$SANDBOX" env HOME="$AGENT_HOME" … bash
set -euo pipefail
retry_install "claude $CLAUDE_VERSION" \
  "curl -fsSL https://claude.ai/install.sh | bash -s -- '$CLAUDE_VERSION'"
EOF

The file is read on the runner side of the privilege drop, so sandbox read access never enters into it, and the privilege drop itself is unchanged — same sudo -u, same env, same pinned XDG dirs. install-proxy-uv.sh runs as the runner and sources the lib normally.

Behaviour is unchanged on both paths: five attempts, 5/10/20/40s plus 0–9s jitter, ::error::failed to install <label> after 5 attempts on exhaustion, one fetch and no sleep on the happy path.

Tests

No new tests — the eight added by #906 and #907 already pin the contract behaviourally on both callers (attempt count, backoff floors, the exhaustion message, the no-sleep happy path), and they pass unmodified against the extracted lib. That is the point of the pair: the shared window is now asserted twice, from opposite sides of the sudo boundary.

Full generator suite: 387 passed. shellcheck -S warning clean on both scripts and the lib.

If you'd rather not review three

The end state is what matters, not the split — squashing this into #907 loses nothing.

The retry loop #906 widened on install-claude-binary.sh and #907 copied to
install-proxy-uv.sh is the same policy in two places: same attempt count,
same backoff formula, same jitter, same pipefail requirement. Extract it to
shared/steps/lib/retry.sh, the pattern lib/run-issue.sh already establishes
for report-failure.sh and rate-limit-preflight.sh.

The claude copy runs as the sandbox user, so it can't source the lib by
path — nothing grants that UID read access to the action's own checkout.
Concatenating the lib onto the front of the heredoc keeps the file read on
the runner side of the privilege drop.

@tend-agent tend-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review — the extraction holds up. I checked the sandbox path locally rather than trusting the shape: exhaustion still exits 1 through the new pipeline (the outer set -eo pipefail carries the sandbox bash's status), and the eight tests from #906/#907 pass unmodified against the lib. Two things outside the diff.

A third pre-agent installer isn't on the shared window. codex/action.yaml's Install Codex CLI step runs npm install -g "@openai/codex@..." bare, and that action's Report failure is gated on steps.codex.outcome == 'failure' — the exact shape retry.sh's header describes. An npm registry blip loses a codex run the way a CDN blip lost a claude one. It can't just call retry_install as-is: it's an inline run: block, so it needs extracting into shared/steps/ first. Follow-up rather than something to fold in here, but worth naming while the reasoning is fresh — I can open it.

The lib's rationale has a known expiry. retry.sh explains the wide window with "loses the run with no outage row (Report failure is gated on the agent step's own outcome)". #857 rewrites that gate to failure() && steps.security.outcome != 'failure', which does file a row for a pre-agent failure. Whichever of the two lands second should drop that parenthetical — the window's real justification is the lost run, not the missing row. The extraction at least makes it one copy to fix instead of two.

@tend-agent

Copy link
Copy Markdown
Collaborator Author

Both points actioned.

The third installer — filed as #909 rather than folded in, for the reason the review gives: it's an inline run: block, so it needs extracting into shared/steps/ before it can call retry_install at all. Writing it up turned up one thing worth settling before anyone wires it: retry_install hardcodes timeout 60, which fits a curl | sh fetching one installer script but not necessarily a global npm install resolving a dependency tree. I haven't measured @openai/codex on a cold runner, so the issue asks for that measurement first — dropping it under the current lib unmeasured would convert slow installs into failures, which is the opposite of the point.

The expiring rationale — fixed here in 1b63e55 instead of leaving it for whichever of this and #857 lands second. The parenthetical is gone; the header now justifies the window by the lost run and says so explicitly:

The lost run is what justifies a window this wide, independent of how the failure is reported afterwards.

That last clause is deliberate — it's what stops a future reader re-deriving the parenthetical from whatever the gate happens to say then. Two test docstrings from #906/#907 carried the same claim (leaves no outage row, loses the run with no outage row); both are updated the same way, so the expiry is gone from all three copies rather than just the lib. Now neither PR has to know about the other.

387 passed, shellcheck -S warning clean on the lib and both callers.

max-sixty pushed a commit that referenced this pull request Aug 11, 2026
…will convert (#914)

## Problem

Filing an issue in this repo is not a note to a maintainer —
`tend-triage` fires on `issues`. Twice in 66 minutes this morning a run
filed an issue describing a fix it had already fully scoped, and triage
then re-derived that fix from the issue body and opened the PR:

| Issue filed | Triage boots | Turns / cost | PR opened |
|---|---|---|---|
| [#909](#909) 07:18:53Z |
[31300864191](https://github.com/max-sixty/tend/actions/runs/31300864191)
07:18:56Z | 64 / $4.08 |
[#910](#910) `fix/issue-909`
07:25:17Z |
| [#911](#911) 07:39:37Z |
[31301666194](https://github.com/max-sixty/tend/actions/runs/31301666194)
07:39:40Z | 58 / $4.20 |
[#912](#912) `fix/issue-911`
07:49:37Z |

Three seconds from issue to triage boot in both cases. Neither issue was
under-specified: #909 named `codex/action.yaml:120-124` and proposed
extracting to `shared/steps/install-codex-cli.sh` sourcing
`lib/retry.sh`; #911 named `proxy/setup-sandbox.sh:319` and proposed the
same treatment. Both PRs implemented exactly the proposal. Triage's own
session log confirms the conversion — run 31300864191's artifact is
`claude-session-logs-n909`, and it ran `gh pr create --head
fix/issue-909`, classifying the reporter as "the bot's own review of
#908".

So the issue bought nothing and cost $8.28 of agent time plus two
threads. The filing run could have opened both PRs itself.

<details><summary>Wider context — the cascade this sat inside</summary>

A single transient CDN 403 lost one matrix leg of `review-reviewers` run
[31297986524](https://github.com/max-sixty/tend/actions/runs/31297986524)
at 06:03Z (`curl: (22) ... 403`, three attempts inside 15.7 s, the other
four legs installing the same version from the same runner at the same
moment succeeded). Between 06:29Z and 07:56Z that produced #906, #907,
#908, #909, #910, #911 and #912 — five stacked PRs and two issues, none
merged, across ~14 agent runs and roughly $30 at list prices.

The engineering is sound and correctly atomic; this PR does not argue
with any of it. Only the two issue hops are pure overhead, and they are
the part with a mechanical fix.

</details>

## Solution

A short section in `running-in-ci` — the skill every workflow loads —
stating that an issue in this repo is picked up by triage, so a fix you
can already scope should be opened as a PR in the same run. The escape
hatches stay: too large or ambiguous to fix, needs a maintainer
decision, or verification is out of reach from CI.

This generalizes a rule that already exists but only in two skills:
`review-runs` ("Issue (fallback): Only for problems too large or
ambiguous to fix directly") and `review-reviewers` carry it, so the runs
that have it don't do this. The runs that did — a `tend-review` session
and a `tend-mention` session — load neither.

## Gate assessment

- **Evidence level**: High — 2 occurrences this window, and the
self-amplifying-output family has been recorded in prior windows
(2026-08-04: review→fix ping-pong over five rounds; two bot PRs seven
minutes apart on one root cause).
- **Structural**: yes for the costly half. The bot's choice to file
rather than fix is stochastic, but once an issue exists the triage
conversion has no decision point — it fires on the `issues` event every
time.
- **Change type**: targeted fix (one short section), normal evidence
bar.
- **Both gates**: pass.

Found by `/tend-ci-runner:review-runs` on run
[31302531474](https://github.com/max-sixty/tend/actions/runs/31302531474).

---------

Co-authored-by: tend-agent <270458913+tend-agent@users.noreply.github.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