06/Developer Tooling

AgentSessionObservability

Watching a dozen AI coding sessions without tabbing through windows

Year
2026
Status
Working tool
Category
Developer Tooling
Stack
Node.js 22 (ESM) · Electron 34 · Server-Sent Events

01/Context

This is the personal project, and it is the one that best explains how I work: the problem was mine, the observation surface was whatever the tool already wrote to disk, and the constraint was that nothing could be instrumented, injected or sent anywhere.

Transcripts reach tens of megabytes, so any naive read-the-file-and-report approach is too slow to poll. That single performance constraint shaped the entire architecture.

02/At a glance

Problem
Running several AI coding sessions at once leaves no way to know which one is working, which is silently blocked on a permission prompt, which errored and which finished — short of tabbing through every terminal and editor window. There is no status API. The only signal is the session registry, the process table, and multi-megabyte JSONL transcripts on disk.
Built
A zero-dependency Electron dashboard that derives full session state purely by observing files — an incremental transcript digester, an 11-rule state machine, per-model cost arithmetic, and a probabilistic ETA with a confidence band.
Role
Sole author and architect. Wrote the build contract that pinned the module shapes, then integrated the modules implemented against it.
Changed
Answers at a glance what previously required opening every window: what is each session doing, how far along, roughly how much longer, and what the token usage would have cost.

03/Architecture

Pure observation. Three read-only sources feed incremental ingesters, a derivation layer computes state, ETA and cost, and a change-hashed store pushes over SSE to a framework-free UI and an Electron shell that consumes its own loopback stream.

Agent Session Observability architecture diagramData flows through 13 components: Session files on disk (Source), Process table (Source), Sidecar agent transcripts (Source), fs.watch + safety poll (Ingest), Incremental digest (Ingest), Liveness + activity (Ingest), Agent scanner (Ingest), Digest cache (Store), Assembly + hash (Service), State · ETA · cost (Decision logic), HTTP + SSE on loopback (Service), Vanilla JS dashboard (Interface), Tray · badge · notifications (Output). Connections: Session files on disk to Incremental digest; Session files on disk to Liveness + activity; Process table to Liveness + activity; Sidecar agent transcripts to Agent scanner; Incremental digest to Digest cache; Incremental digest to Assembly + hash; Liveness + activity to Assembly + hash; Agent scanner to Assembly + hash; Assembly + hash to State · ETA · cost; State · ETA · cost to Assembly + hash; fs.watch + safety poll to Assembly + hash; Assembly + hash to HTTP + SSE on loopback; HTTP + SSE on loopback to Vanilla JS dashboard; HTTP + SSE on loopback to Tray · badge · notifications.Session files on diskRegistry, JSONL transcripts…Process tableLiveness + child processesSidecar agent transc…Background work after the m…fs.watch + safety po…150ms debounce, self-rearmi…Incremental digestStreams from stored byte of…Liveness + activityGuards against recycled pidsAgent scannermtime walk, then bounded he…Digest cacheAtomic write + rename; mtim…Assembly + hashBroadcasts only on real cha…State · ETA · cost11-rule machine, confidence…HTTP + SSE on loopba…6 JSON routes, never routab…Vanilla JS dashboardInline-SVG charts, no build…Tray · badge · notif…Consumes its own loopback S…

04/The system

Read only what changed

The transcript digester streams only the bytes appended since the last tick, from a stored byte offset, and persists digests to a local cache keyed on size and mtime. A cold scan of a large transcript is a one-time cost; every subsequent tick is nearly free.

Digests are written atomically — write then rename — so a crash mid-write cannot leave a corrupt cache behind.

Eleven rules, in a fixed order

State — working, waiting for input, blocked on a permission prompt, stalled, errored, idle, done — is resolved by a numbered rule sequence, with the order itself part of the spec. Where the implementation deviates from the contract, the module header says so and argues the case.

A separate scanner watches each session's sidecar agent directories, so a session whose main turn has ended but whose background agents are still running is correctly reported as busy rather than finished.

Cost arithmetic that respects the pricing model

Cache reads, five-minute cache writes and one-hour cache writes carry different multipliers, so a blended rate is simply wrong. The price table resolves suffixed model ids by longest prefix, applies date-bounded intro pricing per message timestamp, attributes per-model within mixed-model transcripts, and excludes synthetic placeholder messages.

Degrade, not die

Timeouts fence every per-session scan, refresh is re-entrancy guarded, and every I/O path has a degraded handler. The server binds 127.0.0.1 only and is never routable, which is why it needs no auth — a decision documented in the module header rather than assumed.

The store broadcasts only when a clock-independent hash of the payload actually changes, so an idle machine produces no traffic and no repaints.

05/What was hard

Polling a file that grows to tens of megabytes

Re-reading is not an option at poll frequency. Streaming from a stored byte offset, skipping pathologically large single lines, and caching digests against size and mtime is what makes the whole dashboard viable — every other feature depends on that one decision.

A session that looks finished but is not

When the main turn ends but background agents keep running, the obvious signal says done. Watching the sidecar agent directories — with bounded head and tail reads rather than full parses — is what makes the status honest.

A build contract instead of a codebase

The spec pins the exact serialized shape, the numbered rule order, the ETA algorithm, performance budgets, the full route table, and the design tokens down to hex values — enough that modules could be implemented independently against it. That is what let the work parallelise, and it is the same technique used on the larger platform projects.

06/AI

The app makes no model calls and holds no API key. It is an observability layer over AI agent sessions, not an AI product — its AI-specific work is domain modelling of the pricing and the session lifecycle. It is also the clearest artifact of spec-driven, AI-assisted development: a written build contract, parallel implementation, then integration.

07/Automation

Native notifications fire on transitions into waiting-for-input, blocked, errored and done, with per-session debounce and a startup quiet window so pre-existing sessions are seeded rather than firing retroactively. The packaging script builds a real double-clickable macOS app — bundle, Info.plist, icns, ad-hoc signing — without any packaging dependency.

08/Direction & delivery

The spec is written as a binding build contract: exact serialized shapes, numbered rule order, performance budgets (a cold scan of ~116 MB under six seconds, never blocking the event loop more than ~50ms), the full HTTP route table, and design tokens. Nearly every module opens with a header explaining why — including the decisions that look wrong until explained: why the shell is CommonJS, why it reads its own loopback stream instead of importing the store, why the watches are non-recursive.

09/Scale

~14,200
Lines of source
0
Runtime dependencies
11
State machine rules
6 + SSE
HTTP endpoints

Objective size signals taken from the repository. No impact metrics are claimed that the source does not prove.

10/What I would tell the next person

  • A performance budget written into the spec changes the architecture. 'Under six seconds cold' is what forced incremental digestion.
  • Document the decisions that look wrong. A reader who cannot see the constraint will 'fix' them.
  • Not shipping auth can be correct — if you can state precisely why the surface is unreachable, and you write that down.

11/Technologies

  • Node.js 22 (ESM)
  • Electron 34
  • Server-Sent Events
  • node:fs streams
  • JSONL stream parsing
  • Vanilla JavaScript
  • Inline SVG
  • Bash

Integrations

  • Local session registry and JSONL transcripts
  • macOS process table
  • macOS tray, dock and notification APIs

Local-only tool. Binds 127.0.0.1 and is never routable.