Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions server/src/demos/concurrency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ async function testCAS() {
);
check("el mensaje instruye al modelo qué hacer", !!stale?.message.includes("read_file"));
check("el archivo NO se pisó", (await readFile(f, "utf8")) === "cambio-1");

// Y si el que chocó consigo mismo es el MISMO agente, se le dice así.
//
// El bug que cubre: el mensaje decía "lo modificó agente-1" sin aclarar que
// agente-1 era él. El agente concluía que había otro trabajando, se ponía a
// esperar y releía el archivo una y otra vez por un compañero inexistente.
// Visto en una sesión real: cuatro relecturas y un `sleep 5` para nada.
let propio: StaleContentError | null = null;
try {
await fileMutation.writeIfUnchanged({
path: f,
content: "cambio-3",
expected: "original", // viejo otra vez, pero ahora lo escribió él mismo
agentId: "agente-1",
});
} catch (e) {
propio = e instanceof StaleContentError ? e : null;
}
check("rechaza igual si chocó consigo mismo", propio !== null);
check(
"y le dice que fue ÉL, no otro agente",
!!propio?.message.includes("TÚ") && !!propio?.message.includes("nadie más"),
propio?.message ?? "",
);
}

async function testParallelDistinctFiles() {
Expand Down
28 changes: 23 additions & 5 deletions server/src/engine/file-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,36 @@ export class StaleContentError extends Error {
readonly path: string,
/** Quién lo tocó y hace cuánto, si se sabe (registro efímero). */
readonly lastWriter?: { agentId: string; agoMs: number },
/** Quién está intentando escribir ahora, para no atribuirle a otro lo suyo. */
quienEscribe?: string,
) {
super(StaleContentError.buildMessage(path, lastWriter));
super(StaleContentError.buildMessage(path, lastWriter, quienEscribe));
this.name = "StaleContentError";
}

/**
* El mensaje va AL MODELO, no al humano: tiene que decirle qué hacer.
* Con atribución fresca es más útil; sin ella degrada pero sigue siendo accionable.
*
* Y si el último escritor fue ÉL MISMO, se le dice así de claro. Antes leía
* "lo modificó agente-1 hace 3s" sin saber que agente-1 era él, concluía que
* había alguien más trabajando, y se ponía a esperar y a releer el archivo una
* y otra vez por un compañero que no existía. Visto en una sesión real: cuatro
* relecturas y un `sleep 5` para nada.
*/
private static buildMessage(path: string, w?: { agentId: string; agoMs: number }): string {
const quien = w ? ` Lo modificó ${w.agentId} hace ${Math.round(w.agoMs / 1000)}s.` : "";
return `El archivo ${path} cambió desde que lo leíste.${quien} Léelo otra vez (read_file) antes de editarlo, y vuelve a aplicar tu cambio sobre el contenido nuevo.`;
private static buildMessage(
path: string,
w?: { agentId: string; agoMs: number },
quienEscribe?: string,
): string {
if (!w) {
return `El archivo ${path} cambió desde que lo leíste. Léelo otra vez (read_file) antes de editarlo, y vuelve a aplicar tu cambio sobre el contenido nuevo.`;
}
const hace = Math.round(w.agoMs / 1000);
if (quienEscribe && w.agentId === quienEscribe) {
return `El archivo ${path} cambió desde que lo leíste: lo escribiste TÚ hace ${hace}s y te quedaste con la versión de antes. No hay nadie más trabajando en él. Léelo otra vez (read_file) y aplica tu cambio sobre lo que ahora hay.`;
}
return `El archivo ${path} cambió desde que lo leíste. Lo modificó ${w.agentId} hace ${hace}s. Léelo otra vez (read_file) antes de editarlo, y vuelve a aplicar tu cambio sobre el contenido nuevo.`;
}
}

Expand Down Expand Up @@ -78,7 +96,7 @@ export class FileMutation {
if (opts.expected !== undefined) {
const actual = existsSync(key) ? await readFile(key, "utf8") : null;
if (actual !== opts.expected) {
throw new StaleContentError(opts.path, this.writerInfo(key));
throw new StaleContentError(opts.path, this.writerInfo(key), opts.agentId);
}
}

Expand Down
Loading