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:

FileWhat it actually is
DeviceTools.ktA 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.ktParses 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.kt698 lines. Real ProcessBuilder execution producing Flow<RunEvent>, terminating the full descendant tree on cancellation or timeout.
ToolingPorts.ktWorkspaceDiscoveryPort, CatalogProvider, TaskPlanningPort.plan, TaskExecutionPort.start/cancel/events, RunEventSink, RunLedger.
ToolingModels.ktSafetyClass — 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 / PortEventListener live in reaktor-graph-port/PortCapability.kt. GraphTelemetry (in the 261-line reaktor-telemetry) already attaches to them, plus the lifecycle and backStack.top StateFlows, 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-ffi is 311 lines across 12 files; switch/Switch.kt is 43 lines and transport/Transport.kt is 100. See §13.
  • reaktor-io ships a usable carrier now — network/websocket/WebSocket.kt, Sender.kt, Listener.kt and PartySocket.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:

rgpAndroidAppleWorkerDesktop
ActivationRef.hostdevice serialUDIDcolohostname
ActivationRef.sessionpidpidisolate idpid
ActivationRef.epochlaunch countlaunch countdeploy generationprocess start
DefinitionIdapplication idbundle idworker nameapp name
RevisionDigestderived 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:

RungMeansReached byToday
L0 Discoveredthe runtime existsadb devices -l, simctl list, idb list-targets✅
L1 Enumeratedwe know what is installed on itpm list packages -3, listapps, list-apps✅
L2 Sampledone-shot pull of app stateuiautomator dump, idb ui describe-all, logcat -d, a heap dump✅
L3 Streamedcontinuous, without app cooperationlogcat follow, log stream, wrangler tail, proxy flows❌
L4 Attributedstreamed with app-side causal identitythe in-process tap❌
L5 Interactivethe target answers questions and accepts commandsrgp 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 Edit nor Value.
  • 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:

DimensionAnswers
Acquisitionpulled once, streamed, or answered on request
Freshnessobserved when, and still valid for how long
Attributionanonymous, or carrying graph identity and a causal id
Coverage and losswhat fraction was captured, and how much was dropped
Perturbationwas the target altered to obtain this (see §10)
Supported operationwhat this probe or adapter can actually do
Effective grantwhat 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.

RuntimeL0–L3 reachL4–L5 reach
Android device/emulatoradbadb forward tcp:<port> localabstract:reaktor-rgp → WebSocket
Apple simulatorxcrun simctl, idbloopback — the simulator shares the host network
Apple deviceidb (usbmuxd)idb's port forwarding → WebSocket
BrowserCDPpage-side cell over WebSocket
Cloudflare Workerwrangler tail, deployments APIWebSocket 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 serverprocess/HTTP probesdirect WebSocket
Desktopin-processloopback 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:

  1. Both steps are SafetyClass.DeviceWrite and go through prepare → approval → execute like any other write (§14).
  2. Never on a production or store build target, and never on a device whose Channel is stable — refused at prepare, not warned about at execute.
  3. 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.
  4. Captured bodies are Value-class data and are gated by the Values grant (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.

CaptureRungReached by
One-shot sampled profileL2simpleperf record, xctrace record, a JFR dump
Continuous profileL3async-profiler attached, a sampling loop
Samples attributed to graph nodesL4the in-process tap supplies node.id per frame
Profile started and stopped on requestL5rgp 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:

  1. TaskPlanningPort.plan() and EditDomain.prepare() return the same Plan type. One approval surface, whether the operation is pm clear, a GraphPatch, or a live port rewire.
  2. WriteTarget gains a fourth member: Document | Source | Runtime | Device. The existing law holds unchanged — a patch that cannot name its target is refused at prepare. Device exists because pm clear, install and a proxy setting are writes that are not graph patches and must not be forced into PatchOp.
  3. RunLedger is the receipt store for both. It already takes RunEvents 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.

  1. Attachment, fidelity ladder and fact vocabulary in commonMain. Types only, no I/O. Small, and everything below is its client. Settles the ToolingTarget naming.
  2. Frame codec + WebSocket carrier + Mesh and Graph read 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.
  3. Move DeviceTools / DeviceInspection onto attachments, served through the same rgp server, reachable via adb forward. Nothing new is reached — but the device view tree becomes joinable to the graph, and the device stops being a separate subsystem.
  4. L3 streaming probes as rgp subscriptions: logcat follow, log stream, wrangler tail, over the streaming SupervisedProcessExecutor that already exists. Closes no log stream, the most-noticed daily gap, on uninstrumented builds. Profile capture lands here too (§11) — simpleperf and xctrace are L2 probes, and the five-platform harness to drive them already exists inside reaktor-flexbuffer, so this step is largely a promotion rather than a build.
  5. The agent and the in-process tap — a small artifact the app links, sitting on addPortEventListener and the lifecycle/backStack flows, exactly where GraphTelemetry already sits. L4 and L5.
  6. Proxy, CA install, correlation header. This is Traffic, and §8 is what makes it more than a packet log.
  7. Debugger probes under Perturbing. JDWP first.
  8. Unify Plan, add WriteTarget.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. Wire and 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.

GateRequired evidence
1: identityOne 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: carrierA 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 targetThe 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: streamingA log stream survives device sleep/wake and reports the gap. Cancellation terminates the descendant process tree. Overflow drops and says it dropped.
4: profilesA 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: attributionA 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: proxyProxy 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: debuggerEvery 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: writesOne 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:

ItemAdopt
A5, A6 — Android discovery, logcat, shell, file transfer, port forwardingddmlib (com.android.tools.ddms:ddmlib), what Android Studio itself uses
A11 — debugger for JVM and AndroidJDI (com.sun.jdi), in the JDK
A10 — proxy and CA generationBrowserUp Proxy or LittleProxy, plus BouncyCastle. Smaller projects; pin and vendor
A7 — JVM profile captureasync-profiler's Java API, and JFR from the JDK
Apple device controlNothing 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.

DocRelationship
DevToolsThe consumer. Its milestones B–D are gated on this layer.
Graph ProtocolThe wire. This layer is its first server and first client.
FFI SwitchboardThe later carrier (§13).
UI/UX auditWhere the missing observation plane was first recorded (P01–P04).
Performance and ObservabilityProfiling, telemetry and benchmarking on top of this layer; §26 is the verified state.
Workbench layersWhere a workbench capability belongs.