fix: resolve race conditions in blob-store, discovery-cache, and agent-loop

- blob-store: Replace non-atomic check-then-act (existsSync + writeFileSync)
  with writeFileSync using 'wx' flag for atomic exclusive creation
- discovery-cache: Re-read from disk before mutations to avoid stale overwrites,
  and use temp file + rename for atomic saves
- agent-loop: Deep copy messages array in agentLoopContinue to prevent shared
  reference mutations from affecting the original context
This commit is contained in:
frizynn 2026-03-22 22:30:44 -03:00
parent f196309295
commit 806cb76e72
3 changed files with 19 additions and 6 deletions

View file

@ -118,7 +118,10 @@ export function agentLoopContinue(
(async () => {
const newMessages: AgentMessage[] = [];
const currentContext: AgentContext = { ...context };
const currentContext: AgentContext = {
...context,
messages: [...context.messages],
};
stream.push({ type: "agent_start" });
stream.push({ type: "turn_start" });

View file

@ -6,7 +6,7 @@
* provides automatic deduplication across sessions.
*/
import { createHash } from "node:crypto";
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync, accessSync, unlinkSync, statSync } from "node:fs";
import { mkdirSync, readdirSync, readFileSync, writeFileSync, accessSync, unlinkSync, statSync } from "node:fs";
import { join } from "node:path";
const BLOB_PREFIX = "blob:sha256:";
@ -37,8 +37,11 @@ export class BlobStore {
},
};
if (!existsSync(blobPath)) {
writeFileSync(blobPath, data);
try {
writeFileSync(blobPath, data, { flag: "wx" }); // Atomic: fails if file exists
} catch (err: any) {
if (err.code !== "EEXIST") throw err;
// File already exists — expected for content-addressed storage
}
return result;
}

View file

@ -3,7 +3,7 @@
* Stores results at {agentDir}/discovery-cache.json with per-provider TTLs.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { getAgentDir } from "../config.js";
import { type DiscoveredModel, getDefaultTTL } from "./model-discovery.js";
@ -35,6 +35,8 @@ export class ModelDiscoveryCache {
}
set(provider: string, models: DiscoveredModel[], ttlMs?: number): void {
// Re-read from disk to get the latest state before modifying
this.load();
this.data.entries[provider] = {
models,
fetchedAt: Date.now(),
@ -50,6 +52,8 @@ export class ModelDiscoveryCache {
}
clear(provider?: string): void {
// Re-read from disk to get the latest state before modifying
this.load();
if (provider) {
delete this.data.entries[provider];
} else {
@ -89,7 +93,10 @@ export class ModelDiscoveryCache {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(this.cachePath, JSON.stringify(this.data, null, 2), "utf-8");
// Atomic write: write to temp file then rename to avoid partial reads
const tmpPath = this.cachePath + ".tmp";
writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
renameSync(tmpPath, this.cachePath);
} catch {
// Silently ignore write failures (read-only FS, permissions, etc.)
}