feat(interfaces): scenario_control/v1 gains list, and load answers with what it loaded - #232
feat(interfaces): scenario_control/v1 gains list, and load answers with what it loaded#232TedSjoblom wants to merge 2 commits into
Conversation
…th what it loaded `load` takes an id and nothing lists the ids, so a caller has to already know one. A control station cannot offer a picker and an operator reads a directory listing over someone's shoulder. The shape is already settled twice in this repository: replay_control/v1's list_files and scenario_checkpoint/v1's list_checkpoints both take an optional `pattern` and reply with a `base_directory`. scenario_control/v1 is the odd one out. ListRequest/ListResponse follow them clause for clause, including `truncated` — a listing that quietly stopped short reads as "that is all of them". ScenarioInfo carries `loadable` + `reason` rather than filtering the rows out. A library can hold a document a responder can read and will not load; omitting those hides a file the operator can see in the directory, and listing them as loadable is a lie the picker discovers by trying. Two corrections to `load` while this file is open. LoadResponse was empty where StepResponse.tick already makes the argument for answering directly: it now carries the resolved scenario_id, scenario_hash and the new run_id, so a caller need not race the 1 Hz status broadcast to learn its own result. And the stated failure path — "on failure ... the state returns to NO_SCENARIO" — is destructive: followed literally, a typo'd id kills a running exercise. Slipway implements load transactionally instead; this makes that the contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
freol35241
left a comment
There was a problem hiding this comment.
The gap is real and the shape is right — pattern / base_directory / truncated follows list_files and list_checkpoints faithfully, and keeping unloadable rows on the wire with a reason is the correct call. Scope is good: one file, wire-compatible, and no interfaces.yaml change needed since procedures come from the descriptor.
Three things I'd like fixed before merge, plus some polish.
1. bool loadable = 7 has the dangerous default. Proto3 zero is false = "would refuse to load". Any ScenarioInfo built without explicitly setting the field — a simpler responder, a partially-populated row — reports the whole library as unloadable and the picker greys out everything. The safe default has to be the affirmative one. An enum with a countable unknown (LOADABILITY_UNKNOWN = 0) is my preference; optional bool loadable also works.
2. The transactional rewrite contradicts the state machine line above it. The diff keeps load : any -> LOADING -> STOPPED while the prose says a failed load leaves the previous scenario "running untouched". Both can't hold — if it kept running, the state was never LOADING. Dropping | NO_SCENARIO also removed the only hint that a successful load from RUNNING stops the run. Two explicit sentences would settle it: when LOADING is entered (only on commit), and that a successful load does terminate a running exercise.
3. run_id in LoadResponse conflicts with how run_id is minted everywhere else. RestoreResponse.run_id is documented as "the run that just began"; load lands in STOPPED, where no run has begun. And reset returns to tick 0 with the scenario intact — does that re-mint? As written, load -> start -> reset -> start gives two runs from tick 0 sharing one run_id, which undercuts the recording-is-evidence reasoning scenario_checkpoint/v1 leans on. Either drop it from LoadResponse, or document minting at load and re-minting at reset.
Polish:
step_size_msis auint32whereScenarioStatus.step_sizeis agoogle.protobuf.Duration. Same quantity, two encodings — a picker comparing a listed scenario against the running one has to convert.- Overloaded zeros in
ScenarioInfo:duration_ticks = 0is documented as both "unknown" and "open-ended", andvessel_count/step_size_msoverload zero too. Explicit presence would remove the ambiguity. - Siblings are
list_filesandlist_checkpoints; this is barelistwith genericListRequest/ListResponse. No collision risk given the per-interface package, so take it or leave it — but it does break the pattern the description cites as precedent. - Undefined whether
reasonmay be non-empty whenloadableis true.
Three blocking review points on #232, plus the polish items. Loadability. `bool loadable` had the dangerous default: proto3 zero is false, so any ScenarioInfo built without setting the field reports the whole library as unloadable and a picker greys out everything. Replaced with a nested enum whose zero is a countable unknown (LOADABILITY_UNKNOWN / LOADABLE / NOT_LOADABLE), documented as offerable — the safe default is the affirmative one. Matches ScenarioParticipant.Role and ScenarioStatus.State. State machine. The transactional rewrite contradicted the line above it: `load : any -> LOADING -> STOPPED` cannot hold while the prose says a failed load leaves the previous scenario running untouched. Says both things explicitly now — LOADING is entered only once the load has committed, and a successful load *does* terminate a running exercise, coming to rest in STOPPED at tick 0. That also restores the hint lost with `| NO_SCENARIO`: STOPPED is reachable by load from any state. Run identity. Dropped LoadResponse.run_id. It conflicted with how run ids are minted everywhere else — RestoreResponse.run_id is "the run that just began", while load lands in STOPPED where no run has begun, and load -> start -> reset -> start gave two runs from tick 0 sharing one id. A run id is now documented as minted when a run begins, observed on ScenarioStatus.run_id; reset and stop say the run ends there. Polish: - step_size_ms (uint32) -> google.protobuf.Duration step_size, directly comparable with ScenarioStatus.step_size instead of needing conversion. - vessel_count and duration_ticks gain explicit presence. Present 0 vessels is a real answer; absent duration_ticks means no declared end, and the comment states outright that open-ended and undetermined are deliberately not distinguished rather than reusing 0 as a sentinel. - list -> list_scenarios, matching list_files and list_checkpoints. - reason is defined: set for NOT_LOADABLE, may explain LOADABILITY_UNKNOWN, MUST be empty when LOADABLE. Contract only; no interfaces.yaml change, procedures come from the descriptor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed as 2733998 — one file, Blocking1. The dangerous default is gone. enum Loadability {
LOADABILITY_UNKNOWN = 0;
LOADABLE = 1;
NOT_LOADABLE = 2;
}
Loadability loadability = 7;Zero now means "the responder expressed no opinion", and the comment says outright that a picker must offer such a row and let 2. The state machine and the prose now say the same thing. You were right that both couldn't hold. Resolved by stating the two facts explicitly rather than leaving them to be inferred from an arrow:
3. Polish — all four taken
Also dropped the "twelve lines below" cross-reference in VerificationNo Regenerated both SDKs and the docs; the registry reports the rename and the defaults behave: Python SDK suite 164 passed / 1 skipped, JS 18 passing. |
The gap
loadtakes an id, and nothing in the interface lists the ids. A caller has to already know one, so a control station cannot offer a picker — an operator picks a scenario by reading a directory listing over someone's shoulder.Slipway is the server implementation of this interface and resolves ids inside a configured library directory, which is exactly the shape a
listwould describe.Why this shape
The shape is already settled twice in this repository, and this follows it rather than inventing a third:
replay_control/v1scenario_checkpoint/v1ListFilesRequest.patternListCheckpointsRequest.patternListRequest.patternbase_directory,filesbase_directory,checkpoints,truncatedbase_directory,scenarios,truncatedtruncatedis carried for the reasonListCheckpointsResponsegives it: a listing that quietly stopped short reads as "that is all of them", and an operator goes looking for a scenario that is in fact there.The one field worth arguing about
ScenarioInfocarriesbool loadable+string reasoninstead of filtering unloadable rows out. Both alternatives are worse:This is not a corner case for a simulator whose scenario language is still growing: a document parses fine and names a construct the build cannot yet elaborate. Slipway has exactly that today — its library holds both YAML and OpenSCENARIO DSL documents, and most of the DSL subset is declared-but-not-built.
Two corrections to
load, while this file is openBoth were raised on #215 before it merged and have had nowhere to go since.
LoadResponsewas empty.StepResponse.ticktwelve lines below already makes the argument: a caller should not have to race a 1 Hz status broadcast to learn the result of its own call. It now carries the resolvedscenario_id,scenario_hashand the newrun_id. It also makes an ambiguous id visible — a caller that asked forharborand got back a hash it did not expect can say so.The stated failure path was destructive. "on failure … the state returns to NO_SCENARIO", followed literally, means a typo'd scenario id kills a running exercise. Slipway deliberately deviates and implements
loadtransactionally: everything is validated first and the swap happens only on success, so a failed load leaves the running exercise untouched. This makes that the contract.Scope and compatibility
One file, +89/-4. Nothing existing changes wire format —
listis a new procedure andLoadResponse's fields are additions to an empty message, so an old client reading a new reply sees what it always saw. A client callinglistagainst a server that does not serve it gets no responder, which is the same answer it gets for any procedure a connector has not implemented.Generated SDK code is not committed here;
sdks/python/generate_python.shand the SDK test suite both pass against it locally (164 passed, 1 skipped).🤖 Generated with Claude Code