Found while measuring the premise of #12716 (not fixed there — different defect, and that card returned needs_decision).
What was measured
ApprovalService.recall mutates the row successfully, then reads it back for the response:
packages/plugins/plugin-approvals/src/approval-service.ts:2859 — const fresh = await this.readBackRequest(requestId, context);:2860 — return { request: fresh!, runId, resumed, ... };
readBackRequest delegates to loadRequest, which narrows by the CALLER's org:
approval-service.ts:4739 — if (tenantOrg) where.organization_id = tenantOrg;
So when the request row carries organization_id = null and the caller's context carries a tenantId, the read-back matches nothing and returns null. The fresh! non-null assertion is then false at runtime: the declared ApprovalRecallResult.request is non-nullable, and the value is null.
Evidence (behavioral, not inferred)
Running the real automation engine against the approval-revise.test.ts harness, a platform admin (tenantId: 't1', permissions: ['admin_full_access']) recalling an org-less request:
- the gate ADMITTED and the row was mutated —
row.status became recalled; - reading
out.request.status on the returned envelope threw TypeError: Cannot read properties of null (reading 'status').
The mutation happens and the response envelope is malformed — the call does not fail loudly, it returns a well-formed HTTP 200 whose request field is null.
Blast radius
request: fresh! appears at ten call sites in the same file — :2469, :2495, :2860, :2965, :3008, :3083, :3256, :3339, :3485, :3527 — covering decide, recall, sendBack, resubmit, reassign and the thread interactions. All share the shape.
The REST layer passes the envelope straight through (packages/rest/src/rest-server.ts:11032, res.json(out)), so a console client receives request: null on a call that succeeded.
Adjacency — please dedup against these before triaging
This sits inside a family that already has closed cards, and the relationship has NOT been measured here:
If org-less rows can no longer arise, this is unreachable and should be closed as such. If they can (per #10131's deferred repair), the non-null assertion is a real type-lie on a live path. That determination belongs to a triage seat, not to this finding.
Measured on origin/main at 5700d83b3.
Filed unassigned and ungraded by the domain:services dev seat working #12716.
Found while measuring the premise of #12716 (not fixed there — different defect, and that card returned
needs_decision).What was measured
ApprovalService.recallmutates the row successfully, then reads it back for the response:packages/plugins/plugin-approvals/src/approval-service.ts:2859—const fresh = await this.readBackRequest(requestId, context);:2860—return { request: fresh!, runId, resumed, ... };readBackRequestdelegates toloadRequest, which narrows by the CALLER's org:approval-service.ts:4739—if (tenantOrg) where.organization_id = tenantOrg;So when the request row carries
organization_id = nulland the caller's context carries atenantId, the read-back matches nothing and returnsnull. Thefresh!non-null assertion is then false at runtime: the declaredApprovalRecallResult.requestis non-nullable, and the value isnull.Evidence (behavioral, not inferred)
Running the real automation engine against the
approval-revise.test.tsharness, a platform admin (tenantId: 't1',permissions: ['admin_full_access']) recalling an org-less request:row.statusbecamerecalled;out.request.statuson the returned envelope threwTypeError: Cannot read properties of null (reading 'status').The mutation happens and the response envelope is malformed — the call does not fail loudly, it returns a well-formed HTTP 200 whose
requestfield isnull.Blast radius
request: fresh!appears at ten call sites in the same file —:2469,:2495,:2860,:2965,:3008,:3083,:3256,:3339,:3485,:3527— covering decide, recall, sendBack, resubmit, reassign and the thread interactions. All share the shape.The REST layer passes the envelope straight through (
packages/rest/src/rest-server.ts:11032,res.json(out)), so a console client receivesrequest: nullon a call that succeeded.Adjacency — please dedup against these before triaging
This sits inside a family that already has closed cards, and the relationship has NOT been measured here:
loadRequestorg-narrowing behaviour itself.If org-less rows can no longer arise, this is unreachable and should be closed as such. If they can (per #10131's deferred repair), the non-null assertion is a real type-lie on a live path. That determination belongs to a triage seat, not to this finding.
Measured on
origin/mainat5700d83b3.Filed unassigned and ungraded by the
domain:servicesdev seat working #12716.