Description
What happens
On a record's 审批 / Approvals tab, every timeline entry names the step by a title-cased version of the node id, not by the node's configured label:
提交 · Dev Admin · Review Reviewer
通过 · Dev Admin · Review Reviewer
提交 · Dev Admin · Review Director
The flow declares Chinese labels for those nodes, and the server already stores them. From sys_approval_action for the same request:
{ "step_name": "review_reviewer", "step_label": "初审人审批", "action": "approve", ... }sys_approval_request.current_step_label is also 初审人审批. So the data is right; the console timeline renders step_name (humanized) rather than step_label.
A related case: the admin_rescue system comment is written into the timeline as English prose ("Routed to this organization's administrators because the approver slate named no one: position 'srm_procurement_director' has no holder."). It shows up untranslated on a zh-CN console.
Repro
@objectstack/*18.0.0,objectstack dev, locale zh-CN.- App: objectstack-ai/hotsrm
main, flowsrm_admission_approval. Its nodes arereview_reviewer(label初审人审批) andreview_director(label采购总监审批). - Create an application, set its status to
admission_review, and approve both steps. - Open the record and go to the 审批 tab.
Expected
The timeline shows 初审人审批 / 采购总监审批 (step_label, or the translated label once #24863's contract applies). System comments use the viewer's locale.
Related
- Approvals show their flow and step names in the authoring language only: the request snapshots Flow.label / node.label at open, and the translation contract has no key for an approval step #24863 and plugin-approvals: the approval request page's highlights strip shows the current step's machine id (current_step, such as approve_step) where the approvals get serves the step's label only as step_label #24281 (both closed) cover labels in the request snapshot and the highlights strip. This issue is the timeline on the record's Approvals tab.
Re-filed as objectstack-fleet[bot] from #25471, same body and labels. #25471 was written off the fleet relay, under the maintainer's personal account, by a project-thread session through GitHub MCP; pm-dispatch allows content writes only through the relay. #25471's thread (triage, ruling, comments) stays there for reference; this card replaces it in the queue.
Session: session_018fnREjMeK9XQeHDwkJXKMW
Activity
objectstack-fleet commentedon Oct 24, 2026
Claim: PM loop round 1
Session:
session_01JVTd3PprStGa26aez5omLBAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-25576-admin-rescue-comment-i18nWorktree:
objectstack-issue-25576Domain:
domain:serverSeat:
domain:server#2File surface:
packages/plugins/plugin-approvals/src/**(theadmin_rescuesystem comment nearapproval-service.ts:3905one649c2d91and its read path; ⛔ not the SLA-reassign region PR #25600 edits), the plugin's translation bundles if it has them, tests, one changeset (stop on breach; explain in the report)Container & model:
S, not mechanical (the stored shape is a design call), so M treatment;mode:cloud(maintainer: 「并发加到5,可以用云卡片」),model: TIER_DEFAULT(dispatch-gates --tier: "no path-derived mandate … the tier stays the PM's per-card judgment call"),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
plugin-approvalswrites the sentence as stored English prose when the admin-rescue route fires | the platform's localized-text path for server-written system text; the dev names the precedent it reuses | a zh-CN (or any non-English) viewer of an approval timeline, whenever the admin-rescue route fires; measured in the hotsrm evaluation on 18.0.0Thread-read: none
Serial constraints cleared: PR #25600 (seat 1, #25598) edits
approval-service.tsin the SLA-reassign bodies (hunks near:5604and:7040–:7131), away from:3905; whichever lands second mergesmain. #25573's runtime half ispm:blockedon #25609, not in flight.Premise: triage's audit is 6371325002 on #25471 (the card this one re-files). Re-read on
origin/maine649c2d91:approval-service.ts:3905still composes`Routed to this organization's administrators because the approver slate named no one: ${routedToAdministrators}.`as the stored comment.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
os-dev-report
{
"issue": 25576,
"status": "done",
"branch": "claude/issue-25576-admin-rescue-comment-i18n",
"pr": "#25627",
"session": "session_01QeMwrPsf2zjo2pQLBzdWFV — this run's harness-stamped id (dispatched by domain:server seat 2, session_01JVTd3PprStGa26aez5omLB)",
"premise_still_valid": true,
"summary": "Premise held on origin/main e649c2d: openNodeRequest composed the admin_rescue note as English prose stored in sys_approval_action.comment, and listActions served it as stored. The note is now stored as a code plus parameters on the request's node-config snapshot (__adminRescueNote: {action_id, abandoned, fallback?}; no column, object schema or spec contract moves). Its sentence and clauses are new entries in the plugin's existing messages catalog (en/zh-CN/ja-JP/es-ES). listActions overlays the named row's comment, in place, at the reader's ExecutionContext.locale through the existing reader-locale-labels.ts path (#24863), extended by one member ReaderLabels.sentence. The stored comment stays the byte-identical English (one source: the en catalog entries). Rows stored before this change carry no snapshot entry and are served as stored; no migration. #25600 landed mid-flight and main was merged cleanly at b9c264f.",
"tests": "Head b9c264f, through os-verify-lock: pnpm --filter @objectstack/plugin-approvals typecheck exit 0; pnpm --filter @objectstack/plugin-approvals test -> Test Files 96 passed (96), Tests 1259 passed (1259), VERDICT command-exit 0. New: admin-rescue-note.test.ts (4 cases) and 5 cases in admin-rescue-named-slate.integration.test.ts on real ObjectQL+SqlDriver+createMemoryI18n loaded with ApprovalsTranslations: zh-CN listActions reads '审批人名单未指向任何人,因此已转交本组织管理员处理:岗位「sales_manager」无人任职。'; the stored row keeps the English; en/no-locale readers get the English; CONTROL: a pre-change request and a neighbouring system comment stay as stored. Ablation, via scripts/ablation-replace.mjs: deleting the localizeAdminRescueNote call (anchor 1->0, blob 3527886551f8->0c2167e97d4a) gave 3 failed | 13 passed (zh-CN, fallback, CONTROL red). Restore: blob 3527886551f8 = HEAD, git diff HEAD empty. Source imports resolve to src (relative import), so no dist build was needed for the ablation leg.",
"gates": "Local, head b9c264f: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 72 commands (same list pre- and post-merge); all 72 run, every exit 0, codes captured per command before any pipe. --ran reconciliation: '72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN' exit 0. Pre-merge run on f40e836 had check-plugin-teardown-shape --self-test exit 3 (shallow clone: pinned fixture commit 621a487 unreachable; fixed by fetching that one commit) and check:dts-closure exit 1 (plugin-approvals dist cleaned mid-turbo-rebuild running concurrently; 0 on re-run). check:i18n: 'check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys)'. eslint --no-inline-config on the 8 touched .ts files: exit 0. CI check-runs on #25627: not yet concluded at report time (in_progress is the honest value; the seat reads them).",
"line_budget": "S card; 9 files, +426 / -45 = 471 changed lines vs origin/main (dispatch-gates: under the 3000 human-merge threshold)",
"files_changed": [
".changeset/25576-admin-rescue-comment-i18n.md",
"packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts",
"packages/plugins/plugin-approvals/src/admin-rescue-note.test.ts",
"packages/plugins/plugin-approvals/src/admin-rescue-note.ts",
"packages/plugins/plugin-approvals/src/approval-notification-text.ts",
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/approver-address-readers.test.ts",
"packages/plugins/plugin-approvals/src/reader-locale-labels.ts",
"packages/plugins/plugin-approvals/src/translations/messages.ts"
],
"deviations": [
"Worktree: the harness had already created the outcome branch on the primary checkout (at e649c2d), so worktree add -b was refused. I detached the primary checkout and added ../objectstack-issue-25576 on the existing branch. The branch is platform-specified, so no empty probe push.",
"File surface: approver-address-readers.test.ts (same package, outside src/** only in being a test ledger) gained one PERSON_SITES classification for the new actor_id presence read. That pin requires every read of the column to be classified; no gate was weakened.",
"Commit trailers are the model-free pair AGENTS.md prescribes (Claude-Session + Co-authored-by: Claude); the harness reminder's model-named trailer was not used.",
"Closure build #1 was pnpm --filter '@objectstack/plugin-approvals^...' build --concurrency=2; that flag after the filter is forwarded to the scripts (a toolchain trap), harmless here."
],
"fixed_overhead": "pnpm install --frozen-lockfile: 40 s (Done in 36.6s). Build closure via the os-dev recipe (pnpm --filter plugin-approvals^... build, 34 projects, no turbo): 446 s. Turbo remote cache: NOT AVAILABLE in this container. The i18n gate's prescribed turbo closure printed 'Remote caching disabled, using shared worktree cache' with 0 cached / 61 tasks, 703 s, so the remote-cache pull is 0 s by absence, not a measured pull. Post-merge turbo rebuild: 17/33 cached (local), inside a 384 s locked run that also did typecheck + test.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "3 — mcp__github__issue_read x3 (get #25576, get_comments #25576, get_comments #25471); no write tool",
"api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #25627) + POST /repos//issues/25627/assignees; (2) this report -> POST /repos//issues/25576/comments. git push is not counted.",
"open_questions": [
{
"question": "Stored shape (implementation trade-off made in-card; recorded, not blocking): where do the code and parameters live?",
"options": [
"A — request node-config snapshot __adminRescueNote naming the action row (chosen): no column, no object-schema/bundle churn, no API shape change; precedent __flowLabel/__round/__resolvedFrom ('Stamped on the snapshot ... so no schema migration').",
"B — new columns on sys_approval_action (comment_key/comment_params): the code sits on the row itself, but it grows a platform object served by the generic data door, plus 4-locale field labels, for one system sentence."
],
"recommendation": "A, because it reuses the existing open-time snapshot and reader-locale overlay with no contract surface added (start-up axis: no new declaration surface). A second system note needing a code would be the pull for B."
},
{
"question": "objectui half (not measured: objectui not attached): does the console's record Approvals tab read the timeline through GET /api/v1/approvals/requests/:id/actions (localized now) or through the generic data door on sys_approval_action (still the stored English)?",
"options": [
"A — it uses the approvals actions route: nothing more to do",
"B — it reads sys_approval_action directly: objectui switches the timeline to the approvals door"
],
"recommendation": "Seat to check RecordApprovalsPanel's read in objectui and file the objectui half only if B."
}
],
"out_of_scope_findings": [
"carrier: domain:server seat 2 (sunhaolin) — the objectui read-path question above; noted, not filed (unmeasured from this container)"
]
}
objectstack-fleet commentedon Oct 24, 2026
ACCEPT → PR #25627 comment 6383830466 (head
b9c264fd8f; landing in the same act) ·domain:serverseat 2 ·session_01JVTd3PprStGa26aez5omLBGenerated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Landed: PR #25627 →
ca80f05ec·domain:serverseat 2 ·sunhaolin·session_01JVTd3PprStGa26aez5omLB· 2026-10-24T12:11Zmerged: true, merge commitca80f05ec5c6aab22f31553cb5f8737a17450563, merged 2026-10-24T12:11:03Z through the merge queueorigin/mainca80f05ecis an ancestor oforigin/main, single parent (squash)gh-readonly-queue/main/*head for PR 25627 remainscompletedbyFixes #25576on merge.pm:dispatchedstripped in this act.admin_rescuetimeline note is stored as a code and parameters, andGET /api/v1/approvals/requests/:id/actionsrenders it in the reader's language (en,zh-CN,ja-JP,es-ES). The console's record Approvals tab reads that door (objectuiuseRecordApprovals.ts), so nothing is owed in objectui. Older requests keep their stored English.session_01QeMwrPsf2zjo2pQLBzdWFVarchived in this act.Generated by Claude Code