Conversation
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…tomation-runs-hasmore
…ls are readable Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 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 bca8acc2f35dc081f67f12595a3a681634013885 && git checkout bca8acc2f35dc081f67f12595a3a681634013885
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 61170fa385da18a04e3fed75e8c65cbff801e008 99ad620c544e1f3f4e0976596a6e8a7e4620d70e && git checkout -B drift-repro 61170fa385da18a04e3fed75e8c65cbff801e008 && git merge --no-ff 99ad620c544e1f3f4e0976596a6e8a7e4620d70e
node scripts/docs-audit/affected-docs.mjs --json 61170fa385da18a04e3fed75e8c65cbff801e008
|
…d cursor Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
|
| lane | conclusion |
|---|---|
Type Check · source gates |
success |
Type Check · consumer gates |
cancelled |
Type Check · workspace |
cancelled |
Type Check · debt ledger |
cancelled |
The aggregator refused to report a pass over three lanes that were never measured. That is the gate being correct — cancelled is NOT MEASURED, and NOT MEASURED is ⛔ never a pass. It is also ⛔ never a red about the code.
Why they were cancelled. The branch head moved to 6506b7c6 and the PR object updated at 2026-09-21T03:57:25Z — the author's own next push, which cancels in-flight runs on the previous head by the workflows' concurrency group. ⇒ the failure belongs to a head that is no longer the tip.
The authoritative reading is the current head. 6506b7c6: 32 check names, 0 failures, 20 still running (latest run per name; superseded runs of the same name are not the reading).
⛔ Nothing was pushed for this and ⛔ no re-run was spent: there is no live failure to fix, and re-running a superseded head buys nothing. If TypeScript Type Check goes red on 6506b7c6 with its member lanes reading failure rather than cancelled, that is a real reading and this seat will root-cause it.
failure or cancelled. ⛔ Never judge this family by the red badge alone.
Reading taken 2026-09-21T03:57Z.
Generated by Claude Code
Contract reviewServed-tier: 104/104
① Derived judgmentsTruncation signal (the sharpest question). When Accept set and published surface. New optional What breaks the contract story. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL — What must change for a re-review to pass: retire Generated by Claude Code |
…lers actually reach The schema tombstone alone left @objectstack/client typing the key `string` and appending it into a route that no longer reads it — the ADR-0104 silent strip the tombstone exists to prevent, re-created one layer down. Drops the option and the `params.set` from all three run-list surfaces, inverts the URL pin, and qualifies the published hasMore docblocks under a status filter. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…tomation-runs-hasmore
The changeset and the D3 acceptance criteria both promised 'hasMore: true when the window is shorter than the matching set' without saying that the window is taken before the status filter is applied. Both ship to consumers — one as CHANGELOG.md, one into the major-18 upgrade guide — so both now carry the qualification the published docblocks already do. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: 138/138
① Derived judgmentsThe prior FAIL ground is CLOSED, measured first-hand. Boundary — HELD. ② (a) ② (b) The rewritten ordering-key sentence is NOT exactly true — this is the verdict's sole ground. "the only ordering this door has is an optional, non-unique ③ Settled ground — re-measured, undisturbed. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL — What must change for a re-review to pass: make the ordering-key sentence exactly true by replacing "an optional, non-unique Generated by Claude Code |
The sentence that replaced an arguable claim introduced a false one. `startedAt` is required on every layer the sort touches — ExecutionLogEntry (engine.ts:1036) and ExecutionLogSchema (execution.zod.ts:399) both declare it without `?` or .optional(). The word came from the comparator's defensive `?? ''`, which is not evidence of an optional type. Corrected in the published prescription, the changeset, the retired-key entry and its registry mirror, and the reference row regenerated from it. Also repairs a splice artefact: inserting the SDK paragraph severed `ADR-0049 / ADR-0087, #19365.`, orphaning `ADR-0049 / ` mid-field. The pair is restored in the D3 reason and its registry mirror. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…tomation-runs-hasmore
|
…sal truthfully Two sentences copied from the inbox door, where both are true, into the runs door, where neither is: - 'clamped to 1..100' — nothing on this door clamps. parseIntegerParam throws min_value/max_value (query-param.ts:168-173) on the bounds the door reads off ListRunsRequestSchema.shape.limit, and the engine is a bare '?? 20'. The window is REFUSED outside 1..100, not clamped. Lit control: the real clamp in this repo is messaging-service.ts:541. - 'removed in protocol 18' — PROTOCOL_VERSION is '17.0.0' and the prescription publishes '@objectstack/spec 17.5.0'. The SDK was the one surface a CHANGELOG reader lands on, disagreeing with the spec about the single fact a retirement note exists to state. All three docblocks now carry the published version. Same stroke: the D3 acceptanceCriteria cited '(ADR route-ownership rule 5)'; no ADR carries that rule. It is AGENTS.md 'Route & surface ownership' rule 5. Registry mirror regenerated with the generator. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…tomation-runs-hasmore
🔴
|
|
os-dev-report {
"issue": 19365,
"issue_reachable": false,
"status": "done",
"round": "PM loop round 2 — contract-review remediation round 3",
"branch": "claude/issue-19365-automation-runs-hasmore",
"pr": "https://github.com/objectstack-ai/objectstack/pull/19493",
"head_for_re_review": "1be868a5d13e6da7dd6dee15ff9f394b25a32415",
"previous_head_that_FAILed": "81f11e52dea4f74a10eee2abd2f19c734c5db7f8",
"session": "session_01UDXER3sdqfeVYpEWZs5mZx",
"premise_still_valid": true,
"ANSWER_TO_THE_CITATION_QUESTION_ASKED_FIRST": {
"question": "Did I push a change that touches the #6361 or #19365 citations?",
"answer": "NO — neither citation was removed, re-pointed or softened, and I did not touch a `#6361` line at all.",
"the_precise_nuance_stated_rather_than_glossed": "My round-3 diff DID edit three lines that CONTAIN `(#19365)` — they are the three SDK docblocks — but only to replace the version phrase `protocol 18` with `@objectstack/spec 17.5.0`. The citation itself is byte-identical on all three and still points at 19365.",
"measurement": "git diff 81f11e5..1be868a, filtered to lines carrying either number, returns exactly three removed/added pairs, each differing only in the version phrase. Citation counts in packages/client/src/index.ts are IDENTICAL at both heads: #19365 = 3, #6361 = 1. Nothing added, nothing dropped.",
"where_my_6361_citations_came_from": "Round 2, not round 3 — four added lines across the changeset, the client test comment, and the D3 entry plus its registry mirror. They were green at 81f11e5, which is the control you cite.",
"the_nine_numbers_confirmed": "The PR's added lines cite exactly nine distinct numbers, which matches your probe: #126, #204, #4127, #6361, #7300, #7359, #8054, #17667, #19365.",
"⛔_what_I_have_not_done_and_will_not": "I have not removed, re-pointed or softened either citation, have not re-run the job, and will not. Greening a board-side 404 by editing a published citation is the exact defect class this card exists to close — a visible red turned into a silent falsehood."
},
"a_push_I_am_HOLDING_pending_your_answer": {
"what": "A local merge commit 99ad620 (origin/main eec56c3 into my branch) plus the five commits it brings. It is committed in my worktree and NOT pushed.",
"why_held": "Your instruction was to not push again until you answer if my change touched either citation. My reading is that it did not — the citations are preserved verbatim — but the honest description is 'edited lines that contain a citation', which is close enough to the line you drew that I am not going to decide it for you. The merge touches no citation (it is other people's work plus a registry regeneration) and pushing it would move the head and restart CI while the blocker sits with the maintainer, which is noise you did not ask for.",
"cost_of_holding": "None that I can see. Say the word and I push it; say the opposite and I drop it.",
"note": "The head I am reporting, 1be868a, is the PUSHED head and is what CI judged."
},
"ground_1_the_clamp_claim": {
"verdict": "CONFIRMED false, and I reproduced your false-zero exactly.",
"the_method_note_reproduced": "grep for the literal `clamped to 1..100` in packages/client/src/index.ts exits 1 — ZERO hits — because the sentence wraps across two comment lines. grep for the shape `clamped` finds it at :5543 immediately. Same lesson as the ADR splice last round: ⛔ never conclude 'absent' from one literal miss; re-derive from what the thing IS.",
"readings_that_prove_the_door_REFUSES": [
"packages/runtime/src/query-param.ts:168-173 — `throw invalidQueryParam(param, 'min_value', …)` and `'max_value'`. It throws; there is no Math.min/Math.max anywhere in it.",
"packages/runtime/src/domains/automation.ts:2652-2658 — the door reads `ListRunsRequestSchema.shape.limit.unwrap()` and passes its `minValue`/`maxValue` straight into that throwing parser.",
"packages/services/service-automation/src/engine.ts:4632 — `const limit = options?.limit ?? 20;` and no clamp downstream.",
"The refusal is PINNED: the #8054 cases in automation-runs-query-validation.test.ts assert `?limit=0` and `?limit=101` answer 400 with `details.code === 'VALIDATION_FAILED'`.",
"LIT CONTROL, and it lights: packages/services/service-messaging/src/messaging-service.ts:541 is `Math.min(Math.max(opts.limit ?? 50, 1), 200)` — a real clamp. The instrument sees one where one exists, so the zero on this door is a reading.",
"Provenance of the false sentence confirmed: packages/client/src/index.ts:6465, the inbox door, says `50, clamped to 1..200` — where it is TRUE. I copied a true sentence onto a door with the opposite behaviour."
],
"what_it_says_now": "«Omit `limit` to take the server's window (20). It is bounded to 1..100 and a value outside that range is REFUSED with `400 VALIDATION_FAILED`, never clamped — so raise it deliberately to see further back.»"
},
"ground_2_the_version_claim": {
"verdict": "CONFIRMED false.",
"readings": [
"packages/spec/src/kernel/protocol-version.ts:18 — `export const PROTOCOL_VERSION = '17.0.0'`, with the file's own docblock saying it is kept in lockstep with the package major and asserted against package.json by protocol-version.test.ts. So 'protocol 18' names a protocol that does not exist.",
"packages/spec/src/api/automation-api.zod.ts:522 — the prescription publishes `@objectstack/spec 17.5.0`, and the generated reference, flows.mdx, the changeset and the registry comment all say 17.5 too. The SDK was the single dissenting surface — and the one a CHANGELOG reader lands on, because my own changeset sends them there."
],
"fixed_in_exactly_three_places": "packages/client/src/index.ts at the `automation.runs.list` docblock, the `automation.listRuns` alias docblock, and the `ScopedEnvironmentClient.automation.listRuns` docblock. All three now read `@objectstack/spec` 17.5.0.",
"⛔_what_I_deliberately_did_NOT_touch": "packages/client/src/index.ts:1719 also contains the string 'protocol 18'. It is a PRE-EXISTING docblock about capability-vocabulary producers, is not in this PR's diff, and is a different subject. Verified against the diff before editing and left alone."
},
"same_stroke_citation_relabel_and_a_correction_to_the_instruction": {
"done": "The D3 acceptanceCriteria's `(ADR route-ownership rule 5)` is relabelled, in the entry file, and the registry mirror was regenerated with `gen:migration-registry` — ⛔ never by hand.",
"⭐_I_did_NOT_use_the_wording_you_suggested_and_here_is_why": "You proposed the house form is `Route-ownership rule #N`. I measured it before writing, because writing an unverified citation form is precisely the mistake that produced the last two FAILs. The measured form in this tree is `route-ownership rule N` — lowercase, no `#`, parenthesised: 4 occurrences of `route-ownership rule 1` and 3 of `route-ownership rule 3`, in packages/runtime/src/domains/data.ts:62, action-execution.ts:390 and action-execution-calldata-batch-retired.test.ts:210. ZERO occurrences of a `#`-prefixed or capitalised form. The only two hits for `ADR route-ownership rule 5` were my own entry and its mirror.",
"what_I_wrote": "`(AGENTS.md route-ownership rule 5)` — the measured lowercase form, with the source named since the whole point of the fix was that the citation pointed at the wrong document.",
"if_you_prefer_your_wording": "Say so and I will change it; I am flagging the divergence rather than silently following or silently ignoring."
},
"every_sentence_I_touched_read_back_against_the_code": "The brief demanded this explicitly, so here is each claim with the reading that proves it. «Returns the newest `limit` runs — a WINDOW, not a page» — engine.ts:4805 sorts startedAt descending and slices to `limit`. «removed in @objectstack/spec 17.5.0» — matches the prescription at automation-api.zod.ts:522, and PROTOCOL_VERSION is 17.0.0 so the old 'protocol 18' could not be right. «it was appended to the query string here and read by nothing on the server» — the three `params.set('cursor', …)` lines existed at base and the engine never read the option; both measured in round 2. «Omit `limit` to take the server's window (20)» — parseIntegerParam returns undefined for a falsy raw, and engine.ts:4632 is `?? 20`. «bounded to 1..100» — the schema's own `.min(1).max(100)`, which the door reads rather than restates. «REFUSED with 400 VALIDATION_FAILED, never clamped» — query-param.ts throws min_value/max_value, pinned by the #8054 cases, and no clamp exists on this path. «There is no continuation token — read `hasMore`» — nothing has ever emitted nextCursor. «widen `limit` (1..100, default 20)» on the other two docblocks — range and default both as above, and neither of those two ever claimed a clamp, so neither needed that half of the fix.",
"verification": {
"ci_at_the_reported_head": "35 distinct check names on 1be868a: 32 success, 2 skipped, 1 failure. The single failure is `Lint & Repo Gates`, which is the board-side citation 404 you own and are holding. Judged against the other 34, this head is green.",
"spec_tests": "packages/spec migrations.test.ts + automation-api.zod.test.ts — exit 0, 188 tests pass. These are the two that cover what this round changed: the D3 entry (migrations.test.ts walks every semantic entry and asserts acceptanceCriteria is non-empty) and the tombstone prescription pin (which still matches after the rewording — worth checking, since rewriting a prescription can silently orphan the regex that pins it).",
"generated_artefacts": "`check:generated` after the spec rebuild: all 15 up to date, both before and after the origin/main merge.",
"registry_regeneration_was_a_real_check_not_a_formality": "The merge brought another agent's new semantic entry into registry.ts, a file both sides had touched. I snapshotted git's text merge, regenerated from the entries, and diffed: IDENTICAL. Both entries present, 227 semantic (up from 226). So the text merge happened to be exact — which is the disjoint-per-entry-file property working, and I verified it rather than assumed it."
},
"what_I_did_NOT_measure_this_round_and_why": {
"the_112_family_gate_union": "NOT re-run at this head. Stated plainly rather than implied. Reasons: (1) the local derivation flagged STALE and the stale inputs were `scripts/pm/dispatch-gates.mjs` ITSELF — the tool that decides the family list — which I could not hand-wave, so I merged origin/main to get a real derivation, and that merge is the push I am holding; (2) the shared verify lock refused twice, see below. The last full local reading was 112 derived · 111 exit 0 · 1 NOT MEASURED · 0 unrun at 81f11e5. What replaces it at this head is CI, which ran the real gates on the real head and reached step 181 of 184 in `Lint & Repo Gates` before the board-side 404 — a stronger reading for those families than my local one, with the three steps after #181 NOT MEASURED.",
"the_client_test_suite_and_typecheck": "NOT re-run at this head, and here is the narrowing DECLARED rather than hidden: this round's diff to packages/client/src/index.ts is COMMENTS ONLY. Proven mechanically — every added and removed line in `git diff 81f11e5..HEAD -- packages/client/src/index.ts`, after excluding diff headers, is a TSDoc `*` continuation line; the filter for any non-comment line returns nothing. No executable line moved, so the round-2 readings (client 49 files / 569 tests, typecheck OK at 0 ledger debt) stand for the executable content. CI re-ran the suite anyway.",
"one_reading_that_was_NOT_a_failure": "`pnpm --filter @objectstack/client typecheck` returned `TS2307: Cannot find module '@objectstack/core/logger'`. That is the unbuilt-dependency PREREQUISITE class — a fresh worktree with an un-built closure — ⛔ not a docblock-induced type error, and ⛔ not evidence about my change. I did not report it as a failure and did not 'fix' it."
},
"shared_lock_contention": "The verify lock refused TWICE with `VERDICT queue-timeout (exit 99) · never acquired`, 9 minutes each, holders being other agents' test runs (`--filter @objectstack/driver-sql --filter @objectstack/cli test`, then `--filter @objectstack/runtime test`). ⛔ Exit 99 is NOT MEASURED — not a failure and not a pass. Per the discipline I spent the interval on lock-free work rather than re-queueing blindly, re-requested under the SAME slot name so the queue position carried, and when 18 minutes had bought no progress I narrowed the check instead of holding the box hostage — the narrowing being the comments-only proof above, declared here rather than absorbed. A later, smaller acquisition for the spec build did succeed (waited 3m51s, held 4m47s).",
"mcp_calls": "0 — no MCP GitHub tool at any point across four rounds. All GitHub reads and writes via the REST proxy with curl.",
"api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/issues/19493/comments (this report, on the PR because the card is 404). ZERO label writes. ⛔ No PR-body edit, and specifically ⛔ no edit to the `Part of #19365` line: it is a dangling reference now, but whether that card returns and under what provenance is the maintainer's call, not mine. Pushes this round: 1 (81f11e5..1be868a). One further commit is held unpushed, see above.",
"the_card_404_verified_first_hand": "I re-took your readings rather than relaying them. GET issues/19365 → 404 and GET issues/comments/5756558349 → 404; controls GET pulls/19493 → 200 and GET issues/comments/5756785503 (the re-anchored record) → 200. So the routes work and that card specifically is gone. ⛔ I asserted nothing about why and did not go looking. I read the re-anchored record on the PR and confirmed its two grounds match what I fixed. Noted for the record: my three earlier os-dev-report comments went to that card and are gone with it, so this comment is the only surviving report for rounds 1-3 — the D3 entry's `reason` field in the diff remains the durable copy of the ruling itself, as you said.",
"open_questions": [
{
"question": "Push the held merge commit 99ad620, or drop it?",
"options": [
"Push it — the branch is 5 commits behind origin/main and a current base makes CI meaningful",
"Hold until the citation blocker is resolved, to avoid moving the head while the maintainer decides"
],
"recommendation": "Hold. The merge changes nothing about the two FAIL grounds, and moving the head now restarts CI against a gate that will 404 again for reasons neither of us can fix."
},
{
"question": "Keep `(AGENTS.md route-ownership rule 5)` or switch to the `Route-ownership rule #5` form you suggested?",
"options": [
"Keep the measured lowercase form",
"Use the suggested form"
],
"recommendation": "Keep the measured form — 7 occurrences in-tree use it and 0 use the other; but it is your call and the change is one word."
}
],
"out_of_scope_findings": [
"carried forward, unchanged: packages/client/src/index.ts:6640 (ai.conversations.list) is door ③'s; :3241/:3244 (listRevisions) is a different domain, unmeasured, pointer only; ListRunsResponseSchema.nextCursor stays declared and never emitted; the ?status= residual is pre-existing and stated in six consumer surfaces. ⛔ None touched.",
"noted, not filed, and already yours: the seat's own re-review flagged a FOURTH sibling door — ListFlowsRequestSchema's `limit .default(50)` + `cursor` against a `listFlows()` that takes no arguments and hard-codes `hasMore: false`. The seat said it is filing that separately. ⛔ I did not widen into it. Recording it here only because the card that would have carried the cross-reference is 404."
],
"deviations": [
"MEASUREMENT (declared narrowing): the 112-family gate union and the client suite were not re-run at this head. Both are covered above with the reason and with what stands in their place. A declared narrowing is a deviation, not a silent gap, which is why it is here.",
"INSTRUCTION (flagged, not silently overridden): I used the measured citation form `route-ownership rule 5` rather than the suggested `Route-ownership rule #5`. Reason and measurement given above; trivially reversible.",
"PROCESS: one push this round, made as soon as the edits were read back, per the lesson from two rounds ago. The merge commit is held, per your citation instruction, and I am asking rather than deciding."
],
"ci_status": "1be868a: 32 success / 2 skipped / 1 failure across 35 check names. The failure is `Lint & Repo Gates` stopping at step 181/184 on two board-side 404s (#6361, #19365) that this PR did not introduce and that were green on the previous head with the same lines. That blocker is above me and is with the maintainer; ⛔ I did not touch it, re-run it, or work around it."
}Generated by Claude Code |
Contract reviewServed-tier: 113/113 N = assistant request rows in the isolated reviewer's transcript, every one stamped Head-sha:
Read from the PR API at start and again at the end: unchanged. It is a merge commit ( ① Derived judgmentsAccept/reject set — the request schema. Accept/reject set — the wire. (a) Exported surface. Registry and generated mirrors. Extracted the D3 object from The three cross-surface facts the previous rounds failed on, re-measured. Version: prescription "@objectstack/spec 17.5.0"; generated reference the same; Changed without needing to. Nothing of substance; the ② Semver grade vs. the changeset's declaration
③ Boundary flags
Ground 1 — VERDICT: FAIL Implemented-by: 交接 —— 载体已剥;并附本席对自己一条已发布评论的更正Ground 1 本席第一手复测,成立。 三个 run-list 发射点的判据在同一个文件里就不一致: ⇒ 携带那句承诺的,恰恰是会静默替换的那一个:
|
Part of #19365
Clause-②: yes
Door ① of three.
GET /api/automation/:name/runsdeclared a paginationparameter it never spent, and then reported — as a literal — that there was
nothing more to fetch. Both halves are addressed here.
The ruling, which is the maintainer's call and not this PR's
Comment
5754491070on #19365 records decision batch #204 item 2, lettersC · C · Aper door, maintainer 「204 同意」 2026-09-21. For door ① theruling reads, verbatim:
⛔ Not re-adjudicated here. Letter A — building a cursor protocol — is
explicitly not taken, so no continuation token is minted and
nextCursorstaysabsent.
Why
Part ofand not a closing keyword. Doors ② (export jobs) and ③ (AIconversations) are ruled but gated on a cloud-repo reading riding #19361, and
the ruling has the seat execute them on that reading's return without
re-entering the decision box. A merge that shut the card would strand
two-thirds of the ruled work, so the card stays open and the seat re-labels it.
The gate
scripts/check-partof-closing-keyword.mjsis the mechanical half ofthat, and its RULE 3 is why no sentence here binds a closing keyword to a
number at all — not even one written to prevent an auto-close, which is the
exact incident that gate exists for.
The premise was re-measured, and one half of the card's body is false
Every reading below was re-taken on
origin/mainat5e7d83c, not relayed.cursordeclared, never readListRunsRequestSchemadeclared it;AutomationEngine.listRunsnever looked at the option; no emit site writesnextCursorhasMorehard-codedautomation.tsreturneddeps.success({ runs, hasMore: false }), a literal, besidemerged.slice(0, limit)limitdeclared, never read.default(20)unique to the export doorListRunsRequestSchemacarries it toolimitis read at the boundary (parseIntegerParam, with the1..100boundstaken off the schema itself), forwarded to
IAutomationService, and spent bythe engine as
RunStore.listHistory's window. It is also pinned by liveenforcement in
automation-runs-query-validation.test.ts. Retiring it wouldhave been a regression, not a narrowing, and the ruling says the
/packagesparent ruling
5651023067does not transfer. Both corrections belong on thecard's thread, which is the census.
What "truncated" means at this seam
The tempting signal is
runs.length === limit. It is wrong at exactly oneinput, and that input is undetectable from the response: a flow holding
exactly
limitruns produces a window byte-identical to one held by a flowwith ten thousand. Reporting
truefor the first is as wrong asfalseforthe second.
Only one of the three sources
listRunsmerges was ever capped — the durablehistory arm, because
RunStore.listHistory(flowName, limit)takes the windowas an argument. The paused arm and the in-memory ring are read in full. So the
signal chosen is an over-read of exactly one row: the history arm is asked
for
limit + 1, and the merged, filtered, ordered set is compared againstlimit. Overflow means a run matched that this window does not carry. Theextra row is dropped by the same
.slice(0, limit)that was always there, sonothing on the wire widens.
⛔
RunStore.listHistory's signature is deliberately not redesigned:over-reading is expressible in the
limitit already takes, so the truncationsignal costs the store contract nothing.
Two things
hasMoredeliberately does not mean, both pinned:does not exist any more; it is not "more" and no
limitbrings it back.remedy is a wider
limit, up to the declared 100.One honest residual, pre-existing and unchanged. Under
?status=, thehistory arm's window is still the newest
limit + 1rows of any status,because
listHistoryhas no status slot and the filter is applied to whatcomes back. A status-filtered
hasMore: falsetherefore means "no furthermatch within the scanned window", not "no further match exists". Pushing the
filter down is a store-contract change; the engine's own comment already
recorded this for the listing itself, and it is called out in the new test's
docblock rather than papered over.
Behaviour changes on the wire
1.
?cursor=a&cursor=banswered400 VALIDATION_FAILED; it now answers200with the key ignored. This reverses a decision recorded under #7300,which chose to validate the key rather than decide it — the reasoning being
that a future cursor implementation must not be the one to discover the type
was never enforced. The ruling decides it instead: there will be no cursor
implementation on this door, so a refusal would be validating a key the
contract no longer has. This route declares no closed query-parameter set, so
an unrecognised name has never been refused here on its own account. The old
refusal cases are superseded by cases asserting the opposite on the same
inputs — the shape #7359 and #8054 already used on this route's other two
parameters.
2.
hasMorecan now betrue. A request whose window is shorter than thematching run set receives
truewhere it previously receivedfalse. A callerthat read
falseas "this is the whole history" was always wrong and is nowtold so.
3. A service implementing no
listRunsPageanswers501naming themember, never a
200carrying a guessedhasMore. "Absence must be loud" —falling through to the domain's
404would leave a caller unable to tell "norun listing is mounted here" from "no such flow". The
403run-read grant runsahead of the service probe and is unaffected, which is what that gate's own
note already required.
Shape of the change
cursor: retiredKey(RUNS_LIST_CURSOR_REMOVED). A tombstone, not adeletion: the request schema is not
.strict(), so a bare deletion makes Zodsilently strip whatever a generated client keeps sending — a clean parse and
a parameter that never takes effect, which is this defect re-created one
layer down (ADR-0104). The form is copied from the landed sibling
(The /packages read doors' declared request schemas and their actual query reads diverge in BOTH directions —
?limit=and?cursor=are declared and never read,?type=is read and never declared #17667 / PR feat(spec): the /packages doors declare the query parameters they execute, and retire the two they never did #19364) rather than invented.IAutomationService.listRunsPagereturning theexported
RunListResult({ runs, hasMore }) — the shapeIExportService.listExportJobsalready uses, minus the cursor nothing mints.cursorleaveslistRuns's options in the same stroke.listRunsPageholds the whole method;listRunsis itsrunshalf. ⭐ One implementation, two projections, so there is no second
merge/filter/sort to rot. This is also why ~120 existing
listRunscallsites across
service-automation,plugin-approvals,examples/andpackages/cliare untouched.RETIRED_KEYS_BY_MAJOR[18]entry plus the D3 semantic entryautomation-runs-cursor-retired. No D2 conversion: a conversion rewrites anauthored source or a stored
sys_metadatarow, and this shape is HTTP-only.Registered at 18, not 17, per the sibling convention.
minoracross the three published packages, carrying theADR-0087 disposition
registered automation-runs-cursor-retired.content/docs/automation/flows.mdx's REST route table advertised?cursoron thisroute. That row is false once the key is retired, so it now states the retirement, that a
request still carrying the key is ignored rather than refused, and that
hasMoreiscomputed with a wider
?limitas the remedy. Flagged by Docs Drift Check (5755158989); theother 10 pages it named document the DATA door's
hasMoreand are true as they stand, so nonewas edited. Written by the dispatching seat, not the implementer — the implementer's one body
write was spent at create.
@objectstack/clientdeclaredcursorand appended?cursor=on all three run-listsurfaces (
automation.runs.list,automation.listRuns,client.environment(id).automation.listRuns).Retiring the key in the schema alone would have left the one generated client this repo ships typing it
stringand sending it into a route that no longer reads it — the ADR-0104 silent strip the tombstoneexists to prevent, one layer down. The option and the emitter are gone from all three, the URL pin is
inverted into a three-surface absence pin, and
'@objectstack/client': minorjoins the changeset. Samecall the repo made when GET /api/v1/notifications 从不解析它声明的请求 schema ——
cursor被静默丢弃(SDK 分页永远第一页),limit默认 20 声明 vs 50 实现 #6361 retired the notificationscursor. Added by the dispatching seat after theat-tier contract review FAILed the previous head on exactly this; the implementer's one body write was
spent at create.
Verification
automation-runs-query-validation.test.ts: 48 → 51, and every assertionthat moved is named. Removed: the
#7300cursor-refusal describe (3parametrised cases) and 3
?cursor=preservation rows — superseded, notdeleted, with the replacement asserting the opposite on the same inputs.
Added: 6 retirement cases and 3
hasMore-relay cases. Changed: the doublenow serves
listRunsPage, andcursor: undefinedleft 10 expected optionsobjects. The
limitpreservation rows are byte-identical otherwise —the door still forwards the caller's own window, never a widened one,
because the over-read lives in the engine.
run-list-truncation.test.ts(14 cases) pins the boundary table —fewer than / exactly / more than
limit— plus a spy proving the storeis asked for
limit + 1.pnpm test: runtime 271 files, service-automation 141 files / 1690 tests.pnpm typecheck: spec, runtime, service-automation — all green, no newtest-typecheck-debt.jsonentries.scripts/pm/dispatch-gates.mjs --commands, reconciledwith
--ran): 112 derived · 110 exit 0 · 2 exit 3 (NOT MEASURED) · 0unrun. Exit codes were captured before any pipe. The two are environmental
refusals, ⛔ not findings and ⛔ not passes:
check-plugin-teardown-shape --self-testcannot reach a commit-pinnedpositive control in a shallow checkout (
--is-shallow-repositoryistruehere; the gate itself ran, exit 0), and
check:dual-build-cjs-loadsrefuses without a repo-wide build (38 packages carry no
dist/). CI hasboth. Two further families initially refused on the same prerequisite class
and were converted into real readings by building what they read:
check:skill-examples(258 prose examples type-check) andcheck:type-check-debt(4 ledger entries re-measured, 53 raw errors, noneabove its recorded number).
Serial constraints
Declared adjacency from the dispatch: PR #19373 holds
packages/spec/dropped-refinements.baseline.json,packages/spec/api-surface/root.jsonandpackages/spec/export-origins/root.json. This PR moves none of those three— regeneration landed on the
contractsshards(
api-surface/contracts.json,export-origins/contracts.json) plusauthorable-surface/api.json, all disjoint.origin/mainwas merged beforethis reading and
check:generatedreports all 15 artefacts current.Acceptance notes
Out of scope, observed, ⛔ not filed and ⛔ not widened into this PR:
ListRunsResponseSchema.nextCursorstays declared and never emitted.Not a contract violation — an absent optional key promises nothing — so it
is not class (b), and minting one is letter A, explicitly not taken. Now
commented in place. Whoever takes door ② or ③ touches the same file.
GET /automation(list flows) also ships a literalhasMore: false.Measured, and there it is true: the handler returns every name with
total === names.length, so nothing is withheld. Recorded so the nextreader does not read the two literals as the same defect. No card.
?status=window residual described above is a real narrowing ofwhat
hasMore: falsecan promise. It is pre-existing, it is the engine's ownrecorded limitation, and closing it is a
RunStorecontract change — theruling scoped this card to the truncation signal.
Deviations from the dispatch's declared file surface, both required by the
ruling's own text and reported rather than taken silently:
packages/spec/src/contracts/automation-service.ts(the ruling's "enginereports truncation to the route" needs the contract member the route calls),
and two
packages/runtimetest doubles that stub the run-list service —http-dispatcher.test.tsandautomation-run-read-permission-gate.test.ts.Generated by Claude Code