Skip to content

Post the relink URL in the same turn instead of asking for another trigger#153

Merged
imaustink merged 2 commits into
mainfrom
fix/relink-prompt-in-same-turn
Jul 25, 2026
Merged

Post the relink URL in the same turn instead of asking for another trigger#153
imaustink merged 2 commits into
mainfrom
fix/relink-prompt-in-same-turn

Conversation

@imaustink

Copy link
Copy Markdown
Owner

Follow-up to #152, tested live on #146.

What #152 got wrong

The recovery fired correctly — verified in Redis afterwards: claudeAuthLogin:<subject> gone, claudeAuth:<subject> (the setup-token) intact, a write-back grant minted. But the only thing posted to #146 was:

⚠️ Your linked Claude account's credential looks expired or invalid, so this request couldn't complete. Re-apply the label to try again and I'll post a link for relinking it.

One label application, zero actionable output. The human pays a round trip to receive a link that could have been in the first reply, and on a label-driven run "try again" is itself a second manual step. Invalidate-then-ask-for-a-retry is a dead end dressed up as a recovery.

Change

handleAgentTurnFailure now starts the replacement link immediately after invalidating — in that order, since the reverse would delete the link it just created — and returns its URL. It uses the same pendingIdentityLink / identityLinkPending shape as the first-time link prompt, which arms integration-gateway's existing waitAndResume, so finishing the link re-runs the interrupted request automatically. One label, one link, no further triggers.

The "re-apply the label" / "send your request again" wording (branching on whether the turn has a live channel) survives only as the fallback for when the replacement flow itself can't be started — the credential really is gone at that point, so a retry genuinely will prompt; it just costs another trigger.

Two supporting cleanups:

  • Link-prompt markdown is built by one shared linkPromptText for both the first-time and re-link paths, so they can't drift into describing the same flow differently.
  • start is awaited in a try/catch rather than .catch() on its return value. It's only contractually a promise, and a partial IdentityLinkPort (any test double stubbing start with no return) would otherwise crash this recovery path with a TypeError — which is exactly the sort of failure you don't want on the path that handles failures.

Also carries the follow-up commit that never made it into #152 (merged before it was pushed): invalidation failures are logged rather than swallowed, and don't promise a retry that can't work.

Verification

92 graph tests pass (709 across apps + packages), typecheck clean. New cases: the relink URL arrives in the same turn with identityLinkPending set and the original request preserved for the resume; the fallback path when start fails; and the existing "invalidation itself failed" case. pty-setup-token.test.ts still can't run locally — node-pty is absent from this environment and that file is untouched.

What this does not fix

The currently stored credential is still unrecoverable — its refresh token was rotated away before any of this shipped, so the first label application after deploying this will hand you a relink URL. Complete it once; from then on each run writes its refreshed credential back (#152) and this shouldn't recur.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KkpJCAfAEi2B3hqfgJhboF

imaustink and others added 2 commits July 24, 2026 17:20
`invalidate?.().catch(() => {})` swallowed its failure, so a delete that
didn't happen was indistinguishable from one that did -- in the logs and in
the reply. The user is then told to retry, the next attempt resolves the same
dead credential, and it fails identically forever with nothing explaining why.

Verified against the live cluster that the happy path does work: after the
recovery fired, claudeAuthLogin:<subject> was gone while
claudeAuth:<subject> (the setup-token) survived -- the pre-fix code would have
deleted exactly the wrong one of those two. This covers the case where it
doesn't.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkpJCAfAEi2B3hqfgJhboF
…igger

Tested live on #146: the recovery invalidated the stale credential and posted
"re-apply the label to try again and I'll post a link for relinking it" -- and
nothing else. That is a dead end dressed up as a recovery. The human pays a
round trip to receive a link that could have been in the first reply, and on a
label-driven run the retry instruction is itself a second manual step, so a
single label application produces no actionable output at all.

handleAgentTurnFailure now starts the replacement link right after
invalidating (in that order -- the reverse would delete the new link too) and
returns its URL, using the same pendingIdentityLink/identityLinkPending shape
as the first-time link prompt. That arms integration-gateway's existing
waitAndResume, so completing the link re-runs the interrupted request
automatically: one label, one link, no further triggers. The "re-apply the
label" / "send your request again" wording survives only as the fallback for
when the replacement flow itself fails to start.

The link-prompt markdown is now built by one shared `linkPromptText` for both
the first-time and re-link paths, so the two can't drift into describing the
same flow differently. `start` is awaited in a try/catch rather than via
`.catch()` on its return value: it is only contractually a promise, and a
partial IdentityLinkPort would otherwise crash this recovery path outright.

Also includes the un-merged follow-up from #152: invalidation failures are
logged instead of swallowed, and no longer promise a retry that cannot work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkpJCAfAEi2B3hqfgJhboF
@imaustink
imaustink merged commit fd5fa19 into main Jul 25, 2026
@imaustink
imaustink deleted the fix/relink-prompt-in-same-turn branch July 25, 2026 00:37
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