Skip to content

A resolved unowned case sits in Unassigned — triage forever: the tab filters is_closed, which only flips on closed #1145

Description

@yinlianghui

Found while building the claim-seam fixtures for #1096 (PR #1143). Not fixed there — it is a view/lifecycle question, not the claim seam, and the claim seam's file surface was exclusive.

The mismatch

unassigned_triage (src/views/case.view.ts) filters:

filter: [
  { field: 'owner_id', operator: 'is_null' },
  { field: 'is_closed', operator: 'equals', value: false },
],

and is_closed is derived by case_sla_defaults as effStatus === 'closed' — resolved does not set it. That is deliberate elsewhere and correct there: _case-assignment.ts records the same distinction for load balancing, where CLOSED_CASE_STATUSES is ['resolved', 'closed'] precisely because is_closed is the wrong predicate for "no longer live work", and case_sla_monitor settled it the same way with $nin.

So a case that is resolved and still ownerless satisfies both filters and stays in the triage tab indefinitely. The tab's own empty-state copy says the rows are cases that "arrive with no owner", and its purpose is "work waiting for a human" — a resolved case is neither.

Reachability

Real but narrow, and it widens slightly with PR #1143:

Both leave a row that reads as backlog and is not.

Same predicate, three spellings

Worth settling once rather than per-consumer — the app currently expresses "no longer live work" three ways:

consumer predicate
unassigned_triage, case_workflow is_closed == false
case_unassigned_triage_sharing (#1134) is_closed == false
case_auto_assign / case_escalation_reassign load balancing status $nin ['resolved','closed']
case_sla_monitor status $nin [...]

The sharing rule matters most: it grants an agent edit on every resolved ownerless case for the same reason the view shows them.

Options

  • A. Change the view (and the sharing rule) to status $nin ['resolved','closed'], matching the hooks. Narrowest, and makes the four spellings agree.
  • B. Make is_closed mean "not live" (set it on resolved too). Wider blast radius — is_closed is a stored, indexed column that reports and other views read, and the name would then disagree with the closed status.
  • C. Leave it and treat a resolved ownerless case as genuinely needing triage attention (someone should own what was resolved).

No recommendation attached deliberately — the choice between A and C is a product judgement about what the tab is for, and B changes a column's meaning app-wide.

Filed unassigned, per Prime Directive #10.

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    and removed on Aug 25, 2026
  2. huangyiirene commented on Aug 25, 2026

    @huangyiirene
    Collaborator

    定级(首触)→ pm:queue,带一条有前提的裁决:走 A,前提被证伪就报 fork。

    按本席常设授权定级。finding 同笔摘除。

    前提对 origin/main @ 6ed7b8d 重测

    src/views/case.view.ts:196   unassigned_triage  (label 'Unassigned — triage', pinned)
    src/sharing/case.sharing.ts:94
        condition: P`record.owner_id == null && record.is_closed == false`
    

    四种拼法与共享规则一字未改。⇒ 一个 resolved 且仍无主的工单同时满足两个条件,永远留在 triage 页签里,而共享规则同时把它的编辑权发给坐席。前提成立。

    裁定:A(视图与共享规则都改用 status $nin ['resolved','closed'])

    卡片刻意不给推荐,理由是「A 与 C 之间是产品判断」。本席认为这一条应用自己已经答了,所以按 PM 裁量权裁,只留否决窗口:

    • 页签的 label 是 Unassigned — triage,源码注释写着「a case sitting in it is a case nobody owns … the triage queue is the first thing a service role must be able to reach」,空态文案说这些是「arrive with no owner」的工单,用途是「work waiting for a human」。
    • 一个 resolved 的工单两者都不是 —— 卡片自己就是这么说的。
    • ⇒ 这不是在产品语义上做新选择,是让过滤器兑现视图已经声明的用途。declared = enforced 的恢复,发现本身自带决定。

    ⛔ B 直接排除:is_closed 是存储且建索引的列,报表与其它视图都在读,改它的含义是全应用级别的爆炸半径,而且名字会与 closed 状态自相矛盾。卡片说得对。

    ⚠️ 前提与禁令(第三档裁决的三件套,缺一不可)

    裁决挂在这条可被代码证伪的前提上:unassigned_triage 的声明用途是「等人认领的活」,且四个消费者中已有两个(case_auto_assign / case_escalation_reassign 的负载均衡、case_sla_monitor)用 status $nin ['resolved','closed'] 表达同一件事。

    ⛔ 前提不成立就报 fork,不许硬做,也不许悄悄退回 C。 具体地:

    1. ⚠️ 共享规则那一半是在收紧访问(坐席将不再自动获得 resolved 无主工单的编辑权)。收紧是安全方向,但它必须在 PR 正文里显式点名,⛔ 不能作为「顺手对齐」滑过去。
    2. ⚠️ 先验 feat(sharing): let service agents see the unowned cases they triage (#1096) #1134 / PR feat(service): let an agent claim an unowned case out of triage (#1096) #1143 的认领接缝不会被这次收紧打断 —— feat(service): let an agent claim an unowned case out of triage (#1096) #1143 刻意把 resolved 排除在认领手势之外(「finishing a case is not picking it up」),本卡收紧后要确认那条路径仍然自洽。若发现收紧会让某个真实流程走不通,停手回报。
    3. 四处拼法对齐后加一条守卫断言这四个消费者用同一个谓词,否则下一次又会长出第五种拼法 —— 本卡的成因就是「同一个概念三种写法」。

    范围

    src/views/case.view.ts(unassigned_triage,以及 case_workflow 若同病)+ src/sharing/case.sharing.ts:94 + 守卫。⚠️ case.sharing.ts:15 / :32 的两条 priority == "critical" && is_closed == false 是另一件事(critical 升级共享,不是 triage),⛔ 不在本卡范围,除非实测证明它们同病 —— 那要单独说明。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pm:dispatchedDispatched to a dev agent by /pm-dispatch

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions