writeAtomic, everywhere a file lands

the .part-then-rename rule was applied at two upload routes yesterday and
by this morning the platform had a dozen other writes putting files in the
same folders — an OCR result beside its image, a TTS clip, a download
landing, a save from the editor — every one straight to the final name. a
guarantee that holds for two of fourteen paths is not a guarantee, and the
next person adding a fifteenth had nothing to copy.

so it is a function now, and every durable write goes through it: the file
browser, sendoff, task definitions, skill and tool files, bug reports,
scraped attachments, the chat provider config.

the failure it prevents is silent rather than loud. a truncated m4a decodes
fine, it is simply short — a commander transcribing recordings as they land
would produce a clean transcript of the first few seconds of a two-minute
memo and report success.

two things learned writing it. Bun.write creates the parent chain itself,
so the mkdir option i added was dead weight and several callers were doing
it twice. and Parameters<typeof Bun.write>[1] resolves to the first
overload only — BunFile — which rejects the string and Buffer every caller
actually passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 05:26:40 +00:00
co-authored by Claude Opus 5
parent 20b270ef07
commit e87323f71a
9 changed files with 164 additions and 42 deletions
+7 -6
View File
@@ -2,6 +2,7 @@ import { readdir, mkdir, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { itemsDir, OFFICER_ITEMS_DIR } from '../../data-path';
import { capabilitySources, type CapabilitySource } from '../../capability-sources';
import { writeAtomic } from '@@/write-atomic';
// File-backed task store. Every task is a directory under $OFFICER_ITEMS_DIR/tasks/<dirName>/ with a
// TASK.md (metadata + prose body) and, for script-mode tasks, a sibling implementation file. There are
@@ -296,11 +297,11 @@ export async function createTask(input: CreateTaskInput): Promise<TaskSummary &
const language = input.language ?? null;
await mkdir(taskDir(dirName), { recursive: true });
await Bun.write(
await writeAtomic(
taskFile(dirName),
buildTaskMd({ name, description: input.description ?? null, version: 1, mode, language }, ''),
);
if (mode === 'script') await Bun.write(join(taskDir(dirName), implFileName(language)), '');
if (mode === 'script') await writeAtomic(join(taskDir(dirName), implFileName(language)), '');
return {
dirName,
@@ -327,10 +328,10 @@ export async function updateTask(
const merged: TaskFrontmatter = { ...existing, ...patch };
const body = patch.body ?? existing.body;
await Bun.write(taskFile(dirName), buildTaskMd(merged, body));
await writeAtomic(taskFile(dirName), buildTaskMd(merged, body));
if (patch.implementation !== undefined && patch.implementation !== null) {
await Bun.write(join(taskDir(dirName), implFileName(merged.language)), patch.implementation);
await writeAtomic(join(taskDir(dirName), implFileName(merged.language)), patch.implementation);
}
return getTaskByDirName(dirName);
@@ -355,8 +356,8 @@ export async function importTask(
record: Partial<TaskFrontmatter> & { body?: string; implementation?: string | null },
): Promise<void> {
await mkdir(taskDir(dirName), { recursive: true });
await Bun.write(taskFile(dirName), buildTaskMd(record, record.body ?? ''));
await writeAtomic(taskFile(dirName), buildTaskMd(record, record.body ?? ''));
if (record.implementation != null && record.implementation !== '') {
await Bun.write(join(taskDir(dirName), implFileName(record.language ?? null)), record.implementation);
await writeAtomic(join(taskDir(dirName), implFileName(record.language ?? null)), record.implementation);
}
}