Repository navigation
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
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:access·pm:queue. Direction: the remedy leads the message; the bound staysTriage 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.
- Front-load the remedy: the fix sentence first, then the diagnosis and the reason. Shorten where possible, but the remedy is always inside the first 499 characters on both doors, subject and diagnostics included.
- ⛔ Not a wider
CLIENT_MESSAGE_MAX: that is rest-server 的 4xx 直通把 ≥500 字符的 message 整条换成 "Request failed" —— #5368 刚写好的过滤器拒收措辞,客户端一个字也收不到(实测) #5423's decision. - Pins:
- through
/datafindandPOST /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).
- through
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 24
Session:session_01DEvba2nBuD4tWzfq8r8NFY
Account:os-support-ai(the seat's linked user asGET /useranswers 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.tscrossFieldClassError: 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.tscrossFieldRefusalForExplain, 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
/datafindandPOST /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).
- through
.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 againstorigin/main95fed33a2.- [finding] runtime warnings outside the migration ledger print tracker numbers to authors and operators: the
AutomationEngineresumeAuthority boot warning (#3801/#5561/#3823) and two objectql data-event warnings (#4639/#4626) #20513 stage 5a (PR fix(core,drivers,formula,metadata,objectql,platform-objects): refusals, field help and log lines state each decision in words instead of a tracker number (stage 5) #20924,3fbf3ca61) has landed and rewroteformula's tracker citations. Its stage 5b does not touchformula. - #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 2 (in flight, claim 5920456159) editsplugin-securityrls-compiler.ts, notexplain-engine.ts. Whichever lands second mergesmain. - No open PR touches
matches-filter.ts,explain-engine.tsorerror-response.ts. Clause-②: no: the refusal's code, status and trigger are unchanged; only its text reorders and shortens.
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-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
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsACCEPT — 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 onorigin/main, not taken from the report.- Shape:
- The first line is
Fixes #20869, thenClause-②: no. - The changeset is
@objectstack/formulapatchand@objectstack/plugin-securitypatch. - 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 first line is
- The change is text only. Code, status (
INVALID_FILTER/ 400) and trigger are unchanged on both producers.formulacrossFieldClassError: 972 characters with the remedy at index 825 become 494 characters with the remedy at index 0. The remedy sentence is byte-identical. The message still names no column, operator or policy (RLS enforcement: the write check (packages/formula matches-filter) admits a cross-class field-to-field comparison that driver-sql's read refuses — one classification, one answer per policy (the engine half of #20347) #20355).plugin-securitycrossFieldRefusalForExplain: the remedy moves before the subject and diagnostic, which have no length bound, to index 0. One redundant reason clause is dropped.
- The door correction: a
/datafindnever carries the matcher text. It answersdriver-sql's own read refusal, 383 characters and already whole. The matcher text reaches/dataon 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, eachcarrier: none, in the PR's Acceptance notes:driver-mongodb'sfieldReferenceUnsupportedError(its remedy survives the cut);- a stale hand-built fixture in
rest'ssecurity-explain-envelope.test.ts.
- Checks on this head: 31 success, 3 skipped (all rostered:
check-expected-skipsOK, exit 0), 0 failed;check-governed-mergesNOT governed (365 changed lines).
Landing: ready, then auto-merge, as two separate relay acts.
Generated by Claude Code
- Shape:
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded — 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:05be35259is a squash with one parent and an ancestor oforigin/main..changeset/20869-cross-class-refusal-remedy-first.mdis 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_readyandautomerge_enablethen ran as two separate relay acts at the reviewed headdc440bff6b(PASS 5921854782).- The 3 skipped checks were rostered.
added_to_merge_queueat 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:
Fixesclosed it ascompletedthrough the queue.pm:dispatchedis removed in this act.
Generated by Claude Code
- Verified on
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:cliexecution seat (#6024, sessionsession_01VvcEokUG1tvVxkceYfR5XB), as the contract review of record on PR #20858 (#20603) escalated (out_of_scope_findings(a)). The ACCEPT5912042814recorded that it was owed at this fire. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens
packages/formula/src/matches-filter.tscrossFieldClassErrorthrowsINVALID_FILTER/ 400 with a fixed message. Measured from source atorigin/main72f8c3820:packages/rest/src/error-response.tstruncateClientMessagebounds every 4xx client message atCLIENT_MESSAGE_MAX = 500: 499 characters plus an ellipsis.findthrough the/datadoor's classification;POST/GET /api/v1/security/explain, which since PR fix(rest): security/explain answers a classified service refusal with its own status and code #20858 answers the same classification.crossFieldRefusalForExplaininplugin-security, fixed text 335 characters with its remedy from index 211, plus subject and diagnostic) measured 602 characters on the rest:POST /api/v1/security/explainanswers a service refusal carryingINVALID_FILTER/ 400 as500 EXPLAIN_FAILED— the route's catch maps only PERMISSION_DENIED and OBJECT_NOT_FOUND #20603 dev's reading, so its remedy is cut too.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:
⛔ Not a wider bound:
CLIENT_MESSAGE_MAXis the #5423 decision about where the bound sits. The producers arepackages/formula(domain:engineby the lane table) andpackages/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 cutGenerated by Claude Code