Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Capacitor.Cli.Core/AgentRegisteredResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Capacitor.Cli.Core;

/// <summary>The server's answer to <c>AgentRegisteredAcked</c>. Not registered means the server holds no live
/// entry under the agent id. <see cref="RefusedReason"/> is an open set; a value this daemon does not know
/// is still a refusal.</summary>
public sealed record AgentRegisteredResponse(bool Registered, string? RefusedReason = null) {
public static readonly AgentRegisteredResponse Accepted = new(true);
}
1 change: 1 addition & 0 deletions src/Capacitor.Cli.Core/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,7 @@ public sealed record CurationApplyResponse {
[JsonSerializable(typeof(AgentPidRecord))]
[JsonSerializable(typeof(PidIdentityKind))]
[JsonSerializable(typeof(AgentRegistered))]
[JsonSerializable(typeof(AgentRegisteredResponse))]
[JsonSerializable(typeof(AgentStatusChanged))]
[JsonSerializable(typeof(AgentUnregistered))]
[JsonSerializable(typeof(LaunchFailed))]
Expand Down
28 changes: 19 additions & 9 deletions src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.Resume.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,14 +474,18 @@ internal sealed record ResumeLaunch(ResumeLedgerEntry Entry, WorktreeInfo Worktr
/// <summary>Ends the runtime start with the attempt: a stop during the handshake must not wait for the vendor.</summary>
public CancellationToken Token { get; init; }

/// <summary>The server's reason, empty when it gave none, once it refused the registration.</summary>
public string? RefusedRegistration { get; set; }

public WorkLocation Work => Entry.Launch.Work;
public bool IsPrivate => Entry.Launch.IsPrivate;
public string Origin => Entry.Launch.Origin;
}

const string NativeResumeUnsupported = "native_resume_unsupported";
const string StoppedDuringResume = "stopped";
const string TranscriptRefusedReason = "acp_binding_unavailable";
const string NativeResumeUnsupported = "native_resume_unsupported";
const string StoppedDuringResume = "stopped";
const string TranscriptRefusedReason = "acp_binding_unavailable";
const string RegistrationRefusedReason = "registration_refused";

static bool TranscriptRefused(AgentInstance agent) => agent.ReopenedTranscript == ReopenedTranscript.Refused;

Expand Down Expand Up @@ -843,7 +847,7 @@ internal async Task<ResumeOutcome> ResumeEntryAsync(ResumeLedgerEntry entry, Can
if (IsWithdrawn(entry.AgentId)) return ResumeOutcome.Stopped;

InterruptEntry(entry.AgentId, LastErrorOf(reason, exitCode));
if (!entry.Launch.IsPrivate) await ReportResumeFailedAsync(entry, reason, exitCode, ct);
if (!entry.Launch.IsPrivate && reason != RegistrationRefusedReason) await ReportResumeFailedAsync(entry, reason, exitCode, ct);

return ResumeOutcome.Interrupted;
}
Expand Down Expand Up @@ -871,6 +875,11 @@ static string LastErrorOf(string reason, int? exitCode) =>

if (resume.Agent is null && IsWithdrawn(entry.AgentId)) return (StoppedDuringResume, null);

// The server holds no live entry under this id, so the resume goes on under a new one, unless a stop
// came first: it gave up the worktree, and the teardown has removed it.
if (resume.RefusedRegistration is not null)
return resume.Agent?.StopRequested == true ? (StoppedDuringResume, null) : (RegistrationRefusedReason, null);

if (outcome.Kind == CommandOutcomeKind.LaunchRejected)
return (outcome.RejectReason == CommandRejectedReason.DaemonCapacity ? "daemon_capacity" : "launch_rejected", null);

Expand Down Expand Up @@ -947,7 +956,7 @@ internal async Task<ResumeOutcome> ContinueEntryAsync(ResumeLedgerEntry entry, s
}

if (survived && TryHandOverContinuation(entry, launch!.Entry, agent!)) {
await ReportSupersededAsync(entry, ct);
await ReportSupersededAsync(entry, reason, ct);
if (entry.SessionId is { } previous) StartContinuationTakeover(agent!, previous);

return ResumeOutcome.Continued;
Expand All @@ -967,7 +976,7 @@ internal async Task<ResumeOutcome> ContinueEntryAsync(ResumeLedgerEntry entry, s
// A stop of the successor itself ends the agent it continued too.
if (agent.StopRequested) {
DeleteLedger(entry.AgentId);
await ReportSupersededAsync(entry, ct);
await ReportSupersededAsync(entry, reason, ct);

return ResumeOutcome.Stopped;
}
Expand All @@ -990,9 +999,10 @@ bool TryHandOverContinuation(ResumeLedgerEntry entry, ResumeLedgerEntry successo
}
}

/// <summary>A private agent was never registered, so the server holds no agent to end.</summary>
Task ReportSupersededAsync(ResumeLedgerEntry entry, CancellationToken ct) =>
entry.Launch.IsPrivate
/// <summary>A private agent was never registered, and a refused one has no live entry left on the server:
/// neither has an agent to end, and a status for the refused id could overwrite the failure it recorded.</summary>
Task ReportSupersededAsync(ResumeLedgerEntry entry, string reason, CancellationToken ct) =>
entry.Launch.IsPrivate || reason == RegistrationRefusedReason
? Task.CompletedTask
: _server.ReportAgentEndedForShutdownAsync(entry.AgentId, entry.SessionId, "Completed", SupersededReason, null, ct);

Expand Down
Loading
Loading