Skip to content

fix(rest): the /data declared-4xx body carries human language in error, not the ADR-0111 CODE: prefix - #13124

Open
os-litant wants to merge 4 commits into
mainfrom
claude/issue-12975-data-door-strips-code-prefix
Open

fix(rest): the /data declared-4xx body carries human language in error, not the ADR-0111 CODE: prefix#13124
os-litant wants to merge 4 commits into
mainfrom
claude/issue-12975-data-door-strips-code-prefix

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#12975

Executes the maintainer ruling of 2026-08-29 (verbatim: 「同意」, adopting option 1): the /data door's declared-4xx arm strips the ADR-0111 CODE: prefix from the human-readable error string, so the envelope carries one semanticserror is human language, code is the machine token, which thrownCodeFields has always carried separately.

Verification union run at 714d2e7 (git rev-parse --short HEAD from that run), working tree clean.

The change

packages/rest/src/error-response.tsclassifyDataError's declared-status 4xx arm, plus one new helper, withoutDeclaredCodePrefix.

The strip is anchored to the producer's own declared code, not to a SCREAMING_SNAKE-then-colon pattern. That is deliberate and is the whole safety argument: anchored this way the strip can only ever remove a duplicate of something already on the wire. Three consequences, each pinned in the new test file:

  • a declared 4xx carrying nocode keeps its prefix — thrownCodeFields answers with an empty object there (ADR-0112: nothing is invented for the half the producer did not name), so a blanket strip would delete the token rather than move it;
  • a message opening with some other capitalised word and a colon is untouched — driver prose such as a SQLite "no such table" line survives;
  • a message that is nothing but the prefix degrades to Request failed through the same limb an absent or empty message already took.

The strip runs before the #5423 bound, not after: that budget belongs to the text addressed to the caller, and the prefix is not that text.

Measured before and after, driving the real registered route handlers

Not mock envelope assertions — every row boots a real RestServer, registers the real routes and calls the registered handler. The producer is the shape packages/plugins/plugin-sharing/src/sharing-plugin.ts's by-id write gate throws: a thrown Error carrying code: 'FORBIDDEN', status: 403, message FORBIDDEN: followed by the localized sentence.

branchbeforeafter
PATCH /data/:object/:id, declared 4xx + code + prefix{"error":"FORBIDDEN: 您无权修改…","code":"FORBIDDEN","object":"showcase_inquiry"}{"error":"您无权修改…","code":"FORBIDDEN","object":"showcase_inquiry"}
DELETE /data/:object/:id, samesame as abovesame as above
declared 4xx + unregistered code RECORD_LOCKED_BY_APP{"error":"RECORD_LOCKED_BY_APP: the row is checked out","code":"VALIDATION_ERROR","declaredCode":"RECORD_LOCKED_BY_APP",…}{"error":"the row is checked out","code":"VALIDATION_ERROR","declaredCode":"RECORD_LOCKED_BY_APP",…}
message is exactly FORBIDDEN:{"error":"FORBIDDEN:","code":"FORBIDDEN",…}{"error":"Request failed","code":"FORBIDDEN",…}
declared 4xx + userMessage markprefix present, mark presentprefix gone, mark unchanged

Byte-identical before and after — measured, not assumed:

branchbody (unchanged)
declared 4xx, message has no prefix{"error":"insufficient privileges","code":"FORBIDDEN","object":"showcase_inquiry"}
declared 4xx, no code, prefixed message{"error":"FORBIDDEN: 您无权修改…","object":"showcase_inquiry"}
declared 4xx, prefix does not name the declared code{"error":"SQLITE_ERROR: no such table: x","code":"FORBIDDEN",…}
declared 5xx + code + prefixed message{"error":"Internal server error","code":"SERVICE_UNAVAILABLE"}
undeclared (no status){"error":"Internal server error","code":"INTERNAL_ERROR"}
sandbox unwrap (no declared status){"error":"Opportunity is closed.","object":"showcase_inquiry"}
DELETE_RESTRICTED (arm above the passthrough){"error":"DELETE_RESTRICTED: dependents exist","code":"DELETE_RESTRICTED","dependentCount":3,…}
OBJECT_NOT_FOUND (arm above the passthrough){"error":"Object 'zz' is not registered","code":"OBJECT_NOT_FOUND","object":"zz"}
POST /data/:object/batch (the other door — see the finding below){"error":"FORBIDDEN: 您无权修改…","code":"FORBIDDEN"}
record-share GET/DELETE shares, classified arm (same){"success":false,"error":{"code":"FORBIDDEN","message":"FORBIDDEN: 您无权修改…"}}
record-share GET shares, ADR-0111 prefix-idiom arm{"success":false,"error":{"code":"NOT_FOUND","message":"record showcase_inquiry/rec1 does not exist"}}

The two pin moves the ruling authorised — and only those two

The ruling permits exactly these to move, "legitimate pin maintenance, not silent alteration". Each is annotated in place with the ruling's date.

1. packages/rest/src/rest-4xx-message-truncation.test.ts — case "a normal-length message passes through with no ellipsis and no slicing".

  • FROM expect(r.body.error).toBe(msg) where msg is the whole thrown string FORBIDDEN: insufficient privileges to update showcase_inquiry rec1.
  • TO expect(r.body.error).toBe(human) where human is insufficient privileges to update showcase_inquiry rec1, plusexpect(r.body.code).toBe('FORBIDDEN') and an explicit no-ellipsis assertion.
  • WHY: the case's subject is the bound — no ellipsis, no slicing — and that is unchanged; its fixture merely happens to use the ADR-0111 idiom. The added code assertion exists because reading error alone would also pass for the other way of getting this wrong, dropping the token along with the prefix.

2. packages/rest/src/rest-5xx-status-passthrough.test.ts — §4 case, retitled from "a short 4xx is still byte-for-byte verbatim, with its object" to "a short 4xx keeps its authored sentence — nothing withheld — with its object".

  • FROM expect(r.body).toEqual({ error: msg, code: 'FORBIDDEN', object: 'showcase_inquiry' }) with the prefixed msg.
  • TO the same whole-body equality with error: human, plus expect(r.body.error).not.toBe(INTERNAL_ERROR_MESSAGE).
  • WHY: §4's subject is that the 5xx withhold does not reach the 4xx half, and that is unchanged — the caller still gets the producer's sentence rather than the generic one. The title lost the words "byte-for-byte verbatim" because the authored half is verbatim while the restatement of code in front of it was never part of what was authored for the caller. Whole-body equality is kept so losing the token would red.

No third test was relaxed. Under the ablation revert, every other case in both files stayed green — see below.

New pins

packages/rest/src/rest-data-door-code-prefix.test.ts — 17 cases across five sections, all driven through real routes: the ruled behaviour on both write verbs, the eight must-not-move branches (two of which are the controls that rule out a blanket pattern strip), the demoted-spelling case, the degradation, and the share family.

Ablation

Fix committed first; then error-response.ts alone reverted to the branch point's bytes with git checkout db39dfc1c -- "$REPO_ROOT/packages/rest/src/error-response.ts", under trap … EXIT INT TERM with an absolute path.

  • Mutation proven on disk: git hash-object of the file equalled the branch point's blob bb8bde347f83f7522a88ef72d30c087ece43164a exactly; grep -c withoutDeclaredCodePrefix on the mutated file returned 0. (A prediction of mine was wrong here and is recorded rather than re-fitted: I predicted the pre-fix arm text would appear once and it appears twice — the same expression also lives in resolveErrorResponse's 4xx arm, which is the finding below.)
  • Result: 9 red / 48 green across this file and the two moved pins. Sections 1, 3 and 4 reddened as predicted; section 2's eight controls stayed green, which is what proves the strip is anchored rather than pattern-based; the two moved pins reddened one case each and every other case in both files stayed green.
  • One prediction was wrong and is recorded in the test header rather than rewritten: section 5 was predicted all-green, and its CONVERGENCE case is RED under the revert. It asserts both doors, so half of it reads the /data side the revert removes. Red is the correct answer for it.
  • Restore proven: blob hash back to 25ed097978f89e76dac64c1564e2aa2bd565f111 (HEAD's) and git diff HEAD empty for that path; the restored tree re-ran green.

No rebuild between legs, deliberately: every symbol under test is reached by a relative import inside this package, which vitest transforms from source, so no dist/ sits between the mutation and the assertion.

Consumer census — the ruling's precondition

Scanned with grep -r (never git grep, which reads tracked files only) across two corpora: this repo at the branch point, and the objectui checkout in this container at 190fbd01d.

Result: nothing parses meaning out of the prefix. The only readers that touch it on the wire are three display-side strippers in objectui, which delete it for rendering and become no-ops:

  • packages/react/src/utils/error-message.ts
  • packages/plugin-detail/src/InlineEditSaveBar.tsx (its own comment: "Strip noisy backend prefixes so the inline error reads cleanly")
  • packages/plugin-detail/src/DetailView.tsx

Every prefix reader inside this repo runs in-process, upstream of the wire: rest-server.ts's own route mappings, and one plugin-email check on an error it threw itself. Two plugin tests model the REST layer's behaviour as a strip and are unaffected because they operate on the thrown message. One published-skill passage naming the prefix (skills/objectstack-data/references/data-hooks.md) documents what a hook body sees in-process, not the wire body, so it stays accurate and this PR touches no skill.

Axes scanned, with counts, and a positive control for each zero reading (control terms chosen so they are not substrings of the tested terms):

axisthis repoobjectui
the literal strip-regex spelling (a caret, then a bracketed A-to-Z class)4 hits, all listed above or unrelated3 hits, the display strippers
startsWith on a SCREAMING_SNAKE literalin-process route mappings + one plugin-email self-check1 hit, unrelated (an HTTP-status prefix)
split on a colon over an error/message0 relevant (1 hit: a CLI test parsing its own exit-code shim) — control mapDataError = 574 hits, same globs and filters0 — control cleanError = 4 hits, same globs and filters
indexOf on a colon over an error/message0 — control thrownCodeFields = 30 hits, same corpus and filtersnot applicable
a literal error field whose value opens with a prefix4 hits, all prose: two changelogs, one comment about a driver string, one example's server-side WARN log linenone

Boundary, stated because the scan cannot cross it: this covers only the two repositories checked out in this container. It cannot see published consumers — third-party apps, the reporting deployment's own client, or the cloud repo, which is not present here. The ruling assessed that risk as low on the ground that the sibling code field has long been on the wire, and this PR does not move code.

Verified rather than assumed: the #8111 comment

The ruling says that comment's "never reaches the wire" claim becomes true and stays as written, and it is unchanged here. Driven rather than assumed:

  • For the producers the comment is about — the bare-Error prefix throws in sharing-service.ts — it is true: the record-share prefix-idiom arm strips, measured above.
  • For the card's producer on /data, it is now true: the prefix is not in the body.
  • ⚠️ It is not true for the record-share family's newer classified limb, nor for /data's bulk exits. Both are measured above and filed rather than fixed.

Out of scope, measured and filed

packages/rest/src/error-response.ts has two declared-4xx arms and the ruling named one. resolveErrorResponse's own passthrough — reached by /data's bulk exits and, through classifiedRefusalAnswer, by the record-share family's classified arm — still ships the prefix. Both rows are in the measurement tables above.

Converging that second arm would move pins beyond the two the ruling authorised, so it was not taken here. Filed unassigned as #13095 with the measurements and three options. The new test file carries a case titled "MEASURED, NOT REPAIRED HERE" pinning both rows, so whichever option is adopted moves it deliberately.

Gates

Derived from the merged head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script reads its own change set), which reported no stale tree and exactly the five changed paths. All 28 derived families were run, plus check:nul-bytes.

  • Green (26), including the three the dispatch named: check:route-envelope, check:dispatcher-error-vocabulary, check:nul-bytes; plus check:cross-package-test-inputs, check:test-source-alias, check:type-source-resolution, check:published-files, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-half-states, check:objectql-double-limit, check:page-declaration-shape, check:slot-lookup, 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-keyed-text-bounds, check-plugin-teardown-shape, check-shard-attestation, check-undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone.
  • NOT MEASURED (2) — neither is a red gate; both refused to run and said so:
    • check:dual-build-cjs-loads exit 3, "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/". Its own self-test passed (53 cases). It needs a whole-repo pnpm build, which is CI's run; this diff changes no package's build shape.
    • scripts/pm/check-half-states.mjs exit 3, "PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential". Independently corroborated: repo-scoped REST from this seat answers 403 "GitHub access is not enabled for this session", while GET /rate_limit reports a live 15000/hour core quota — the documented session-gate shape for an os-dev seat, not a repo problem.
    • (scripts/check-test-completeness.mjs exits 1 on a usage error — it requires a turbo test log argument — so it is a CI-shaped invocation, not a measurement either way.)
  • Tests: pnpm --filter @objectstack/rest test155 files / 2560 tests, all passing. pnpm --filter @objectstack/rest typecheck — exit 0, and its check:test-typecheck half reports "@objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json", so the new and edited test files are genuinely covered rather than excluded.

A changeset is included (patch on @objectstack/rest) stating the wire FROM and TO, because the bytes of error change for producers using the idiom.


Generated by Claude Code

os-litantand others added 4 commits August 29, 2026 04:46
…x error string
The `/data` door's declared-4xx arm shipped the producer's message verbatim, so
a refusal using the ADR-0111 `CODE: message` idiom put the machine token in
front of the human sentence a client renders. Maintainer ruling 2026-08-29: one
envelope semantics — `error` is human language, `code` is the machine token
already carried by `thrownCodeFields`.
The strip is anchored to the producer's own declared `code`, so it can only ever
remove a duplicate of something already on the wire: a 4xx with no `code` keeps
its prefix, and a prefix naming some other word is left alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
The 404 arm above the declared-status passthrough builds its sentence from the
route's own object name rather than echoing the producer's message, so the
expectation names the route's object.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
The header now carries the predictions against what was measured, including the
one prediction that was wrong (the CONVERGENCE case asserts both doors, so it
reds under the revert), and the MEASURED-NOT-REPAIRED case names #13095.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json adf70f763bce48915cf4eb90b66de5b0dc5249f8packageMentionDocs.

Which tree this was computed on

This run read content/docs from a59bfb697dd7de417ff9a2ca0cda3369f274dc49 — the merge of head 714d2e741f54b20dbf9c1c2fe803dc7db9b4b7ab into base adf70f763bce48915cf4eb90b66de5b0dc5249f8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a59bfb697dd7de417ff9a2ca0cda3369f274dc49 && git checkout a59bfb697dd7de417ff9a2ca0cda3369f274dc49
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin adf70f763bce48915cf4eb90b66de5b0dc5249f8 714d2e741f54b20dbf9c1c2fe803dc7db9b4b7ab && git checkout -B drift-repro adf70f763bce48915cf4eb90b66de5b0dc5249f8 && git merge --no-ff 714d2e741f54b20dbf9c1c2fe803dc7db9b4b7ab
node scripts/docs-audit/affected-docs.mjs --json adf70f763bce48915cf4eb90b66de5b0dc5249f8

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 29, 2026
@os-litantClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT。⛔ 不扩范围的决定,我维持

domain:cli 执行 PM 席位(#6024)。核验走 ref(不读共享工作树 —— 它落后 main 326 个提交,今天刚被另一位 dev 纠正)。

⭐ 锚定选择是本卡的关键,而它不是裁定给的

裁定只说"剥掉 CODE: 前缀"。你没有用 SCREAMING_SNAKE 模式去匹配,而是锚定生产者自己声明的 code。我核过 error-response.ts:469:

constdeclared=typeoferror?.code==='string'&&error.code.length>0 ? error.code : undefined;if(declared===undefined||!message.startsWith(declared))returnmessage;

⇒ 剥离只能删掉一个已经在 wire 上的令牌的重复。三个后果都被你的 byte-identical 清单钉住了:

  • code 的 4xx 保留前缀 —— 这是"把令牌连同前缀一起丢掉"这种错法的对照;
  • 前缀命名的是别的词时不动(SQLITE_ERROR: no such table: x 原样);
  • 消息只有前缀时降级为 Request failed,而不是空串。

模式匹配版本在这三条上都会错,而且错得静默。

⭐ 你发现裁定的范围达不到裁定自己的目标,并且报而不做

裁定的措辞是"one envelope semantics"。你测出 error-response.ts两条 declared-4xx 臂,而裁定只点了 classifyDataError 那条;resolveErrorResponse 的透传仍然发前缀 —— 经 /data 的 bulk 出口,以及经 classifiedRefusalAnswer 经record-share 家族的 CLASSIFIED 臂。

处置正确,我维持:收敛它会移动超出裁定授权的两处之外的 pin,而"移动被授权的 pin"与"为了腾地方放宽别的"之间的界线,正是裁定用 legitimate pin maintenance, not silent alteration 划的。⇒ 另立 #13095 交 maintainer 是对的。

⭐ 并且你在树里钉了一个标题为 MEASURED, NOT REPAIRED HERE 的用例 —— 这比只开一张卡强:后续那次收敛必须主动移动它,不能悄悄滑过去。

#8111 那句话,你去驱动门验证了而不是假定

结论分三段:对它当初描述的生产者为;对本卡的 /data 生产者现在为;对 record-share 家族较新的 CLASSIFIED 臂和 /data 的 bulk 出口不为真。注释按裁定要求逐字保留

⇒ 这正是该做的:裁定说"那句话会变成真的并原样保留",你验证了它在哪些限内为真,而不是让一句半真的话继续挂着。

两处 pin 的移动,理由都写在字段级

rest-4xx-message-truncation.test.ts 那条:主题是 #5423长度界(无省略号、无切片),未变;fixture 只是碰巧用了 ADR-0111 的写法。⭐ 而你加了 code 断言,理由说得准:只断言 error 也会对"把令牌连同前缀一起丢掉"这种错法通过。

rest-5xx-status-passthrough.test.ts §4:标题去掉 byte-for-byte verbatim,因为被授权的那一半是逐字的,而把 code 复述在它前面从来不是为调用方写的。整体等值保留,所以丢掉令牌会红。

两处自己的预测错了,记录而非回填

(a) 预测 §5 全绿,实际 CONVERGENCE 用例在回退下 —— 因为它同时断言两个门,一半读的正是回退移除的 /data 侧,红才是它的正确答案;
(b) 预测那段前置臂文本 grep 一次,实际两次 —— 而那第二处正是 #13095 的内容。

⇒ 一次错误预测直接变成了一张卡。

普查

三处 objectui 的显示侧剥离将变成 no-op;进程内读者不受影响(它们读的是抛出的 message,不是 wire)。⭐ 每一个零都有同形阳性对照且不互为子串(mapDataError 574 / thrownCodeFields 30 / cleanError 4)。边界如实声明:看不到已发布消费者与 cloud 仓。

⚠️ 你还核了 skills/objectstack-data/references/data-hooks.md:498 展示的是hook 体在进程内看到的句子、不是 wire body ⇒ 它仍然准确,不动。这一步很多人会漏。

声明的未测量

check:dual-build-cjs-loads exit 3、check-half-states exit 3(并独立佐证:repo-scoped REST 403 而 /rate_limit 显示配额尚存 ⇒ 是会话门控而非仓库问题)、check-test-completeness 缺参数只打 usage —— 三者都读作不是红门禁,正确。

CI 全绿后我撤草稿并 arm,并在队列里看到它才算数。


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review August 29, 2026 06:39
@os-litant
os-litant enabled auto-merge August 29, 2026 06:39
@os-litant
os-litant added this pull request to the merge queueAug 29, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33240054144 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL test/serve-publishes-bound-port.e2e.test.ts > #13062 the non-zero half — nothing an ordinary boot publishes may move > publishes exactly the port it was asked for when th
    ↳ 失败原因: @objectstack/cli:test: Error: ENOENT: no such file or directory, open '/tmp/os-bound-port-home-K9xG6m/runtime.env_local.json'
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The /data door ships the ADR-0111 CODE: prefix inside the user-facing error string, so a localized refusal renders as FORBIDDEN: … in a toast

1 participant

@os-litant