forked from admin/VolcanicAgenticCore
fix(claude-code): a late expiry after a completed login replays without a second login
This commit is contained in:
@@ -87,17 +87,24 @@ public sealed class ClaudeCodeApi(SessionRegistry registry, ClaudeCodeApiOptions
|
||||
try
|
||||
{
|
||||
var loginUi = Options.LoginUi;
|
||||
var coordinator = Options.Coordinator ?? ClaudeLoginCoordinator.Shared;
|
||||
var generation = coordinator.Generation;
|
||||
var expired = await RunAttemptAsync(model, context, streamOptions, writer, deferLoginFailure: loginUi is not null, ct).ConfigureAwait(false);
|
||||
if (expired is null || loginUi is null) return;
|
||||
|
||||
// The session expired before anything was published: pause, let the owner log in, then replay exactly once.
|
||||
// A login that succeeded after this attempt started makes the expiry stale: replay without another flow.
|
||||
if (coordinator.Generation != generation)
|
||||
{
|
||||
await RunAttemptAsync(model, context, streamOptions, writer, deferLoginFailure: false, ct).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var builder = new AssistantMessageBuilder(model);
|
||||
Notify(streamOptions.SessionId, "Claude Code session expired; waiting for the bot owner to sign in…", NotifyKind.Warning);
|
||||
LoginOutcome outcome;
|
||||
try
|
||||
{
|
||||
outcome = await (Options.Coordinator ?? ClaudeLoginCoordinator.Shared)
|
||||
.EnsureLoggedInAsync(flowCt => RunLoginFlowAsync(loginUi, flowCt), ct).ConfigureAwait(false);
|
||||
outcome = await coordinator.EnsureLoggedInAsync(flowCt => RunLoginFlowAsync(loginUi, flowCt), ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,9 @@ namespace Volcanic.AgenticCore.Extensions.ClaudeCode;
|
||||
/// <summary>
|
||||
/// Keeps Claude Code logins single-flight: every session shares one <c>claude</c> configuration, so concurrent expired
|
||||
/// requests wait on the same flow. The flow runs detached from any caller's token; a caller's cancellation only stops
|
||||
/// its own wait. The slot clears when the flow completes, so a later expiry starts a fresh one.
|
||||
/// its own wait. The slot clears when the flow completes, so a later expiry starts a fresh one. Each successful flow
|
||||
/// advances <see cref="Generation"/>, so a request whose attempt started before a login finished can tell that its
|
||||
/// late expiry is stale and replay without starting another flow.
|
||||
/// </summary>
|
||||
public sealed class ClaudeLoginCoordinator
|
||||
{
|
||||
@@ -12,6 +14,10 @@ public sealed class ClaudeLoginCoordinator
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
private Task<LoginOutcome>? _current;
|
||||
private long _generation;
|
||||
|
||||
/// <summary>How many flows have succeeded so far; read it before an attempt, compare after a login-required failure.</summary>
|
||||
public long Generation => Interlocked.Read(ref _generation);
|
||||
|
||||
/// <summary>Joins the in-flight login, or starts one. <paramref name="runFlow"/> only ever runs for the caller that starts it.</summary>
|
||||
public Task<LoginOutcome> EnsureLoggedInAsync(Func<CancellationToken, Task<LoginOutcome>> runFlow, CancellationToken cancellationToken)
|
||||
@@ -33,17 +39,21 @@ public sealed class ClaudeLoginCoordinator
|
||||
// overwrite it with the now-finished task. The slot would then stay stuck on a stale result forever, since
|
||||
// nothing else ever clears it. Task.Yield() guarantees the finally always runs on a later, separate turn.
|
||||
await Task.Yield();
|
||||
LoginOutcome outcome;
|
||||
try
|
||||
{
|
||||
return await runFlow(CancellationToken.None).ConfigureAwait(false);
|
||||
outcome = await runFlow(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return LoginOutcome.Failed(ex.Message);
|
||||
outcome = LoginOutcome.Failed(ex.Message);
|
||||
}
|
||||
finally
|
||||
lock (_lock)
|
||||
{
|
||||
lock (_lock) _current = null;
|
||||
// Advanced before the slot clears: a caller that finds the slot empty already sees the new generation.
|
||||
if (outcome.Success) Interlocked.Increment(ref _generation);
|
||||
_current = null;
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user