mostlyright-data 0.29.7__tar.gz → 0.29.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/PKG-INFO +1 -1
  2. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/pyproject.toml +1 -1
  3. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/commands.md +1 -1
  4. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/live-run.md +3 -2
  5. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/recipe.py +6 -5
  6. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/recipe_explain.py +9 -7
  7. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4.py +39 -10
  8. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_query.py +1 -2
  9. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_runs.py +156 -26
  10. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/.gitignore +0 -0
  11. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/README.md +0 -0
  12. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/scripts/hatch_build.py +0 -0
  13. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/SKILL.md +0 -0
  14. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/agents/openai.yaml +0 -0
  15. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  16. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  17. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  18. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  19. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  20. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  21. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  22. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  23. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  24. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/agent-protocol.md +0 -0
  25. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  26. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  27. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/boundaries.md +0 -0
  28. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  29. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  30. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/installation-parity.md +0 -0
  31. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  32. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  33. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  34. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/one-install.md +0 -0
  35. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/prediction-labels.md +0 -0
  36. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/preparing-table-search.md +0 -0
  37. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/promote.md +0 -0
  38. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/readers.md +0 -0
  39. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/receipts.md +0 -0
  40. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  41. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  42. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/reference-pages.md +0 -0
  43. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/required-protocol.md +0 -0
  44. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/source-credentials.md +0 -0
  45. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/sources.md +0 -0
  46. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  47. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/transforms.md +0 -0
  48. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  49. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  50. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  51. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/__init__.py +0 -0
  52. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  53. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/canonical.py +0 -0
  54. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/formats.py +0 -0
  55. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  56. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/key_seam.py +0 -0
  57. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/page_coverage.py +0 -0
  58. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  59. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/session_probes.py +0 -0
  60. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/skill_assets.py +0 -0
  61. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/table_manifest.py +0 -0
  62. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  63. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  64. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  65. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/activity.py +0 -0
  66. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  67. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/categories.py +0 -0
  68. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/commands.py +0 -0
  69. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  70. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/download.py +0 -0
  71. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  72. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  73. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/parity.py +0 -0
  74. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/probe.py +0 -0
  75. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  76. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  77. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/propose.py +0 -0
  78. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  79. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  80. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  81. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/research.py +0 -0
  82. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/router.py +0 -0
  83. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  84. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/runs.py +0 -0
  85. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/session.py +0 -0
  86. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/stream.py +0 -0
  87. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  88. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  89. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/transport.py +0 -0
  90. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  91. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  92. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  93. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  94. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  95. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  96. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  97. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  98. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  99. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
  100. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  101. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  102. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  103. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  104. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  105. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  106. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  107. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  108. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  109. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  110. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  111. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  112. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  113. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  114. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  115. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  116. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  117. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  118. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/login.py +0 -0
  119. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  120. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  121. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  122. {mostlyright_data-0.29.7 → mostlyright_data-0.29.9}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.29.7
3
+ Version: 0.29.9
4
4
  Summary: Mostly Right hosted CLI for reviewed datasets
5
5
  Project-URL: Homepage, https://mostlyright.md/
6
6
  Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.29.7"
3
+ version = "0.29.9"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -18,7 +18,7 @@ gap rather than doing anything, and `export-hosted-candidate`, which is a backen
18
18
  | `mr-data dataset` | Bring the dataset page into existence before there is anything on it, then fill it in while somebody watches. `dataset create --name TEXT` mints it and prints the `dataset_id`, unless the user's own request already has an empty dataset open, in which case it answers `open_request_reused` with that `dataset_id` and mints nothing (`--separate` mints a new one for a different dataset); `dataset show ID` reads it back; `dataset set ID --name TEXT --topics "a,b,c" --license ID --description-file F` writes the title, the descriptive tags, the SPDX licence and the description under the version it was read at, retrying once if somebody else wrote first, and an empty `--topics` or `--license` takes that value off the page; send `--name` and `--topics` whenever you send `--description-file`, because a description sent on its own can make Studio write a new title and new tags, and read `changed` and `written_by_studio` on the receipt for every field the write moved; a saved write whose public sync fails exits 2 and reports `public_projection_synced: false` — use `dataset sync ID` to retry that sync without rewriting Studio; `dataset note ID --heading H --blocks-file B` writes one cell of the decision record that OUTLIVES every run, and `--list` reads it back; `dataset watch ID` follows the page's own event stream; `dataset activity ID --phase P --message TEXT` says what is happening right now, silently, and is never a chat message and never a cell; `dataset publish ID [--mode public|link|private]` says who can read the dataset — `public` lists it in the public directory and serves it at an address anybody can read, `link` serves it at an unlisted address, `private` takes it back to the workspace — and `dataset publish ID --show` reads that back without changing it; `dataset archive ID --confirm-name TITLE` retires the page and frees its title, deleting nothing. |
19
19
  | `mr-data recipe` | Register one recipe document — dataset, question, table plan, sources, transform, checks and units — in one call, and print the identifiers the server derived. The document is read first and every fault comes back in one refusal, each with the JSON pointer that names it; `--no-lint` sends it as written instead. Registration refuses `THIN_BRIEF_MISSING` when the dataset's record carries no answered question and no delegation, and `THIN_RECIPE_DATASET_UNBOUND` when the document names no `dataset.id`; `--delegated "their words"` writes the delegation onto the dataset and registers in the same command, and clears the first of those two and never the second. `mr-data recipe show ID` reads one back. `mr-data recipe explain FILE` (or a recipe identifier) lists every reason its table can or cannot refresh, each with a rule code and Studio's suggested repair, and exits 2 when one blocks it. `mr-data recipe forecast RECIPE_ID --digest DIGEST` says what one run would ask of each source before it runs, and exits 3 when the run would overrun today's platform quota or one attempt. `mr-data recipe readiness --table TABLE_ID --current-only` reads the revision one table runs now. [Making a table incremental](making-a-table-incremental.md#the-loop-explain-forecast-run-diagnose) says when to use each. |
20
20
  | `mr-data cover` | Generate and attach one branded 1200×630 dataset cover. Give it the dataset ID and what the image should depict; Studio fixes the model, single-color style, curated random palette, dimensions and storage. |
21
- | `mr-data run` | Start one run against a registered recipe, named as `--recipe RECIPE_ID --digest RECIPE_DIGEST` — both required, neither positional, and the digest is the bare hex the registration receipt printed. The mode is one of six: `sample`, `full`, `refresh`, `backfill`, `compact` and `replay`. Four have a shorthand flag (`--sample`, `--full`, `--refresh`, `--backfill`) and two do not, so write `--mode MODE`, which is accepted for every one of them. A refresh executes only Studio's persisted source-action plan, and a plan shape it does not refresh returns `RESYNC_REQUIRED` before acquisition; [which plans refresh](making-a-table-incremental.md#which-plans-refresh) lists them, and `mr-data recipe explain` says which one a table is. Use `mr-data table resync TABLE_ID --request-id UUID` for an explicit full source reread, retaining that UUID if the response is uncertain. `--backfill` states the exact window with `--window START END`. `--mode replay --sources-from RUN_ID` asks Studio to run the registered revision against the RETAINED RAW INPUTS of one named successful run of the same table: nothing is fetched from this computer, nothing is acquired again, the result is compared against that run and never becomes the live version, and it neither asks for nor records an approval. Where Studio has replay switched off, or the named run is not successful, not the same table, or no longer retains its inputs, it answers a typed refusal — report the code rather than retrying in another mode. `--sources-from` on any other mode is refused `THIN_ARGUMENT_INVALID` before anything is sent. `--max-rows` and `--max-source-bytes` bound ONE SOURCE rather than the finished table. A large run is held for a size confirmation (not a charge), which `--confirm` settles and whose printed `confirm_command` re-states every argument the request carried. `--approve-full RUN_ID` releases the full an existing legacy sample-first pair is holding once its preview has succeeded, naming either half of the pair; where the deployment still wants a person at a browser it answers `THIN_INTERACTIVE_HUMAN_REQUIRED` and names the run page. `--retry RUN_ID` tries one failed run again when its triple carries `room_fault: true`, re-stating that run's own coordinate under a byte ceiling that cannot narrow it, and refuses with the triple when the recipe was at fault. `--cancel RUN` stops one. |
21
+ | `mr-data run` | Start one run against a registered recipe, named as `--recipe RECIPE_ID --digest RECIPE_DIGEST` — both required, neither positional, and the digest is the bare hex the registration receipt printed. The mode is one of six: `sample`, `full`, `refresh`, `backfill`, `compact` and `replay`. Four have a shorthand flag (`--sample`, `--full`, `--refresh`, `--backfill`) and two do not, so write `--mode MODE`, which is accepted for every one of them. A refresh executes only Studio's persisted source-action plan, and a plan shape it does not refresh returns `RESYNC_REQUIRED` before acquisition; [which plans refresh](making-a-table-incremental.md#which-plans-refresh) lists them, and `mr-data recipe explain` says which one a table is. Use `mr-data table resync TABLE_ID --request-id UUID` for an explicit full source reread, retaining that UUID if the response is uncertain. `--backfill` states the exact window with `--window START END`. `--mode replay --sources-from RUN_ID` asks Studio to run the registered revision against the RETAINED RAW INPUTS of one named successful run of the same table: nothing is fetched from this computer, nothing is acquired again, the result is compared against that run and never becomes the live version, and it neither asks for nor records an approval. Where Studio has replay switched off, or the named run is not successful, not the same table, or no longer retains its inputs, it answers a typed refusal — report the code rather than retrying in another mode. `--sources-from` on any other mode is refused `THIN_ARGUMENT_INVALID` before anything is sent. `--max-rows` and `--max-source-bytes` bound ONE SOURCE rather than the finished table. A large run is held for a size confirmation (not a charge), which `--confirm` settles and whose printed `confirm_command` re-states every argument the request carried. `--approve-full RUN_ID` releases the full an existing legacy sample-first pair is holding once its preview has succeeded, naming either half of the pair; where the deployment still wants a person at a browser it answers `THIN_INTERACTIVE_HUMAN_REQUIRED` and names the run page. `--retry RUN_ID` tries one failed run again when its triple carries `room_fault: true`, re-stating that run's own coordinate and ceilings and naming the failed run so Studio starts a new attempt, and refuses with the triple when the recipe was at fault. `--cancel RUN` stops one. |
22
22
  | `mr-data status` | Report which of the seven states one run is in, what it delivered and whether a ceiling cut it short, and — on a failure — what failed, where, and whether the fault was the execution room's rather than the recipe's. Exits non-zero on a failed run. `--diagnose` adds everything else Studio can say about the run: its progress, a per-row source's plan counts, its checkpoints, and how each attempt ended, with a lost attempt's cause marked as an inference. |
23
23
  | `mr-data runs` | Report this workspace's own runs: identifier, mode, state and creation time, plus the failure triple of any that failed and whether that failure was the execution room's. `--dataset DATASET_ID` narrows it to one dataset's runs, so "what have I run for this dataset?" is one scoped read, and a value that is not an identifier is refused `THIN_ARGUMENT_INVALID`. `--status` and `--mode` narrow it too and are refused `THIN_ARGUMENT_INVALID` for a word outside the seven states or the six modes; the three combine. Studio narrows before it cuts a page, so a filtered page holds the newest runs that match however many runs the workspace has. `--limit` says how many, and pages are followed to reach it; a listing that does not end says how many were read and suggests a smaller `--limit` or a narrower filter. The answer prints `next_cursor`, and `--cursor` continues from it. |
24
24
  | `mr-data watch` | Stream one run's live progress under the durable event type of each stage, resuming across stream cuts. Exits non-zero when the run failed. |
@@ -28,8 +28,9 @@ appends again, and `watch` stops rather than reconnecting.
28
28
 
29
29
  **A terminal run is cached against `(recipe, digest, mode, clamps)`.** Re-running that exact
30
30
  coordinate replays the stored result rather than making a new attempt — a stored failure included —
31
- which is why `mr-data run --retry RUN_ID` moves a ceiling rather than re-sending the request as it
32
- was. It is also why `SANDBOX_MEMORY_BOUNDARY` reads as a source problem and is not one: it names
31
+ which is why `mr-data run --retry RUN_ID` names the failed run to Studio rather than re-sending the
32
+ request as it was; the ceilings stay the same unless Studio does not admit the named retry, and
33
+ then the byte ceiling is raised as it was before. It is also why `SANDBOX_MEMORY_BOUNDARY` reads as a source problem and is not one: it names
33
34
  the source being fetched, the cause is a clean room with no delegated memory cgroup, it is
34
35
  intermittent and per-instance, and `room_fault` is true on that run so nothing has to be inferred
35
36
  from the wording. Report the failure once, retry it when supported, then report the replacement
@@ -80,11 +80,9 @@ SHOW_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-read.v1"
80
80
  READINESS_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-readiness.v1"
81
81
  PROPOSE_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-propose.v1"
82
82
 
83
- #: The bound on the document. The same number the authoring lane bounded one of its six documents
84
- #: by -- four times the contract's own 262144-byte ceiling on a canonical recipe -- rather than a
85
- #: second number chosen here, because this document is those six documents in one and the ceiling
86
- #: it has to clear is the canonical form's.
87
- MAX_DOCUMENT_BYTES = 4 * 262_144
83
+ #: A plain input file may occupy up to Studio's 2 MiB body ceiling. Before submission,
84
+ #: the complete canonical UTF-8 request envelope must also fit that ceiling.
85
+ MAX_DOCUMENT_BYTES = v4.MAX_RECIPE_REQUEST_BYTES
88
86
 
89
87
  #: ``recipe.schema.json#/$defs/recipe_document``'s required members, in the order the schema lists
90
88
  #: them. Checked here before the network, for the reason every early refusal in this client
@@ -1187,6 +1185,9 @@ def register(
1187
1185
  """
1188
1186
 
1189
1187
  document = read_document(Path(args.target))
1188
+ # UUIDs have fixed wire length. Refuse an oversized envelope before credentials,
1189
+ # capability reads or delegation writes; the actual client checks it again.
1190
+ v4.recipe_registration_body(document, "00000000-0000-0000-0000-000000000000")
1190
1191
  # New grammar must be refused as unserved before local lint can diagnose its shape.
1191
1192
  keyless = any(
1192
1193
  capability == "keyless_window" for _, capability in recipe_explain.gated_grammar(document)
@@ -39,6 +39,7 @@ from mostlyright.data_harness.thin.transport import ThinLaneError
39
39
  from mostlyright.data_harness.thin.v4 import (
40
40
  EXPLAIN_AND_FORECAST_REQUEST_VERSION,
41
41
  StudioV4Client,
42
+ check_recipe_request_size,
42
43
  identifier,
43
44
  )
44
45
 
@@ -1231,17 +1232,18 @@ def explain_document_at_studio(
1231
1232
  nothing, which is what makes it safe to ask before a registration.
1232
1233
  """
1233
1234
 
1235
+ body = {
1236
+ "schema_version": ROUTES_REQUEST_VERSION,
1237
+ "workspace_id": str(client.session.workspace_id),
1238
+ "document": dict(document),
1239
+ }
1240
+ check_recipe_request_size(body)
1234
1241
  try:
1235
- # The one inherited `_call` (thin/v4.py): the route rides the same transport, byte
1236
- # ceiling and refusal translation as every other V4 call.
1242
+ # The one inherited `_call` carries the ordinary response ceiling.
1237
1243
  answer = client._call(
1238
1244
  "POST",
1239
1245
  EXPLAIN_REFRESH_PATH,
1240
- body={
1241
- "schema_version": ROUTES_REQUEST_VERSION,
1242
- "workspace_id": str(client.session.workspace_id),
1243
- "document": dict(document),
1244
- },
1246
+ body=body,
1245
1247
  expected=(200,),
1246
1248
  )
1247
1249
  except ThinLaneError as refusal:
@@ -39,6 +39,7 @@ from collections.abc import Mapping
39
39
  from typing import Any
40
40
  from uuid import UUID
41
41
 
42
+ from mostlyright.data_harness.canonical import canonical_json_bytes
42
43
  from mostlyright.data_harness.thin.runs import StudioApiClient
43
44
  from mostlyright.data_harness.thin.transport import ThinLaneError
44
45
 
@@ -915,6 +916,35 @@ def identifier(value: str, field: str) -> str:
915
916
  raise ThinLaneError("THIN_REQUEST_INVALID", f"{value!r} is not a {field}") from error
916
917
 
917
918
 
919
+ # Studio's API request-body middleware accepts at most 2 MiB. Bound the complete
920
+ # UTF-8 wire envelope, not the recipe's character count or its formatted file size.
921
+ MAX_RECIPE_REQUEST_BYTES = 2 * 1024 * 1024
922
+ # A GET recipe includes the document plus identifiers and registration metadata.
923
+ # Give that envelope 1 MiB headroom; ordinary JSON routes retain their 1 MiB cap.
924
+ MAX_RECIPE_RESPONSE_BYTES = MAX_RECIPE_REQUEST_BYTES + 1024 * 1024
925
+
926
+
927
+ def check_recipe_request_size(body: Mapping[str, Any]) -> None:
928
+ size = len(canonical_json_bytes(body))
929
+ if size > MAX_RECIPE_REQUEST_BYTES:
930
+ raise ThinLaneError(
931
+ "THIN_RECIPE_REQUEST_TOO_LARGE",
932
+ f"the serialized recipe request is {size} bytes; Studio accepts at most "
933
+ f"{MAX_RECIPE_REQUEST_BYTES} bytes including its request envelope. "
934
+ "Reduce the recipe before submitting it",
935
+ )
936
+
937
+
938
+ def recipe_registration_body(document: Mapping[str, Any], workspace_id: str) -> dict[str, Any]:
939
+ body = {
940
+ "workspace_id": workspace_id,
941
+ "document": dict(document),
942
+ "require_category": True,
943
+ }
944
+ check_recipe_request_size(body)
945
+ return body
946
+
947
+
918
948
  class StudioV4Client(StudioApiClient):
919
949
  """The V4 routes, over the transport and the hardening :class:`StudioApiClient` already has.
920
950
 
@@ -993,11 +1023,7 @@ class StudioV4Client(StudioApiClient):
993
1023
  return self._call(
994
1024
  "POST",
995
1025
  CREATE_RECIPE_PATH,
996
- body={
997
- "workspace_id": str(self.session.workspace_id),
998
- "document": dict(document),
999
- "require_category": True,
1000
- },
1026
+ body=recipe_registration_body(document, str(self.session.workspace_id)),
1001
1027
  expected=(201,),
1002
1028
  )
1003
1029
 
@@ -1008,6 +1034,7 @@ class StudioV4Client(StudioApiClient):
1008
1034
  "GET",
1009
1035
  GET_RECIPE_PATH.format(recipe_id=identifier(recipe_id, "recipe identifier")),
1010
1036
  expected=(200,),
1037
+ maximum=MAX_RECIPE_RESPONSE_BYTES,
1011
1038
  )
1012
1039
 
1013
1040
  def recipe_readiness(
@@ -1049,14 +1076,16 @@ class StudioV4Client(StudioApiClient):
1049
1076
  def explain_recipe_refresh(self, document: Mapping[str, Any]) -> dict[str, Any]:
1050
1077
  """Judge one unregistered recipe document for refresh, and read what this Studio serves."""
1051
1078
 
1079
+ body = {
1080
+ "schema_version": EXPLAIN_AND_FORECAST_REQUEST_VERSION,
1081
+ "workspace_id": str(self.session.workspace_id),
1082
+ "document": dict(document),
1083
+ }
1084
+ check_recipe_request_size(body)
1052
1085
  return self._call(
1053
1086
  "POST",
1054
1087
  EXPLAIN_RECIPE_REFRESH_PATH,
1055
- body={
1056
- "schema_version": EXPLAIN_AND_FORECAST_REQUEST_VERSION,
1057
- "workspace_id": str(self.session.workspace_id),
1058
- "document": dict(document),
1059
- },
1088
+ body=body,
1060
1089
  expected=(200,),
1061
1090
  )
1062
1091
 
@@ -54,7 +54,6 @@ from mostlyright.data_harness.thin.session import (
54
54
  )
55
55
  from mostlyright.data_harness.thin.transport import ThinLaneError, UrlLibTransport
56
56
  from mostlyright.data_harness.thin.v4 import (
57
- GET_RECIPE_PATH,
58
57
  GET_RUN_PATH,
59
58
  GET_RUN_QUERY_PATH,
60
59
  RUN_QUERY_PATH,
@@ -278,7 +277,7 @@ class StudioV4QueryClient(StudioV4Client):
278
277
  def recipe_source_ids(self, recipe_id: str) -> list[str]:
279
278
  """The sources a recipe registered, which a session opened for its dataset may probe."""
280
279
 
281
- record = self._call("GET", GET_RECIPE_PATH.format(recipe_id=recipe_id), expected=(200,))
280
+ record = self.recipe(recipe_id)
282
281
  listed = record.get("source_ids")
283
282
  sources = (
284
283
  [item for item in listed if isinstance(item, str) and item]
@@ -129,8 +129,10 @@ DISCARD_HISTORY_FLAG = "--discard-snapshot-history"
129
129
  #: ⚠ AND ANOTHER ATTEMPT NEEDS A DIFFERENT COORDINATE. Studio derives a run's identity from
130
130
  #: ``(recipe_id, recipe_digest, mode, clamps, resource_class)`` and replays a terminal result for
131
131
  #: an identical command, so re-issuing the same request returns the same failure without waking a
132
- #: worker. ``mr-data run --retry RUN_ID`` is what turns that into one command; see
133
- #: :func:`retry_clamps` for the one coordinate it moves and why moving it changes nothing else.
132
+ #: worker. ``mr-data run --retry RUN_ID`` is what turns that into one command: it names the failed
133
+ #: run as ``retry_of_run_id``, which Studio folds into the identity, and the ceilings go out as
134
+ #: they were. See :func:`retry_clamps` for the coordinate it moves instead when Studio answers
135
+ #: ``RUN_NOT_RETRYABLE`` or a stored run that is already over.
134
136
  ROOM_FAULT_CODES: frozenset[str] = frozenset(
135
137
  {
136
138
  "EXECUTION_LEASE_EXPIRED",
@@ -591,9 +593,12 @@ def declare_run_arguments(parser: argparse.ArgumentParser) -> None:
591
593
  "--retry",
592
594
  metavar="RUN_ID",
593
595
  help="try this failed run again, when what failed was the room rather than the recipe; "
594
- "the same recipe, digest, mode and ceilings go out under one byte clamp raised above what "
595
- "the document declares, because an identical command replays the stored failure. A run "
596
- "that states a byte ceiling of its own has no such coordinate to move and is refused",
596
+ "the same recipe, digest, mode and ceilings go out naming the failed run, so Studio "
597
+ "starts a new attempt rather than replaying the stored failure. When Studio does not "
598
+ "admit that for the run, or answers with a stored run that is already over, the "
599
+ "command goes out once more under a byte clamp raised above what the document "
600
+ "declares. A stored run that is queued or running is printed as it is and no second "
601
+ "run is started",
597
602
  )
598
603
  parser.add_argument(
599
604
  "--approve-full",
@@ -1289,12 +1294,45 @@ def fresh_full_record(run: Mapping[str, Any]) -> bool:
1289
1294
  )
1290
1295
 
1291
1296
 
1297
+ #: The refusals of a retry that named the failed run after which the same retry goes out once
1298
+ #: more under a moved byte clamp, which is what this command sent before it named the run.
1299
+ #: ``RUN_NOT_RETRYABLE`` is Studio declining ``retry_of_run_id`` for this run: its list of
1300
+ #: platform failures is its own, and a Studio that has not yet deployed a code this client reads
1301
+ #: as a room fault answers with it. The snapshot-history refusal is here because the command it
1302
+ #: prints is re-typed without ``retry_of_run_id`` (there is no flag for it), so that command has
1303
+ #: to carry a coordinate of its own.
1304
+ #:
1305
+ #: ``KEEL_ENGINE_REQUIRED`` is here because naming the run can cause it. Studio never hands a
1306
+ #: retry of a Python run to Keel (``v4/runs.py``, ``retry_of_python_run``), and then refuses a
1307
+ #: recipe the Python engine does not build. The body this command sent before it named the run
1308
+ #: carries no ``retry_of_run_id``, so admission chooses the engine again and the run can start.
1309
+ #: When the second answer is the same refusal, it is raised as it is.
1310
+ _CLAMP_RETRY_CODES: frozenset[str] = frozenset(
1311
+ {"RUN_NOT_RETRYABLE", SNAPSHOT_HISTORY_CODE, "KEEL_ENGINE_REQUIRED"}
1312
+ )
1313
+
1314
+ #: The states of a run answered to the named retry that mean no new attempt started. Studio's own
1315
+ #: automatic resume of a lost progressive full starts its successor with the body the named retry
1316
+ #: sends (``apps/api/.../v4/auto_resume.py``: the lost run's clamps and resource class, with
1317
+ #: ``resume_capture_run_id`` and ``retry_of_run_id`` both naming it), so once that successor
1318
+ #: exists the named retry derives its run id and Studio answers ``201`` with the successor's
1319
+ #: stored record. When that record is already over, the retry goes out under the moved byte
1320
+ #: clamp, which is a coordinate the successor does not hold.
1321
+ #:
1322
+ #: ``succeeded`` is a member for the same reason as the other two: a run that is over is not the
1323
+ #: attempt this command was asked for. A plain start of the same command is answered with the
1324
+ #: failed run's stored record, so without this the table could not be built again from this
1325
+ #: command once a successor had succeeded, and 0.29.7 started a run here.
1326
+ _STORED_END_STATUSES: frozenset[str] = TERMINAL_RUN_STATUSES
1327
+
1328
+
1292
1329
  def retry_body(
1293
1330
  run: Mapping[str, Any],
1294
1331
  run_id: str,
1295
1332
  *,
1296
1333
  declared_bytes: int = 0,
1297
1334
  fresh_read_control: bool = False,
1335
+ clamp_moved: bool = False,
1298
1336
  ) -> dict[str, Any]:
1299
1337
  """The ``create_run_command`` that is this failed run again, rebuilt off its own record.
1300
1338
 
@@ -1302,6 +1340,12 @@ def retry_body(
1302
1340
  nothing else: the whole point of ``--retry RUN_ID`` is that it does not require somebody to
1303
1341
  have kept the recipe, the digest, the mode and the ceilings the first attempt went out with.
1304
1342
 
1343
+ THE FAILED RUN IS NAMED, AND THE CEILINGS ARE THE ONES IT CARRIED. ``retry_of_run_id`` enters
1344
+ the identity Studio derives, so the command is a new attempt without any ceiling changing
1345
+ (planning#234). ``clamp_moved`` builds the other body instead: no ``retry_of_run_id``, and
1346
+ :func:`retry_clamps` in place of the stated ceilings, for a Studio that answered
1347
+ ``RUN_NOT_RETRYABLE`` to the first.
1348
+
1305
1349
  ⚠ A FRESH FULL IS RETRIED FRESH, AND THE BODY SAYS SO. Studio already keeps a resumed chain
1306
1350
  on the answer lineage its origin was admitted with, so a retry of a fresh full reads fresh
1307
1351
  whatever this body says; stating ``reuse_saved_answers: false`` makes the body, and the rerun
@@ -1319,7 +1363,16 @@ def retry_body(
1319
1363
  "THIN_RESPONSE_INVALID", f"Studio reported run {run_id} without a {member}"
1320
1364
  )
1321
1365
  body[member] = stated
1322
- body["clamps"] = retry_clamps(run, declared_bytes=declared_bytes)
1366
+ if clamp_moved:
1367
+ body["clamps"] = retry_clamps(run, declared_bytes=declared_bytes)
1368
+ else:
1369
+ carried = run.get("clamps")
1370
+ clamps = dict(carried) if isinstance(carried, Mapping) else {}
1371
+ if "window" in clamps:
1372
+ clamps["window"] = _command_window(clamps["window"])
1373
+ if clamps:
1374
+ body["clamps"] = clamps
1375
+ body["retry_of_run_id"] = identifier(run_id, "run identifier")
1323
1376
  if body["mode"] == "full":
1324
1377
  body["progressive"] = True
1325
1378
  if run.get("progressive") is True:
@@ -1395,7 +1448,67 @@ def retry(run_id: str, *, client: StudioV4RunClient) -> dict[str, Any]:
1395
1448
  "--mode replay --sources-from RUN_ID once the room is well, or build the recipe "
1396
1449
  "rather than replaying it",
1397
1450
  )
1398
- # ⚠ THE RECIPE IS READ BEFORE THE RETRY IS BUILT, and that round trip is the price of the
1451
+ # ⚠ THE CAPABILITY IS READ ONLY WHEN IT CAN CHANGE THE BODY. A retry of anything but a fresh
1452
+ # full sends the same body whatever this Studio serves, so asking would be a round trip that
1453
+ # decides nothing.
1454
+ fresh_read_control = False
1455
+ if fresh_full_record(run):
1456
+ table_id = run.get("table_id")
1457
+ if isinstance(table_id, str) and table_id:
1458
+ fresh_read_control = (
1459
+ table_capabilities(client, identifier(table_id, "table identifier")).get(
1460
+ FRESH_READ_CONTROL
1461
+ )
1462
+ is True
1463
+ )
1464
+ body = retry_body(run, run_id, fresh_read_control=fresh_read_control)
1465
+ try:
1466
+ started = client.create_run(body)
1467
+ except ThinLaneError as refusal:
1468
+ if getattr(refusal, "studio_code", None) in _CLAMP_RETRY_CODES:
1469
+ return _retry_under_a_moved_clamp(
1470
+ run, run_id, client=client, triple=triple, fresh_read_control=fresh_read_control
1471
+ )
1472
+ held = _held_for_confirmation(refusal, body)
1473
+ if held is None:
1474
+ raise
1475
+ # ⚠ SETTLED BY THE HELD RUN'S OWN ID. A re-typed start has no flag for
1476
+ # `retry_of_run_id`, so it would name the failed run's coordinate and Studio would answer
1477
+ # with the stored failure. The refusal names the run Studio held, and `--confirm-held`
1478
+ # confirms that exact record.
1479
+ held["confirm_command"] = confirm_held_command(
1480
+ identifier(str(held["run_id"]), "run identifier")
1481
+ )
1482
+ held["retry_of"] = run_id
1483
+ held["retried_failure"] = triple
1484
+ return held
1485
+ if started.get("status") in _STORED_END_STATUSES:
1486
+ # ⚠ A RUN THAT IS ALREADY OVER IS NOT ANOTHER ATTEMPT. Studio answered the named retry
1487
+ # with a record it had stored under the same identity (see `_STORED_END_STATUSES`), so
1488
+ # printing it as `run_retried` would hand back a stored failure as the retry
1489
+ # (planning#234). The moved clamp is the coordinate 0.29.7 sent, and it starts a run.
1490
+ return _retry_under_a_moved_clamp(
1491
+ run, run_id, client=client, triple=triple, fresh_read_control=fresh_read_control
1492
+ )
1493
+ return _retried(client, started, run_id=run_id, triple=triple, clamp_moved=False)
1494
+
1495
+
1496
+ def _retry_under_a_moved_clamp(
1497
+ run: Mapping[str, Any],
1498
+ run_id: str,
1499
+ *,
1500
+ client: StudioV4RunClient,
1501
+ triple: Mapping[str, Any],
1502
+ fresh_read_control: bool,
1503
+ ) -> dict[str, Any]:
1504
+ """The same retry without ``retry_of_run_id``, under the one ceiling that can move.
1505
+
1506
+ What ``--retry`` sent before it named the failed run, kept for a Studio that answers
1507
+ ``RUN_NOT_RETRYABLE`` or ``KEEL_ENGINE_REQUIRED`` to the named retry, or answers it with a
1508
+ stored run that is already over.
1509
+ """
1510
+
1511
+ # ⚠ THE RECIPE IS READ BEFORE THIS BODY IS BUILT, and that round trip is the price of the
1399
1512
  # clamp being inert. Studio projects from the document's own declared ceilings, so a clamp
1400
1513
  # chosen without reading them is a clamp that might lower what the run projects -- and
1401
1514
  # `projected_bytes` is what the spend and sample-first thresholds are read against.
@@ -1411,24 +1524,12 @@ def retry(run_id: str, *, client: StudioV4RunClient) -> dict[str, Any]:
1411
1524
  "Studio answered without the recipe document this run was started against, so the "
1412
1525
  "bytes it is projected against could not be read and a retry cannot be sized",
1413
1526
  )
1414
- # ⚠ THE CAPABILITY IS READ ONLY WHEN IT CAN CHANGE THE BODY. A retry of anything but a fresh
1415
- # full sends the same body whatever this Studio serves, so asking would be a round trip that
1416
- # decides nothing.
1417
- fresh_read_control = False
1418
- if fresh_full_record(run):
1419
- table_id = run.get("table_id")
1420
- if isinstance(table_id, str) and table_id:
1421
- fresh_read_control = (
1422
- table_capabilities(client, identifier(table_id, "table identifier")).get(
1423
- FRESH_READ_CONTROL
1424
- )
1425
- is True
1426
- )
1427
1527
  body = retry_body(
1428
1528
  run,
1429
1529
  run_id,
1430
1530
  declared_bytes=declared_source_bytes(document),
1431
1531
  fresh_read_control=fresh_read_control,
1532
+ clamp_moved=True,
1432
1533
  )
1433
1534
  try:
1434
1535
  started = client.create_run(body)
@@ -1450,10 +1551,25 @@ def retry(run_id: str, *, client: StudioV4RunClient) -> dict[str, Any]:
1450
1551
  held["retry_of"] = run_id
1451
1552
  held["retried_failure"] = triple
1452
1553
  return held
1554
+ return _retried(client, started, run_id=run_id, triple=triple, clamp_moved=True)
1555
+
1556
+
1557
+ def _retried(
1558
+ client: StudioV4RunClient,
1559
+ started: Mapping[str, Any],
1560
+ *,
1561
+ run_id: str,
1562
+ triple: Mapping[str, Any],
1563
+ clamp_moved: bool,
1564
+ ) -> dict[str, Any]:
1565
+ """The receipt of a retry Studio started, with the cell that says why the run exists."""
1566
+
1453
1567
  payload = _started(started, client.session, status="run_retried")
1454
1568
  payload["retry_of"] = run_id
1455
1569
  payload["retried_failure"] = triple
1456
- payload["note"] = _retry_note(client, started, run_id=run_id, triple=triple)
1570
+ payload["note"] = _retry_note(
1571
+ client, started, run_id=run_id, triple=triple, clamp_moved=clamp_moved
1572
+ )
1457
1573
  return payload
1458
1574
 
1459
1575
 
@@ -1497,6 +1613,7 @@ def _retry_note(
1497
1613
  *,
1498
1614
  run_id: str,
1499
1615
  triple: Mapping[str, Any],
1616
+ clamp_moved: bool = False,
1500
1617
  ) -> dict[str, Any] | None:
1501
1618
  """One cell on the NEW run saying which fault it is another attempt at, or ``None``.
1502
1619
 
@@ -1524,17 +1641,31 @@ def _retry_note(
1524
1641
  except ThinLaneError:
1525
1642
  return None
1526
1643
  try:
1527
- cell = client.append_cell(fresh, body=_retry_cell(client, fresh, run_id, triple))
1644
+ cell = client.append_cell(
1645
+ fresh, body=_retry_cell(client, fresh, run_id, triple, clamp_moved=clamp_moved)
1646
+ )
1528
1647
  except ThinLaneError as refusal:
1529
1648
  return {"written": False, "refusal": {"code": refusal.code, "detail": refusal.detail}}
1530
1649
  return {"written": True, "cell_id": cell.get("cell_id")}
1531
1650
 
1532
1651
 
1533
1652
  def _retry_cell(
1534
- client: StudioV4RunClient, fresh: str, run_id: str, triple: Mapping[str, Any]
1653
+ client: StudioV4RunClient,
1654
+ fresh: str,
1655
+ run_id: str,
1656
+ triple: Mapping[str, Any],
1657
+ *,
1658
+ clamp_moved: bool = False,
1535
1659
  ) -> dict[str, Any]:
1536
1660
  """The append command that cell is, composed where its refusals are already being caught."""
1537
1661
 
1662
+ coordinate = (
1663
+ "A byte ceiling above anything the sources can deliver gives the attempt a coordinate "
1664
+ "of its own; an identical command would have replayed the stored failure."
1665
+ if clamp_moved
1666
+ else "The attempt names the failed run and carries the same ceilings; an identical "
1667
+ "command that did not name it would have replayed the stored failure."
1668
+ )
1538
1669
  return run_cell_body(
1539
1670
  workspace_id=client.session.workspace_id,
1540
1671
  run_id=fresh,
@@ -1544,9 +1675,8 @@ def _retry_cell(
1544
1675
  f"Run {run_id} failed at the {triple['failed_stage']} stage with "
1545
1676
  f"{triple['failure_code']}: {triple['failure_detail']}\n\n"
1546
1677
  "That code is about the room the run landed in rather than about this recipe, so the "
1547
- "document is unchanged. A byte ceiling above anything the sources can deliver gives "
1548
- "the attempt a coordinate of its own; an identical command would have replayed the "
1549
- "stored failure. Full retries use progressive acquisition. When the failed full was "
1678
+ f"document is unchanged. {coordinate} "
1679
+ "Full retries use progressive acquisition. When the failed full was "
1550
1680
  "already progressive, the retry also names it as the capture ancestor so Studio can "
1551
1681
  "reuse compatible sealed pages."
1552
1682
  ),