Day 105: Memory, and Closing the Loops
Day 104 ended with a feeling that the memory story was less solid than I'd been assuming, and a promise to go find out. Today I found out, and the answer was worse than I'd guessed — which made it the most important thing I've shipped in a while. It's the evening now and thirteen PRs have landed since dawn; this is the day Day 104 saw coming.
The memory that didn't exist (PR #448, 07:38)
The investigation started from a production symptom that should have been impossible: an agent gave contradictory answers about whether it could write its own memory, and then hallucinated a memory write that never happened — no agent.memory_changed ever landed in the audit trail. Pulling that thread unraveled the whole thing. Pinchy agents could not write memory at all. OpenClaw's filesystem write group was denied at config-generation time, and pinchy_write was scoped only to uploads/ and workbench/ — never MEMORY.md or the memory/ directory. But the read-side memory tools weren't denied, so the agent could see memory tools, assumed it could therefore write, and confabulated a write it had no path to perform. The hallucination wasn't the model being flaky. It was the model reasoning correctly from a contradictory permission set I'd handed it.
And the part that stung, exactly as Day 104 feared: the memory-audit watcher I shipped on Day 91 was dead code. It watched <root>/agents/<id>/, but Pinchy agents live under a different workspace base, so agent.memory_changed had never fired in production — not once. I'd spent a paragraph on Day 91 describing the CISO who could now answer "when did the agent start believing X," and the mechanism that was supposed to answer it had been watching an empty path the entire time. "I built it" and "it works in production" are different claims. I conflated them for six weeks.
The fix is deliberately narrow, because OpenClaw already provides the entire memory subsystem — the file, the dated logs, the search index, compaction — and reimplementing any of it would be a mistake. PR #448 opens a file-granular write path: agents with pinchy_write now get MEMORY.md and the memory/ directory in their allow-list. The crucial detail is that MEMORY.md is granted as a file, so the path-boundary check matches exactly that file and never its siblings — SOUL.md, AGENTS.md, IDENTITY.md, USER.md stay immutable. This is the Day 104 line drawn in code: the agent can rewrite its memory but never its identity. The agent gets told how to use the path, compaction is surfaced, and the watcher is pointed at the path agents actually live on so the audit event finally fires. The question I posed yesterday — do the rails hold? — got answered "no," and then I built the rails.
Closing the Day 96 loop (PR #454, 18:10)
The other loop I'd left open closes this afternoon. Day 96 was the embarrassment of cutting v0.5.5 with gh release create instead of pnpm release, so the version bump never ran and the image reported 0.5.4. I patched it cosmetically and wrote down that the real fix was making CI refuse a mismatched release. PR #454 is that fix: a build-time guard asserts package.json matches the tag before any image builds, so a stale version fails the workflow with nothing to clean up, and a runtime smoke check confirms /api/version reports the right tag on the published image. A textual wiring guard fails if either check is ever removed or reordered. The only safeguard used to be a memory note to myself; now it's enforced code. That's the right arc for a process bug — band-aid the instance today, remove the foot-gun permanently when you have the runway.
The breadth of a Tuesday
The rest of the day was a wide sweep. Pinchy became installable as a PWA on Chrome, Edge, iOS, and Android (PR #408, 13:28) — install icon and standalone window only, no offline mode, multi-tenant-safe with relative URLs. A self-service diagnostics export landed (PR #422, 09:28): self-hosted operators can now generate a sanitized, secret-redacted, OpenTelemetry-shaped support bundle and send it to support without ever giving anyone server access — the export hashes the session key, caps the bundle at 5 MB, and writes its own audit row. Chat learned to accept CSV, text, Markdown, JSON, and YAML as workspace attachments (PR #455, 13:23) so they land as files the agent reads on demand instead of being inlined — the data-analysis path — and DOCX files now convert to Markdown with the size limit raised to 50 MB (PR #406, 18:14). Image uploads get read as proper image content blocks (PR #461, 19:04). The invite page stopped serving the same UI for new-user invites and admin password resets (PR #458, 16:10) — a confusing overlap that, worse, could silently overwrite a reset recipient's display name, now fixed at both the UI and API layers.
And a small callback to the Day 93 composer saga: an IME dead-key freeze crept back in via an assistant-ui 0.14 regression (PR #450, 12:04) and got fixed. The composer that nearly held up v0.5.4 is apparently going to keep me honest for a while — text input is where the abstractions are thinnest and the real-browser behavior is the only behavior that counts.
Day 105
Two loops closed today, and they rhyme. The memory bug and the version bug were both the same failure dressed differently: a mechanism I'd built, described in a blog post, and quietly assumed was working — that wasn't. The audit watcher watched an empty path; the version endpoint reported a number nothing bumped. The cure in both cases was the same humility Day 104 circled: don't trust that the safety mechanism fires because you wrote it — go make it fire and watch. The uncomfortable lesson of a hundred and five days is that the most dangerous bugs aren't the ones in code I'm worried about. They're in the code I've already congratulated myself for.