Repository navigation
0.0.5
Added
-
A tenth ring buffer,
FramePerfRecord, for per-frame performance data. Carries the fieldsFrameTimingexposes (buildMicros,rasterMicros,vsyncOverheadMicros,totalSpanMicros) plus a per-frame block-attribution map.TelescopeStore.recordFramePerf/.recentFramePerf/.onFramePerfRecordfollow the shape of the existing nine buffers, with one deliberate difference: the buffer reads its ownsetFramePerfCapacity(default 3600, about a minute at 60fps) instead of the sharedsetCapacity, so a useful frame window does not simultaneously inflate the HTTP, log, exception, dump, model, cache, event, gate and query buffers.clearFramePerf()is a new public, non-test-only method that empties only this buffer, for a production caller that needs to zero it at the start of a measurement session without touching the other nine. (lib/src/records/frame_perf_record.dart,lib/src/telescope_store.dart,lib/telescope.dart) -
FramePerfWatcher, which fills that buffer by joining the two sources the engine reports separately. Frame magnitude comes fromSchedulerBinding.addTimingsCallback, which carriesFrameTiming.frameNumberbut arrives late and batched (roughly 100ms later on web). Per-frame attribution comes from drainingFlutterTimelineat the end of every frame, which knows what ran but not which frame number it was. So the drain parks its block map in a bounded pending map and the timings callback is the sole emission point, writing one complete record per timing. A timing with no block map still emits, because frame magnitude without attribution is still worth having; a block map whose timing never arrives is dropped by the bound.Two things about the drain are easy to get wrong and are pinned in the source. It re-registers itself as its last act, because
addPostFrameCallbackis one-shot and a drain that does not would run exactly once, freezing the liveness counter at 1 and making every later session unreportable. And the collect runs in a MICROTASK scheduled by the post-frame callback rather than inline in it:SchedulerBinding.handleDrawFramewraps the whole post-frame phase in its ownPOST_FRAMEspan, so an inline collect runs with a non-empty nesting stack and tripsassert(_stackPointer == 0)inFlutterTimeline. That assert is stripped outside debug, where the same call would instead write a stale start time into the freshly swapped buffer and report an enormous duration for a block that never ran.The watcher also exposes a static, monotonic
livenessCounterincremented once per frame drawn. It is the only reliable proof the engine is rendering:SchedulerBinding.framesEnabledwas measured reportingtrue, with aresumedlifecycle and an armedonReportTimings, on a Chrome page that had produced one frame in two seconds. The watcher is opt-in and is NOT auto-installed byTelescopePlugin.install(); register it withTelescopePlugin.registerWatcher(FramePerfWatcher()). (lib/src/watchers/frame_perf_watcher.dart,lib/telescope.dart) -
ext.telescope.frames,telescope:framesandtelescope_frames, the wire surface for the new frame-perf buffer. The extension returns the buffer's records alongsideFramePerfWatcher.livenessCounter, so a caller reading an empty result can tell a quiet app from a stalled engine without a second round trip. The naming follows telescope's plural-noun convention (requests,exceptions,queries, ...); the wire saysframes, notframe_perf, even though the Dart types keep that prefix to name the record and the watcher.TelescopeArtisanProvidernow ships 7 CLI commands and 10 MCP tools. (lib/src/extensions/register_telescope_extensions.dart,lib/src/commands/telescope_frames_command.dart,lib/src/telescope_artisan_provider.dart,test/src/commands/telescope_frames_command_test.dart)
Changed
- The registry dispatch fires on a published release now, not on every push that touches the skill. Under the push trigger
fluttersdk/aiclimbed to v1.3.75, and most of those releases re-published identical skill content: a docs commit and a release commit each cost the registry a version. The registry version now tracks published telescope releases instead of counting commits.workflow_dispatchstays as the manual escape hatch when a skill fix has to reach users before the next release. (.github/workflows/dispatch-to-registry.yml)
Fixed
-
The registry dispatch could never fire, so a release would have shipped a skill the registry never received.
dispatch-to-registry.ymldeclaredrelease: [published], butpublish.ymlcreates the release withsoftprops/action-gh-releaseunder the defaultGITHUB_TOKEN, and GitHub does not start workflow runs from events raised by that token. The run history confirms it: noreleaseevent has ever appeared there, only the manual run and the retiredpushtrigger. It has also had no chance to be noticed, since the switch to that trigger landed after the last release (0.0.4, 2026-06-17). The failure mode is silent, becausepublish.ymlgoes green either way and the only symptom is a user installing a skill one release behind.publish.ymlnow calls the workflow directly withneeds: github-release, which removes the cross-workflow event and sequences the dispatch after pub.dev has accepted the release rather than alongside it;workflow_callreplaces the dead trigger andworkflow_dispatchstays as the manual escape hatch. Secrets are passed by name rather than inherited, since the called workflow needs exactly two. Second bug in the same file, fixed alongside: the version extractor tested the ref against^v[0-9]+\.[0-9]+\.[0-9]+, but this repo tags without thevprefix, so that branch never matched a real tag and always fell through to readingpubspec.yaml. (.github/workflows/dispatch-to-registry.yml,.github/workflows/publish.yml) -
doc/mcp/tool-reference.mdstill promised newest-first records and a 200-entry cap. 0.0.3 corrected the MCP tool descriptions to the real wire shape and retired the newest-first shorthand, but this page kept it in all eightlimitrows, plus a "typically 200-500 depending on buffer type" capacity note.limitreturns the most recent N records in chronological order (_trimtakeslist.sublist(list.length - limit), so the oldest of that window is first and the newest is last), and every buffer caps at 500. A client reading this page and not reversing its iteration displayed the window backwards. (doc/mcp/tool-reference.md) -
telescope_tail's MCP schema told agents the log buffer holds 200 entries; it holds 500. Thelimitparameter description read "enforced by the ring-buffer size, typically 200" whileTelescopeStore._capis 500 and every other tool descriptor (telescope_requests,telescope_exceptions,telescope_events,telescope_gates,telescope_queries) correctly said 500. An agent budgeting a tail window against the documented number would under-read by 300 entries and conclude records had been evicted when they were still in the buffer. (lib/src/telescope_artisan_provider.dart) -
CI could not have passed on its next run, and
publish.ymlwould have blocked the release with it. The Flutter tool ships ananalysis_options.yamlmigrator that appends ananalyzer.excludeblock forbuild/plus the six platform runner directories, and it runs on everyflutter pub get. That is the first step of bothci.ymlandpublish.yml, so by the timedart pub publish --dry-runran later in the same job the checkout was dirty:1 checked-in file is modified in git, exit 65. Reproduced on a clean checkout ofmasterwith the exit code read directly rather than through a pipe. Nothing had reported it because no CI run here postdates the migrator. Both files now carry the block the migrator wants, which makes it a no-op; the hand-written comment above it survives apub get, since the migrator reads the parsed YAML, finds the excludes present and skips the file. Reverting the file inside the workflow was rejected as the alternative: it hides the drift and leaves every contributor's tree dirty after apub get. The same fix landed influttersdk_wind(#178) andfluttersdk_wind_diagnostics_contracts(#1). (analysis_options.yaml,example/analysis_options.yaml)