One of the durability fixes earlier this month added size-based rotation to the audit log. When the current file passes a configurable threshold, the writer gzips it to a timestamped archive (audit.log.2026-05-24T11-30-00-123Z.gz) and truncates the active file. Cleanup runs in parallel: anything older than the keep limit gets deleted. Standard log-rotation pattern, well-tested under stress, working in production.

The change touched the writer side only. The reader side — the small CLI that walks recent sequence runs and lets an attorney see what could be rolled back — was not updated. Its loadSequences function still looked like this:

function loadSequences() {
  if (!existsSync(AUDIT_LOG)) return [];
  const raw = readFileSync(AUDIT_LOG, 'utf8');
  const out = [];
  for (const line of raw.split('\n')) {
    if (!line.trim()) continue;
    try {
      const j = JSON.parse(line);
      if (j.action === 'sequence') out.push(j);
    } catch {}
  }
  return out;
}

One file. The active audit.log. Nothing else.

The result, after the rotation feature shipped: every entry written before the most recent rotation became invisible to the reader. The data was still on disk, gzipped, with the right permissions, recoverable manually with gunzip and grep. But the attorney's tool — which reads the audit log specifically to find recent work — couldn't see any of it. The list command reported the wrong total. The show command reported "No sequence found" for any label whose audit entry had been rotated.

I ran a stress probe. Forced 10 sequence audit entries into the system with the rotation threshold low enough that 8 ended up in archives and 2 stayed in the active log. The list command showed two entries and confidently reported "2 total." The other 8 sequences existed on disk and were unreachable through the official interface.

The fix is small, the gap was large

The fix took 20 lines. Glob the directory for audit.log.*.gz archives, gunzip each, parse the same way, then append entries from the current log:

function loadSequences() {
  const out = [];
  if (existsSync(AUDIT_DIR)) {
    const archives = readdirSync(AUDIT_DIR)
      .filter(f => f.startsWith('audit.log.') && f.endsWith('.gz'))
      .sort();  // ISO-8601 sorts chronologically
    for (const a of archives) {
      for (const j of _readAuditLines(join(AUDIT_DIR, a))) out.push(j);
    }
  }
  if (existsSync(AUDIT_LOG)) {
    for (const j of _readAuditLines(AUDIT_LOG)) out.push(j);
  }
  return out;
}

After the fix, the same probe shows all 10 sequences. The "X total" count is correct. The show command works on any label regardless of which archive holds its audit entry.

The interesting thing is not that the fix was simple. The interesting thing is that the gap existed for six releases before anyone tried the reader on a system with rotated archives. The writer side had full test coverage. The reader side had test coverage. Neither test set included a scenario where rotation had fired before the reader was invoked. The integration wasn't tested as an integration.

The pattern

This is a specific instance of a broader shape: upstream changes its storage layout, downstream readers still look at the old layout. The fix to the upstream layer was correct in isolation and got verified in isolation. The reader was correct as originally written. The integration broke silently because nothing exercised it after the upstream change.

The pattern shows up whenever the data model is shared across writer and reader code paths that aren't in the same file. Audit logs are one example. Database migrations are another (the migration changes the schema, every read query in the application needs to be checked). Configuration formats are another (a renamed key in the writer needs a corresponding reader update). The cost is always the same: the upstream change ships, looks fine, and the downstream readers silently produce wrong answers until someone runs them on a system that's exercised the new format.

The defense is a consumer-side sweep at the same time as the producer-side change. When the rotation feature shipped, the right move would have been to grep the codebase for "audit.log" and check every read site. That grep would have surfaced loadSequences and the fix would have shipped at the same time as the rotation. Instead, the writer changed in 4.5.94, and the reader didn't get updated until 4.5.100 — six releases later, after a probe surfaced it.

The smaller fix in the same release

While I was in the file, I noticed that sequences missing the optional label field were rendering as the literal string "undefined" in the list output. The shape is familiar — we've fixed this same pattern in five other places in this codebase over the past month (any time a function reads value-or-falls-through and the fallthrough is the JavaScript undefined coerced to the string "undefined"). Now renders as "(no label)".

Both fixes are small but the discipline they encode is real. If you change a storage format, sweep the consumers. If you let a value be optional, render its absence with a typed placeholder, not the language's spelling of undefined.

The version on the install URL is 4.5.100.