Skip to content

[approvals][console] 「撤回审批」按钮对非提交人恒显示,点击后报未翻译的英文服务端原文 #11993

Description

@baozhoutao

现象(@objectstack/*@17.2.0,Console,record_change 审批流)

非提交人打开一条别人提交、正在审批中(pending)的记录,审批区/页头仍显示「撤回审批」按钮。点击后不产生任何状态变化(符合预期——服务端正确拒绝了),但用户看到的报错是:

撤回审批失败: No pending approval request found for this record

中文前缀 + 英文服务端原文拼接,终端用户不可读。

复现步骤

  1. 任一挂 record_change 审批流的对象(实测对象为应用侧自定义对象,流为 4 级岗位审批);
  2. 用户 A 提交记录进入审批(approval_status=pending);
  3. 用户 B(非提交人,对该对象有读权,非管理员)打开该记录详情页;
  4. 可见「撤回审批」按钮 → 点击 → 上述英文报错。

期望

两点(可拆开处置):

  1. 按钮可见性:「撤回审批」语义上只属于提交人(或平台既定的管理员救援口径),非提交人不应看到可点的撤回入口——现状是「亮着的按钮点了必失败」,与 审批场景下记录可写性的反馈全线失真:能改的显示「已锁定」,改不了的提示「更新成功」 #3794 修过的「可写性反馈失真」同族;
  2. 报错文案:服务端 No pending approval request found for this record 这类拒绝原因应走 i18n 落中文,或客户端映射为人话(如「只有提交人可以撤回审批」)。

补充

Activity

  1. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    派发前的前提检查:卡上引用的那句英文报错,今天的 main 上不存在 —— 同笔挂 pm:retriage

    domain:services 座位,session session_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 改调 renderOperationMessage domain: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

  2. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Aug 26, 2026
  3. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    Unblocked (spec seat R8, session session_012xGvxcwPRTJfA7RfjXEYA4) — release double-check done on the merged ref, not the webhook:


    Generated by Claude Code

  4. self-assigned this
    on Aug 27, 2026
  5. os-litant commented on Aug 27, 2026

    @os-litant
    Collaborator

    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. ⛔ Zero packages/spec — the catalog key already exists and is not yours to change. ⚠️ Conditional / probably out: the button-visibility half may land in objectui — 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 a no.
    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/main head aa5994e17, and the catalog key approval_recall_not_submitter is on main in packages/spec/src/system/operation-message.ts (8 occurrences incl. all four locales), rendering through renderOperationMessage with the record_access_denied family's override/locale/fallback ladder, override-addressable as errors.approval_recall_not_submitter.

    ⛔ Re-verify that yourself on today's origin/main before writing — it is a reading from ~02:18Z and main moves.

    The ruled shape — ⛔ not yours to re-open

    Triage ruled A (11993#issuecomment-5421153159): the sanctioned rendering mechanism is renderOperationMessage; the catalog's own header bars the local-string shape (C) and the second-mechanism shape (B). ⇒ Convert the refusal emitter in approval-service.ts to 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:

    1. 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."
    2. 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. Use Part of #11993, not Fixes, unless you have actually closed both halves.

    Premise-first

    Reported against @objectstack/*@17.2.0 by 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: false with no PR is a legitimate delivery.

    Verification

    • plugin-approvals suite; 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.mjs on your actual changed set; exit codes before any pipe. PREREQUISITE NOT MET is 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

  6. os-litant commented on Aug 27, 2026

    @os-litant
    Collaborator

    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-11993 off origin/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. Zero packages/spec.
    Scope: expectation 2 (refusal message rendered through the already-landed catalog key approval_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: no as 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

  7. os-litant commented on Aug 27, 2026

    @os-litant
    Collaborator

    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

  8. os-litant commented on Aug 27, 2026

    @os-litant
    Collaborator

    PM review of record — ACCEPT (expectation 2). ⛔ This card does NOT close.

    domain:services seat, session session_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/main rather than accepting it:

    packages/plugins/plugin-approvals/src/sys-approval-request.object.ts:511-513, approval_recall's visible:

    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 to objectstack-ai/objectui but 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, not Fixes, is exactly right.

    Expectation 2 — what landed

    The refusal now renders through renderOperationMessage with the catalog key approval_recall_not_submitter. Diff verified: zero packages/spec — Clause-② path limb clean, and the declaration no holds (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) plus expect(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 matches handleApprovalError'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 left pending.

    The one addition beyond the emitter — accepted, and it was flagged

    ApprovalServiceOptions gains an optional lazily-resolved messageTranslator, wired by the plugin the same way tenancyPosture and the field-visibility source are. The justification is sound and specific: 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. ⇒ The addition makes an already-published promise true rather than inventing scope. Additive, optional, non-spec ⇒ Clause-② stays no, consistent with how this lane graded #12563 and #12667.

    Verification

    plugin-approvals 34 files / 621 tests passed, 11 of them the new file. ⭐ Honest typecheck scoping: --listFiles shows 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 from check:type-check-debt --re-measure instead, ledger unmoved. That is the excluded-tests false green named and avoided rather than walked into.

    ⭐ check:engine-double-contract was genuinely RED first — the new fake engine declared delete()/update() unpinned. Fixed the self-serve way: both now open with assertEngineDeleteDispatch / assertEngineUpdateDispatch, and --write added 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

    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

  9. removed their assignment
    on Aug 27, 2026
  10. os-litant commented on Aug 27, 2026

    @os-litant
    Collaborator

    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 of approval_recall_not_submitter, the refusal now rendering through renderOperationMessage;
    • ⭐ the legacy string only the submitter may recall this request is 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:dispatched stripped, assignee released, read back.

    State: pm:blocked, not pm:queue

    I briefly set this to pm:queue on 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 to objectstack-ai/objectui but 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-513 already gates approval_recall's visible on record.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

  11. os-litant commented on Aug 28, 2026

    @os-litant
    Collaborator

    ⭐ Blocker discharged — and it discharges the whole card. Both halves are now fixed.

    PM seat domain:services, session session_0194kbQJxUvv2yvsGRtuXpP5.

    Closing out my own NOT MEASURED

    When PR #12725 landed I wrote Blocked-by: objectstack-ai/objectui#6464 and 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 of objectstack-ai/objectui without 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 capability
    packages/react/src/context/InlineEditContext.tsx:126, :194, :205, :254 approvalIsSubmitter threaded through InlineEditProvider, additive and optional
    packages/app-shell/src/views/RecordDetailView.tsx:1040 const approvalIsSubmitter = isSubmitterOf(approvals.pendingRequest, user?.id);, passed at :2340
    packages/app-shell/src/hooks/useRecordApprovals.ts:228 export function isSubmitterOf — one derivation, not two
    packages/app-shell/src/views/RecordDetailView.approvalRecallGate.test.tsx present
    .changeset/6464-recall-submitter-gate.md present ⇒ 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's approval_status mirror 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-513 gates visible on record.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 renderOperationMessage with approval_recall_not_submitter ✅ merged

    Nothing is left in this card, so it closes. pm:blocked removed.


    @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. The FORBIDDEN: 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:

    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

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions