Status: In production. The qwr machine has been online since 2026-06, continuously hardened through 2026-08-01 (F2 _qgc bidirectional barrier fix, F4 _qgc spawn path integration).
0. Scenario: Three Panels Editing the Same File Concurrently
Timeline →
Panel Left (p0) AI Agent → reads foo.js → decides to edit L10-L20 → edit_file(foo.js, ...)
Panel Mid (p1) AI Agent → reads foo.js → decides to edit L30-L40 → edit_file(foo.js, ...)
Panel Right (p2) AI Agent → reads foo.js → decides to edit L50-L60 → edit_file(foo.js, ...)
Three edit_file IPC calls arrive at the main process almost simultaneously.
The question: Who succeeds? Who fails? Is data lost on failure? Can Timeline recover it? How strong is the foundation, really?
Answer upfront: The first succeeds. The other two fail CAS → AI auto re-reads and retries → Timeline archives everything. Five layers of defense. Zero data loss. Zero dirty writes.
1. First Line of Defense: Per-File Serial Queue (qwr Layer ①)
Location: shell/ipc-state.ts — _qw Map + _qe() function.
_qw: Map<filePath, Promise chain>
_qe(filePath, fn):
prev = _qw.get(filePath) || Promise.resolve()
p = prev.then(() => _ac.then(fn))
_qw.set(filePath, p)
return p
Core mechanism: Same file serialized, different files parallelized.
Three panels' edit_file(foo.js) arrive at the main process → all enter _qe('foo.js', fn).
- Panel A's Promise starts immediately (queue empty, prev=resolved)
- Panel B's Promise chains after A (prev = A's Promise)
- Panel C's Promise chains after B (prev = B's Promise)
Result: Strict FIFO execution. Each write completes and hits disk before the next one begins.
2. Second Line of Defense: Global Command Barrier (qwr Layer ②)
Location: shell/ipc-state.ts — _co / _ac bidirectional gate.
_co: Promise chain (command side — a command waits for the previous command to finish)
_ac: Promise (write side — a write waits for all pending writes + current command)
_qgc(): registers a command → returns release function
Sets _ac = waitWrites.then(() => p)
— where waitWrites = Promise.all(all currently pending writes)
— p only resolves when release() is called
— therefore _ac stays pending until all pending writes + command complete
_qe(): every write operation internally awaits _ac → waits for _ac to resolve before writing
Bidirectional semantics:
| Direction | Implementation | Effect |
|---|---|---|
| Command waits for writes | _co = prev.then(() => p), p waits for waitWrites before release | Before command executes, all pending writes are on disk |
| Writes wait for command | _qe internally await _ac, _ac waits for waitWrites then p | During command execution, all newly arriving writes queue up |
Why must _ac wait for p? (2026-08-01 F2 fix)
Old implementation: _ac = waitWrites.then(() => { }) — only waited for "pending writes at the moment _qgc was called." New writes arriving during command execution saw _ac already resolved and passed straight through. The bidirectional gate was only half-implemented.
Fix: _ac = waitWrites.then(() => p) — first wait for pending writes, then wait for command completion. During command execution, all new _qe calls queue up. Bidirectional semantics now complete.
Why is this needed? Consider: Panel Mid executes run_command (e.g., npm run build) while Panel Left does edit_file on a source file. If the command reads source files before the write hits disk, the build sees stale code → compiled output mismatches source. The command barrier eliminates this race window.
Current integration status: _qgc is fully defined in ipc-state.ts (with 2026-08-01 F2 semantic fix) and integrated into the qz-spawn.ts spawn IPC handler as of 2026-08-01 F4. run_command via the qqqide:qz:spawn channel is automatically protected by the command barrier: before execution, wait for all pending writes to land on disk; during execution, block all newly arriving writes. The ghrun direct spawn path is not yet integrated (it's a separate channel, not triggered by AI tools, with no barrier requirement).
3. Third Line of Defense: edit_file CAS Re-read (qwr Layer ③)
Location: shell/ipc-edit.ts — inside registerEditIpc.
_qe(args.path, async () => {
// ★ CAS check
const snap = _sn[args.path];
if (snap) {
const st = await fs.promises.stat(args.path);
if (st.mtimeMs !== snap.mtimeMs || st.size !== snap.size) {
return 'Error: file has been modified externally since last read.
Please re-read the file and try again.';
}
}
// ... execute edits
});
Scenario replay:
T0: Panel Left reads foo.js → _sn['foo.js'] = {mtime: 1000, size: 5000}
T1: Panel Mid reads foo.js → _sn['foo.js'] = {mtime: 1000, size: 5000} (overwrites)
T2: Panel Right reads foo.js → _sn['foo.js'] = {mtime: 1000, size: 5000} (overwrites)
T3: Panel Left edit_file → enters queue → CAS: mtime=1000 ✅ → write succeeds
→ _sn['foo.js'] = {mtime: 2000, size: 5100} (updated)
T4: Panel Mid edit_file → enters queue (waits for Left) → CAS: mtime=2000 ≠ 1000 ❌
→ returns "file has been modified externally since last read"
T5: Panel Right edit_file → same ❌
Key point: Only one panel's write passes the CAS check. Subsequent panels receive a clear error — no silent overwrites.
Why not locks? CAS is stateless optimistic concurrency — no acquire/release/timeout/deadlock detection needed. Just verify before execution: "Is the version I read still the latest?" This is the classic MVCC pattern from databases.
4. Fourth Line of Defense: External Modification Detection (qwr Layer ④)
Location: shell/ipc-fs.ts — read_file records snapshots + write_file validates.
read_file → _sn[path] = {mtimeMs: st.mtimeMs, size: st.size}
write_file → reads _sn[path]
→ stats disk
→ mismatch → rejects: "file has been modified externally since last read"
Why does _sn have only one slot?
_sn is Record<path, {mtimeMs, size}> — one record per file. Three panels share the same main process; _sn is a process-level singleton. The last read_file overwrites the previous snapshot.
This is intentional:
- Panel Left reads foo.js at T0, Panel Mid also reads foo.js at T2,
_snstores T2's snapshot - Panel Left attempts edit_file at T5 → CAS based on T2's snapshot (not T0) → if Panel Mid modified the file at T3, Panel Left gets CAS failure
- Panel Left doesn't know who modified it (snapshot was overwritten), but it knows "someone did" → rejects the write
Cost: Possible "false positive" — Panel Left's edit gets rejected even though the actual modification was by Panel Left itself (Panel Mid only read, didn't write). But this is safe: better to reject a legitimate write than accept an illegitimate overwrite. The AI receives the error, re-reads the file, and retries — cost is one extra API call.
5. Fifth Line of Defense: Snapshot Self-Update + Auto Syntax Gate (qwr Layer ⑤)
edit_file/create_file/write_file on success →
① stat new file → _sn[path] = {new mtime, new size}
② checkSyntaxSync(path, originalContent) → syntax invalid → revert file + report error
delete_file on success →
delete _sn[path]
Auto Syntax Gate (§59):
checkSyntaxSync:
.js → new vm.Script(content) // zero spawn, V8 in-process parse
.mjs → new Function(content)
.json → JSON.parse(content)
fail → fs.writeFileSync(path, originalContent) // revert
→ return syntax error description
Key: The syntax gate sits inside the qwr queue, after CAS. An edit passes CAS, writes to disk, but if it produces invalid JS → file is reverted → _sn rolls back → subsequent queued edits are not affected by this invalid write.
6. Timeline's Role: Not Conflict Prevention — Zero-Loss Audit Trail
The five-layer defense ensures only one write succeeds at a time. Timeline solves a different problem: successful writes are never lost.
Timeline storage:
blobs/{sha256[:2]}/{sha256}.gz — immutable content (SHA256-addressed, never deleted)
timeline.db — SQLite full index
timeline.wal — NDJSON incremental log (≤99 lines)
timeline.db.bak — index backup
In the three-panel concurrent scenario:
- Panel Left's write passes CAS → succeeds → Hook Q captures before/after →
_a4PersistToTimeline→ SHA256 blob + DB entry - Panel Mid's write fails CAS → never reaches Timeline (never wrote to disk)
- Panel Mid's AI receives error → re-reads file (now reading Left's modified version) → re-edits → CAS passes → Timeline records second version
Timeline's immutability guarantee: Even if Panel Left and Panel Mid alternate modifying the same file 100 times, Timeline has 100 blobs — none lost. SHA256 content addressing means two identical writes auto-deduplicate (same blob_hash), but the DB records two entries with different timestamps.
7. ghrun vs. qwr: Relationship
ghrun (Rust process manager)'s queue is a process lifecycle management queue, not a file write queue.
ghrun manages:
- spawning child processes (timeout/memory/JobObject/watchdog)
- process group isolation (parent dies → children die)
- LSP daemon (frozen)
qwr manages:
- per-file Promise serial queue (Layer ①)
- global command barrier (Layer ②)
- CAS snapshots (Layers ③④⑤)
The two are orthogonal: ghrun manages "is the process alive?", qwr manages "was the file written? any conflicts?"
run_command involves both: the command is spawned via ghrun, and the command's filesystem effects are protected by qwr. This is why _qgc should govern qz:spawn / ghrun:exec / engine:invoke — ensuring all writes are on disk before command execution, and all writes are blocked during command execution.
8. Defense Strength Assessment
Currently Achieved ✅
| Threat | Defense | Strength | Status |
|---|---|---|---|
| Concurrent writes to same file → dirty data | Layer ① per-file queue | ✅ Fully resolved | In production |
| Write overwrites read → silent data loss | Layer ③ CAS re-read | ✅ Fully resolved | In production |
| External editor modifies file → IDE unaware | Layer ④ external modification detection | ✅ Fully resolved | In production |
| Edit produces syntax error → file corruption | Layer ⑤ auto syntax gate | ✅ Fully resolved | In production |
| Command+write race → reads stale data | Layer ② command barrier | ✅ Fully resolved | In production (F4 spawn integration) |
| AI overwrites AI → no awareness | AI receives error → auto re-read → retry | ✅ Working correctly | In production |
Areas for Enhancement 🟡
1. _qgc spawn path integration ✅ Completed (2026-08-01 F4)
Integrated into the qqqide:qz:spawn handler: before spawn, _qgc() registers → waits for all pending writes to land on disk → executes command → release() unblocks. run_command is automatically protected by the barrier. The ghrun direct spawn path is not integrated (non-AI-tool channel, no barrier requirement).
2. Line-level CAS — reduce false positives 🔮 Long-term
Current CAS granularity is per-file {mtime, size}. Non-overlapping edits (Panel Left edits L10, Panel Mid edits L50) still trigger false positives.
Theoretically, we could use Timeline diffs to auto-detect whether edit regions conflict: no conflict → auto-merge, conflict → return diff summary for AI semantic merge.
Assessment: Not worth doing right now. The cost of a false positive is one extra API call (AI re-reads and retries). Implementing line-level CAS requires: a diff parser, edit region extraction, conflict matrix logic, and auto-merge logic. High complexity, low payoff. False positives are rare in practice (three panels simultaneously editing the same file is already uncommon). Revisit if real pain emerges.
3. Cross-process CAS 🔒 Not needed
Currently all panels are in the same Electron window; _qw and _sn are in-process memory structures. Cross-window access to the same project is already prevented by _qqq/.lockk (30s heartbeat / 60s expiry) — application-level solution, zero extra complexity.
Assessment: Not needed. OS-level file locks (flock/LockFileEx) introduce platform differences, stale lock recovery on crash, and permission issues — all to solve a problem already covered by the application-level lock. Cost exceeds benefit.
9. Conclusion
When three panels edit the same file concurrently:
- Three
edit_fileIPC calls arrive at the main process → per-file Promise queue strictly serializes them - First write: CAS check passes → lands on disk → syntax gate passes →
_snupdated - Second and third writes: CAS check fails (mtime changed) → returns "modified externally" → AI receives error → auto re-reads file → retries edit on new version
- Timeline: every successful write produces an immutable blob → zero data loss
- ghrun: not directly in the write path, but the command barrier (
_qgc) coordinates write vs. command timing
Defense rating: All five layers are in production (①②③④⑤). Under the current architecture, there is no possible scenario where "two panels simultaneously overwrite the same file." The worst case is the second panel's AI retrying after receiving an error — costing one extra API call.
Comparison with git: git's concurrency model is "optimistic write + conflict detection + manual merge." Our model is "optimistic CAS + auto retry." The difference: git assumes a human makes the merge decision; we assume the AI re-reads, re-thinks, and re-generates the edit after semantic understanding. For AI, "re-read + re-think + re-write" is more reliable than "parse diff conflict markers."
Appendix: Full Call Chain (Three-Panel edit_file Complete Path)
┌─ Panel Left iframe ───────────────────────────────────────────────┐
│ AI Agent calls edit_file({path:'foo.js', edits:[...]}) │
│ → tools-exec.js executeTool('edit_file', args) │
│ → panel-a4.js _a4WrappedExecuteTool() │
│ → reads before content → _a4EnsureBeforeBaseline (writes │
│ timeline) │
│ → bridge.ai.edit_file(args) ──────────────────────IPC──────────┐│
└───────────────────────────────────────────────────────────────────┘│
│
┌─ Panel Mid iframe ─────────────────────────────────────────────────┐│
│ AI Agent calls edit_file({path:'foo.js', edits:[...]}) ││
│ → ... same ... → bridge.ai.edit_file(args) ────────IPC────────────┐││
└────────────────────────────────────────────────────────────────────┘││
││
┌─ Panel Right iframe ───────────────────────────────────────────────┐││
│ AI Agent calls edit_file({path:'foo.js', edits:[...]}) │││
│ → ... same ... → bridge.ai.edit_file(args) ────────IPC────────────┐│││
└────────────────────────────────────────────────────────────────────┘│││
│││
╔═══════════════════ Electron Main Process ══════════════════════════╗ │││
║ ║ │││
║ ipcMain.handle('qqqide:ai:edit_file') ←── 3 IPC calls arrive ║ │││
║ almost simultaneously ║ │││
║ ║ │││
║ _qe('foo.js', fn) ──────────────────── qwr Layer ① serial queue ║ │││
║ │ ║ │││
║ ├─ Panel A's fn(): ║ │││
║ │ await _ac ──────────────────── qwr Layer ② command barrier ║ │││
║ │ CAS: _sn['foo.js'] OK ─────── qwr Layer ③ CAS re-read ║ │││
║ │ read original → Pass1 match → Pass2 replace → writeFile ║ │││
║ │ checkSyntaxSync(foo.js) OK ── qwr Layer ⑤ auto syntax gate ║ │││
║ │ _sn['foo.js'] = new snapshot ─ qwr Layer ⑤ snapshot update ║ │││
║ │ → returns "✓ 1 edit applied" ║ │││
║ │ ║ │││
║ ├─ Panel B's fn() (waits for A): ║ │││
║ │ await _ac ║ │││
║ │ CAS: _sn['foo.js'] mtime mismatch ❌ ║ │││
║ │ → returns "Error: file has been modified externally" ║ │││
║ │ ║ │││
║ └─ Panel C's fn() (waits for B): ║ │││
║ same → CAS fails ❌ ║ │││
║ ║ │││
╚═════════════════════════════════════════════════════════════════════╝ │││
│││
Panel B/C receive error → AI re-reads file → retries on new version │││
│││
╔═══════════════════ Timeline (async) ════════════════════════════════╗ │││
║ After Panel A's successful write: ║ │││
║ _a4PersistToTimeline('foo.js', before, after) ║ │││
║ → SHA256(after) → gzip → blobs/{sha}.gz ║ │││
║ → INSERT INTO versions (file_path, blob_hash, ...) ║ │││
║ → append timeline.wal ║ │││
║ ║ │││
║ After Panel B's successful retry: ║ │││
║ same → another blob → another DB entry → complete history ║ │││
╚════════════════════════════════════════════════════════════════════╝
Last updated: 2026-08-01 F4. F2: _qgc bidirectional barrier semantic fix. F4: _qgc spawn path integration. All five layers in production. The qwr five-layer defense system has recorded zero incidents of "data loss due to concurrent writes" since its 2026-06 launch.