Releases: itsonu/flutter-lamp
Release list
v0.20.0
The dashboard becomes a runtime observability surface rather than a telemetry
viewer, and one real defect in the tool accounting is fixed. Nine peer tabs
become four destinations; nothing about the collectors, the store or the MCP
tool surface changed.
Fixed - the tool accounting was fabricating a zero
- A tool call whose handler threw was never counted. The cost meter wrapped
registration and recorded after the handler returned, so the SDK's conversion
of a throw into an error result took the call out of the accounting
altogether. Measured against a live app: five tools called with no isolate
attached, four of them erroring, all four missing — the session reported
0 calls, 0 errors. A dashboard rendering that would have shown a confident
zero while nothing worked. There is a mutation-checked regression test: remove
thecatchand it fails.
Fixed - readings that were not what they claimed
- The frame and heap charts printed the drawing ceiling under the word
"max". The plotting area is padded 15% above the data so the series does not
touch the top edge, and that padded number was the caption. A frame chart
whose real peak was 41.0ms captioned47.1; the heap chart captioned a
maximum above the heap capacity it reported four inches higher, which is
physically impossible in a memory tool. The caption now gives the observed
maximum, the last value and the sample count. Pinned by a check that runs the
shipped drawing code against known data. 0was printed where nothing could have been measured. With no runtime
attached,ExceptionsandNetworkshowed a zero rather than naming which
kind of nothing applied.- Readings taken before a disconnect looked current. Frame, heap and network
figures kept their live typography beside aNOT CONNECTEDbadge; the
staleness stamp reached two of six statistics and now reaches all of them. Pauseasserted the stream wasRECEIVINGwhile every reading behind it
was frozen — byte-identical output measured 19.5 seconds apart while the
server processed 32 further tool calls.STREAMandVIEWare now separate
states, because both are true at once.- The dashboard claimed seven collectors were
ACTIVEwith the VM
unreachable. Collector health is last-known and was never invalidated. - "last event never" appeared beside a list of events, because the
last-event time was set on live pushes only and never seeded from the
snapshot.
Fixed - the stream was unusable under load
- The row you were reading moved 2,416px down the page in six seconds.
Entries are prepended newest-first, so every arrival pushed the thing being
read further away, at roughly 400px/s. The stream now anchors on a row rather
than on scroll position: at the top it follows, scrolled away it pins and
reports what arrived (N new entries — jump to latest). Measured after: 0px
drift following, 0.2px pinned. The browser's ownoverflow-anchorhad to be
disabled, because it was fighting the explicit anchor and silently pushing a
following view off the top. - A typo could not be corrected in the search field. The input is rebuilt on
every keystroke and a refocus put the caret at the end of the string; caret
and selection are now preserved exactly. - Over half the visible stream was the dashboard's own heap poller — 52-67%
of the newest 400 rows, with zero frame, log or network rows visible while
hundreds of frames sat captured. Consecutive samples now roll up the way
reconnect churn already did, every original one disclosure away. Errorsnamed three unrelated things at once — MCP tool failures, janky
frames and app exceptions — and a 41.8ms frame rendered in the same red as a
crash. It is gone; the filters now each return exactly what they say.
Added - what the process knows about itself
- Tool-call telemetry reaches the browser. The cost meter gains per-tool
total duration and last-called time plus a window of recent calls, and the
dashboard broadcasts it. All of it was already collected and none of it was
reachable outsideruntime_status. - The connected agent is named.
src/core/mcpClient.tsreads the
clientInfothe client sends atinitialize, so the dashboard can say
"Claude Code 2.1.0" instead of asserting that "an agent" is present. Shown as
self-reported, because that is what it is. - A
telemetrymessage on the dashboard WebSocket carrying server identity,
tool accounting, collector health, unobservable categories and retention.
Diffed before sending, so an idle dashboard stays silent.
Changed - the dashboard
- Four destinations instead of nine. Overview (a session report), Activity
(one stream with source and category filters), Performance (frame analysis),
Diagnostics (topology, health, capability, blind spots). Logs, Network,
Exceptions, Timeline and the MCP call log became filters; three of them had
been rendering the identical empty sentence in a 120px panel. Inspector became
a section of Diagnostics, because it is a capability list. - The header carries three independent states — runtime, MCP, stream —
because a single badge could not express "the app is gone but MCP is serving
an agent", which is a real and common state and is why the old header said
"app not connected" while 239 tool calls were being served. - Findings state the evidence and, where the evidence contains one, the
remedy. The VM Service diagnosis event carries concrete transport advice;
the finding renders it verbatim. Remediation is never synthesised. Findings
are ordered most severe first, from the severity already assigned. - Keyboard operable throughout. 418 interactive elements previously had no
keyboard path. The tab strip is one tab stop with arrow-key movement; the
activity stream is one tab stop with a roving cursor, so several hundred rows
never become several hundred tab stops. Focus survives the re-render. - Tab, filter and search live in the URL, so a filtered view can be reloaded
or shared, and a drill-through is reversed by the browser's own Back button. - Non-text contrast. Every boundary was one token at 1.33:1. Control
boundaries now clear 3:1 (WCAG 1.4.11) while structural edges and row
separators stay quiet, because a card edge is not a component boundary. Export JSONis nowExport events, with its scope stated: this browser's
buffer, not the session. The full export remains theexport_sessiontool.
Added - documentation
PRODUCT.mdrecords the product truth every design pass had been
re-deriving: two audiences of equal weight, the positioning, what is
observable, what is not, and the principles — including "the dashboard must
never imply a fact that the runtime cannot actually observe".docs/Data-Path.mdtraces the topology and proves it rather than
asserting it: an MCP tool call's own event was observed arriving on the
dashboard socket with the same event id.docs/DESIGN.mdreplacesdocs/Design.md, which documented the
seven-tab dashboard, Google Fonts and tokens that no longer exist.
Known limitation, unchanged in this release
The frame budget is a fixed 16.7ms (60fps). On a faster display the jank count
is conservative, and both the finding and the Performance view say so rather
than presenting it as absolute.
v0.19.1
Documentation only — no runtime change. Published because README.md ships
inside the package, so the corrections below were not visible to anyone reading
0.19.0 on npm.
Fixed - README claims that had gone stale
- "Framework errors (
Flutter.Error) are always captured regardless." Not
true, and measured untrue: the widget inspector posts that event only while
structured error reporting is on, which the framework disables in profile mode
and on the web. On those targets a thrown framework error leaves no trace at
all. The same claim had already been corrected in
docs/Implementation-Notes.mdand was left standing in the README. - The retention list named five categories and omitted three (state,
rebuilds, navigation), and gave only the 60fps frame figure. A 25-minute run
measured eviction beginning at 4 minutes under a 10fps workload, and the note
that every diagnosis carries a retention warning once eviction starts was
missing entirely. - Security did not mention the VM Service credential scrub, nor that
FLUTTER_LAMP_REDACT=offdeliberately does not disable it. That distinction is
the point of the fix in 0.19.0: the switch exists so a developer can read their
own app's headers and log text, not so the server will hand out the key to the
app it is attached to. - "Agents should not call all sixteen" — the table lists 22.
- The roadmap listed state-management activity and navigation as next; both
shipped in 0.17.0. - Three tool rows described output that grew in 0.19.0:
runtime_status(session
cost, VM-to-host clock offset),diagnose_runtime(stablecauselabel,
unobservable coverage),get_state_activity(Provider and Bloc, not only
Riverpod).
The README's tool table and the server's tools/list were then diffed rather
than eyeballed: 22 each, with no entry on either side missing from the other.
v0.19.0
A minor release, not a patch: Diagnosis gains fields and two tools gain output.
It also carries a security fix — 0.18.1 and earlier can leak the VM Service
credential into tool output when the observed app logs its own debug-service URI,
which a web target does on startup. That token authorises evaluate, so
upgrade rather than pin.
Added - Phase D, evaluation
The diagnosis engine is now measured against recorded sessions instead of only
against its own unit tests. Every other test here builds a store by hand and
checks what the engine does with it; that measures the code against its author's
expectations, which is not the same as measuring it against an app that actually
misbehaved.
-
eval/incidents/*.json- golden incidents: a real session captured from a
running app withexport_sessioninfullmode, plus what the right answer
is. Eight so far, across three targets — an Android device over adb, a Windows
desktop build, and Chrome — because a target is part of what a diagnosis has to
cope with, not an implementation detail. Two deliberately straddle the jank threshold: 74/381 frames
(19.4%) must stayunknown, 48/240 (20.0%) must be diagnosed asjank. The
pairing is the point - together they pin the boundary, which is the part of a
heuristic that drifts. Two unrelated incidents would not. The other two are a
ranking pair: both contain an exception and jank over threshold, and their
right answers are opposite, so no fixed ordering of the two hypotheses can
satisfy both. The fifth coversnetwork: one endpoint returns 500 four times
while another returns 200 four times, so connectivity demonstrably works and
the finding is specific to an endpoint. The sixth coversmemory: twelve heap
samples rising monotonically from 89MB to 217MB with no drop anywhere, which
is the argument — a healthy app under load produces a sawtooth, and twelve
consecutive rises is not one.externalUsageMBis 0 throughout, so the growth
is Dart heap rather than external allocation accounted elsewhere, and
heapCapacityMBclimbs with it, so the VM was expanding for live data rather
than sitting on uncollected garbage. EveryCauseKindnow has a recording.The seventh is a second negative, and it pairs with the first on a different
axis. One abstains because the jank ratio is too low (19.4% of 381 frames);
the new one abstains despite a ratio of 100% — one janky frame out of one —
because the sample is too small. Dropping the three-frame floor to one turns
a single 90.4ms startup frame into a confident jank verdict, which would be the
most embarrassing false positive available: technically accurate, useless, and
true of every app that has ever started. Neither negative catches the other's
bound.The eighth is the first recorded on a non-native target: the same workload on
Chrome, where two collectors genuinely cannot see their domain.jankis still
the right answer (30 of 52 frames, build-bound at 48.1ms against 5.4ms), and
the diagnosis must also reportexceptionandnetworkas unobservable
rather than merely empty.Expectationgains an optionalunobservablefield
and replay feeds the recorded collector health back in, so that is scored
behaviour rather than a claim in prose. -
Two ranking policies pinned by unit test, both found by mutation rather
than by reading the code. Sustained heap growth must not outrank an evidenced
jank pattern — promotingmemoryabovejankpassed the entire eval suite
unnoticed, because no recording constrains that edge and a recording that
would needs a session where the ground truth is genuinely arguable (if
allocation pressure caused the collection pauses that caused the jank, memory
is the cause and jank the symptom). And heap growth is never described as a
leak: the restraint inGrowth is not proof of a leakis part of the
contract, not a wording accident. -
probe/flaky-server.mjsand anetworkscenario inbloc_probe. Offline
and deterministic — an incident that depends on someone else's server is not
reproducible, and a probe should not make outbound requests to record a
fixture. -
Header redaction is now proven on a recorded artifact, not only in a unit
test. The probe sendsAuthorization: Bearer …on every request; the recorded
incident contains"authorization": "[REDACTED]", listsauthorizationunder
redactedHeaders, and the token string appears nowhere in the file. The
untested claim was never the redaction function — it was that redaction runs
at capture, so the artifact a developer pastes into a chat is clean. -
src/eval/replay.ts- hydrates a recorded session and re-runs the
diagnosers. Nothing new had to be invented to record an incident:
export_sessionwas already versioned, carried every event and diagnosis, and
had its shape pinned by a test. -
src/eval/score.ts- top-1 accuracy, evidence recall, false-confidence
rate, unknown precision, dangling-evidence count. -
A CI gate, asymmetric on purpose. Accuracy has a floor; false confidence
has a ceiling of zero. A tool that says "unknown" when it cannot tell costs a
developer nothing - they go and look themselves, which is what they were doing
anyway. One that says "the network call caused your jank" at 85% confidence
when it did not sends them where the bug is not, and spends the credibility
that makes the correct answers worth reading. -
RuntimeStore.hydrate- loads already-stamped events back in unchanged.
Notadd(), which mints fresh ids from a counter: replaying through it would
renumber everything, so everyexc_00042a diagnosis cited would point
somewhere else and evidence precision would be meaningless.
Verified by mutation rather than assumed, because a gate that only ever passes
is decoration. Loosening the jank threshold to 15% makes the 19.4% session get a
confident jank verdict and trips three tests including false confidence.
Tightening it to 25% makes the 20.0% session abstain and trips two - notably
not false confidence, because abstaining when jank was real is wrong, not
confidently wrong. On the exception path: blinding the exception detector, or
letting jank outrank it, trips false confidence both times; removing only the
stack-trace confidence bonus trips the band and leaves false confidence at zero.
Same asymmetry, from the other side. On the ranking: restoring the unconditional
exception priority trips false confidence at 25%, while dropping jank's strength
below the confidence threshold trips status and band with false confidence still
at 0% - abstention and confident wrongness scored apart, which is the whole
point of the metric.
Verified
-
A 25-minute session, past every ring capacity. Every recorded incident is
under a minute and reportsevicted: 0, so retention had never been exercised
against a real app — and the frame ring holds 1,000 events, roughly 100 seconds
at 10fps. Held a session for 25 minutes, sampling every 60s: frames cap at
1,000 at minute 4, eviction climbs to 6,168 frames and 5,167 state events by
minute 25, and from the moment it starts the diagnosis carries the retention
note. Truncation is not silent. The diagnosis stayed stable across 75 tool
calls with no drift.Two things it does not show, stated because the run looks more conclusive than
it is. The jank ratio held at 31% throughout, but the workload is uniform — so
this shows the mechanism survives eviction, not that the ratio would reflect a
session whose behaviour changed once the denominator wraps. And the probe for
the server's own memory returned nothing, so there is no data on process
growth over the run.
Added - verification
-
probe/sweep-live.mjs— calls every read-only tool against a running app
and exits non-zero if any errored or if the VM Service credential turned up in
a response.verify-release.shchecks the package; nothing checked the
behaviour, and both defects that made 0.18.0 unusable were found by hand
exactly this way. Verified to fail, not assumed to: removing the credential
scrub makes it reportcredential leaks: 1 -> get_logson a web target, and it
is what caught theget_timelinedefect above.Its limits are written down rather than left to be discovered. The credential
check is opportunistic — it can only see a leak the app actually logged, and
only while that line is still in the backlog DDS replays on connect. Reading
collector health offruntime_statusinstead ofruntime_healthmade it
silently print nothing at first, which is the same class of bug it exists to
catch.
Added - coverage
-
coverage.unobservableon every diagnosis: of the empty categories, those
nothing could have seen, because the collector responsible is degraded or
unavailable on this target.emptyis unchanged, so nothing about its meaning
moved — the split is the point. An agent readingempty: [exception, network]
on a web target would conclude the app threw nothing and made no failing
requests; both are unfounded there, and the second is reassurance drawn from a
question that was never asked.Measured rather than reasoned about: on Chrome
networkisunavailable(no
dart:io) andexceptionisdegraded(structured error reporting is off off
native), whilenavigationis empty simply because the app did not navigate.
Three empty categories, two different reasons, and the diagnosis now says
which is which. A category that is blind but still holds events is not counted
as a hole.
Added - cost
- What this server costs the caller is now measured. Every response is input
tokens on the agent's next turn, and that was previously guessed at. Measured
against a live app with ~1,700 events retained:tools/listis 16.7kB
(~4,200 tokens) before any work happens, `diagnose...
v0.18.1
A security and correctness fix over 0.18.0, which shipped before either defect
was found. 0.18.0 should not be used: it leaks the VM Service auth token
into tool output, and it makes the app under observation throw.
Fixed - found by running against a real app
Two defects that 168 passing tests did not catch, found within minutes of
pointing the server at a live Flutter app (device A015, Android 16, Flutter
3.44.1). Both were mock-shaped blind spots: a mock does not care what a
parameter is called, and a unit test does not read a whole tool response.
- The Widget Inspector calls used the wrong parameter name, and the failure
landed inside the app being observed.get_widget_treeand
get_selected_widgetpassedgroupName; Flutter requiresobjectGroup.
getRootWidgetSummaryTreeis registered through
_registerObjectGroupServiceExtension, which doesparameters['objectGroup']!,
andgetSelectedSummaryWidgetthrough_registerServiceExtensionWithArg,
which asserts the key is present. So the tool did not merely fail to read the
tree — it threw an exception inside the app, which then surfaced in
get_exceptionsand anchoredwhat_changed. A diagnosis tool contaminating
its own evidence is worse than one that returns an error.disposeGrouphad
the name right all along, which is why the group lifecycle tests passed. what_changedleaked the VM Service auth token. The 0.18.0 audit fixed
export_sessionand asserted, in this changelog, that redaction at capture
meant credentials never enter the store. That was wrong, and this is where:
ConnectionManager.open()wrote the raw URI into asystemevent's message
anddata.wsUri, and returned it fromconnect_vm. Found live at
$.system[0].message. The path segment of a VM Service URI authorises
evaluate— arbitrary Dart execution in the app. Fixed where the value
enters rather than at each consumer; host and port survive.- Token redaction no longer honours
FLUTTER_LAMP_REDACT=off. That flag is
a choice about observed evidence — headers, log text. Nobody asking to see
their own request headers asked for a remote-code-execution credential in
every export and on every dashboard. Evidence redaction still opts out.
Regression guards for both, each verified to fail when the bug is reintroduced.
They assert at the source boundary, because neither faulty call is reachable
from a unit test: registerTools holds a connection singleton, and
ConnectionManager needs a live VmService.
v0.18.0
Merges the probe-app branch, and corrects a measurement in 0.17.0's favour.
Added
probe/- two self-driving probe apps (riverpod_probeon
flutter_riverpod 3.4.2,bloc_probeon flutter_bloc 9.1.1) and
probe/measure.mjs, the harness. Each app runs a five-phase workload and
prints a phase marker before every phase, so an event on the VM Service can
be attributed to what caused it. Claims about state-management observability
are now re-checkable instead of remembered.export_session(P3) - the session as versioned JSON.fullarchives
everything;briefcarries the diagnoses plus only the events their evidence
cites. One measured example, onprobe/riverpod_probe: 22 events / 19KB
against 417 / 152KB. The ratio depends entirely on how much the session
captured and how much of it the diagnoses cite.get_state_activity- query state-change activity directly rather than
only through findings.
Fixed
-
Bloc: both earlier conclusions were wrong, in opposite directions. The
merged branch concluded stock Bloc was invisible - 143 transitions, zero
events, noext.bloc.*RPC. All of that is true, and the conclusion still did
not follow:flutter_bloc9.1.1 depends transitively onprovider(its own
pubspec.lock), andproviderpostsprovider:provider_changed, so a
flutter_bloc app is not silent.The correction then overstated in the other direction. Measured on the probe:
20 transitions against ~1,220 provider events, becausestormWatchers = 60
widgets each watch the bloc.provider:provider_changedfires once per
notified dependent, not once per transition - so the count measures how many
widgets were notified, Bloc transition counts cannot be recovered from it, and
Bloc itself remains uninstrumented for the VM Service.What is now claimed, and no more: there is no
ext.bloc.*RPC; flutter_bloc
depends on provider; provider emits change notifications; therefore a Bloc app
on that dependency path exposes state-related activity indirectly. Bloc
internals are not directly observable, and an app avoiding provider-backed
lookup would be silent here.probe/EVIDENCE.mdrecords each measurement with
its kind (observed / inferred / documented) and what it does not establish.
Fixed - audit pass
- The session export leaked the VM Service auth token.
export_session
carriedsession.wsUriverbatim, and the path segment of a VM Service URI is
a credential grantingevaluate, i.e. arbitrary Dart execution in the running
app - in an artifact whose stated purpose is to be attached to bug reports.
Redaction now happens insideexportSessionitself rather than at one call
site, so the artifact is safe regardless of caller. Host and port survive. stateFindingreported saturated activity as a discovery. With continuous
state churn - exactly what a rebuild storm produces - every frame falls inside
the 1s window and the ratio reads 100% whether or not jank is related. It now
computes the same ratio for smooth frames as a control and withholds the
finding when the difference is under 15 points. Documented as a correlation
heuristic, with the symmetric window called out: it cannot show the state
change came first.stateFindingwas invoked twice, duplicating itself in every performance
diagnosis - a merge artifact git resolved silently.- The reconnection give-up event was emitted asynchronously, behind an adb
subprocess call added in 0.15.0. The most important record in a session's
timeline was gated on an optional external tool that can block, making the
test flaky (1 run in 3) and the behaviour genuinely wrong. The terminal event
is now recorded synchronously; the transport explanation follows as a separate
enriching event. - Corrected three places that contradicted each other on Bloc after the merge:
performance.tsclaimed Bloc announces on the Extension stream, and
stateActivity.tsclaimed Bloc is invisible.
Changed
diagnose_performance's state finding now measures the fraction of janky
frames falling within 1s of state activity, rather than comparing raw volumes- a falsifiable claim instead of two counts side by side. Scored 0.5, below
every causal finding, because churn and expensive builds both follow the same
tap.
- a falsifiable claim instead of two counts side by side. Scored 0.5, below
- The state collector registers its handler before subscribing, per 0.16.0. The
branch predated that fix and would have reintroduced it.
v0.3.0
Storage release. Fixes a bug that silently destroyed the evidence the project
exists to preserve.
Fixed
- Frame events no longer evict every other kind of evidence. The store was
one shared 5,000-event buffer.FrameCollectorwrites an event per frame, so
at 60fps frames filled the entire buffer in about 83 seconds and then evicted
everything older — the exceptions, network requests and logs you were actually
chasing. Two minutes into a session, the exception from minute one was gone.
Each category now has its own ring buffer, so a noisy stream can only evict
itself. Defaults: 3,000 logs, 1,000 exceptions, 1,000 network, 1,000 frames,
500 system, overridable through theRuntimeStoreconstructor. - Insertion is O(1). The buffer was trimmed with
splice(0, n)on every
insert past capacity, copying the whole array 60 times a second inside the
tool meant to diagnose performance problems. Replaced with a circular buffer:
200,000 events went from 9,943ms to 81ms, a 122x improvement.
Added
runtime_statusnow reportsretention: per-category capacity, how many
events are retained, how many were evicted, and the timestamp of the oldest
event still held. A capped buffer is fine; a silently capped one leaves an
agent reasoning over truncated history without knowing it.CATEGORIESis exported fromcore/events.tsas a runtime value, with the
Categorytype derived from it so the two cannot drift.
Compatibility
No tool renamed, removed, or reshaped. RuntimeStore.query() returns the same
most-recent-first ordering, now merged across the per-category buffers.
runtime_status.retention is additive.
v0.2.0 — security release
Security release. Both issues were live defects in 0.1.0 — upgrade if you ran the dashboard or inspected authenticated requests.
Credentials no longer leave the app. get_network and diagnose_runtime previously returned full headers for failing requests, sending Authorization bearer tokens and Cookie / Set-Cookie values to the AI model and to every browser watching the dashboard. Redaction now runs at capture, so secrets never enter the event store at all.
The dashboard WebSocket no longer accepts cross-origin connections. Binding to 127.0.0.1 does not protect a WebSocket — browsers exempt WebSocket from the same-origin policy, so any page you had open could connect to ws://127.0.0.1:7373/ws and read your entire runtime stream. The handshake now requires a per-process token that only same-origin script can read.
No tool was renamed, removed, or reshaped. Credential header values read [REDACTED], with the withheld names listed in data.redactedHeaders.
Also adds SECURITY.md and docs/Improvement-Plan.md — a full audit of the v0.1.0 implementation with a prioritized backlog.
Full detail in CHANGELOG.md.
v0.1.0 — first public release
First public release of Flutter Lamp — an MCP server that gives your AI live eyes on a running Flutter app through the Dart VM Service.
12 MCP tools: VM connect, logs, exceptions with reconstructed stack traces, network (Dio & package:http), frame timings with jank detection, widget tree, selected widget, memory, VM timeline, evidence-first diagnose_runtime, and a live browser dashboard on http://127.0.0.1:7373.
Full detail in CHANGELOG.md.