Current-direction reconciliation, 1 October 2026. This is the September tooling design, not the latest DevTools status: live native device inspection/vitals now exist. Operations & Reliability owns the current map. Profiles are attachment facts with independent acquisition, freshness, attribution, coverage/loss and perturbation dimensions; a single ladder rung does not prove all capabilities. See Graph evidence and Profiling. Shadow capture reuses graph hooks, authority and receipts; it does not add another probe/ledger system.
Reaktor Tooling Layer
The DevTools design named three structural gaps: DevTools inspects only the composition it renders itself, nothing is captured, and the inspector is read-only everywhere. All three are one missing layer.
reaktor-tooling is the layer that turns a machine you can reach into a target you can
attach to. Platform tools do the reaching. rgp/1 does
the attaching. Everything the platform tools can do beyond reaching is served through the
same target as lower-fidelity facts, so no pane ever branches on how a fact arrived.
Status: implementation design, reviewed against local source on 13 September 2026. Section 1 records existing code. Sections 3 onward specify proposed behaviour. Section 12 records three decisions taken with Shibasis on that date.
:::tip Where to start Section 5 is the load-bearing idea — the fidelity ladder, and the rule that a rung is a property of a fact, not of a target. Section 15 is the build order. Everything else supports those two. :::
1. What exists today
reaktor-tooling is 7,271 lines and is already the right home. Read from source:
| File | What it actually is |
|---|---|
DeviceTools.kt | A fixed-argv command grammar for adb / xcrun simctl / idb. 12 DeviceActions, 6 of them writes. Validates device identity, bundle identifiers, sandbox-relative paths and artifact types before producing argv. Returns a described DeviceCommand, never executes one. |
DeviceInspection.kt | Parses uiautomator dump XML and idb ui describe-all JSON into one DeviceViewSnapshot — a tree with bounds, hit-testing, search and collapse. Bounded at 2 MiB, 8,192 elements, depth 128. Resolves no XML entities and fetches nothing. |
SupervisedProcessExecutor.kt | 698 lines. Real ProcessBuilder execution producing Flow<RunEvent>, terminating the full descendant tree on cancellation or timeout. |
ToolingPorts.kt | WorkspaceDiscoveryPort, CatalogProvider, TaskPlanningPort.plan, TaskExecutionPort.start/cancel/events, RunEventSink, RunLedger. |
ToolingModels.kt | SafetyClass — 11 members, 7 requiring approval, from ReadOnly through DeviceWrite to Destructive and UnknownRemoteEffect. |
database/, infra/, ide/ | Memgraph inspection, query receipts, migration ledger, worker operations, LSP4J. |
Three facts from adjacent modules matter as much:
- The in-process tap already has hook points.
addPortEventListener/PortEventListenerlive inreaktor-graph-port/PortCapability.kt.GraphTelemetry(in the 261-linereaktor-telemetry) already attaches to them, plus thelifecycleandbackStack.topStateFlows, and its own documentation calls this non-invasive — zero changes to Graph.kt, Node.kt, or any existing framework code. The tap is a second sink on a mechanism that works today, not new instrumentation. - The Switchboard is a sketch.
reaktor-ffiis 311 lines across 12 files;switch/Switch.ktis 43 lines andtransport/Transport.ktis 100. See §13. reaktor-ioships a usable carrier now —network/websocket/WebSocket.kt,Sender.kt,Listener.ktandPartySocket.kt.
So the device path is not a greenfield. What is missing is not probes. It is a model that holds facts from several probes at several fidelities under one identity.
2. Why this is not a plugin set
Flipper's architecture was the right shape — desktop, device SDK, device picker, app picker, a channel per plugin — and it still lost to React Native DevTools, which adopted a standard wire protocol instead. The reason is worth stating plainly, because it is the trap this layer has to avoid.
A plugin per tool means an identity per tool. Flipper's layout plugin, its network plugin and its database plugin each had their own notion of "the app you are looking at". Nothing could be asked across two of them. Android Studio has the same seam today: Layout Inspector and logcat are separate windows with separate lifecycles.
The alternative is not fewer tools. It is one identity and a shared vocabulary of facts, with every tool a producer into it. That is what the rest of this document specifies.
3. Three nouns
Probe — a way to reach a runtime. Owns executable resolution, argv grammar, port
forwarding and transport. adb, simctl, idb, CDP, WebSocket, loopback, a proxy, a
debugger. DeviceTools is a probe family today.
Adapter — turns probe output into typed facts in one vocabulary.
DeviceInspection.viewTree is an adapter today.
Attachment — one live relationship to one running activation, carrying facts from several probes at several fidelities under one identity.
Probes and adapters exist. Attachment is the missing layer and it is the whole design.
4. An attachment is an rgp target
Do not build a device model beside the protocol model. They are the same model.
rgp/1 §3 already specifies the identity this layer needs, and its fields map onto device reality with no adaptation:
| rgp | Android | Apple | Worker | Desktop |
|---|---|---|---|---|
ActivationRef.host | device serial | UDID | colo | hostname |
ActivationRef.session | pid | pid | isolate id | pid |
ActivationRef.epoch | launch count | launch count | deploy generation | process start |
DefinitionId | application id | bundle id | worker name | app name |
RevisionDigest | derived from HELLO, unknown below L5 | " | " | " |
TargetId = definition@revision#activation addresses a device-hosted app exactly as it
addresses a Worker. One connection multiplexes every target with a sessionId per attachment —
CDP's flattened session mode, for the reason rgp already gives: a client that opens one socket
per target cannot correlate anything across them.
The consequence worth stating: a device with an uninstrumented release build is a legitimate
rgp target. It reports a small capability set, a RevisionDigest of unknown, and facts at
low rungs. It is not a second-class object in a different subsystem.
5. The fidelity ladder
The load-bearing idea. Six rungs, each strictly more than the last:
| Rung | Means | Reached by | Today |
|---|---|---|---|
| L0 Discovered | the runtime exists | adb devices -l, simctl list, idb list-targets | ✅ |
| L1 Enumerated | we know what is installed on it | pm list packages -3, listapps, list-apps | ✅ |
| L2 Sampled | one-shot pull of app state | uiautomator dump, idb ui describe-all, logcat -d, a heap dump | ✅ |
| L3 Streamed | continuous, without app cooperation | logcat follow, log stream, wrangler tail, proxy flows | ❌ |
| L4 Attributed | streamed with app-side causal identity | the in-process tap | ❌ |
| L5 Interactive | the target answers questions and accepts commands | rgp Graph / Wire / Value / Edit | ❌ |
The ladder summarises; the contract is a set of independent dimensions
Revised 13 September 2026 after review. The ladder above is a useful summary and a poor contract. Its rungs are ordinal, and the properties they bundle are independent:
- A read-only interactive target answers requests and supports neither
EditnorValue. - An attributed snapshot is one-shot in time and fully attributed in identity.
- An indefinitely streamed log is continuous and unattributed.
- A start/stop command for a sampled profile makes the samples neither complete nor more causally informative.
Treating the rung as an ordinal would advertise operations a target does not support and invite callers to read evidence quality off a number. So the fact contract carries seven independent dimensions:
| Dimension | Answers |
|---|---|
| Acquisition | pulled once, streamed, or answered on request |
| Freshness | observed when, and still valid for how long |
| Attribution | anonymous, or carrying graph identity and a causal id |
| Coverage and loss | what fraction was captured, and how much was dropped |
| Perturbation | was the target altered to obtain this (see §10) |
| Supported operation | what this probe or adapter can actually do |
| Effective grant | what this session is authorised to do, which further restricts the above |
TargetCapability is derived from the probe or adapter contract, then restricted by target
support and session authority. It is never inferred from a rung. The existing Fact and
BindingEvidence provenance semantics carry forward unchanged.
The ladder stays in the documentation because it communicates the shape quickly. It does not appear in the type system.
Acceptance: a read-only interactive target, an attributed one-shot snapshot, and a writable
target whose Values grant is denied must each render correctly and distinctly.
6. Reach and fidelity are independent
The same Android device, at the same moment:
┌─ adb ──────────────► L0 L1 L2 L3 any build, including a release APK
│ you did not instrument
device ── Attachment ──┤
│
└─ rgp over ───────────► L4 L5 when the app links the agent
adb forward
Both merged onto one Attachment, one element tree, one selection. This is the capability neither Android Studio nor Flipper has, and it is the direct answer to the DevTools audit's gap G1 — a selection that travels — extended past the process boundary.
The practical payoff: the same pane works on a colleague's release build (L2 view tree, L3 logs, no attribution) and on your debug build (L5 graph, L4 traffic), with the difference visible rather than hidden.
7. Transports per platform
Concrete, because the plumbing is where this design either works or does not.
| Runtime | L0–L3 reach | L4–L5 reach |
|---|---|---|
| Android device/emulator | adb | adb forward tcp:<port> localabstract:reaktor-rgp → WebSocket |
| Apple simulator | xcrun simctl, idb | loopback — the simulator shares the host network |
| Apple device | idb (usbmuxd) | idb's port forwarding → WebSocket |
| Browser | CDP | page-side cell over WebSocket |
| Cloudflare Worker | wrangler tail, deployments API | WebSocket to a Durable Object, which owns the coordination room. Isolates serve multiple concurrent requests, and a switch in a stateless handler still dies before anyone attaches |
| JVM server | process/HTTP probes | direct WebSocket |
| Desktop | in-process | loopback WebSocket (§12, decision 2) |
adb forward and idb's forwarding are the entire story for getting a socket onto a device.
No custom USB transport is required, and none should be written.
8. Interceptors
Two kinds, and they are not interchangeable.
Out-of-process — an HTTP proxy, adb reverse, a CDP proxy sitting between the tool and
Hermes. It sees bytes and no meaning: it cannot say which port issued a call, which actor was
waiting, or what the call was part of.
Its reach is narrower than it first appears (review). Installing a user CA does not make a modern Android app trust it — the app's network security configuration decides that — and certificate pinning defeats interception regardless. Proxy settings can also be bypassed by an app that does not honour them. So the capability is a platform and build matrix, published as one, and a flow whose TLS cannot be terminated is reported as metadata-only rather than described as captured.
In-process — a tap on addPortEventListener and the graph's lifecycle/backStack flows.
Sees meaning: port, actor, causal id, declared contract. Requires an instrumented build.
The correlation header is the join
The in-process tap stamps every outgoing request with a correlation id. The proxy observes the stamp on the wire. A raw HTTP flow is thereby attributed to a graph port, an actor and a causal chain.
When the stamp is absent — a third-party SDK, a platform request, a release build with no agent — the flow stays unattributed and says so. It is not guessed at by URL matching, because equal URLs no more prove identity than equal operation names prove two resources are the same service.
This is Chrome's initiator chain, working across process and language boundaries. Nothing in the comparison set can do it, because none of them have a typed whole-system graph to attribute to. It is the single most differentiated thing in this layer.
9. The network proxy
In scope for v1 (§12, decision 3). It is the only way to capture traffic from an uninstrumented build, and it is genuinely invasive, so its rules are part of the design rather than an operational footnote.
What it requires: a local mitm proxy on the desktop, the device pointed at it, and a CA
certificate trusted on the device. On Android that is
adb shell settings put global http_proxy <host>:<port> plus a user CA install; on a
simulator it is the host keychain plus the simulator's trust store.
Rules, enforced in types rather than in prose:
- Both steps are
SafetyClass.DeviceWriteand go throughprepare→ approval →executelike any other write (§14). - Never on a production or store build target, and never on a device whose
Channelisstable— refused atprepare, not warned about at execute. - A recovery contract lands before the probe does. Persist the original proxy and trust state plus the exact resources this attachment owns, before mutating anything. Cleanup preserves a pre-existing proxy entry or CA that the attachment did not create, handles the case where another probe shares the device, and records recovery pending when the device is unreachable. Unconditional rollback after a client crash or a dropped connection cannot be guaranteed, so the pending state is part of the design rather than an error path. The revert is a planned operation with a receipt. A tool that leaves a device silently proxied is a security incident with a progress bar.
- Captured bodies are
Value-class data and are gated by theValuesgrant (rgp §11), not by the capture capability. Descriptors are not payloads.
10. Debuggers
In scope for v1 (decision 3), under a capability of their own.
lldb, JDWP and the CDP debugger domain are the deepest reach available and the most dangerous thing in this layer, because they violate rgp's first cost rule (§13): a tool that perturbs the timing of the thing it inspects cannot be used to debug timing. A debugger pause does exactly that.
The design consequence is a seventh TargetCapability:
Perturbing, // this attachment can stop the world; facts taken while stopped are stamped
and a rule: any fact produced while the target was suspended carries a perturbed stamp,
and no timing measurement may be derived from a perturbed interval. Stack frames, variable
reads and thread state are legitimate under it. A latency number is not.
Scope for v1: attach/detach, thread and frame enumeration, bounded variable reads under the
Values grant, and breakpoints. Stepping and expression evaluation are per-language surfaces
and follow later. JDWP for Android and JVM first — it is one protocol across both and covers
the majority consumer; lldb after.
The relationship to L5 is worth being clear about: for an instrumented build, the in-process tap answers most debugger questions without stopping the world, and should be preferred. The debugger earns its place exactly where the tap cannot reach — a release build, a crash before the graph came up, native frames.
11. Profilers are probes
Every profiler worth having is reached by a process this layer already knows how to spawn:
adb shell simpleperf record, xctrace, async-profiler's agent, wrangler tail. They are
probes like adb is a probe, and a profile is a fact on an
attachment carrying its rung.
| Capture | Rung | Reached by |
|---|---|---|
| One-shot sampled profile | L2 | simpleperf record, xctrace record, a JFR dump |
| Continuous profile | L3 | async-profiler attached, a sampling loop |
| Samples attributed to graph nodes | L4 | the in-process tap supplies node.id per frame |
| Profile started and stopped on request | L5 | rgp command against a live target |
Two consequences follow, and both are the reason profiling belongs here rather than beside here.
A perturbed profile must say so. A capture taken while a
debugger held the target, or on a build whose instrumentation changed inlining,
is not comparable with one that was not. The ladder is where that is recorded instead of
assumed away, and Perturbing (§10) is what stamps it.
A profile is addressed like everything else. It belongs to a TargetId, so two
activations of one definition — the Pixel and the simulator, this revision and the last —
produce comparable captures without a naming convention, and a regression is attributable to
a revision rather than to a filename.
The framework already owns a working five-platform capture harness; it is currently filed inside one module. Performance and Observability §26 records that finding and the promotion it implies.
Trace format
Emit Perfetto protos for captures. Keep ReaktorFlamegraphFrame as the normalised summary.
Android's system trace is Perfetto, Chrome's performance traces export to it, and the
DevTools design already asks for SQL over a trace artifact. Emitting
the format everyone converges on makes a Reaktor capture readable in ui.perfetto.dev by
someone who has never seen this workbench, and it is why Wire and profile timelines can
share one viewer rather than two.
The summary and the capture are two artifacts doing two jobs: the small diffable one gates CI, the full one is what you open when the gate fails. Do not collapse them.
12. Decisions
Taken 13 September 2026.
1. WebSocket now, Switchboard later. See §13.
2. The desktop reads the tooling layer through rgp even when everything is local. One code
path for local and remote; a device target and the desktop preview look identical to every
pane; the protocol is dogfooded by its own author before it is exported. The cost is real and
is accepted: the frame codec and the Mesh/Graph read path become a prerequisite for
further DevTools work rather than a parallel track. §15 reflects this.
3. Both invasive capabilities are in scope for v1 — the HTTP proxy with CA install (§9) and debugger attach (§10) — under the constraints each section states.
Open — naming. ToolingTarget already means a thing you can build
(Application | Service | Worker | Library | Store | Tool). rgp Target means a thing that
is running. Two different nouns, one word, about to meet in one module. Recommendation: the
running one is Attachment in this layer and Target on the wire; ToolingTarget is left
alone. Not yet ratified.
13. The carrier
rgp §5 says its transport is the Switchboard: frames on trunks, inheriting routing, credits and cancellation. The Switchboard is 311 lines.
If the tooling layer waits on rgp, and rgp waits on the Switchboard, nothing ships for quarters — and the device layer is the thing blocking every other DevTools milestone.
The same section also says rgp is not bound to that transport, and names the two
properties any carrier must supply: ordered, independently flow-controlled streams, and
framing. rgp's own implementation plan already names WebSocket over a Durable Object as a
transport for Workers. reaktor-io ships WebSocket.send(ByteArray) and PartySocket today,
and adb forward / idb forwarding put a WebSocket on a device in one command.
Decision: WebSocket is the v1 carrier. The Switchboard becomes a second carrier when it is real, behind the same interface. This removes the deepest dependency in the stack and costs nothing the protocol promises. Flow control that the Switchboard would have inherited is implemented once, in the carrier interface, against the two properties §5 names.
14. Writes: one plan, one ledger
The third DevTools gap — the inspector is read-only everywhere — needs no new write path. Both halves already exist and are the same shape.
DeviceTools.command() returns a described DeviceCommand(argv, safety, definitionFiles, timeout) that something else executes. rgp Edit is prepare(patches): Plan then
execute(PlanRef, AuthorityRef): ExecutionReceipt. These are the same contract written twice.
The unification, in three changes:
TaskPlanningPort.plan()andEditDomain.prepare()return the samePlantype. One approval surface, whether the operation ispm clear, aGraphPatch, or a live port rewire.WriteTargetgains a fourth member:Document | Source | Runtime | Device. The existing law holds unchanged — a patch that cannot name its target is refused atprepare.Deviceexists becausepm clear,installand a proxy setting are writes that are not graph patches and must not be forced intoPatchOp.RunLedgeris the receipt store for both. It already takesRunEvents and already has a durable-store port. One audit record for every mutation this tool performs.
SafetyClass and rgp's effect requirements are the same lattice and should be stated once.
PatchOp stays sealed; there is still no setProperty(nodeId, key, value).
15. Build order
Reordered by decision 2 — the protocol moves ahead of the device work, because everything reads through it.
- Attachment, fidelity ladder and fact vocabulary in
commonMain. Types only, no I/O. Small, and everything below is its client. Settles theToolingTargetnaming. - Frame codec + WebSocket carrier +
MeshandGraphread path, loopback only. The desktop composition becomes an rgp target served by this layer and consumed by DevTools over loopback. This is decision 2's cost, paid first. - Move
DeviceTools/DeviceInspectiononto attachments, served through the same rgp server, reachable viaadb forward. Nothing new is reached — but the device view tree becomes joinable to the graph, and the device stops being a separate subsystem. - L3 streaming probes as rgp subscriptions:
logcatfollow,log stream,wrangler tail, over the streamingSupervisedProcessExecutorthat already exists. Closes no log stream, the most-noticed daily gap, on uninstrumented builds. Profile capture lands here too (§11) —simpleperfandxctraceare L2 probes, and the five-platform harness to drive them already exists insidereaktor-flexbuffer, so this step is largely a promotion rather than a build. - The agent and the in-process tap — a small artifact the app links, sitting on
addPortEventListenerand the lifecycle/backStack flows, exactly whereGraphTelemetryalready sits. L4 and L5. - Proxy, CA install, correlation header. This is Traffic, and §8 is what makes it more than a packet log.
- Debugger probes under
Perturbing. JDWP first. - Unify
Plan, addWriteTarget.Device, one ledger. The write path.
Steps 1 and 2 are the commitment. Steps 3 and 4 are weeks and close a real daily gap. Steps 5 and 6 are what make this unlike every other tool in the category.
16. What this does not give you
Stated so no milestone claims it by implication.
- It does not make an uninstrumented app introspectable. L2 and L3 are photographs and text. A release build with no agent will never reach L4.
- It does not prove execution. A route drawn from declared bindings stays declared.
Wireand the correlation header are what establish that a route ran; the ladder exists precisely so this cannot be fudged. - It does not measure timing under a debugger. See §10.
- It does not federate environments. Staging and production are separate meshes with no routing between them, and cross-mesh comparison is still an open rgp decision.
- It does not replace the platform profilers. Instruments and the Android Studio profilers own sampling profiles and heap analysis; this layer's job is to make their artifacts addressable by the same target identity, not to reimplement them.
17. Acceptance gates
Per step, evidence required before the step is called done.
| Gate | Required evidence |
|---|---|
| 1: identity | One attachment survives a reconnect and a process restart with its DefinitionId intact and a new ActivationRef. A late frame from epoch n−1 is rejected, not applied. Two activations of one definition stay distinct. |
| 2: carrier | A slow subscription does not head-of-line-block a command. A dropped event increments droppedSinceCursor and the gap renders as a gap. Attaching does not change the frame timing of the inspected composition, measured. |
| 3: device as target | The same DevTools pane renders a desktop target and an adb-reached device target with no branch on locality. Every device-sourced fact renders its rung. A view tree at L2 cannot render as live. |
| 4: streaming | A log stream survives device sleep/wake and reports the gap. Cancellation terminates the descendant process tree. Overflow drops and says it dropped. |
| 4: profiles | A capture is addressed by TargetId, not by filename, and two activations of one definition produce comparable captures. A perturbed capture is stamped and is excluded from comparison. The full capture opens in ui.perfetto.dev unmodified. |
| 5: attribution | A port invocation in the app appears as an L4 fact with its port, actor and causal id, and the same invocation at L3 without the agent appears unattributed rather than guessed. |
| 6: proxy | Proxy settings and CA trust are reverted on detach, with a receipt. prepare refuses a stable-channel target. A captured body is withheld without the Values grant. |
| 7: debugger | Every fact taken while suspended is stamped perturbed. No latency figure is derivable from a perturbed interval. Detach always resumes the target, including after a client crash. |
| 8: writes | One approval surface for a device command and a graph patch. A patch with no WriteTarget is refused at prepare. Both land in one ledger. Reconnect and retry do not duplicate an effect. |
18. Review corrections and adopted libraries
What changed after the 13 September review
- The fidelity ladder is a summary, and seven independent dimensions are the contract (§5). Capability derives from the probe contract, restricted by target support and session authority.
- Out-of-process interception has a platform and build matrix (§8). A user CA does not confer trust on a modern Android app, pinning defeats interception, and TLS-unterminable flows are reported as metadata-only.
- A recovery contract precedes the invasive probes (§9), including the recovery pending state for an unreachable device.
- Workers: isolates serve multiple concurrent requests. A Durable Object owning the coordination room does not make every request a persistently introspectable process.
Adopted rather than built
Java libraries cover JVM and Android and nothing on iOS, Native, JS or Workers. See the build-versus-adopt table for the full set. For this layer:
| Item | Adopt |
|---|---|
| A5, A6 — Android discovery, logcat, shell, file transfer, port forwarding | ddmlib (com.android.tools.ddms:ddmlib), what Android Studio itself uses |
| A11 — debugger for JVM and Android | JDI (com.sun.jdi), in the JDK |
| A10 — proxy and CA generation | BrowserUp Proxy or LittleProxy, plus BouncyCastle. Smaller projects; pin and vendor |
| A7 — JVM profile capture | async-profiler's Java API, and JFR from the JDK |
| Apple device control | Nothing exists. simctl stays a CLI; idb exposes gRPC, so a generated client is the only leverage |
SupervisedProcessExecutor stays hand-rolled — its descendant-kill behaviour is better than
the common process libraries offer.
Related docs
| Doc | Relationship |
|---|---|
| DevTools | The consumer. Its milestones B–D are gated on this layer. |
| Graph Protocol | The wire. This layer is its first server and first client. |
| FFI Switchboard | The later carrier (§13). |
| UI/UX audit | Where the missing observation plane was first recorded (P01–P04). |
| Performance and Observability | Profiling, telemetry and benchmarking on top of this layer; §26 is the verified state. |
| Workbench layers | Where a workbench capability belongs. |