fix(service): title the escalation follow-up task with the case number, not the record id - #1227
Merged
Merged
Conversation
The escalation branch of `case_status_side_effects` subjected its follow-up
task `Escalated case ${caseId} needs attention` — the primary key. A demo org
with nine seeded escalations therefore opened All Tasks on nine urgent rows
that differ only in a 16-character opaque id, which matches nothing the reader
has seen: every case surface in this app names a case CASE-00039.
Title it `Escalated: CASE-00039 · Login SSO failure after password reset`
instead — identifier first, so a truncating column still discriminates. Both
parts are read off the afterUpdate pre-image, which is the whole stored row, so
no extra read. The id stays in `related_to_case`, where a relationship belongs.
Capped at 255: `crm_task.subject` declares maxLength 255 and the engine
enforces it, while `crm_case.subject` allows the same 255 — an uncapped
composition is 279 characters and the insert is rejected, silently, because
this hook is async + onError: log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzS6NUXo5A5KEwj1KjvPHk
Two assertions passed against the pre-fix hook: nine raw ids are nine distinct strings, and a short subject never exercises the 255 cap. Assert the case number is present and the id absent, and pin the composed length at the cap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzS6NUXo5A5KEwj1KjvPHk
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzS6NUXo5A5KEwj1KjvPHk
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-sam
marked this pull request as ready for review
August 22, 2026 14:40
This was referenced Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1208
Description
Escalating a case creates an urgent follow-up task for the account owner. That
task was subjected with the case's primary key:
On a demo org with the seeded escalations, All Tasks therefore opened on
nine urgent rows, all due tomorrow, differing only in a 16-character opaque
string — and a string that appears nowhere else in the product: case pages, list
views, breadcrumbs and
crm_case.display_titleall name a caseCASE-00039.The task now reads
Escalated: CASE-00039 · Login SSO failure after password reset— identifier first, so the Subject column (which visibly truncates) stilltells the rows apart, then the case subject for human context. The id is not
dropped: it stays in
related_to_case, where a relationship belongs.What was measured, not assumed
afterUpdatepre-image is the whole stored row (driver.findOnewith noprojection). Observed keys included
case_number: "CASE-00001"andsubject.case_numberis an engine-issued autonumber (CASE-{00000}) generated atinsert, so it is never on an update payload but always on the pre-image —
escalation performs no extra read.
subjectstill prefersinput, sincean update may be renaming the case in the same write.
crm_task.subjectdeclaresmaxLength: 255and the engine enforces it — measured:Subject must be ≤ 255 characters (got 256).crm_case.subjectallows the same 255, so an uncappedEscalated:+ number + separator + subject is 279 characters and theinsert is rejected. This hook is
async: true+onError: 'log', so thatrejection would surface nowhere: the escalation task would simply never exist
— strictly worse than the bug being fixed. The composed title is capped at 255
with the tail trimmed (
…), which is what keeps the identifier intact.crm_case.subjectisrequired+notNull; insert with''is rejectedSubject is required),but the composition is still total: a blank half drops the separator rather
than dangling it, and both blank falls back to a fixed string.
The repo mechanism this lands on
Composition is inline in the handler. Hook bodies ship body-only through
QuickJS; a shared helper would make
extractHookBodythrow, which the CLI buildcatches and silently answers by bundling the closure — with no gate going red.
The build line confirms the property held:
Skipping legacy runtime bundle (all 39 callables are body-only).Type of Change
Related Issues
Fixes #1208
Related to #1226 (sibling occurrences in
lead.hook.tsandopportunity.hook.ts, filed for triage — out of scope here)Changes Made
src/objects/case.hook.ts— the escalation branch ofcase_status_side_effectscomposes the task subject fromcase_numberandsubjectoff the pre-image, capped at 255 with the tail trimmed. Nothing elsein the file changed; the insert's other fields,
owner_idresolution and therelated_to_*set are untouched.test/escalation-task-subject.test.ts— new pin, 9 tests. Runs the loweredbody through real QuickJS (
runHookBody), and drives the cap against a realObjectQL on the shipped
crm_taskmetadata..changeset/escalation-task-titled-by-case-number.md.A downstream effect worth naming:
src/flows/task-urgent-alert.flow.tsnotifieson
"{record.subject}", so the urgent-task alert inherits the readable title.Testing
All eight gates of
pnpm verify, run under the shared verification lock at370bacc(the final commit on this branch):validate✓ Validation passed (1436ms)typechecktsc --noEmit, no diagnosticslintlint:i18n-gate✓ i18n lint gate: 0 i18n/missing-* issueshygiene✓ source hygiene cleanhygiene:tokens✓ source token ratchet cleanbuild✓ Build complete·Skipping legacy runtime bundle (all 39 callables are body-only)testTest Files 116 passed (116)·Tests 2736 passed, 1 skippedCI's own first step was run too:
node scripts/check-stackblitz-lock.mjs→ exit0,
package-lock.json is in sync with package.json (v3).Token ratchet (shrink-only; no ceiling touched):
Reverse verification. With the fix reverted (
git checkout origin/main -- src/objects/case.hook.ts) and the pin left in place, 7 of 9 tests go red,asserting against the real old string:
The two that stay green are the two that must:
is the length the shipped metadata declaresreadscrm_taskmetadata, andrejects the uncapped compositionmeasures the engine — neither depends on the hook. An earlier draftof the pin had 5 red, because two assertions passed for the wrong reason —
nine raw ids are nine distinct strings, and a short subject never exercises
the cap. Both were rewritten (commit
220a85b) before this run.pnpm test)pnpm lint)pnpm build)of the lowered body and against a real engine instead
Checklist
old subject;
grepovercontent/,docs/,e2e/andsrc/found noother copy of the string
Additional Notes
i18n. The task subject is a hook-generated runtime string, and this repo has
no mechanism for translating those:
lint:i18n-gatecovers thei18n/missing-*family, which is metadata-driven (object/field/view/page labels), and every
hook-generated string in
src/objects/*.hook.tsis hardcoded English today. Nolocale rows were added and no new mechanism was invented — reported here rather
than built, per the brief.
Sibling occurrences are filed, not fixed. #1226 covers
src/objects/lead.hook.ts:301andsrc/objects/opportunity.hook.ts:240, whichcarry the same raw-id shape. Untouched here.
Generated by Claude Code
Generated by Claude Code