Skip to content
Open
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
12 changes: 12 additions & 0 deletions internal/cli/errorhint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ func TestErrorHint(t *testing.T) {
ae: &coreapi.APIError{Status: 409, Code: "not_embedded", Extra: map[string]any{"semanticFloor": float64(1735689600)}},
want: "--semantic-floor all",
},
{
// The wait rides the envelope; without it the hint still says what
// happened, since "run_cooldown (HTTP 429)" alone reads as a fault.
name: "run_cooldown names the wait",
ae: &coreapi.APIError{Status: 429, Code: "run_cooldown", Extra: map[string]any{"retryAfter": float64(41)}},
want: "try again in 41 s",
},
{
name: "run_cooldown without a wait still hints",
ae: &coreapi.APIError{Status: 429, Code: "run_cooldown"},
want: "less than a minute ago",
},
{
// The end of a key's life, and the one failure the platform
// manufactures on its own schedule: a CLI key that lapses from disuse
Expand Down
10 changes: 10 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ func errorHint(ae *coreapi.APIError) string {
return fmt.Sprintf("this message is older than the indexed window (indexed from %s) — reach further back with: openemail mailboxes update <id> --semantic-floor all", fmtEpoch(int64(floor)))
}
return "this message has not been embedded — it may predate the indexed window, or the backfill may still be running"
case "run_cooldown":
// A pickup run is one POP3 login at the customer's provider, and
// providers count those, so core dispatches a source at most once a
// minute, by schedule or by hand. A run already in flight collects
// whatever is new, so there is nothing to gain by waiting it out at
// the keyboard; `retryAfter` rides the envelope in whole seconds.
if wait, ok := ae.Extra["retryAfter"].(float64); ok && wait > 0 {
return fmt.Sprintf("this source was dispatched less than a minute ago, and a run in flight already collects whatever is new; try again in %d s", int(wait))
}
return "this source was dispatched less than a minute ago, and a run in flight already collects whatever is new; try again shortly"
case "verification_unavailable":
// A resolver outage, not the customer's DNS. Retrying is the whole fix.
return "DNS could not be queried just now — nothing was changed; try again shortly"
Expand Down
5 changes: 4 additions & 1 deletion internal/coreapi/pickups.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ func (c *Client) DeletePickup(ctx context.Context, mailboxID, pickupID string) e
}

// RunPickup schedules an out-of-band run (202 scheduled). 409 disabled if the
// source is disabled.
// source is disabled; 429 run_cooldown if the source was dispatched (by
// schedule or by hand) under a minute ago, with `retryAfter` seconds in the
// envelope. A run already in flight absorbs the trigger, so the cooldown
// bounds the run AFTER the run, not this one.
func (c *Client) RunPickup(ctx context.Context, mailboxID, pickupID string) (string, error) {
var out struct {
Status string `json:"status"`
Expand Down
Loading