Skip to content

Fix/web app duplicate task title - #12266

Open
Yny4ii wants to merge 2 commits into
BasedHardware:mainfrom
Yny4ii:fix/web-app-duplicate-task-title
Open

Fix/web app duplicate task title#12266
Yny4ii wants to merge 2 commits into
BasedHardware:mainfrom
Yny4ii:fix/web-app-duplicate-task-title

Conversation

@Yny4ii

@Yny4ii Yny4ii commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What changed and why

POST /v1/action-items hashed the task title as an idempotency key, so creating a second task named like an existing one (123) returned the original document (same due date, gone after reload). Create is now idempotent only when the client sends Idempotency-Key; the web app sends a per-submit UUID, and the proxy forwards it.

Product invariants affected

none

How it was verified

  • BACKEND_UNIT_TEST_FILE_LIST=tests/unit/test_action_item_idempotency.py BACKEND_PYTEST_XDIST=0 ./test.sh → 11 passed
  • NODE_OPTIONS=--no-experimental-webstorage bunx vitest run --config vitest.config.mts src/lib/__tests__/actionItemList.test.ts → 2 passed
  • Did not exercise live /tasks against this backend: production still hashes titles until this change is deployed. Could not confirm two same-title tasks persist after reload on a real account.

Tests

Regression: backend/tests/unit/test_action_item_idempotency.py::test_router_does_not_hash_description_as_idempotency_key — two POSTs with the same description and no header must both insert.

Web defensive list merge: web/app/src/lib/__tests__/actionItemList.test.ts — if the API still returns an existing id, the list replaces that row instead of prepending a ghost duplicate.

Failure class (fixes)

Failure-Class: none

Review in cubic

Yny4ii and others added 2 commits August 26, 2026 18:42
POST /v1/action-items hashed the description as an idempotency key, so a
second task with the same title returned the original document (wrong due
date, gone after reload). Honor an optional Idempotency-Key instead;
titles are not unique.

Failure-Class: none
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @Yny4ii — this is a clean fix for a real correctness bug, and the verification section is exactly how I wish every PR reported testing.

What I verified

  • backend/routers/action_items.py_content_idempotency_key() (the sha256 over uid + normalized title from fix(action_items): content-hash idempotency on POST /v1/action-items #7093) is replaced by _client_idempotency_key(), which only strips/blank-checks a caller-supplied Idempotency-Key header (Annotated[Optional[str], Header(alias='Idempotency-Key', max_length=256)]; blank → None → always insert). The lookup stays scoped to users/{uid}/action-items with equality on the stored idempotency_key field, so there is no cross-user surface.
  • backend/database/action_items.py — docstring-only change; the db-layer opaque-key support already existed.
  • backend/tests/unit/test_action_item_idempotency.pytest_router_does_not_hash_description_as_idempotency_key (two POSTs, same description, no header → both insert) pins exactly the bug being fixed.
  • web/app/src/lib/api.tscreateActionItem sends Idempotency-Key: crypto.randomUUID() per submit, and fetchWithAuth merges caller headers after the defaults, so the key survives.
  • web/app/src/app/api/proxy/[...path]/route.ts — forwards Idempotency-Key the same way as the existing FCM headers.
  • web/app/src/lib/actionItemList.ts + __tests__/actionItemList.test.tsprependOrReplaceById is a good defensive touch: while production still runs the old hash behavior, a replayed id replaces the row instead of prepending a ghost duplicate that vanishes on reload.
  • web/app/src/hooks/useActionItems.tsaddItem now routes through that helper. Note the +226/−194 here is almost all arrow-paren reformatting; the functional change is one line.
  • docs/api-reference/app-client-openapi.json and the regenerated clients (macOS Swift, Windows TS, web app, admin, personas) expose the optional header consistently.

One decision for maintainers

Removing the server-side title-hash fallback changes the contract for every client, and only the web app sends the key today:

  • macOS: APIClient+TaskCatalog.createActionItem posts to v1/action-items with no Idempotency-Key (the taskIntelligenceMutation helper used for work-intents/checkpoints does send one, but plain task creates don't).
  • Flutter: app/lib/backend/http/api/action_items.dart createActionItem sends no key.

The hash was originally added (#7093) to stop flaky-network retry duplicates from the desktop client, so until desktop/Flutter adopt the header, a network-level retry on those clients can again produce a duplicate. On net this is still the right trade — current behavior silently returns the wrong task on same-titled creates — but whether to merge ahead of client adoption or together with it is a maintainer sequencing call. The regenerated clients already support the header, so a follow-up on the desktop side is straightforward.

Minor, non-blocking: the db idempotency lookup filters completed == False (pre-existing, unchanged here), so a retry that lands after the original task was completed will insert a new one. Worth knowing; not this PR's bug.

CI note

The failing checks are not from this branch: web-app-checks fails on a frozen bun.lock drift introduced on main (prettier was added to web/app/package.json without a lockfile update — other PRs fail identically), the Desktop Swift jobs fail with the same format-drift/sentinel signature seen on maintainer PRs today, and the backend Build failure is a pre-existing pyright unused-import in backend/utils/subscription.py that also fails on main.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added the needs-maintainer-review Needs a human maintainer to sign off before merge label Aug 26, 2026

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Real bug (title-hash idempotency collision) and clear fix, but diff exceeds the confidence-gate size ceiling and CI has multiple failing required checks, so approve-only per policy.

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Follow-up on my earlier review — the head is unchanged (c8c3231), and @kodjima33's approval settles the one open sequencing call (merging ahead of desktop/Flutter adopting the Idempotency-Key header — the regenerated clients already support it, so follow-ups there are easy).

State check since that review:

  • The core change still checks out: backend/routers/action_items.py _client_idempotency_key() (optional header, max_length=256, blank → always-insert) replacing the title-hash from fix(action_items): content-hash idempotency on POST /v1/action-items #7093, with the web wiring intact — per-submit crypto.randomUUID() in web/app/src/lib/api.ts, forwarding in web/app/src/app/api/proxy/[...path]/route.ts, and the prependOrReplaceById merge from web/app/src/lib/actionItemList.ts wired into addItem.
  • The five red checks all predate the current base, not this diff: PR Metadata Preflight / Build fail on the frozen web/app bun.lock drift, which main fixed in fix(web): add the pinned prettier deps to web/app's lockfile #12288 after these checks ran; the Desktop Swift jobs fail on format-lint/sentinel errors and an AppState+Transcription.swift compile issue — a file this PR never touches — and main's Desktop Checks are green as of today. A rebase onto current main should clear all five; worth doing before merge so this doesn't land carrying red CI.

Label updates: removed needs-maintainer-review (human approval landed on this head) and added positive-signal + ci-failing-pre-existing. Thanks again @Yny4ii — the reproduction and regression tests made this an easy approve.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added positive-signal Good PR — positive signal, not a formal approval ci-failing-pre-existing CI check failing for reasons pre-existing/unrelated to this PR (red main) and removed needs-maintainer-review Needs a human maintainer to sign off before merge labels Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-failing-pre-existing CI check failing for reasons pre-existing/unrelated to this PR (red main) positive-signal Good PR — positive signal, not a formal approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants