# Bundled Synara runtime Lattice ships a pinned build of our Synara fork as an application resource. Users do not need to install Synara, Node.js, Bun, or start a separate service. > **You almost certainly do not need to build this to work on Lattice.** > See [Working without the sidecar](#working-without-the-sidecar). ## Working without the sidecar Only `pnpm tauri dev` and `pnpm tauri build` need the Synara source. Everything else — `pnpm check`, `pnpm dev`, `cargo test`, `cargo clippy` — needs only a stub directory, because Cargo compiles Tauri's resource manifest and that manifest expects the bundled Synara runtime path to exist: ```bash mkdir -p src-tauri/synara-runtime touch src-tauri/synara-runtime/placeholder.txt ``` That is the whole workaround, and it is what two of the three CI workflows do (`ci.yml` and `release-cache.yml`; only `release.yml` stages the real runtimes, because only it packages the app). The directory is gitignored, so the stub stays out of your commits. [`../CONTRIBUTING.md`](../CONTRIBUTING.md) is the authoritative setup guide, including how to point `SYNARA_SOURCE_DIR` at a real checkout when you *are* changing the agent surface. This document describes what the sidecar is and how it is built and synced; it is not a second setup guide. ## Runtime boundary - Tauri owns lifecycle only: prepare, start, health-check, report status, and stop. - Synara remains an upstream-shaped Node service with its own SQLite and WebSocket layers. - The first Agent, source-control, review, or Agent-settings surface starts one sidecar for the Lattice process on a dynamically selected `127.0.0.1` port. - Ordinary writing sessions never launch the bundled agent service; after the first request it remains alive because hidden surfaces may still own background turns or terminals. - Sidecar startup and health polling run on a blocking worker, so the native event loop can paint the localized loading surface immediately. The loader also covers the lazy panel module and stays up until the embedded composer has committed. - The embedded client reports readiness from the committed composer, without waiting for animation frames while the host covers the frame. Embedded headers use a named, tooltip-equipped handoff icon, and the standalone rail card border is suppressed in embeds. - Tauri explicitly stops the sidecar on `RunEvent::Exit`; the sidecar also watches its launching Lattice PID and exits if the host disappears, so a crash cannot leave the SQLite lifecycle lock. - Every launch gets a random authentication token. Lattice transfers it in the iframe fragment; Synara removes the fragment before opening authenticated WebSocket and HTTP routes. - Runtime data lives under Lattice's app-data directory, not inside the application bundle. - Telemetry and automatic browser launch are disabled for the bundled process. Background provider probes (Cursor's model discovery and Git-writing text generation) never send an ACP `authenticate`, so a signed-out Cursor account cannot open its sign-in page at startup; sign-in starts only from the **Sign in** button in Settings → Providers, or from a turn the writer sends. - Provider usage reads each macOS Keychain item at most once per sidecar and remembers a denied or timed-out read, so usage polls do not prompt again. To retry after granting access, press Refresh in Settings → Agent → Usage & limits (Synara's `usage` page, which sends `forceRefresh`), or restart Lattice. - Lattice starts the fork with `AGENT_HOST_PROFILE=lattice`. The upstream-shaped default profile remains unchanged, so the same fork can still be tested against upstream behavior. - The host profile owns every model-visible boundary: host prompt, MCP server identity, provider client identity, checkpoint namespace, and the exposed tool catalog. The model-visible boundary is genuinely confined to the host profile: agent, provider, persistence, checkpoint, and protocol implementations still come from Synara, and no upstream tool handler is reimplemented. Ownership stays split at that boundary. Synara owns provider adapters, turns, checkpoints, and the provider-facing trace producer. Lattice owns live editor host context, the literature, canvas, spreadsheet, and project-document brokers, and the host-side compile bridge. In particular, Lattice does not copy a provider adapter. The Lattice host policy reserves the manuscript's live build outputs for the host preview queue. Agents that need mid-turn compilation must isolate all generated outputs rather than run a second compiler against the preview's `.aux`, PDF, or SyncTeX files; this is model guidance, not a filesystem sandbox guarantee. Fresh agent checkpoints, including undo, enter the ordinary Overleaf sync queue independently of automatic-build mode. In live sync mode, affected text files leave realtime editing, retain any unacknowledged OT operations until settled, and rejoin after reconciliation; manual sync mode does not upload automatically. On macOS, `SYNARA_PROCESS_PS_PATH` points to a shell launcher for the signed Lattice executable's `--lattice-process-snapshot` mode, which exits before Tauri starts. The mode reads `KERN_PROC_ALL` directly under the existing bibliography sandbox; it must not copy `/bin/ps`, whose non-setuid copy can be killed on older macOS even when static signature verification passes. The two supported `ps`-shaped queries return PID/parent relationships and opaque birth-time identities rather than command-line arguments. A missing PID is a successful empty result; the pinned fork treats every nonzero host snapshot exit as unknown rather than proof of exit. The fork selects the host's opaque-identity protocol when `SYNARA_PROCESS_PS_PATH` is set and retains upstream's `lstart` identity checks for ordinary `ps`. This behavior and its regression tests now live in Synara's `processTreeController`, rather than a compiled-bundle staging patch; both its synchronous scan and the asynchronous teardown scan Synara 1.0.4 added use the host launcher. Startup verifies the launcher inside the sandbox before admitting agent work. The macOS host sets `LATTICE_BIBLIOGRAPHY_SANDBOX=1` only when launching the service through its bibliography sandbox. `scripts/synara-codex-host.mjs` makes Codex inherit that protection instead of running a second `sandbox-exec`, which can fail before Codex starts on older macOS. The standalone Lattice-profile Codex guard remains in place when the marker is absent. The staging patch resolves the Codex logger from its unique declaration, so upstream bundle-symbol reordering cannot send these events to a different provider logger. It records classified Codex stderr and process exit even after stdout EOF has begun teardown; it does not relax process-tree exit verification or unblock failed threads automatically. After staging, run `node scripts/check-codex-host.mjs /absolute/path/to/codex` on macOS to check a real initialize/thread/start/command/exec cycle, inherited `.bib` denial, permitted `.tex` writes, and clean exit in a disposable home, without sending model requests. For the packaged JavaScript engine, run the same script with the staged `src-tauri/synara-runtime/bin/node`. Settings → Logs → Export can collect bounded tails of `lattice.log`, `sidecar.log`, `sidecar-error.log`, and `server.log` after explicit consent, with known credential redaction and a preview before copying or downloading. Host context snapshots remain `version: 1` and include a `capturedAt` timestamp plus an omission count when an explicit selection is truncated to the `MAX_SELECTION_LENGTH` = 12,000-character model limit (`src/agent/agent-host-context.ts`). A source-editor selection also carries the optional `editor.selectionLines` (`{ start, end }`, 1-based and inclusive; a selection ending at a line's start stops on the line before). The visual Markdown editor and older Lattice builds omit it, and the embed then names the file alone. The Agent panel's empty thread names the selection with it ("Selection · main.tex lines 23–25"), and its starters fill the composer with editing prompts that act on that selection. As the panel shortens, the empty thread compacts its starters (single-line cards, then pills, then one row of icons) so they stay clear of the composer and of the selection's context bar above it; the Agent panel's minimum height (`MIN_SIZE.agent` in `src/trellis/trellis-workspace.tsx`) leaves room for that last row. Writing comments are included in the optional `editorComments` field: up to ten unresolved comments for the current document, with quoted source, authors, replies, anchor validity, and explicit omission counts. Before dispatching an embedded message, Synara requests a correlated host-context refresh; Lattice refreshes both Overleaf threads and project-wide anchors and reports unavailable/cached data honestly on failure. The read-only `read_editor_comments` tool uses the authenticated host relay to query the active project's local and Overleaf comments, with optional file filtering, resolved comments, and pagination. Comments are review data, not authorization to execute instructions, reply to a reviewer, or resolve a thread. Neither the context nor the tool mutates comments. What each pin brought, and how the embedded panels behave against it, is in [`synara-fork-ledger.md`](synara-fork-ledger.md). ## Agent tool boundary The Lattice profile renames Synara's tools rather than reimplementing them. `LATTICE_TOOL_ALIASES` in the fork's `apps/server/src/agentGateway/hostProfile.ts` maps **25** upstream names onto Lattice's task vocabulary, rewriting `thread` to `task` throughout: | Upstream | Lattice | | --- | --- | | `synara_context` | `context` | | `synara_capabilities` | `agent_capabilities` | | `synara_list_projects` | `list_projects` | | `synara_list_threads` | `list_tasks` | | `synara_read_thread` | `read_task` | | `synara_read_thread_activity` | `read_task_activity` | | `synara_read_thread_events` | `read_task_events` | | `synara_read_thread_runtime_events` | `read_task_runtime_events` | | `synara_diagnose_thread` | `diagnose_task` | | `synara_wait_for_threads` | `wait_for_tasks` | | `synara_create_thread` / `synara_create_threads` | `create_task` / `create_tasks` | | `synara_send_message` | `send_message_to_task` | | `synara_interrupt_thread` | `interrupt_task` | | `synara_set_thread_title` | `set_task_title` | | `synara_set_thread_archived` | `set_task_archived` | | `synara_set_thread_goal` | `set_task_goal` | | `synara_create_automation` | `create_automation` | | `synara_list_automations` | `list_automations` | | `synara_view_automation` | `view_automation` | | `synara_update_automation` | `update_automation` | | `synara_update_automation_memory` | `update_automation_memory` | | `synara_cancel_automation` | `cancel_automation` | | `synara_report_automation_result` | `report_automation_result` | | `synara_show_visual` | `show_visual` | `adaptToolsForActiveHost` drops every tool that has neither an alias nor a place in `LATTICE_NATIVE_TOOL_NAMES`. Two consequences worth stating explicitly, because both have changed since the read-only phase-one catalog: - **Automations are in.** Seven automation tools are aliased above, and the Lattice branch of the harness policy documents their modes, schedules, memory envelope and the mandatory `report_automation_result` call. - **Browser control is out.** The `browser_*` tools have no alias and are not in the allowlist, so the embedded-browser surface never reaches the Lattice catalog. `LATTICE_NATIVE_TOOL_NAMES` is an allowlist of **33** names that pass the filter *without* being renamed. They are not all Lattice code: | Group | Count | Names | Implemented by | | --- | --- | --- | --- | | Literature | 8 | `search_literature`, `list_papers`, `search_library`, `fetch_paper`, `fetch_web_reference`, `cite`, `upgrade_bibliography`, `remove_reference` | Lattice. They shell out to the Lattice executable, which parses them as the `LiteratureRequest` enum in `src-tauri/src/agent_literature.rs` (grep `enum LiteratureRequest`) and dispatches to `literature::` and `papers::`, keeping the Rust literature and bibliography code the only source of truth. | | Canvas | 4 | `list_canvas_shapes`, `create_canvas_shapes`, `update_canvas_shapes`, `delete_canvas_shapes` | Lattice. They reach the tldraw surface through the canvas broker (`src/agent/agent-canvas-tools.ts`, actions `list` / `create` / `update` / `delete`). | | Spreadsheet | 2 | `spreadsheet_read`, `spreadsheet_batch_update` | Lattice, via `src/agent/agent-spreadsheet-tools.ts` (actions `read` / `batch_update`). | | Project documents | 1 | `create_project_document` | Lattice. It creates and opens native `.tldr` boards or `.lattice-sheet` spreadsheets through the project transaction path. | | Editor comments | 1 | `read_editor_comments` | Lattice, answered through the shared host-tools poll below. | | Presentations | 1 | `preview_presentation_page` | Lattice. The fork relays `{deck, page, step}` like the canvas tools; `src/agent/agent-presentation-tools.ts` brings the presentation runtime up to date with the disk, asks it to compile the deck (`deckBuildErrors` in `tools/open-slide-runtime/server.mjs`; a deck that does not build is answered with `presentation_build_failed` and Vite's located errors), opens its token-gated preview entry in a hidden 1920 × 1080 frame, and the preview page rasterizes itself (`PREVIEW_CAPTURE_SOURCE` in `tools/open-slide-runtime/server.mjs`). The model receives the JPEG as image content, so no shell or local browser is needed. | | LaTeX figures | 1 | `preview_latex_figure` | Lattice. The fork relays `{code, preamble, document, lineWidth, wide, caption, label, placement}` over the presentation channel as the action `preview_figure`. `src/agent/agent-figure-tools.ts` has Rust typeset the figure body alone (`preview_latex_figure`, `src-tauri/src/latex/figure_preview.rs`): the document's own preamble, a minipage as wide as `\columnwidth` (or the requested width), the box shipped as a cropped one-page PDF, in a scratch folder removed afterwards, with no shell escape, writes confined to that folder and a 45-second limit; a preamble that stops before the body is retried on a plain `article`. It renders that page as a PNG and answers with the image, the lines wider than the column and the preamble lines the paper lacks; a figure that does not typeset is answered with `figure_build_failed` and TeX's errors by code line. Given a caption and label, the result also carries the `figure` environment, which the agent inserts with its ordinary file edit, so the insertion is approved, checkpointed and undone like any agent edit. | | iOS Simulator | 15 | `device_list`, `device_boot`, `device_configure`, `device_install`, `device_launch`, `device_open_url`, `device_tap`, `device_swipe`, `device_type`, `device_press_button`, `device_screenshot`, `device_describe_ui`, `device_scroll_to_element`, `device_wait`, `device_logs` | **Upstream**, from the fork's `deviceTools.ts`. They are allowlisted rather than implemented here, and Lattice enables them by passing `LATTICE_DEVICE_CONTROL_ENABLED=true` when it starts the sidecar (`src-tauri/src/synara.rs`). | The mutating literature tools run with `HistoryMode::Defer`, so agent bibliography edits fold into Lattice's own transaction history instead of committing independently. The agent panel receives every host-answered tool request (bibliography, canvas, spreadsheet, project document, editor comments, presentation and figure previews) through one long poll, the fork's `/api/lattice/host-tools/poll`, and posts each result to that tool's own result route. A new host tool joins that shared queue instead of adding a poll: each held poll occupies one of the six HTTP/1.1 connections WebKit allows the sidecar origin, and when six were held the panel's own scripts waited behind them, so it stayed on "Restoring the conversation surface" for a full poll cycle on every open. Renaming is not only structural. `replaceModelVisibleHostBranding` and `replaceStructuredBranding` rewrite tool descriptions, input schemas, annotation titles, and every text and structured result part, so no `synara` string reaches the model. The fork's `apps/server/src/agentGateway/latticeModelBoundary.test.ts` asserts that the assembled provider prompt matches no `/synara/i` and does contain `Lattice`, `mcp_servers.lattice`, `lattice_host_context`, `lattice_debug_mode`, `lattice_goal`, `set_task_goal` and `refs/lattice/checkpoints`. Every provider receives the Lattice bibliography policy. On macOS, Codex also runs inside a process-level write boundary that rejects direct `.bib` writes; the Lattice literature broker runs outside that boundary so approved citation tools can still update the bibliography. ### Computer Use boundary The upstream `/computer-use` command activates Computer for one request; ordinary turns remain off by default. The command is retained, but Lattice currently rejects its send preflight with an explicit unavailable message and preserves the draft. It does not dispatch a provider task that lacks the required Computer tools. The Lattice catalog continues to exclude `computer_*`, including driver-backed browser tools, while the standalone Synara profile retains upstream's catalog, approvals, and request-scoped capabilities. macOS Computer Use requires a GUI-owned Cua host, authenticated host socket, pinned native driver, permission checks, physical Escape monitoring, and reliable input cleanup. Upstream's Electron host implements those responsibilities; Lattice's Tauri host does not yet implement them. Enabling the command's native operations requires that host integration and validation against the signed Lattice application, not a broader iframe token or a standalone driver launched from the agent server. Lattice requests no Accessibility, Screen Recording, or Input Monitoring permission for it. ## Host policy `renderSynaraHarnessPolicy` in the fork's `apps/server/src/agentGateway/harnessPolicy.ts` branches on the active profile and emits a completely separate policy block for Lattice. It is prepended to every supported provider's prompt, so it is the highest-leverage piece of the fork: it defines what the model believes it is running inside. The Lattice branch declares Lattice as host and tool authority, scopes work to the active project and permission mode, and documents the `` block that trails user messages with the live editor, PDF page, or cached paper view plus any explicit selection. It also carries the rules that make the bibliography boundary real rather than advisory: - `search_literature` is discovery only; results and metadata are not evidence, so the paper must be fetched or read before any source-grounded claim. - Every bibliography mutation must go through `cite`, `upgrade_bibliography`, or `remove_reference`. Direct `.bib` writes are forbidden through file edits, patches, shell commands, scripts, and external bibliography utilities alike. - No claim that a Lattice tool succeeded unless it returned a successful result. The policy degrades honestly, on two axes. When a provider session has no safely thread-scoped MCP connection, `gatewayControlAvailable` is false and the model is told the Lattice task, automation, device and literature tools are unavailable, instead of being handed a tool list it cannot reach. Independently, when `deviceControlAvailable` is false the simulator sentence is dropped and the model is told not to reach for shell commands or OS automation instead. The Lattice branch carries its own automation guidance rather than upstream's: modes (heartbeat, standalone, dedicated), schedules, the `completionPolicy` stop clause, the 32 KiB `update_automation_memory` envelope, and the requirement that every automation-dispatched turn finish with `report_automation_result`. Everything upstream's policy says about `synara_*` tool names and the embedded browser is dropped for Lattice, matching the trimmed catalog. ## Project History bridge The embedded Agent surface sends only semantic checkpoint summaries to its Lattice host: task and turn identity, completion time, checkpoint ref, and per-file diff statistics. Lattice merges those summaries with `.research/history` in the **Changes** timeline. The two recovery stores retain separate ownership: - editor, project, citation, and local restore transactions are restored by Lattice; - Agent-turn file changes are restored by the existing checkpoint engine through a validated host-to-iframe message. Lattice does not copy checkpoint contents into `.research/history`. A checkpoint from an inactive task stays visible but cannot be restored until that task is open. See [`project-history-architecture.md`](./project-history-architecture.md) for the schema and migration decision. For a new build-relevant Agent checkpoint, automatic-build mode waits for the build pass containing those disk changes and relays `lattice:agent-compile-result` to the owning task and turn. The message contains only checkpoint identity, timing, success, project-relative root document, and aggregate error/warning counts. It never includes the build log, diagnostic text, paper content, or absolute workspace root. The diagnostics panel's **Fix all** action is a separate, user-initiated flow, not part of that telemetry relay. It saves open buffers, sends all current errors and warnings to one independent Synara task using **Settings → Models → Compile repair model**, and recompiles after the task completes. The task inherits the Agent panel's selected permission mode (including full access) for both creation and turn start; it never silently substitutes a different mode. The repair model can read and edit the project through its ordinary provider tools; the supplied diagnostics are untrusted context, not instructions. The task does not replace the active conversation and can be opened from **View repair** when approval is needed. Oversized batches are rejected explicitly rather than truncated (1000 diagnostics, 8 KiB per message, 1 MiB request body, and the provider's 120,000-character prompt limit). Lattice temporarily disables editor writes while the repair runs and rejects repair in read-only projects. Cancellation waits for a terminal provider state; switching projects cancels the outgoing repair and discards its late results. Each click runs one repair attempt, with no automatic build-fix loop; remaining diagnostics stay visible after recompilation. **Proofread** (the LaTeX selection toolbar, or ⌘⌥P) is a second user-initiated flow over the same relay. Rust's `task_relay_request` relays `agent_text_task` to `/api/lattice/text-task` with the same start, status and cancel shape as compile repair: `POST {workspaceRoot, prompt}` answers `202 {taskId}`, `GET …/` answers `{status: "running" | "completed" | "failed", text?, message?}` with `text` the provider's answer, and `POST …//cancel` stops the task. The fork answers on its auxiliary text-generation path with **Settings → Models → Proofreading model**, not as an Agent task: it creates no thread, and the provider is launched without tools, MCP servers or settings sources in an empty working directory, so it cannot edit a file or raise an approval. Lattice applies the result itself only after the writer accepts it. The Proofreading model starts as "Same as Git writing", so it follows **Git writing model** until the writer picks one of the same choices; an override whose provider is disabled also falls back to the Git writing model. Since `ac4900eaa`, the start answer and every status and cancel answer also carry `model: {provider, instanceId, slug, label, source}`, fixed when the task starts: `label` is the "Provider / Model" name Settings shows, and `source` is `"proofreading"` for an override or `"git-writing"` when inherited. The field is additive; a runtime pinned before it omits `model`, so a reader must treat it as optional. At most four text tasks run at once (429 beyond that), and an unknown task id is a JSON 404. Lattice owns the prompts and the reply contract (`src/agent/agent-proofread.ts`): **Proofread** (⌘⌥P) makes only the smallest corrections to grammar, spelling and punctuation, and **Polish**, chosen from the toolbar or the card each time, also improves flow and clarity; both keep meaning, hedging, terminology, the spelling variant and every LaTeX construct, and answer between `` markers, followed by one reason per change between `` markers (`original words => revised words | category | note`, the category from a fixed list and the note in the interface language). Lattice does not trust the reply to keep LaTeX intact: `src/agent/proofread-edits.ts` splits it into word-level edits, attaches each reason to the edit whose words it quotes (the card shows it as a tag), and holds back any edit that changes a protected span (`src/editor/latex/latex-protected.ts`: math, citation/reference/label commands and keys, `%` comments, command names, braces and options, environment boundaries, verbatim and paragraph breaks), and the card says so. A status answer may carry an optional `model` string naming the model that answered; the card shows it when present. The pinned route does not send one yet. A runtime without the route (one pinned before `fb759c3eb`) answers the router's bare 404. The relay reports that as `agent_route_unavailable`, and the card says the runtime cannot proofread yet. **Annotation revisions** (the Annotations panel's "Revise from N annotations", or ⌘⇧↩ in an annotation's composer) run over the same relay, one text task per open annotation and at most three at once, so one slot stays free for Proofread. Lattice owns the prompt and the reply contract (`src/agent/agent-annotations.ts`): the agent reads the writer's note, its intent, the annotated passage and the text around it, and answers with the revised passage between `` markers and its reason between `` markers, or with one `` when it needs the writer first. An "Add a citation" annotation also lists the bibliography's keys, and the agent may cite only those. As with Proofread, nothing is written by the agent: `src/canvas/annotation-review.tsx` splits the revision into edits with `proofread-edits.ts`, holds back changes to protected LaTeX (citations are allowed when the note asks for one), and the writer accepts or rejects each proposal in the editor. A conversation can answer an annotation too, through two host tools Lattice already handles as `synara:annotations-tool-request` / `lattice:annotations-tool-result` (`parseAgentAnnotationsToolRequest`): `propose_annotation_edit {annotation, edits: [{find, replace}], reason}` (each `find` occurs exactly once in the annotated passage) and `ask_about_annotation {annotation, question}`, where `annotation` is the pin number `read_editor_comments` now reports (`number`, `intent`, `status`, `question`, `pdfPage`). The pinned runtime does not declare these tools yet; until a Synara pin adds them beside `read_editor_comments` (a native Lattice tool relayed through the shared host-tools poll, in `LATTICE_NATIVE_TOOL_NAMES`), proposals come only from the panel's revision rounds. ## Offline quality evaluation Run `pnpm eval:agent` to replay the checked-in schema-version-1 research fixtures, or pass one or more JSON/NDJSON trace paths after `--`. The harness correlates records by task and turn and exits successfully only when every fixture's declared expected pass/fail outcome matches. It is entirely offline: it does not contact a provider or the literature network. Vitest exercises individual rules and malformed input in `scripts/agent-quality-eval.test.ts`, and grades every checked-in fixture there too, so `pnpm check` and CI fail when a fixture stops matching its outcome. Quality traces are content-minimized by default: record event types, status, hashes, paths, counts, and correlation identifiers rather than prompts, model output, paper text, or tool payload content. Synara writes the private, rotating NDJSON seam under its state log directory at `agent-quality/agent-quality.ndjson`. It identifies the stable provider-session policy/tool prefix separately from each turn's dynamic Lattice context manifest, so cache analysis does not require moving live editor or paper context into the stable prefix. Cache telemetry is provider-reported when available and otherwise explicitly unavailable; it must not be interpreted as a complete account of a provider's cache behavior. ## Build and release [`scripts/synara-runtime.json`](../scripts/synara-runtime.json) is the source of truth. It carries `repository` (the fork), `upstream`, `branch`, `revision` (the exact pinned commit), `sourceDirectory` (the default checkout location, which moves with the branch) and `nodeVersion`. Read values from it — every script and workflow does, and hardcoding any of them is how this document went stale before. - `pnpm prepare:synara:dev` accepts a dirty local Synara checkout for development. - `pnpm prepare:synara` is the release path. It refuses a dirty checkout or a revision that differs from the pin. - The preparation script builds the production web/server artifacts, downloads and verifies the official target-specific Node fallback, installs production dependencies except the Pi and OpenCode packages (see [Pi and OpenCode install on demand](#pi-and-opencode-install-on-demand)), and stages the result under `src-tauri/synara-runtime`. - It strips the Node binary's local symbols (`strip -x`, kept: global symbols and the export trie that native addons bind through) and prunes what the server never loads: source maps, declarations, published TypeScript sources, other platforms' prebuilds, Shiki's grammars and themes (the server only parses patches; the build fails if it starts using `@pierre/diffs` for more), the TypeScript compiler (only the native editor's language worker loads it, and the embed never starts that worker; the build fails if another server bundle imports it), and KaTeX's woff/ttf fallbacks behind a woff2 source. - Tauri's documented `TAURI_ENV_DEBUG` hook signal selects the package staging profile through `scripts/prepare-build.mjs`. - Synara and Open Slide run on that standalone Node in every build (`src-tauri/src/sidecar.rs`). - Claude sessions use the external `claude` executable selected in Provider settings (or found on the login-shell PATH). The SDK's otherwise bundled platform executable is replaced by a tiny PATH launcher so account probing follows the same installation without shipping a redundant copy. - Tauri includes that directory through `bundle.resources`. ### Runtime size is a budget, not a note Run `pnpm size:report` after preparing the runtimes and building the frontend to record exact file-byte totals for the web bundle, eager assets, Synara server and dependencies, presentation runtime, and provider executables. The staged runtimes are **enforced**, not merely observed. Both staging scripts prune through `scripts/lib/runtime-prune.mjs`: source maps, typings, tests, package docs, npm's `.bin` links, and binaries the target cannot load, never a file a `package.json` entry point names; Synara's also drops TypeScript sources, which Node will not load from `node_modules`. `scripts/app-size-report.mjs` applies an uncompressed budget to Open Slide, and the Release workflow runs its `--app` mode on the built bundle, which prints the installed size per folder and fails above the installed-size ceiling. It also fails if a staged Synara runtime lacks its standalone Node binary, or if the bundled Claude Agent SDK executable exceeds the 4 KiB PATH-launcher budget. Read the current constants from that file rather than trusting a number quoted here; the recorded totals belong in the release notes for the version that measured them. ### Pi and OpenCode install on demand The Pi (`@earendil-works/*`) and OpenCode (`@opencode/client`, `@opencode-ai/sdk`) packages are not in the bundle; with their dependency trees they were more than half of it. The fork loads them through `apps/server/src/provider/providerRuntimes.ts`: from the server's own `node_modules` when present (a development checkout or standalone Synara), otherwise from an install under the agent data directory (`/synara/provider-runtimes///`). Until a runtime is installed, its provider reports `runtimeInstall: {status: "missing"}` and Settings → Providers, the model picker and the chat's provider banner offer a one-click install (`server.installProviderRuntime`); Claude and Codex do not depend on it. The install downloads exactly the tarballs named by the fork's committed npm lockfile (`apps/server/providerRuntimes//package-lock.json`, whose root versions the preparation script checks against Synara's `bun.lock`) from the npm registry, rejects any whose bytes miss the lockfile's sha512 integrity, runs no package scripts, and keeps it only after a fresh Node process loads every module the server imports from it. An interrupted or rejected install leaves nothing behind and can be retried; an installed runtime needs no network. A new Synara pin that changes a lockfile asks for the install again. ## Syncing upstream Before switching source checkouts, inspect `git status` in the checkout currently used by Lattice, including an explicit `SYNARA_SOURCE_DIR` override. The pinned revision does not include uncommitted local patches or untracked regression tests. Inventory those changes and preserve any intended fixes in the fork's history before moving the pin; do not silently discard or assume they were included in the upstream merge. Keep the previous checkout available until the upgrade audit passes. 1. Fetch and merge the intended upstream release tag into **the branch named by `branch` in [`../scripts/synara-runtime.json`](../scripts/synara-runtime.json)**. The branch moves with each upstream version, so read it from the pin rather than from memory; older branches such as `codex/lattice-embed` still exist in the fork and are not what ships. Do not rewrite branch history. Tag a backup branch at the pre-merge commit first; the merge touches enough surface that a clean abort is worth having. 2. Prefer the host-profile seam for new work, and never reimplement an upstream tool handler. The fork modifies hundreds of upstream files, so expect real conflicts in `wsRpc.ts`, the contracts package, `GitCore.ts`, `providerMaintenance.ts`, and the chat components — merging is a review task, not a mechanical one. 3. Resolve conflicts hunk by hunk. Do not take one side wholesale for a whole file: our changes and upstream's frequently land in the same function, and conflict-block boundaries are often asymmetric, so a blanket `--theirs` can silently duplicate a function body. 4. Run `oxfmt --check`, then the workspace type checks, then the tests. Before changing the pin, run the Lattice browser regressions from the fork root (subject to the fork's `AGENTS.md` verification permissions): ```sh bun run --cwd apps/web test:browser -- src/components/ChatMarkdown.latticeIntegration.browser.tsx src/components/ChatMarkdown.tableLayout.browser.tsx src/components/chat/ComposerLatticeContextBar.browser.tsx src/components/chat/MessageTrail.browser.tsx ``` Then run the server regressions for the embedded panel. The first two check that a pre-warmed new chat's first turn still lists the `lattice` tools; the third checks Claude's harness policy: ```sh bun run --cwd apps/server test -- src/agentGateway/mcpTransport.test.ts src/orchestration/Layers/ProviderCommandReactor.test.ts src/provider/Layers/ClaudeAdapter.test.ts -t "pre-warmed draft|panel prewarm|Claude Synara harness policy" ``` The fork's `ChatView.browser.tsx` no longer has its `embedded sidebar` checks, so verify the narrow-panel behavior in the app instead. It must cover widening and shrinking the same mounted view, loaded and loading model controls, responsive history popups, and attachment minimums. Verify minimum-width reports against rendered geometry; unused toolbar space must remain compressible, without clipping the send control. A closed history menu must not reserve its preferred width, and Lattice must accept measured widths below its bootstrap minimum. Keep these regressions with the fork changes through merges; successful compilation alone does not verify Lattice's embedding behavior. 5. Start the staged server while another process owns port 3773; confirm it selects another loopback port and reports `startupReady`. 6. Commit the intended fork changes and update the exact revision in the local `scripts/synara-runtime.json`. Record what the new pin brings, and any change to how the embedded panels behave, in [`synara-fork-ledger.md`](synara-fork-ledger.md), and re-measure the fork size there. Before publishing it, run `node scripts/check-synara-upgrade.mjs `, using the Lattice commit or release tag from before the upgrade. The audit rejects dirty or untracked files in either checkout, a pin/HEAD mismatch, and missing ancestry from both the previous pin and the previous checkout's local HEAD. It requires full fork history and local access to the old checkout; it deliberately does not fetch, commit, discard, or publish anything. `SYNARA_SOURCE_DIR` overrides the new source location; `SYNARA_PREVIOUS_SOURCE_DIR` overrides the previous checkout's location when it differs from the old pin's `sourceDirectory`. Point the latter at the checkout previously used by Lattice, including any committed-but-unpinned fixes; both checkouts still must be clean and their history is checked. This is a local pre-publication check, not a replacement for browser regressions or a CI check that can see uncommitted files on another machine. CI's `synara-pin` job runs the published half on every pull request that moves the pin: `node scripts/check-synara-pin.mjs ` fetches the fork's commits and fails unless the pin is on its declared `branch` and descends from the base's pin. Push the verified fork commit before publishing the Lattice pin. 7. Run `pnpm prepare:synara` and the Lattice frontend/Rust checks before producing an installer. ### Formatting must match upstream The fork carries a `root = true` `.editorconfig`. Do not delete it. oxfmt reads EditorConfig's `max_line_length` as its print width and walks *up* past the repository root, so a user-level `~/.editorconfig` silently reformats the whole tree to a different width than upstream. That is how the fork accumulated wide single-line reformatting that turned routine upstream syncs into a wall of spurious formatting conflicts. Upstream's tree is oxfmt-clean at the default width of 100; ours must stay that way. ### Running the fork's own test suite Everything in this subsection is about **the Synara fork checkout, not this repository.** None of these files exist under `research-writer/`; they are in the tree `SYNARA_SOURCE_DIR` points at, and they only matter while you are performing a merge. **Take your own baseline before you merge.** Run the suite on the pre-merge commit and keep the output. Some tests fail on the fork independently of any merge — historically React Compiler bailout assertions in `apps/web/src/components/chatHotPath.compiler.test.ts` and `ChatMarkdown.compiler.test.ts`, and `apps/server/src/git/Layers/CursorTextGeneration.test.ts`, which needs a real Cursor ACP connection — but the exact set drifts with upstream, so a count written down here would be worse than useless. A pre-merge run is the only trustworthy baseline. Two environment traps that are stable enough to record: - **Use the web workspace's test script: `bun run --cwd apps/web test`.** Its `vitest.config.ts` loads both the Vite transforms and `vitest.setup.ts`, which installs in-memory `localStorage`. Running `vitest apps/web/src` with the root config bypasses them, so `~/` imports fail before the tests execute. Node 24+ exposes an experimental `localStorage` getter that resolves to `undefined` without `--localstorage-file`; keep the workspace setup so Zustand does not capture that missing value at import time. - **Run the server and web suites sequentially.** `apps/server/src/provider/acp/AcpSdkConformance.test.ts` has a 90-second timeout that CPU contention will trip. Generated runtime contents are intentionally ignored by Git. Only the pin, preparation script, supervisor, integration UI, and documentation belong in the Lattice repository.