Skip to content

fix(runtime,mcp): a declarative update action whose write landed under a failing roll-up answers its own success at both action doors - #28882

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-1minft
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-1minft

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

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.

维护者速读

  • 问题:声明式 update 动作(operation: 'update')的写入已经生效、只是父记录汇总重算失败(ERR_SUMMARY_RECOMPUTE)时,REST 动作入口和 MCP run_action 都返回错误,审计也记失败,调用方会再跑一次动作。
  • 现在:runtime 按汇总成功时的方式组好动作结果,挂到拒绝上;REST 入口答 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.

Measured reach per action kind (ruling 3)

kind where ERR_SUMMARY_RECOMPUTE arrives this PR
declarative update callData('update') throws it to both doors with the write stored and the write's answer attached; one write, no later step fixed
api the in-process forward lands on a REST data door, which answers 2xx since #28781 converted inside; unchanged
flow the create_record / update_record / delete_record nodes catch it and return success: false, so the run fails with a flow refusal; no transaction, earlier steps stay committed converted inside; unchanged
script / body thrown into the author's body at ctx.engine, ctx.api and the sandbox ctx.api.object(x) repo (shared with hook and job bodies); no answer exists to relay follow-up #28861 (seat ruling)

Ruling 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 real HttpDispatcher over an engine double whose update applies the write then throws the refusal (routed through assertEngineUpdateDispatch; engine-double ledger updated), with the real callData and an audit spy.
    • REST door: 200 with { operation, object, id, record, undo }, update applied once, one audit row succeeded.
    • MCP bridge runAction (buildMcpBridge): the refusal carries { ok, action, objectName, recordId, result }, update applied once, audit row succeeded.
    • Pre-write guard: an engine PERMISSION_DENIED before the write answers 403 / throws as before at both doors, nothing written, audit rows failed.
    • One error log line naming "stl_account.open_cases".
    • Red on origin/main 3b920f6d (test only): 3 red, the guard green.
  • packages/mcp/src/mcp-run-action-stale-summary.test.ts (4, HTTP /mcp transport): the attached answer is the tool result; a pre-write refusal keeps its rendering with details; a stale refusal with no answer still answers its error; one log line. Red on base: 2 red.
  • Ablations, each landed on disk then restored:
ablation red
executeDeclarativeUpdateAction throws the bare refusal 2 (REST, MCP bridge)
MCP dispatch does not re-attach the envelope 1 (MCP bridge)
audit outcome ignores the carrier 1 (MCP bridge)
REST door reading off 2 (REST, log)
run_action tool reading off 2 (relay, log)
  • Suites: @objectstack/runtime 515 files, 9265 passed (19 skipped); @objectstack/mcp 53 files, 644/644.

Acceptance notes

  • No result shape or audit status change. Each door answers the body it already answers for the same action when every roll-up recomputes.
  • No packages/spec/** edit; check:error-code-provenance exit 0.

Verification

Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 69 commands on the diff (after merging main at 45efba2a); all 69 exit 0, and --ran reconciles 69 derived, 69 run, 0 NOT-MEASURED. check:engine-double-contract first read exit 1 on the new runtime double (unrecorded in the pinned ledger), fixed by --write and committing scripts/engine-double-contract.pinned.json. pnpm --filter @objectstack/spec check:error-code-provenance: exit 0. pnpm --filter @objectstack/runtime typecheck and pnpm --filter @objectstack/mcp typecheck (both run check:test-typecheck): exit 0. Scoped eslint over the five changed .ts files: exit 0. After the merge (nothing under packages/runtime, packages/mcp or packages/types): the new files with action-declarative-update.test.ts, action-invocation-audit.test.ts and mcp-action-refusal-details.test.ts green (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 (updateData attaching 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 real HttpDispatcher and callData. Full dogfood not owed (no !, no narrowing arm).


Generated by Claude Code

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 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
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76bf6c50d86f2894526851fb0a1c751f4074bba9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 61b082952f653842f705dae47998ae951e4a4fd3 — the merge of head 74cf24a45eaa0c4a3aad4686ac35cde19ae1d75f into base 76bf6c50d86f2894526851fb0a1c751f4074bba9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

席位核对

ACCEPT · domain:server seat 5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt · 2026-11-02T02:47Z · head 74cf24a4 · #28856

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

席位意见:与 diff 相符。声明式更新动作写入已落库、只是父记录汇总重算失败时,REST 动作入口和 MCP run_action 都答该动作原本的成功结果,调用审计记为成功;写入之前的拒绝照旧答错误。脚本/body 动作按席位裁定另立 #28861。

  1. Shape: draft against main, Fixes #28856, patch changesets for @objectstack/runtime and @objectstack/mcp; Clause-②: no, Metadata-protocol: none. Declaration face only, no contract review owed. check-governed-merges --pr 28882: NOT governed, +401 / -17. git merge-tree against main: clean.
  2. Against the order:
  3. Accepted deviations: none. landedWriteResultOrError gains an otherwise renderer so run_action keeps errorResultFromThrown for every other throw.
  4. Gates: red first (runtime 3, mcp 2), 5 ablations each red. runtime 9265 passed, mcp 644/644. 69/69 derived gates exit 0, --ran 0 NOT-MEASURED. Both typechecks exit 0. Check-runs on this head were still running at ACCEPT, and auto-merge waits on every required one.
  5. Out of scope:
  6. Landing: pr_ready + automerge_enable through the relay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant