feat(action-items): task cleanup — backend endpoints + Windows UI - #4
Open
formed2forge wants to merge 107 commits into
Open
feat(action-items): task cleanup — backend endpoints + Windows UI#4formed2forge wants to merge 107 commits into
formed2forge wants to merge 107 commits into
Conversation
formed2forge
force-pushed
the
worktree-feat-action-items-cleanup
branch
2 times, most recently
from
August 26, 2026 15:02
310b3f4 to
652d31b
Compare
formed2forge
force-pushed
the
worktree-feat-action-items-cleanup
branch
2 times, most recently
from
August 27, 2026 19:19
df6ab70 to
35597f6
Compare
…rom Settings Nik's call, with screenshots of exactly what to hide: the Task/Insight/Memory Assistant panes in Advanced, and Notification Previews, Background Style, and Draggable Floating Bar in the Floating Bar pane. All are commented out rather than deleted, under a HIDDEN DELIBERATELY marker — 73c7f85 re-added this exact surface once before because a previous hide left the code looking like an accident. Matching sidebar search entries are commented out with them. The assistants keep running with their stored settings; only the settings UI is hidden. Every stored default keeps applying. navigate_via_shortcut gains tasksettings/floatingbarsettings cases that post the app's existing deep-link notifications, so QA reaches these panes with no cursor input — used to verify this change on a running bundle: Advanced now flows AI Setup -> Analysis Throttle, and the Floating Bar pane shows only Show/Typed Questions/Screen Sharing/Voice/Faster. Verification: swift build; swift test --filter SettingsAssistantControlsTests (14 tests, 0 failures); in-app window captures of both panes from com.omi.omi-hide-check built from this branch.
* chore(deps): bump @tschk/moonshine packages to 0.4.0 * chore(deps): update bun.lock for moonshine 0.4.0 Regenerate web/app/bun.lock so bun install --frozen-lockfile matches the ^0.4.0 package.json ranges. Dockerfile and test.sh both use --frozen-lockfile, so the stale ^0.3.7 lockfile failed CI and image builds instead of installing 0.4.0. Verification: bun install --frozen-lockfile succeeds and resolves @tschk/moonshine* to 0.4.0. * chore(deps): regenerate bun.lock — resolve @tschk/moonshine 0.4.1 0.4.0 shipped with stale ^0.3.6 internal ranges; 0.4.1 is the fixed release. This also unblocks the --frozen-lockfile CI/Docker builds flagged by review bots. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…es dashboard (BasedHardware#12329) A push-to-talk row built on the already-wired realtime_voice client journey (omi_client_journey_* metrics): headline transport success rate (success/failure denominator, matching the existing journey stats), accepted attempts, success-path duration p95, issue count, and timeseries for terminal outcomes, bounded issue classes, and client-kind split. Panel descriptions state the two limits explicitly: the journey observes the PTT voice WebSocket leg only (the on-device realtime-hub lane is not yet server-observed), and transport success says nothing about task quality. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…edHardware#12328) * feat(desktop): add source attribution to floating_bar_query_sent Most PTT turns dispatch through the realtime hub and never fired this event, so typed vs voice volume was unreadable. Tag each dispatch path and emit the event from the hub commit so every floating-bar query is counted exactly once. Co-authored-by: Cursor <cursoragent@cursor.com> * test(desktop): keep floating-bar query analytics teardown Xcode 16.4-safe Async XCTest super.tearDown from an @mainactor suite transfers a non-Sendable test instance and fails the pinned-toolchain hook check. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Auto-generated: consolidates unreleased changelog fragments into v0.12.232 and regenerates CHANGELOG.json.
…rom Settings (BasedHardware#12327) ## What Hides from Settings, per Nik's screenshots: the **Task / Insight / Memory Assistant** panes in Advanced, and the **Notification Previews / Background Style / Draggable Floating Bar** rows in the Floating Bar pane. Everything is **commented out, not deleted**, under a `HIDDEN DELIBERATELY` marker. That marker is the point: this exact surface was re-added once before (73c7f85, "give the three proactive assistants a pane you can reach") because the previous hide left the code looking like dead-by-accident. Matching sidebar search-index entries are commented out with the rows so search cannot point at hidden panes. The assistants keep running with their stored settings — only the settings UI is hidden. All stored defaults keep applying (previews, background, draggability, intervals, confidence). `navigate_via_shortcut` gains `tasksettings` / `floatingbarsettings` cases that post the app's existing deep-link notifications (`.navigateToTaskSettings`, `.navigateToFloatingBarSettings`), so QA can land on these panes with zero cursor input — this is how the change itself was verified. ## Verification - `swift build` clean; `swift test --filter SettingsAssistantControlsTests` — 14 tests, 0 failures. - Running bundle (`com.omi.omi-hide-check`, built from this branch), navigated via the new bridge shortcuts, in-app window captures: **Advanced flows AI Setup → Analysis Throttle** (trio gone); **Floating Bar shows only Show / Typed Questions / Screen Sharing / Voice / Faster**. Line-Count-Exception: desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift | 4792 -> 4801 | nine lines: two navigate_via_shortcut cases posting existing deep-link notifications, needed to verify the hidden panes cursor-free Failure-Class: none <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/BasedHardware/omi/pull/12327?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
…ant pane Follow-up to BasedHardware#12327 (the amend carrying this was force-pushed after the merge watcher had already captured the pre-amend head, so the PR merged without it). The Tasks-page gear posted navigateToTaskSettings, which opened Advanced and highlighted advanced.taskassistant — a card BasedHardware#12327 hides. A control that deep-links to a pane that no longer renders is exactly the dangling door that got the previous hide reverted by 73c7f85. The gear is hidden under the same HIDDEN DELIBERATELY marker, and the stale highlight is dropped (the notification still navigates to Advanced; the bridge shortcut uses it). HiddenSettingsSurfacesTests pins all six hidden setting ids out of the settings search index and asserts the surviving floating-bar rows stay searchable — the regression coverage for the hide itself. Verification: exercised on a running dev bundle — Tasks-page header shows only Search / Select / + (no gear), captured; swift test --filter HiddenSettingsSurfacesTests|SettingsAssistantControlsTests: 17 tests, 0 failures. Failure-Class: none
…seam Review round: the search-index tests would still pass if the gear or the deep-link highlight came back. HiddenSettingsSurfacesPolicy is now the one typed decision both views consult — TasksPage gates the gear on tasksHeaderShowsSettingsGear, SettingsPage highlights only what taskSettingsHighlight returns — so restoring either surface requires flipping a value the regression tests own, not deleting a comment. Tests: gear policy pinned false; taskSettingsHighlight nil while the pane is hidden; highlightIfVisible refuses every hidden id and passes visible ones; search index proven disjoint from the policy's hidden set. Verification: swift test --filter HiddenSettingsSurfacesTests| SettingsAssistantControlsTests (21 tests, 0 failures); running dev bundle rebuilt from this commit — Tasks header still gear-free (captured). Failure-Class: none
…constants Review round: policy-constant assertions stay green if a view bypasses the policy. The gear is now a component (TasksHeaderSettingsGear) whose real body builder the test executes — nothing renders under the production policy, the button renders when forced visible (the control case that makes the absence meaningful). The .navigateToTaskSettings transition is data (SettingsDeepLinkTransition.taskSettings()) applied verbatim by onReceive, and the test drives that exact production value: Advanced, nil highlight. SwiftUI's accessibility tree does not materialize in the CLI test host (hosted NSHostingView probes found zero AX buttons even forced-visible), so the body value is the deepest reliably executable seam; full-page composition stays owned by the e2e flows. Verification: HiddenSettingsSurfacesTests 9 tests 0 failures; SettingsAssistantControls+AgentPillLifecycle 98 tests 0 failures; rebuilt dev bundle — Tasks header still gear-free (captured). Failure-Class: none
Auto-generated: consolidates unreleased changelog fragments into v0.12.233 and regenerates CHANGELOG.json.
…with zone label (BasedHardware#12349) Failure-Class: new
Auto-generated: consolidates unreleased changelog fragments into v0.12.234 and regenerates CHANGELOG.json.
* fix(macos): restore shell clicks after reopening Failure-Class: none * chore(ci): retrigger Repo Checks after a concurrency-group cancellation Formatting and Hygiene were cancelled at 20:27:47 by the repo-checks concurrency group, and the run that followed was skipped rather than executed, so Formatting has no successful run on this head. No code change.
Stub the temporal helper added to app integrations so the two hermetic test harnesses collect without importing production database paths. Reword the macOS parity comment so the legacy-memory inventory does not mistake documentation for a new legacy assistant surface. Verified with the exact backend/test.sh file-isolated runner: 35 passed. Legacy memory surface ratchet: PASS, 892 findings across 82 path counters.
…e#12336) Reuse the shared compact primary button style so the post-rating referral CTA uses the same inverse label treatment as the referral sheet. Failure-Class: none Verification: xcrun swift test -c debug --package-path Desktop --filter RatingPromptPolicyTests Verification: ./scripts/swift-format-wrapper.sh lint -r Desktop/Sources/RatingPrompt.swift Desktop/Tests/RatingPromptPolicyTests.swift Verification: python3 scripts/check_desktop_test_quality.py
Auto-generated: consolidates unreleased changelog fragments into v0.12.235 and regenerates CHANGELOG.json.
) (BasedHardware#10960) * feat(ci): retire superseded Windows release sync PRs (BasedHardware#10727) Each Windows release opens a release/windows-v* sync PR to stamp desktop/windows/package.json back onto main. The release tag is authoritative, so older open sync PRs are pure review noise once a newer release has a PR; nine had accumulated. Add a testable Python helper that lists open PRs whose same-repo head matches the release/windows-v* prefix, excludes the current release PR, and closes the rest as superseded with a pointer to the newest. The selection predicate is unit-tested (current PR retained, unrelated heads never selected) and wired into the checks-manifest so the contract runs in CI. Cleanup stays best-effort and non-fatal: publishing and tags remain authoritative. Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * ci(desktop-windows): close superseded version-sync PRs after release After the current release's sync PR exists, invoke the retire helper so older release/windows-v* PRs targeting main are closed as superseded. Failure is non-fatal: the release tag is already published and remains the source of truth. Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * fix(ci): never close fork PRs and page past the list limit cubic review follow-up on BasedHardware#10960: the search matched any open PR whose head starts with release/windows-v*, which could include fork-origin contributor PRs this release job must not touch. Request isCrossRepository in the gh query, default the selection to same-repo only, and add a fork fixture to the contract test. Also pass an explicit --limit so cleanup does not silently stop at the CLI default (30) after a long outage or backlog growth. Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * fix(ci): make --self-test run without release args The script advertised `--self-test` as a hermetic check, but argparse required --current-pr/--version even in that mode, so the documented invocation failed before reaching the test. Make those args optional and validate them only for the real cleanup path. Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * fix(ci): list Windows sync PRs without head: search gh pr list --search head:release/windows-v returns zero same-repo results, so retirement became a no-op. List open main PRs and filter by headRefName prefix locally, with a regression test on the query args. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): paginate Windows sync PR retirement listing gh pr list --limit 100 truncated when main has 100+ open PRs, so older superseded release/windows-v* sync PRs could be missed. Fetch all open PRs via gh api --paginate --slurp and keep local prefix/fork filters. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: CommandCodeBot <noreply@commandcode.ai> Co-authored-by: Cursor <cursoragent@cursor.com>
…Hardware#12337) * fix(backend): replace sunset Assistants file-chat with Chat Completions OpenAI retired Assistants on 2026-08-26, so every /v1/threads create 404s and non-vision file chat is down. Stream PDFs as Chat Completions file parts on gpt-4.1 (documented file-input contract) and keep images on the verified gpt-5.6-luna vision lane. Failure-Class: FC-pinned-vendor-request-contract-retired Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai> * fix(backend): satisfy Chat Completions typecheck without kwargs spread Unpacking a token-budget dict into create() is untyped and fails pyright. Failure-Class: FC-pinned-vendor-request-contract-retired Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai> * test(backend): make isolated file-chat CI unit stubs non-flaky CI ran each unit file in its own process; incomplete BYOK/LLM stubs and a duration-guard race on desktop transcribe failed the backend suite. Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai> * fix(stt): mark Soniox re-exports for strict pyright Upstream 7c900a9 moved the Soniox client into utils.stt.soniox but left SafeSonioxSocket / process_audio_soniox imported in streaming.py without a re-export marker; receiver.py and test_soniox_streaming.py import them through utils.stt.streaming, so strict pyright fails main and every PR merge ref. Carry the one-line marker so this backend CI lane can go green; matches the existing SafeDeepgramSocket compat re-export convention on the previous line. Co-authored-by: multica-agent <github@multica.ai> * feat(backend): route remaining LLM HTTP through gateway File-chat completions, company-paid desktop Vertex traffic (text + single embed), and both embedding surfaces now hop backend/llm_gateway so accounting (build_accounting_event / gateway metrics) is the single spend ledger when OMI_LLM_GATEWAY_FEATURE_MODE=gateway: - gateway: OpenAI-shaped /v1/embeddings surface (omi:auto:openai-embeddings, omi:auto:gemini-embeddings) with accounting; desktop-vertex-* chat lanes generated from vertex_pt_routing; VertexGeminiProvider owns the PT policy (pin, promotion latch, overflow ladder, reachability, capacity header, regional vs multi-region host split) and gains tools/toolConfig translation plus :predict embeddings; validator accepts file parts, json_object, and the extra_body-style google options field. - backend: chat_file streams/sync completions through the file-chat lanes (OpenAI Files upload/download stays direct by design); the OpenAI embeddings proxy and gemini_embed_query hop the gateway lanes with BYOK kept on documented thin direct paths; desktop_proxy stays the BFF (auth/metering/limits) and translates Gemini JSON via utils/llm/desktop_gemini_gateway.py, keeping BYOK and batchEmbedContents direct. - FEATURE_MODE=off keeps every legacy direct path (kill switch unchanged); inventory + guardrails updated; omni WS stays blocked. * refactor(backend): split vertex wire/policy and desktop gateway hop out of the ratcheted files providers.py and desktop_proxy.py grew past the product line-count ratchet; the vertex adapter's pure wire translation (vertex_wire.py), the PT policy mixin (vertex_pt_policy.py), shared provider types (provider_types.py), and the desktop BFF's gateway hop + Gemini body sanitization (utils/llm/desktop_gemini_gateway.py) move into focused modules with the original call sites and test seams preserved via re-exports. * fix(backend): keep Vertex token refresh on wall clock and attribute file-chat spend The gateway Vertex provider was sharing the PT monotonic clock with ADC token expiry, so tokens never refreshed after the first fetch. File-chat hops also omitted the user uid, leaving ledger rows unattributed, and the desktop tool-loop minted a new tool_call_id after the ordinal advanced. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai> * fix(backend): restore embeddings BYOK async fallback and temporal test stubs Direct-path aembed_query/aembed_documents now notify and fall back to the Omi key on BYOK 401 like the sync methods. Hermetic app-integration harnesses stub utils.llm.temporal so collection survives the main merge that imports current_date_for_uid. Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai> * fix(backend): pin file-chat documents to gpt-5.6-luna Live probe confirmed Luna accepts the Chat Completions file-part contract, so the documents lane leaves gpt-4.1. Keep the separate file-chat-documents lane because the request shape still differs. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
Review follow-ups: the per-call-site voice strings are how the lanes drifted apart (marin), so both session builders now read RealtimeHubVoicePolicy.voiceName(for:) - Charon for Gemini, cedar for OpenAI - and two tests pin the pairing, including that a failover's alternate provider resolves to cedar. The stale 'closest match to marin' comment is corrected and the changelog claim softened to config-level voice identity (audible similarity is not machine-auditable and is not claimed). Verification: swift test RealtimeHubVoicePolicyTests 2 passed; on the built bundle, gemini primary ready -> production failover -> openai gpt-realtime-2 ready with the policy voice accepted.
The policy lookup tests stayed green if a session builder hardcoded a voice again, so the voice-bearing fragments the builders embed are now production seams (openAIOutputAudioConfig / geminiSpeechConfig) and the tests assert the payloads a real session is configured with - reintroducing a per-call-site voice string fails them. Verification: swift build clean; 4 voice tests pass.
Auto-generated: consolidates unreleased changelog fragments into v0.12.240 and regenerates CHANGELOG.json.
…Rewind Snapshot download only ran inside JITProactivityRuntime.admission, which requires a notify-worthy context visit from screen capture. Signed-in admitted startups now run the same flag -> fetch -> reconcile chain once (owner change re-runs it via the admission token task), so receipts stop depending on capture being live. Fail-closed gate preserved; empty watchlists still persist receipts; mirror sync stays non-blocking. Co-authored-by: multica-agent <github@multica.ai>
…Rewind (BasedHardware#12384) Failure-Class: new ## What changed and why The macOS client only downloaded the JIT trigger snapshot inside `JITProactivityRuntime.admission`, which is reached exclusively through `JITProactivityCoordinator.handle` after a **notify-worthy context visit** from `ContextProactivityEngine`. Live evidence on the allowlisted account after BasedHardware#12381 shipped in Omi Beta 0.12.240 / 12240: `jit_trigger_snapshot_receipts` = 0, UA `Omi Beta/12240` still hits `GET /v1/jit/knowledge-ledger/prompt-snapshot` but never `rollout-decision` or `trigger-snapshot` — because Sparkle replaced the signed binary and the TCC Screen Recording grant likely dropped, so the director never evaluated and the client fix could never run. Snapshot sync must not depend on screen capture. - `JITProactivityRuntime.syncTriggerSnapshot(authorizationSnapshot:)` runs the exact admission chain — `jitProactivityFlags` → guard `permitsNewLane` → `fetchJITTriggerSnapshot` → `JITTriggerMirror.reconcile` — with no observation, no evaluation, no delivery. The fail-closed gate is unchanged: unknown/disabled still performs no snapshot read. - `DesktopHomeSignedInStartup.runProductServicesIfAdmitted` fires it once after `isProductShellAdmitted`, as a detached task so a slow authority route never gates product startup. The `.task(id: productShellAdmissionToken)` restart supplies the cheap owner-change retry (the token includes `boundOwnerID`). No new timers or loops. - A complete **empty** snapshot still persists its receipt (`rowCount` 0), and the BasedHardware#12381 contract is preserved: the downstream ledger-mirror sync inside `fetchJITTriggerSnapshot` stays non-blocking for the receipt. ## Product invariants affected - INV-CUTOVER-1 The hook stays strictly behind the existing `isProductShellAdmitted` fence and invents no parallel cohort/generation primitive; it is a read-only, owner-bound snapshot reconcile on the already-admitted path. ## Testing - `JITProactivityRuntimeTests.testStartupSyncFetchesSnapshotAndPersistsEmptyWatchlistReceiptWithoutAContextVisit` — effective=enabled startup fetches the snapshot and writes a receipt with zero context visits and an empty watchlist. - `JITProactivityRuntimeTests.testStartupSyncFailsClosedWhenAuthorityDoesNotPermitNewLane` — unknown, rollout-disabled, kill-switch, and `effective=disabled` skip the snapshot read and write no receipt. - `JITProactivityRuntimeTests.testStartupSyncSwallowsSnapshotFailureWithoutPersistingAReceipt` — one shot, no loop: an unavailable snapshot is swallowed content-free. - `ProactiveLaneClientTests.testStartupSnapshotSyncIssuesRolloutThenSnapshotGETAndWritesReceipt` — wire-level: the runtime startup sync drives the real client routes in order (`/v1/jit/rollout-decision` then `/v1/jit/trigger-snapshot`) and persists the empty-watchlist receipt. - `ProactiveLaneClientTests.testStartupSnapshotSyncWithEffectiveDisabledNeverIssuesSnapshotGET` — wire-level: `effective=disabled` reads only the rollout decision. - Existing BasedHardware#12381 wire/runtime suites (`ProactiveLaneClientTests`, `JITProactivityRuntimeTests`, `JITTriggerMirrorTests`, `JITProactivityPolicyTests`, `JITProactivityDeliveryTests`, `FloatingBarLaunchPolicyTests`) all pass unchanged. Ambient proactive work still needs screen capture; this change only removes that dependency from the snapshot/receipt sync. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/BasedHardware/omi/pull/12384?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
Auto-generated: consolidates unreleased changelog fragments into v0.12.241 and regenerates CHANGELOG.json.
- database/vector_db.py: bulk-fetch action item embeddings from Pinecone - routers/action_items.py: add created_at + conversation_id fields to Create/Update request models (for import flows) - routers/action_items_cleanup.py: new cleanup router - utils/action_item_cleanup.py: cleanup utility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ase images main.py: register action_items_cleanup router diarizer/Dockerfile, modal/Dockerfile: switch base image from private gcr.io/based-hardware-dev/python:3.11-slim-forky to public python:3.11-slim-bookworm (Debian 12) + matching CUDA 13.2.1 repo URLs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire the backend /v1/action-items/cleanup/preview + execute endpoints into a modal in Settings → Advanced → Task maintenance. Flow: strategy selection (stale/overdue/vague on by default; slow AI strategies opt-in with a warning) → server-side analysis → preview with breakdown + samples → one-click delete → tasksReconcile(). Rate limiting is handled entirely server-side; the client makes two HTTP calls regardless of task count. 3-minute timeout covers LLM strategies over large accounts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
…ntend client 36 backend tests cover _is_vague patterns, stale-age/overdue/vague/merge_candidates logic, preview session staging, breakdown shape, sample capping, 410 on expired session, and deletion delegation. 5 frontend tests cover taskCleanupPreview/Execute endpoint wiring and the 180-second timeout override for LLM strategies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
ThreadPoolExecutor(max_workers=0) raises ValueError. Return an empty CleanupPreviewResponse immediately when no strategies are selected rather than crashing. Surfaced by the router test suite. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
- Drop unused json/datetime/timezone imports from action_items_cleanup.py (reportUnusedImport errors in CI) - Replace bare type annotations on chain.invoke() results with cast() at both LLM call sites in action_item_cleanup.py; pyright cannot narrow the return type of with_structured_output() invocations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
The two new action-items cleanup routes (preview + execute) fall under the /v1/action-items prefix and are automatically included in the app-client surface. Regenerated docs/api-reference/app-client-openapi.json to satisfy the Public Developer API contract CI check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
POST /v1/action-items/cleanup/preview and /execute are firebase_id_token authenticated first-party app routes in the action_items data domain. Required by the backend-route-policy-baseline CI check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
black==26.5.1 --line-length 120 --skip-string-normalization Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
The vague-cleanup strategy's _DANGLING_PATTERN treated "the <noun>" as an
unresolved reference alongside pronouns like "it"/"them"/"that", so
default-on cleanup flagged ordinary tasks ("Clean the kitchen", "Fix the
sink", "Change the oil") for deletion. Restrict the dangling-reference
match to actual pronouns/demonstratives.
Verified: reproduced the false positives against the old pattern, then
confirmed backend/tests/unit/test_action_item_cleanup_strategies.py and
test_action_items_cleanup_router.py (42 tests) pass with the fix.
Failure-Class: FC-regex-overmatch | new
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The preview endpoint already staged every candidate's id, strategy, and description server-side, but execute only accepted a session_id and always deleted the full staged set — no way to keep an individual task. Add description to CleanupCandidateMeta (so the UI can render a full reviewable list, not just the 5-per-strategy sample) and excluded_ids to CleanupExecuteRequest, filtered out of the staged ids before deletion. Verified: backend/tests/unit/test_action_items_cleanup_router.py (12 tests) and test_action_item_cleanup_strategies.py pass; black --check clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The preview screen showed 5 samples per strategy behind a single "Delete N tasks" button — a leftover false positive (e.g. from a vague-strategy edge case) had no way to be excluded before deletion. Show every staged candidate with a checkbox (default checked), a select-all/deselect-all pair, and a live count; unchecked ids are passed through as excluded_ids on execute. Verified: pnpm vitest run on TaskCleanupModal.test.tsx and taskCleanup.test.ts (8 tests) pass; typecheck:web and typecheck:node clean; eslint clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…anup exclusion fields CleanupCandidateMeta.description and CleanupExecuteRequest.excluded_ids added in the prior commit. Dart and Swift generators produced no diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both routes declared rate_limit: none in the manifest despite preview fanning out to ~10 concurrent conv_discard LLM calls per click (two ThreadPoolExecutor(max_workers=5) pools across llm_relevance and conversation_context) over up to 2000 tasks, repeatable with no cooldown, and execute being an irreversible batch delete. Wire the existing with_rate_limit(uid, policy) wrapper (same mechanism as memories:delete_batch / knowledge_graph:canonical) with new policies: action_items:cleanup_preview (15/hour) and action_items:cleanup_execute (10/hour, matching memories:delete_batch's order of magnitude for a destructive batch op). Updated the manifest entries from rate_limit: none to the new policy/uid/wrapper wiring. Verified: backend/tests/unit/test_action_items_cleanup_router.py (17 tests, incl. new policy-existence + source-level wiring tests following the test_memories_create.py convention) and test_rate_limiting.py (26 tests) pass; black --check clean; route_policy_inventory.py --check shows no new manifest-schema issues for these two routes (the 473 missing-entry count is pre-existing unrelated legacy debt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…files An earlier commit on this branch (06cedf8) swapped gcr.io/based-hardware-dev/python:3.11-slim-forky for the public python:3.11-slim-bookworm in these two Dockerfiles, unrelated to the action-items cleanup feature this PR is about. Confirmed with the author: it was a local workaround for lacking access to the private image during dev, not an intentional infra change — these two images back deployed diarizer/modal workloads in production, so the swap doesn't belong riding along with an unrelated feature PR. Verified: scripts/runtime_image_contracts.py check passes (10 registered images) — no runtime_images.json entries reference the base image tag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_action_items() hard-caps at 2000 open tasks per query (_ACTION_ITEMS_LIST_HARD_MAX, guarding a real prod 504 incident) but the cleanup preview strategies all read through it with no pagination, and nothing in the response said so. An account with tens of thousands of stale tasks — the PR's own stated target — got a silent partial scan: click "Delete 800 tasks", believe you're done, when 40,000+ were never even considered. Add get_open_action_items_count() (Firestore count() aggregation, no document reads, no cap — same pattern as get_action_items_count_by_conversation) and a public get_action_items_list_scan_cap() accessor. cleanup_preview now computes the true open-task count once and returns total_open_action_items, scan_cap, and scan_truncated on every response (including the empty-strategies short-circuit), instead of raising the cap or adding a pagination loop that would risk reintroducing the timeout the cap exists to prevent. Verified: backend/tests/unit/test_open_action_items_count.py (4 new, count() arithmetic + soft-delete exclusion, mirroring test_conversation_action_items_count.py) and test_action_items_cleanup_router.py (3 new truncation tests, 20 total) pass; test_bounded_firestore_list_reads.py (existing hard-cap coverage) still passes unchanged; black --check clean; route_policy_inventory.py --check shows no new issues for these routes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ncated Surfaces the backend's new total_open_action_items/scan_cap/scan_truncated preview fields: when scan_truncated is true, an amber notice says how many of the account's open tasks were actually scanned vs. how many exist in total, and to run cleanup again to reach the rest. Verified: pnpm vitest run on TaskCleanupModal.test.tsx (2 new tests) and taskCleanup.test.ts (10 tests total) pass; typecheck:web clean; eslint clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n-truncation fields CleanupPreviewResponse.total_open_action_items/scan_cap/scan_truncated added in the prior commit. Dart and Swift generators produced no diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…es_v1.json action_items_cleanup.py calls action_items_db.delete_action_items_batch but was not listed in task_intelligence_sources_v1.json, causing test_task_intelligence_contract_freeze.py to fail. Added the router to mobile_manual's owner_paths and writer_anchors — the same source that already owns action_items.py and the same delete_action_items_batch symbol. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rebase onto current main and fix the four review blockers: filter locked tasks, atomic Redis session claim with terminal execute results, registered executors instead of nested ThreadPoolExecutors, and oldest-first scan cursors carried across preview runs. Windows passes next_scan_cursor on follow-up analyzes. Verified: pytest tests/unit/test_action_item_cleanup_strategies.py tests/unit/test_action_items_cleanup_router.py tests/unit/test_open_action_items_count.py (60 passed) Failure-Class: FC-cleanup-session-race | new Co-authored-by: Cursor <cursoragent@cursor.com>
…ursor Adds CleanupPreviewRequest.scan_cursor and CleanupPreviewResponse.next_scan_cursor to the app-client OpenAPI surface and all generated Swift/TS clients. Verified: PYTHONUTF8=1 pytest tests/unit/test_app_client_swift_generator.py tests/unit/test_app_client_ts_generator.py (15 passed) Co-authored-by: Cursor <cursoragent@cursor.com>
formed2forge
force-pushed
the
worktree-feat-action-items-cleanup
branch
from
August 29, 2026 16:05
35597f6 to
9a104e2
Compare
Type _run_llm_batches with TypeVar so conversation_context can pass (cid, batch) tuples while llm_relevance still passes list[dict] batches. Verified: pyright utils/action_item_cleanup.py (0 errors); pytest test_action_item_cleanup_strategies.py test_action_items_cleanup_router.py (56 passed). Co-authored-by: Cursor <cursoragent@cursor.com>
…nt ratchet Extract action-item cleanup scan helpers and Redis path-keyed cache ops into dedicated modules so action_items.py and redis_db.py stay under the 1500-line product-file threshold without PR-body exceptions. Verified: pytest test_open_action_items_count.py test_action_item_cleanup_* (60 passed). Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POST /v1/action-items/cleanup/preview(runs six cleanup strategies in a thread pool, stages candidates in Redis for 5 min) andPOST /v1/action-items/cleanup/execute(deletes staged candidates, purges vectors, sends client notification). Strategies:stale_age,overdue,semantic_dedup,llm_relevance,conversation_context,vague.fetch_action_item_vectorsadded todatabase/vector_db.pyfor bulk Pinecone fetches (used by semantic dedup).stale_age,overdue,vague) are on by default; slow LLM/vector strategies show an amber warning and are off by default. 180-second axios timeout covers worst-case LLM runs. 410 on session expiry routes back to config with a toast._is_vague, stale-age/overdue/vague/merge logic, router preview + execute contracts) + 5 frontend tests (endpoint wiring, 180 s timeout).Why this is needed
Account accumulates ~35 000 tasks. No existing path to bulk-remove stale, overdue, duplicate, or vague entries. All heavy processing is server-side so the client makes exactly 2 HTTP calls (preview then execute) regardless of account size.
Test plan
backend/tests/unit/test_action_item_cleanup_strategies.py— 26 tests, all passbackend/tests/unit/test_action_items_cleanup_router.py— 10 tests, all passdesktop/windows/src/renderer/src/lib/taskCleanup.test.ts— 5 tests, all passPOST /v1/action-items/cleanup/preview+/executeagainst a real accountKnown edge
Passing
strategies=[]to the router crashesThreadPoolExecutor(max_workers=0)— pre-existing, frontend never sends an empty list. Tracked separately.🤖 Generated with Claude Code
https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p