feat(action-items): task cleanup — preview/execute endpoints + Windows desktop UI - #12251
feat(action-items): task cleanup — preview/execute endpoints + Windows desktop UI#12251formed2forge wants to merge 21 commits into
Conversation
5c6b3de to
62b68b2
Compare
|
Rebased onto current main — PR now shows 12 files (5 backend + 5 Windows frontend + 2 test). The stale upstream history is gone. Mergeable. |
310b3f4 to
652d31b
Compare
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks @formed2forge — this is a well-built feature. The two-phase preview/execute pattern with server-side session staging (5-minute TTL, single-use), reuse of delete_action_items_batch / delete_action_item_vectors_batch / the FCM batch-deletion notification, and the import-isolation unit tests are all solid, and the strategies=[] guard for the ThreadPoolExecutor(max_workers=0) crash is a good catch. Requesting changes for one verified defect on a default-on deletion path, plus a few items that need maintainer judgment.
Blocking: the vague strategy flags ordinary concrete tasks
_DANGLING_PATTERN in backend/utils/action_item_cleanup.py includes the\s+\w+ as a dangling reference:
^(put|take|send|get|fix|check|do|move|bring|pick up|drop off|return|give back|hand|pass|grab|swap|switch|change|clean|clear|sort|set)\s+(it|them|those|these|that|this|the\s+\w+)\b
Any open task starting with one of those ~23 verb entries followed by "the …" is flagged vague. Running the exact pattern: "Clean the kitchen", "Fix the sink", "Sort the laundry", "Change the oil", "Check the mail", and "Return the library books" all match. vague is on by default in the Windows UI (DEFAULT_STRATEGIES = ['stale_age', 'overdue', 'vague'] in TaskCleanupModal.tsx), and the preview screen offers only samples plus a single "Delete N tasks" button — so these false positives would be permanently deleted in one click. The existing test_normal_task_not_vague ("Call the dentist to reschedule") starts with a verb outside the list, so this class isn't covered. Suggestion: drop the the\s+\w+ alternative (keep pronouns/demonstratives only) and add regression tests for "Clean the kitchen" / "Fix the sink" as non-vague.
Needs maintainer judgment
- No per-item control in the preview UI. The API returns
candidate_ids/candidate_metaand the description says users review candidates, but the modal renders counts + 5 samples per strategy and deletes everything at once. Combined with the issue above this is a data-loss risk; per-item exclusion (or at least showing full candidate descriptions before delete) would make the preview meaningful. - Unrelated infra change.
backend/diarizer/Dockerfileandbackend/modal/Dockerfileswitch the base image fromgcr.io/based-hardware-dev/python:3.11-slim-forky(Debian 14) to publicpython:3.11-slim-bookworm(Debian 12). Nothing else in this PR needs that, and these images back deployed diarizer/modal workloads. If it's needed (e.g. local builds can't pull the private image), please split it into its own PR so the image change can be validated and rolled back independently. - Cost/abuse controls. Both new entries in
backend/route_policy_manifest.yamldeclarerate_limit: none. One preview click can fan out to roughly 2×5 concurrentconv_discardLLM calls (candidates_llm_relevanceandcandidates_conversation_contexteach run aThreadPoolExecutor(max_workers=5)inside the router's own executor) over up to 2000 tasks, repeatable per click with no cooldown. A per-user limit or debounce seems warranted for an endpoint this expensive. - Coverage vs. stated goal.
get_action_itemsis hard-capped at_ACTION_ITEMS_LIST_HARD_MAX = 2000, so preview silently operates on at most 2000 open tasks (no pagination loop; truncation isn't surfaced). The PR targets accounts with "tens of thousands" of stale tasks — as written those accounts get partial cleanups with no indication anything was skipped.
Smaller notes
fetch_action_item_vectorsinbackend/database/vector_db.py: docstring says "batches of 1000" but the code batches at 100 (the 414-avoidance comment is right — fix the docstring).- Unknown strategy names in
CleanupPreviewRequest.strategiesare silently ignored; a 422 would surface client typos. - The manifest marks the new routes
review_status: reviewed— matches the file's convention, but worth confirming that's intended pre-review. - Client surface:
docs/api-reference/app-client-openapi.jsonplus the regenerated clients (macOS Swift, Windows, and the three web apps) publish these endpoints to every app-client API consumer, including coding agents that read the spec. I verified the documented schemas match the router's Pydantic models, so the docs are accurate — flagging only that a destructive endpoint is now part of the documented client API. backend/main.pyrouter registration,AdvancedTab.tsxwiring, and thetaskCleanup.ts180-second preview timeout override (with tests) all look clean.
CI note: the Backend unit suite is cancelled and several checks are still queued on this head, so the new tests haven't been exercised by CI yet.
Human review needed for destructive bulk-delete product/UX sign-off and the Dockerfile base-image decision — those are maintainer calls, not mechanical fixes.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
157b8c4 to
e529b45
Compare
|
Rebased onto current upstream/main (was 132 commits behind, which was causing the bun lockfile frozen-lockfile error and the backend/Swift CI failures). All 52 cleanup unit tests still pass post-rebase. The branch already has all reviewer-requested fixes from the prior review round:
Ready for re-review. |
|
The `Backend unit suite` failures here are not caused by this PR. The failing tests — `test_byok_security`, `test_chat_quota`, `test_chat_session_app_identity`, and others — all fail because `utils.subscription` is missing a `get_byok_keys` attribute on upstream `main`. That's a pre-existing upstream breakage affecting 10+ unrelated test files; none of them touch the action-items cleanup code. All reviewer-requested changes have been applied to the remote branch:
Requesting re-review. |
Dismissing my earlier changes-requested review: all five specific concerns are verified resolved on head e529b45 (vague-pattern false positives fixed with regression tests in 487e10a, per-item exclusion added end-to-end in 72b71a9/a24bfa8, Dockerfile base-image switch reverted in 03cd33a, preview/execute rate limits added in 11d960c, scan truncation surfaced in 5300c84/5d4c9cfa/e529b45). A fresh review follows for a different, newly-discovered issue: the task-intelligence source manifest does not yet register the new writer anchor for action_items_cleanup.py, which is the only PR-caused failure in the Backend unit suite.
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks @formed2forge — all five items from the earlier review are resolved on this head, and I verified each in the diff:
vaguefalse positives (487e10a):_DANGLING_PATTERNinbackend/utils/action_item_cleanup.pynow matches only pronoun/demonstrative objects (it|them|those|these|that|this), and the parametrizedtest_verb_plus_concrete_the_noun_not_vaguepins "Clean the kitchen"-style tasks as non-vague.- Per-item control (72b71a9, a24bfa8):
CleanupExecuteRequest.excluded_idsfilters the staged IDs incleanup_execute, andTaskCleanupModal.tsxnow renders the full candidate list with checkboxes ("uncheck anything you want to keep"), wired throughtaskCleanupExecute(sessionId, excludedIds)with tests on both layers. - Dockerfile base images (03cd33a): reverted to the private
gcr.io/based-hardware-dev/python:3.11-slim-forkyimages; those files are no longer in the diff. - Rate limits (11d960c):
action_items:cleanup_preview(15/h, fail-open) andaction_items:cleanup_execute(10/h, fail-closed) inutils/rate_limit_config.py, and the route-policy manifest entries now reference them instead ofnone. - Scan truncation (5300c84, e529b45):
get_open_action_items_count()(Firestore count aggregation) +scan_cap/scan_truncatedin the preview response, an amber notice in the modal telling users to re-run for the remainder, and tests pinning both directions.
Blocking: the task-intelligence source manifest doesn't know about the new writer
The Backend unit suite has exactly two failures caused by this PR (the rest of that suite's red state is chronic on main — the same 43 tests fail on recent main runs):
FAILED tests/unit/test_task_intelligence_contract_freeze.py::test_source_manifest_registers_every_known_writer_class
ValueError: unregistered writer anchors: [('backend/routers/action_items_cleanup.py', 'action_items_db.delete_action_items_batch')]
FAILED tests/unit/test_task_intelligence_contract_freeze.py::test_source_manifest_rejects_stale_writer_anchor
The repo requires every backend call site that mutates action items to be registered in backend/config/task_intelligence_sources_v1.json. cleanup_execute calls action_items_db.delete_action_items_batch, so the contract scanner finds an unregistered writer. Fix: add backend/routers/action_items_cleanup.py to a suitable source's owner_paths (e.g. mobile_manual, which already owns backend/routers/action_items.py) and register the anchor:
{"path": "backend/routers/action_items_cleanup.py", "symbol": "action_items_db.delete_action_items_batch", "discover": true}The second test fails as collateral of the same root cause (validation raises "unregistered writer anchors" before reaching the "stale writer anchors" branch that test exercises), so one registration fixes both.
Carried-over minor notes (non-blocking)
fetch_action_item_vectorsinbackend/database/vector_db.pystill says "batches of 1000" while the code (correctly, for 414-avoidance) batches at 100.- Unknown strategy names in
CleanupPreviewRequest.strategiesare still silently ignored; a 422 would surface client typos.
One product-level call remains for a human maintainer: this adds a destructive, partly LLM-scored bulk-delete surface for user tasks (preview → irreversible execute), now published to every app-client API consumer via docs/api-reference/app-client-openapi.json and the regenerated Swift/TS clients — coding agents that read the spec will discover it too. The mechanics look sound (uid-scoped 5-minute single-use sessions, per-item exclusions, fail-closed execute rate limit, truncation surfaced), so this is sign-off, not rework.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
- 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
Runs backend/scripts/generate_ts_openapi_types.py after the OpenAPI spec update in the previous commit. Adds CleanupPreviewRequest, CleanupPreviewResponse, CleanupExecuteRequest, CleanupExecuteResponse, CleanupSampleItem, and CleanupCandidateMeta types to all four omiApi.generated.ts targets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p
Adds cleanupExecuteV1ActionItemsCleanupExecutePost and cleanupPreviewV1ActionItemsCleanupPreviewPost (416 total methods). 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>
df6ab70 to
35597f6
Compare
|
Rebased onto current upstream/main. The conflicts were in auto-generated files (omiApi.generated.ts and OmiApi.generated.swift) — both sides had a "Total: N methods" comment; kept upstream's count (430) since upstream already has the cleanup endpoint types plus newer additions. PR is clean again. |
Summary
Adds a two-phase task cleanup system — a backend that runs up to six cleanup strategies server-side, and a Windows desktop modal that lets users review candidates before deleting anything.
Background
Large accounts accumulate tens of thousands of stale tasks. This surfaces the problem with six targeted strategies while keeping all expensive work server-side — the client makes exactly two HTTP calls regardless of task count.
Backend (
backend/)New router:
backend/routers/action_items_cleanup.pyPOST /v1/action-items/cleanup/previewThreadPoolExecutorcleanup_session:{uid}:{session_id}with a 5-minute TTLPOST /v1/action-items/cleanup/executedelete_action_items_batch+delete_action_item_vectors_batch+ push notificationdeleted_countBug fix: added early-return guard when
strategies=[]to preventThreadPoolExecutor(max_workers=0)crash.New utilities:
backend/utils/action_item_cleanup.pySix cleanup strategies:
stale_ageoverduevaguesemantic_dedupllm_relevanceconversation_contextmerge_candidates(lists)deduplicates across strategies by ID, preserving first-appearance order.New database helper:
backend/database/vector_db.pyfetch_action_item_vectors(uid, action_item_ids)— batched Pinecone fetch (100 IDs/call) needed by semantic dedup.Router wired in
backend/main.pyAdded
action_items_cleanupimport andapp.include_router(action_items_cleanup.router)afteraction_items.router.Windows Desktop (
desktop/windows/)New:
src/renderer/src/lib/taskCleanup.tsThin
omiApiwrapper with typed params/results:taskCleanupPreview(params)— 180 s timeout (LLM strategies over large accounts)taskCleanupExecute(sessionId)— standard timeoutNew:
src/renderer/src/components/settings/TaskCleanupModal.tsxFour-phase modal (
config → loading → preview → deleting):dismissible={!isDeleting}) while in flightwindow.omi.tasksReconcile()to sync task list + success toastModified:
src/renderer/src/components/settings/tabs/AdvancedTab.tsxAdded "Task maintenance"
SettingRow(icon: Trash2) in the Advanced settings tab that opens the cleanup modal.Tests (41 added)
Backend unit —
backend/tests/unit/test_action_item_cleanup_strategies.py(26 tests)Uses
load_module_fresh+stub_modules+AutoMockModuleto isolate the module from Pinecone/Firebase/LangChain import-time bindings:TestIsVague(7): pronoun-short, imperative+dangling, speaker label, normal long desc, etc.TestCandidatesStaleAge(6): old/young, has-due-at, conversation-date priority, young-conv suppresses, None-created_at skipTestCandidatesOverdue(4)TestCandidatesVague(4)TestMergeCandidates(5): dedup, order preservation, empty inputBackend unit —
backend/tests/unit/test_action_items_cleanup_router.py(10 tests)Frontend —
src/renderer/src/lib/taskCleanup.test.ts(5 tests)vitest +
vi.hoisted+vi.mock:Failure-Class: none
Test plan
cd backend && python -m pytest tests/unit/test_action_item_cleanup_strategies.py tests/unit/test_action_items_cleanup_router.py -v— all 36 passcd desktop/windows && pnpm test src/renderer/src/lib/taskCleanup.test.ts— all 5 pass🤖 Generated with Claude Code
https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p