close
Skip to content

fix: reject unknown tag ids on board columns instead of silent drop (#1897) - #2568

Merged
iamwhatever merged 1 commit into
mainfrom
fix/board-column-tag-filter-2-1897
Aug 10, 2026
Merged

fix: reject unknown tag ids on board columns instead of silent drop (#1897)#2568
iamwhatever merged 1 commit into
mainfrom
fix/board-column-tag-filter-2-1897

Conversation

@bolichen97

Copy link
Copy Markdown
Collaborator

Summary

Fixes the board column tag filter appearing to do nothing (#1897). A column PATCH/POST whose tag_ids named a tag id missing from the vocabulary was silently stripped while still returning 200 — the column landed back on tag_ids: [], the deliberate match-all state, so the lane kept showing every session ("same count as ALL SESSIONS").

What changed

  • _normalize_column rejects unknown/non-string tag ids (400 + invalid_column_payload, the handler's existing error shape) on both PATCH /api/chat/tag-columns/{id} and POST /api/chat/tag-columns. The empty list stays valid — it is the documented clear-filter/match-all state the board UI depends on (clear-filter button, both add-column flows).
  • load_tags() prunes dangling tag ids from persisted columns. A crash mid-tag-delete can leave a deleted id on a column (the vocab write commits first, board strip is best-effort); under the stricter validation a dangling id would make that column's filter permanently un-editable (the popover echoes the full list back). Mirrors the slot-restore prune, including its fail-open rule when the vocabulary is unknown (corrupt tags.json).
  • updateColumnMutation gains onError invalidating ['chat-tags'] + ['tag-columns']: a popover holding a stale tag list (tag deleted from another window) re-syncs to server truth instead of showing a selection that was never applied.
  • columnMatches semantics (any/all/none, include_untagged) are untouched — they were verified correct.

Root-cause verification

The reported happy path (valid id → PATCH → persist → render) was reproduced green: the popover, _normalize_column's valid_ids, and slot tags all share one state._tags store. The only reachable way to land on the match-all symptom is an id the vocabulary doesn't hold at PATCH time (stale popover cache after a cross-window tag delete, or a dangling id from a crash mid-delete) — precisely the silent-drop path this PR makes loud, self-healing, and self-repairing at load.

Designed residual (noted for reviewers)

A PATCH carrying one stale + one valid id rejects the whole payload — the valid toggle is lost until the onError re-sync redraws the popover (one extra click). Reject-don't-guess is deliberate: partially applying a filter write would reintroduce a quiet divergence between what the user selected and what persisted.

Tests

  • Backend (test/test_chat_tags.py): accepted-valid persists end-to-end · unknown id → 400 + code, prior filter untouched (update AND create) · empty list still accepted on both · non-string entries rejected · load-time dangling-id prune · fail-open when vocabulary unknown. All 8 new tests verified failing on unfixed code.
  • Frontend (ChatSidebar.boardColumnTagFilter.test.tsx): a filtered column renders only matching sessions (the issue's expected behavior) · PATCH rejection invalidates both caches. Verified failing without the fix.
  • Gates: isort/flake8/mypy clean · backend pytest (only pre-existing host-env failures, identical on clean main) · npx tsc -b clean · full vitest 12164 passed.

Screenshots (asserting harness: website/scripts/capture-board-column-tag-filter.mjs)

Unfiltered column (match-all) Filtered to the Jira tag
unfiltered filtered

Closes #1897

…1897)

A column PATCH/POST naming a tag id missing from the vocabulary was
silently stripped while still returning 200 — the column landed back on
tag_ids [] (the deliberate match-all state), so the board filter
appeared to do nothing: every column kept showing the full session list.

- _normalize_column now rejects a payload with an unknown or non-string
  tag id (400 invalid_column_payload, same shape as the existing
  branch). The empty list stays valid: it is the documented
  clear-filter/match-all state the board UI depends on.
- load_tags() now prunes dangling tag ids from persisted columns (a
  crash mid-tag-delete can leave them), mirroring the slot-restore
  prune including its fail-open rule for an unknown vocabulary —
  without this, a dangling id would make that column's filter
  permanently un-editable under the stricter validation.
- The board's updateColumnMutation re-syncs the chat-tags and
  tag-columns caches on error, so a popover holding a stale tag list
  redraws from server truth instead of showing a selection that was
  never applied.
- Backend tests: accepted-valid / rejected-unknown (update + create) /
  empty-list-still-allowed / dangling-id prune (+ fail-open).
- Frontend tests: a filtered column renders only matching sessions;
  the PATCH-rejection path invalidates both caches.
- Asserting screenshot harness + evidence under
  temp-screenshots/board-column-tag-filter/.

Closes #1897
@bolichen97
bolichen97 requested a review from a team August 10, 2026 14:38
@bolichen97
bolichen97 requested a review from a team as a code owner August 10, 2026 14:38
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 61000eafc9a6a2ec2a4ecd8c389cfeb11589dade — this comment is updated in place on each push.

Review details

The PR is small and well-contained. Let me verify one remaining concern: whether the load-time prune mutating in-memory only (not persisted) causes any inconsistency, and whether the reject-path breaks the update flow when tag_ids is absent from a PATCH body.

Both are handled correctly:

  • PATCH without tag_ids in body → "tag_ids" not in raw → keeps existing (no validation), so rename/reorder-only PATCHes never reject.
  • Load-time prune is idempotent and gated on _tags_authoritative (fail-open on unknown vocab), mirroring existing behavior.
  • Reorder path doesn't call _normalize_column.

The reject-instead-of-drop change is the correct fix, the empty-list match-all state stays valid, and the frontend onError re-sync is sound.

No findings.

[OPUS-REVIEWED] 61000ea

Verdict parsed from the review's SHA-scoped output markers for commit 61000eafc9a6a2ec2a4ecd8c389cfeb11589dade.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 61000eafc9a6a2ec2a4ecd8c389cfeb11589dade: <one-sentence reason>

@github-actions

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

Advisory UX-level review of 61000eafc9a6a2ec2a4ecd8c389cfeb11589dade — updated in place on each push; does not block merge.

UX-Verdict: PASS

The fix turns an invisible silent failure into a self-healing one; the rendered board matches the claims, and the residual rejection path is rare and self-correcting.

Suggestions

  • The new onError re-syncs caches but shows the user nothing: after a 400, the popover silently redraws and a valid toggle bundled with a stale id quietly un-applies (the PR's own "designed residual"). Sibling mutations in the same file surface failures (onError: (e) => setBulkModelError(...), setCleanupError(...) in ChatSidebar.tsx); add a matching one-line inline notice in the filter popover — e.g. "Tag list was out of date and has been refreshed — reapply your selection" — so the lost click is explained rather than left for the user to notice.

[UX-REVIEWED] 61000ea

@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of 61000eafc9a6a2ec2a4ecd8c389cfeb11589dade — updated in place on each push; does not block merge.

Design-Verdict: PASS

Root cause fixed at the right layers: API rejects, load-time prune self-heals persisted dangling ids, client re-syncs — with fail-open guards preserved.

Suggestions

  • The 400 is still user-invisible: the popover toggle just reverts on re-sync. A small error surface (toast/inline notice) would make "reject-don't-guess" legible to the user instead of looking like a lost click.

[DESIGN-REVIEWED] 61000ea

@github-actions

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 61000eafc9a6a2ec2a4ecd8c389cfeb11589dade and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 61000ea

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 61000eafc9a6a2ec2a4ecd8c389cfeb11589dade: <one-sentence reason>

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 10, 2026
@iamwhatever
iamwhatever merged commit fdb2946 into main Aug 10, 2026
51 checks passed
@iamwhatever
iamwhatever deleted the fix/board-column-tag-filter-2-1897 branch August 10, 2026 15:32
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 10, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…irodotdev#1897) (kirodotdev#2568)

A column PATCH/POST naming a tag id missing from the vocabulary was
silently stripped while still returning 200 — the column landed back on
tag_ids [] (the deliberate match-all state), so the board filter
appeared to do nothing: every column kept showing the full session list.

- _normalize_column now rejects a payload with an unknown or non-string
  tag id (400 invalid_column_payload, same shape as the existing
  branch). The empty list stays valid: it is the documented
  clear-filter/match-all state the board UI depends on.
- load_tags() now prunes dangling tag ids from persisted columns (a
  crash mid-tag-delete can leave them), mirroring the slot-restore
  prune including its fail-open rule for an unknown vocabulary —
  without this, a dangling id would make that column's filter
  permanently un-editable under the stricter validation.
- The board's updateColumnMutation re-syncs the chat-tags and
  tag-columns caches on error, so a popover holding a stale tag list
  redraws from server truth instead of showing a selection that was
  never applied.
- Backend tests: accepted-valid / rejected-unknown (update + create) /
  empty-list-still-allowed / dangling-id prune (+ fail-open).
- Frontend tests: a filtered column renders only matching sessions;
  the PATCH-rejection path invalidates both caches.
- Asserting screenshot harness + evidence under
  temp-screenshots/board-column-tag-filter/.

Closes kirodotdev#1897
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.

Board column tag filter has no effect on displayed sessions

2 participants