fix(agent-help): isolate trail refresh backoff · Entire

fix(agent-help): isolate trail refresh backoff

789278f→main·

dipree·2d ago·4 files·+59 added/-18 removed

Sessions

01KXJZ6CA411K8PPS44YE4J2NPView transcript

[?
Fix Agent Help Trail AvailabilityPi·GPT-5.6-sol·1 step](/content/gh/entireio/cli/session/019f65cc-0712-7af1-9340-2ded310ed0f4#timeline-01KXJZ6CA411K8PPS44YE4J2NP/index.html)

Changes

4

100 unmodified lines

101
102
103
104
105
106
107
108
109
110
108
109
110
111
111
112
113
114
115
116
117

100 unmodified lines

if scope.AuthKey == "" {
        return repoLine, false
    }
    if recentAgentHelpTrailsRefreshFailure(ctx, scope, now) {
        return repoLine, false
    }

refreshCtx, cancel := context.WithTimeout(ctx, trailEnablementRefreshTimeout)
    defer cancel()
    if err := refresh(refreshCtx, scope); err != nil {
        // A short negative cache keeps an offline authenticated user from paying
        // this timeout on every agent-help invocation while still retrying much
        // sooner than a definitive disabled result.
        _ = cacheTrailsEnablementRefreshFailure(ctx, scope, time.Now())
        // A separate short backoff keeps an offline authenticated user from paying
        // this timeout on every agent-help invocation. It must not alter the shared
        // enablement decision, which SessionStart uses for context injection.
        _ = saveAgentHelpTrailsRefreshFailure(ctx, scope, time.Now())
        return repoLine, false
    }
    return repoLine, cachedTrailsEnablementForScope(ctx, scope, time.Now()) == trailEnablementCacheEnabled

Mcmd/entire/cli/agent_help_cmd.go+7/-4

157 unmodified lines

158
159
160
161
161
162
163
164
6 unmodified lines

171
172
173
174
175
174
175
176
177
180
181
178
179
180
181
182
183
184
185
186
187
188
189

157 unmodified lines

t.Fatalf("refresh calls after first invocation = %d, want 1", refreshCalls)
    }

// The failed attempt leaves a short-lived negative entry, so another
    // The failed attempt leaves a short-lived agent-help-only backoff, so another
    // invocation does not repeat the blocking refresh.
    _, enabled = agentHelpRepoContextWithRefresh(t.Context(), func(context.Context, trailEnablementScope) error {
        refreshCalls++
6 unmodified lines

t.Fatalf("refresh calls after second invocation = %d, want 1", refreshCalls)

// Unlike a definitive disabled result, the failure entry becomes unknown
    // after the short backoff and can be retried.
    scope, err := currentTrailEnablementScope(t.Context())
    if err != nil {
        t.Fatalf("resolve trail scope: %v", err)
    }
    if got := cachedTrailsEnablementForScope(t.Context(), scope, time.Now().Add(trailEnablementRefreshFailureCacheTTL+time.Second)); got != trailEnablementCacheUnknown {
        t.Fatalf("cache after failure backoff = %v, want unknown", got)
    // The shared decision remains unknown, so SessionStart is not prevented from
    // doing its own authoritative refresh and context-injection decision.
    if got := cachedTrailsEnablementForScope(t.Context(), scope, time.Now()); got != trailEnablementCacheUnknown {
        t.Fatalf("shared trails cache after agent-help failure = %v, want unknown", got)
    }
    // The agent-help-only marker expires after the short backoff and permits a
    // later help invocation to retry.
    if recentAgentHelpTrailsRefreshFailure(t.Context(), scope, time.Now().Add(agentHelpTrailsRefreshFailureBackoff+time.Second)) {
        t.Fatal("agent-help refresh failure should expire after the backoff")
    }
}

Mcmd/entire/cli/agent_help_cmd_test.go+10/-5

187 unmodified lines

188
189
190
191
192
193
194
195
196
197
198
199
200
201

187 unmodified lines

TrailsEnabledRepoKey   string     `json:"trails_enabled_repo_key,omitempty"`
    TrailsEnabledAPIBase   string     `json:"trails_enabled_api_base,omitempty"`
    TrailsEnabledAuthKey   string     `json:"trails_enabled_auth_key,omitempty"`

// Agent-help refresh failures use a separate, short-lived backoff. Keeping
    // this out of TrailsEnabled ensures a transient help-command failure cannot
    // suppress SessionStart's authoritative enablement probe or context injection.
    TrailsAgentHelpRefreshFailedAt *time.Time `json:"trails_agent_help_refresh_failed_at,omitempty"`
    TrailsAgentHelpFailureRepoKey  string     `json:"trails_agent_help_failure_repo_key,omitempty"`
    TrailsAgentHelpFailureAPIBase  string     `json:"trails_agent_help_failure_api_base,omitempty"`
    TrailsAgentHelpFailureAuthKey  string     `json:"trails_agent_help_failure_auth_key,omitempty"`
}

// SummaryGenerationSettings configures provider selection for on-demand

Mcmd/entire/cli/settings/settings.go+8

22 unmodified lines

23
24
25
26
26
27
28
29
166 unmodified lines

196
197
198
199
200
201
202
203
204
205
206
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234

22 unmodified lines

const (
    trailEnablementCacheTTL                   = time.Hour
    trailEnablementRefreshFailureCacheTTL     = 5 * time.Minute
    agentHelpTrailsRefreshFailureBackoff      = 5 * time.Minute
    trailEnablementSessionStartRefreshTimeout = time.Second
    trailEnablementRefreshTimeout             = 3 * time.Second
)
166 unmodified lines

return nil
}

// cacheTrailsEnablementRefreshFailure records a short-lived disabled decision
// after an agent-help refresh fails. It prevents repeated invocations from each
// waiting on the same degraded network while retrying much sooner than a real
// server-provided disabled decision. Backdating reuses the existing cache schema
// and expiry logic while giving the entry only the failure TTL remaining.
func cacheTrailsEnablementRefreshFailure(ctx context.Context, scope trailEnablementScope, now time.Time) error {
    checkedAt := now.Add(-trailEnablementCacheTTL + trailEnablementRefreshFailureCacheTTL)
    return saveTrailsEnabledForScope(ctx, scope, false, checkedAt)
// recentAgentHelpTrailsRefreshFailure reports whether agent-help should back off
// after a failed availability refresh for this exact repo/API/auth scope. This
// marker is deliberately separate from TrailsEnabled: lifecycle SessionStart
// must still perform its authoritative probe and decide context injection.
func recentAgentHelpTrailsRefreshFailure(ctx context.Context, scope trailEnablementScope, now time.Time) bool {
    prefs, err := settings.LoadClonePreferences(ctx)
    if err != nil || prefs.TrailsAgentHelpRefreshFailedAt == nil {
        return false
    }
    if prefs.TrailsAgentHelpFailureRepoKey != scope.RepoKey ||
        prefs.TrailsAgentHelpFailureAPIBase != scope.APIBase ||
        prefs.TrailsAgentHelpFailureAuthKey != scope.AuthKey {
        return false
    }
    failedAt := *prefs.TrailsAgentHelpRefreshFailedAt
    if failedAt.IsZero() || now.Before(failedAt) {
        return false
    }
    return now.Sub(failedAt) <= agentHelpTrailsRefreshFailureBackoff
}

func saveAgentHelpTrailsRefreshFailure(ctx context.Context, scope trailEnablementScope, failedAt time.Time) error {
    failedAtUTC := failedAt.UTC()
    if err := settings.ModifyClonePreferences(ctx, func(prefs *settings.ClonePreferences) error {
        prefs.TrailsAgentHelpRefreshFailedAt = &failedAtUTC
        prefs.TrailsAgentHelpFailureRepoKey = scope.RepoKey
        prefs.TrailsAgentHelpFailureAPIBase = scope.APIBase
        prefs.TrailsAgentHelpFailureAuthKey = scope.AuthKey
        return nil
    }); err != nil {
        return fmt.Errorf("save clone preferences: %w", err)
    }
    return nil
}

func refreshTrailsEnabledCacheIfStaleForScope(ctx context.Context, scope trailEnablementScope) error {