Repository navigation
fix(runtime,mcp): a declarative update action whose write landed under a failing roll-up answers its own success at both action doors - #28882
Conversation
…r a failing roll-up answers its own success at both action doors executeDeclarativeUpdateAction composes its result from the answer callData's producer attached to the stale-summary refusal and attaches it in turn; the MCP dispatch wraps it in run_action's envelope, the REST action door and the run_action tool relay it, and the invocation audit row records succeeded. A refusal before the write is unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift Check6 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not 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
Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 61b082952f653842f705dae47998ae951e4a4fd3 && git checkout 61b082952f653842f705dae47998ae951e4a4fd3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76bf6c50d86f2894526851fb0a1c751f4074bba9 74cf24a45eaa0c4a3aad4686ac35cde19ae1d75f && git checkout -B drift-repro 76bf6c50d86f2894526851fb0a1c751f4074bba9 && git merge --no-ff 74cf24a45eaa0c4a3aad4686ac35cde19ae1d75f
node scripts/docs-audit/affected-docs.mjs --json 76bf6c50d86f2894526851fb0a1c751f4074bba9 |
|
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」. 席位意见:与 diff 相符。声明式更新动作写入已落库、只是父记录汇总重算失败时,REST 动作入口和 MCP run_action 都答该动作原本的成功结果,调用审计记为成功;写入之前的拒绝照旧答错误。脚本/body 动作按席位裁定另立 #28861。
|
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Requested by Leehom · project thread
Fixes #28856
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches what an action door answers when a parent roll-up cannot be recomputed after the action's write.维护者速读
operation: 'update')的写入已经生效、只是父记录汇总重算失败(ERR_SUMMARY_RECOMPUTE)时,REST 动作入口和 MCPrun_action都返回错误,审计也记失败,调用方会再跑一次动作。200和原有的声明式结果,run_action答原有的{ ok, action, objectName, recordId, result },审计记succeeded。没有新错误码、字段、状态或形状;汇总列过期按字段每进程以 error 记一次日志。packages/spec/**和ui/**。api转发落到已经答成功的数据入口;flow的记录节点把它变成节点失败;script/body 按 seat 裁定拆到 scripts: a body's ctx.engine / ctx.api write that lands under a failing parent summary recompute throws into the author's code, so later steps never run and the action, hook or job reports a failure #28861(作者代码看到什么需要另行裁定)。What changes
Through #28781's carrier in
@objectstack/types(earnedAnswerOf/withEarnedAnswer/staleSummaryRefusalOrThrow/staleSummaryAnswerLines); the code is read inline there, no new constant.packages/runtime/src/action-execution.ts):executeDeclarativeUpdateActionwraps its onecallData('update'). On the stale refusal it takes the write's answer thatcallData's producer attached (the protocol'supdateDatasince fix(lint,metadata-protocol,rest,runtime): refuse a sum/avg roll-up the engine cannot recompute, and answer a landed child write's success #28781, the ObjectQL fallback since data writes: form submit, batch / *ManyData, cloneData, the MCP create_record bridge and callData's fallback still answer 500 after a landed write when the summary recompute fails #28782), composes theDeclarativeUpdateResultexactly as from a normal answer (record,undoanchor), and throwswithEarnedAnswer(stale, result). Every other throw propagates unchanged.dispatchAdmittedBusinessAction) re-attaches that result insiderun_action's envelope{ ok, action, objectName, recordId, result }.invokeBusinessActionwrites the invocation audit row assucceededwhen the refusal carries an answer, the outcome the tool answers. No new audit status.packages/runtime/src/domains/actions.ts,dispatchAdmittedRestAction's catch):deps.success(earned), with the stale column logged through the dispatcher logger. The handler's audit row then reads the200assucceeded, as it already does.run_action(packages/mcp/src/mcp-http-tools.ts): its catch calls the fix(mcp): update_record and delete_record answer a write that landed under a failing roll-up with their own success #28854 helperlandedWriteResultOrError, which now takes the fallback renderer, so a refusal with no answer still renders througherrorResultFromThrown(details kept).Measured reach per action kind (ruling 3)
ERR_SUMMARY_RECOMPUTEarrivescallData('update')throws it to both doors with the write stored and the write's answer attached; one write, no later stepapi2xxsince #28781flowcreate_record/update_record/delete_recordnodes catch it and returnsuccess: false, so the run fails with a flow refusal; no transaction, earlier steps stay committedctx.engine,ctx.apiand the sandboxctx.api.object(x)repo (shared with hook and job bodies); no answer exists to relayRuling 2's multi-step case does not arise in this PR: the declarative update is one write, and the multi-step kinds are
flow(converted inside) and script/body (#28861). No handler runs inside a rolling-back transaction.Tests
packages/runtime/src/action-declarative-update-stale-summary.test.ts(4): the realHttpDispatcherover an engine double whoseupdateapplies the write then throws the refusal (routed throughassertEngineUpdateDispatch; engine-double ledger updated), with the realcallDataand anauditspy.200with{ operation, object, id, record, undo }, update applied once, one audit rowsucceeded.runAction(buildMcpBridge): the refusal carries{ ok, action, objectName, recordId, result }, update applied once, audit rowsucceeded.PERMISSION_DENIEDbefore the write answers403/ throws as before at both doors, nothing written, audit rowsfailed.errorlog line naming"stl_account.open_cases".origin/main3b920f6d(test only): 3 red, the guard green.packages/mcp/src/mcp-run-action-stale-summary.test.ts(4, HTTP/mcptransport): the attached answer is the tool result; a pre-write refusal keeps its rendering withdetails; a stale refusal with no answer still answers its error; one log line. Red on base: 2 red.executeDeclarativeUpdateActionthrows the bare refusalrun_actiontool reading off@objectstack/runtime515 files, 9265 passed (19 skipped);@objectstack/mcp53 files, 644/644.Acceptance notes
packages/spec/**edit;check:error-code-provenanceexit 0.Verification
Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 69 commands on the diff (after mergingmainat45efba2a); all 69 exit 0, and--ranreconciles 69 derived, 69 run, 0 NOT-MEASURED.check:engine-double-contractfirst read exit 1 on the new runtime double (unrecorded in the pinned ledger), fixed by--writeand committingscripts/engine-double-contract.pinned.json.pnpm --filter @objectstack/spec check:error-code-provenance: exit 0.pnpm --filter @objectstack/runtime typecheckandpnpm --filter @objectstack/mcp typecheck(both runcheck:test-typecheck): exit 0. Scoped eslint over the five changed.tsfiles: exit 0. After the merge (nothing underpackages/runtime,packages/mcporpackages/types): the new files withaction-declarative-update.test.ts,action-invocation-audit.test.tsandmcp-action-refusal-details.test.tsgreen (79 tests); the full suites above ran before it.No live probe: storing a stale roll-up needs the lint roll-up gate turned off at the runtime door (the #28782 recipe), and the producer half (
updateDataattaching the write's answer) is #28781's, pinned on a real store by its REST suite; this change is the action composition and the two doors, pinned above through the realHttpDispatcherandcallData. Full dogfood not owed (no!, no narrowing arm).Generated by Claude Code