WIP - #131
Draft
benh wants to merge 31 commits into
Draft
Conversation
mypy 1.18 rejects `if x is Any:` as `comparison-overlap` when `x` is declared as a union of concrete types. The comparison is correct at runtime: `get_args` on an annotation such as `dict[str, Any]` hands back the `typing.Any` object itself, so the converters meet it as a value. Only the declared parameter types disagree, since they do not mention `Any`. Route the comparison through `is_annotation_any`, whose parameter is typed `object` — the honest domain of what typing introspection returns. The converters' declared unions stay as they are, and the runtime behavior is identical: the same `is` comparison, one call deeper. Until now this error failed the build of every target whose closure reaches `reboot/api.py` under a fresh mypy run, which is how it was found: Bazel's remote cache had been serving stale mypy results, so CI never re-ran mypy over these files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A `description=` was only set on `McpMethodOptions.description`, so a Reader, Writer, Transaction or Workflow that was not also an MCP tool didn't have it. It is now set on `MethodOptions.description` for every method, and MCP tool and resource descriptions read from there. `McpMethodOptions.description` is deprecated but still read as a fallback, so protos that already set it keep their descriptions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first of several changes, split so that each can be reviewed on its own and so the dashboard can be tried out while the rest is written. It is not a documented feature yet: nothing opens a page by itself, so seeing one means passing `--open-dashboard` or visiting the URL. `rbt dev run` now starts a second Reboot application alongside the developer's, with its own state store, holding what the dashboard needs. The companion watches the developer's `api/` directory and records what those files declare, so the dashboard can describe an application. It also serves the page itself. The page reads that schema reactively and renders one section per state type: its fields, and each method's kind, whether it constructs, whether it is reachable over MCP, its signature and the errors it raises. Dashboard state, such as which detail views are open and which are closed, is saved in Reboot state, so it survives a hot reload and an `rbt dev run` restart. Auto-open is complete but off. `_AUTO_OPEN_DASHBOARD` is False, so only `--open-dashboard` opens a page. We don't reopen the dashboard if the developer already has it open, and we use the `Presence` library to determine whether they do. Note that presence does not drain through a DevPod workstation's port forward, which is filed separately. This will eventually supersede the inspect dashboard at `/__/inspect`, which lists state instances and their values. It does not replace it yet and both exist meanwhile: this describes an application's API, its state types, their fields and their methods, and cannot yet show the data behind them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A method can say what it does, but a state type is the sum of its state and its methods, and its name alone does not say what it is for. `Type` now takes a description, which the dashboard shows beside the state type's name and file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manylinux images build every CPython with `--disable-shared` and delete even the static `libpython` archives. The `reboot-dev-reboot` genrule links `reboot_native.node` with `-lpython3.10`, a flag emitted by `python3.10-config --ldflags --embed`. That link has never been able to succeed inside these images. CI stayed green only while Bazel's remote cache served the genrule's outputs. The first cache miss made every platform fail deterministically. On x86_64 that miss came from a runner hardware swap: it changed the `lscpu` portion of `the_environment.txt`, and with it the whole cache scope. Point `python`/`python3` at a python-build-standalone CPython 3.10, which ships `libpython3.10.so`. It is the same build `reboot/nodejs/prepare_environment.sh` downloads. `pip`/`pip3` stay on the manylinux interpreter, whose layout `auditwheel` and the wheel builds expect. `python3` and `pip` therefore deliberately name different installations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
benh
force-pushed
the
caller-static-analysis-2
branch
3 times, most recently
from
August 17, 2026 01:38
883f11f to
26736aa
Compare
benh
commented
Aug 17, 2026
benh
force-pushed
the
caller-static-analysis-2
branch
8 times, most recently
from
August 17, 2026 03:26
0c5757f to
cb9462a
Compare
`rbt dashboard` needed `--api-directory`, naming a directory the `.rbtrc` already names for `rbt generate`. Two places to say the same thing is two places to change it, and nothing tells you when only one of them moves -- the dashboard just watches a directory the rest of the tooling has stopped using. So it reads what `rbt generate` was told instead, through a new `ArgumentParser.dot_rc_arguments`, which returns what the `.rbtrc` gives any subcommand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
Work that waits on nothing -- parsing, hashing, encoding -- never gives the event loop a chance of its own, so a servicer doing it over a collection holds its process for as long as the whole collection takes, and everything else it serves waits that long. `concurrently` is the wrong tool, because there is nothing to overlap. Measured over twelve parses of a 45KB file, it left the loop unable to answer for 24ms at a stretch -- 15ms even limited to one at a time, since its tasks are scheduled together and the loop drains several before looking at anything else -- and cost 30% more wall-clock in task machinery. An `asyncio.sleep(0)` in the loop measures best, at 6ms, but invites the question of why it is there and not somewhere else. This answers it: the yield falls out of how the work was grouped, which is a decision the caller has to make anyway, and the collection bounds it the way `concurrently`'s does. It takes elements rather than awaitables, because nothing is being run: the work stays in the caller's body, where it can go on mutating whatever it likes. Note an `async for` alone will not do -- `await` on something that resolves without suspending never reaches the event loop at all, which is why the yield has to live in here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
benh
force-pushed
the
caller-static-analysis-2
branch
6 times, most recently
from
August 17, 2026 05:40
f63d480 to
49dde08
Compare
The API files say which state types exist. They say nothing about
which file implements one, and the name does not say either --
`servicers.py` may implement several state types while being named
after none of them. What does say is the application:
Application(servicers=[AccountServicer, BankServicer, ...])
so this reads the entry point, resolves each registered servicer back
to the file defining it, and asks that class what it services.
Read rather than imported. Importing an application means having its
generated code, its dependencies and its `sys.path`, and the dashboard
is meant to work before any of that exists -- the same reason the API
files are read the way they are.
Driven by the API rather than by the filesystem: the API is what says
which state types there are to look for, so a state type appearing or
disappearing is what sets this going. `until_changes` suspends the
workflow in between, so it wakes when the declarations move rather
than on a timer.
Recorded one state per state type, so that working out one state
type's implementation neither waits on nor overwrites another's. A
state type the application registers no servicer for is recorded as
such rather than left looking unanalyzed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
benh
force-pushed
the
caller-static-analysis-2
branch
2 times, most recently
from
August 17, 2026 06:21
8a18a13 to
d6cacd4
Compare
benh
commented
Aug 17, 2026
benh
force-pushed
the
caller-static-analysis-2
branch
from
August 17, 2026 06:32
d6cacd4 to
514cf11
Compare
rileysdev
force-pushed
the
riley/reboot-inspect
branch
from
August 18, 2026 22:25
5f669a8 to
0a7c6b1
Compare
benh
force-pushed
the
caller-static-analysis-2
branch
6 times, most recently
from
August 19, 2026 05:34
57d31a4 to
6c66384
Compare
Scaffolding for recognizing a state type however it is reached, with no new spelling recognized yet. What a file's imports bound each of its names to becomes `Imports`, whose `try_resolve_module` answers which module a dotted name refers into and what it refers to there; the modules those imports may have Python load are its `may_load`, which is what following the imports follows. Resolution asks about a dotted name (`_path` turns `rbt.Shop` into one) rather than a bare one, and a servicer's base is matched by shape -- `X.Servicer`, or `X.singleton.Servicer` -- with `X` any dotted name. Only the one spelling that already resolved does: a name imported from a `_rbt` module directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A name imported from a module that is not generated is followed into that module's own file, and on, until the chain ends in a `_rbt` module -- so a state type re-exported through another of the developer's files resolves. A circular import terminates: a file already being followed is not followed again. Following made a file's answer depend on other files' contents, so an iteration now carries a `Files` value through everything it does: `known` is what the previous iteration analyzed, `parsed` what this one has parsed and not yet analyzed, `analyzed` what it has finished, and `pending` the frontier: every file reached, parsed or kept, whose imports are not yet followed -- entering once and leaving once, handled. Each `File` records the files it depends on, by the digest each had when it was read -- recorded even when resolution answered "no", since a "no" depends on them just the same -- and a known file is kept only while its own digest and every dependency's still match; otherwise it is parsed and analyzed again. A file read mid-chain is parsed once into `pending`, and being parsed this iteration it can never serve a stale answer. Analyzing is one thing, not a "processing" step and an "analysis" step: `_analyze_file` carries one `Analysis` -- immutable, like `Files` -- through every class and method in a file, resolving what each class services, analyzing each method with `_analyze_method`, and recording each method under its servicer with the method reset for the next. The dependencies the analysis accumulates while resolving names are the file's, harvested when the file finishes into `Files`. Resolution reads other files through the `Analysis` too; a chain of imports is followed by asking each followed file's own imports, so whose imports a name resolves against is always the file that bound it. A chain is never followed into generated code by reading it: the `_rbt` module name alone says what a name from one is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A top-level `Alias = Name` binds the alias to whatever the name was imported as, in written order, so an alias of an alias resolves too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`import shop.v1.shop_rbt as rbt` binds `rbt` to the module, and `rbt.Shop` reaches the state type through it -- as does `import a.b`, which binds `a`, a component at a time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`from shop.v1 import shop_rbt` imports a module as easily as a name, so a dotted name whose head was imported that way is tried as a module of where it came from: `shop_rbt.Shop` is `shop.v1.shop_rbt`'s `Shop`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A relative import is resolved to a path where it is collected, since that is the one place the importing file's own directory is known; from there it is followed like any other module, spelled as a path, with `os.sep` telling the two spellings apart. A name from a generated module reached relatively is spelled back as dotted from the root that holds it, so the same state type is named whichever way it was imported. An iteration follows them to their files too, so a servicer reachable only through one is found -- with files deduped by their absolute path, since a file can now be reached under two spellings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A name bound by no import may be any star-imported module's. The last star import wins in Python, so they are searched last to first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
The std library offers its state types this way: `from reboot.std.collections.v1.sorted_map import SortedMap` is a plain module re-exporting from generated code. A name now resolves through the directories on `sys.path` as well -- the developer's own, since the dashboard runs in their environment. Installed files are read for what their names refer to and nothing else: never analyzed, never watched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`_read` and `_try_find_file_of` make OS calls, and they were made with the event loop held, so a slow disk stalled every dashboard request for as long as the disk took. Both now go through `aiofiles`, which runs the call in a thread -- the way file operations are done everywhere else in the repo -- and everything between `files()` and the two of them becomes `async` to carry the `await` down. Parsing still holds the interpreter: `ast.parse` is CPU-bound, so no thread frees the loop from it, and `cooperatively` already bounds it to a file at a time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A save landing while an iteration reads produces a torn snapshot: one file read before the save, another after. The watch is armed before anything is read, so the save's event is already waiting when the iteration finishes and the next one begins at once -- where a file kept against a stale dependency digest fails its check and is analyzed again. The digests recorded per dependency are what make the tear detectable. Installed packages are the exception: they are read but not watched, so a `pip install` made while the dashboard runs is only noticed on the next save under the roots. Watching the packages too may be worth doing someday. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
benh
force-pushed
the
caller-static-analysis-2
branch
from
August 19, 2026 06:11
6c66384 to
473bad0
Compare
A call on anything that evaluates to a reference is a Reboot call. The reference says which state type is called, the attribute names the method, and the context handed as the first argument is what makes it a call, so a call without the context is unsupported rather than recorded. A call is a `ServicerInfo.Method.Call`, recorded on the analysis in the order met and folded into the method's entry under its servicer. A statement that is just an expression is now evaluated too, since that is how most calls are written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`idempotently`, `per_workflow`, `per_iteration`, `always`,
`reactively` and `until` each return something the method can still
be called on, so a chain passes through them and what comes after
one of them names the method. Without this,
`ref.idempotently('x').deposit(context)` would record a call to a
method named `idempotently`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`.schedule(when=...)` runs the method later and `.spawn()` runs it as a task, so a chain remembers meeting one of them and the call at its end is recorded as reached that way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A method called on the state type itself, handed the context, is a constructor: it makes the state and returns a reference to it with the response. The call is recorded as such, and unpacking the pair, as in `shop, _ = await Shop.open(context, 'a')`, binds the first name to a reference to the state made, so calls through it resolve. A state type reached some other way is still unsupported, since whatever the code does with it is not followed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`Account.forall(ids)` names many existing states the way `ref` names one, so what it evaluates to is a reference and a call through it is recorded like any other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
`.read(context)` and `.write(context, ...)` on a reference reach the state directly and name no method, so each is recorded as what it is, with an empty method name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
benh
force-pushed
the
caller-static-analysis-2
branch
from
August 19, 2026 07:26
c0e5eb4 to
09aaad9
Compare
The method's proto entry gains `unsupported`, so whoever reads the calls can be told they may be incomplete instead of being left to trust them. With that, everything a method's analysis finds now has a home under its servicer, and the calls are proven to reach it all the way through `analyze`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A call that hands the context to a function defined in the same file is followed into it. The helper's body is analyzed with the receiving parameter bound to the context and with its names kept apart from the caller's, so the calls the helper makes are recorded for the calling method. A helper is followed at most once for each method, which is what stops a helper that reaches itself and keeps its calls from being recorded more than once. A helper the file does not define, or defines more than once, is still unsupported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
A helper in the same file is now followed even when it is not handed the context, since what it returns may be a reference or the context, and a name assigned the result is bound to it. A helper returning a tuple is unpacked element by element, the way a constructor's pair already was, and returns that disagree bind nothing. A tuple expression itself now evaluates the same way, so a reference in a tuple that nothing unpacks is still unsupported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
benh
force-pushed
the
caller-static-analysis-2
branch
from
August 19, 2026 07:41
2b42e72 to
3bf7daa
Compare
A call that hands the context first, on a receiver the analysis cannot identify, is recorded as ambiguous. The method name is kept and the state type is left empty, so the dashboard can say that a method named `restock` is called on something, rather than saying nothing at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPEVMhyDRxZEuH8eiykv99
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.