PackageTrack
Sign in Get early access

ailog

Structured JSONL logging designed to be read by an AI. Trace correlation, causal chains, error fingerprinting, secret redaction and a digest CLI. Zero dependencies.

0.4.0 FuruyamaMasayuki/ai-readable-logs

What this package is like to depend on

Last release 28 days ago

27 Jul 2026

Too new to tell

only 1 dated releases

Nearly every release is documented

notes for 1 of 1 stable releases

Nothing withdrawn

no release was ever pulled

0 months old

1 releases · first in 2026

1 release in the last 12 months

see the full history below

Release timeline

1 releases · Jul 2026 to Jul 2026
Release Pre-release

Releases

latest 1
  1. 0.4.0 27 Jul 2026
    Release notes

    Changed

    • A release build now logs nothing unless you ask it to. enabled on Logger.create defaults to !isReleaseBuild instead of true, so flutter build / dart compile exe ship silent by default and only a debug or profile build logs out of the box. Opt back in with Logger.create(sink: ..., enabled: true) — a plain argument, so it works the same in every mode — or with a runtime flag (enabled: userOptedIn) for a diagnostics toggle in settings. Logger.forTesting() passes enabled: true itself, so a test compiled AOT still logs.

      The tradeoff is real and worth stating: the failures most worth analyzing are the ones users hit in production, and a silent release cannot describe them. The default is chosen for the case where nobody has decided yet — writing to a user's device and printing to their console are both things to opt into, not out of. When you can retrieve the log, enabled: true with minimumLevel: byBuildMode(debug: LogLevel.trace, release: LogLevel.info) is the better configuration.

      Verified against a real dart compile exe binary: default events=0, enabled: true events=2, forTesting() events=2; the same four cases under JIT give 2 / 2 / 2 with only enabled: false at 0.

    • Span has no end() — the README documented one that never existed. The real methods are succeed() and fail(). tool/documented_api_check.dart now references every documented API and is analyzed in CI, so an example that drifts from the code fails the build.

    • fnv1a64 (returning int) is replaced by fnv1a64Hex (returning the 16-character hex string). A 64-bit value cannot be represented in a web int, so no int-returning form can be correct everywhere this package runs. shortHash also drops its unused seed parameter, whose default was itself a web-breaking literal.

    Added

    • installDebugSync + ailog_sync: the log syncs itself while you debug. Every other way to get a file off a device is something you do — adb pull, Xcode's container download, a share button, a tee pipeline with a grep after it. This is a call at startup and a command on your machine, after which a local .jsonl grows as you use the app:

      final recent = MemorySink(capacity: 20000);
      installDebugSync(recent);
      
      dart run ailog:ailog_sync --vm-service <uri from flutter run> \
        -o app.jsonl --watch
      

      It registers a VM Service extension and the CLI pulls from the socket flutter run already opened, asking each time for everything past the last seq it holds — so it is a pull, not a push: nothing is dropped when the app is busy, attaching late still gets the buffer's whole history, and no prefix-stripping is needed because the events never touch a text stream. Debug and profile only; a release build serves no VM Service, and installDebugSync refuses to register there so the callback and the buffer it closes over fold out of the binary.

      If the buffer rolls over between polls the CLI says how many events were lost, rather than writing a file with a silent hole in it.

      ailog_sync also reads stdin (flutter run | dart run ailog:ailog_sync), which replaces the documented tee+grep recipe with one command, keeps non-JSONL output flowing to your terminal, and rejects JSON that is not ours so an app logging API responses cannot poison the file.

      The VM Service client is ~130 lines on dart:io's WebSocket rather than package:vm_service, so the package stays zero-dependency. Tested against a real app process over a real socket — a fake client would exercise none of the parts that actually break.

    • The digest now reports its own incompleteness. Size-based rotation deletes older files by design, so a digest built from the survivors is a partial view — measured: 100,000 events written, 63,686 reported, nothing saying so. seq makes the loss exactly computable (lowest seq 36315 → 36,314 events missing), and it is now stated directly under the event count, in both Markdown and JSON, with digest.missingEvents for programmatic use. Gaps in the middle (a file not supplied) are counted too.

    • ailog_digest --format pretty — not a digest, a replay: re-renders a recovered .jsonl file exactly the way ConsoleSink shows events live. Closes the one gap in the human-readability story: the file itself is machine-shaped by design, and there was no way to look at one with human eyes short of reading raw JSON. Colour on a terminal, plain with -o; interleaved non-JSON lines pass through untouched.

    • logger.interaction(name) — records what the user did. Defaults to trace, so at a production level these stay out of the file while being retained as breadcrumbs, which puts the user's path through the app directly into the causal chain of whatever fails next. Takes an intent name rather than a caption, so it survives copy changes and translation.

    • JsonlPrintSink. Prints the same wire format JsonlFileSink writes, one line per event, through print. Exists for a real device with no reachable filesystem path: flutter run already mirrors the app's print output into your terminal live, so piping that session through tee and extracting the JSON lines gets you a file ailog_digest can read, with no adb pull or Xcode device menu involved. Composes safely with capturePrints — no re-logging loop — verified by test.

    • Build-mode control. isDebugBuild / isProfileBuild / isReleaseBuild / currentBuildMode are const, read from the compiler-defined dart.vm.product and dart.vm.profile — the same values Flutter's kReleaseMode is built on, with no Flutter dependency. byBuildMode(debug:, profile:, release:) picks any value per mode. Logger.create(enabled: false) and Logger.disabled() switch logging off: measured at 5 ns per call in a release AOT build, ahead of all formatting, sanitizing and breadcrumb work. Because the constants fold at compile time, isReleaseBuild ? Logger.disabled() : Logger.create(sink: ...) lets the AOT compiler drop the sink entirely — verified by compiling and confirming the dead branch's strings are absent from the binary.

    • ConsoleSink.usingPrint() / ConsoleSink(write: ...).

    • benchmark/logging_benchmark.dart, so the README's performance numbers can be re-run rather than believed.

    • Whole-log aggregates in the digest. Every message shape counted (lease acquired ×40 vs lease released ×9), and min/max/last of every numeric context field. Driven by a blind A/B test: a summarized digest lost to the raw log on a connection-pool leak because the evidence — the releases that never happened — lived in the successful requests, which summarization had discarded. With the counts added, the digest found the same root cause from a fifth of the bytes.

    • LogFilter / LogSelection: choose what is worth an AI's context window — collapseRepeats, aroundErrors, onlyFailedTraces, level/logger/time/count bounds. Aggregates are computed over the unfiltered input, and both output formats state what was dropped.

    • String output. MemorySink.toJsonl()/toMarkdown()/export(), LogSelection.toReport() (digest + surviving events), buildDigest(events), digestFromJsonl(text). No filesystem — works on web.

    • capturePrints: route ordinary print() calls into the structured log (tagged print, ambient trace attached), with a re-entrancy guard so a console sink cannot feed back into the log.

    • Digest honesty: breadcrumb entries are labeled as breadcrumbs, loggers that appear only inside causal chains are called out, and a group's context sample is labeled first of N (with the most recent shown when it differs).

    Fixed

    • The package did not compile for web at all. ids.dart held 0xcbf29ce484222325 as an integer literal, which is a hard dart2js compile error ("can't be represented exactly in JavaScript") — so every Flutter web or dart2js build failed outright. Nothing caught it: dart analyze and dart test both run on the VM, where a 64-bit int is ordinary. FNV-1a is now computed over two 32-bit halves using arithmetic rather than wide bitwise ops, which is exact on both platforms. Hashes are byte-identical to before — verified against the previous output, the canonical FNV-1a 64 vectors, a dart2js build run under Node, and the Kotlin port compiled and executed for cross-language parity. CI now compiles for web so this cannot regress.
    • Logger.create crashed on dart2js under Node. Random.secure() throws a raw JS ReferenceError, not the UnsupportedError the fallback caught, so the deterministic-Random path written for exactly this case never ran and construction took the program down with it.
    • Checkpoints named the dart2js runtime as the caller whenever the bundle wasn't called main.dart.js. The guard keyed on Flutter web's default output name, so dart compile js -o app.js and any custom bundler name reported → app.js:3881 StackTrace_current as the user's code — confidently wrong, which is the one outcome the guard exists to prevent. It now tests whether a frame resolves to a Dart source position at all.
    • The schema legend listed redacted among the event keys, though it is a convention applying to any field rather than a key — a reader could go looking for an event field that never exists. It is now _convention:redacted, and a test asserts every key toJson can emit is documented (the previous test enumerated keys by hand and so could not catch a newly added one).
    • A value whose toString() throws crashed the host program. Passing a domain object with a buggy override, an uninitialized late field, or a throwing getter in context: — or throwing one — propagated straight out of logger.info() / logger.error(). That is precisely the failure this package promises never to cause. All value paths (context values, map keys, past-depth-limit values, Uri, and the thrown error itself) now contain it and record <toString() threw StateError>, naming the exception rather than silently dropping the field.
    • A script that only called flush() never exited. JsonlFileSink's default flushInterval schedules a periodic Timer, and only close() cancels it — a live Timer keeps the isolate alive, so main() returning after flush() alone left the process hanging until killed. Found by running the README's own quick-start example verbatim. The class doc, the Quick Start in both READMEs, and every example now call close(), and a subprocess regression test (test/regression/) guards it going forward.
    • includePlatformContext duplicated ~133 bytes on every line — OS, Dart version, pid and locale, identical each time, in a format whose whole premise is not wasting a context window. Measured: a 100-event file grew 73%, 182 → 315 bytes per line. JsonlFileSink now writes the platform into each file's _hdr record once, and the option's documentation states the per-event cost.
    • JsonlFileSink could silently lose events. Repeated IOSink.flush() on a handle from File.openWrite(), with writes arriving between flushes, dropped data — measured at 9 of 15 events lost in an ordinary request-handler-shaped loop. The sink was rewritten onto a synchronous RandomAccessFile with an explicit buffer; durability no longer depends on the event loop getting a turn. New: isHealthy, droppedEvents, onError, bufferBytes, flushOnErrorLevel.
    • package:ailog frames are now classified as noise, not application frames — previously a digest's five-frame budget could be spent entirely on this package's own zone plumbing, and the fingerprint could group unrelated bugs logged through the same helper.
    • Digest timestamps rendered in local time while the JSONL is UTC.
    Open source →

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive