Skip to content

feat(action-items): task cleanup — backend endpoints + Windows UI - #4

Open
formed2forge wants to merge 107 commits into
mainfrom
worktree-feat-action-items-cleanup
Open

feat(action-items): task cleanup — backend endpoints + Windows UI#4
formed2forge wants to merge 107 commits into
mainfrom
worktree-feat-action-items-cleanup

Conversation

@formed2forge

@formed2forge formed2forge commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • Backend: two new endpoints — POST /v1/action-items/cleanup/preview (runs six cleanup strategies in a thread pool, stages candidates in Redis for 5 min) and POST /v1/action-items/cleanup/execute (deletes staged candidates, purges vectors, sends client notification). Strategies: stale_age, overdue, semantic_dedup, llm_relevance, conversation_context, vague.
  • Backend: fetch_action_item_vectors added to database/vector_db.py for bulk Pinecone fetches (used by semantic dedup).
  • Windows desktop: Settings → Advanced → "Task maintenance" row opens a three-phase modal (config → analyze → preview/delete). Fast strategies (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.
  • Tests: 36 backend unit tests (_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 pass
  • backend/tests/unit/test_action_items_cleanup_router.py — 10 tests, all pass
  • desktop/windows/src/renderer/src/lib/taskCleanup.test.ts — 5 tests, all pass
  • Manual: open Settings → Advanced, click "Clean up tasks…", run fast strategies, confirm preview shows counts + sample, confirm delete succeeds and task list refreshes
  • Backend: POST /v1/action-items/cleanup/preview + /execute against a real account

Known edge

Passing strategies=[] to the router crashes ThreadPoolExecutor(max_workers=0) — pre-existing, frontend never sends an empty list. Tracked separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XCp5LUrL4FLcaLUkdDg49p

@formed2forge
formed2forge force-pushed the worktree-feat-action-items-cleanup branch 2 times, most recently from 310b3f4 to 652d31b Compare August 26, 2026 15:02
@formed2forge
formed2forge force-pushed the worktree-feat-action-items-cleanup branch 2 times, most recently from df6ab70 to 35597f6 Compare August 27, 2026 19:19
kodjima33 and others added 25 commits August 27, 2026 21:18
…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.
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.
github-actions Bot and others added 27 commits August 29, 2026 09:20
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
formed2forge force-pushed the worktree-feat-action-items-cleanup branch from 35597f6 to 9a104e2 Compare August 29, 2026 16:05
formed2forge and others added 2 commits August 29, 2026 12:49
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>
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.

5 participants