Skip to content

fix(desktop): debounce staged task promotion#7592

Open
aryaminus wants to merge 2 commits into
BasedHardware:mainfrom
aryaminus:fix/desktop-promote-debounce
Open

fix(desktop): debounce staged task promotion#7592
aryaminus wants to merge 2 commits into
BasedHardware:mainfrom
aryaminus:fix/desktop-promote-debounce

Conversation

@aryaminus
Copy link
Copy Markdown
Contributor

@aryaminus aryaminus commented Jun 2, 2026

Summary

  • Add a 30-second successful-promotion debounce inside TaskPromotionService.promoteIfNeeded()
  • Collapse rapid staged-task promotion bursts from save/complete/delete/startup/timer call sites into one backend promotion window
  • Keep startup and no-op checks from delaying the first real staged task promotion by only starting the cooldown after a task is actually promoted

Closes #7439
Closes #7441

Why

The desktop app currently calls promoteIfNeeded() from five places:

  • app startup
  • the 60s safety timer
  • after saving a staged task
  • after completing a screenshot-sourced task
  • after deleting a screenshot-sourced task

The actor already prevents overlapping calls with isPromoting, but it does not debounce sequential calls. If several triggers fire a few seconds apart, each one still reaches POST /v1/staged-tasks/promote, which contributes to the Firestore write spike described in #7439/#7441.

Implementation details

  • Adds promotionDebounceInterval = 30 seconds
  • Tracks lastPromotedAt inside the actor, so no extra locking is needed
  • Skips only calls that happen within 30s after a successful promotion
  • Does not update the cooldown when the backend returns no promoted task or when the API call fails

That last point matters: if there is no staged task at startup, the first later real staged task promotion is not blocked by a stale no-op cooldown.

Relation to existing work

This is complementary to backend dedup/idempotency work like #7092/#7093. Those PRs reduce duplicate task creation. This PR reduces the desktop call frequency that was causing promote endpoint write amplification.

Verification

  • xcrun swift build -c debug --package-path Desktop

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR adds a 30-second debounce to TaskPromotionService.promoteIfNeeded() to prevent burst backend calls when a user rapidly completes, deletes, or saves tasks. A lastPromotedAt timestamp (initialized to Date.distantPast) is checked before each promotion run, and is only updated when at least one task is actually promoted.

  • A new lastPromotedAt: Date property gates all calls to promoteIfNeeded() — the guard passes on the very first call (distantPast) and blocks subsequent calls within 30 seconds of a successful promotion.
  • The timestamp is stamped only inside the if !promotedTasks.isEmpty block, so no-op calls (no staged tasks, API errors) do not activate the cooldown and event-driven callers are not unnecessarily silenced.
  • Because the debounce is shared between event-driven callers and the 60-second safety-net timer, a promotion occurring in the second half of a timer cycle can cause the immediately following timer tick to be skipped, effectively doubling that cycle's latency from 60 s to 120 s.

Confidence Score: 4/5

Safe to merge; the debounce logic is correct and the actor serialization is sound. The safety-net timer can silently skip one cycle after an event-driven promotion, but this only affects recovery latency for missed promotions, not correctness.

The core change is small and well-reasoned. The two concerns are the shared debounce gate between event-driven calls and the safety-net timer (which can push safety-net latency from 60 s to 120 s in edge cases) and the late stamping of lastPromotedAt after async side-effects. Neither causes data loss or incorrect promotions; they only affect timing guarantees of the background safety net.

desktop/Desktop/Sources/ProactiveAssistants/Assistants/TaskExtraction/TaskPromotionService.swift — specifically the interaction between the debounce guard and the safety-net timer call site in startSafetyTimer().

Important Files Changed

Filename Overview
desktop/Desktop/Sources/ProactiveAssistants/Assistants/TaskExtraction/TaskPromotionService.swift Adds a 30-second debounce to promoteIfNeeded() via lastPromotedAt tracking; debounce timestamp is only updated when at least one task is actually promoted. The debounce also gates the safety-net timer, which can delay its effective cadence in edge cases.

Sequence Diagram

sequenceDiagram
    participant E as Event (complete/delete)
    participant TPS as TaskPromotionService
    participant API as APIClient
    participant ST as SafetyTimer (60s)

    Note over TPS: lastPromotedAt = distantPast

    TPS->>TPS: start() calls promoteIfNeeded()
    TPS->>TPS: 30s check passes (distantPast)
    TPS->>API: promoteTopStagedTask()
    API-->>TPS: "promoted=true, task A"
    TPS->>TPS: "lastPromotedAt = t5"

    E->>TPS: promoteIfNeeded() at t20
    TPS->>TPS: "15s < 30s, DEBOUNCED, return []"

    E->>TPS: promoteIfNeeded() at t36
    TPS->>TPS: "31s >= 30s, passes"
    TPS->>API: promoteTopStagedTask()
    API-->>TPS: "promoted=true, task B"
    TPS->>TPS: "lastPromotedAt = t36"

    ST->>TPS: timer tick at t65
    TPS->>TPS: "29s < 30s, DEBOUNCED, return []"
    Note over ST: next tick at t125, not t65
Loading

Reviews (1): Last reviewed commit: "fix(desktop): debounce staged task promo..." | Re-trigger Greptile

@aryaminus
Copy link
Copy Markdown
Contributor Author

Addressed Greptile's two debounce comments in follow-up commit 39389e337:\n\n- safety-net timer now calls promoteIfNeeded(bypassDebounce: true), so the 60s safety path does not get stretched to 120s by the event-driven debounce\n- lastPromotedAt is now stamped immediately after the backend returns a promoted task, before local storage sync and notification side effects

@aryaminus aryaminus force-pushed the fix/desktop-promote-debounce branch from 39389e3 to 52c3cc7 Compare June 2, 2026 22:26
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: promoteIfNeeded called from 5 sites with no debounce perf: desktop promote timer reduced from 5min to 60s causing 5x Firestore write spike

1 participant