← Back to Blog

Day 73: A Chat That Stops Claiming Green

Friday. Three threads landed today and they're each the kind of small thing that separates a product you can demo from a product you can leave running. Cold-start UX, audit export for compliance, and the test coverage that proves yesterday's cascade fix actually broke the cascade.

The Indicator Was Lying

Pinchy's chat header has a small status dot — green when OpenClaw is connected, amber when it's reconnecting, red when it isn't. The dot's value depends on knowing the answer; the bug was that during cold-start, before any signal had been received, the front-end defaulted to isOpenClawConnected: true. So a fresh session — open the page, agent is still booting — showed a green dot for the first second or two, until the first real status update arrived and possibly flipped it amber. A small lie, but a corrosive one: a green dot that turns amber three seconds in is more confusing than a red dot that turns green when it should.

PR #198 fixes it at both ends. The client-side default is false — assume disconnected until you've heard otherwise — so the cold-start state is the honest one. The server-side cold-start path now emits an explicit openclaw_status: connecting rather than relying on the absence of a signal, so the client has something to render against during the boot window. The indicator stops being a credibility leak.

PR #197 is the next layer of the same problem. Even with the dot honest, the chat composer was rendering an idle state when the agent itself was still booting — you could type a message into a session that wouldn't be able to receive it for another five seconds. The fix introduces a Starting Agent state that holds the composer in a clearly-not-ready presentation until the first message is actually painted on screen. The atomicity matters: there's a moment between agent has acknowledged the session and first message is rendered where the previous code would briefly flash idle, and the test that landed pins that invariant down — the Starting Agent state must persist across the empty-history transition without flickering through idle first.

One smaller related fix: the chat now never renders Send and Stop buttons together. There had been a ~60ms window during the streaming-to-idle transition where both could be visible at once, looking like a bug to anyone who happened to glance at it. The reducer-level guarantee is now that exactly one of the two is mounted at any moment.

Audit PDF Export

The other big push today: the audit log learned to export. Until now, the audit panel was a screen — viewable, filterable, but not extractable into the kind of artifact someone outside Pinchy would expect. The new export endpoint produces a PDF — proper document, organisation header, paginated, signed with a per-export integrity hash — and a CSV alongside it whose final column is the same hash, computed row-wise, so a row tampered with after export can be detected against the original. The kind of artifact a procurement questionnaire asks for under can audit data be exported in tamper-evident form, where the answer used to be not yet.

The PR that ships the export sits on top of a small refactor (PR #218) that we'd been putting off for a while: the audit-emit code paths had grown into half a dozen subtly different shapes, and the export endpoint needed a single canonical view of the same events to render. The refactor consolidated them; the self-review afterwards caught a handful of small things that landed as discrete cleanup commits. The code review caught one larger thing — an audit emission that was still fire-and-forget, swallowing failures into a .catch() that did nothing — which got turned into a proper await, with the rare cases that genuinely benefit from deferral routed through a new deferAuditLog helper. The pattern is now obvious from the call site instead of hidden in a callback.

The Cascade Fix Gets a Test

Yesterday's #193 fix landed the writer change; today's commits add the test that proves it works. agent-create-no-restart is the spec that creates an agent and then asserts that the gateway didn't restart — by counting the number of cascade markers in the OpenClaw log between agent-create-N and agent-create-N+1. A warm-up regenerate happens before the assertion, so the test isn't catching a baseline restart that would have happened anyway. Five-second wait after warmup, to let any latent cascade markers finish appearing in the log before the count is taken. Review feedback on the spec landed as cleanup commits — particularly the small things, like asserting the marker count exactly rather than as a lower bound.

One subtle related fix: the agent-create writer was emitting a config file that didn't match OpenClaw's exact on-disk format. Specifically, OpenClaw normalises the file with a trailing newline and a trimEnd on each line; Pinchy's writer wasn't. The result was that even the no-cascade RPC path could trigger an inotify event after the RPC apply, because the file as Pinchy wrote it differed from the file as OpenClaw saved it. Match the format byte-for-byte and the inotify event stops firing.

Where the Credentials Live

A quieter refactor in the background: pinchy-odoo and pinchy-web both used to read their credentials directly from the secrets file at startup. They now fetch them via a Pinchy API endpoint, so the canonical location of any given secret is owned by Pinchy alone — the plugins don't have a second blessed read path that could fall out of sync. pinchy-email got the same #209-style guardrails for credential handling: redact known credential field names from logs, never serialise the raw value into an error message. None of this is visible in the UI; it's the post-migration tidying that follows any architectural change once you've lived with it for a week.

CI Housekeeping

Two CI cleanups worth flagging. The GHCR sha-* prune workflow that we'd torn out a few weeks ago (#212) got rebuilt — old commit-hash images need pruning to keep the registry size sane, but the original implementation was pruning released tags too, breaking the ability to republish a tag if a release went sideways. The rebuild distinguishes the two: sha-* images get pruned aggressively, semver tags never do. The end-user upgrade test now skips gracefully when the previous-version images aren't available (rather than failing the whole pipeline because Docker Hub couldn't be reached), and the Odoo e2e finally got the dropdown-menu-trigger selector right by switching to data-slot instead of role+svg, plus a Better Auth rate-limit bypass for test mode.

Day 73

The week is cohering around v0.5.0 in a way it hadn't been until now. Each day this week has dropped one piece of thing-that-was-quietly-wrong — secrets ownership, cascade restarts, indicator lies, audit export — and the cumulative effect is that the product behaves the way it's been describing itself for months. The audit export specifically is the kind of capability where the answer to can it do that stops being almost. The next week is the one where v0.5.0's release notes get written.

← Day 72: Agent Create Without the Cascade Day 74: The Saturday Hardening Pass →

Pinchy is open source and ready to deploy. Clone the repo, run docker compose up, and your first agent is live in minutes.