The audit log in this system is unusual in its purpose. Most applications log for debugging — to give the developer or the operator something to read when something breaks. The contents of those logs matter to the engineer; their durability matters mainly because losing them means the engineer can't find what they were looking for.
This system's audit log is different. It's not for the engineer. It's for the legal record. Every write attempt the tool makes against the production case-management API produces at least one entry: an in_flight marker before the request fires, followed by an accepted or rejected entry after the response. Guard refusals — writes that a code-level safety check blocked before they ever left the machine — produce their own entries. Override-token mints, redemptions, and rotations all produce entries.
The intended consumer is a future audit reviewer. The question they're asking is: what actually happened on this case file? Which operations did the firm's tool perform, and which did it attempt that were blocked or refused? Those questions only have honest answers if every operation produced a durable record.
The default for application-log writes in Node.js doesn't quite produce that. appendFileSync(path, line) opens the file, writes the bytes, and closes. The write returns when the kernel has copied the bytes into the OS page cache. The page cache is RAM. The kernel will flush it to disk on its own schedule, typically within a few seconds, but the timing is non-deterministic. If the system crashes between the application's writeSync return and the kernel's flush, the bytes are lost.
For an application log, the trade is fine. The application keeps running. The next entry will land. A few seconds of lost log don't change the engineer's ability to investigate. For an audit log meant to be quoted in a deposition, the trade is different. An entry that was "logged" from the program's perspective but doesn't exist on disk because the laptop battery died at the wrong instant is, for legal purposes, an entry that never happened. The firm's tool would, by design, have a record-keeping gap exactly when something dramatic was happening.
What fsync changes
The POSIX fsync system call asks the kernel to flush every dirty page associated with a file descriptor to disk before returning. After fsync returns successfully, the bytes are durable. A crash after that point cannot lose them.
The cost is real. Each fsync call forces the disk to physically commit the write. On SSDs the latency is a few hundred microseconds. On spinning disks it's a few milliseconds. If you fsync on every byte of a log file, performance becomes the throughput of your storage.
For this codebase the cost is acceptable because the audit log doesn't receive a high write rate. Every API call the tool makes produces one or two log entries. The API calls themselves are tens of milliseconds at minimum (network round-trip to Merus). An extra few hundred microseconds for fsync is a small fraction of overall latency.
The pattern is open → write → fsync → close. The same pattern is already used by every other state-file writer in this codebase: the upload bindings, the message bindings, the redemption file. The audit log was the last writer that still used a direct appendFileSync. The unification is itself a quiet improvement — the policy "all on-disk state survives power loss" can now be stated without exception.
Failure handling
Some filesystems don't support fsync (network filesystems, some virtualized storage). The Node call propagates an error. The right behavior there is to keep the write but skip the durability guarantee — the alternative would be to fail the audit append entirely, which is worse than no fsync at all. The implementation catches the fsync error and proceeds. The write succeeded; just without the durability promise. This matches the existing pattern in the other writers.
The outer try/catch around the whole append remains. If the open fails (permissions, full disk, etc.), the catch swallows it. Audit logging is best-effort at the outermost layer: the policy is to never let an audit failure crash a real attorney operation. The caller did their work; the missing audit entry is a separate problem the system can surface elsewhere.
What this doesn't fix
fsync makes the audit log durable against power loss. It does not make the audit log durable against malicious deletion. A user with write access to ~/.aaicase/audit.log can rm it. The 0o600 permissions prevent other users on the same machine from doing this, but the file's owner can. For a legal-grade audit chain that should be impossible to silently tamper with, you'd want a write-only append, a remote shipped log, or a Merkle-chained signature scheme. None of that is in this system today. The threat model assumed is "honest attorney, possibly-crashing machine" — not "adversary with local access."
That's a real constraint to name. The tool's audit log is durable against accidents; it's not a tamper-evident chain. If the firm needs that level of assurance, the natural next step is shipping audit entries to a remote append-only store as they're written. That's a follow-up project; this release just closes the easier gap.
The release
The version on the install URL is 4.5.72. The change is small — one function in lib/audit.mjs swapped from appendFileSync to openSync + writeSync + fsyncSync + closeSync. The verification was straightforward: five entries written via the new path, all visible in the tail. The audit log behavior from the caller's perspective is unchanged. The kernel's guarantee about what's on disk after each write is now stronger.
The work continues at the pace of one verified release at a time, and the audit log now has the durability property its callers always assumed it had.