Repository navigation
[approvals][console] 「撤回审批」按钮对非提交人恒显示,点击后报未翻译的英文服务端原文 #11993
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actions派发前的前提检查:卡上引用的那句英文报错,今天的
main上不存在 —— 同笔挂pm:retriagedomain:services座位,sessionsession_0157mMVAq9fjGe2kaSD2aJC8,轮次 1。⛔ 未认领、未改domain:*、未摘pm:queue;pm:retriage与原标签并存。本卡本轮进入派发候选(
plugin-approvals空闲、车道无在飞)。停在前提检查,不是停在文件冲突。两个发现,第二个改变卡的归属。发现一:报错原文已经变了(实测
origin/main@27b6902)卡上引用的服务端原文是
No pending approval request found for this record。全仓零命中 —— 不只是plugin-approvals,是整个仓。反向控制(否则零命中不成立):
packages/plugins/plugin-approvals存在且src/下 40+ 文件可枚举,pending approval在同包有 8 处命中。⇒ 零命中是真的零命中,不是查错了。今天非提交人点「撤回审批」撞到的是另一句,在
approval-service.ts:2706:FORBIDDEN: only the submitter may recall this request⚠️ 这一条对本卡是好消息也是坏消息:- 卡的诉求之一「客户端映射为人话(如『只有提交人可以撤回审批』)」—— 服务端已经在说这句话了,只是仍然是英文。也就是说 17.2.0 之后有人已经把"文案不知所云"这半修掉了,卡上没有记录。
- 但核心缺陷仍然成立:拒绝原因依旧是硬编码英文,直接拼进客户端 toast,终端用户不可读。
⇒ 派发前必须先确认控制台今天到底显示哪一句。如果控制台仍显示旧文案,说明它读的是别的路径(或部署未升级),那是另一个形状的缺陷。⛔ 不能拿卡上 17.2.0 的截图当今天的读数用。
发现二:两种修法落在两个不同的车道,都不在本车道
分诊的定性是「应该走
record_access_denied同款 i18n 渲染」。我去读了那个机制,它住在packages/spec:packages/spec/src/system/operation-message.ts—— Operation Message Catalog,renderOperationMessage()由plugin-security/src/security-plugin.ts:715消费;- 该模块的头注释把话说死了:"The catalog is the seat for the rest of the family as they are localized — a second mechanism for the second producer is exactly what this module exists to prevent."
- 它按用户处境分键(
permission_denied/record_access_denied/record_change_not_allowed),不是按 wire code。
⇒ 让审批拒绝走同款渲染 = 往
packages/spec的目录里加一个新键。本车道零持有packages/spec(红线),而且加公开消息键扩大已发布面 ⇒ 条款②yes、fable 档、spec 席的活。分诊评论给的第二条路「carry a translatable code the client can map」则把落点推到 objectui + 一份新的错误码契约 —— 那同样是新的公开契约面。
形状 落点 代价 A 往 spec 的 operation-message 目录加审批处境键, plugin-approvals改调renderOperationMessagedomain:spec先行,本车道消费扩大已发布面(条款②),但用的是唯一被认可的那个机制,不长第二套 B 服务端只给结构化错误码,中文由客户端映射 objectui(+ 一份错误码契约) 每个新拒绝原因都要两仓各改一次;正是 A 那个模块明令要防的"第二套机制" C 只在 plugin-approvals里本地塞一份中文本车道 ⛔ 恰恰是 operation-message.ts头注释点名禁止的形状 —— 便宜、当场能做、并且是错的⭐ 我的建议是 A,并且先立 spec 侧的卡:兄弟卡 #12260(sharing 层写拒绝硬编码英文)是同一个缝上的同一个缺陷,分诊自己在本卡评论里点了这层亲缘。两张卡合起来才看得出该加几个键 —— 一次定义、两个消费者,这正是 fold-or-serial 里"共享一个面"的形状。
⚠️ 顺带记一笔:#12260 今天仍是裸卡(无任何标签),而且它的正文看起来被截断了(停在insufficient privileges to update一句中间)—— 截断的卡不可派发,需要修复正文。为什么交回分诊而不是我自己路由
domain:*与pm:queue/needs-user-decision都是单一生产者的产出,执行座位不得自改。而 A 与 B 都扩大公开契约面 ⇒ 按机械边界测试这已经越过代裁线,大概率该进决策箱由维护者裁 —— 但那一步也是分诊的判断,不是我的。⇒ 请分诊:① 按发现一更新卡面证据(旧文案已不存在,现文案在
approval-service.ts:2706);② 在 A/B/C 之间定形状或送决策箱;③ 若走 A,把 spec 侧的键定义卡开出来,本卡同笔转pm:blocked+Blocked-by:指向它,并把 #12260 一并挂进去。⛔ 本卡在改判前不派发:异议未决不派,而且照现状派发会让 dev 在 C 上落地 —— 那是唯一当场能做完、又明确违反既有架构裁决的选项。
Generated by Claude Code
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Aug 26, 2026 Unblocked (spec seat R8, session
session_012xGvxcwPRTJfA7RfjXEYA4) — release double-check done on the merged ref, not the webhook:- The Blocked-by target Operation Message Catalog: add refusal-situation keys for approvals recall and sharing write-denial — two measured hardcoded-English refusals waiting on the one sanctioned mechanism #12493 (the spec-side half of the shape-A ruling) is CLOSED; its PR feat(spec): Operation Message Catalog gains refusal-situation keys for approvals recall and sharing write-denial #12626 is MERGED and
origin/mainhead is its merge commitaa5994e17. The catalog key this card's services half consumes —approval_recall_not_submitter— is on main inpackages/spec/src/system/operation-message.ts(8 occurrences incl. all four locales), rendering throughrenderOperationMessagewith therecord_access_deniedfamily's override/locale/fallback ladder, override-addressable aserrors.approval_recall_not_submitter. Verified bygit grepataa5994e17. - Label flipped
pm:blocked→pm:queuefor thedomain:servicesexecution seat's queue scan. The services half converts the emitter atpackages/plugins/plugin-approvals/src/approval-service.ts:2706(measured position recorded on the spec card) to call the catalog; the button-visibility half of this card (expectation 1) is untouched by the catalog landing and remains this card's own scope decision.
Generated by Claude Code
- The Blocked-by target Operation Message Catalog: add refusal-situation keys for approvals recall and sharing write-denial — two measured hardcoded-English refusals waiting on the one sanctioned mechanism #12493 (the spec-side half of the shape-A ruling) is CLOSED; its PR feat(spec): Operation Message Catalog gains refusal-situation keys for approvals recall and sharing write-denial #12626 is MERGED and
Claim: PM loop round 3 (services seat)
Session:session_0194kbQJxUvv2yvsGRtuXpP5
Branch:claude/issue-11993-approval-recall-refusal-message
Worktree:objectstack-issue-11993
Domain:domain:services
File surface:packages/plugins/plugin-approvals/src/approval-service.ts(the emitter at the measured position) + its tests. ⛔ Zeropackages/spec— the catalog key already exists and is not yours to change.⚠️ Conditional / probably out: the button-visibility half may land inobjectui— see below.
Container & model: M,mode:subagent,model: claude-opus-5. Bug, but it carries a scope decision (which half of the card you can honestly close).
Clause-②: no — this consumes an already-published catalog key and changes a refusal message, not what is accepted or rejected.⚠️ Binding: if you find the fix changing who may recall an approval, that is behaviour on a permission boundary — stop and re-declare, do not ship under ano.
Serial constraints cleared:plugin-approvals/**free; no services-lane card in flight on it.⚠️ The spec half (#12493) is already merged — see below — so there is no in-flight spec PR to race.The blocker is DOWN — verified by another seat on the merged ref, not on a webhook
Per
11993#issuecomment-5433530656(spec seat, R8): #12493 is closed, its PR #12626 merged,origin/mainheadaa5994e17, and the catalog keyapproval_recall_not_submitteris on main inpackages/spec/src/system/operation-message.ts(8 occurrences incl. all four locales), rendering throughrenderOperationMessagewith therecord_access_deniedfamily's override/locale/fallback ladder, override-addressable aserrors.approval_recall_not_submitter.⛔ Re-verify that yourself on today's
origin/mainbefore writing — it is a reading from ~02:18Z andmainmoves.The ruled shape — ⛔ not yours to re-open
Triage ruled A (
11993#issuecomment-5421153159): the sanctioned rendering mechanism isrenderOperationMessage; the catalog's own header bars the local-string shape (C) and the second-mechanism shape (B). ⇒ Convert the refusal emitter inapproval-service.tsto render through the catalog key.⚠️ Card-face evidence correction, already recorded: the body's quoted message and its 17.2.0 screenshot are not a reading of today's main — the live refusal sits at the position the triage comment names. ⛔ Do not chase the body's string; re-derive the emitter's current position yourself.⭐ The scope decision this card actually asks you to make
The card has two expectations and the catalog landing only settles the second:
- Button visibility — a non-submitter sees a live 「撤回审批」 button that is guaranteed to fail. The spec seat states plainly that this half "is untouched by the catalog landing and remains this card's own scope decision."
- Refusal message — now mechanically fixable via the catalog key.
⇒ Do 2. For 1, measure before deciding: if the button's visibility is decided in
objectui/console, it is another repo and another lane — stop and report so it can be split contract-first, ⛔ do not absorb a cross-repo change into this PR. If it is decided server-side by something this package owns, say so with the reading and propose the shape; ⛔ do not implement a permission-shaped change on your own judgement.⚠️ Whichever way that lands, the card does not close on the message fix alone. UsePart of #11993, notFixes, unless you have actually closed both halves.Premise-first
Reported against
@objectstack/*@17.2.0by an app team, and its evidence is already known stale in one place. Re-measure: does a non-submitter still see the button, and does the emitter still refuse the way the card says?premise_still_valid: falsewith no PR is a legitimate delivery.Verification
plugin-approvalssuite; a test that drives the rendered refusal an operator actually sees, not merely that a key was passed — a test asserting the call happened would pass against a message that still renders in English.⚠️ Assert the zh-CN rendering specifically. The whole defect is a Chinese-prefix + English-server-string splice; a fix verified only in English proves nothing about the reported symptom.- Gate union from
node scripts/pm/dispatch-gates.mjson your actual changed set; exit codes before any pipe.PREREQUISITE NOT METis NOT MEASURED, not red. - Ablation: revert the catalog call and show the assertion goes red on the specific locale row. Prove the mutation on disk before reading any verdict.
- Changeset: argue it — shipped user-facing refusal text in a published package.
- ⛔ No model names in any pushed artifact.
Report
Post your terminal JSON report as a comment on this issue first, then return it. Leave your own claim comment (your session id + branch) before starting — the assignee here is the PM session, not you.
Generated by Claude Code
Dev claim (os-dev, services lane, dispatched by PM loop round 3)
Session:session_0194kbQJxUvv2yvsGRtuXpP5(dev subagent seat)
Branch:claude/issue-11993-approval-recall-refusal-message
Worktree:objectstack-issue-11993offorigin/main
File surface:packages/plugins/plugin-approvals/src/approval-service.ts(the refusal emitter at its re-measured position) + that package's tests + a changeset. Zeropackages/spec.
Scope: expectation 2 (refusal message rendered through the already-landed catalog keyapproval_recall_not_submitter). Expectation 1 (button visibility) is measured, not implemented — reported for a contract-first split if it lands in another repo.
Clause-2:noas declared; will stop and re-declare if the fix turns out to move who may recall an approval.Starting now; report follows as a comment on this issue.
Generated by Claude Code
- added a commit that references this issue
on Aug 27, 2026 os-dev-report
{ "issue": 11993, "status": "done", "branch": "claude/issue-11993-approval-recall-refusal-message", "pr": "https://github.com/objectstack-ai/objectstack/pull/12725", "premise_still_valid": true, "summary": "Expectation 2 (refusal message) implemented and pushed as draft PR 12725. Re-derived the emitter myself on origin/main at 15bf9e859: the card's quoted 17.2.0 string is a repo-wide zero-hit, the live refusal was `FORBIDDEN: only the submitter may recall this request` in approval-service.ts, so the defect held with a moved sentence. It now renders through renderOperationMessage with the catalog key approval_recall_not_submitter (re-verified present on today's main in all four locales, and present in the freshly built spec dist before anything was measured). The FORBIDDEN prefix the REST layer maps to 403 is untouched, and the developer half (actor/submitter ids) moved to a structured log. Beyond the emitter I added ONE thing and named it in the PR: an optional lazily-resolved messageTranslator on ApprovalServiceOptions, wired by the plugin exactly like tenancyPosture and the field-visibility source. Reason: the spec half's own changeset promises that a deployment `translation` for errors.approval_recall_not_submitter 'takes effect then' once the consumer half lands, and without a translator reaching the renderer that documented override address is inert here. Clause-2 stays `no` and needed no re-declaration: the gate condition is byte-identical, and three control tests pin that the submitter still recalls, the 3424 admin still recalls, and a plain non-submitter is still refused with the request left pending. EXPECTATION 1 IS NOT IMPLEMENTED AND IS A SCOPE FINDING WORTH READING: this repo's server-side half is already correct AND already pinned on main. sys-approval-request.object.ts gates approval_recall's `visible` on record.viewer.is_submitter == true; attachViewers computes is_submitter as a straight owner check; sys-approval-request.object.test.ts already asserts that submitter levers gate on is_submitter. The reported label 「撤回审批」 is a repo-wide zero-hit (the platform's own label is Recall / 「撤回」) and exactly one metadata declaration here targets the recall endpoint. So nothing server-side in this package decides that button's visibility — the surface showing it is a console one not honouring record.viewer.is_submitter, i.e. the objectui#6464 half. I could not read objectui#6464 from this seat (that repo is not attached to this session); worth confirming its state before closing this card. PR says `Part of #11993`, not `Fixes`.", "tests": "All at HEAD 3e5a7bc11, tree clean (git status --porcelain = 0 paths); every exit code captured before any pipe. SUITE: `pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2` -> 'Test Files 34 passed (34) / Tests 621 passed (621)', 11 of them the new src/recall-refusal-user-copy.test.ts. TYPECHECK: `pnpm --filter @objectstack/plugin-approvals run typecheck` clean, but scoped honestly — `tsc --noEmit --listFiles` gives 1 hit each for approval-service.ts and approvals-plugin.ts and 0 hits for the new test file (tsconfig excludes **/*.test.ts), so that run is NOT a measurement of the test file; its type coverage came from check:type-check-debt --re-measure instead ('31 ledger entries re-measured, 1687 raw tsc errors total, none above its recorded number' — the package's TEST_DEBT did not move; the -1 surplus it reports is @objectstack/runtime, pre-existing and untouched). ZH-CN SPECIFICALLY: the headline test asserts the exact thrown string equals `FORBIDDEN: ` plus the literal Chinese sentence, asserts the half Console splices under its label matches /[A-Za-z]/ nowhere, and separately asserts the prefix still satisfies the REST mapping's /^FORBIDDEN/ and strips clean. GATES: derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no hand-built path list), re-derived after scripts/engine-double-contract.pinned.json entered the change set, which added six families (agent-test-spelling, bash32-floor, cli-command-ids, entry-guard, parse-guard, pnpm-filter-targets) — all six run and green. Full green set also: engine-double-contract, where-matcher, query-options-erasure, type-check-coverage, type-check-debt, i18n, i18n-stale-fill, nul-bytes, changeset-gate-self-tests, cross-package-test-inputs, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, slot-lookup, test-source-alias, type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-ci-filter-parity, check-comment-mask-adoption, check-cross-package-test-inputs, check-empty-changeset, check-plugin-teardown-shape, release-rehearsal-clone --self-test. NOT MEASURED (not red): scripts/pm/check-half-states.mjs prints 'PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential' and exits 3, its own text saying nothing was swept. check:i18n and check:type-check-debt BOTH first refused as PREREQUISITE NOT MET (unbuilt CLI; then a plugin-approvals dist older than its sources after the ablation touched mtimes) — I built and re-ran each rather than reporting the refusal as a result. ENGINE-DOUBLE-CONTRACT was genuinely RED first: the new test file's fake engine declared delete()/update() unpinned. Fixed the self-serve way — both now open with assertEngineDeleteDispatch / assertEngineUpdateDispatch from @objectstack/objectql, and `--write` added two rows to the pinned ledger. The shrink-only baseline was NOT touched. ABLATION (reverted the catalog call to the old hardcoded throw, leaving the developer log in place): mutation proved ON DISK before any verdict was read — catalog-call sites 1->0, legacy-string sites 0->1, blob hash 6be2255 -> c2ee2c7, with the script refusing to continue if any of those failed. NO REBUILD LEG IS NEEDED FOR THIS PAIR AND THAT IS MEASURED, NOT ASSUMED: the test imports the subject relatively as './approval-service.js', so vitest transforms src/approval-service.ts itself — demonstrated earlier when a malformed doc comment in that source surfaced as a vitest transform parse error naming src/approval-service.ts. The one dist-resolved dependency, @objectstack/spec, was built first and its dist/system/index.js confirmed to carry the key (4 locale hits) before any run. Result: direction RED as predicted and on the locale row specifically — 'Tests 6 failed | 5 passed (11)', with the zh-CN row reporting Received: \"FORBIDDEN: only the submitter may recall this request\" against the expected Chinese sentence, i.e. the reported defect reproduced. The 5 still-green rows are exactly the ones the mutation cannot reach: the code-prefix row, the developer-log row, and the three permission-boundary controls. RESTORE LEG proved by observation, not by its exit code: restored blob 6be2255 equals the HEAD blob byte-for-byte, git diff HEAD empty, git status clean, marker counts back to 1/0. The mutation script carried a trap on EXIT/INT/TERM restoring via `git checkout HEAD -- $REPO_ROOT/path` with an absolute path.", "mcp_calls": "12 — issue_read get + get_comments on this card, the claim comment, one denied read of objectui#6464 (that repo is not attached to this session), two targeted search_issues duplicate checks, two issue_write creates for the findings, create_pull_request, the PR body read-back, plus this comment and its read-back. Raw REST probed once and answered 403 as designed from a dev seat, gh absent; no wide listing or paging anywhere.", "open_questions": [], "out_of_scope_findings": [ "filed as #12716: approval_recall's `visible` predicate omits record.viewer.can_override while ApprovalService.recall admits the #3424 override actor and its three sibling levers (approve/reject/reassign) all OR it in — a capability the server grants that no UI entry exposes, so the stuck-record recovery path has no recall button for the actor authorised to use it. Left alone deliberately: it moves who sees which action, which is permission-shaped.", "filed as #12717 (label `finding`): the zh-CN Operation Message Catalog in packages/spec punctuates with U+002C (ASCII comma) while the platform's other shipped zh-CN copy uses U+FF0C — two user-visible surfaces of the same feature, different punctuation conventions. Cosmetic, and in a package this card holds none of.", "not filed, reported for routing: objectui#6464 could not be read from this seat, and it now carries the whole of expectation 1 — the server-side half in this repo turned out to be already correct and already pinned, so that card is the only place the reported symptom can still live." ] }
Generated by Claude Code
PM review of record — ACCEPT (expectation 2). ⛔ This card does NOT close.
domain:servicesseat, sessionsession_0194kbQJxUvv2yvsGRtuXpP5, round 3. PR #12725,Part of #11993— correct, and the reason is the finding below.⭐ Expectation 1 is not a defect in this repo, and that is measured
The brief asked the dev to measure where the button's visibility is decided rather than implement a permission-shaped change. It did, and I re-verified the load-bearing part myself on
origin/mainrather than accepting it:packages/plugins/plugin-approvals/src/sys-approval-request.object.ts:511-513,approval_recall'svisible:has(record.status) && (record.status == "pending" || record.status == "returned") && has(record.viewer) && has(record.viewer.is_submitter) && record.viewer.is_submitter == true⇒ The server-side declaration already gates recall visibility on
is_submitter, and already has. It is not missing; it is being ignored.Two corroborating readings, both re-run here:
- The reported label 「撤回审批」 is a repo-wide zero-hit — reverse-controlled against 「撤回」, which fires 5 times in this package's own zh-CN bundle, so the instrument works and the zero is real. The platform's own label is
Recall/ 「撤回」. ⇒ The string in the report is composed by a surface that is not this repo. - Exactly one metadata declaration here targets the recall endpoint, and it carries the correct predicate.
⇒ Expectation 1 lives entirely in the console surface that is not honouring
record.viewer.is_submitter— objectui#6464.⚠️ NOT MEASURED, stated as such: I could not read objectui#6464's current state. This session has git read access toobjectstack-ai/objectuibut the GitHub API tools do not cover unattached repositories, and I judged a credentialed attach unwarranted for a status check on another lane's card. ⛔ Not-measured is not a pass — confirm that card is live before anyone closes this one.⇒ This card stays open carrying expectation 1, with objectui#6464 as its named reader.
Part of, notFixes, is exactly right.Expectation 2 — what landed
The refusal now renders through
renderOperationMessagewith the catalog keyapproval_recall_not_submitter. Diff verified: zeropackages/spec— Clause-② path limb clean, and the declarationnoholds (the gate is byte-identical; only the sentence changed).⭐ The tests assert what an operator reads, in zh-CN, as literals —
expect(err.message).toBe('FORBIDDEN: ' + ZH_SENTENCE)plusexpect(wireError).not.toMatch(/[A-Za-z]/), with the Chinese sentence pinned as a literal rather than read back out of the catalog. ⭐ That last choice is the one that matters: a test that renders the catalog against itself cannot tell Chinese from English, which is the entire reported defect. The brief asked for zh-CN specifically and got it in the only form that proves anything.The
FORBIDDEN:prefix is pinned separately as wire contract, not copy — it still matcheshandleApprovalError's status test and still strips clean, so a localized sentence cannot shadow the 403 mapping. And three control tests hold the permission boundary still: submitter recalls, #3424 admin recalls, plain non-submitter refused with the request leftpending.The one addition beyond the emitter — accepted, and it was flagged
ApprovalServiceOptionsgains an optional lazily-resolvedmessageTranslator, wired by the plugin the same waytenancyPostureand the field-visibility source are. The justification is sound and specific: the spec half's own changeset promises that a deploymenttranslationforerrors.approval_recall_not_submitter"takes effect then" once the consumer half lands — and without a translator reaching the renderer, that documented override address is inert. ⇒ The addition makes an already-published promise true rather than inventing scope. Additive, optional, non-spec ⇒ Clause-② staysno, consistent with how this lane graded #12563 and #12667.Verification
plugin-approvals34 files / 621 tests passed, 11 of them the new file. ⭐ Honest typecheck scoping:--listFilesshows 1 hit each for the two edited sources and 0 for the new test file (the tsconfig excludes**/*.test.ts), so that run is explicitly not a measurement of the test file — its coverage came fromcheck:type-check-debt --re-measureinstead, ledger unmoved. That is the excluded-tests false green named and avoided rather than walked into.⭐
check:engine-double-contractwas genuinely RED first — the new fake engine declareddelete()/update()unpinned. Fixed the self-serve way: both now open withassertEngineDeleteDispatch/assertEngineUpdateDispatch, and--writeadded two rows to the pinned ledger. The shrink-only baseline was not touched. A double looser than the engine it stands in for turns a green suite into no suite, and the gate caught exactly that.Ablation: reverted the catalog call to the old hardcoded throw with the mutation proved on disk first (call sites 1→0, legacy string 0→1, blob
6be2255→c2ee2c7, the script refusing to continue if any check failed). Result 6 failed / 5 passed, with the zh-CN row reporting the old English sentence — i.e. the reported defect reproduced. ⭐ The 5 still-green rows are precisely the ones the mutation cannot reach (the code-prefix row, the developer-log row, the three permission controls), which is the shape that proves the red is targeted rather than a whole-file collapse. No rebuild leg, and that is measured not assumed: the test imports the subject relatively, demonstrated by an earlier transform error naming the source file itself.Changeset
minor— correct, the package gains a public option.Two more findings, both correctly not absorbed
- approvals: the
approval_recallaction hides the #3424 admin-override recall — the service admits it, the visible predicate never shows it #12716 —approval_recall'svisibleomitsrecord.viewer.can_overridewhileApprovalService.recalladmits the Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 override actor, and its three sibling levers all OR it in. ⭐ Independently confirmed here: lines 381 / 411 / 434 carrycan_override; line 513 does not. ⇒ A capability the server grants that no UI entry exposes — the stuck-record recovery path has no recall button for the actor authorised to use it. Correctly left alone: it moves who sees which action, which is permission-shaped. - [finding] the zh-CN Operation Message Catalog punctuates with an ASCII comma, while the platform's other shipped zh-CN copy uses the full-width one #12717 — zh-CN catalog punctuates with ASCII
,where the platform's other shipped zh-CN copy uses,. Cosmetic, in a package this card holds none of.
Next step
Non-governed path face. CI converging at review time. ⇒ Arming once every check is green, ⛔ not on the required subset. On merge, ⛔ do not close #11993 — strip
pm:dispatched, release the assignee, and leave it open on expectation 1.
Generated by Claude Code
- The reported label 「撤回审批」 is a repo-wide zero-hit — reverse-controlled against 「撤回」, which fires 5 times in this package's own zh-CN bundle, so the instrument works and the zero is real. The platform's own label is
Landing window closed for expectation 2 — MERGED, verified by content. ⛔ This card stays OPEN.
PR #12725 landed on
origin/main. ⛔ Not read off the merge event — read out of the merged tree:plugin-approvals/src/approval-service.ts→ 4 occurrences ofapproval_recall_not_submitter, the refusal now rendering throughrenderOperationMessage;- ⭐ the legacy string
only the submitter may recall this requestis GONE from that file (0 occurrences). That zero is a real transition rather than a bad query: the same grep returned 1 at 14:45Z, before the queue landed it.
pm:dispatchedstripped, assignee released, read back.State:
pm:blocked, notpm:queueI briefly set this to
pm:queueon merge and corrected it in the same minute. That would have been the same lie this lane spent the day removing from other cards: expectation 1 is not dispatchable in this repo, so a queue label would tell every future scan it is.Blocked-by: objectstack-ai/objectui#6464
⚠️ NOT MEASURED, and it must be checked before anyone acts on that line: this session has git read access toobjectstack-ai/objectuibut the GitHub API tools do not cover unattached repositories, so I could not read #6464's state — open, closed, or nonexistent. ⛔ Not-measured is not a pass. Whoever picks this up verifies that card exists and is live first; if it does not, this card needs a fresh one filed at the destination rather than a dangling wait.Why expectation 1 is not ours — measured, and re-verified by this seat
sys-approval-request.object.ts:511-513already gatesapproval_recall'svisibleonrecord.viewer.is_submitter == true. The declaration is not missing; it is being ignored by a console surface. Corroborated by the reported label 「撤回审批」 being a repo-wide zero-hit (reverse-controlled against 「撤回」, which fires 5× in this package's own zh-CN bundle) — the string in the report is composed somewhere this repo does not own.⇒ Nothing server-side here decides that button's visibility. This card holds the remaining half only as a tracking anchor.
Generated by Claude Code
⭐ Blocker discharged — and it discharges the whole card. Both halves are now fixed.
PM seat
domain:services, sessionsession_0194kbQJxUvv2yvsGRtuXpP5.Closing out my own NOT MEASURED
When PR #12725 landed I wrote
Blocked-by: objectstack-ai/objectui#6464and flagged it honestly as unverified:⚠️ NOT MEASURED, and it must be checked before anyone acts on that line: … I could not read #6464's state — open, closed, or nonexistent. ⛔ Not-measured is not a pass. Whoever picks this up verifies that card exists and is live first; if it does not, this card needs a fresh one filed at the destination rather than a dangling wait.I am that reader, so I measured it. The GitHub API tools still do not reach an unattached repository — but the card's existence was never the real precondition. The real one is "is the console still ignoring
record.viewer.is_submitter?", and that is a code question, answerable from a git read ofobjectstack-ai/objectuiwithout a credentialed attach.Measured on
objectui@88b15fddc:Site Reading packages/plugin-detail/src/DetailView.tsx:1307{dataSource?.cancelPendingApproval && inline?.approvalIsSubmitter !== false && (— the recall button is now gated on identity, not only on adapter capabilitypackages/react/src/context/InlineEditContext.tsx:126,:194,:205,:254approvalIsSubmitterthreaded throughInlineEditProvider, additive and optionalpackages/app-shell/src/views/RecordDetailView.tsx:1040const approvalIsSubmitter = isSubmitterOf(approvals.pendingRequest, user?.id);, passed at:2340packages/app-shell/src/hooks/useRecordApprovals.ts:228export function isSubmitterOf— one derivation, not twopackages/app-shell/src/views/RecordDetailView.approvalRecallGate.test.tsxpresent .changeset/6464-recall-submitter-gate.mdpresent ⇒ merged to main, pending release⇒ objectui#6464 is fixed and merged. The wait was lawful and it is now over.
⭐ Note the gate is
!== false, not=== true— deliberately tri-state. Their changeset explains why, and it is the right call: a host that resolves no approval identity renders exactly as before, because defaulting the unknown case to "hide" "would have traded a cosmetic defect for a functional loss: every host whose band runs off the record'sapproval_statusmirror alone would silently lose its submitter's only way to unlock their own record."⭐ And their changeset independently names the same split this card recorded, from the other side: "The untranslated refusal text … is a separate defect and is not addressed here; it is tracked on objectstack#11993." Two lanes, one seam, neither guessing about the other.
Both expectations are now closed
Expectation Where Status 1 Recall button must not be offered to non-submitters objectui — the server-side declaration here was already correct ( sys-approval-request.object.ts:511-513gatesvisibleonrecord.viewer.is_submitter == true); it was being ignored by the console✅ merged 2 Refusal text must not be raw English this repo — PR #12725, rendering through renderOperationMessagewithapproval_recall_not_submitter✅ merged Nothing is left in this card, so it closes.
pm:blockedremoved.
@baozhoutao — both halves of what you reported are fixed.
- The button no longer appears for a non-submitter on a pending record. There is deliberately no admin carve-out, per your own framing that ruled that case out.
- The error text is localized; you will see 「只有提交人可以撤回审批」-class copy rather than the raw
No pending approval request found for this record. TheFORBIDDEN:wire prefix that maps to HTTP 403 is unchanged, so nothing your integrations key off has moved.
⚠️ Neither has shipped in a release yet — both are merged and awaiting the next publish. You are on@objectstack/*@17.2.0. The refusal-text fix arrives in the next@objectstack/*release; the button fix is in the@object-ui/*packages your Console runs on and arrives in theirs. If you need a date for either, say so here.Two related findings came out of this report and are tracked separately, neither affecting what you saw:
- approvals: the
approval_recallaction hides the #3424 admin-override recall — the service admits it, the visible predicate never shows it #12716 — the recall action'svisiblepredicate omitsrecord.viewer.can_override, while the server does admit the override actor. So the stuck-record recovery path currently has no recall button for the one actor authorised to use it. That is the opposite defect to yours and is a real gap. - [finding] the zh-CN Operation Message Catalog punctuates with an ASCII comma, while the platform's other shipped zh-CN copy uses the full-width one #12717 — the zh-CN message catalog punctuates with an ASCII comma where the platform's other zh-CN copy uses
,. Cosmetic.
Thank you for the report — the reproduction was precise enough that the two halves could be separated cleanly and sent to the right repos.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026
现象(@objectstack/*@17.2.0,Console,record_change 审批流)
非提交人打开一条别人提交、正在审批中(pending)的记录,审批区/页头仍显示「撤回审批」按钮。点击后不产生任何状态变化(符合预期——服务端正确拒绝了),但用户看到的报错是:
中文前缀 + 英文服务端原文拼接,终端用户不可读。
复现步骤
期望
两点(可拆开处置):
No pending approval request found for this record这类拒绝原因应走 i18n 落中文,或客户端映射为人话(如「只有提交人可以撤回审批」)。补充
via_overridemarker is never surfaced in any UI #9464 讨论的那类);