Skip to content

Latest commit

 

History

History
146 lines (111 loc) · 8.78 KB

File metadata and controls

146 lines (111 loc) · 8.78 KB

Architecture

BrainSync Universal is intentionally one source file, no build step, no transpiler. This document explains the moving parts so you can find your way around src/extension.js.

High level

                    ┌──────────────────────────────────────────────────────┐
                    │  IDE host (VS Code / Kiro / Windsurf / Cursor / ...) │
                    └─────────────┬───────────────────────────┬────────────┘
                                  │                           │
                          activates extension          file watchers
                                  │                           │
                                  ▼                           ▼
                          BrainSyncController       schedule auto-sync
                                  │
            ┌─────────────────────┼──────────────────────┐
            ▼                     ▼                      ▼
   BrainSyncStore         BrainSyncDashboard       Watchers
   (memory.jsonl ops)     (webview UI)             (debounced triggers)
            │
            ▼
   Per-workspace `.brainsync/`            ~/.brainsync-vault/projects/<key>/
   memory.jsonl     ◀──────── mirror ───────► memory.jsonl
   shadows/                                   latest-context.md
   sync-state.json
   backups/

Five core concepts

1. Memory entries

Every captured note, conversation, rule, decision, lesson, or imported chunk lives as one line of newline-delimited JSON in .brainsync/memory.jsonl. The shape is MemoryEntry, fields documented at the top of extension.js. Entries are deduped by ${projectKey}:${kind}:${hash} where hash = sha256(projectKey || kind || title || content). The newer updatedAt wins on dedup.

2. Two log files, one logical store

The canonical log is the workspace log at <workspace>/.brainsync/memory.jsonl. On every read, BrainSync also reads the vault log at ~/.brainsync-vault/projects/<project-key>/memory.jsonl and merges them. Writes go to both. The vault is what lets the same repo opened in different IDEs converge: the project key is derived from the git remote URL (or workspace path fallback), so all hosts hit the same vault folder.

3. Shadow-based delta extraction

Every time BrainSync writes to an export target (e.g., .windsurfrules), it stores a byte-exact copy in .brainsync/shadows/<target-flattened>. On the next sync, we diff currentFile vs shadow. Anything novel in currentFile is content the IDE agent added — we extract it, ingest it as memory, then re-render the export with everything included. This is what makes "Windsurf agent edits .windsurfrules, Kiro picks it up next session" work without complex parsing.

The diff is line-based and skips known boilerplate markers (frontmatter, BrainSync section headers, etc.) so we don't false-positive on our own template.

4. Loop-safe auto-sync

A naive watcher would chase its own tail: write .windsurfrules → watcher fires → trigger import → trigger export → write .windsurfrules → repeat forever. Three guards prevent that:

  • lastWriteTime cooldown — when BrainSync writes, we record the time. The watcher checks Date.now() - lastWriteTime < 1500ms and ignores changes inside that window.
  • No-op hash skip — if the rendered context hash matches the previous export and no entries were rescued from shadows, the entire export phase is skipped.
  • Adaptive debounce — base 800ms, backs off to 4s after 3 consecutive no-ops, 8s after 6+. Resets to 800ms on real change. This keeps idle sync near-zero CPU while staying responsive.

5. mtime-based incremental import

.brainsync/sync-state.json records { mtimeMs, size } per file the importer touches. On the next pass, files whose stat hasn't changed are skipped before any read happens. This is the single biggest performance win on large workspaces.

The sync cycle (exportAgentFiles())

PHASE 1 — RESCUE
  for each export target:
    read current file
    read shadow
    delta = diff(current, shadow)
    if delta has novel content:
      ingest as memory entries

PHASE 2 — BUILD
  entries = readAllEntries()                  // workspace + vault, deduped, ranked
  contextText = buildContext(entries)         // markdown sections by kind
  if hash(contextText) == lastExportHash and no rescues:
    return noop

PHASE 3 — WRITE
  for each export target:
    next = targetSpecificContext(target, contextText)   // wraps with frontmatter for .mdc / kiro
    if existing == next: ensure shadow exists, continue
    backup existing → .brainsync/backups/<timestamp>/
    write next atomically
    write shadow := next
    update sync-state mtime

  update sync-state.lastExportHash
  notify dashboard

writeTextAtomic writes to a .tmp file in the same directory then renames. On Windows, where rename can fail with EPERM if a watcher has the target open, it falls back through vscode.workspace.fs.writeFilefs.copyFile → direct fs.writeFile. The cleanup of the tmp file happens in a finally block.

IDE conversation imports

Beyond rule files, BrainSync ingests "off-disk" chat sources where the IDE writes plaintext:

Source Format What we extract
Antigravity Brain (~/.gemini/antigravity/brain/) Plain .txt / .md / .json Per-session overview log (*.system_generated/logs/overview.txt) and standalone artifacts that mention the project.
Windsurf Cascade NDJSON (~/AppData/Roaming/Windsurf/User/acp-events/*.ndjson) NDJSON event stream Stitched user/agent message chunks per session, filtered to ones mentioning the project.
Windsurf Cascade SQLite (state.vscdbwindsurf.acp.metadataCache) SQLite TEXT cell with JSON Per-session metadata (title, status, timestamps, cwd) for sessions whose workspace matches the project.
Windsurf Cascade .pb files Encrypted protobuf Cannot read. Confirmed by entropy analysis (~37% printable, no readable runs ≥ 20 chars). The metadata reader above is the workaround.

For sources we can't read, the workflow is "select chat text in the IDE → run BrainSync Universal: Capture Clipboard as Conversation". One keystroke gets the actual content into BrainSync without trying to decrypt anything.

SQLite without a native dependency

To read Windsurf's state.vscdb, BrainSync ships a vendored copy of sql.js under vendor/sql.js/ (only sql-wasm.js and sql-wasm.wasm, ~700KB combined). This is a pure WASM build of SQLite that runs in any Node context. We chose this over better-sqlite3 because:

  • No native compile step — works inside an Electron-based extension host without ABI matching.
  • One file (the WASM) plus one tiny loader. Easy to vendor.
  • BrainSync only does point lookups; performance isn't the bottleneck.

The loader is lazy — loadSqlJs() only initializes when a Windsurf metadata read is actually requested.

File layout

src/
  extension.js                 # everything: store, sync, importers, watchers, UI
vendor/
  sql.js/
    sql-wasm.js                # ~46KB
    sql-wasm.wasm              # ~644KB
    package.json               # upstream metadata + license
    LICENSE                    # MIT
resources/
  brainsync.svg                # activity-bar icon
docs/
  ARCHITECTURE.md              # this file
  PUBLISHING.md                # marketplace + Open VSX walkthrough
scripts/
  build-vsix.ps1               # convenience wrapper around vsce
package.json                   # extension manifest
README.md
CHANGELOG.md
CONTRIBUTING.md
LICENSE

extension.js is intentionally a single file. Splitting it across modules would add a build step and not help readability much — the file is ~2200 lines of straight-line code with each section commented. If you find a section growing beyond ~600 lines, that's the right time to extract.

What's deliberately not here

  • No transpiler / bundler. Plain ES2022, runs as-is.
  • No test framework yet. Smoke testing is "load the VSIX, look at the rule files, run a few captures." If you contribute one, prefer Node node:test over Jest/vitest to keep the dependency tree small.
  • No telemetry. Not optional, not opt-in, not "anonymous." None.
  • No outbound network at runtime. The only network calls in the project are at publish time (vsce publish, ovsx publish).
  • No vector store / embeddings. Search is keyword-scored. If you need semantic, plug your own retriever in alongside; don't replace the canonical store.