fix(coding-agent): queue file mutations across edit and write

closes #2327
This commit is contained in:
Mario Zechner
2026-03-20 01:10:56 +01:00
parent 527c2af5e7
commit 74a46fc7ea
12 changed files with 481 additions and 220 deletions

View File

@@ -30,7 +30,7 @@ import { existsSync, readFileSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { StringEnum } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getAgentDir } from "@mariozechner/pi-coding-agent";
import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { type Static, Type } from "@sinclair/typebox";
const PROVIDER = "google-antigravity";
@@ -228,12 +228,14 @@ function imageExtension(mimeType: string): string {
}
async function saveImage(base64Data: string, mimeType: string, outputDir: string): Promise<string> {
await mkdir(outputDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const ext = imageExtension(mimeType);
const filename = `image-${timestamp}-${randomUUID().slice(0, 8)}.${ext}`;
const filePath = join(outputDir, filename);
await writeFile(filePath, Buffer.from(base64Data, "base64"));
await withFileMutationQueue(filePath, async () => {
await mkdir(outputDir, { recursive: true });
await writeFile(filePath, Buffer.from(base64Data, "base64"));
});
return filePath;
}

View File

@@ -19,7 +19,7 @@ import * as path from "node:path";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { Message } from "@mariozechner/pi-ai";
import { StringEnum } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
@@ -207,11 +207,13 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
return results;
}
function writePromptToTempFile(agentName: string, prompt: string): { dir: string; filePath: string } {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
const safeName = agentName.replace(/[^\w.-]+/g, "_");
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
await withFileMutationQueue(filePath, async () => {
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
});
return { dir: tmpDir, filePath };
}
@@ -274,7 +276,7 @@ async function runSingleAgent(
try {
if (agent.systemPrompt.trim()) {
const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
tmpPromptDir = tmp.dir;
tmpPromptPath = tmp.filePath;
args.push("--append-system-prompt", tmpPromptPath);

View File

@@ -21,10 +21,10 @@
*/
import type { TextContent } from "@mariozechner/pi-ai";
import { type ExtensionAPI, getAgentDir } from "@mariozechner/pi-coding-agent";
import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { appendFileSync, constants, readFileSync } from "fs";
import { access, readFile } from "fs/promises";
import { constants, readFileSync } from "fs";
import { access, appendFile, readFile } from "fs/promises";
import { join, resolve } from "path";
const LOG_FILE = join(getAgentDir(), "read-access.log");
@@ -44,14 +44,16 @@ function isBlockedPath(path: string): boolean {
return BLOCKED_PATTERNS.some((pattern) => pattern.test(path));
}
function logAccess(path: string, allowed: boolean, reason?: string) {
async function logAccess(path: string, allowed: boolean, reason?: string) {
const timestamp = new Date().toISOString();
const status = allowed ? "ALLOWED" : "BLOCKED";
const msg = reason ? ` (${reason})` : "";
const line = `[${timestamp}] ${status}: ${path}${msg}\n`;
try {
appendFileSync(LOG_FILE, line);
await withFileMutationQueue(LOG_FILE, async () => {
await appendFile(LOG_FILE, line);
});
} catch {
// Ignore logging errors
}
@@ -77,7 +79,7 @@ export default function (pi: ExtensionAPI) {
// Check if path is blocked
if (isBlockedPath(absolutePath)) {
logAccess(absolutePath, false, "matches blocked pattern");
await logAccess(absolutePath, false, "matches blocked pattern");
return {
content: [
{
@@ -90,7 +92,7 @@ export default function (pi: ExtensionAPI) {
}
// Log allowed access
logAccess(absolutePath, true);
await logAccess(absolutePath, true);
// Perform the actual read (simplified implementation)
try {

View File

@@ -14,6 +14,7 @@
* built-in `grep` tool in src/core/tools/grep.ts for a more complete implementation.
*/
import { mkdtemp, writeFile } from "node:fs/promises";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import {
DEFAULT_MAX_BYTES,
@@ -21,11 +22,11 @@ import {
formatSize,
type TruncationResult,
truncateHead,
withFileMutationQueue,
} from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { execSync } from "child_process";
import { mkdtempSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
@@ -108,9 +109,11 @@ export default function (pi: ExtensionAPI) {
if (truncation.truncated) {
// Save full output to a temp file so LLM can access it if needed
const tempDir = mkdtempSync(join(tmpdir(), "pi-rg-"));
const tempDir = await mkdtemp(join(tmpdir(), "pi-rg-"));
const tempFile = join(tempDir, "output.txt");
writeFileSync(tempFile, output);
await withFileMutationQueue(tempFile, async () => {
await writeFile(tempFile, output, "utf8");
});
details.truncation = truncation;
details.fullOutputPath = tempFile;