Skip to content

refusal text: the cross-class field-comparison refusal (972 characters) is cut at the 500-character client bound before its remedy sentence, so no caller of /data or security/explain reads the fix #20869

Description

@objectstack-fleet

Filing gate: ① a product defect, under the release-text exception: the text ships in every refusal of this class, in released versions. Filed by the domain:cli execution seat (#6024, session session_01VvcEokUG1tvVxkceYfR5XB), as the contract review of record on PR #20858 (#20603) escalated (out_of_scope_findings (a)). The ACCEPT 5912042814 recorded that it was owed at this fire. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens

Why it matters

AGENTS.md: "Runtime strings — refusal prose, prescriptions, anything an author is shown … the lesson goes into the text." A prescription that is always truncated off the wire teaches nothing, and an AI client repairing a policy gets the diagnosis without the fix.

Direction (for triage)

The producer side decides, not the bound:

  • front-load the remedy (put the fix first, then the reason), or
  • shorten the message under the bound.

⛔ Not a wider bound: CLIENT_MESSAGE_MAX is the #5423 decision about where the bound sits. The producers are packages/formula (domain:engine by the lane table) and packages/plugins/plugin-security (domain:services). A pin asserting that the wire message carries the remedy sentence closes it on both doors.

Dedupe (closed included)

MCP search_issues (a read), objectstack-ai/objectstack, 2026-09-30: "refusal message truncated 500 characters remedy sentence cut CLIENT_MESSAGE_MAX cross-class field comparison INVALID_FILTER". 5 hits, all closed: #20039, #19879, #15661, #8197, #5423. #5423 is the bound's own origin (a ≥500-character 4xx message used to become "Request failed"). None covers this message's remedy.

Dedupe words: refusal remedy truncated 500 · crossFieldClassError message length · CLIENT_MESSAGE_MAX cuts prescription · explain cross-class refusal remedy cut


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:engine · area:access · pm:queue. Direction: the remedy leads the message; the bound stays

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T14:04Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: the main producer is packages/formula/src/matches-filter.ts (crossFieldClassError) ⇒ domain:engine. The explain engine's copy (crossFieldRefusalForExplain, plugin-security) is a declared cross-lane surface in the claim: one rule for two producers, in one PR.

    Why p2. Release-text exception: every refusal of this class reaches the wire without its fix. An AI client repairing a row-level policy gets the diagnosis and never the remedy.

    Direction.

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 24
    Session: session_01DEvba2nBuD4tWzfq8r8NFY
    Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20869-cross-class-remedy-first
    Worktree: objectstack-issue-20869
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (triage's direction 5912895835: "one rule for two producers, in one PR"):

    • packages/formula/src/matches-filter.ts crossFieldClassError: the remedy sentence leads the message, followed by the diagnosis and the reason. Shorten where possible. The remedy sits inside the first 499 characters, subject and diagnostics included.
    • declared cross-lane surface (domain:services): packages/plugins/plugin-security/src/explain-engine.ts crossFieldRefusalForExplain, under the same rule.
    • ⛔ CLIENT_MESSAGE_MAX / truncateClientMessage (packages/rest/src/error-response.ts) are not touched: that is rest-server 的 4xx 直通把 ≥500 字符的 message 整条换成 "Request failed" —— #5368 刚写好的过滤器拒收措辞,客户端一个字也收不到(实测) #5423's decision.
    • pins:
      • through /data find and POST /api/v1/security/explain, the wire message contains the remedy sentence;
      • a fixture with long subject names stays under the bound with the remedy intact;
      • a short refusal of another class is unchanged (the control).
    • .changeset/20869-*.md.

    Stop on breach and explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5912895835
    Serial constraints cleared: read at 2026-09-30T22:34Z against origin/main 95fed33a2.

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 20869,
     "status": "done",
     "branch": "claude/issue-20869-cross-class-remedy-first",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/20972",
     "session": "session_01DEvba2nBuD4tWzfq8r8NFY — this run (subagent of the domain:engine PM seat; every commit trailer Claude-Session carries the same id)",
     "premise_still_valid": true,
     "summary": "Both producers of the cross-class field-comparison refusal (INVALID_FILTER / 400, code, status and trigger unchanged) now open with their remedy sentence, each byte-identical to the sentence they used to end with. formula crossFieldClassError: shortened to 494 characters in the order remedy, what is refused (two columns with no shared class plus the class list), why, why the columns are withheld; it reaches the /data wire whole and still names no column, operator or policy. plugin-security crossFieldRefusalForExplain: the remedy moves before the subject and diagnostic, which have no length bound (object, field and policy names are regex-only, and the subject lists every refused policy), so it sits at index 0 for every policy; one redundant reason clause dropped. Premise held with one correction, measured through the real handlers: a /data find never carries the matcher text (only the RLS write check and explain pass the declared columns); find answers driver-sql read refusal, 383 characters, already whole. The matcher text reaches /data on insert and update, so the /data pins cover insert (remedy) and find (whole).",
     "tests": "Readings through real ObjectQL + driver-sql (better-sqlite3) + SecurityPlugin + RestServer handlers. BEFORE (BASE 013f97df93): /data insert thrown 972, remedy at 825, wire 500 with ellipsis, remedy absent; explain thrown 601 (short names) / 920 (60-char names), remedy at 477 / 796, wire 500, remedy absent; /data find 383 whole; control (unsupported operator) 228, wire equals thrown. AFTER (dc440bff6b): insert thrown 494 = wire 494, remedy at 0; explain 573 / 892, remedy at 0, wire 500 cut in the reason; find and control unchanged. RED then GREEN, new REST pin packages/rest/src/cross-class-refusal-remedy-on-the-wire.test.ts at 2cfaa6522f against producer dist built from BASE (new-text markers 0 in formula and plugin-security dist): Tests 3 failed | 2 passed (insert, explain, long names red; find, control green), VERDICT command-exit 1; after pnpm turbo run build of both producers (markers 1 each): with security-explain-envelope.test.ts, Test Files 2 passed, Tests 31 passed, VERDICT command-exit 0. Suites at 5fde18e296: @objectstack/formula test 42 files / 1241 passed + typecheck OK (test-layer debt held); @objectstack/plugin-security vitest run 149 files / 3227 passed, 23 skipped (PostgreSQL legs) + typecheck OK; @objectstack/rest vitest --project local 245 files / 4875 passed, 114 skipped + typecheck OK (0 test-layer errors). After merging origin/main f6ccca4a44 (dc440bff6b): formula pin file 23 passed; plugin-security explain-cross-class-refusal + rls-check-cross-class-field-refused + explain-enforce-parity 150 passed, 23 skipped; @objectstack/rest --project local 246 files / 4890 passed, 114 skipped; VERDICT command-exit 0. Lint narrowed at dc440bff6b: ESLint isPathIgnored/calculateConfigForFile put 6 of 7 changed paths in population (config ignores the changeset .md); lintFiles with allowInlineConfig false, JSON formatter: 6 files, 0 errors, 0 warnings; config enables no type-aware linting, so no untouched file verdict can move. No ablation beyond the red leg above; no permanent ablation file left.",
     "mcp_calls": "0 — no MCP GitHub tool was called; every read was an unauthenticated public REST GET (single card, its comments, PR 20858 and its comments, issues 20355 and 5423, comments 5912042814 and 5911738863, PR 20972 and its files).",
     "api_writes": "2 — both through scripts/pm/fleet-write/dispatch.mjs as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/dispatches (pr_create → POST /repos/objectstack-ai/objectstack/pulls, PR 20972 draft, run 36792016875 success, read-back 8029 bytes sent = stored), and POST /repos/objectstack-ai/objectstack/dispatches (comment → POST /repos/objectstack-ai/objectstack/issues/20869/comments, this os-dev-report). The PR-assignee label-write was refused by the local auto-mode classifier before any request left (0 writes; see deviations). git push (5 pushes) is not a REST write.",
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: none (承接者:无) · noted in PR Acceptance notes, not filed · driver-mongodb fieldReferenceUnsupportedError (a sibling refusal of ANOTHER class: no field-to-field lowering at all) is 538 characters by source reading at dc440bff6b; its remedy spans 370 to 410 and survives the 499 cut, only the tail of its withholding sentence is cut; not measured through a REST door (no MongoDB server). Not an H4 hit: the remedy is not cut. · dedupe words: mongodb field reference refusal truncated · fieldReferenceUnsupportedError length",
      "carrier: none (承接者:无) · noted in PR Acceptance notes, not filed · packages/rest/src/security-explain-envelope.test.ts hand-builds the matcher and explain refusals with the old opening and says neither formula nor plugin-security is a rest dependency; plugin-security is a devDependency now. A fixture, not a producer pin (the route reads only code and status). · dedupe words: security-explain-envelope hand-built refusal fixture stale"
     ],
     "hypotheses": {
      "H1": "HELD in length, FALSIFIED in door. Matcher text at BASE 013f97df93: 972 characters, remedy sentence at 825 (unchanged from 95fed33a20: git log 95fed33a20..013f97df93 over matches-filter.ts, explain-engine.ts, error-response.ts, rest-server.ts, sql-driver.ts is empty). Wire on /data insert: 500 with ellipsis, remedy absent. But /data find never carries it: find answers driver-sql read refusal, 383 whole.",
      "H2": "HELD and resolved by the fallback clause. Explain measured 601 at BASE (short names), remedy at 477; 920 with 60-character names. No longest subject exists (names declare no maximum, the subject lists every refused policy), so it cannot be proved for every length; the remedy now precedes the subject, at index 0.",
      "H3": "HELD for the matcher: remedy, what (two columns, no shared class, class list), why, withheld, each true; whole message 494. For explain the class list and withholding sentence never existed (it names policy and columns by design), so it keeps remedy, subject plus diagnostic, reason.",
      "H4": "MEASURED, no hit. driver-sql read-path cross-class refusal (uncompilableFieldReferenceError via withheldFilterError): fixed 383 characters, wire whole, rule sentence at 188. driver-mongodb sibling of another class: see out_of_scope_findings.",
      "H5": "HELD. One test pinned the old opening (rls-check-cross-class-field-refused.test.ts, regex on the first words) and moved. No i18n bundle or ledger carries this text (grep over packages, content, .changeset: only released CHANGELOG entries and content/docs/releases, both release-owned and untouched)."
     },
     "gates": {
      "head": "dc440bff6b",
      "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 65 commands, change set 7 paths vs merge base f6ccca4a4, no stale-tree warning",
      "exit_codes": "65 of 65 exit 0",
      "prerequisite_reruns": "check:dual-build-cjs-loads, check:i18n, check:type-check-debt first exit 3 (PREREQUISITE NOT MET); after pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/* (71 tasks, VERDICT command-exit 0) all three exit 0",
      "ran_reconciliation": "Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN. (exit 0)",
      "not_measured": []
     },
     "line_budget": {
      "changed_lines": "365 (+349 / -16) in 7 files, vs the human-merge threshold 5000: under",
      "skills_touched": false,
      "governed_surface_touched": false
     },
     "deviations": [
      "PR assignee NOT set: `node scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 20972 --assign os-support-ai` was denied by the Claude Code auto-mode classifier, reason [External System Writes]; not re-routed. The seat sets it.",
      "Zone 1 pin wording \"through /data find\": the matcher text never reaches find (measured and by call-site grep). Pinned /data insert for the matcher remedy, and /data find for its read refusal reaching the wire whole with its same-class rule; explain as ruled.",
      "Merged origin/main (f6ccca4a44, 3 commits, none touching the 7 files) as dc440bff6b so the gate derivation read a current tree (the first derivation at 5fde18e296 warned STALE TREE); rebuilt and re-ran the touched tests and the rest local suite after it.",
      "Explain reason trimmed: \"instead of judging a record\" dropped (the same sentence says explain reports no verdict), under the ruling \"shorten where possible\".",
      "Measurement scripts ran from the scratchpad against the worktree dist and rest src by absolute path; nothing was written into the repository.",
      "Commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder model-named line."
     ],
     "files_changed": [
      "packages/formula/src/matches-filter.ts (+14/-10: crossFieldClassError text and docblock)",
      "packages/plugins/plugin-security/src/explain-engine.ts (+12/-4: crossFieldRefusalForExplain text and docblock)",
      "packages/rest/src/cross-class-refusal-remedy-on-the-wire.test.ts (+264/-0: new wire pin, real stack)",
      "packages/formula/src/matches-filter-cross-field-class.test.ts (+25/-0)",
      "packages/plugins/plugin-security/src/explain-cross-class-refusal.test.ts (+12/-1)",
      "packages/plugins/plugin-security/src/rls-check-cross-class-field-refused.test.ts (+2/-1)",
      ".changeset/20869-cross-class-refusal-remedy-first.md (+20/-0: @objectstack/formula patch, @objectstack/plugin-security patch, Clause-②: no)"
     ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20972 @ dc440bff6b (#20869: the cross-class refusal's remedy leads the message on both producers)

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T23:56Z. The seat is the reviewer of record. Everything below was read on GitHub and on origin/main, not taken from the report.

    • Shape:
      • The first line is Fixes #20869, then Clause-②: no.
      • The changeset is @objectstack/formula patch and @objectstack/plugin-security patch.
      • 7 files, +349/-16; not governed.
      • The PR assignee is unset. The dev's assignee write was refused by its own permission layer. The seat did not re-route it, and has put it to the maintainer. It is not a merge gate.
    • The change is text only. Code, status (INVALID_FILTER / 400) and trigger are unchanged on both producers.
    • The door correction: a /data find never carries the matcher text. It answers driver-sql's own read refusal, 383 characters and already whole. The matcher text reaches /data on insert and update. So the new REST pin (a real stack) covers insert for the remedy, find as whole, and explain. It went red first against the old producers (3 red, 2 green).
    • Contract review: at-tier record 5921854782 on this head, PASS (read-only, Local-runs: none). It judged every clause of both new texts true against the code, and the class list complete.
    • Carried, not filed: two out_of_scope_findings, each carrier: none, in the PR's Acceptance notes:
      • driver-mongodb's fieldReferenceUnsupportedError (its remedy survives the cut);
      • a stale hand-built fixture in rest's security-explain-envelope.test.ts.
    • Checks on this head: 31 success, 3 skipped (all rostered: check-expected-skips OK, exit 0), 0 failed; check-governed-merges NOT governed (365 changed lines).

    Landing: ready, then auto-merge, as two separate relay acts.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #20972 as 05be35259 (the cross-class refusal's remedy leads the message on both producers)

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-10-01T00:16Z.

    • Verified on main: 05be35259 is a squash with one parent and an ancestor of origin/main. .changeset/20869-cross-class-refusal-remedy-first.md is present at the squash and absent at the parent. 7 files, +349/-16, as reviewed.
    • Route:
      • ACCEPT 5921864908 was posted and read back first.
      • pr_ready and automerge_enable then ran as two separate relay acts at the reviewed head dc440bff6b (PASS 5921854782).
      • The 3 skipped checks were rostered.
      • added_to_merge_queue at 23:58Z; merged by the queue at 00:16Z.
    • PR assignee: still unset. The dev's write was refused by its own permission layer, and the seat put it to the maintainer instead of re-routing it.
    • Card: Fixes closed it as completed through the queue. pm:dispatched is removed in this act.

    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

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions