Skip to content

console: every list navigation sends two POST /security/explain and a PATCH sys_user_preference, approvals and inbox poll repeatedly, and Cmd+K drops the first keystrokes — measured on a HotCRM tenant #32627

Closed

Description

@objectstack-fleet

coordinator#1 · hotlong · session_6eb9ec2e0e3f410b91239dab7a337b9e

Filing gate: ① defects, measured in the maintainer-requested browser pass of HotCRM 6.1.2 on a hosted staging environment (2026-11-12). The maintainer agreed to file: 「同意立卡」.

Reader: the domain:ui lane (console list views, user preference writes, the command palette), and domain:server for security/explain if the call belongs there. Suggested priority p2. Triage decides.

Dedupe (objectstack, open and closed):

  • security/explain: 10 hits, none about how often the console calls it per navigation.
  • sys_user_preference PATCH: 4 QA-run cards, none for the per-navigation write.
  • command palette keystrokes: 0.

维护者速读

在一个托管的 HotCRM 环境里,浏览器每打开一个列表都要多发好几个请求:两次权限解释、一次写用户偏好;审批和通知还隔一会儿就轮询一次。网络慢的时候,这些都会拖慢页面。另外,按 Cmd+K 后马上打字,前几个字会丢。

Readings

Taken in the browser pane, on a staging environment with HotCRM 6.1.2. Network log from navigating contracts, quotes, campaigns, forecasts, dashboards, an account and a lead.

  1. Per list navigation: POST /api/v1/security/explain ×2 and PATCH /api/v1/data/sys_user_preference/<id> ×1, on every list, every time.
  2. A grouped list fetches per group. The forecast list grouped by a field issued one GET /data/crm_forecast?…&filter=[period_label = …] per group (7), plus 14 security/explain calls.
  3. Polling: GET /approvals/requests?status=pending… and GET /data/sys_inbox_message?top=20… repeat throughout, alongside each navigation.
  4. Cmd+K: the palette opens a moment after the shortcut, and characters typed in that moment are lost (typing 「Acme」 straight after the shortcut left the input empty).
  5. Not concluded: the executive dashboard's charts took about 20–25 s to appear. This machine's link is slow, and the tenant responses carried no Server-Timing, so server time could not be separated. Re-measure on a normal link before acting on it.

Done when

  • One list navigation sends at most one permission read and writes preferences only when a preference changed.
  • A grouped list fetches in one request.
  • Polling backs off when nothing changes.
  • Cmd+K keeps the keystrokes typed while it opens.

🤖 Generated with Claude Code

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:uiDomain lane — seat card indexed by label:pm:seat
    on Nov 12, 2026
  2. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: first touch (requested by domain:ui#1) → domain:ui · p2 · pm:queue · 2026-11-12T08:53Z

    ⛔ Not a claim.

    • Lane: domain:ui (console list navigation, sys_user_preference writes, approvals/inbox polling, the command palette). If the /security/explain cadence turns out to be a server answer rather than a console call pattern, the claimer splits that part to domain:server.
    • Grade: p2, as suggested. Measured on a hosted HotCRM tenant; filed on the maintainer's 「同意立卡」.

    Generated by Claude Code

  3. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Nov 12, 2026
  4. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-lizheng · session_012MmDsRT6hvZsQH9dTzn4CA

    Claim: PM loop round 2 · 2026-11-12T20:06Z
    Session: session_012MmDsRT6hvZsQH9dTzn4CA
    Account: os-lizheng
    Branch: claude/issue-32627-list-nav-request-budget
    Worktree: objectstack-issue-32627
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: the measured call sites, with their tests. The seat's read at origin/main found:

    • the security/explain callers in plugin-grid (hooks/useRecordCrudVerdicts.ts, rowCrudAffordances.ts);
    • the sys_user_preference writers in app-shell (context/ListPreferencesProvider.tsx and its siblings);
    • the grouped-list fetch in plugin-grid / plugin-list;
    • the pollers in app-shell (hooks/sharedUserFeeds.ts, hooks/inboxArrivals.ts, hooks/useRecordApprovals.ts);
    • the palette in app-shell (chrome/CommandPalette.tsx, context/CommandPaletteProvider.tsx);
    • root .changeset/.

    ⛔ Not app-shell/src/views/metadata-admin/** (domain:ui#6 #32774). ⛔ Not views/studio-design/** (this seat's #32610). ⛔ No server code: a server-side part is split to domain:server (stop on breach; explain in the report).
    Container & model: L, mode:subagent, model: default tier (dispatch-gates --tier: "no path-derived mandate"), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the console's list navigation sends POST /security/explain twice and PATCH sys_user_preference once per list; a grouped list fetches once per group; approvals and inbox poll without backing off; Cmd+K drops the first keystrokes (objectstack ui/) | no platform path covers it | every console user, measured on a hosted HotCRM 6.1.2 tenant on 2026-11-12
    Thread-read: 6819160849
    Premise: the card's readings were measured in a browser, and the seat's read of origin/main 6aa95dca finds each named call site present. Whether each count is a console call pattern or a server answer is the dev's measurement. Triage 6819160849 splits a server part to domain:server.
    Serial constraints cleared: domain:ui#6 in flight on #32770 (fields, types, data-objectstack, components form, plugin-form), #32774 (metadata-admin/**, the StudioDesignSurface clone host) and #32794 (app-shell type-check heap, tooling), all disjoint at file level from this surface. This seat's #32432 (login form, i18n) and #32610 (studio-design, the copilot draft path) are disjoint. Merges main before its PR.

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 12, 2026
  6. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-lizheng · session_012MmDsRT6hvZsQH9dTzn4CA
    os-dev-report

    {
     "issue": 32627,
     "status": "done",
     "branch": "claude/issue-32627-list-nav-request-budget",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/32853",
     "session": "session_012MmDsRT6hvZsQH9dTzn4CA",
     "premise_still_valid": true,
     "summary": "Draft PR #32853 (Part of #32627, 12 files, +721/-13) delivers the four console call patterns, each measured first with in-process harnesses over the real components at origin/main 6aa95dca5. (1) Grouped list: ObjectGrid asked POST /security/explain each time a group's page landed (objectGridGrouping.ts grew `data` per group); rows are now handed to the verdict probe once all open groups have answered: 2 requests (update, delete) instead of one pair per group (harness 6 -> 2, tenant 14). (2) Preferences: the per-navigation PATCH is the recents row ui.recent (RecentItemsProvider via useTrackRouteAsRecent), one per navigation to a list that is not already most recent, a real change already guarded; ui.list.* wrote 0 per navigation. ListPreferencesProvider compared by identity, so a no-op patch (re-stated sort/columns, Clear all over nothing, a change undone inside the debounce) wrote; it now compares value with the previous state and with the value the store holds. (3) sharedUserFeeds inbox/approvals: an unchanged poll answer doubles the next delay up to 3 min (the hidden cadence); a changed answer, tab return or push-channel change restores 60 s; 10-min quiet window 11 -> 5 reads each, no arrival dropped (whole window read each tick). (4) CommandPaletteProvider holds printable keys + Backspace from the open request until the palette input has focus (2 s cap) and CommandPalette applies them: 'Acme' typed in the opening window now lands. Not done, needs a server capability (split to domain:server): one permission read per list (ExplainRequestSchema takes one operation, so update+delete = 2 requests by contract) and one request for a grouped list (per-group paging, ruling A objectui#7189; no per-group window in the data door). Item 5 untouched.",
     "tests": "Final head 7f46652ef: the six pin files `pnpm --dir ui exec vitest run` (serverGroupedVerdictProbe-32627, ObjectView.userListState-12418, sharedInboxFeed.cadence-7249, sharedUserFeeds.pushChannel-27626, sharedInboxFeed.transient404, CommandPalette.typeahead-32627) -> Test Files 6 passed (6), Tests 71 passed (71). Scoped suites (pre-merge head bc05ceb85/b4f392cd6, files importing the changed modules): grid grouping + shared-feed consumers 48 files -> 46 passed, 2 failed = the two old-behaviour pins (pushChannel 60 s window expected 11 got 5; transient404 revived-cadence expected >6 got 5), updated on purpose, then those two files 15/15 passed; palette/AppHeader/ObjectView list-state/preference-budget 60 files -> 502/502 passed; plugin-list grouping + ListView 18 files -> 441/441 passed. Typecheck: @object-ui/plugin-grid type-check exit 0; @object-ui/app-shell tsc --noEmit exit 0, tsconfig.test.json exit 0 after building @object-ui/plugin-calendar (env only). eslint changed ui files: 0 errors (55 warnings, existing patterns). Ablation (source only, no dist path: ui tests alias @object-ui/* to src): five fix files restored from 6aa95dca5 with blob-hash proof (mutated == base blob != HEAD blob), pins run -> Test Files 5 failed | 1 passed (6), Tests 10 failed | 61 passed (71): grouped 4 requests vs 2; palette expected '' to be 'Acme' x2; list-state save called 1x where 0 expected x2; polling expected 11 to be 5, expected 7 to be 4 x2, pushChannel {11,11} vs {5,5} and vs {11,5}; all other CONTROLs green; restore via git checkout HEAD, every blob back to HEAD hash, git diff HEAD empty.",
     "gates": "dispatch-gates --commands derived 37 families on f9341b0ad (after merging origin/main); all 37 ran locally: 36 exit 0, pnpm check:test-source-alias exit 1 (edited 12418 test reaches lazy import('@objectstack/lint')) -> fixed by a module-top import in 7f46652ef, re-run exit 0 ('check-test-source-alias OK'). `pnpm --dir ui exec vitest run scripts/` under the lock: VERDICT command-exit 0, Test Files 171 passed | 2 skipped. --ran reconciliation: 37 derived, 37 run, 0 NOT-MEASURED, 0 UNRUN (its own line notes the record carries no exit codes; the codes above are from my per-gate log). check:new-line-citations: 0 new. check:nul-bytes exit 0. PR #32853 check-runs at 7f46652ef read once right after create: Check Changeset, Check PR Size, Governed Surface Queue Guard, Lint (ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, CI release & engine gates, scoped x3), PR-body guards, Type Check (workspace, source gates, consumer gates, debt ledger) all in_progress; opt-in/queue/schedule jobs skipped. Not awaited (CI convergence is the seat's).",
     "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
     "mcp_calls": "0",
     "api_writes": "2 — (1) relay pr_create: POST /repos/objectstack-ai/objectstack/dispatches (session) -> fleet-write run 43315231576 sent POST /repos/objectstack-ai/objectstack/pulls (draft #32853) + POST /repos/objectstack-ai/objectstack/issues/32853/assignees [os-lizheng]; (2) this os-dev-report via post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/32627/comments. git push not counted.",
     "open_questions": [
      {
       "question": "Card clause 'one list navigation sends at most one permission read': a flat list sends exactly two explain requests (update, delete) because ExplainRequestSchema takes one `operation`. Change the contract or accept two as the floor?",
       "options": [
        "A: spec + server accept several operations in one batched explain request (domain:server card; ExplainRequest gains an operations list, response keyed by operation). Need: halves a measured 2 small POSTs per list; long-term: clean contract-first API but a second request shape beside the single-op one; AI-error: neutral if the old shape stays strict; startup: new API surface, default no.",
        "B: accept one request per operation as the list's budget and close that clause as 'one permission read per operation'. Need: zero work, the measured cost is two batched POSTs; long-term: no new shape; AI-error: neutral; startup: nothing added.",
        "C: ask `delete` lazily when a row's menu opens. Need: saves one POST, but the kebab's Delete would appear then vanish; long-term: a second verdict timing path; AI-error: neutral; startup: adds client complexity."
       ],
       "recommendation": "B, because the four axes favour adding nothing: the remaining cost is two batched requests per list, and A adds a second request shape the startup axis defaults against; A only if the maintainer wants the literal one-read budget."
      },
      {
       "question": "Card clause 'a grouped list fetches in one request': each open group pages its own rows (ruling A objectui#7189). One request for N paged groups needs a per-group window the data door does not have. Build it?",
       "options": [
        "A: domain:server adds a grouped-rows door (first page of each group in one query). Need: 7 group GETs -> 1 on a seven-group list; long-term: a new data-door capability to keep; AI-error: neutral; startup: new capability, default no.",
        "B: keep per-group paging as ruled. Need: unchanged; long-term: matches the ruling; AI-error: neutral; startup: nothing added.",
        "C: client-side, fetch all small groups in one `$or` find and partition by key. Need: fewer requests when groups are small; long-term: re-implements the group key predicate client-side (a second dialect), wrong under concurrent writes; AI-error: worse; startup: neutral."
       ],
       "recommendation": "B, because C rebuilds a second key dialect (against contract-first) and A is a new capability the measured need (one list) does not yet pull; revisit A if grouped lists prove slow on a normal link."
      },
      {
       "question": "The tenant's per-navigation PATCH is the recents row ui.recent, a real change (new head) on every navigation to a different list. Keep writing recents per visit?",
       "options": [
        "A: keep (one small PATCH per navigation to a new list, already skipped on revisiting the head).",
        "B: coalesce recents writes over a longer window and flush on tab hide / sign-out (fewer PATCHes; a closed tab can lose the last visits on other devices).",
        "C: keep recents per browser only (no remote write; loses cross-device recents)."
       ],
       "recommendation": "A, because the write carries real information, the store already guards no-op writes, and B/C trade correctness or a shipped cross-device feature for one small request."
      }
     ],
     "security_findings": "none",
     "out_of_scope_findings": [
      "carrier: #32627 remaining half -> domain:server · one permission read per list navigation needs the explain contract to accept several operations · evidence: packages/spec/src/security/explain.zod.ts ExplainRequestSchema has a single `operation: ExplainOperationSchema`; useRecordCrudVerdicts sends one batched request per operation; harness at 6aa95dca5 and 7f46652ef: flat list = 2 requests (update 14 ids, delete 14 ids) · dedupe words: explain batch operations, multi-operation explain, security/explain list budget, recordIds operations",
      "carrier: #32627 remaining half -> domain:server · a grouped list in one request needs a per-group row window in the data door · evidence: useServerGroupRows sends compileListViewGroupRowsQuery per open leaf group with $top/$skip per group (ruling A objectui#7189); query window functions are retired (packages/spec/src/migrations/entries/semantic/17.query-window-functions-retired.ts); harness: 1 header query + 1 list window + 7 group pages for 7 groups · dedupe words: grouped list one request, per-group window, group rows batch, server grouping rows query",
      "carrier: none · noted, not filed · the per-navigation PATCH is ui.recent (recents), measured 1 per navigation to a list that is not the most recent and 0 on revisiting it; a real change, listed as an open question for the seat"
     ]
    }
  7. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-lizheng · session_012MmDsRT6hvZsQH9dTzn4CA

    Landed (part) · 2026-11-12T23:09Z

    Reading Value
    PR #32853 MERGED as 75102c8050b0eb3d3c6ba8f958c0fe722f519e52 (Part of #32627)
    Delivered A grouped grid asks the write verdict once per operation; preferences are written only on a real change; inbox and approvals polls back off; Cmd+K keeps the keys typed while it opens.

    ACCEPT → PR #32853 comment 6837140213. One queue ejection was infrastructure (a checkout timeout, 6837991682), re-queued once and merged.

    The card stays open. Two of its "Done when" clauses need a new server capability. Whether to build them, or to reword the clauses to today's floor, is the maintainer's decision; the decision box follows in the next comment. pm:dispatched → needs-user-decision in this stroke, assignee cleared.

  8. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-lizheng · session_012MmDsRT6hvZsQH9dTzn4CA

    待裁 · #32627 剩余两条验收是否还要做(需要服务端新能力)· 2026-11-12T23:10Z

    ⛔ Not a claim. PR #32853 已交付控制台侧四项。卡片「Done when」里有两条只有新增服务端能力才能做到。砍掉或改写验收归维护者,席位不代裁,转入决策箱。

    一句话问题: 列表导航目前固定发 2 次权限解释请求(update、delete 各一次),分组列表仍是每组各发一次取数请求。是加服务端能力把它们压到 1 次,还是把这两条验收改为现状?

    已交付(PR #32853,Part of #32627):

    • 分组列表的权限解释由「每组一对」改为「每个操作一次」:7 组从 14 次降到 2 次。
    • 偏好只在真正变化时才写。
    • 审批与通知轮询在无变化时退避到 3 分钟,不丢新消息。
    • Cmd+K 打开过程中输入的字符不再丢失。

    前提(实测,dev 报告 6837079447):

    1. ExplainRequestSchema(packages/spec/src/security/explain.zod.ts)只接受单个 operation。所以一个平铺列表最少 2 次请求,每次带当前页全部行 id。
    2. 分组列表每组自行分页(ruling A,objectui#7189),数据接口没有「每组取首页」的窗口;query window functions 已退役。
    3. 卡上每次导航都有的 PATCH sys_user_preference 是「最近访问」(ui.recent):每次进入一个不是当前最近项的列表时写一次,是真实变化,重访同一列表不写。

    协议声明: 选项 1A 改变已发布的 explain 契约(新增多操作请求形状);2A 新增数据接口能力。其余选项不改协议。

    选项 × 代价 × 工作量:

    问题 选项 客户可感知后果 工作量
    1 权限解释 A:explain 契约支持一次请求多个操作 每个列表少一个小 POST domain:spec + domain:server + ui,约 3 个 PR
    B:接受「每个操作一次」为预算,改写该条验收 无变化(已是 2 个批量请求) 0
    2 分组取数 A:服务端新增「各组首页一次取回」 7 组的列表从 7 次取数变 1 次 domain:server 新能力,约 1–2 个 PR
    B:维持每组分页(既裁 ruling A),改写该条验收 无变化 0
    3 最近访问写入 A:保持每次进入新列表写一次 无变化 0
    B/C:批量写或只存本机 少一个小请求;关闭标签可能丢最近项,或失去跨设备同步 ui 1 个 PR

    os-decision-facets

    • ① 项目长远合理性:1B、2B 不新增契约形状。1A 让 explain 多出第二种请求形状。2A 是新的数据接口能力,需长期维护。
    • ② 实际业务拉动:实测剩余成本是每个列表 2 个批量小请求,加上分组列表按组数取数。目前只有一个 HotCRM 慢链路测量,没有正常链路上的慢证据。
    • ③ 防 AI 犯错:各选项中性。2 若走客户端 $or 合并,会重造分组键方言,已被否。
    • ④ 创业阶段不扩散:1B、2B、3A 不新增任何能力面。

    推荐:1B、2B、3A。

    • 理由:剩余开销已是常数级批量请求,新增契约和能力的长期成本大于收益。
    • 推翻条件:若正常链路上实测分组列表仍慢,再开 2A。
    • 置信缺口:卡上第 5 项(仪表盘 20–25 秒)尚无正常链路读数。

    裁后执行:

    Analysis-tier: seat tier (default), not the ceiling tier.

  9. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 12, 2026
  10. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Brief-claimed-by: session_01Q4EjjMnyLqJdzuktEmRPXH · batch #2 · 2026-11-13T04:46Z

    Decision batch #2, taken on the maintainer's 「5张一批按总监决裁格式给我」; batch #1 was answered 「以上原则同意」.

  11. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Ruling recorded · #32627 · 1B · 2B · 3A · 2026-11-13T05:22Z

    Maintainer, verbatim (triage#1 session): 「32677 暂停,先由元数据项目自己处理。其他同意」, answering decision batch #2 (proposed 32627 1B·2B·3A / 32749 A / 32979 A / 32677 B / 32688 C). The batch was analysed independently at the fable tier, with prior rulings checked first.


    Generated by Claude Code

  12. removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions