The qwr Machine — Five-Layer Defense Against Concurrent AI Code Edits

文档发表:2026-06-15 · 阅读:57 · 更新:2026-08-01

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:

DirectionImplementationEffect
Command waits for writes_co = prev.then(() => p), p waits for waitWrites before releaseBefore command executes, all pending writes are on disk
Writes wait for command_qe internally await _ac, _ac waits for waitWrites then pDuring 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.tsread_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, _sn stores 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 ✅

ThreatDefenseStrengthStatus
Concurrent writes to same file → dirty dataLayer ① per-file queue✅ Fully resolvedIn production
Write overwrites read → silent data lossLayer ③ CAS re-read✅ Fully resolvedIn production
External editor modifies file → IDE unawareLayer ④ external modification detection✅ Fully resolvedIn production
Edit produces syntax error → file corruptionLayer ⑤ auto syntax gate✅ Fully resolvedIn production
Command+write race → reads stale dataLayer ② command barrier✅ Fully resolvedIn production (F4 spawn integration)
AI overwrites AI → no awarenessAI receives error → auto re-read → retry✅ Working correctlyIn 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:

  1. Three edit_file IPC calls arrive at the main process → per-file Promise queue strictly serializes them
  2. First write: CAS check passes → lands on disk → syntax gate passes → _sn updated
  3. Second and third writes: CAS check fails (mtime changed) → returns "modified externally" → AI receives error → auto re-reads file → retries edit on new version
  4. Timeline: every successful write produces an immutable blob → zero data loss
  5. 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.