cherry-pick(state): lock-wrapped appends for journal, audit, workflow-logger

Cherry-pick of gsd-build/gsd-2 53babec29 — lock-wrapped append half.
Wraps appends to .sf/journal/, .sf/audit/events.jsonl, and the
workflow-logger error log in withFileLockSync (onLocked: skip),
preserving best-effort semantics while preventing torn writes
under contention.

Companion to the atomic-write half landed in 3df56cb94. Path-renames
(gsdRoot→sfRoot, gsd-db→sf-db) preserved during conflict resolution.

Co-Authored-By: Jeremy <jeremy@fluxlabs.net>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-28 05:27:44 +02:00
parent f1f4b840e1
commit 22d4579690
3 changed files with 60 additions and 6 deletions

View file

@ -12,10 +12,19 @@
* - Silent failure: journal writes never throw absence of events is the failure signal
*/
import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
import {
appendFileSync,
closeSync,
existsSync,
mkdirSync,
openSync,
readdirSync,
readFileSync,
} from "node:fs";
import { join } from "node:path";
import { sfRoot } from "./paths.js";
import { isStaleWrite } from "./auto/turn-epoch.js";
import { withFileLockSync } from "./file-lock.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
import { isAuditEnvelopeEnabled } from "./uok/audit-toggle.js";
@ -102,7 +111,19 @@ export function emitJournalEvent(basePath: string, entry: JournalEntry): void {
mkdirSync(journalDir, { recursive: true });
const dateStr = entry.ts.slice(0, 10);
const filePath = join(journalDir, `${dateStr}.jsonl`);
appendFileSync(filePath, JSON.stringify(entry) + "\n");
// Ensure file exists so proper-lockfile can acquire a lock against it.
if (!existsSync(filePath)) closeSync(openSync(filePath, "a"));
// onLocked: "skip" — journal writes are best-effort. POSIX O_APPEND
// atomicity still protects small entries; the lock mainly serializes
// larger writes and gives cross-process exclusivity on platforms where
// O_APPEND semantics are weaker (Windows).
withFileLockSync(
filePath,
() => {
appendFileSync(filePath, JSON.stringify(entry) + "\n");
},
{ onLocked: "skip" },
);
} catch {
// Silent failure — journal must never break auto-mode
}

View file

@ -1,9 +1,10 @@
import { appendFileSync, mkdirSync } from "node:fs";
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import { sfRoot } from "../paths.js";
import { isDbAvailable, insertAuditEvent } from "../sf-db.js";
import { withFileLockSync } from "../file-lock.js";
import type { AuditEventEnvelope } from "./contracts.js";
function auditLogPath(basePath: string): string {
@ -37,7 +38,21 @@ export function buildAuditEnvelope(args: {
export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope): void {
try {
ensureAuditDir(basePath);
appendFileSync(auditLogPath(basePath), `${JSON.stringify(event)}\n`, "utf-8");
const path = auditLogPath(basePath);
// proper-lockfile requires the target file to exist before locking.
// Touch it via open(O_APPEND|O_CREAT) so the first writer wins the race
// atomically at the kernel level.
if (!existsSync(path)) closeSync(openSync(path, "a"));
// onLocked: "skip" — audit writes are best-effort; under heavy contention
// POSIX O_APPEND atomicity still protects small line writes, so skipping
// the lock rather than stalling orchestration is the correct tradeoff.
withFileLockSync(
path,
() => {
appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
},
{ onLocked: "skip" },
);
} catch {
// Best-effort: audit writes must never break orchestration.
}

View file

@ -16,9 +16,17 @@
// the start of each unit to prevent log bleed between units running in the same
// Node process.
import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
import {
appendFileSync,
closeSync,
existsSync,
mkdirSync,
openSync,
readFileSync,
} from "node:fs";
import { join } from "node:path";
import { withFileLockSync } from "./file-lock.js";
import { appendNotification } from "./notification-store.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
import { isAuditEnvelopeEnabled } from "./uok/audit-toggle.js";
@ -308,8 +316,18 @@ function _push(
try {
const auditDir = join(_auditBasePath, ".sf");
mkdirSync(auditDir, { recursive: true });
const auditPath = join(auditDir, "audit-log.jsonl");
const sanitized = _sanitizeForAudit(entry);
appendFileSync(join(auditDir, "audit-log.jsonl"), JSON.stringify(sanitized) + "\n", "utf-8");
// Ensure file exists so proper-lockfile can acquire a lock against it.
if (!existsSync(auditPath)) closeSync(openSync(auditPath, "a"));
// onLocked: "skip" — never block error logging on lock contention.
withFileLockSync(
auditPath,
() => {
appendFileSync(auditPath, JSON.stringify(sanitized) + "\n", "utf-8");
},
{ onLocked: "skip" },
);
} catch (auditErr) {
// Best-effort — never let audit write failures bubble up
_writeStderr(`[sf:audit] failed to persist log entry: ${(auditErr as Error).message}\n`);