Skip to content

feat(desktop): focus nudges that name the real task, in plain language - #11392

Merged
kodjima33 merged 4 commits into
mainfrom
kodjima33/task-nudges-changelog
Aug 11, 2026
Merged

feat(desktop): focus nudges that name the real task, in plain language#11392
kodjima33 merged 4 commits into
mainfrom
kodjima33/task-nudges-changelog

Conversation

@kodjima33

@kodjima33 kodjima33 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What

The daily-task nudge could not fire on the two screens it exists for, and when it did fire it read a database row aloud. Five fixes, each traced to a real failure observed on a running build.

1. Dwell anchored to the app, not the window title. TikTok and YouTube rewrite the tab title on every video, and each rewrite restarted the 30s dwell clock. Measured across a real session: 14 consecutive TikTok contexts, max dwell 6s, zero evaluations — the one activity a distraction nudge exists for was the one it could never fire in.

2. Commitment nudges must name work Omi actually holds. A card reciting a prompt example — "You still haven't sent the investor update to Bob" — shipped five times at up to 95% confidence, higher than every grounded card, so neither the confidence bar nor dedup could catch it. SuggestionCommitmentGuard requires the suggestion to cover a real open commitment.

3. Due dates are phrased, not printed. ISO8601DateFormatter rendered in UTC, so a task due 23:59 EDT read as tomorrow; the model scored a due-today task as not-yet-urgent (80%) and it fell under the 85% bar. SuggestionDueDescription uses local calendar days.

4. Goals join grounding, owner-scoped. getGoals() documents that its shared cache is not owner-validated, so the fetch captures a RuntimeOwnerAuthorizationSnapshot, passes it, and stores only if still current — revalidated at read time and cleared on stop(), so an account switch cannot put one user's goals in another's prompt.

5. Prompt v7 — two-beat voice. Name the screen, then the one thing. No dates. Placeholders instead of literal names: the model was copying "Bob", and later "TikTok", straight out of the examples regardless of the real screen.

Before → after, same task:

call dad (due 2026-08-10)
TikTok is fine — but you said you'd call Ilya today

Verification

Exercised on a running named bundle, not compile-only.

  • Dwell fix live: 17:00:16 a same-app YouTube title change evaluated instead of logging skippedDwell, dwell 54s.
  • Delivered cards captured from the floating bar: "TikTok is fine — but you said you'd call Ilya today", "TikTok is fun, but you still need to read messages today".
  • Task rotation: completing a task through the real checkbox moved today_count 2 → 1 and the next nudge named the remaining commitment.
  • Guard caught a live regression: an intermediate prompt produced "You have 3 tasks due Aug 10" at 95% — a tally, not a task — and the guard suppressed it.
  • 30 focused tests, 0 failures. make preflight: 19/19.

A debug bridge action probe_suggestion_nudge drives grounding → evaluation → delivery, because the path cannot otherwise be exercised without holding a leisure window frontmost for 30s. It reports the real delivery outcome (delivered / filtered_* / rejected_owner) via a pure SuggestionDeliveryPolicy, so it cannot claim success for a filtered card.

Known-remaining, deliberately not in this change

  • Goals grounding returns empty in practice — wired and guarded, payoff unproven.
  • RECENT SUGGESTIONS can bias task selection; the model re-picks from it and the guard then suppresses the result.
  • A suppressed card still consumes the 180s evaluation cooldown.

Review in cubic

Failure class

SuggestionCommitmentGuard's over-strict coverage threshold and the dwell anchor's app-only key were both introduced earlier on this same unmerged branch and corrected before it landed, so neither is an instance of a recurring production failure.

Failure-Class: none

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 12 files

Confidence score: 3/5

  • In desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionModels.swift, grounding currently drops meaningful two-character task tokens (for cases like “Fix UI”/“Review PR”) and, with the current shared-token/coverage thresholds, short valid commitments can be structurally rejected; this can suppress legitimate daily-task nudges—preserve important short identifiers and relax/conditionalize short-text matching rules.
  • In desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift, async state handling has a few race paths (commitmentsInFlight set before awaited evaluation, stale prior-account goal responses mutating current cache, and lastGoalsRefresh advancing before fetch success) that can leave suggestions ungrounded or delayed after transient failures—scope in-flight state per request/owner, discard superseded responses, and only advance refresh markers on success (or clear on failure).
  • In desktop/macos/Desktop/Tests/SuggestionAssistantTests.swift, mutating RuntimeOwnerAuthorizationAuthority.shared without restoring it can leak owner state across test classes and create flaky downstream failures—add teardown restoration or inject a test-local authority instead of writing the process-wide singleton.
  • In desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistantSettings.swift and desktop/macos/e2e/flows/screen-recording-permission.yaml, undocumented version jumps plus no probe-specific e2e coverage make behavior changes harder to validate and maintain—document v5–v7 cache/version intent and add a suggestion-probe flow/assertions to de-risk regressions.
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="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionModels.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionModels.swift:360">
P2: Combining `minimumSharedTokens = 2` with `requiredCoverage = 0.5` makes short real commitments structurally unable to pass the guard: a commitment with a single content word can never reach 2 shared tokens, and a two-word commitment (e.g. "Call mom", "Meditate daily") requires the model to reproduce *both* words exactly, while any paraphrase that keeps only one identifying word is filtered as `<n>` computed and ungrounded. Since the goal is to catch fabricated nudges, this can silently drop legitimate grounded commitment nudges for short tasks and skew the new `filtered_ungrounded_commitment` telemetry. Consider capping `minimumSharedTokens` at `min(2, commitmentTokens.count)` inside the matcher so short commitments only need full coverage, not an impossible second token.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionModels.swift:372">
P2: Daily-task cards for concise tasks containing common two-letter identifiers (`Fix UI`, `Review PR`, `Run CI`) are always filtered as ungrounded; preserve meaningful two-character tokens here while excluding short filler words so exact task references can clear the two-token guard.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistantSettings.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistantSettings.swift:46">
P3: The version-bump comment jumps from v3/v4 explanations to a value of 7 while this same diff re-adds the anti-fabrication rules the comment says v4 removed — and v5–v7 are undocumented. A maintainer can't tell whether the jump to 7 is the result of real prompt changes or an accidental excess bump, which matters because a higher-than-necessary version silently discards every user's customized prompt. Consider documenting each version (v5/v6/v7) and reconciling the comment with the prompt it describes.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift:184">
P2: The commitment guard reads a single shared `commitmentsInFlight` slot that is written in `analyze` (and in `probeEvaluateAndDeliver`) *before* the awaited network evaluation. The actor re-enters while `evaluate` is suspended, so a second evaluation/probe that starts before the first delivery reads the slot will overwrite it, and the first result's `resolveDelivery` then validates its `commitment` nudge against the *other* grounding. In production this is masked by the 180s cooldown, but the new `probeEvaluateAndDeliver` path deliberately bypasses dwell and cooldown, so two overlapping probes (or a probe racing a legit evaluation) can either wrongly suppress a grounded card or admit one that was grounded against different commitments — producing misleading `filtered_ungrounded_commitment` / `delivered` telemetry. Consider scoping the commitments to the in-flight `AssistantResult` (e.g. carry them through the result/evaluation identity) instead of sharing one mutable slot.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift:291">
P3: `lastGoalsRefresh` is bumped synchronously *before* the fire-and-forget fetch completes and is never reset on failure (the `catch` only logs). So a single transient goal-fetch failure at refresh time suppresses goal grounding for the full `goalsRefreshInterval` (600s), and during the in-flight window a newly switched owner gets no goals even though the deadline rule is already satisfied. Since the PR's goal is that goals stay current at read time, consider resetting `lastGoalsRefresh` in the error/superseded paths (or only setting it on a successful store) so a failed or owner-switched fetch retries on the next evaluation instead of being locked out for ten minutes.</violation>

<violation number="3" location="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift:308">
P2: A delayed fetch for a prior account can erase newer current-owner goals, leaving subsequent nudges without goal grounding until another refresh completes. Drop the superseded response without mutating the cache it no longer owns.</violation>
</file>

<file name="desktop/macos/Desktop/Tests/SuggestionAssistantTests.swift">

<violation number="1" location="desktop/macos/Desktop/Tests/SuggestionAssistantTests.swift:607">
P2: These tests mutate the process-wide `RuntimeOwnerAuthorizationAuthority.shared` singleton, transitioning the real owner and leaving global ownership in a mutated (owner-a) state after the class finishes. Other test classes in the same bundle — or any production goal-fetch path that reads the shared authority — can then observe the wrong owner or a stale generation, producing cross-test interference and flakiness. Use a private `RuntimeOwnerAuthorizationAuthority()` instance, as `RuntimeOwnerIdentityTests` already does for the identical API, so the tests don't touch global auth state.</violation>
</file>

<file name="desktop/macos/e2e/flows/screen-recording-permission.yaml">

<violation number="1" location="desktop/macos/e2e/flows/screen-recording-permission.yaml:19">
P3: This flow is about verifying the screen-recording permission state and Rewind monitoring UI — none of its steps exercise the suggestion-nudge probe. The new `ProactiveAssistantsPlugin+SuggestionProbe.swift` is a debug/test seam reachable only through the automation bridge's `probe_suggestion_nudge` action, so listing it under `screen-recording-permission`'s `covers:` misrepresents that this flow covers it. Consider moving this entry to a flow that actually drives the automation bridge / suggestion path (or dropping it), so coverage metadata stays accurate and change-triggering stays honest.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +372 to +374
private static func contentTokens(_ text: String) -> Set<String> {
SuggestionDeduplication.normalize(text).subtracting(stopwords)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Daily-task cards for concise tasks containing common two-letter identifiers (Fix UI, Review PR, Run CI) are always filtered as ungrounded; preserve meaningful two-character tokens here while excluding short filler words so exact task references can clear the two-token guard.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionModels.swift, line 372:

<comment>Daily-task cards for concise tasks containing common two-letter identifiers (`Fix UI`, `Review PR`, `Run CI`) are always filtered as ungrounded; preserve meaningful two-character tokens here while excluding short filler words so exact task references can clear the two-token guard.</comment>

<file context>
@@ -264,6 +270,134 @@ enum SuggestionSearchTerm {
+    "moment", "good", "time", "need", "needs", "should", "would", "could", "make", "made",
+  ]
+
+  private static func contentTokens(_ text: String) -> Set<String> {
+    SuggestionDeduplication.normalize(text).subtracting(stopwords)
+  }
</file context>
Suggested change
private static func contentTokens(_ text: String) -> Set<String> {
SuggestionDeduplication.normalize(text).subtracting(stopwords)
}
private static func contentTokens(_ text: String) -> Set<String> {
let shortStopwords: Set<String> = [
"an", "as", "at", "be", "by", "do", "if", "in", "is", "it", "of", "on", "or", "so", "to", "up", "we",
]
let lowered = text.lowercased()
let stripped = lowered.map { $0.isLetter || $0.isNumber || $0 == " " ? $0 : " " }
return Set(
String(stripped)
.split(separator: " ")
.map(String.init)
.filter { $0.count > 1 }
)
.subtracting(stopwords)
.subtracting(shortStopwords)
}

Comment on lines +308 to +314
guard RuntimeOwnerIdentity.isAuthorizationCurrent(snapshot) else {
cachedGoals = []
cachedGoalsSnapshot = nil
lastGoalsRefresh = .distantPast
log("Suggestion: dropped goal grounding from a superseded owner")
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A delayed fetch for a prior account can erase newer current-owner goals, leaving subsequent nudges without goal grounding until another refresh completes. Drop the superseded response without mutating the cache it no longer owns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift, line 308:

<comment>A delayed fetch for a prior account can erase newer current-owner goals, leaving subsequent nudges without goal grounding until another refresh completes. Drop the superseded response without mutating the cache it no longer owns.</comment>

<file context>
@@ -242,11 +265,76 @@ actor SuggestionAssistant: ProactiveAssistant {
+
+  /// Drop the result outright if the account changed while the fetch was in flight.
+  private func storeGoals(_ goals: [String], snapshot: RuntimeOwnerAuthorizationSnapshot) {
+    guard RuntimeOwnerIdentity.isAuthorizationCurrent(snapshot) else {
+      cachedGoals = []
+      cachedGoalsSnapshot = nil
</file context>
Suggested change
guard RuntimeOwnerIdentity.isAuthorizationCurrent(snapshot) else {
cachedGoals = []
cachedGoalsSnapshot = nil
lastGoalsRefresh = .distantPast
log("Suggestion: dropped goal grounding from a superseded owner")
return
}
guard RuntimeOwnerIdentity.isAuthorizationCurrent(snapshot) else {
log("Suggestion: dropped goal grounding from a superseded owner")
return
}

/// next owner's prompt. These pin the capture/pass/validate contract at the authority
/// level: a snapshot taken before an account switch must not validate after it.
final class SuggestionGoalOwnerScopingTests: XCTestCase {
private let authority = RuntimeOwnerAuthorizationAuthority.shared

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These tests mutate the process-wide RuntimeOwnerAuthorizationAuthority.shared singleton, transitioning the real owner and leaving global ownership in a mutated (owner-a) state after the class finishes. Other test classes in the same bundle — or any production goal-fetch path that reads the shared authority — can then observe the wrong owner or a stale generation, producing cross-test interference and flakiness. Use a private RuntimeOwnerAuthorizationAuthority() instance, as RuntimeOwnerIdentityTests already does for the identical API, so the tests don't touch global auth state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Tests/SuggestionAssistantTests.swift, line 607:

<comment>These tests mutate the process-wide `RuntimeOwnerAuthorizationAuthority.shared` singleton, transitioning the real owner and leaving global ownership in a mutated (owner-a) state after the class finishes. Other test classes in the same bundle — or any production goal-fetch path that reads the shared authority — can then observe the wrong owner or a stale generation, producing cross-test interference and flakiness. Use a private `RuntimeOwnerAuthorizationAuthority()` instance, as `RuntimeOwnerIdentityTests` already does for the identical API, so the tests don't touch global auth state.</comment>

<file context>
@@ -343,3 +343,303 @@ final class SuggestionPromptContractTests: XCTestCase {
+/// next owner's prompt. These pin the capture/pass/validate contract at the authority
+/// level: a snapshot taken before an account switch must not validate after it.
+final class SuggestionGoalOwnerScopingTests: XCTestCase {
+  private let authority = RuntimeOwnerAuthorizationAuthority.shared
+
+  private func signIn(_ owner: String) {
</file context>
Suggested change
private let authority = RuntimeOwnerAuthorizationAuthority.shared
private let authority = RuntimeOwnerAuthorizationAuthority()


/// One shared word is a coincidence — "update", "send" and "follow" appear in most
/// commitments. Two is a reference.
static let minimumSharedTokens = 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Combining minimumSharedTokens = 2 with requiredCoverage = 0.5 makes short real commitments structurally unable to pass the guard: a commitment with a single content word can never reach 2 shared tokens, and a two-word commitment (e.g. "Call mom", "Meditate daily") requires the model to reproduce both words exactly, while any paraphrase that keeps only one identifying word is filtered as <n> computed and ungrounded. Since the goal is to catch fabricated nudges, this can silently drop legitimate grounded commitment nudges for short tasks and skew the new filtered_ungrounded_commitment telemetry. Consider capping minimumSharedTokens at min(2, commitmentTokens.count) inside the matcher so short commitments only need full coverage, not an impossible second token.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionModels.swift, line 360:

<comment>Combining `minimumSharedTokens = 2` with `requiredCoverage = 0.5` makes short real commitments structurally unable to pass the guard: a commitment with a single content word can never reach 2 shared tokens, and a two-word commitment (e.g. "Call mom", "Meditate daily") requires the model to reproduce *both* words exactly, while any paraphrase that keeps only one identifying word is filtered as `<n>` computed and ungrounded. Since the goal is to catch fabricated nudges, this can silently drop legitimate grounded commitment nudges for short tasks and skew the new `filtered_ungrounded_commitment` telemetry. Consider capping `minimumSharedTokens` at `min(2, commitmentTokens.count)` inside the matcher so short commitments only need full coverage, not an impossible second token.</comment>

<file context>
@@ -264,6 +270,134 @@ enum SuggestionSearchTerm {
+
+  /// One shared word is a coincidence — "update", "send" and "follow" appear in most
+  /// commitments. Two is a reference.
+  static let minimumSharedTokens = 2
+
+  /// Words that carry no identifying signal, so they cannot vouch for a commitment.
</file context>

clearPendingContext()
lastEvaluationAt = now
dailyBudget.recordEvaluation(now: now)
commitmentsInFlight = grounding.openCommitments

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The commitment guard reads a single shared commitmentsInFlight slot that is written in analyze (and in probeEvaluateAndDeliver) before the awaited network evaluation. The actor re-enters while evaluate is suspended, so a second evaluation/probe that starts before the first delivery reads the slot will overwrite it, and the first result's resolveDelivery then validates its commitment nudge against the other grounding. In production this is masked by the 180s cooldown, but the new probeEvaluateAndDeliver path deliberately bypasses dwell and cooldown, so two overlapping probes (or a probe racing a legit evaluation) can either wrongly suppress a grounded card or admit one that was grounded against different commitments — producing misleading filtered_ungrounded_commitment / delivered telemetry. Consider scoping the commitments to the in-flight AssistantResult (e.g. carry them through the result/evaluation identity) instead of sharing one mutable slot.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift, line 184:

<comment>The commitment guard reads a single shared `commitmentsInFlight` slot that is written in `analyze` (and in `probeEvaluateAndDeliver`) *before* the awaited network evaluation. The actor re-enters while `evaluate` is suspended, so a second evaluation/probe that starts before the first delivery reads the slot will overwrite it, and the first result's `resolveDelivery` then validates its `commitment` nudge against the *other* grounding. In production this is masked by the 180s cooldown, but the new `probeEvaluateAndDeliver` path deliberately bypasses dwell and cooldown, so two overlapping probes (or a probe racing a legit evaluation) can either wrongly suppress a grounded card or admit one that was grounded against different commitments — producing misleading `filtered_ungrounded_commitment` / `delivered` telemetry. Consider scoping the commitments to the in-flight `AssistantResult` (e.g. carry them through the result/evaluation identity) instead of sharing one mutable slot.</comment>

<file context>
@@ -161,6 +181,7 @@ actor SuggestionAssistant: ProactiveAssistant {
     clearPendingContext()
     lastEvaluationAt = now
     dailyBudget.recordEvaluation(now: now)
+    commitmentsInFlight = grounding.openCommitments
 
     do {
</file context>

/// discarded. v3 removed the invented names from the examples (a model was reproducing
/// them as the user's own commitments); v4 moved the anti-fabrication rule out of the
/// prompt into SuggestionCommitmentGuard, because as prose it also suppressed real nudges.
private let currentPromptVersion = 7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The version-bump comment jumps from v3/v4 explanations to a value of 7 while this same diff re-adds the anti-fabrication rules the comment says v4 removed — and v5–v7 are undocumented. A maintainer can't tell whether the jump to 7 is the result of real prompt changes or an accidental excess bump, which matters because a higher-than-necessary version silently discards every user's customized prompt. Consider documenting each version (v5/v6/v7) and reconciling the comment with the prompt it describes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistantSettings.swift, line 46:

<comment>The version-bump comment jumps from v3/v4 explanations to a value of 7 while this same diff re-adds the anti-fabrication rules the comment says v4 removed — and v5–v7 are undocumented. A maintainer can't tell whether the jump to 7 is the result of real prompt changes or an accidental excess bump, which matters because a higher-than-necessary version silently discards every user's customized prompt. Consider documenting each version (v5/v6/v7) and reconciling the comment with the prompt it describes.</comment>

<file context>
@@ -39,7 +39,11 @@ class SuggestionAssistantSettings {
+  /// discarded. v3 removed the invented names from the examples (a model was reproducing
+  /// them as the user's own commitments); v4 moved the anti-fabrication rule out of the
+  /// prompt into SuggestionCommitmentGuard, because as prose it also suppressed real nudges.
+  private let currentPromptVersion = 7
 
   /// System prompt. Inherits the shape of the shipped Insight prompt — which was never
</file context>

cachedGoalsSnapshot = nil
return
}
lastGoalsRefresh = Date()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: lastGoalsRefresh is bumped synchronously before the fire-and-forget fetch completes and is never reset on failure (the catch only logs). So a single transient goal-fetch failure at refresh time suppresses goal grounding for the full goalsRefreshInterval (600s), and during the in-flight window a newly switched owner gets no goals even though the deadline rule is already satisfied. Since the PR's goal is that goals stay current at read time, consider resetting lastGoalsRefresh in the error/superseded paths (or only setting it on a successful store) so a failed or owner-switched fetch retries on the next evaluation instead of being locked out for ten minutes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/Suggestions/SuggestionAssistant.swift, line 291:

<comment>`lastGoalsRefresh` is bumped synchronously *before* the fire-and-forget fetch completes and is never reset on failure (the `catch` only logs). So a single transient goal-fetch failure at refresh time suppresses goal grounding for the full `goalsRefreshInterval` (600s), and during the in-flight window a newly switched owner gets no goals even though the deadline rule is already satisfied. Since the PR's goal is that goals stay current at read time, consider resetting `lastGoalsRefresh` in the error/superseded paths (or only setting it on a successful store) so a failed or owner-switched fetch retries on the next evaluation instead of being locked out for ten minutes.</comment>

<file context>
@@ -242,11 +265,76 @@ actor SuggestionAssistant: ProactiveAssistant {
+      cachedGoalsSnapshot = nil
+      return
+    }
+    lastGoalsRefresh = Date()
+    Task { [weak self] in
+      do {
</file context>

- desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Components/SettingsContentView+BillingHelpers.swift
- desktop/macos/Desktop/Sources/MainWindow/RewindOnlyView.swift
- desktop/macos/Desktop/Sources/ProactiveAssistants/Core/ProactiveCaptureSystemProbe.swift
- desktop/macos/Desktop/Sources/ProactiveAssistants/ProactiveAssistantsPlugin+SuggestionProbe.swift

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This flow is about verifying the screen-recording permission state and Rewind monitoring UI — none of its steps exercise the suggestion-nudge probe. The new ProactiveAssistantsPlugin+SuggestionProbe.swift is a debug/test seam reachable only through the automation bridge's probe_suggestion_nudge action, so listing it under screen-recording-permission's covers: misrepresents that this flow covers it. Consider moving this entry to a flow that actually drives the automation bridge / suggestion path (or dropping it), so coverage metadata stays accurate and change-triggering stays honest.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/e2e/flows/screen-recording-permission.yaml, line 19:

<comment>This flow is about verifying the screen-recording permission state and Rewind monitoring UI — none of its steps exercise the suggestion-nudge probe. The new `ProactiveAssistantsPlugin+SuggestionProbe.swift` is a debug/test seam reachable only through the automation bridge's `probe_suggestion_nudge` action, so listing it under `screen-recording-permission`'s `covers:` misrepresents that this flow covers it. Consider moving this entry to a flow that actually drives the automation bridge / suggestion path (or dropping it), so coverage metadata stays accurate and change-triggering stays honest.</comment>

<file context>
@@ -16,6 +16,7 @@ covers:
   - desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Components/SettingsContentView+BillingHelpers.swift
   - desktop/macos/Desktop/Sources/MainWindow/RewindOnlyView.swift
   - desktop/macos/Desktop/Sources/ProactiveAssistants/Core/ProactiveCaptureSystemProbe.swift
+  - desktop/macos/Desktop/Sources/ProactiveAssistants/ProactiveAssistantsPlugin+SuggestionProbe.swift
   - desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/MemoryExtraction/MemoryAssistant.swift
   - desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/MemoryExtraction/MemoryAssistantTelemetry.swift
</file context>

The daily-task nudge could not fire on the two screens it exists for, and when it
did fire it read a database row aloud.

- Dwell now anchors to the app, not the window title. TikTok and YouTube rewrite
  the tab title on every video, and each rewrite restarted the 30s dwell clock —
  measured across a real session, 14 consecutive TikTok contexts, max dwell 6s,
  zero evaluations. `SuggestionDwellAnchor` keeps the clock running through
  same-app title churn.
- Commitment nudges must name work Omi actually holds. `SuggestionCommitmentGuard`
  requires the suggestion to cover a real open commitment; a card reciting a
  prompt example ("You still haven't sent the investor update to Bob") shipped
  five times at up to 95% confidence, above every grounded card, so neither the
  confidence bar nor dedup could catch it.
- Due dates are phrased, not printed. `ISO8601DateFormatter` rendered in UTC, so
  a task due 23:59 EDT read as *tomorrow*; the model scored a due-today task as
  not-yet-urgent (80%) and it fell under the 85% bar. `SuggestionDueDescription`
  uses local calendar days and says "due today" / "overdue by 3 days".
- Goals join grounding, owner-scoped. `getGoals()` documents that its shared
  cache is not owner-validated, so the fetch captures a
  RuntimeOwnerAuthorizationSnapshot, passes it, and stores only if the
  authorization is still current — revalidated again at read time and cleared on
  stop, so an account switch cannot put one user's goals in another's prompt.
- Prompt v7: two-beat voice (name the screen, then the one thing), no dates, and
  placeholders instead of literal names — the model was copying "Bob" and later
  "TikTok" straight out of the examples regardless of the real screen.

Verification: exercised on the running named bundle, not compile-only.
- Dwell fix live: 17:00:16 a same-app YouTube title change evaluated instead of
  logging skippedDwell, dwell 54s (omi-notch-49084 log).
- Delivered cards captured from the floating bar: "TikTok is fine — but you said
  you'd call Ilya today", "TikTok is fun, but you still need to read messages
  today".
- Completing a task via the real checkbox moved the nudge to the next task
  (today_count 2 -> 1, next card named the remaining commitment).
- 30 focused tests, 0 failures; swift-format clean on all changed files.

Known-remaining, deliberately not in this change: goals grounding returns empty
in practice, RECENT SUGGESTIONS can bias task selection, and a suppressed card
still consumes the 180s evaluation cooldown.
The automation probe is a test seam, not runtime behaviour, so it moves to
ProactiveAssistantsPlugin+SuggestionProbe.swift rather than growing an
already-ratcheted product file; the plugin returns to its exact baseline.

DesktopAutomationBridge.swift does grow by the 13-line action registration —
registrations must live in that file's registry — so its baseline is raised with
a justification rather than silently.

Also replaces force-unwraps in the new date tests with XCTUnwrap (swiftlint), and
covers the new file in the screen-recording-permission e2e flow.

make preflight: 19/19 checks pass.
…ss pages

Insight ran on `gemini-pro-latest` for everyone until the premium tier dropped it
to Flash. Click-through fell with it: 2.34% in the week of 2026-04-12 (39.8k sent,
336 distinct clickers) against 0.7-0.96% through July. The prompt is byte-identical
to the April peak and the July logic changes were fixes, so the model was the only
thing that moved. Insight is the cheap place to spend: a 10-minute timer caps it at
~6 analyses/hour regardless of how much the user switches windows.

- ModelQoS.Gemini.insight is Pro on every tier.
- GeminiClient logs its model at construction. The model previously appeared only
  inside the request URL, so a tier change could not be confirmed on a real machine
   — which is how the April regression went unnoticed for four months.
- SuggestionDwellAnchor keys on context identity, not just the app. Keying on the
  app alone let ten minutes on a work page carry into a TikTok tab and fire
  immediately; titles sharing no significant word now restart the clock, while
  churn inside one sitting (TikTok relabelling the tab per video) still does not.
- Comment placement: the due-date rationale sat above refreshGoalsIfStale and now
  documents describeCommitment, where it belongs.

Verification: exercised on the running named bundle.
- Runtime log confirms the tier: `GeminiClient: model=gemini-2.5-pro fallback=gemini-2.5-flash`.
- Real Insight run through the two-phase SQL tool loop returned `INSIGHT [75%]`.
- Delivered card captured from the floating bar: "Discord onboarding is fine - but
  you said you'd recruit people for Omi onboarding by Saturday".
- 117 focused tests, 0 failures; swift-format clean.
@kodjima33
kodjima33 force-pushed the kodjima33/task-nudges-changelog branch from da38bfd to 0648fcf Compare August 11, 2026 04:01
`requiredCoverage` was measured against every content word in the commitment, so a
long task demanded proportionally more evidence than a short one. "Exchange weekly
tasks with accountability partner and update the shared Google Doc tracker" has ten
content words, and a concise, correct nudge naming two of them was filtered as
ungrounded.

Two shared words are still the floor. Above that, sharing something *distinctive* —
a word outside the generic doing-verbs (send/update/follow/check/...) — is a
reference regardless of how long the task is. Proportional coverage remains the
fallback, which is what keeps "you should send an update" from matching any
commitment that happens to contain both words.

Tests: concise nudges for a real ten-word commitment are admitted; generic overlap
with that same long commitment is still rejected. 119 focused tests pass.

Verified on the running named bundle: delivered card captured from the floating bar,
"Discord onboarding is fine - but you said you'd recruit people for Omi onboarding
and Discord by Saturday", with GeminiClient reporting model=gemini-2.5-pro.

No class is declared because SuggestionCommitmentGuard was introduced earlier on this
same unmerged branch; the over-strict threshold never reached a user, so this is an
in-branch correction rather than an instance of a recurring production failure.

Failure-Class: none
@kodjima33
kodjima33 merged commit 3f44b8f into main Aug 11, 2026
28 checks passed
@kodjima33
kodjima33 deleted the kodjima33/task-nudges-changelog branch August 11, 2026 05:01
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.

1 participant