Conversation
|
Someone is attempting to deploy a commit to the Comp AI - PoC Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
2 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/src/google/gmail-sync.service.ts">
<violation number="1" location="apps/api/src/google/gmail-sync.service.ts:247">
P1: A rate-limited or unauthorized message fetch is reported as a successful settle, so its backoff/reconnect state is never persisted and the next sync tick immediately replays up to 120 Gmail calls with the same bad condition. Preserve the cursor while routing these outcomes through the corresponding state transition (and return a rate-limited/reconnect outcome).</violation>
<violation number="2" location="apps/api/src/google/gmail-sync.service.ts:514">
P3: The new `isRetryable` type predicate is unsound: it is declared to narrow the result to the `failed` variant, but it returns `true` for `rate-limited` and `unauthorized` outcomes as well, which are different union members. It is safe only because the current caller uses it as a plain boolean; any future call that relies on the narrowed type would be handed the wrong shape. Consider dropping the bogus narrow guard and returning a plain `boolean`, or narrowing to the full union of retryable outcomes.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| const message = await this.gmail.getMessage(accessToken, id); | ||
| if (message.outcome !== "ok") continue; | ||
| if (message.outcome !== "ok") { | ||
| if (isRetryable(message)) deferred += 1; |
There was a problem hiding this comment.
P1: A rate-limited or unauthorized message fetch is reported as a successful settle, so its backoff/reconnect state is never persisted and the next sync tick immediately replays up to 120 Gmail calls with the same bad condition. Preserve the cursor while routing these outcomes through the corresponding state transition (and return a rate-limited/reconnect outcome).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/google/gmail-sync.service.ts, line 247:
<comment>A rate-limited or unauthorized message fetch is reported as a successful settle, so its backoff/reconnect state is never persisted and the next sync tick immediately replays up to 120 Gmail calls with the same bad condition. Preserve the cursor while routing these outcomes through the corresponding state transition (and return a rate-limited/reconnect outcome).</comment>
<file context>
@@ -237,16 +239,20 @@ export class GmailSyncService {
const message = await this.gmail.getMessage(accessToken, id);
- if (message.outcome !== "ok") continue;
+ if (message.outcome !== "ok") {
+ if (isRetryable(message)) deferred += 1;
+ continue;
+ }
</file context>
|
|
||
| function isRetryable( | ||
| result: GoogleResult<unknown>, | ||
| ): result is Extract<GoogleResult<unknown>, { outcome: "failed" }> { |
There was a problem hiding this comment.
P3: The new isRetryable type predicate is unsound: it is declared to narrow the result to the failed variant, but it returns true for rate-limited and unauthorized outcomes as well, which are different union members. It is safe only because the current caller uses it as a plain boolean; any future call that relies on the narrowed type would be handed the wrong shape. Consider dropping the bogus narrow guard and returning a plain boolean, or narrowing to the full union of retryable outcomes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/google/gmail-sync.service.ts, line 514:
<comment>The new `isRetryable` type predicate is unsound: it is declared to narrow the result to the `failed` variant, but it returns `true` for `rate-limited` and `unauthorized` outcomes as well, which are different union members. It is safe only because the current caller uses it as a plain boolean; any future call that relies on the narrowed type would be handed the wrong shape. Consider dropping the bogus narrow guard and returning a plain `boolean`, or narrowing to the full union of retryable outcomes.</comment>
<file context>
@@ -502,3 +508,10 @@ export class GmailSyncService {
+
+function isRetryable(
+ result: GoogleResult<unknown>,
+): result is Extract<GoogleResult<unknown>, { outcome: "failed" }> {
+ if (result.outcome === "failed") return result.retryable;
+ return result.outcome === "rate-limited" || result.outcome === "unauthorized";
</file context>
| ): result is Extract<GoogleResult<unknown>, { outcome: "failed" }> { | |
| ): boolean { |
Why
The Gmail incremental sync can silently lose email.
ingest()fetches each message in a batch of up to 120; whengetMessagefails it doescontinueand the message is dropped. Back inincremental(), the history cursor is advanced tohistory.data.historyIdwheneverremaining === 0— butremainingonly counts messages beyond the batch cap, not failed fetches.So a rate limit (429), an expired access token mid-batch (401), a timeout, or any transient 5xx makes the affected messages permanently absent: the cursor moves past them and they are never retried, even on later ticks.
What
ingest()now returns adeferredcount for message fetches that failed retryably (rate-limited,unauthorized, orfailedwithretryable: true).incremental()keeps the cursor atstartHistoryIdwhendeferred > 0, so the same history window is replayed next tick. Messages already written are filtered bygmailMessageId, so re-running the window is idempotent.cursor-invalid(message deleted, 404/410) and a non-retryable failure (e.g. 403) will never succeed, so holding the cursor on them would stall the mailbox forever.Tests
New
apps/api/test/gmail-sync.spec.ts:startHistoryIdVerified:
bun run check-types,bunx biome checkon changed files, and the new spec all pass.Summary by cubic
Prevent silent email loss by holding the Gmail sync cursor when message fetches fail with retryable errors. The same history window is replayed on the next tick; non-retryable cases still advance.
ingest()tracksdeferredfor retryable outcomes (rate-limited,unauthorized, orfailedwithretryable: true).incremental()keeps the cursor atstartHistoryIdwhendeferred > 0and replays the window; stored messages are deduped bygmailMessageId.cursor-invalid); log now includesdeferred, and tests cover both paths.Written for commit acfc9d0. Summary will update on new commits.