feat(account-health): reconcile remote bind state and ignore stale runtime reports
Track an authRevision per local record so a fresh bind cancels in-flight probes, sync server-confirmed health back into the desktop cache via reconcileTrackedAccountRemoteState, include verified_at in the upsert signature, and drop runtime health reports older than the stored verified_at on the server side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ interface PersistedAccountHealthRecord {
|
|||||||
interface AccountHealthRecord {
|
interface AccountHealthRecord {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
|
authRevision: number;
|
||||||
authState: AccountAuthState;
|
authState: AccountAuthState;
|
||||||
probeState: AccountProbeState;
|
probeState: AccountProbeState;
|
||||||
authReason: AccountAuthReason;
|
authReason: AccountAuthReason;
|
||||||
@@ -111,6 +112,7 @@ function normalizePersisted(
|
|||||||
return {
|
return {
|
||||||
accountId: record.accountId,
|
accountId: record.accountId,
|
||||||
platform: record.platform,
|
platform: record.platform,
|
||||||
|
authRevision: 0,
|
||||||
authState: record.lastVerifiedAt ? "active" : "unknown",
|
authState: record.lastVerifiedAt ? "active" : "unknown",
|
||||||
probeState: "idle",
|
probeState: "idle",
|
||||||
authReason: record.lastVerifiedAt ? "probe_success" : null,
|
authReason: record.lastVerifiedAt ? "probe_success" : null,
|
||||||
@@ -223,6 +225,7 @@ function ensureRecord(account: PublishAccountIdentity): AccountHealthRecord {
|
|||||||
const created: AccountHealthRecord = {
|
const created: AccountHealthRecord = {
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
platform: account.platform,
|
platform: account.platform,
|
||||||
|
authRevision: 0,
|
||||||
authState: "unknown",
|
authState: "unknown",
|
||||||
probeState: "idle",
|
probeState: "idle",
|
||||||
authReason: null,
|
authReason: null,
|
||||||
@@ -360,6 +363,7 @@ async function performProbeLocked(
|
|||||||
): Promise<AccountHealthSnapshot> {
|
): Promise<AccountHealthSnapshot> {
|
||||||
const record = ensureRecord(account);
|
const record = ensureRecord(account);
|
||||||
const adapter = getPlatformAdapter(account.platform);
|
const adapter = getPlatformAdapter(account.platform);
|
||||||
|
const probeAuthRevision = record.authRevision;
|
||||||
const previousSignature = snapshotSignature(record);
|
const previousSignature = snapshotSignature(record);
|
||||||
|
|
||||||
record.probeState = "probing";
|
record.probeState = "probing";
|
||||||
@@ -369,6 +373,9 @@ async function performProbeLocked(
|
|||||||
|
|
||||||
if (options.allowSilentRefresh) {
|
if (options.allowSilentRefresh) {
|
||||||
const refreshed = await adapter.silentRefresh(account, partition).catch(() => false);
|
const refreshed = await adapter.silentRefresh(account, partition).catch(() => false);
|
||||||
|
if (record.authRevision !== probeAuthRevision) {
|
||||||
|
return toPublicSnapshot(record);
|
||||||
|
}
|
||||||
if (!refreshed) {
|
if (!refreshed) {
|
||||||
const currentSignature = snapshotSignature(record);
|
const currentSignature = snapshotSignature(record);
|
||||||
record.authReason = "silent_refresh_failed";
|
record.authReason = "silent_refresh_failed";
|
||||||
@@ -383,6 +390,10 @@ async function performProbeLocked(
|
|||||||
sessionFingerprint: null,
|
sessionFingerprint: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (record.authRevision !== probeAuthRevision) {
|
||||||
|
return toPublicSnapshot(record);
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
switch (result.verdict) {
|
switch (result.verdict) {
|
||||||
case "active":
|
case "active":
|
||||||
@@ -455,6 +466,20 @@ function enqueueProbeJob(job: AccountProbeJob): void {
|
|||||||
drainProbeQueue();
|
drainProbeQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeQueuedProbeJob(accountId: string): void {
|
||||||
|
const job = probeJobs.get(accountId);
|
||||||
|
if (!job || job.status !== "queued") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
probeJobs.delete(accountId);
|
||||||
|
for (let index = probeQueue.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (probeQueue[index] === accountId) {
|
||||||
|
probeQueue.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleProbeRetry(job: AccountProbeJob): void {
|
function scheduleProbeRetry(job: AccountProbeJob): void {
|
||||||
const retryIndex = Math.min(Math.max(job.attempt - 2, 0), ACCOUNT_PROBE_RETRY_BACKOFF_MS.length - 1);
|
const retryIndex = Math.min(Math.max(job.attempt - 2, 0), ACCOUNT_PROBE_RETRY_BACKOFF_MS.length - 1);
|
||||||
const backoff = ACCOUNT_PROBE_RETRY_BACKOFF_MS[retryIndex]
|
const backoff = ACCOUNT_PROBE_RETRY_BACKOFF_MS[retryIndex]
|
||||||
@@ -689,6 +714,7 @@ export function markTrackedAccountBound(
|
|||||||
};
|
};
|
||||||
|
|
||||||
trackedAccounts.set(account.id, account);
|
trackedAccounts.set(account.id, account);
|
||||||
|
record.authRevision += 1;
|
||||||
record.platform = account.platform;
|
record.platform = account.platform;
|
||||||
record.authState = "active";
|
record.authState = "active";
|
||||||
record.probeState = "idle";
|
record.probeState = "idle";
|
||||||
@@ -702,7 +728,66 @@ export function markTrackedAccountBound(
|
|||||||
record.suspectedExpiredAt = null;
|
record.suspectedExpiredAt = null;
|
||||||
record.confirmFailureCount = 0;
|
record.confirmFailureCount = 0;
|
||||||
|
|
||||||
return commitRecord(record, previousSignature);
|
const snapshot = commitRecord(record, previousSignature);
|
||||||
|
const queuedJob = probeJobs.get(account.id);
|
||||||
|
if (queuedJob?.status === "queued") {
|
||||||
|
removeQueuedProbeJob(account.id);
|
||||||
|
queuedJob.resolve(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reconcileTrackedAccountRemoteState(
|
||||||
|
account: PublishAccountIdentity,
|
||||||
|
input: {
|
||||||
|
health?: DesktopAccountInfo["health"] | null;
|
||||||
|
verifiedAt?: string | null;
|
||||||
|
profile?: AccountHealthProfile | null;
|
||||||
|
},
|
||||||
|
): AccountHealthSnapshot | null {
|
||||||
|
if (input.health !== "live") {
|
||||||
|
return getAccountHealthSnapshot(account.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteVerifiedAt = parseVerifiedAt(input.verifiedAt);
|
||||||
|
if (!remoteVerifiedAt) {
|
||||||
|
return getAccountHealthSnapshot(account.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = ensureRecord(account);
|
||||||
|
const newestLocalStateAt = Math.max(
|
||||||
|
record.lastVerifiedAt ?? 0,
|
||||||
|
record.lastAuthFailureAt ?? 0,
|
||||||
|
record.lastProbeAt ?? 0,
|
||||||
|
);
|
||||||
|
if (remoteVerifiedAt <= newestLocalStateAt) {
|
||||||
|
return toPublicSnapshot(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousSignature = snapshotSignature(record);
|
||||||
|
record.authRevision += 1;
|
||||||
|
record.platform = account.platform;
|
||||||
|
record.authState = "active";
|
||||||
|
record.probeState = "idle";
|
||||||
|
record.authReason = "bind_success";
|
||||||
|
record.lastVerifiedAt = remoteVerifiedAt;
|
||||||
|
record.lastProbeAt = remoteVerifiedAt;
|
||||||
|
record.nextProbeAt = nextRegularProbeAt();
|
||||||
|
record.lastAuthFailureAt = null;
|
||||||
|
record.profile = input.profile ?? record.profile;
|
||||||
|
record.sessionFingerprint = input.profile?.subjectUid ?? record.sessionFingerprint;
|
||||||
|
record.suspectedExpiredAt = null;
|
||||||
|
record.confirmFailureCount = 0;
|
||||||
|
|
||||||
|
const snapshot = commitRecord(record, previousSignature);
|
||||||
|
const queuedJob = probeJobs.get(account.id);
|
||||||
|
if (queuedJob?.status === "queued") {
|
||||||
|
removeQueuedProbeJob(account.id);
|
||||||
|
queuedJob.resolve(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAccountHealthSnapshot(accountId: string): AccountHealthSnapshot | null {
|
export function getAccountHealthSnapshot(accountId: string): AccountHealthSnapshot | null {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
getProjectedAccountHealth,
|
getProjectedAccountHealth,
|
||||||
markTrackedAccountBound,
|
markTrackedAccountBound,
|
||||||
probeTrackedAccount,
|
probeTrackedAccount,
|
||||||
|
reconcileTrackedAccountRemoteState,
|
||||||
reportAccountFailure,
|
reportAccountFailure,
|
||||||
syncTrackedAccounts,
|
syncTrackedAccounts,
|
||||||
} from "./account-health";
|
} from "./account-health";
|
||||||
@@ -948,6 +949,19 @@ async function syncAccounts(source: "startup" | "manual"): Promise<void> {
|
|||||||
const syncedAccounts: Array<DesktopAccountInfo | null> = accounts.map((account) => {
|
const syncedAccounts: Array<DesktopAccountInfo | null> = accounts.map((account) => {
|
||||||
const normalizedPlatformUid = normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid;
|
const normalizedPlatformUid = normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid;
|
||||||
const isAIPlatform = isAIPlatformId(account.platform);
|
const isAIPlatform = isAIPlatformId(account.platform);
|
||||||
|
const accountIdentity = accountIdentityFromDesktopAccount({
|
||||||
|
...account,
|
||||||
|
platform_uid: normalizedPlatformUid,
|
||||||
|
});
|
||||||
|
reconcileTrackedAccountRemoteState(accountIdentity, {
|
||||||
|
health: account.health,
|
||||||
|
verifiedAt: account.verified_at,
|
||||||
|
profile: {
|
||||||
|
subjectUid: normalizedPlatformUid,
|
||||||
|
displayName: account.display_name,
|
||||||
|
avatarUrl: account.avatar_url,
|
||||||
|
},
|
||||||
|
});
|
||||||
const projected = getProjectedAccountHealth({
|
const projected = getProjectedAccountHealth({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
platform: account.platform,
|
platform: account.platform,
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ function accountUpsertSignature(payload: UpsertDesktopAccountRequest): string {
|
|||||||
display_name: payload.display_name.trim(),
|
display_name: payload.display_name.trim(),
|
||||||
avatar_url: payload.avatar_url ?? null,
|
avatar_url: payload.avatar_url ?? null,
|
||||||
health: payload.health,
|
health: payload.health,
|
||||||
|
verified_at: payload.verified_at ?? null,
|
||||||
tags: payload.tags ?? [],
|
tags: payload.tags ?? [],
|
||||||
if_sync_version: payload.if_sync_version ?? null,
|
if_sync_version: payload.if_sync_version ?? null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -504,6 +504,9 @@ func (s *DesktopAccountService) applyRuntimeHealth(ctx context.Context, workspac
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !shouldApplyDesktopAccountRuntimeHealth(items[index], report) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
health := effectiveDesktopAccountRuntimeHealth(report)
|
health := effectiveDesktopAccountRuntimeHealth(report)
|
||||||
authState := report.AuthState
|
authState := report.AuthState
|
||||||
@@ -523,6 +526,13 @@ func (s *DesktopAccountService) applyRuntimeHealth(ctx context.Context, workspac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldApplyDesktopAccountRuntimeHealth(item DesktopAccountView, report bufferedDesktopAccountHealthReport) bool {
|
||||||
|
if item.VerifiedAt == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !report.CheckedAt.Before(*item.VerifiedAt)
|
||||||
|
}
|
||||||
|
|
||||||
func effectiveDesktopAccountRuntimeHealth(report bufferedDesktopAccountHealthReport) string {
|
func effectiveDesktopAccountRuntimeHealth(report bufferedDesktopAccountHealthReport) string {
|
||||||
authState := strings.TrimSpace(report.AuthState)
|
authState := strings.TrimSpace(report.AuthState)
|
||||||
probeState := strings.TrimSpace(report.ProbeState)
|
probeState := strings.TrimSpace(report.ProbeState)
|
||||||
|
|||||||
@@ -117,3 +117,43 @@ func TestBuildDesktopAccountView_FallsBackToStoredClientWhenNoPresence(t *testin
|
|||||||
}
|
}
|
||||||
assert.Equal(t, &lastSeen, view.ClientLastSeenAt)
|
assert.Equal(t, &lastSeen, view.ClientLastSeenAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldApplyDesktopAccountRuntimeHealth_IgnoresReportOlderThanVerifiedAt(t *testing.T) {
|
||||||
|
verifiedAt := time.Date(2026, 4, 30, 9, 30, 0, 0, time.UTC)
|
||||||
|
reportCheckedAt := verifiedAt.Add(-time.Minute)
|
||||||
|
|
||||||
|
shouldApply := shouldApplyDesktopAccountRuntimeHealth(
|
||||||
|
DesktopAccountView{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Health: "live",
|
||||||
|
VerifiedAt: &verifiedAt,
|
||||||
|
},
|
||||||
|
bufferedDesktopAccountHealthReport{
|
||||||
|
Health: "expired",
|
||||||
|
AuthState: "expired",
|
||||||
|
CheckedAt: reportCheckedAt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.False(t, shouldApply)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldApplyDesktopAccountRuntimeHealth_AppliesReportAfterVerifiedAt(t *testing.T) {
|
||||||
|
verifiedAt := time.Date(2026, 4, 30, 9, 30, 0, 0, time.UTC)
|
||||||
|
reportCheckedAt := verifiedAt.Add(time.Minute)
|
||||||
|
|
||||||
|
shouldApply := shouldApplyDesktopAccountRuntimeHealth(
|
||||||
|
DesktopAccountView{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Health: "live",
|
||||||
|
VerifiedAt: &verifiedAt,
|
||||||
|
},
|
||||||
|
bufferedDesktopAccountHealthReport{
|
||||||
|
Health: "expired",
|
||||||
|
AuthState: "expired",
|
||||||
|
CheckedAt: reportCheckedAt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, shouldApply)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user