Repository navigation
fix(case): the agent an escalation takes a case from keeps read on it - #2115
Conversation
Escalation hands a case to the service_manager pool, and service_agent reads crm_case with readScope own, so the agent lost the case in the same save: a Critical create landed on "Record not found" under "Case created successfully", and Escalate Case did the same on refresh. case_escalation_reassign now records whom it took the case from in a new readonly lookup, escalated_from, in the escalation write itself. A new criteria rule, case_escalated_from_sharing, shares each open case read-only with the user that column names (a field recipient, one person per case). Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv Co-authored-by: Claude <noreply@anthropic.com>
A booted-stack suite drives the two escalation paths as a real agent (a case logged Critical, and Escalate on a case the agent did not create) and reads each case back as that agent, as another agent, and as the system: the agent keeps read, nobody else gains it, the manager's grant is unchanged, the grant is read-only, closing ends it, and a caller cannot name the recipient. The crm_case sharing roster gains the rule, the seeding model maps the field recipient the platform seeds, and the admin sharing pages, the cases page (25 fields, the Escalation group, the sharing list) and STATUS's field figure follow, in every locale. Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
席位核对复核人:repo:hotcrm 执行席(#1353)· 判决 ACCEPT · 钉 head 形态: 前提实测:派发令 H3 路线(H2 成立)。diff 逐行读过:
dev 读数:基线上客服新建 Critical 案例后,控制台在成功提示上方显示「Record not found」,GET 返回 404;点 Escalate 同样;High 对照正常。分支上新建与升级后客服都能读(控制台 + API);另一位客服仍 404;经理的编辑授权不变;客服编辑被 403 拒绝;调用方自带的 门禁 check-runs(钉 head):Build and Test (22.x) ✓ · Typecheck and Build ✓ · Quality Checks ✓ · Check Changeset ✓ · Playwright ✓ · link-check ✓ · CodeQL ✓ · Analyze Code (javascript) ✓ · Label Pull Request ✓ · Vercel Preview Comments ✓ — 10/10 success,mergeable_state 接受的偏差:① 落点超出认领面(字段、语言包、文档、 范围外去向:控制台把「对当前用户隐藏的记录」显示成「Record not found … may have been deleted」(objectui 落地条款:席位经中继 |
Part of #2060 (item 3, finding B-04). Items 4, 5 and 7 to 18 of the card, and item 1's delete half, are not touched here; the card stays open.
Clause-②: no
What was wrong (measured on
mainf1c80837,@objectstack/*18.0.0-next.1)A service agent loses a case in the same save that escalates it.
service.agent: New Case → priority Critical → Create. The toast reads "Case created successfully" and the page reads "Record not found — The record you are looking for does not exist or may have been deleted." (DOM probe:recordNotFound: true,mayHaveBeenDeleted: true, subject absent). The same steps with priority High (control) land on the record (subjectOnPage: true).GET /api/v1/data/crm_case/IDanswers404 RECORD_NOT_FOUNDright after the Critical create, and still 2.5 s later. The stored row isstatus: escalated,owner_id= theservice_managerholder,created_by= the agent. The High control answers 200. A PATCH tostatus: escalatedanswers 404 afterwards, both on a case the agent created and on one an admin created and handed to them.Producer:
case_escalation_reassign,src/service/objects/_case-assignment.ts:436on base (if (best) input.owner_id = best;,beforeUpdate). It is reached on create bycase_escalation_on_create(case-escalation.flow.ts:265, rebinding theassign_senior_agentnode torecord-after-create), and on Escalate Case by theescalatenode (case-actions.flow.ts:80, user context).case_sla_defaultsdoes not move the owner. The agent then has nothing to read with:service_agentholdscrm_casewithreadScope: 'own'(src/sales/profiles/service-agent.profile.ts:25), and no sharing rule names the person the case was taken from.What changed
escalated_from, a new readonlylookup → sys_useroncrm_case(Escalation group).case_escalation_reassignstamps the owner it moves the case away from, in the escalation write itself. That is door ① of the module header, so the transfer gate is not involved and no permission changes. An ownerless case is stamped with nobody.case_escalated_from_sharing(Escalated Cases — Previous Owner):condition: record.is_closed == false,accessLevel: read,sharedWith: { type: 'field', value: 'escalated_from' }. It shares the case with exactly one person, the one it was taken from. It is bounded like the two escalation rules beside it, and they are untouched.src/docs/crm_admin.md.content/docs/service/cases*.mdxgets 25 fields, the Escalation group row and a sharing bullet.docs/STATUS.mdgoes from 357 to 358 fields. One changeset.The profile line (
readScope: 'own', nocrm_case.allowTransfer) and both standing escalation rules are unchanged.Why a stored previous owner and not
created_by(H2 / H3)H2, read in the installed tree:
ShareRecipientTypeincludes'field'(@objectstack/specsrc/security/sharing.zod.ts:113-120). It is a record-relative recipient, andvaluenames a user-typed field on the record. The runtime honours it:plugin-sharingmapRecipientTypemapsfield(distindex.js:6861), andexpandRecipientForRecord/fieldHoldsUsersread the named column per matched record, accepting auserfield or alookup → sys_user(index.js:4872,:3959).created_byis injected aslookup → sys_user, so a creator grant would also have run.The creator is the wrong person to key on, and the measurement shows it:
created_bynull), triage claims have no creator, and on the first demo boot all 38 cases the agent owned hadcreated_by: null. The tester's own escalate step (CASE-00039, a case claimed out of triage) is that shape.The spec's own guidance for record-relative audiences ("a user field the application stores on the record … plus a
fieldrecipient naming it") is the stored-owner shape. So the route is H3 with "its previous owner", and nothing in the H4 list was needed.Measured after the change (live server, port 4163, fresh DB,
pnpm demo:staff)recordNotFound: false, subject on page). The Escalation section shows ESCALATED FROM: the agent. Escalate Case on an admin-handed case (created_bynull) stays on the record after the run and after a reload.service_agentanswers 404 on all four cases.sys_record_share):case_escalated_from_sharing→ agentread, andcase_escalation_sharing→ manageredit, which is exactly the manager's grant from before.403 FORBIDDEN(read only);escalated_fromthe agent supplies, on create or on update, is dropped by the readonly strip and stored null;Tests
test/escalated-case-previous-owner.test.ts(new) runs 6 cases on the booted app. Each one writes as a real agent and reads back as that agent, as another agent and as the system:code: FORBIDDEN,status: 403;sharing-coveragegets the fourth roster entry.sharing-seedingmaps thefieldrecipient the platform seeds, and adds a witness for the rule.Ablation. Both runs used
ablation-replacein wrap mode under the verify lock, and both restored to blob == HEAD withgit diff HEADempty. The subject resolves fromsrcthroughobjectstack.config, so nodistwas involved.escalated_fromassertions failed (null), and the read-only case gotRECORD_NOT_FOUND 404instead ofFORBIDDEN 403.serviceSharingRules: 3 failed / 3 passed ("the agent lost the case they just logged").Gates (all at
934e991c, exit codes captured before any pipe)pnpm validate: 0, "✓ Validation passed", "Data: 18 Objects 358 Fields"pnpm typecheck: 0pnpm lint: 0, "18 suggestion(s)", none naming a file or key of this PRpnpm lint:i18n-gate: 0, "✓ i18n lint gate: 0i18n/missing-*issues"pnpm hygiene: 0, "✓ source hygiene clean"pnpm hygiene:tokens: 0, "✓ source token ratchet clean"pnpm build: 0, "✓ Build complete", the same 8 author-time warnings as basepnpm test: 0, "Test Files 181 passed (181)", "Tests 3686 passed | 1 skipped (3687)"Playwright e2e is CI's.
Outside the claim's file surface, and why
The claim names
case.sharing.tsand, if the producer sits there, the assignment path. The producer is there. A stored principal also needs:case.object.tsand its label in the four packs (every field ships in all four);docs/STATUS.md, which goes red on the field count;src/docs/crm_admin.md.The line count is 219 (201 added, 18 deleted) against the dispatched budget of about 150: src 67, docs 26, tests 119, changeset 7.
Acceptance notes (not filed)
content/docs/service/cases*.mdx(the "three sections, 10 of the 24 fields" table and the Details bullet's "Escalated Date … displayed nowhere") describes the tab as it was before the field-group sections. Measured: the Details tab renders the object's groups, and Escalation shows Escalated, Escalated Date, Escalation Reason and Escalated From. Those sentences were already false, so they are left as they were. Carrier: item 16 of this card (docs that describe the app as it was).src/docs/crm_admin.md's sharing table already omitted Unassigned Cases — Triage. This PR adds only its own row. Carrier: item 16 of this card.service.agent. On a box staffed after the seed settled, they went to the admin through the seed claim. This is an observation only.Generated by Claude Code