fix(coding-agent): queue file mutations across edit and write
closes #2327
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user