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. |
| 5 | import fs from 'node:fs'; |
| 6 | import { open } from './harness.mjs'; |
| 7 | |
| 8 | const 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(). |
| 11 | const 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. |
| 25 | const 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 | |
| 69 | const 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 | }; |
| 76 | fs.writeFileSync('dev/results/conflict.json', JSON.stringify(findings, null, 2)); |
| 77 | console.log(JSON.stringify(findings, null, 2)); |
| 78 | await s.close(); |