Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/wf_conflict.mjs

4.0 KiB, 1 run

created by r2519314175:839, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// Workflow 4 — the two-agents-one-file experiment (the roadmap question).
2// Drives the app's REAL file tools (run_tool) directly to characterise what
3// happens when a second writer changes a file underneath the first. No LLM
4// needed — the tools are the thing under test — so this run is free.
5import fs from 'node:fs';
6import { open } from './harness.mjs';
7
8const s = await open({ name: 'wf-conflict', connect: false });
9// The file tools need an app instance; a chat's app or the diamond app. Easiest:
10// use the workspace's own tool runner, which daimond.js exposes as tools().
11const run = (name, args) => s.page.evaluate(
12 ([n, a]) => window.__daimondTools ? window.__daimondTools(n, a) :
13 // fall back to the Files panel's own runner via a fresh DaimondApp
14 (async () => {
15 // tools() isn't on window; drive via a throwaway app created like the workspace does
16 const mod = window.__daimondApp;
17 throw new Error('no tool runner exposed');
18 })(),
19 [name, JSON.stringify(args)]
20).catch(e => 'ERR:' + e.message);
21
22// The app doesn't expose tools() on window, so exercise the same OPFS primitives
23// the tools use, matching file_write (overwrite) and file_edit (exact replace)
24// semantics exactly, to characterise the behaviour deterministically.
25const result = await s.page.evaluate(async () => {
26 const root = await navigator.storage.getDirectory();
27 const write = async (p, c) => { const fh = await root.getFileHandle(p, { create: true }); const w = await fh.createWritable(); await w.write(c); await w.close(); };
28 const read = async (p) => { const fh = await root.getFileHandle(p); return await (await fh.getFile()).text(); };
29 // file_edit semantics: exact unique substring replace, else "not found".
30 const edit = async (p, oldS, newS) => {
31 const cur = await read(p);
32 const i = cur.indexOf(oldS);
33 if (i === -1) return { ok: false, reason: 'old_string not found' };
34 if (cur.indexOf(oldS, i + 1) !== -1) return { ok: false, reason: 'old_string not unique' };
35 await write(p, cur.slice(0, i) + newS + cur.slice(i + oldS.length));
36 return { ok: true };
37 };
38
39 const out = {};
40 const FILE = 'race.txt';
41 await write(FILE, 'alpha\nbravo\ncharlie\ndelta\n');
42
43 // Agent A reads it (holds a stale copy). Agent B rewrites it behind A's back.
44 const aStale = await read(FILE);
45 await write(FILE, 'alpha\nBRAVO-B\ncharlie\nDELTA-B\n'); // agent B's edit
46
47 // (1) Agent A edits a region B did NOT touch, anchor still present.
48 const e1 = await edit(FILE, 'charlie', 'CHARLIE-A');
49 out.editDisjointRegion = { ...e1, fileAfter: await read(FILE) };
50
51 // Reset for the next case.
52 await write(FILE, 'alpha\nbravo\ncharlie\ndelta\n');
53 await read(FILE); // A reads
54 await write(FILE, 'alpha\nbravo\nGONE\ndelta\n'); // B destroys A's anchor
55 // (2) Agent A edits anchoring on text B destroyed.
56 const e2 = await edit(FILE, 'charlie', 'CHARLIE-A');
57 out.editDestroyedAnchor = { ...e2, fileAfter: await read(FILE) };
58
59 // Reset. (3) Agent A does a whole-file WRITE from its stale copy.
60 await write(FILE, 'alpha\nbravo\ncharlie\ndelta\n');
61 const stale = await read(FILE); // A's stale copy
62 await write(FILE, 'alpha\nBRAVO-B\ncharlie\nDELTA-B\n'); // B's edit
63 await write(FILE, stale); // A writes stale copy back
64 out.writeFromStale = { fileAfter: await read(FILE), bDataSurvived: (await read(FILE)).includes('BRAVO-B') };
65
66 return out;
67});
68
69const findings = {
70 workflow: 'file-conflict', llmCost: 0,
71 editDisjointRegion_merged: result.editDisjointRegion.ok && result.editDisjointRegion.fileAfter.includes('CHARLIE-A') && result.editDisjointRegion.fileAfter.includes('BRAVO-B'),
72 editDestroyedAnchor_failedSafe: !result.editDestroyedAnchor.ok,
73 writeFromStale_clobberedSilently: !result.writeFromStale.bDataSurvived,
74 raw: result,
75};
76fs.writeFileSync('dev/results/conflict.json', JSON.stringify(findings, null, 2));
77console.log(JSON.stringify(findings, null, 2));
78await s.close();