Skip to content

automation resume: a known top-level key with a type-mismatched value is silently dropped — the submission is still treated as empty #9416

Closed

Description

@os-zhuang

Found while implementing #8796 (the resume body's outer envelope became a closed key set). Out of that card's scope: the ruling there covers unknown KEYS only; this is the same silent-drop family one axis over, on the VALUE of an accepted key, and widening refusals further is its own decision.

Measured on the resume branch, packages/runtime/src/domains/automation.ts

The signal assembly (deliberately field-by-field, per #3801) type-guards each accepted key and silently skips a value that fails the guard:

const inputs = (b.inputs ?? b.variables);
if (inputs && typeof inputs === 'object') signal.variables = inputs;
if (b.output && typeof b.output === 'object') signal.output = b.output;
if (typeof b.branchLabel === 'string') signal.branchLabel = b.branchLabel;

So after #8796's closed set lands, POST .../resume {"inputs": "a string"} passes the key check (the key IS accepted), the value is dropped by the type guard, the engine receives an empty signal, and the caller gets HTTP 200 success:true with the submission treated as empty — exactly the failure shape #8796 measured for unknown keys, reachable through a well-spelled key with a mis-shaped value. Same for {"output": 42} and {"branchLabel": 7}. A non-object body (JSON string/number/boolean) and an empty-array body degrade the same way: they normalize to an empty signal and answer 200.

The decision this needs

Option A is the shape the sibling arms already chose; filed rather than fixed because it widens refusals on an accepted public surface beyond the #8796 ruling's text.

Refs


Generated by Claude Code

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 18, 2026
  2. added theissue type on Aug 18, 2026
  3. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Triage: lands in packages/runtime/src/domains/automation.ts ⇒ domain:cli, type Bug, pm:queue — enqueued under the standing meta-criterion (maintainer, 2026-08-07): a silent-drop surface joins the refusal set its sibling limbs already ruled loud, inheriting the #8796 ruling (closed key set, loud refusal) together with its reason, plus the #3899 toggle-arm precedent (truthy non-boolean enabled is refused, not coerced). The value axis carries no semantic difference that would reopen the ruling: the engine contract already types variables/output as objects, so refusing a mis-shaped value is declared=enforced restoration, not a new contract. Ruled route: Option A — 400, located, naming the key and expected type; non-object and array bodies refuse the same way. Veto window: this round's report on #6015.

    ⚠️ Clause-② is YES for the dispatching seat: this changes accept/reject behavior on a public route — dispatch at the contract-review tier (CONTRACT_REVIEW_TIER), and the claim comment must carry Clause-②: yes.

    Triage seat routine — session session_pm_triage_20260818T0104Z.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 18, 2026
  5. self-assigned this
    on Aug 18, 2026
  6. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 19
    Session: session_012WKSnqAaoqtW3QX7SSf1Vk
    Branch: claude/issue-9416-resume-value-type-refusal
    Worktree: objectstack-issue-9416
    Domain: domain:cli
    File surface, declared to REGION level (required by the same-package exemption): packages/runtime/src/domains/automation.ts — the resume signal-assembly block only (the inputs/variables/output/branchLabel type-guard sequence and its refusal path), plus test siblings. ⛔ Not respondToFlowTrigger, ⛔ not the toggle arm, ⛔ not flow-dispatch-status.ts.
    Container & model: M, mode:subagent, model: opus — DOWNGRADE under the maintainer's one-time Clause-② authorization (verbatim 「授权一次性降档到 opus」, naming this card among five).
    Clause-②: yes — triage wrote it into the card; Option A refuses a request shape that is accepted today.

    ⭐ Serial constraints — first dispatch under the NEW same-package exemption

    Maintainer, 2026-08-18, verbatim: 「放开同包,同文件仍硬串行」. This supersedes the 2026-08-13 refusals and this morning's re-confirmation, and it was granted on evidence measured today: #9460 and #9446 were developed concurrently in packages/runtime (different files) and merged cleanly at 11:43:09Z and 12:28:20Z with no conflict.

    ⇒ This card runs alongside in-flight packages/runtime work — #9462 (endpoint-executor.ts), #9457 (vitest config), #9568 (dispatcher-error-vocabulary.ts). All four declare disjoint region surfaces. ⛔ Same-file remains hard serial and none of these four share one.

    The exemption buys queueing, not collision-safety, so the four alternative rules apply and are in the dispatch: ① region-level file surface (above) · ② merge main once before opening the PR · ③ merge again after a sibling card lands · ④ conflicts go to the merge queue, ⛔ the PM does not hand-order them. ⛔ The exemption does not spread to other cards.

    Premise check on origin/main: domains/automation.ts last moved with #9446 (ca2e020e4), which touched the flow-trigger responder and not the resume assembly — so this card's block is intact. ⚠️ Locate it by text, not by the card's line numbers: this file has absorbed #8684, #8796 and #9446 since the card was written.

    ⚠️ Read the card's ruling comment before writing: Option A is ruled (400, located, naming the key and the expected type; non-object and array bodies refuse the same way), inheriting #8796's closed-key-set ruling and the #3899 toggle-arm precedent. ⛔ Option B is rejected.


    Generated by Claude Code

  7. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 9416,
      "status": "done",
      "branch": "claude/issue-9416-resume-value-type-refusal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9685",
      "premise_still_valid": true,
      "summary": "Option A implemented in the resume signal-assembly block of packages/runtime/src/domains/automation.ts, located by text (the file had absorbed #8684/#8796/#9446). Two refusals now wrap the field-by-field assembly: the body itself must be a non-array object (a JSON string/number/boolean and an EMPTY array previously normalised to {} and answered 200 on an empty signal), and each accepted key must carry a contract-shaped value — inputs/variables/output an object, branchLabel a string — each offending key getting its own invalid_type entry naming the key, the expected type and the type received. Both reuse the file's own validationFailure helper and the ADR-0114 catalog members the sibling arms already answer with; no new envelope, no new code, no spec widening. Two deliberate calls worth review: null IS refused (JSON can express it, and it was the value dropped most quietly), while a key whose value is `undefined` counts as ABSENT (JSON.stringify drops it, so no HTTP caller can produce one — over the wire this is indistinguishable from the toggle arm's `in`-based presence test; it differs only for in-process callers, where `{ inputs: maybeUndefined }` means 'no inputs'). Arrays are refused for inputs/output too: ResumeSignal types them Record<string, unknown>, which excludes arrays, and the old `typeof === 'object'` guard forwarded them — the exact Option B failure the ruling rejects. Assembly is unchanged in substance: never a body spread, so #3801's symbol-keyed service-authority marker stays unforgeable. Merged origin/main once before opening (clean, main had not touched automation.ts); stayed inside the declared region — respondToFlowTrigger, the toggle arm and flow-dispatch-status.ts untouched.",
      "tests": "All at HEAD 155e19388, the final commit; union re-derived from the ACTUAL changed paths with `node scripts/pm/dispatch-gates.mjs` and re-derived again after the main merge. TARGETED: `pnpm --filter @objectstack/runtime typecheck` clean; the two resume test files `Test Files 2 passed (2) / Tests 51 passed (51)`; full `@objectstack/runtime` `Test Files 171 passed (171) / Tests 2569 passed (2569)`. ABLATION (restore the silent-skip guards via `git checkout HEAD~1 -- packages/runtime/src/domains/automation.ts`, the branch point being exactly the pre-fix file): PREDICTED BEFORE RUNNING 30 red / 11 green in the new file with the #8796 sibling file staying 10/10 green; ACTUAL `Tests 30 failed | 21 passed (51)`, `Test Files 1 failed | 1 passed (2)` — exact match on both numbers and on the file split. The one new-file test predicted green is the ordering pin 'reports an unknown KEY ahead of a mis-shaped value' (the #8796 unknown-key arm survives the ablation, so it cannot redden for this defect and is not claimed to); the non-empty-array test DID redden because its locator moves from the key check back to `(body)`. Restored with `git checkout claude/issue-9416-resume-value-type-refusal -- <path>`; post-restore diff against the branch empty. GATES, all green: check:route-envelope (16 dispatcher domains audited, 0 new ratchet), check:cross-package-test-inputs, check:changeset-gate-self-tests, check:objectui-changeset, check:nul-bytes (plus a manual control-byte grep over the three changed files), check-adr-0087-registration (1 declared-breaking changeset, disposition present), check-changeset-no-major, check-empty-changeset, check-cross-package-test-inputs, docs-audit/check-affected-docs. CONVENTION-TRIGGERED by the new test file, none named in the dispatch and all found by re-derivation: check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, and check:type-check-debt — the load-bearing one: packages/runtime/tsconfig.json EXCLUDES **/*.test.ts, so `tsc --noEmit` never read the new file, and the package carries a 227-error TEST_DEBT entry that a new hidden test file can push up. Ran it the way CI does, with the workspace closure built first (turbo build, 70/70 tasks): '33 ledger entries re-measured, none above its recorded number. surplus: none' — the new file contributes ZERO new type errors. CONSUMERS, prefix direction `...@objectstack/runtime` (downstream), stated because the direction is what makes the claim checkable: @objectstack/client 23 files / 310 tests, @objectstack/hono 2 / 73, @objectstack/http-conformance 4 / 72, and @objectstack/dogfood test/flow-durable-suspend.dogfood.test.ts 1 / 11 — the only consumer in the repo that POSTs a real resume body over HTTP. All green; no consumer fixture flipped. CI JOBS at report time, read once without waiting (maintainer 2026-08-10): TypeScript Type Check in_progress, Lint & Repo Gates in_progress, Test Core (1-3/3) in_progress, Dogfood Regression Gate (1-3/3) in_progress, Build Core in_progress, Temporal Conformance (live PG + MySQL) in_progress. No gate had reported a conclusion when this report was filed.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Zone 2 assumptions — both falsified-checks came back confirming, one with evidence worth keeping:

    • "The four value-shape cases and the two body-shape cases all route through this one block." Confirmed — one door. route-ledger.ts registers POST /automation/:name/runs/:runId/resume once; dispatcher-plugin.ts forwards req.body into dispatcher.dispatch → handleAutomation → this arm; packages/rest has no resume route at all (the approval decision path runs through ApprovalService in-process and never touches this body assembly). All six shapes enter at the same two statements, so this is not a half-fix.
    • "validationFailure can express 'expected type' per key." Confirmed, no widening. invalid_type is already a member of the closed FieldErrorCode catalog in packages/spec/src/api/errors.zod.ts, and the per-field message carries the expected type — which is exactly what the toggle arm next door already does. packages/spec was not touched.

    Not filed as issues, but flagged for the reviewer (neither is a defect and both are already-known state, so filing would be noise):

    • packages/runtime/tsconfig.json excludes **/*.test.ts, so the package's typecheck script does not read its own tests — the AGENTS.md hazard. It is pre-existing, already ledgered as the 227-error TEST_DEBT entry with its tracking link, out of this card's declared region, and the re-measure above proves this PR adds nothing to it.
    • The toggle arm tests key presence with 'enabled' in body while this arm uses !== undefined. Over HTTP the two are identical (JSON cannot produce an undefined value); the divergence is documented in the code comment rather than silently taken.

    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

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions